Completed
Push — master ( 8d91c8...8217b1 )
by Robin
44s queued 10s
created

FileSearchBackend::mapSearchOrder()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2017 Robin Appelman <[email protected]>
4
 *
5
 * @license GNU AGPL version 3 or any later version
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Affero General Public License as
9
 * published by the Free Software Foundation, either version 3 of the
10
 * License, or (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License
18
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
 *
20
 */
21
22
namespace OCA\DAV\Files;
23
24
use OC\Files\Search\SearchBinaryOperator;
25
use OC\Files\Search\SearchComparison;
26
use OC\Files\Search\SearchOrder;
27
use OC\Files\Search\SearchQuery;
28
use OC\Files\View;
29
use OCA\DAV\Connector\Sabre\Directory;
30
use OCA\DAV\Connector\Sabre\FilesPlugin;
31
use OCA\DAV\Connector\Sabre\TagsPlugin;
32
use OCP\Files\Cache\ICacheEntry;
33
use OCP\Files\Folder;
34
use OCP\Files\IRootFolder;
35
use OCP\Files\Node;
36
use OCP\Files\Search\ISearchOperator;
37
use OCP\Files\Search\ISearchOrder;
38
use OCP\Files\Search\ISearchQuery;
39
use OCP\IUser;
40
use OCP\Share\IManager;
41
use Sabre\DAV\Exception\NotFound;
42
use Sabre\DAV\Tree;
43
use SearchDAV\Backend\ISearchBackend;
44
use SearchDAV\Backend\SearchPropertyDefinition;
45
use SearchDAV\Backend\SearchResult;
46
use SearchDAV\XML\BasicSearch;
47
use SearchDAV\XML\Literal;
48
use SearchDAV\XML\Operator;
49
use SearchDAV\XML\Order;
50
51
class FileSearchBackend implements ISearchBackend {
52
	/** @var Tree */
53
	private $tree;
54
55
	/** @var IUser */
56
	private $user;
57
58
	/** @var IRootFolder */
59
	private $rootFolder;
60
61
	/** @var IManager */
62
	private $shareManager;
63
64
	/** @var View */
65
	private $view;
66
67
	/**
68
	 * FileSearchBackend constructor.
69
	 *
70
	 * @param Tree $tree
71
	 * @param IUser $user
72
	 * @param IRootFolder $rootFolder
73
	 * @param IManager $shareManager
74
	 * @param View $view
75
	 * @internal param IRootFolder $rootFolder
76
	 */
77
	public function __construct(Tree $tree, IUser $user, IRootFolder $rootFolder, IManager $shareManager, View $view) {
78
		$this->tree = $tree;
79
		$this->user = $user;
80
		$this->rootFolder = $rootFolder;
81
		$this->shareManager = $shareManager;
82
		$this->view = $view;
83
	}
84
85
	/**
86
	 * Search endpoint will be remote.php/dav
87
	 *
88
	 * @return string
89
	 */
90
	public function getArbiterPath() {
91
		return '';
92
	}
93
94
	public function isValidScope($href, $depth, $path) {
95
		// only allow scopes inside the dav server
96
		if (is_null($path)) {
97
			return false;
98
		}
99
100
		try {
101
			$node = $this->tree->getNodeForPath($path);
102
			return $node instanceof Directory;
103
		} 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...
104
			return false;
105
		}
106
	}
107
108
	public function getPropertyDefinitionsForScope($href, $path) {
109
		// all valid scopes support the same schema
110
111
		//todo dynamically load all propfind properties that are supported
112
		return [
113
			// queryable properties
114
			new SearchPropertyDefinition('{DAV:}displayname', true, false, true),
115
			new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
116
			new SearchPropertyDefinition('{DAV:}getlastmodified', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
117
			new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
118
			new SearchPropertyDefinition(TagsPlugin::FAVORITE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_BOOLEAN),
119
120
			// select only properties
121
			new SearchPropertyDefinition('{DAV:}resourcetype', false, true, false),
122
			new SearchPropertyDefinition('{DAV:}getcontentlength', false, true, false),
123
			new SearchPropertyDefinition(FilesPlugin::CHECKSUMS_PROPERTYNAME, false, true, false),
124
			new SearchPropertyDefinition(FilesPlugin::PERMISSIONS_PROPERTYNAME, false, true, false),
125
			new SearchPropertyDefinition(FilesPlugin::GETETAG_PROPERTYNAME, false, true, false),
126
			new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, false, true, false),
127
			new SearchPropertyDefinition(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, false, true, false),
128
			new SearchPropertyDefinition(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, false, true, false),
129
			new SearchPropertyDefinition(FilesPlugin::HAS_PREVIEW_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_BOOLEAN),
130
			new SearchPropertyDefinition(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
131
			new SearchPropertyDefinition(FilesPlugin::FILEID_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
132
		];
133
	}
134
135
	/**
136
	 * @param BasicSearch $search
137
	 * @return SearchResult[]
138
	 */
139
	public function search(BasicSearch $search) {
140
		if (count($search->from) !== 1) {
141
			throw new \InvalidArgumentException('Searching more than one folder is not supported');
142
		}
143
		$query = $this->transformQuery($search);
144
		$scope = $search->from[0];
145
		if ($scope->path === null) {
146
			throw new \InvalidArgumentException('Using uri\'s as scope is not supported, please use a path relative to the search arbiter instead');
147
		}
148
		$node = $this->tree->getNodeForPath($scope->path);
149
		if (!$node instanceof Directory) {
150
			throw new \InvalidArgumentException('Search is only supported on directories');
151
		}
152
153
		$fileInfo = $node->getFileInfo();
154
		$folder = $this->rootFolder->get($fileInfo->getPath());
155
		/** @var Folder $folder $results */
156
		$results = $folder->search($query);
157
158
		return array_map(function (Node $node) {
159
			if ($node instanceof Folder) {
160
				return new SearchResult(new \OCA\DAV\Connector\Sabre\Directory($this->view, $node, $this->tree, $this->shareManager), $this->getHrefForNode($node));
161
			} else {
162
				return new SearchResult(new \OCA\DAV\Connector\Sabre\File($this->view, $node, $this->shareManager), $this->getHrefForNode($node));
163
			}
164
		}, $results);
165
	}
166
167
	/**
168
	 * @param Node $node
169
	 * @return string
170
	 */
171
	private function getHrefForNode(Node $node) {
172
		$base = '/files/' . $this->user->getUID();
173
		return $base . $this->view->getRelativePath($node->getPath());
174
	}
175
176
	/**
177
	 * @param BasicSearch $query
178
	 * @return ISearchQuery
179
	 */
180
	private function transformQuery(BasicSearch $query) {
181
		// TODO offset, limit
182
		$orders = array_map([$this, 'mapSearchOrder'], $query->orderBy);
183
		return new SearchQuery($this->transformSearchOperation($query->where), 0, 0, $orders, $this->user);
184
	}
185
186
	/**
187
	 * @param Order $order
188
	 * @return ISearchOrder
189
	 */
190
	private function mapSearchOrder(Order $order) {
191
		return new SearchOrder($order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING, $this->mapPropertyNameToColumn($order->property));
192
	}
193
194
	/**
195
	 * @param Operator $operator
196
	 * @return ISearchOperator
197
	 */
198
	private function transformSearchOperation(Operator $operator) {
199
		list(, $trimmedType) = explode('}', $operator->type);
200
		switch ($operator->type) {
201
			case Operator::OPERATION_AND:
202
			case Operator::OPERATION_OR:
203
			case Operator::OPERATION_NOT:
204
				$arguments = array_map([$this, 'transformSearchOperation'], $operator->arguments);
205
				return new SearchBinaryOperator($trimmedType, $arguments);
206
			case Operator::OPERATION_EQUAL:
207
			case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
208
			case Operator::OPERATION_GREATER_THAN:
209
			case Operator::OPERATION_LESS_OR_EQUAL_THAN:
210
			case Operator::OPERATION_LESS_THAN:
211
			case Operator::OPERATION_IS_LIKE:
212
				if (count($operator->arguments) !== 2) {
213
					throw new \InvalidArgumentException('Invalid number of arguments for ' . $trimmedType . ' operation');
214
				}
215 View Code Duplication
				if (!is_string($operator->arguments[0])) {
216
					throw new \InvalidArgumentException('Invalid argument 1 for ' . $trimmedType . ' operation, expected property');
217
				}
218 View Code Duplication
				if (!($operator->arguments[1] instanceof Literal)) {
0 ignored issues
show
Bug introduced by
The class SearchDAV\XML\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...
219
					throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal');
220
				}
221
				return new SearchComparison($trimmedType, $this->mapPropertyNameToColumn($operator->arguments[0]), $this->castValue($operator->arguments[0], $operator->arguments[1]->value));
222
			case Operator::OPERATION_IS_COLLECTION:
223
				return new SearchComparison('eq', 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE);
224
			default:
225
				throw new \InvalidArgumentException('Unsupported operation ' . $trimmedType.  ' (' . $operator->type . ')');
226
		}
227
	}
228
229
	/**
230
	 * @param string $propertyName
231
	 * @return string
232
	 */
233
	private function mapPropertyNameToColumn($propertyName) {
234
		switch ($propertyName) {
235
			case '{DAV:}displayname':
236
				return 'name';
237
			case '{DAV:}getcontenttype':
238
				return 'mimetype';
239
			case '{DAV:}getlastmodified':
240
				return 'mtime';
241
			case FilesPlugin::SIZE_PROPERTYNAME:
242
				return 'size';
243
			case TagsPlugin::FAVORITE_PROPERTYNAME:
244
				return 'favorite';
245
			case TagsPlugin::TAGS_PROPERTYNAME:
246
				return 'tagname';
247
			default:
248
				throw new \InvalidArgumentException('Unsupported property for search or order: ' . $propertyName);
249
		}
250
	}
251
252
	private function castValue($propertyName, $value) {
253
		$allProps = $this->getPropertyDefinitionsForScope('', '');
254
		foreach ($allProps as $prop) {
255
			if ($prop->name === $propertyName) {
256
				$dataType = $prop->dataType;
257
				switch ($dataType) {
258
					case SearchPropertyDefinition::DATATYPE_BOOLEAN:
259
						return $value === 'yes';
260
					case SearchPropertyDefinition::DATATYPE_DECIMAL:
261
					case SearchPropertyDefinition::DATATYPE_INTEGER:
262
					case SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER:
263
						return 0 + $value;
264
					case SearchPropertyDefinition::DATATYPE_DATETIME:
265
						if (is_numeric($value)) {
266
							return 0 + $value;
267
						}
268
						$date = \DateTime::createFromFormat(\DateTime::ATOM, $value);
269
						return ($date instanceof  \DateTime) ? $date->getTimestamp() : 0;
270
					default:
271
						return $value;
272
				}
273
			}
274
		}
275
		return $value;
276
	}
277
}
278