Completed
Push — master ( 74ac5d...2a8e92 )
by Robin
24:33
created

FileSearchBackend::mapPropertyNameToCollumn()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 14
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 12
nc 5
nop 1
dl 0
loc 14
rs 8.8571
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 OCP\Files\Cache\ICacheEntry;
32
use OCP\Files\Folder;
33
use OCP\Files\IRootFolder;
34
use OCP\Files\Node;
35
use OCP\Files\Search\ISearchOperator;
36
use OCP\Files\Search\ISearchOrder;
37
use OCP\Files\Search\ISearchQuery;
38
use OCP\IUser;
39
use OCP\Share\IManager;
40
use Sabre\DAV\Exception\NotFound;
41
use Sabre\DAV\Tree;
42
use SearchDAV\Backend\ISearchBackend;
43
use SearchDAV\Backend\SearchPropertyDefinition;
44
use SearchDAV\Backend\SearchResult;
45
use SearchDAV\XML\BasicSearch;
46
use SearchDAV\XML\Literal;
47
use SearchDAV\XML\Operator;
48
use SearchDAV\XML\Order;
49
50
class FileSearchBackend implements ISearchBackend {
51
	/** @var Tree */
52
	private $tree;
53
54
	/** @var IUser */
55
	private $user;
56
57
	/** @var IRootFolder */
58
	private $rootFolder;
59
60
	/** @var IManager */
61
	private $shareManager;
62
63
	/** @var View */
64
	private $view;
65
66
	/**
67
	 * FileSearchBackend constructor.
68
	 *
69
	 * @param Tree $tree
70
	 * @param IUser $user
71
	 * @param IRootFolder $rootFolder
72
	 * @param IManager $shareManager
73
	 * @param View $view
74
	 * @internal param IRootFolder $rootFolder
75
	 */
76
	public function __construct(Tree $tree, IUser $user, IRootFolder $rootFolder, IManager $shareManager, View $view) {
77
		$this->tree = $tree;
78
		$this->user = $user;
79
		$this->rootFolder = $rootFolder;
80
		$this->shareManager = $shareManager;
81
		$this->view = $view;
82
	}
83
84
	/**
85
	 * Search endpoint will be remote.php/dav
86
	 *
87
	 * @return string
88
	 */
89
	public function getArbiterPath() {
90
		return '';
91
	}
92
93
	public function isValidScope($href, $depth, $path) {
94
		// only allow scopes inside the dav server
95
		if (is_null($path)) {
96
			return false;
97
		}
98
99
		try {
100
			$node = $this->tree->getNodeForPath($path);
101
			return $node instanceof Directory;
102
		} 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...
103
			return false;
104
		}
105
	}
