Completed
Push — master ( 9df998...698146 )
by
unknown
18:00 queued 15s
created
apps/dav/lib/Files/FileSearchBackend.php 2 patches
Indentation   +497 added lines, -497 removed lines patch added patch discarded remove patch
@@ -41,501 +41,501 @@
 block discarded – undo
41 41
 use SearchDAV\Query\Query;
42 42
 
43 43
 class FileSearchBackend implements ISearchBackend {
44
-	public const OPERATOR_LIMIT = 100;
45
-
46
-	public function __construct(
47
-		private CachingTree $tree,
48
-		private IUser $user,
49
-		private IRootFolder $rootFolder,
50
-		private IManager $shareManager,
51
-		private View $view,
52
-		private IFilesMetadataManager $filesMetadataManager,
53
-	) {
54
-	}
55
-
56
-	/**
57
-	 * Search endpoint will be remote.php/dav
58
-	 */
59
-	public function getArbiterPath(): string {
60
-		return '';
61
-	}
62
-
63
-	public function isValidScope(string $href, $depth, ?string $path): bool {
64
-		// only allow scopes inside the dav server
65
-		if (is_null($path)) {
66
-			return false;
67
-		}
68
-
69
-		try {
70
-			$node = $this->tree->getNodeForPath($path);
71
-			return $node instanceof Directory;
72
-		} catch (NotFound $e) {
73
-			return false;
74
-		}
75
-	}
76
-
77
-	public function getPropertyDefinitionsForScope(string $href, ?string $path): array {
78
-		// all valid scopes support the same schema
79
-
80
-		//todo dynamically load all propfind properties that are supported
81
-		$props = [
82
-			// queryable properties
83
-			new SearchPropertyDefinition('{DAV:}displayname', true, true, true),
84
-			new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
85
-			new SearchPropertyDefinition('{DAV:}getlastmodified', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
86
-			new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
87
-			new SearchPropertyDefinition(TagsPlugin::FAVORITE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_BOOLEAN),
88
-			new SearchPropertyDefinition(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME, true, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
89
-			new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, true, true, false),
90
-
91
-			// select only properties
92
-			new SearchPropertyDefinition('{DAV:}resourcetype', true, false, false),
93
-			new SearchPropertyDefinition('{DAV:}getcontentlength', true, false, false),
94
-			new SearchPropertyDefinition(FilesPlugin::CHECKSUMS_PROPERTYNAME, true, false, false),
95
-			new SearchPropertyDefinition(FilesPlugin::PERMISSIONS_PROPERTYNAME, true, false, false),
96
-			new SearchPropertyDefinition(FilesPlugin::GETETAG_PROPERTYNAME, true, false, false),
97
-			new SearchPropertyDefinition(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, true, false, false),
98
-			new SearchPropertyDefinition(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, true, false, false),
99
-			new SearchPropertyDefinition(FilesPlugin::HAS_PREVIEW_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_BOOLEAN),
100
-			new SearchPropertyDefinition(FilesPlugin::FILEID_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
101
-		];
102
-
103
-		return array_merge($props, $this->getPropertyDefinitionsForMetadata());
104
-	}
105
-
106
-
107
-	private function getPropertyDefinitionsForMetadata(): array {
108
-		$metadataProps = [];
109
-		$metadata = $this->filesMetadataManager->getKnownMetadata();
110
-		$indexes = $metadata->getIndexes();
111
-		foreach ($metadata->getKeys() as $key) {
112
-			$isIndex = in_array($key, $indexes);
113
-			$type = match ($metadata->getType($key)) {
114
-				IMetadataValueWrapper::TYPE_INT => SearchPropertyDefinition::DATATYPE_INTEGER,
115
-				IMetadataValueWrapper::TYPE_FLOAT => SearchPropertyDefinition::DATATYPE_DECIMAL,
116
-				IMetadataValueWrapper::TYPE_BOOL => SearchPropertyDefinition::DATATYPE_BOOLEAN,
117
-				default => SearchPropertyDefinition::DATATYPE_STRING
118
-			};
119
-			$metadataProps[] = new SearchPropertyDefinition(
120
-				FilesPlugin::FILE_METADATA_PREFIX . $key,
121
-				true,
122
-				$isIndex,
123
-				$isIndex,
124
-				$type
125
-			);
126
-		}
127
-
128
-		return $metadataProps;
129
-	}
130
-
131
-	/**
132
-	 * @param INode[] $nodes
133
-	 * @param string[] $requestProperties
134
-	 */
135
-	public function preloadPropertyFor(array $nodes, array $requestProperties): void {
136
-	}
137
-
138
-	private function getFolderForPath(?string $path = null): Folder {
139
-		if ($path === null) {
140
-			throw new \InvalidArgumentException('Using uri\'s as scope is not supported, please use a path relative to the search arbiter instead');
141
-		}
142
-
143
-		$node = $this->tree->getNodeForPath($path);
144
-
145
-		if (!$node instanceof Directory) {
146
-			throw new \InvalidArgumentException('Search is only supported on directories');
147
-		}
148
-
149
-		$fileInfo = $node->getFileInfo();
150
-
151
-		/** @var Folder */
152
-		return $this->rootFolder->get($fileInfo->getPath());
153
-	}
154
-
155
-	/**
156
-	 * @param Query $search
157
-	 * @return SearchResult[]
158
-	 */
159
-	public function search(Query $search): array {
160
-		switch (count($search->from)) {
161
-			case 0:
162
-				throw new \InvalidArgumentException('You need to specify a scope for the search.');
163
-				break;
164
-			case 1:
165
-				$scope = $search->from[0];
166
-				$folder = $this->getFolderForPath($scope->path);
167
-				$query = $this->transformQuery($search);
168
-				$results = $folder->search($query);
169
-				break;
170
-			default:
171
-				$scopes = [];
172
-				foreach ($search->from as $scope) {
173
-					$folder = $this->getFolderForPath($scope->path);
174
-					$folderStorage = $folder->getStorage();
175
-					if ($folderStorage->instanceOfStorage(Jail::class)) {
176
-						/** @var Jail $folderStorage */
177
-						$internalPath = $folderStorage->getUnjailedPath($folder->getInternalPath());
178
-					} else {
179
-						$internalPath = $folder->getInternalPath();
180
-					}
181
-
182
-					$scopes[] = new SearchBinaryOperator(
183
-						ISearchBinaryOperator::OPERATOR_AND,
184
-						[
185
-							new SearchComparison(
186
-								ISearchComparison::COMPARE_EQUAL,
187
-								'storage',
188
-								$folderStorage->getCache()->getNumericStorageId(),
189
-								''
190
-							),
191
-							new SearchComparison(
192
-								ISearchComparison::COMPARE_LIKE,
193
-								'path',
194
-								$internalPath . '/%',
195
-								''
196
-							),
197
-						]
198
-					);
199
-				}
200
-
201
-				$scopeOperators = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $scopes);
202
-				$query = $this->transformQuery($search, $scopeOperators);
203
-				$userFolder = $this->rootFolder->getUserFolder($this->user->getUID());
204
-				$results = $userFolder->search($query);
205
-		}
206
-
207
-		/** @var SearchResult[] $nodes */
208
-		$nodes = array_map(function (Node $node) {
209
-			if ($node instanceof Folder) {
210
-				$davNode = new Directory($this->view, $node, $this->tree, $this->shareManager);
211
-			} else {
212
-				$davNode = new File($this->view, $node, $this->shareManager);
213
-			}
214
-			$path = $this->getHrefForNode($node);
215
-			$this->tree->cacheNode($davNode, $path);
216
-			return new SearchResult($davNode, $path);
217
-		}, $results);
218
-
219
-		if (!$query->limitToHome()) {
220
-			// Sort again, since the result from multiple storages is appended and not sorted
221
-			usort($nodes, function (SearchResult $a, SearchResult $b) use ($search) {
222
-				return $this->sort($a, $b, $search->orderBy);
223
-			});
224
-		}
225
-
226
-		// If a limit is provided use only return that number of files
227
-		if ($search->limit->maxResults !== 0) {
228
-			$nodes = \array_slice($nodes, 0, $search->limit->maxResults);
229
-		}
230
-
231
-		return $nodes;
232
-	}
233
-
234
-	private function sort(SearchResult $a, SearchResult $b, array $orders) {
235
-		/** @var Order $order */
236
-		foreach ($orders as $order) {
237
-			$v1 = $this->getSearchResultProperty($a, $order->property);
238
-			$v2 = $this->getSearchResultProperty($b, $order->property);
239
-
240
-
241
-			if ($v1 === null && $v2 === null) {
242
-				continue;
243
-			}
244
-			if ($v1 === null) {
245
-				return $order->order === Order::ASC ? 1 : -1;
246
-			}
247
-			if ($v2 === null) {
248
-				return $order->order === Order::ASC ? -1 : 1;
249
-			}
250
-
251
-			$s = $this->compareProperties($v1, $v2, $order);
252
-			if ($s === 0) {
253
-				continue;
254
-			}
255
-
256
-			if ($order->order === Order::DESC) {
257
-				$s = -$s;
258
-			}
259
-			return $s;
260
-		}
261
-
262
-		return 0;
263
-	}
264
-
265
-	private function compareProperties($a, $b, Order $order) {
266
-		switch ($order->property->dataType) {
267
-			case SearchPropertyDefinition::DATATYPE_STRING:
268
-				return strcmp($a, $b);
269
-			case SearchPropertyDefinition::DATATYPE_BOOLEAN:
270
-				if ($a === $b) {
271
-					return 0;
272
-				}
273
-				if ($a === false) {
274
-					return -1;
275
-				}
276
-				return 1;
277
-			default:
278
-				if ($a === $b) {
279
-					return 0;
280
-				}
281
-				if ($a < $b) {
282
-					return -1;
283
-				}
284
-				return 1;
285
-		}
286
-	}
287
-
288
-	private function getSearchResultProperty(SearchResult $result, SearchPropertyDefinition $property) {
289
-		/** @var \OCA\DAV\Connector\Sabre\Node $node */
290
-		$node = $result->node;
291
-
292
-		switch ($property->name) {
293
-			case '{DAV:}displayname':
294
-				return $node->getName();
295
-			case '{DAV:}getlastmodified':
296
-				return $node->getLastModified();
297
-			case FilesPlugin::SIZE_PROPERTYNAME:
298
-				return $node->getSize();
299
-			case FilesPlugin::INTERNAL_FILEID_PROPERTYNAME:
300
-				return $node->getInternalFileId();
301
-			default:
302
-				return null;
303
-		}
304
-	}
305
-
306
-	/**
307
-	 * @param Node $node
308
-	 * @return string
309
-	 */
310
-	private function getHrefForNode(Node $node) {
311
-		$base = '/files/' . $this->user->getUID();
312
-		return $base . $this->view->getRelativePath($node->getPath());
313
-	}
314
-
315
-	/**
316
-	 * @param Query $query
317
-	 *
318
-	 * @return ISearchQuery
319
-	 */
320
-	private function transformQuery(Query $query, ?SearchBinaryOperator $scopeOperators = null): ISearchQuery {
321
-		$orders = array_map(function (Order $order): ISearchOrder {
322
-			$direction = $order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING;
323
-			if (str_starts_with($order->property->name, FilesPlugin::FILE_METADATA_PREFIX)) {
324
-				return new SearchOrder($direction, substr($order->property->name, strlen(FilesPlugin::FILE_METADATA_PREFIX)), IMetadataQuery::EXTRA);
325
-			} else {
326
-				return new SearchOrder($direction, $this->mapPropertyNameToColumn($order->property));
327
-			}
328
-		}, $query->orderBy);
329
-
330
-		$limit = $query->limit;
331
-		$offset = $limit->firstResult;
332
-
333
-		$limitHome = false;
334
-		$ownerProp = $this->extractWhereValue($query->where, FilesPlugin::OWNER_ID_PROPERTYNAME, Operator::OPERATION_EQUAL);
335
-		if ($ownerProp !== null) {
336
-			if ($ownerProp === $this->user->getUID()) {
337
-				$limitHome = true;
338
-			} else {
339
-				throw new \InvalidArgumentException("Invalid search value for '{http://owncloud.org/ns}owner-id', only the current user id is allowed");
340
-			}
341
-		}
342
-
343
-		$operatorCount = $this->countSearchOperators($query->where);
344
-		if ($operatorCount > self::OPERATOR_LIMIT) {
345
-			throw new \InvalidArgumentException('Invalid search query, maximum operator limit of ' . self::OPERATOR_LIMIT . ' exceeded, got ' . $operatorCount . ' operators');
346
-		}
347
-
348
-		/** @var SearchBinaryOperator|SearchComparison */
349
-		$queryOperators = $this->transformSearchOperation($query->where);
350
-		if ($scopeOperators === null) {
351
-			$operators = $queryOperators;
352
-		} else {
353
-			$operators = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$queryOperators, $scopeOperators]);
354
-		}
355
-
356
-		return new SearchQuery(
357
-			$operators,
358
-			(int)$limit->maxResults,
359
-			$offset,
360
-			$orders,
361
-			$this->user,
362
-			$limitHome
363
-		);
364
-	}
365
-
366
-	private function countSearchOperators(Operator $operator): int {
367
-		switch ($operator->type) {
368
-			case Operator::OPERATION_AND:
369
-			case Operator::OPERATION_OR:
370
-			case Operator::OPERATION_NOT:
371
-				/** @var Operator[] $arguments */
372
-				$arguments = $operator->arguments;
373
-				return array_sum(array_map([$this, 'countSearchOperators'], $arguments));
374
-			case Operator::OPERATION_EQUAL:
375
-			case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
376
-			case Operator::OPERATION_GREATER_THAN:
377
-			case Operator::OPERATION_LESS_OR_EQUAL_THAN:
378
-			case Operator::OPERATION_LESS_THAN:
379
-			case Operator::OPERATION_IS_LIKE:
380
-			case Operator::OPERATION_IS_COLLECTION:
381
-			default:
382
-				return 1;
383
-		}
384
-	}
385
-
386
-	/**
387
-	 * @param Operator $operator
388
-	 * @return ISearchOperator
389
-	 */
390
-	private function transformSearchOperation(Operator $operator) {
391
-		[, $trimmedType] = explode('}', $operator->type);
392
-		switch ($operator->type) {
393
-			case Operator::OPERATION_AND:
394
-			case Operator::OPERATION_OR:
395
-			case Operator::OPERATION_NOT:
396
-				$arguments = array_map([$this, 'transformSearchOperation'], $operator->arguments);
397
-				return new SearchBinaryOperator($trimmedType, $arguments);
398
-			case Operator::OPERATION_EQUAL:
399
-			case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
400
-			case Operator::OPERATION_GREATER_THAN:
401
-			case Operator::OPERATION_LESS_OR_EQUAL_THAN:
402
-			case Operator::OPERATION_LESS_THAN:
403
-			case Operator::OPERATION_IS_LIKE:
404
-				if (count($operator->arguments) !== 2) {
405
-					throw new \InvalidArgumentException('Invalid number of arguments for ' . $trimmedType . ' operation');
406
-				}
407
-				if (!($operator->arguments[1] instanceof Literal)) {
408
-					throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal');
409
-				}
410
-				$value = $operator->arguments[1]->value;
411
-				// no break
412
-			case Operator::OPERATION_IS_DEFINED:
413
-				if (!($operator->arguments[0] instanceof SearchPropertyDefinition)) {
414
-					throw new \InvalidArgumentException('Invalid argument 1 for ' . $trimmedType . ' operation, expected property');
415
-				}
416
-				$property = $operator->arguments[0];
417
-
418
-				if (str_starts_with($property->name, FilesPlugin::FILE_METADATA_PREFIX)) {
419
-					$field = substr($property->name, strlen(FilesPlugin::FILE_METADATA_PREFIX));
420
-					$extra = IMetadataQuery::EXTRA;
421
-				} else {
422
-					$field = $this->mapPropertyNameToColumn($property);
423
-				}
424
-
425
-				try {
426
-					$castedValue = $this->castValue($property, $value ?? '');
427
-				} catch (\Error $e) {
428
-					throw new \InvalidArgumentException('Invalid property value for ' . $property->name, previous: $e);
429
-				}
430
-
431
-				return new SearchComparison(
432
-					$trimmedType,
433
-					$field,
434
-					$castedValue,
435
-					$extra ?? ''
436
-				);
437
-
438
-			case Operator::OPERATION_IS_COLLECTION:
439
-				return new SearchComparison('eq', 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE);
440
-			default:
441
-				throw new \InvalidArgumentException('Unsupported operation ' . $trimmedType . ' (' . $operator->type . ')');
442
-		}
443
-	}
444
-
445
-	/**
446
-	 * @param SearchPropertyDefinition $property
447
-	 * @return string
448
-	 */
449
-	private function mapPropertyNameToColumn(SearchPropertyDefinition $property) {
450
-		switch ($property->name) {
451
-			case '{DAV:}displayname':
452
-				return 'name';
453
-			case '{DAV:}getcontenttype':
454
-				return 'mimetype';
455
-			case '{DAV:}getlastmodified':
456
-				return 'mtime';
457
-			case FilesPlugin::SIZE_PROPERTYNAME:
458
-				return 'size';
459
-			case TagsPlugin::FAVORITE_PROPERTYNAME:
460
-				return 'favorite';
461
-			case TagsPlugin::TAGS_PROPERTYNAME:
462
-				return 'tagname';
463
-			case FilesPlugin::INTERNAL_FILEID_PROPERTYNAME:
464
-				return 'fileid';
465
-			default:
466
-				throw new \InvalidArgumentException('Unsupported property for search or order: ' . $property->name);
467
-		}
468
-	}
469
-
470
-	private function castValue(SearchPropertyDefinition $property, $value) {
471
-		if ($value === '') {
472
-			return '';
473
-		}
474
-
475
-		switch ($property->dataType) {
476
-			case SearchPropertyDefinition::DATATYPE_BOOLEAN:
477
-				return $value === 'yes';
478
-			case SearchPropertyDefinition::DATATYPE_DECIMAL:
479
-			case SearchPropertyDefinition::DATATYPE_INTEGER:
480
-			case SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER:
481
-				return 0 + $value;
482
-			case SearchPropertyDefinition::DATATYPE_DATETIME:
483
-				if (is_numeric($value)) {
484
-					return max(0, 0 + $value);
485
-				}
486
-				$date = \DateTime::createFromFormat(\DateTimeInterface::ATOM, (string)$value);
487
-				return ($date instanceof \DateTime && $date->getTimestamp() !== false) ? $date->getTimestamp() : 0;
488
-			default:
489
-				return $value;
490
-		}
491
-	}
492
-
493
-	/**
494
-	 * Get a specific property from the were clause
495
-	 */
496
-	private function extractWhereValue(Operator &$operator, string $propertyName, string $comparison, bool $acceptableLocation = true): ?string {
497
-		switch ($operator->type) {
498
-			case Operator::OPERATION_AND:
499
-			case Operator::OPERATION_OR:
500
-			case Operator::OPERATION_NOT:
501
-				foreach ($operator->arguments as &$argument) {
502
-					$value = $this->extractWhereValue($argument, $propertyName, $comparison, $acceptableLocation && $operator->type === Operator::OPERATION_AND);
503
-					if ($value !== null) {
504
-						return $value;
505
-					}
506
-				}
507
-				return null;
508
-			case Operator::OPERATION_EQUAL:
509
-			case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
510
-			case Operator::OPERATION_GREATER_THAN:
511
-			case Operator::OPERATION_LESS_OR_EQUAL_THAN:
512
-			case Operator::OPERATION_LESS_THAN:
513
-			case Operator::OPERATION_IS_LIKE:
514
-				if ($operator->arguments[0]->name === $propertyName) {
515
-					if ($operator->type === $comparison) {
516
-						if ($acceptableLocation) {
517
-							if ($operator->arguments[1] instanceof Literal) {
518
-								$value = $operator->arguments[1]->value;
519
-
520
-								// to remove the comparison from the query, we replace it with an empty AND
521
-								$operator = new Operator(Operator::OPERATION_AND);
522
-
523
-								return $value;
524
-							} else {
525
-								throw new \InvalidArgumentException("searching by '$propertyName' is only allowed with a literal value");
526
-							}
527
-						} else {
528
-							throw new \InvalidArgumentException("searching by '$propertyName' is not allowed inside a '{DAV:}or' or '{DAV:}not'");
529
-						}
530
-					} else {
531
-						throw new \InvalidArgumentException("searching by '$propertyName' is only allowed inside a '$comparison'");
532
-					}
533
-				} else {
534
-					return null;
535
-				}
536
-				// no break
537
-			default:
538
-				return null;
539
-		}
540
-	}
44
+    public const OPERATOR_LIMIT = 100;
45
+
46
+    public function __construct(
47
+        private CachingTree $tree,
48
+        private IUser $user,
49
+        private IRootFolder $rootFolder,
50
+        private IManager $shareManager,
51
+        private View $view,
52
+        private IFilesMetadataManager $filesMetadataManager,
53
+    ) {
54
+    }
55
+
56
+    /**
57
+     * Search endpoint will be remote.php/dav
58
+     */
59
+    public function getArbiterPath(): string {
60
+        return '';
61
+    }
62
+
63
+    public function isValidScope(string $href, $depth, ?string $path): bool {
64
+        // only allow scopes inside the dav server
65
+        if (is_null($path)) {
66
+            return false;
67
+        }
68
+
69
+        try {
70
+            $node = $this->tree->getNodeForPath($path);
71
+            return $node instanceof Directory;
72
+        } catch (NotFound $e) {
73
+            return false;
74
+        }
75
+    }
76
+
77
+    public function getPropertyDefinitionsForScope(string $href, ?string $path): array {
78
+        // all valid scopes support the same schema
79
+
80
+        //todo dynamically load all propfind properties that are supported
81
+        $props = [
82
+            // queryable properties
83
+            new SearchPropertyDefinition('{DAV:}displayname', true, true, true),
84
+            new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
85
+            new SearchPropertyDefinition('{DAV:}getlastmodified', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
86
+            new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
87
+            new SearchPropertyDefinition(TagsPlugin::FAVORITE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_BOOLEAN),
88
+            new SearchPropertyDefinition(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME, true, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
89
+            new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, true, true, false),
90
+
91
+            // select only properties
92
+            new SearchPropertyDefinition('{DAV:}resourcetype', true, false, false),
93
+            new SearchPropertyDefinition('{DAV:}getcontentlength', true, false, false),
94
+            new SearchPropertyDefinition(FilesPlugin::CHECKSUMS_PROPERTYNAME, true, false, false),
95
+            new SearchPropertyDefinition(FilesPlugin::PERMISSIONS_PROPERTYNAME, true, false, false),
96
+            new SearchPropertyDefinition(FilesPlugin::GETETAG_PROPERTYNAME, true, false, false),
97
+            new SearchPropertyDefinition(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, true, false, false),
98
+            new SearchPropertyDefinition(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, true, false, false),
99
+            new SearchPropertyDefinition(FilesPlugin::HAS_PREVIEW_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_BOOLEAN),
100
+            new SearchPropertyDefinition(FilesPlugin::FILEID_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
101
+        ];
102
+
103
+        return array_merge($props, $this->getPropertyDefinitionsForMetadata());
104
+    }
105
+
106
+
107
+    private function getPropertyDefinitionsForMetadata(): array {
108
+        $metadataProps = [];
109
+        $metadata = $this->filesMetadataManager->getKnownMetadata();
110
+        $indexes = $metadata->getIndexes();
111
+        foreach ($metadata->getKeys() as $key) {
112
+            $isIndex = in_array($key, $indexes);
113
+            $type = match ($metadata->getType($key)) {
114
+                IMetadataValueWrapper::TYPE_INT => SearchPropertyDefinition::DATATYPE_INTEGER,
115
+                IMetadataValueWrapper::TYPE_FLOAT => SearchPropertyDefinition::DATATYPE_DECIMAL,
116
+                IMetadataValueWrapper::TYPE_BOOL => SearchPropertyDefinition::DATATYPE_BOOLEAN,
117
+                default => SearchPropertyDefinition::DATATYPE_STRING
118
+            };
119
+            $metadataProps[] = new SearchPropertyDefinition(
120
+                FilesPlugin::FILE_METADATA_PREFIX . $key,
121
+                true,
122
+                $isIndex,
123
+                $isIndex,
124
+                $type
125
+            );
126
+        }
127
+
128
+        return $metadataProps;
129
+    }
130
+
131
+    /**
132
+     * @param INode[] $nodes
133
+     * @param string[] $requestProperties
134
+     */
135
+    public function preloadPropertyFor(array $nodes, array $requestProperties): void {
136
+    }
137
+
138
+    private function getFolderForPath(?string $path = null): Folder {
139
+        if ($path === null) {
140
+            throw new \InvalidArgumentException('Using uri\'s as scope is not supported, please use a path relative to the search arbiter instead');
141
+        }
142
+
143
+        $node = $this->tree->getNodeForPath($path);
144
+
145
+        if (!$node instanceof Directory) {
146
+            throw new \InvalidArgumentException('Search is only supported on directories');
147
+        }
148
+
149
+        $fileInfo = $node->getFileInfo();
150
+
151
+        /** @var Folder */
152
+        return $this->rootFolder->get($fileInfo->getPath());
153
+    }
154
+
155
+    /**
156
+     * @param Query $search
157
+     * @return SearchResult[]
158
+     */
159
+    public function search(Query $search): array {
160
+        switch (count($search->from)) {
161
+            case 0:
162
+                throw new \InvalidArgumentException('You need to specify a scope for the search.');
163
+                break;
164
+            case 1:
165
+                $scope = $search->from[0];
166
+                $folder = $this->getFolderForPath($scope->path);
167
+                $query = $this->transformQuery($search);
168
+                $results = $folder->search($query);
169
+                break;
170
+            default:
171
+                $scopes = [];
172
+                foreach ($search->from as $scope) {
173
+                    $folder = $this->getFolderForPath($scope->path);
174
+                    $folderStorage = $folder->getStorage();
175
+                    if ($folderStorage->instanceOfStorage(Jail::class)) {
176
+                        /** @var Jail $folderStorage */
177
+                        $internalPath = $folderStorage->getUnjailedPath($folder->getInternalPath());
178
+                    } else {
179
+                        $internalPath = $folder->getInternalPath();
180
+                    }
181
+
182
+                    $scopes[] = new SearchBinaryOperator(
183
+                        ISearchBinaryOperator::OPERATOR_AND,
184
+                        [
185
+                            new SearchComparison(
186
+                                ISearchComparison::COMPARE_EQUAL,
187
+                                'storage',
188
+                                $folderStorage->getCache()->getNumericStorageId(),
189
+                                ''
190
+                            ),
191
+                            new SearchComparison(
192
+                                ISearchComparison::COMPARE_LIKE,
193
+                                'path',
194
+                                $internalPath . '/%',
195
+                                ''
196
+                            ),
197
+                        ]
198
+                    );
199
+                }
200
+
201
+                $scopeOperators = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $scopes);
202
+                $query = $this->transformQuery($search, $scopeOperators);
203
+                $userFolder = $this->rootFolder->getUserFolder($this->user->getUID());
204
+                $results = $userFolder->search($query);
205
+        }
206
+
207
+        /** @var SearchResult[] $nodes */
208
+        $nodes = array_map(function (Node $node) {
209
+            if ($node instanceof Folder) {
210
+                $davNode = new Directory($this->view, $node, $this->tree, $this->shareManager);
211
+            } else {
212
+                $davNode = new File($this->view, $node, $this->shareManager);
213
+            }
214
+            $path = $this->getHrefForNode($node);
215
+            $this->tree->cacheNode($davNode, $path);
216
+            return new SearchResult($davNode, $path);
217
+        }, $results);
218
+
219
+        if (!$query->limitToHome()) {
220
+            // Sort again, since the result from multiple storages is appended and not sorted
221
+            usort($nodes, function (SearchResult $a, SearchResult $b) use ($search) {
222
+                return $this->sort($a, $b, $search->orderBy);
223
+            });
224
+        }
225
+
226
+        // If a limit is provided use only return that number of files
227
+        if ($search->limit->maxResults !== 0) {
228
+            $nodes = \array_slice($nodes, 0, $search->limit->maxResults);
229
+        }
230
+
231
+        return $nodes;
232
+    }
233
+
234
+    private function sort(SearchResult $a, SearchResult $b, array $orders) {
235
+        /** @var Order $order */
236
+        foreach ($orders as $order) {
237
+            $v1 = $this->getSearchResultProperty($a, $order->property);
238
+            $v2 = $this->getSearchResultProperty($b, $order->property);
239
+
240
+
241
+            if ($v1 === null && $v2 === null) {
242
+                continue;
243
+            }
244
+            if ($v1 === null) {
245
+                return $order->order === Order::ASC ? 1 : -1;
246
+            }
247
+            if ($v2 === null) {
248
+                return $order->order === Order::ASC ? -1 : 1;
249
+            }
250
+
251
+            $s = $this->compareProperties($v1, $v2, $order);
252
+            if ($s === 0) {
253
+                continue;
254
+            }
255
+
256
+            if ($order->order === Order::DESC) {
257
+                $s = -$s;
258
+            }
259
+            return $s;
260
+        }
261
+
262
+        return 0;
263
+    }
264
+
265
+    private function compareProperties($a, $b, Order $order) {
266
+        switch ($order->property->dataType) {
267
+            case SearchPropertyDefinition::DATATYPE_STRING:
268
+                return strcmp($a, $b);
269
+            case SearchPropertyDefinition::DATATYPE_BOOLEAN:
270
+                if ($a === $b) {
271
+                    return 0;
272
+                }
273
+                if ($a === false) {
274
+                    return -1;
275
+                }
276
+                return 1;
277
+            default:
278
+                if ($a === $b) {
279
+                    return 0;
280
+                }
281
+                if ($a < $b) {
282
+                    return -1;
283
+                }
284
+                return 1;
285
+        }
286
+    }
287
+
288
+    private function getSearchResultProperty(SearchResult $result, SearchPropertyDefinition $property) {
289
+        /** @var \OCA\DAV\Connector\Sabre\Node $node */
290
+        $node = $result->node;
291
+
292
+        switch ($property->name) {
293
+            case '{DAV:}displayname':
294
+                return $node->getName();
295
+            case '{DAV:}getlastmodified':
296
+                return $node->getLastModified();
297
+            case FilesPlugin::SIZE_PROPERTYNAME:
298
+                return $node->getSize();
299
+            case FilesPlugin::INTERNAL_FILEID_PROPERTYNAME:
300
+                return $node->getInternalFileId();
301
+            default:
302
+                return null;
303
+        }
304
+    }
305
+
306
+    /**
307
+     * @param Node $node
308
+     * @return string
309
+     */
310
+    private function getHrefForNode(Node $node) {
311
+        $base = '/files/' . $this->user->getUID();
312
+        return $base . $this->view->getRelativePath($node->getPath());
313
+    }
314
+
315
+    /**
316
+     * @param Query $query
317
+     *
318
+     * @return ISearchQuery
319
+     */
320
+    private function transformQuery(Query $query, ?SearchBinaryOperator $scopeOperators = null): ISearchQuery {
321
+        $orders = array_map(function (Order $order): ISearchOrder {
322
+            $direction = $order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING;
323
+            if (str_starts_with($order->property->name, FilesPlugin::FILE_METADATA_PREFIX)) {
324
+                return new SearchOrder($direction, substr($order->property->name, strlen(FilesPlugin::FILE_METADATA_PREFIX)), IMetadataQuery::EXTRA);
325
+            } else {
326
+                return new SearchOrder($direction, $this->mapPropertyNameToColumn($order->property));
327
+            }
328
+        }, $query->orderBy);
329
+
330
+        $limit = $query->limit;
331
+        $offset = $limit->firstResult;
332
+
333
+        $limitHome = false;
334
+        $ownerProp = $this->extractWhereValue($query->where, FilesPlugin::OWNER_ID_PROPERTYNAME, Operator::OPERATION_EQUAL);
335
+        if ($ownerProp !== null) {
336
+            if ($ownerProp === $this->user->getUID()) {
337
+                $limitHome = true;
338
+            } else {
339
+                throw new \InvalidArgumentException("Invalid search value for '{http://owncloud.org/ns}owner-id', only the current user id is allowed");
340
+            }
341
+        }
342
+
343
+        $operatorCount = $this->countSearchOperators($query->where);
344
+        if ($operatorCount > self::OPERATOR_LIMIT) {
345
+            throw new \InvalidArgumentException('Invalid search query, maximum operator limit of ' . self::OPERATOR_LIMIT . ' exceeded, got ' . $operatorCount . ' operators');
346
+        }
347
+
348
+        /** @var SearchBinaryOperator|SearchComparison */
349
+        $queryOperators = $this->transformSearchOperation($query->where);
350
+        if ($scopeOperators === null) {
351
+            $operators = $queryOperators;
352
+        } else {
353
+            $operators = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$queryOperators, $scopeOperators]);
354
+        }
355
+
356
+        return new SearchQuery(
357
+            $operators,
358
+            (int)$limit->maxResults,
359
+            $offset,
360
+            $orders,
361
+            $this->user,
362
+            $limitHome
363
+        );
364
+    }
365
+
366
+    private function countSearchOperators(Operator $operator): int {
367
+        switch ($operator->type) {
368
+            case Operator::OPERATION_AND:
369
+            case Operator::OPERATION_OR:
370
+            case Operator::OPERATION_NOT:
371
+                /** @var Operator[] $arguments */
372
+                $arguments = $operator->arguments;
373
+                return array_sum(array_map([$this, 'countSearchOperators'], $arguments));
374
+            case Operator::OPERATION_EQUAL:
375
+            case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
376
+            case Operator::OPERATION_GREATER_THAN:
377
+            case Operator::OPERATION_LESS_OR_EQUAL_THAN:
378
+            case Operator::OPERATION_LESS_THAN:
379
+            case Operator::OPERATION_IS_LIKE:
380
+            case Operator::OPERATION_IS_COLLECTION:
381
+            default:
382
+                return 1;
383
+        }
384
+    }
385
+
386
+    /**
387
+     * @param Operator $operator
388
+     * @return ISearchOperator
389
+     */
390
+    private function transformSearchOperation(Operator $operator) {
391
+        [, $trimmedType] = explode('}', $operator->type);
392
+        switch ($operator->type) {
393
+            case Operator::OPERATION_AND:
394
+            case Operator::OPERATION_OR:
395
+            case Operator::OPERATION_NOT:
396
+                $arguments = array_map([$this, 'transformSearchOperation'], $operator->arguments);
397
+                return new SearchBinaryOperator($trimmedType, $arguments);
398
+            case Operator::OPERATION_EQUAL:
399
+            case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
400
+            case Operator::OPERATION_GREATER_THAN:
401
+            case Operator::OPERATION_LESS_OR_EQUAL_THAN:
402
+            case Operator::OPERATION_LESS_THAN:
403
+            case Operator::OPERATION_IS_LIKE:
404
+                if (count($operator->arguments) !== 2) {
405
+                    throw new \InvalidArgumentException('Invalid number of arguments for ' . $trimmedType . ' operation');
406
+                }
407
+                if (!($operator->arguments[1] instanceof Literal)) {
408
+                    throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal');
409
+                }
410
+                $value = $operator->arguments[1]->value;
411
+                // no break
412
+            case Operator::OPERATION_IS_DEFINED:
413
+                if (!($operator->arguments[0] instanceof SearchPropertyDefinition)) {
414
+                    throw new \InvalidArgumentException('Invalid argument 1 for ' . $trimmedType . ' operation, expected property');
415
+                }
416
+                $property = $operator->arguments[0];
417
+
418
+                if (str_starts_with($property->name, FilesPlugin::FILE_METADATA_PREFIX)) {
419
+                    $field = substr($property->name, strlen(FilesPlugin::FILE_METADATA_PREFIX));
420
+                    $extra = IMetadataQuery::EXTRA;
421
+                } else {
422
+                    $field = $this->mapPropertyNameToColumn($property);
423
+                }
424
+
425
+                try {
426
+                    $castedValue = $this->castValue($property, $value ?? '');
427
+                } catch (\Error $e) {
428
+                    throw new \InvalidArgumentException('Invalid property value for ' . $property->name, previous: $e);
429
+                }
430
+
431
+                return new SearchComparison(
432
+                    $trimmedType,
433
+                    $field,
434
+                    $castedValue,
435
+                    $extra ?? ''
436
+                );
437
+
438
+            case Operator::OPERATION_IS_COLLECTION:
439
+                return new SearchComparison('eq', 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE);
440
+            default:
441
+                throw new \InvalidArgumentException('Unsupported operation ' . $trimmedType . ' (' . $operator->type . ')');
442
+        }
443
+    }
444
+
445
+    /**
446
+     * @param SearchPropertyDefinition $property
447
+     * @return string
448
+     */
449
+    private function mapPropertyNameToColumn(SearchPropertyDefinition $property) {
450
+        switch ($property->name) {
451
+            case '{DAV:}displayname':
452
+                return 'name';
453
+            case '{DAV:}getcontenttype':
454
+                return 'mimetype';
455
+            case '{DAV:}getlastmodified':
456
+                return 'mtime';
457
+            case FilesPlugin::SIZE_PROPERTYNAME:
458
+                return 'size';
459
+            case TagsPlugin::FAVORITE_PROPERTYNAME:
460
+                return 'favorite';
461
+            case TagsPlugin::TAGS_PROPERTYNAME:
462
+                return 'tagname';
463
+            case FilesPlugin::INTERNAL_FILEID_PROPERTYNAME:
464
+                return 'fileid';
465
+            default:
466
+                throw new \InvalidArgumentException('Unsupported property for search or order: ' . $property->name);
467
+        }
468
+    }
469
+
470
+    private function castValue(SearchPropertyDefinition $property, $value) {
471
+        if ($value === '') {
472
+            return '';
473
+        }
474
+
475
+        switch ($property->dataType) {
476
+            case SearchPropertyDefinition::DATATYPE_BOOLEAN:
477
+                return $value === 'yes';
478
+            case SearchPropertyDefinition::DATATYPE_DECIMAL:
479
+            case SearchPropertyDefinition::DATATYPE_INTEGER:
480
+            case SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER:
481
+                return 0 + $value;
482
+            case SearchPropertyDefinition::DATATYPE_DATETIME:
483
+                if (is_numeric($value)) {
484
+                    return max(0, 0 + $value);
485
+                }
486
+                $date = \DateTime::createFromFormat(\DateTimeInterface::ATOM, (string)$value);
487
+                return ($date instanceof \DateTime && $date->getTimestamp() !== false) ? $date->getTimestamp() : 0;
488
+            default:
489
+                return $value;
490
+        }
491
+    }
492
+
493
+    /**
494
+     * Get a specific property from the were clause
495
+     */
496
+    private function extractWhereValue(Operator &$operator, string $propertyName, string $comparison, bool $acceptableLocation = true): ?string {
497
+        switch ($operator->type) {
498
+            case Operator::OPERATION_AND:
499
+            case Operator::OPERATION_OR:
500
+            case Operator::OPERATION_NOT:
501
+                foreach ($operator->arguments as &$argument) {
502
+                    $value = $this->extractWhereValue($argument, $propertyName, $comparison, $acceptableLocation && $operator->type === Operator::OPERATION_AND);
503
+                    if ($value !== null) {
504
+                        return $value;
505
+                    }
506
+                }
507
+                return null;
508
+            case Operator::OPERATION_EQUAL:
509
+            case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
510
+            case Operator::OPERATION_GREATER_THAN:
511
+            case Operator::OPERATION_LESS_OR_EQUAL_THAN:
512
+            case Operator::OPERATION_LESS_THAN:
513
+            case Operator::OPERATION_IS_LIKE:
514
+                if ($operator->arguments[0]->name === $propertyName) {
515
+                    if ($operator->type === $comparison) {
516
+                        if ($acceptableLocation) {
517
+                            if ($operator->arguments[1] instanceof Literal) {
518
+                                $value = $operator->arguments[1]->value;
519
+
520
+                                // to remove the comparison from the query, we replace it with an empty AND
521
+                                $operator = new Operator(Operator::OPERATION_AND);
522
+
523
+                                return $value;
524
+                            } else {
525
+                                throw new \InvalidArgumentException("searching by '$propertyName' is only allowed with a literal value");
526
+                            }
527
+                        } else {
528
+                            throw new \InvalidArgumentException("searching by '$propertyName' is not allowed inside a '{DAV:}or' or '{DAV:}not'");
529
+                        }
530
+                    } else {
531
+                        throw new \InvalidArgumentException("searching by '$propertyName' is only allowed inside a '$comparison'");
532
+                    }
533
+                } else {
534
+                    return null;
535
+                }
536
+                // no break
537
+            default:
538
+                return null;
539
+        }
540
+    }
541 541
 }
