Completed
Push — master ( b93938...e5c086 )
by Morris
15:45
created

FileSearchBackend::transformSearchOperation()   C

Complexity

Conditions 14
Paths 29

Size

Total Lines 30
Code Lines 25

Duplication

Lines 6
Ratio 20 %

Importance

Changes 0
Metric Value
cc 14
eloc 25
nc 29
nop 1
dl 6
loc 30
rs 5.0864
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @copyright Copyright (c) 2017 Robin Appelman <[email protected]>
4
 *
5
 * @author Robin Appelman <[email protected]>
6
 *
7
 * @license GNU AGPL version 3 or any later version
8
 *
9
 * This program is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License as
11
 * published by the Free Software Foundation, either version 3 of the
12
 * License, or (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU Affero General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Affero General Public License
20
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
 *
22
 */
23
24
namespace OCA\DAV\Files;
25
26
use OC\Files\Search\SearchBinaryOperator;
27
use OC\Files\Search\SearchComparison;
28
use OC\Files\Search\SearchOrder;
29
use OC\Files\Search\SearchQuery;
30
use OC\Files\View;
31
use OCA\DAV\Connector\Sabre\CachingTree;
32
use OCA\DAV\Connector\Sabre\Directory;
33
use OCA\DAV\Connector\Sabre\FilesPlugin;
34
use OCA\DAV\Connector\Sabre\TagsPlugin;
35
use OCP\Files\Cache\ICacheEntry;
36
use OCP\Files\Folder;
37
use OCP\Files\IRootFolder;
38
use OCP\Files\Node;
39
use OCP\Files\Search\ISearchOperator;
40
use OCP\Files\Search\ISearchOrder;
41
use OCP\Files\Search\ISearchQuery;
42
use OCP\IUser;
43
use OCP\Share\IManager;
44
use Sabre\DAV\Exception\NotFound;
45
use SearchDAV\Backend\ISearchBackend;
46
use SearchDAV\Backend\SearchPropertyDefinition;
47
use SearchDAV\Backend\SearchResult;
48
use SearchDAV\Query\Query;
49
use SearchDAV\Query\Literal;
50
use SearchDAV\Query\Operator;
51
use SearchDAV\Query\Order;
52
53
class FileSearchBackend implements ISearchBackend {
54
	/** @var CachingTree */
55
	private $tree;
56
57
	/** @var IUser */
58
	private $user;
59
60
	/** @var IRootFolder */
61
	private $rootFolder;
62
63
	/** @var IManager */
64
	private $shareManager;
65
66
	/** @var View */
67
	private $view;
68
69
	/**
70
	 * FileSearchBackend constructor.
71
	 *
72
	 * @param CachingTree $tree
73
	 * @param IUser $user
74
	 * @param IRootFolder $rootFolder
75
	 * @param IManager $shareManager
76
	 * @param View $view
77
	 * @internal param IRootFolder $rootFolder
78
	 */
79
	public function __construct(CachingTree $tree, IUser $user, IRootFolder $rootFolder, IManager $shareManager, View $view) {
80
		$this->tree = $tree;
81
		$this->user = $user;
82
		$this->rootFolder = $rootFolder;
83
		$this->shareManager = $shareManager;
84
		$this->view = $view;
85
	}
86
87
	/**
88
	 * Search endpoint will be remote.php/dav
89
	 *
90
	 * @return string
91
	 */
92
	public function getArbiterPath() {
93
		return '';
94
	}
95
96
	public function isValidScope($href, $depth, $path) {
97
		// only allow scopes inside the dav server
98
		if (is_null($path)) {
99
			return false;
100
		}
101
102
		try {
103
			$node = $this->tree->getNodeForPath($path);
104
			return $node instanceof Directory;
105
		} catch (NotFound $e) {
0 ignored issues
show
Bug introduced by
The class Sabre\DAV\Exception\NotFound does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
106
			return false;
107
		}
108
	}
109
110
	public function getPropertyDefinitionsForScope($href, $path) {
111
		// all valid scopes support the same schema
112
113
		//todo dynamically load all propfind properties that are supported
114
		return [
115
			// queryable properties
116
			new SearchPropertyDefinition('{DAV:}displayname', true, false, true),
117
			new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
118
			new SearchPropertyDefinition('{DAV:}getlastmodified', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
119
			new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
120
			new SearchPropertyDefinition(TagsPlugin::FAVORITE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_BOOLEAN),
121
			new SearchPropertyDefinition(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME, true, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
122
123
			// select only properties
124
			new SearchPropertyDefinition('{DAV:}resourcetype', false, true, false),
125
			new SearchPropertyDefinition('{DAV:}getcontentlength', false, true, false),
126
			new SearchPropertyDefinition(FilesPlugin::CHECKSUMS_PROPERTYNAME, false, true, false),
127
			new SearchPropertyDefinition(FilesPlugin::PERMISSIONS_PROPERTYNAME, false, true, false),
128
			new SearchPropertyDefinition(FilesPlugin::GETETAG_PROPERTYNAME, false, true, false),
129
			new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, false, true, false),
130
			new SearchPropertyDefinition(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, false, true, false),
131
			new SearchPropertyDefinition(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, false, true, false),
132
			new SearchPropertyDefinition(FilesPlugin::HAS_PREVIEW_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_BOOLEAN),
133
			new SearchPropertyDefinition(FilesPlugin::FILEID_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
134
		];
135
	}
136
137
	/**
138
	 * @param Query $search
139
	 * @return SearchResult[]
140
	 */
141
	public function search(Query $search) {
142
		if (count($search->from) !== 1) {
143
			throw new \InvalidArgumentException('Searching more than one folder is not supported');
144
		}
145
		$query = $this->transformQuery($search);
146
		$scope = $search->from[0];
147
		if ($scope->path === null) {
148
			throw new \InvalidArgumentException('Using uri\'s as scope is not supported, please use a path relative to the search arbiter instead');
149
		}
150
		$node = $this->tree->getNodeForPath($scope->path);
151
		if (!$node instanceof Directory) {
152
			throw new \InvalidArgumentException('Search is only supported on directories');
153
		}
154
155
		$fileInfo = $node->getFileInfo();
156
		$folder = $this->rootFolder->get($fileInfo->getPath());
157
		/** @var Folder $folder $results */
158
		$results = $folder->search($query);
159
160
		/** @var SearchResult[] $nodes */
161
		$nodes = array_map(function (Node $node) {
162 View Code Duplication
			if ($node instanceof Folder) {
163
				$davNode = new \OCA\DAV\Connector\Sabre\Directory($this->view, $node, $this->tree, $this->shareManager);
164
			} else {
165
				$davNode = new \OCA\DAV\Connector\Sabre\File($this->view, $node, $this->shareManager);
166
			}
167
			$path = $this->getHrefForNode($node);
168
			$this->tree->cacheNode($davNode, $path);
169
			return new SearchResult($davNode, $path);
170
		}, $results);
171
172
		// Sort again, since the result from multiple storages is appended and not sorted
173
		usort($nodes, function (SearchResult $a, SearchResult $b) use ($search) {
174
			return $this->sort($a, $b, $search->orderBy);
175
		});
176
177
		// If a limit is provided use only return that number of files
178
		if ($search->limit->maxResults !== 0) {
179
			$nodes = \array_slice($nodes, 0, $search->limit->maxResults);
180
		}
181
182
		return $nodes;
183
	}
184
185
	private function sort(SearchResult $a, SearchResult $b, array $orders) {
186
		/** @var Order $order */
187
		foreach ($orders as $order) {
188
			$v1 = $this->getSearchResultProperty($a, $order->property);
189
			$v2 = $this->getSearchResultProperty($b, $order->property);
190
191
192
			if ($v1 === null && $v2 === null) {
193
				continue;
194
			}
195
			if ($v1 === null) {
196
				return $order->order === Order::ASC ? 1 : -1;
197
			}
198
			if ($v2 === null) {
199
				return $order->order === Order::ASC ? -1 : 1;
200
			}
201
202
			$s = $this->compareProperties($v1, $v2, $order);
203
			if ($s === 0) {
204
				continue;
205
			}
206
207
			if ($order->order === Order::DESC) {
208
				$s = -$s;
209
			}
210
			return $s;
211
		}
212
213
		return 0;
214
	}
215
216
	private function compareProperties($a, $b, Order $order) {
217
		switch ($order->property->dataType) {
218
			case SearchPropertyDefinition::DATATYPE_STRING:
219
				return strcmp($a, $b);
220 View Code Duplication
			case SearchPropertyDefinition::DATATYPE_BOOLEAN:
221
				if ($a === $b) {
222
					return 0;
223
				}
224
				if ($a === false) {
225
					return -1;
226
				}
227
				return 1;
228 View Code Duplication
			default:
229
				if ($a === $b) {
230
					return 0;
231
				}
232
				if ($a < $b) {
233
					return -1;
234
				}
235
				return 1;
236
		}
237
	}
238
239
	private function getSearchResultProperty(SearchResult $result, SearchPropertyDefinition $property) {
240
		/** @var \OCA\DAV\Connector\Sabre\Node $node */
241
		$node = $result->node;
242
243
		switch ($property->name) {
244
			case '{DAV:}displayname':
245
				return $node->getName();
0 ignored issues
show
Bug introduced by
Consider using $node->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
246
			case '{DAV:}getlastmodified':
247
				return $node->getLastModified();
248
			case FilesPlugin::SIZE_PROPERTYNAME:
249
				return $node->getSize();
250
			case FilesPlugin::INTERNAL_FILEID_PROPERTYNAME:
251
				return $node->getInternalFileId();
252
			default:
253
				return null;
254
		}
255
	}
256
257
	/**
258
	 * @param Node $node
259
	 * @return string
260
	 */
261
	private function getHrefForNode(Node $node) {
262
		$base = '/files/' . $this->user->getUID();
263
		return $base . $this->view->getRelativePath($node->getPath());
264
	}
265
266
	/**
267
	 * @param Query $query
268
	 * @return ISearchQuery
269
	 */
270
	private function transformQuery(Query $query) {
271
		// TODO offset
272
		$limit = $query->limit;
273
		$orders = array_map([$this, 'mapSearchOrder'], $query->orderBy);
274
		return new SearchQuery($this->transformSearchOperation($query->where), (int)$limit->maxResults, 0, $orders, $this->user);
275
	}
276
277
	/**
278
	 * @param Order $order
279
	 * @return ISearchOrder
280
	 */
281
	private function mapSearchOrder(Order $order) {
282
		return new SearchOrder($order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING, $this->mapPropertyNameToColumn($order->property));
283
	}
284
285
	/**
286
	 * @param Operator $operator
287
	 * @return ISearchOperator
288
	 */
289
	private function transformSearchOperation(Operator $operator) {
290
		list(, $trimmedType) = explode('}', $operator->type);
291
		switch ($operator->type) {
292
			case Operator::OPERATION_AND:
293
			case Operator::OPERATION_OR:
294
			case Operator::OPERATION_NOT:
295
				$arguments = array_map([$this, 'transformSearchOperation'], $operator->arguments);
296
				return new SearchBinaryOperator($trimmedType, $arguments);
297
			case Operator::OPERATION_EQUAL:
298
			case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
299
			case Operator::OPERATION_GREATER_THAN:
300
			case Operator::OPERATION_LESS_OR_EQUAL_THAN:
301
			case Operator::OPERATION_LESS_THAN:
302
			case Operator::OPERATION_IS_LIKE:
303
				if (count($operator->arguments) !== 2) {
304
					throw new \InvalidArgumentException('Invalid number of arguments for ' . $trimmedType . ' operation');
305
				}
306 View Code Duplication
				if (!($operator->arguments[0] instanceof SearchPropertyDefinition)) {
0 ignored issues
show
Bug introduced by
The class SearchDAV\Backend\SearchPropertyDefinition does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
307
					throw new \InvalidArgumentException('Invalid argument 1 for ' . $trimmedType . ' operation, expected property');
308
				}
309 View Code Duplication
				if (!($operator->arguments[1] instanceof Literal)) {
0 ignored issues
show
Bug introduced by
The class SearchDAV\Query\Literal does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
310
					throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal');
311
				}
312
				return new SearchComparison($trimmedType, $this->mapPropertyNameToColumn($operator->arguments[0]), $this->castValue($operator->arguments[0], $operator->arguments[1]->value));
313
			case Operator::OPERATION_IS_COLLECTION:
314
				return new SearchComparison('eq', 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE);
315
			default:
316
				throw new \InvalidArgumentException('Unsupported operation ' . $trimmedType . ' (' . $operator->type . ')');
317
		}
318
	}
319
320
	/**
321
	 * @param SearchPropertyDefinition $property
322
	 * @return string
323
	 */
324
	private function mapPropertyNameToColumn(SearchPropertyDefinition $property) {
325
		switch ($property->name) {
326
			case '{DAV:}displayname':
327
				return 'name';
328
			case '{DAV:}getcontenttype':
329
				return 'mimetype';
330
			case '{DAV:}getlastmodified':
331
				return 'mtime';
332
			case FilesPlugin::SIZE_PROPERTYNAME:
333
				return 'size';
334
			case TagsPlugin::FAVORITE_PROPERTYNAME:
335
				return 'favorite';
336
			case TagsPlugin::TAGS_PROPERTYNAME:
337
				return 'tagname';
338
			case FilesPlugin::INTERNAL_FILEID_PROPERTYNAME:
339
				return 'fileid';
340
			default:
341
				throw new \InvalidArgumentException('Unsupported property for search or order: ' . $property->name);
342
		}
343
	}
344
345
	private function castValue(SearchPropertyDefinition $property, $value) {
346
		switch ($property->dataType) {
347
			case SearchPropertyDefinition::DATATYPE_BOOLEAN:
348
				return $value === 'yes';
349
			case SearchPropertyDefinition::DATATYPE_DECIMAL:
350
			case SearchPropertyDefinition::DATATYPE_INTEGER:
351
			case SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER:
352
				return 0 + $value;
353
			case SearchPropertyDefinition::DATATYPE_DATETIME:
354
				if (is_numeric($value)) {
355
					return 0 + $value;
356
				}
357
				$date = \DateTime::createFromFormat(\DateTime::ATOM, $value);
358
				return ($date instanceof \DateTime) ? $date->getTimestamp() : 0;
359
			default:
360
				return $value;
361
		}
362
	}
363
}
364