106
107
	public function getPropertyDefinitionsForScope($href, $path) {
108
		// all valid scopes support the same schema
109
110
		//todo dynamically load all propfind properties that are supported
111
		return [
112
			// queryable properties
113
			new SearchPropertyDefinition('{DAV:}displayname', true, false, true),
114
			new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
115
			new SearchPropertyDefinition('{DAV:}getlastmodifed', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
116
			new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
117
118
			// select only properties
119
			new SearchPropertyDefinition('{DAV:}resourcetype', false, true, false),
120
			new SearchPropertyDefinition('{DAV:}getcontentlength', false, true, false),
121
			new SearchPropertyDefinition(FilesPlugin::CHECKSUMS_PROPERTYNAME, false, true, false),
122
			new SearchPropertyDefinition(FilesPlugin::PERMISSIONS_PROPERTYNAME, false, true, false),
123
			new SearchPropertyDefinition(FilesPlugin::GETETAG_PROPERTYNAME, false, true, false),
124
			new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, false, true, false),
125
			new SearchPropertyDefinition(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, false, true, false),
126
			new SearchPropertyDefinition(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, false, true, false),
127
			new SearchPropertyDefinition(FilesPlugin::HAS_PREVIEW_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_BOOLEAN),
128
			new SearchPropertyDefinition(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
129
			new SearchPropertyDefinition(FilesPlugin::FILEID_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
130
		];
131
	}
132
133
	/**
134
	 * @param BasicSearch $search
135
	 * @return SearchResult[]
136
	 */
137
	public function search(BasicSearch $search) {
138
		if (count($search->from) !== 1) {
139
			throw new \InvalidArgumentException('Searching more than one folder is not supported');
140
		}
141
		$query = $this->transformQuery($search);
142
		$scope = $search->from[0];
143
		if ($scope->path === null) {
144
			throw new \InvalidArgumentException('Using uri\'s as scope is not supported, please use a path relative to the search arbiter instead');
145
		}
146
		$node = $this->tree->getNodeForPath($scope->path);
147
		if (!$node instanceof Directory) {
148
			throw new \InvalidArgumentException('Search is only supported on directories');
149
		}
150
151
		$fileInfo = $node->getFileInfo();
152
		$folder = $this->rootFolder->get($fileInfo->getPath());
153
		/** @var Folder $folder $results */
154
		$results = $folder->search($query);
155
156
		return array_map(function (Node $node) {
157
			if ($node instanceof Folder) {
158
				return new SearchResult(new \OCA\DAV\Connector\Sabre\Directory($this->view, $node, $this->tree, $this->shareManager), $this->getHrefForNode($node));
159
			} else {
160
				return new SearchResult(new \OCA\DAV\Connector\Sabre\File($this->view, $node, $this->shareManager), $this->getHrefForNode($node));
161
			}
162
		}, $results);
163
	}
164
165
	/**
166
	 * @param Node $node
167
	 * @return string
168
	 */
169
	private function getHrefForNode(Node $node) {
170
		$base = '/files/' . $this->user->getUID();
171
		return $base . $this->view->getRelativePath($node->getPath());
172
	}
173
174
	/**
175
	 * @param BasicSearch $query
176
	 * @return ISearchQuery
177
	 */
178
	private function transformQuery(BasicSearch $query) {
179
		// TODO offset, limit
180
		$orders = array_map([$this, 'mapSearchOrder'], $query->orderBy);
181
		return new SearchQuery($this->transformSearchOperation($query->where), 0, 0, $orders);
182
	}
183
184
	/**
185
	 * @param Order $order
186
	 * @return ISearchOrder
187
	 */
188
	private function mapSearchOrder(Order $order) {
189
		return new SearchOrder($order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING, $this->mapPropertyNameToCollumn($order->property));
190
	}
191
192
	/**
193
	 * @param Operator $operator
194
	 * @return ISearchOperator
195
	 */
196
	private function transformSearchOperation(Operator $operator) {
197
		list(, $trimmedType) = explode('}', $operator->type);
198
		switch ($operator->type) {
199
			case Operator::OPERATION_AND:
200
			case Operator::OPERATION_OR:
201
			case Operator::OPERATION_NOT:
202
				$arguments = array_map([$this, 'transformSearchOperation'], $operator->arguments);
203
				return new SearchBinaryOperator($trimmedType, $arguments);
204
			case Operator::OPERATION_EQUAL:
205
			case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
206
			case Operator::OPERATION_GREATER_THAN:
207
			case Operator::OPERATION_LESS_OR_EQUAL_THAN:
208
			case Operator::OPERATION_LESS_THAN:
209
			case Operator::OPERATION_IS_LIKE:
210
				if (count($operator->arguments) !== 2) {
211
					throw new \InvalidArgumentException('Invalid number of arguments for ' . $trimmedType . ' operation');
212
				}
213
				if (gettype($operator->arguments[0]) !== 'string') {
214
					throw new \InvalidArgumentException('Invalid argument 1 for ' . $trimmedType . ' operation, expected property');
215
				}
216
				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...
217
					throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal');
218
				}
219
				return new SearchComparison($trimmedType, $this->mapPropertyNameToCollumn($operator->arguments[0]), $this->castValue($operator->arguments[0], $operator->arguments[1]->value));
220
			case Operator::OPERATION_IS_COLLECTION:
221
				return new SearchComparison('eq', 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE);
222
			default:
223
				throw new \InvalidArgumentException('Unsupported operation ' . $trimmedType.  ' (' . $operator->type . ')');
224
		}
225
	}
226
227
	/**
228
	 * @param string $propertyName
229
	 * @return string
230
	 */
231
	private function mapPropertyNameToCollumn($propertyName) {
232
		switch ($propertyName) {
233
			case '{DAV:}displayname':
234
				return 'name';
235
			case '{DAV:}getcontenttype':
236
				return 'mimetype';
237
			case '{DAV:}getlastmodifed':
238
				return 'mtime';
239
			case FilesPlugin::SIZE_PROPERTYNAME:
240
				return 'size';
241
			default:
242
				throw new \InvalidArgumentException('Unsupported property for search or order: ' . $propertyName);
243
		}
244
	}
245
246
	private function castValue($propertyName, $value) {
247
		$allProps = $this->getPropertyDefinitionsForScope('', '');
248
		foreach ($allProps as $prop) {
249
			if ($prop->name === $propertyName) {
250
				$dataType = $prop->dataType;
251
				switch ($dataType) {
252
					case SearchPropertyDefinition::DATATYPE_BOOLEAN:
253
						return $value === 'yes';
254
					case SearchPropertyDefinition::DATATYPE_DECIMAL:
255
					case SearchPropertyDefinition::DATATYPE_INTEGER:
256
					case SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER:
257
						return 0 + $value;
258
					default:
259
						return $value;
260
				}
261
			}
262
		}
263
		return $value;
264
	}
265
}
266