1 | <?php |
||
2 | /** |
||
3 | * @copyright Copyright (c) 2017 Robin Appelman <[email protected]> |
||
4 | * |
||
5 | * @author Christian <[email protected]> |
||
6 | * @author Christoph Wurst <[email protected]> |
||
7 | * @author Robin Appelman <[email protected]> |
||
8 | * @author Roeland Jago Douma <[email protected]> |
||
9 | * |
||
10 | * @license GNU AGPL version 3 or any later version |
||
11 | * |
||
12 | * This program is free software: you can redistribute it and/or modify |
||
13 | * it under the terms of the GNU Affero General Public License as |
||
14 | * published by the Free Software Foundation, either version 3 of the |
||
15 | * License, or (at your option) any later version. |
||
16 | * |
||
17 | * This program is distributed in the hope that it will be useful, |
||
18 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
19 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
20 | * GNU Affero General Public License for more details. |
||
21 | * |
||
22 | * You should have received a copy of the GNU Affero General Public License |
||
23 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
||
24 | * |
||
25 | */ |
||
26 | namespace OCA\DAV\Files; |
||
27 | |||
28 | use OC\Files\Search\SearchBinaryOperator; |
||
29 | use OC\Files\Search\SearchComparison; |
||
30 | use OC\Files\Search\SearchOrder; |
||
31 | use OC\Files\Search\SearchQuery; |
||
32 | use OC\Files\View; |
||
33 | use OC\Metadata\IMetadataManager; |
||
34 | use OCA\DAV\Connector\Sabre\CachingTree; |
||
35 | use OCA\DAV\Connector\Sabre\Directory; |
||
36 | use OCA\DAV\Connector\Sabre\FilesPlugin; |
||
37 | use OCA\DAV\Connector\Sabre\TagsPlugin; |
||
38 | use OCP\Files\Cache\ICacheEntry; |
||
39 | use OCP\Files\Folder; |
||
40 | use OCP\Files\IRootFolder; |
||
41 | use OCP\Files\Node; |
||
42 | use OCP\Files\Search\ISearchOperator; |
||
43 | use OCP\Files\Search\ISearchOrder; |
||
44 | use OCP\Files\Search\ISearchQuery; |
||
45 | use OCP\IUser; |
||
46 | use OCP\Share\IManager; |
||
47 | use Sabre\DAV\Exception\NotFound; |
||
48 | use Sabre\DAV\INode; |
||
49 | use SearchDAV\Backend\ISearchBackend; |
||
50 | use SearchDAV\Backend\SearchPropertyDefinition; |
||
51 | use SearchDAV\Backend\SearchResult; |
||
52 | use SearchDAV\Query\Literal; |
||
53 | use SearchDAV\Query\Operator; |
||
54 | use SearchDAV\Query\Order; |
||
55 | use SearchDAV\Query\Query; |
||
56 | |||
57 | class FileSearchBackend implements ISearchBackend { |
||
58 | public const OPERATOR_LIMIT = 100; |
||
59 | |||
60 | /** @var CachingTree */ |
||
61 | private $tree; |
||
62 | |||
63 | /** @var IUser */ |
||
64 | private $user; |
||
65 | |||
66 | /** @var IRootFolder */ |
||
67 | private $rootFolder; |
||
68 | |||
69 | /** @var IManager */ |
||
70 | private $shareManager; |
||
71 | |||
72 | /** @var View */ |
||
73 | private $view; |
||
74 | |||
75 | /** |
||
76 | * FileSearchBackend constructor. |
||
77 | * |
||
78 | * @param CachingTree $tree |
||
79 | * @param IUser $user |
||
80 | * @param IRootFolder $rootFolder |
||
81 | * @param IManager $shareManager |
||
82 | * @param View $view |
||
83 | * @internal param IRootFolder $rootFolder |
||
84 | */ |
||
85 | public function __construct(CachingTree $tree, IUser $user, IRootFolder $rootFolder, IManager $shareManager, View $view) { |
||
86 | $this->tree = $tree; |
||
87 | $this->user = $user; |
||
88 | $this->rootFolder = $rootFolder; |
||
89 | $this->shareManager = $shareManager; |
||
90 | $this->view = $view; |
||
91 | } |
||
92 | |||
93 | /** |
||
94 | * Search endpoint will be remote.php/dav |
||
95 | */ |
||
96 | public function getArbiterPath(): string { |
||
97 | return ''; |
||
98 | } |
||
99 | |||
100 | public function isValidScope(string $href, $depth, ?string $path): bool { |
||
101 | // only allow scopes inside the dav server |
||
102 | if (is_null($path)) { |
||
103 | return false; |
||
104 | } |
||
105 | |||
106 | try { |
||
107 | $node = $this->tree->getNodeForPath($path); |
||
108 | return $node instanceof Directory; |
||
109 | } catch (NotFound $e) { |
||
110 | return false; |
||
111 | } |
||
112 | } |
||
113 | |||
114 | public function getPropertyDefinitionsForScope(string $href, ?string $path): array { |
||
115 | // all valid scopes support the same schema |
||
116 | |||
117 | //todo dynamically load all propfind properties that are supported |
||
118 | return [ |
||
119 | // queryable properties |
||
120 | new SearchPropertyDefinition('{DAV:}displayname', true, true, true), |
||
121 | new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), |
||
122 | new SearchPropertyDefinition('{DAV:}getlastmodified', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME), |
||
123 | new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), |
||
124 | new SearchPropertyDefinition(TagsPlugin::FAVORITE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_BOOLEAN), |
||
125 | new SearchPropertyDefinition(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME, true, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), |
||
126 | new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, true, true, false), |
||
127 | |||
128 | // select only properties |
||
129 | new SearchPropertyDefinition('{DAV:}resourcetype', true, false, false), |
||
130 | new SearchPropertyDefinition('{DAV:}getcontentlength', true, false, false), |
||
131 | new SearchPropertyDefinition(FilesPlugin::CHECKSUMS_PROPERTYNAME, true, false, false), |
||
132 | new SearchPropertyDefinition(FilesPlugin::PERMISSIONS_PROPERTYNAME, true, false, false), |
||
133 | new SearchPropertyDefinition(FilesPlugin::GETETAG_PROPERTYNAME, true, false, false), |
||
134 | new SearchPropertyDefinition(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, true, false, false), |
||
135 | new SearchPropertyDefinition(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, true, false, false), |
||
136 | new SearchPropertyDefinition(FilesPlugin::HAS_PREVIEW_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_BOOLEAN), |
||
137 | new SearchPropertyDefinition(FilesPlugin::FILE_METADATA_SIZE, true, false, false, SearchPropertyDefinition::DATATYPE_STRING), |
||
138 | new SearchPropertyDefinition(FilesPlugin::FILEID_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), |
||
139 | ]; |
||
140 | } |
||
141 | |||
142 | /** |
||
143 | * @param INode[] $nodes |
||
144 | * @param string[] $requestProperties |
||
145 | */ |
||
146 | public function preloadPropertyFor(array $nodes, array $requestProperties): void { |
||
147 | if (in_array(FilesPlugin::FILE_METADATA_SIZE, $requestProperties, true)) { |
||
148 | // Preloading of the metadata |
||
149 | $fileIds = []; |
||
150 | foreach ($nodes as $node) { |
||
151 | /** @var \OCP\Files\Node|\OCA\DAV\Connector\Sabre\Node $node */ |
||
152 | if (str_starts_with($node->getFileInfo()->getMimeType(), 'image/')) { |
||
153 | /** @var \OCA\DAV\Connector\Sabre\File $node */ |
||
154 | $fileIds[] = $node->getFileInfo()->getId(); |
||
155 | } |
||
156 | } |
||
157 | /** @var IMetaDataManager $metadataManager */ |
||
158 | $metadataManager = \OC::$server->get(IMetadataManager::class); |
||
159 | $preloadedMetadata = $metadataManager->fetchMetadataFor('size', $fileIds); |
||
160 | foreach ($nodes as $node) { |
||
161 | /** @var \OCP\Files\Node|\OCA\DAV\Connector\Sabre\Node $node */ |
||
162 | if (str_starts_with($node->getFileInfo()->getMimeType(), 'image/')) { |
||
163 | /** @var \OCA\DAV\Connector\Sabre\File $node */ |
||
164 | $node->setMetadata('size', $preloadedMetadata[$node->getFileInfo()->getId()]); |
||
165 | } |
||
166 | } |
||
167 | } |
||
168 | } |
||
169 | |||
170 | /** |
||
171 | * @param Query $search |
||
172 | * @return SearchResult[] |
||
173 | */ |
||
174 | public function search(Query $search): array { |
||
175 | if (count($search->from) !== 1) { |
||
176 | throw new \InvalidArgumentException('Searching more than one folder is not supported'); |
||
177 | } |
||
178 | $query = $this->transformQuery($search); |
||
179 | $scope = $search->from[0]; |
||
180 | if ($scope->path === null) { |
||
181 | throw new \InvalidArgumentException('Using uri\'s as scope is not supported, please use a path relative to the search arbiter instead'); |
||
182 | } |
||
183 | $node = $this->tree->getNodeForPath($scope->path); |
||
184 | if (!$node instanceof Directory) { |
||
185 | throw new \InvalidArgumentException('Search is only supported on directories'); |
||
186 | } |
||
187 | |||
188 | $fileInfo = $node->getFileInfo(); |
||
189 | $folder = $this->rootFolder->get($fileInfo->getPath()); |
||
190 | /** @var Folder $folder $results */ |
||
191 | $results = $folder->search($query); |
||
192 | |||
193 | /** @var SearchResult[] $nodes */ |
||
194 | $nodes = array_map(function (Node $node) { |
||
195 | if ($node instanceof Folder) { |
||
196 | $davNode = new \OCA\DAV\Connector\Sabre\Directory($this->view, $node, $this->tree, $this->shareManager); |
||
197 | } else { |
||
198 | $davNode = new \OCA\DAV\Connector\Sabre\File($this->view, $node, $this->shareManager); |
||
199 | } |
||
200 | $path = $this->getHrefForNode($node); |
||
201 | $this->tree->cacheNode($davNode, $path); |
||
202 | return new SearchResult($davNode, $path); |
||
203 | }, $results); |
||
204 | |||
205 | if (!$query->limitToHome()) { |
||
206 | // Sort again, since the result from multiple storages is appended and not sorted |
||
207 | usort($nodes, function (SearchResult $a, SearchResult $b) use ($search) { |
||
208 | return $this->sort($a, $b, $search->orderBy); |
||
209 | }); |
||
210 | } |
||
211 | |||
212 | // If a limit is provided use only return that number of files |
||
213 | if ($search->limit->maxResults !== 0) { |
||
214 | $nodes = \array_slice($nodes, 0, $search->limit->maxResults); |
||
215 | } |
||
216 | |||
217 | return $nodes; |
||
218 | } |
||
219 | |||
220 | private function sort(SearchResult $a, SearchResult $b, array $orders) { |
||
221 | /** @var Order $order */ |
||
222 | foreach ($orders as $order) { |
||
223 | $v1 = $this->getSearchResultProperty($a, $order->property); |
||
224 | $v2 = $this->getSearchResultProperty($b, $order->property); |
||
225 | |||
226 | |||
227 | if ($v1 === null && $v2 === null) { |
||
228 | continue; |
||
229 | } |
||
230 | if ($v1 === null) { |
||
231 | return $order->order === Order::ASC ? 1 : -1; |
||
232 | } |
||
233 | if ($v2 === null) { |
||
234 | return $order->order === Order::ASC ? -1 : 1; |
||
235 | } |
||
236 | |||
237 | $s = $this->compareProperties($v1, $v2, $order); |
||
238 | if ($s === 0) { |
||
239 | continue; |
||
240 | } |
||
241 | |||
242 | if ($order->order === Order::DESC) { |
||
243 | $s = -$s; |
||
244 | } |
||
245 | return $s; |
||
246 | } |
||
247 | |||
248 | return 0; |
||
249 | } |
||
250 | |||
251 | private function compareProperties($a, $b, Order $order) { |
||
252 | switch ($order->property->dataType) { |
||
253 | case SearchPropertyDefinition::DATATYPE_STRING: |
||
254 | return strcmp($a, $b); |
||
255 | case SearchPropertyDefinition::DATATYPE_BOOLEAN: |
||
256 | if ($a === $b) { |
||
257 | return 0; |
||
258 | } |
||
259 | if ($a === false) { |
||
260 | return -1; |
||
261 | } |
||
262 | return 1; |
||
263 | default: |
||
264 | if ($a === $b) { |
||
265 | return 0; |
||
266 | } |
||
267 | if ($a < $b) { |
||
268 | return -1; |
||
269 | } |
||
270 | return 1; |
||
271 | } |
||
272 | } |
||
273 | |||
274 | private function getSearchResultProperty(SearchResult $result, SearchPropertyDefinition $property) { |
||
275 | /** @var \OCA\DAV\Connector\Sabre\Node $node */ |
||
276 | $node = $result->node; |
||
277 | |||
278 | switch ($property->name) { |
||
279 | case '{DAV:}displayname': |
||
280 | return $node->getName(); |
||
281 | case '{DAV:}getlastmodified': |
||
282 | return $node->getLastModified(); |
||
283 | case FilesPlugin::SIZE_PROPERTYNAME: |
||
284 | return $node->getSize(); |
||
285 | case FilesPlugin::INTERNAL_FILEID_PROPERTYNAME: |
||
286 | return $node->getInternalFileId(); |
||
287 | default: |
||
288 | return null; |
||
289 | } |
||
290 | } |
||
291 | |||
292 | /** |
||
293 | * @param Node $node |
||
294 | * @return string |
||
295 | */ |
||
296 | private function getHrefForNode(Node $node) { |
||
297 | $base = '/files/' . $this->user->getUID(); |
||
298 | return $base . $this->view->getRelativePath($node->getPath()); |
||
299 | } |
||
300 | |||
301 | /** |
||
302 | * @param Query $query |
||
303 | * @return ISearchQuery |
||
304 | */ |
||
305 | private function transformQuery(Query $query): ISearchQuery { |
||
306 | $limit = $query->limit; |
||
307 | $orders = array_map([$this, 'mapSearchOrder'], $query->orderBy); |
||
308 | $offset = $limit->firstResult; |
||
309 | |||
310 | $limitHome = false; |
||
311 | $ownerProp = $this->extractWhereValue($query->where, FilesPlugin::OWNER_ID_PROPERTYNAME, Operator::OPERATION_EQUAL); |
||
312 | if ($ownerProp !== null) { |
||
313 | if ($ownerProp === $this->user->getUID()) { |
||
314 | $limitHome = true; |
||
315 | } else { |
||
316 | throw new \InvalidArgumentException("Invalid search value for '{http://owncloud.org/ns}owner-id', only the current user id is allowed"); |
||
317 | } |
||
318 | } |
||
319 | |||
320 | $operatorCount = $this->countSearchOperators($query->where); |
||
321 | if ($operatorCount > self::OPERATOR_LIMIT) { |
||
322 | throw new \InvalidArgumentException('Invalid search query, maximum operator limit of ' . self::OPERATOR_LIMIT . ' exceeded, got ' . $operatorCount . ' operators'); |
||
323 | } |
||
324 | |||
325 | return new SearchQuery( |
||
326 | $this->transformSearchOperation($query->where), |
||
327 | (int)$limit->maxResults, |
||
328 | $offset, |
||
329 | $orders, |
||
330 | $this->user, |
||
331 | $limitHome |
||
332 | ); |
||
333 | } |
||
334 | |||
335 | private function countSearchOperators(Operator $operator): int { |
||
336 | switch ($operator->type) { |
||
337 | case Operator::OPERATION_AND: |
||
338 | case Operator::OPERATION_OR: |
||
339 | case Operator::OPERATION_NOT: |
||
340 | /** @var Operator[] $arguments */ |
||
341 | $arguments = $operator->arguments; |
||
342 | return array_sum(array_map([$this, 'countSearchOperators'], $arguments)); |
||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
Loading history...
|
|||
343 | case Operator::OPERATION_EQUAL: |
||
344 | case Operator::OPERATION_GREATER_OR_EQUAL_THAN: |
||
345 | case Operator::OPERATION_GREATER_THAN: |
||
346 | case Operator::OPERATION_LESS_OR_EQUAL_THAN: |
||
347 | case Operator::OPERATION_LESS_THAN: |
||
348 | case Operator::OPERATION_IS_LIKE: |
||
349 | case Operator::OPERATION_IS_COLLECTION: |
||
350 | default: |
||
351 | return 1; |
||
352 | } |
||
353 | } |
||
354 | |||
355 | /** |
||
356 | * @param Order $order |
||
357 | * @return ISearchOrder |
||
358 | */ |
||
359 | private function mapSearchOrder(Order $order) { |
||
360 | return new SearchOrder($order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING, $this->mapPropertyNameToColumn($order->property)); |
||
361 | } |
||
362 | |||
363 | /** |
||
364 | * @param Operator $operator |
||
365 | * @return ISearchOperator |
||
366 | */ |
||
367 | private function transformSearchOperation(Operator $operator) { |
||
368 | [, $trimmedType] = explode('}', $operator->type); |
||
369 | switch ($operator->type) { |
||
370 | case Operator::OPERATION_AND: |
||
371 | case Operator::OPERATION_OR: |
||
372 | case Operator::OPERATION_NOT: |
||
373 | $arguments = array_map([$this, 'transformSearchOperation'], $operator->arguments); |
||
374 | return new SearchBinaryOperator($trimmedType, $arguments); |
||
375 | case Operator::OPERATION_EQUAL: |
||
376 | case Operator::OPERATION_GREATER_OR_EQUAL_THAN: |
||
377 | case Operator::OPERATION_GREATER_THAN: |
||
378 | case Operator::OPERATION_LESS_OR_EQUAL_THAN: |
||
379 | case Operator::OPERATION_LESS_THAN: |
||
380 | case Operator::OPERATION_IS_LIKE: |
||
381 | if (count($operator->arguments) !== 2) { |
||
382 | throw new \InvalidArgumentException('Invalid number of arguments for ' . $trimmedType . ' operation'); |
||
383 | } |
||
384 | if (!($operator->arguments[0] instanceof SearchPropertyDefinition)) { |
||
385 | throw new \InvalidArgumentException('Invalid argument 1 for ' . $trimmedType . ' operation, expected property'); |
||
386 | } |
||
387 | if (!($operator->arguments[1] instanceof Literal)) { |
||
388 | throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal'); |
||
389 | } |
||
390 | return new SearchComparison($trimmedType, $this->mapPropertyNameToColumn($operator->arguments[0]), $this->castValue($operator->arguments[0], $operator->arguments[1]->value)); |
||
391 | case Operator::OPERATION_IS_COLLECTION: |
||
392 | return new SearchComparison('eq', 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE); |
||
393 | default: |
||
394 | throw new \InvalidArgumentException('Unsupported operation ' . $trimmedType . ' (' . $operator->type . ')'); |
||
395 | } |
||
396 | } |
||
397 | |||
398 | /** |
||
399 | * @param SearchPropertyDefinition $property |
||
400 | * @return string |
||
401 | */ |
||
402 | private function mapPropertyNameToColumn(SearchPropertyDefinition $property) { |
||
403 | switch ($property->name) { |
||
404 | case '{DAV:}displayname': |
||
405 | return 'name'; |
||
406 | case '{DAV:}getcontenttype': |
||
407 | return 'mimetype'; |
||
408 | case '{DAV:}getlastmodified': |
||
409 | return 'mtime'; |
||
410 | case FilesPlugin::SIZE_PROPERTYNAME: |
||
411 | return 'size'; |
||
412 | case TagsPlugin::FAVORITE_PROPERTYNAME: |
||
413 | return 'favorite'; |
||
414 | case TagsPlugin::TAGS_PROPERTYNAME: |
||
415 | return 'tagname'; |
||
416 | case FilesPlugin::INTERNAL_FILEID_PROPERTYNAME: |
||
417 | return 'fileid'; |
||
418 | default: |
||
419 | throw new \InvalidArgumentException('Unsupported property for search or order: ' . $property->name); |
||
420 | } |
||
421 | } |
||
422 | |||
423 | private function castValue(SearchPropertyDefinition $property, $value) { |
||
424 | switch ($property->dataType) { |
||
425 | case SearchPropertyDefinition::DATATYPE_BOOLEAN: |
||
426 | return $value === 'yes'; |
||
427 | case SearchPropertyDefinition::DATATYPE_DECIMAL: |
||
428 | case SearchPropertyDefinition::DATATYPE_INTEGER: |
||
429 | case SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER: |
||
430 | return 0 + $value; |
||
431 | case SearchPropertyDefinition::DATATYPE_DATETIME: |
||
432 | if (is_numeric($value)) { |
||
433 | return max(0, 0 + $value); |
||
434 | } |
||
435 | $date = \DateTime::createFromFormat(\DateTimeInterface::ATOM, (string)$value); |
||
436 | return ($date instanceof \DateTime && $date->getTimestamp() !== false) ? $date->getTimestamp() : 0; |
||
437 | default: |
||
438 | return $value; |
||
439 | } |
||
440 | } |
||
441 | |||
442 | /** |
||
443 | * Get a specific property from the were clause |
||
444 | */ |
||
445 | private function extractWhereValue(Operator &$operator, string $propertyName, string $comparison, bool $acceptableLocation = true): ?string { |
||
446 | switch ($operator->type) { |
||
447 | case Operator::OPERATION_AND: |
||
448 | case Operator::OPERATION_OR: |
||
449 | case Operator::OPERATION_NOT: |
||
450 | foreach ($operator->arguments as &$argument) { |
||
451 | $value = $this->extractWhereValue($argument, $propertyName, $comparison, $acceptableLocation && $operator->type === Operator::OPERATION_AND); |
||
452 | if ($value !== null) { |
||
453 | return $value; |
||
454 | } |
||
455 | } |
||
456 | return null; |
||
457 | case Operator::OPERATION_EQUAL: |
||
458 | case Operator::OPERATION_GREATER_OR_EQUAL_THAN: |
||
459 | case Operator::OPERATION_GREATER_THAN: |
||
460 | case Operator::OPERATION_LESS_OR_EQUAL_THAN: |
||
461 | case Operator::OPERATION_LESS_THAN: |
||
462 | case Operator::OPERATION_IS_LIKE: |
||
463 | if ($operator->arguments[0]->name === $propertyName) { |
||
0 ignored issues
–
show
|
|||
464 | if ($operator->type === $comparison) { |
||
465 | if ($acceptableLocation) { |
||
466 | if ($operator->arguments[1] instanceof Literal) { |
||
467 | $value = $operator->arguments[1]->value; |
||
468 | |||
469 | // to remove the comparison from the query, we replace it with an empty AND |
||
470 | $operator = new Operator(Operator::OPERATION_AND); |
||
471 | |||
472 | return $value; |
||
473 | } else { |
||
474 | throw new \InvalidArgumentException("searching by '$propertyName' is only allowed with a literal value"); |
||
475 | } |
||
476 | } else { |
||
477 | throw new \InvalidArgumentException("searching by '$propertyName' is not allowed inside a '{DAV:}or' or '{DAV:}not'"); |
||
478 | } |
||
479 | } else { |
||
480 | throw new \InvalidArgumentException("searching by '$propertyName' is only allowed inside a '$comparison'"); |
||
481 | } |
||
482 | } else { |
||
483 | return null; |
||
484 | } |
||
485 | // no break |
||
486 | default: |
||
487 | return null; |
||
488 | } |
||
489 | } |
||
490 | } |
||
491 |