Please login to merge, or discard this patch.
Spacing   +17 added lines, -17 removed lines patch added patch discarded remove patch
@@ -117,7 +117,7 @@  discard block
 block discarded – undo
117 117
 				default => SearchPropertyDefinition::DATATYPE_STRING
118 118
 			};
119 119
 			$metadataProps[] = new SearchPropertyDefinition(
120
-				FilesPlugin::FILE_METADATA_PREFIX . $key,
120
+				FilesPlugin::FILE_METADATA_PREFIX.$key,
121 121
 				true,
122 122
 				$isIndex,
123 123
 				$isIndex,
@@ -191,7 +191,7 @@  discard block
 block discarded – undo
191 191
 							new SearchComparison(
192 192
 								ISearchComparison::COMPARE_LIKE,
193 193
 								'path',
194
-								$internalPath . '/%',
194
+								$internalPath.'/%',
195 195
 								''
196 196
 							),
197 197
 						]
@@ -205,7 +205,7 @@  discard block
 block discarded – undo
205 205
 		}
206 206
 
207 207
 		/** @var SearchResult[] $nodes */
208
-		$nodes = array_map(function (Node $node) {
208
+		$nodes = array_map(function(Node $node) {
209 209
 			if ($node instanceof Folder) {
210 210
 				$davNode = new Directory($this->view, $node, $this->tree, $this->shareManager);
211 211
 			} else {
@@ -218,7 +218,7 @@  discard block
 block discarded – undo
218 218
 
219 219
 		if (!$query->limitToHome()) {
220 220
 			// Sort again, since the result from multiple storages is appended and not sorted
221
-			usort($nodes, function (SearchResult $a, SearchResult $b) use ($search) {
221
+			usort($nodes, function(SearchResult $a, SearchResult $b) use ($search) {
222 222
 				return $this->sort($a, $b, $search->orderBy);
223 223
 			});
224 224
 		}
@@ -308,8 +308,8 @@  discard block
 block discarded – undo
308 308
 	 * @return string
309 309
 	 */
310 310
 	private function getHrefForNode(Node $node) {
311
-		$base = '/files/' . $this->user->getUID();
312
-		return $base . $this->view->getRelativePath($node->getPath());
311
+		$base = '/files/'.$this->user->getUID();
312
+		return $base.$this->view->getRelativePath($node->getPath());
313 313
 	}
314 314
 
315 315
 	/**
@@ -318,7 +318,7 @@  discard block
 block discarded – undo
318 318
 	 * @return ISearchQuery
319 319
 	 */
320 320
 	private function transformQuery(Query $query, ?SearchBinaryOperator $scopeOperators = null): ISearchQuery {
321
-		$orders = array_map(function (Order $order): ISearchOrder {
321
+		$orders = array_map(function(Order $order): ISearchOrder {
322 322
 			$direction = $order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING;
323 323
 			if (str_starts_with($order->property->name, FilesPlugin::FILE_METADATA_PREFIX)) {
324 324
 				return new SearchOrder($direction, substr($order->property->name, strlen(FilesPlugin::FILE_METADATA_PREFIX)), IMetadataQuery::EXTRA);
@@ -342,7 +342,7 @@  discard block
 block discarded – undo
342 342
 
343 343
 		$operatorCount = $this->countSearchOperators($query->where);
344 344
 		if ($operatorCount > self::OPERATOR_LIMIT) {
345
-			throw new \InvalidArgumentException('Invalid search query, maximum operator limit of ' . self::OPERATOR_LIMIT . ' exceeded, got ' . $operatorCount . ' operators');
345
+			throw new \InvalidArgumentException('Invalid search query, maximum operator limit of '.self::OPERATOR_LIMIT.' exceeded, got '.$operatorCount.' operators');
346 346
 		}
347 347
 
348 348
 		/** @var SearchBinaryOperator|SearchComparison */
@@ -355,7 +355,7 @@  discard block
 block discarded – undo
355 355
 
356 356
 		return new SearchQuery(
357 357
 			$operators,
358
-			(int)$limit->maxResults,
358
+			(int) $limit->maxResults,
359 359
 			$offset,
360 360
 			$orders,
361 361
 			$this->user,
@@ -402,16 +402,16 @@  discard block
 block discarded – undo
402 402
 			case Operator::OPERATION_LESS_THAN:
403 403
 			case Operator::OPERATION_IS_LIKE:
404 404
 				if (count($operator->arguments) !== 2) {
405
-					throw new \InvalidArgumentException('Invalid number of arguments for ' . $trimmedType . ' operation');
405
+					throw new \InvalidArgumentException('Invalid number of arguments for '.$trimmedType.' operation');
406 406
 				}
407 407
 				if (!($operator->arguments[1] instanceof Literal)) {
408
-					throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal');
408
+					throw new \InvalidArgumentException('Invalid argument 2 for '.$trimmedType.' operation, expected literal');
409 409
 				}
410 410
 				$value = $operator->arguments[1]->value;
411 411
 				// no break
412 412
 			case Operator::OPERATION_IS_DEFINED:
413 413
 				if (!($operator->arguments[0] instanceof SearchPropertyDefinition)) {
414
-					throw new \InvalidArgumentException('Invalid argument 1 for ' . $trimmedType . ' operation, expected property');
414
+					throw new \InvalidArgumentException('Invalid argument 1 for '.$trimmedType.' operation, expected property');
415 415
 				}
416 416
 				$property = $operator->arguments[0];
417 417
 
@@ -425,7 +425,7 @@  discard block
 block discarded – undo
425 425
 				try {
426 426
 					$castedValue = $this->castValue($property, $value ?? '');
427 427
 				} catch (\Error $e) {
428
-					throw new \InvalidArgumentException('Invalid property value for ' . $property->name, previous: $e);
428
+					throw new \InvalidArgumentException('Invalid property value for '.$property->name, previous: $e);
429 429
 				}
430 430
 
431 431
 				return new SearchComparison(
@@ -438,7 +438,7 @@  discard block
 block discarded – undo
438 438
 			case Operator::OPERATION_IS_COLLECTION:
439 439
 				return new SearchComparison('eq', 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE);
440 440
 			default:
441
-				throw new \InvalidArgumentException('Unsupported operation ' . $trimmedType . ' (' . $operator->type . ')');
441
+				throw new \InvalidArgumentException('Unsupported operation '.$trimmedType.' ('.$operator->type.')');
442 442
 		}
443 443
 	}
444 444
 
@@ -463,7 +463,7 @@  discard block
 block discarded – undo
463 463
 			case FilesPlugin::INTERNAL_FILEID_PROPERTYNAME:
464 464
 				return 'fileid';
465 465
 			default:
466
-				throw new \InvalidArgumentException('Unsupported property for search or order: ' . $property->name);
466
+				throw new \InvalidArgumentException('Unsupported property for search or order: '.$property->name);
467 467
 		}
468 468
 	}
469 469
 
@@ -483,7 +483,7 @@  discard block
 block discarded – undo
483 483
 				if (is_numeric($value)) {
484 484
 					return max(0, 0 + $value);
485 485
 				}
486
-				$date = \DateTime::createFromFormat(\DateTimeInterface::ATOM, (string)$value);
486
+				$date = \DateTime::createFromFormat(\DateTimeInterface::ATOM, (string) $value);
487 487
 				return ($date instanceof \DateTime && $date->getTimestamp() !== false) ? $date->getTimestamp() : 0;
488 488
 			default:
489 489
 				return $value;
@@ -493,7 +493,7 @@  discard block
 block discarded – undo
493 493
 	/**
494 494
 	 * Get a specific property from the were clause
495 495
 	 */
496
-	private function extractWhereValue(Operator &$operator, string $propertyName, string $comparison, bool $acceptableLocation = true): ?string {
496
+	private function extractWhereValue(Operator & $operator, string $propertyName, string $comparison, bool $acceptableLocation = true): ?string {
497 497
 		switch ($operator->type) {
498 498
 			case Operator::OPERATION_AND:
499 499
 			case Operator::OPERATION_OR:
Please login to merge, or discard this patch.