diff options
author | ArthurHoaro <arthur@hoa.ro> | 2020-11-12 13:11:07 +0100 |
---|---|---|
committer | ArthurHoaro <arthur@hoa.ro> | 2020-11-12 13:11:07 +0100 |
commit | af50eba28a7bd286de4c8c9ee6dc5216b915d149 (patch) | |
tree | ffa30a9358e82d27be75d8fc5e57f3c8820dc6d3 /application/bookmark/BookmarkFileService.php | |
parent | b6f678a5a1d15acf284ebcec16c905e976671ce1 (diff) | |
parent | 1409f1c89a7ca01456ae2dcd6357d296e2b99f5a (diff) | |
download | Shaarli-af50eba28a7bd286de4c8c9ee6dc5216b915d149.tar.gz Shaarli-af50eba28a7bd286de4c8c9ee6dc5216b915d149.tar.zst Shaarli-af50eba28a7bd286de4c8c9ee6dc5216b915d149.zip |
Merge tag 'v0.12.1' into latestlatest
v0.12.1
Diffstat (limited to 'application/bookmark/BookmarkFileService.php')
-rw-r--r-- | application/bookmark/BookmarkFileService.php | 134 |
1 files changed, 76 insertions, 58 deletions
diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index c9ec2609..6666a251 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php | |||
@@ -1,10 +1,12 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | declare(strict_types=1); | ||
3 | 4 | ||
4 | namespace Shaarli\Bookmark; | 5 | namespace Shaarli\Bookmark; |
5 | 6 | ||
6 | 7 | use DateTime; | |
7 | use Exception; | 8 | use Exception; |
9 | use malkusch\lock\mutex\Mutex; | ||
8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | 10 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; |
9 | use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; | 11 | use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; |
10 | use Shaarli\Bookmark\Exception\EmptyDataStoreException; | 12 | use Shaarli\Bookmark\Exception\EmptyDataStoreException; |
@@ -47,15 +49,19 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
47 | /** @var bool true for logged in users. Default value to retrieve private bookmarks. */ | 49 | /** @var bool true for logged in users. Default value to retrieve private bookmarks. */ |
48 | protected $isLoggedIn; | 50 | protected $isLoggedIn; |
49 | 51 | ||
52 | /** @var Mutex */ | ||
53 | protected $mutex; | ||
54 | |||
50 | /** | 55 | /** |
51 | * @inheritDoc | 56 | * @inheritDoc |
52 | */ | 57 | */ |
53 | public function __construct(ConfigManager $conf, History $history, $isLoggedIn) | 58 | public function __construct(ConfigManager $conf, History $history, Mutex $mutex, bool $isLoggedIn) |
54 | { | 59 | { |
55 | $this->conf = $conf; | 60 | $this->conf = $conf; |
56 | $this->history = $history; | 61 | $this->history = $history; |
62 | $this->mutex = $mutex; | ||
57 | $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn); | 63 | $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn); |
58 | $this->bookmarksIO = new BookmarkIO($this->conf); | 64 | $this->bookmarksIO = new BookmarkIO($this->conf, $this->mutex); |
59 | $this->isLoggedIn = $isLoggedIn; | 65 | $this->isLoggedIn = $isLoggedIn; |
60 | 66 | ||
61 | if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) { | 67 | if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) { |
@@ -63,7 +69,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
63 | } else { | 69 | } else { |
64 | try { | 70 | try { |
65 | $this->bookmarks = $this->bookmarksIO->read(); | 71 | $this->bookmarks = $this->bookmarksIO->read(); |
66 | } catch (EmptyDataStoreException|DatastoreNotInitializedException $e) { | 72 | } catch (EmptyDataStoreException | DatastoreNotInitializedException $e) { |
67 | $this->bookmarks = new BookmarkArray(); | 73 | $this->bookmarks = new BookmarkArray(); |
68 | 74 | ||
69 | if ($this->isLoggedIn) { | 75 | if ($this->isLoggedIn) { |
@@ -79,25 +85,29 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
79 | if (! $this->bookmarks instanceof BookmarkArray) { | 85 | if (! $this->bookmarks instanceof BookmarkArray) { |
80 | $this->migrate(); | 86 | $this->migrate(); |
81 | exit( | 87 | exit( |
82 | 'Your data store has been migrated, please reload the page.'. PHP_EOL . | 88 | 'Your data store has been migrated, please reload the page.' . PHP_EOL . |
83 | 'If this message keeps showing up, please delete data/updates.txt file.' | 89 | 'If this message keeps showing up, please delete data/updates.txt file.' |
84 | ); | 90 | ); |
85 | } | 91 | } |
86 | } | 92 | } |
87 | 93 | ||
88 | $this->bookmarkFilter = new BookmarkFilter($this->bookmarks); | 94 | $this->bookmarkFilter = new BookmarkFilter($this->bookmarks, $this->conf); |
89 | } | 95 | } |
90 | 96 | ||
91 | /** | 97 | /** |
92 | * @inheritDoc | 98 | * @inheritDoc |
93 | */ | 99 | */ |
94 | public function findByHash($hash) | 100 | public function findByHash(string $hash, string $privateKey = null): Bookmark |
95 | { | 101 | { |
96 | $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash); | 102 | $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash); |
97 | // PHP 7.3 introduced array_key_first() to avoid this hack | 103 | // PHP 7.3 introduced array_key_first() to avoid this hack |
98 | $first = reset($bookmark); | 104 | $first = reset($bookmark); |
99 | if (! $this->isLoggedIn && $first->isPrivate()) { | 105 | if ( |
100 | throw new Exception('Not authorized'); | 106 | !$this->isLoggedIn |
107 | && $first->isPrivate() | ||
108 | && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key')) | ||
109 | ) { | ||
110 | throw new BookmarkNotFoundException(); | ||
101 | } | 111 | } |
102 | 112 | ||
103 | return $first; | 113 | return $first; |
@@ -106,7 +116,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
106 | /** | 116 | /** |
107 | * @inheritDoc | 117 | * @inheritDoc |
108 | */ | 118 | */ |
109 | public function findByUrl($url) | 119 | public function findByUrl(string $url): ?Bookmark |
110 | { | 120 | { |
111 | return $this->bookmarks->getByUrl($url); | 121 | return $this->bookmarks->getByUrl($url); |
112 | } | 122 | } |
@@ -115,10 +125,10 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
115 | * @inheritDoc | 125 | * @inheritDoc |
116 | */ | 126 | */ |
117 | public function search( | 127 | public function search( |
118 | $request = [], | 128 | array $request = [], |
119 | $visibility = null, | 129 | string $visibility = null, |
120 | $caseSensitive = false, | 130 | bool $caseSensitive = false, |
121 | $untaggedOnly = false, | 131 | bool $untaggedOnly = false, |
122 | bool $ignoreSticky = false | 132 | bool $ignoreSticky = false |
123 | ) { | 133 | ) { |
124 | if ($visibility === null) { | 134 | if ($visibility === null) { |
@@ -126,8 +136,8 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
126 | } | 136 | } |
127 | 137 | ||
128 | // Filter bookmark database according to parameters. | 138 | // Filter bookmark database according to parameters. |
129 | $searchtags = isset($request['searchtags']) ? $request['searchtags'] : ''; | 139 | $searchTags = isset($request['searchtags']) ? $request['searchtags'] : ''; |
130 | $searchterm = isset($request['searchterm']) ? $request['searchterm'] : ''; | 140 | $searchTerm = isset($request['searchterm']) ? $request['searchterm'] : ''; |
131 | 141 | ||
132 | if ($ignoreSticky) { | 142 | if ($ignoreSticky) { |
133 | $this->bookmarks->reorder('DESC', true); | 143 | $this->bookmarks->reorder('DESC', true); |
@@ -135,7 +145,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
135 | 145 | ||
136 | return $this->bookmarkFilter->filter( | 146 | return $this->bookmarkFilter->filter( |
137 | BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT, | 147 | BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT, |
138 | [$searchtags, $searchterm], | 148 | [$searchTags, $searchTerm], |
139 | $caseSensitive, | 149 | $caseSensitive, |
140 | $visibility, | 150 | $visibility, |
141 | $untaggedOnly | 151 | $untaggedOnly |
@@ -145,7 +155,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
145 | /** | 155 | /** |
146 | * @inheritDoc | 156 | * @inheritDoc |
147 | */ | 157 | */ |
148 | public function get($id, $visibility = null) | 158 | public function get(int $id, string $visibility = null): Bookmark |
149 | { | 159 | { |
150 | if (! isset($this->bookmarks[$id])) { | 160 | if (! isset($this->bookmarks[$id])) { |
151 | throw new BookmarkNotFoundException(); | 161 | throw new BookmarkNotFoundException(); |
@@ -156,7 +166,8 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
156 | } | 166 | } |
157 | 167 | ||
158 | $bookmark = $this->bookmarks[$id]; | 168 | $bookmark = $this->bookmarks[$id]; |
159 | if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') | 169 | if ( |
170 | ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') | ||
160 | || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') | 171 | || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') |
161 | ) { | 172 | ) { |
162 | throw new Exception('Unauthorized'); | 173 | throw new Exception('Unauthorized'); |
@@ -168,20 +179,17 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
168 | /** | 179 | /** |
169 | * @inheritDoc | 180 | * @inheritDoc |
170 | */ | 181 | */ |
171 | public function set($bookmark, $save = true) | 182 | public function set(Bookmark $bookmark, bool $save = true): Bookmark |
172 | { | 183 | { |
173 | if (true !== $this->isLoggedIn) { | 184 | if (true !== $this->isLoggedIn) { |
174 | throw new Exception(t('You\'re not authorized to alter the datastore')); | 185 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
175 | } | 186 | } |
176 | if (! $bookmark instanceof Bookmark) { | ||
177 | throw new Exception(t('Provided data is invalid')); | ||
178 | } | ||
179 | if (! isset($this->bookmarks[$bookmark->getId()])) { | 187 | if (! isset($this->bookmarks[$bookmark->getId()])) { |
180 | throw new BookmarkNotFoundException(); | 188 | throw new BookmarkNotFoundException(); |
181 | } | 189 | } |
182 | $bookmark->validate(); | 190 | $bookmark->validate(); |
183 | 191 | ||
184 | $bookmark->setUpdated(new \DateTime()); | 192 | $bookmark->setUpdated(new DateTime()); |
185 | $this->bookmarks[$bookmark->getId()] = $bookmark; | 193 | $this->bookmarks[$bookmark->getId()] = $bookmark; |
186 | if ($save === true) { | 194 | if ($save === true) { |
187 | $this->save(); | 195 | $this->save(); |
@@ -193,15 +201,12 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
193 | /** | 201 | /** |
194 | * @inheritDoc | 202 | * @inheritDoc |
195 | */ | 203 | */ |
196 | public function add($bookmark, $save = true) | 204 | public function add(Bookmark $bookmark, bool $save = true): Bookmark |
197 | { | 205 | { |
198 | if (true !== $this->isLoggedIn) { | 206 | if (true !== $this->isLoggedIn) { |
199 | throw new Exception(t('You\'re not authorized to alter the datastore')); | 207 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
200 | } | 208 | } |
201 | if (! $bookmark instanceof Bookmark) { | 209 | if (!empty($bookmark->getId())) { |
202 | throw new Exception(t('Provided data is invalid')); | ||
203 | } | ||
204 | if (! empty($bookmark->getId())) { | ||
205 | throw new Exception(t('This bookmarks already exists')); | 210 | throw new Exception(t('This bookmarks already exists')); |
206 | } | 211 | } |
207 | $bookmark->setId($this->bookmarks->getNextId()); | 212 | $bookmark->setId($this->bookmarks->getNextId()); |
@@ -218,14 +223,11 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
218 | /** | 223 | /** |
219 | * @inheritDoc | 224 | * @inheritDoc |
220 | */ | 225 | */ |
221 | public function addOrSet($bookmark, $save = true) | 226 | public function addOrSet(Bookmark $bookmark, bool $save = true): Bookmark |
222 | { | 227 | { |
223 | if (true !== $this->isLoggedIn) { | 228 | if (true !== $this->isLoggedIn) { |
224 | throw new Exception(t('You\'re not authorized to alter the datastore')); | 229 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
225 | } | 230 | } |
226 | if (! $bookmark instanceof Bookmark) { | ||
227 | throw new Exception('Provided data is invalid'); | ||
228 | } | ||
229 | if ($bookmark->getId() === null) { | 231 | if ($bookmark->getId() === null) { |
230 | return $this->add($bookmark, $save); | 232 | return $this->add($bookmark, $save); |
231 | } | 233 | } |
@@ -235,14 +237,11 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
235 | /** | 237 | /** |
236 | * @inheritDoc | 238 | * @inheritDoc |
237 | */ | 239 | */ |
238 | public function remove($bookmark, $save = true) | 240 | public function remove(Bookmark $bookmark, bool $save = true): void |
239 | { | 241 | { |
240 | if (true !== $this->isLoggedIn) { | 242 | if (true !== $this->isLoggedIn) { |
241 | throw new Exception(t('You\'re not authorized to alter the datastore')); | 243 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
242 | } | 244 | } |
243 | if (! $bookmark instanceof Bookmark) { | ||
244 | throw new Exception(t('Provided data is invalid')); | ||
245 | } | ||
246 | if (! isset($this->bookmarks[$bookmark->getId()])) { | 245 | if (! isset($this->bookmarks[$bookmark->getId()])) { |
247 | throw new BookmarkNotFoundException(); | 246 | throw new BookmarkNotFoundException(); |
248 | } | 247 | } |
@@ -257,7 +256,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
257 | /** | 256 | /** |
258 | * @inheritDoc | 257 | * @inheritDoc |
259 | */ | 258 | */ |
260 | public function exists($id, $visibility = null) | 259 | public function exists(int $id, string $visibility = null): bool |
261 | { | 260 | { |
262 | if (! isset($this->bookmarks[$id])) { | 261 | if (! isset($this->bookmarks[$id])) { |
263 | return false; | 262 | return false; |
@@ -268,7 +267,8 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
268 | } | 267 | } |
269 | 268 | ||
270 | $bookmark = $this->bookmarks[$id]; | 269 | $bookmark = $this->bookmarks[$id]; |
271 | if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') | 270 | if ( |
271 | ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') | ||
272 | || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') | 272 | || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') |
273 | ) { | 273 | ) { |
274 | return false; | 274 | return false; |
@@ -280,7 +280,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
280 | /** | 280 | /** |
281 | * @inheritDoc | 281 | * @inheritDoc |
282 | */ | 282 | */ |
283 | public function count($visibility = null) | 283 | public function count(string $visibility = null): int |
284 | { | 284 | { |
285 | return count($this->search([], $visibility)); | 285 | return count($this->search([], $visibility)); |
286 | } | 286 | } |
@@ -288,7 +288,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
288 | /** | 288 | /** |
289 | * @inheritDoc | 289 | * @inheritDoc |
290 | */ | 290 | */ |
291 | public function save() | 291 | public function save(): void |
292 | { | 292 | { |
293 | if (true !== $this->isLoggedIn) { | 293 | if (true !== $this->isLoggedIn) { |
294 | // TODO: raise an Exception instead | 294 | // TODO: raise an Exception instead |
@@ -303,14 +303,15 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
303 | /** | 303 | /** |
304 | * @inheritDoc | 304 | * @inheritDoc |
305 | */ | 305 | */ |
306 | public function bookmarksCountPerTag($filteringTags = [], $visibility = null) | 306 | public function bookmarksCountPerTag(array $filteringTags = [], string $visibility = null): array |
307 | { | 307 | { |
308 | $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility); | 308 | $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility); |
309 | $tags = []; | 309 | $tags = []; |
310 | $caseMapping = []; | 310 | $caseMapping = []; |
311 | foreach ($bookmarks as $bookmark) { | 311 | foreach ($bookmarks as $bookmark) { |
312 | foreach ($bookmark->getTags() as $tag) { | 312 | foreach ($bookmark->getTags() as $tag) { |
313 | if (empty($tag) | 313 | if ( |
314 | empty($tag) | ||
314 | || (! $this->isLoggedIn && startsWith($tag, '.')) | 315 | || (! $this->isLoggedIn && startsWith($tag, '.')) |
315 | || $tag === BookmarkMarkdownFormatter::NO_MD_TAG | 316 | || $tag === BookmarkMarkdownFormatter::NO_MD_TAG |
316 | || in_array($tag, $filteringTags, true) | 317 | || in_array($tag, $filteringTags, true) |
@@ -339,38 +340,55 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
339 | $keys = array_keys($tags); | 340 | $keys = array_keys($tags); |
340 | $tmpTags = array_combine($keys, $keys); | 341 | $tmpTags = array_combine($keys, $keys); |
341 | array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags); | 342 | array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags); |
343 | |||
342 | return $tags; | 344 | return $tags; |
343 | } | 345 | } |
344 | 346 | ||
345 | /** | 347 | /** |
346 | * @inheritDoc | 348 | * @inheritDoc |
347 | */ | 349 | */ |
348 | public function days() | 350 | public function findByDate( |
349 | { | 351 | \DateTimeInterface $from, |
350 | $bookmarkDays = []; | 352 | \DateTimeInterface $to, |
351 | foreach ($this->search() as $bookmark) { | 353 | ?\DateTimeInterface &$previous, |
352 | $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0; | 354 | ?\DateTimeInterface &$next |
355 | ): array { | ||
356 | $out = []; | ||
357 | $previous = null; | ||
358 | $next = null; | ||
359 | |||
360 | foreach ($this->search([], null, false, false, true) as $bookmark) { | ||
361 | if ($to < $bookmark->getCreated()) { | ||
362 | $next = $bookmark->getCreated(); | ||
363 | } elseif ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) { | ||
364 | $out[] = $bookmark; | ||
365 | } else { | ||
366 | if ($previous !== null) { | ||
367 | break; | ||
368 | } | ||
369 | $previous = $bookmark->getCreated(); | ||
370 | } | ||
353 | } | 371 | } |
354 | $bookmarkDays = array_keys($bookmarkDays); | ||
355 | sort($bookmarkDays); | ||
356 | 372 | ||
357 | return $bookmarkDays; | 373 | return $out; |
358 | } | 374 | } |
359 | 375 | ||
360 | /** | 376 | /** |
361 | * @inheritDoc | 377 | * @inheritDoc |
362 | */ | 378 | */ |
363 | public function filterDay($request) | 379 | public function getLatest(): ?Bookmark |
364 | { | 380 | { |
365 | $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; | 381 | foreach ($this->search([], null, false, false, true) as $bookmark) { |
382 | return $bookmark; | ||
383 | } | ||
366 | 384 | ||
367 | return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request, false, $visibility); | 385 | return null; |
368 | } | 386 | } |
369 | 387 | ||
370 | /** | 388 | /** |
371 | * @inheritDoc | 389 | * @inheritDoc |
372 | */ | 390 | */ |
373 | public function initialize() | 391 | public function initialize(): void |
374 | { | 392 | { |
375 | $initializer = new BookmarkInitializer($this); | 393 | $initializer = new BookmarkInitializer($this); |
376 | $initializer->initialize(); | 394 | $initializer->initialize(); |
@@ -383,7 +401,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
383 | /** | 401 | /** |
384 | * Handles migration to the new database format (BookmarksArray). | 402 | * Handles migration to the new database format (BookmarksArray). |
385 | */ | 403 | */ |
386 | protected function migrate() | 404 | protected function migrate(): void |
387 | { | 405 | { |
388 | $bookmarkDb = new LegacyLinkDB( | 406 | $bookmarkDb = new LegacyLinkDB( |
389 | $this->conf->get('resource.datastore'), | 407 | $this->conf->get('resource.datastore'), |
@@ -391,14 +409,14 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
391 | false | 409 | false |
392 | ); | 410 | ); |
393 | $updater = new LegacyUpdater( | 411 | $updater = new LegacyUpdater( |
394 | UpdaterUtils::read_updates_file($this->conf->get('resource.updates')), | 412 | UpdaterUtils::readUpdatesFile($this->conf->get('resource.updates')), |
395 | $bookmarkDb, | 413 | $bookmarkDb, |
396 | $this->conf, | 414 | $this->conf, |
397 | true | 415 | true |
398 | ); | 416 | ); |
399 | $newUpdates = $updater->update(); | 417 | $newUpdates = $updater->update(); |
400 | if (! empty($newUpdates)) { | 418 | if (! empty($newUpdates)) { |
401 | UpdaterUtils::write_updates_file( | 419 | UpdaterUtils::writeUpdatesFile( |
402 | $this->conf->get('resource.updates'), | 420 | $this->conf->get('resource.updates'), |
403 | $updater->getDoneUpdates() | 421 | $updater->getDoneUpdates() |
404 | ); | 422 | ); |