Passed
Push — master ( 804a15...6331f1 )
by Morris
12:05
created

FilesReportPlugin::getCirclesFileIds()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 3
nc 2
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Joas Schilling <[email protected]>
6
 * @author Thomas Müller <[email protected]>
7
 * @author Vincent Petry <[email protected]>
8
 *
9
 * @license AGPL-3.0
10
 *
11
 * This code is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU Affero General Public License, version 3,
13
 * as published by the Free Software Foundation.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
 * GNU Affero General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU Affero General Public License, version 3,
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
22
 *
23
 */
24
25
namespace OCA\DAV\Connector\Sabre;
26
27
use OC\Files\View;
28
use OCP\App\IAppManager;
29
use Sabre\DAV\Exception\PreconditionFailed;
30
use Sabre\DAV\Exception\BadRequest;
31
use Sabre\DAV\ServerPlugin;
32
use Sabre\DAV\Tree;
33
use Sabre\DAV\Xml\Element\Response;
34
use Sabre\DAV\Xml\Response\MultiStatus;
35
use Sabre\DAV\PropFind;
36
use OCP\SystemTag\ISystemTagObjectMapper;
37
use OCP\IUserSession;
38
use OCP\Files\Folder;
39
use OCP\IGroupManager;
40
use OCP\SystemTag\ISystemTagManager;
41
use OCP\SystemTag\TagNotFoundException;
42
use OCP\ITagManager;
43
44
class FilesReportPlugin extends ServerPlugin {
45
46
	// namespace
47
	const NS_OWNCLOUD = 'http://owncloud.org/ns';
48
	const REPORT_NAME            = '{http://owncloud.org/ns}filter-files';
49
	const SYSTEMTAG_PROPERTYNAME = '{http://owncloud.org/ns}systemtag';
50
	const CIRCLE_PROPERTYNAME = '{http://owncloud.org/ns}circle';
51
52
	/**
53
	 * Reference to main server object
54
	 *
55
	 * @var \Sabre\DAV\Server
56
	 */
57
	private $server;
58
59
	/**
60
	 * @var Tree
61
	 */
62
	private $tree;
63
64
	/**
65
	 * @var View
66
	 */
67
	private $fileView;
68
69
	/**
70
	 * @var ISystemTagManager
71
	 */
72
	private $tagManager;
73
74
	/**
75
	 * @var ISystemTagObjectMapper
76
	 */
77
	private $tagMapper;
78
79
	/**
80
	 * Manager for private tags
81
	 *
82
	 * @var ITagManager
83
	 */
84
	private $fileTagger;
85
86
	/**
87
	 * @var IUserSession
88
	 */
89
	private $userSession;
90
91
	/**
92
	 * @var IGroupManager
93
	 */
94
	private $groupManager;
95
96
	/**
97
	 * @var Folder
98
	 */
99
	private $userFolder;
100
101
	/**
102
	 * @var IAppManager
103
	 */
104
	private $appManager;
105
106
	/**
107
	 * @param Tree $tree
108
	 * @param View $view
109
	 * @param ISystemTagManager $tagManager
110
	 * @param ISystemTagObjectMapper $tagMapper
111
	 * @param ITagManager $fileTagger manager for private tags
112
	 * @param IUserSession $userSession
113
	 * @param IGroupManager $groupManager
114
	 * @param Folder $userFolder
115
	 * @param IAppManager $appManager
116
	 */
117
	public function __construct(Tree $tree,
118
								View $view,
119
								ISystemTagManager $tagManager,
120
								ISystemTagObjectMapper $tagMapper,
121
								ITagManager $fileTagger,
122
								IUserSession $userSession,
123
								IGroupManager $groupManager,
124
								Folder $userFolder,
125
								IAppManager $appManager
126
	) {
127
		$this->tree = $tree;
128
		$this->fileView = $view;
129
		$this->tagManager = $tagManager;
130
		$this->tagMapper = $tagMapper;
131
		$this->fileTagger = $fileTagger;
132
		$this->userSession = $userSession;
133
		$this->groupManager = $groupManager;
134
		$this->userFolder = $userFolder;
135
		$this->appManager = $appManager;
136
	}
137
138
	/**
139
	 * This initializes the plugin.
140
	 *
141
	 * This function is called by \Sabre\DAV\Server, after
142
	 * addPlugin is called.
143
	 *
144
	 * This method should set up the required event subscriptions.
145
	 *
146
	 * @param \Sabre\DAV\Server $server
147
	 * @return void
148
	 */
149
	public function initialize(\Sabre\DAV\Server $server) {
150
151
		$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
152
153
		$this->server = $server;
154
		$this->server->on('report', array($this, 'onReport'));
155
	}
156
157
	/**
158
	 * Returns a list of reports this plugin supports.
159
	 *
160
	 * This will be used in the {DAV:}supported-report-set property.
161
	 *
162
	 * @param string $uri
163
	 * @return array
164
	 */
165
	public function getSupportedReportSet($uri) {
166
		return [self::REPORT_NAME];
167
	}
168
169
	/**
170
	 * REPORT operations to look for files
171
	 *
172
	 * @param string $reportName
173
	 * @param $report
174
	 * @param string $uri
175
	 * @return bool
176
	 * @throws BadRequest
177
	 * @throws PreconditionFailed
178
	 * @internal param $ [] $report
179
	 */
180
	public function onReport($reportName, $report, $uri) {
181
		$reportTargetNode = $this->server->tree->getNodeForPath($uri);
182
		if (!$reportTargetNode instanceof Directory || $reportName !== self::REPORT_NAME) {
183
			return;
184
		}
185
186
		$ns = '{' . $this::NS_OWNCLOUD . '}';
187
		$requestedProps = [];
188
		$filterRules = [];
189
190
		// parse report properties and gather filter info
191
		foreach ($report as $reportProps) {
192
			$name = $reportProps['name'];
193
			if ($name === $ns . 'filter-rules') {
194
				$filterRules = $reportProps['value'];
195
			} else if ($name === '{DAV:}prop') {
196
				// propfind properties
197
				foreach ($reportProps['value'] as $propVal) {
198
					$requestedProps[] = $propVal['name'];
199
				}
200
			}
201
		}
202
203
		if (empty($filterRules)) {
204
			// an empty filter would return all existing files which would be slow
205
			throw new BadRequest('Missing filter-rule block in request');
206
		}
207
208
		// gather all file ids matching filter
209
		try {
210
			$resultFileIds = $this->processFilterRules($filterRules);
211
		} catch (TagNotFoundException $e) {
212
			throw new PreconditionFailed('Cannot filter by non-existing tag', 0, $e);
0 ignored issues
show
Unused Code introduced by
The call to Sabre\DAV\Exception\Prec...onFailed::__construct() has too many arguments starting with $e. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

212
			throw /** @scrutinizer ignore-call */ new PreconditionFailed('Cannot filter by non-existing tag', 0, $e);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
213
		}
214
215
		// find sabre nodes by file id, restricted to the root node path
216
		$results = $this->findNodesByFileIds($reportTargetNode, $resultFileIds);
217
218
		$filesUri = $this->getFilesBaseUri($uri, $reportTargetNode->getPath());
219
		$responses = $this->prepareResponses($filesUri, $requestedProps, $results);
220
221
		$xml = $this->server->xml->write(
222
			'{DAV:}multistatus',
223
			new MultiStatus($responses)
224
		);
225
226
		$this->server->httpResponse->setStatus(207);
227
		$this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
228
		$this->server->httpResponse->setBody($xml);
229
230
		return false;
231
	}
232
233
	/**
234
	 * Returns the base uri of the files root by removing
235
	 * the subpath from the URI
236
	 *
237
	 * @param string $uri URI from this request
238
	 * @param string $subPath subpath to remove from the URI
239
	 *
240
	 * @return string files base uri
241
	 */
242
	private function getFilesBaseUri($uri, $subPath) {
243
		$uri = trim($uri, '/');
244
		$subPath = trim($subPath, '/');
245
		if (empty($subPath)) {
246
			$filesUri = $uri;
247
		} else {
248
			$filesUri = substr($uri, 0, strlen($uri) - strlen($subPath));
249
		}
250
		$filesUri = trim($filesUri, '/');
251
		if (empty($filesUri)) {
252
			return '';
253
		}
254
		return '/' . $filesUri;
255
	}
256
257
	/**
258
	 * Find file ids matching the given filter rules
259
	 *
260
	 * @param array $filterRules
261
	 * @return array array of unique file id results
262
	 *
263
	 * @throws TagNotFoundException whenever a tag was not found
264
	 */
265
	protected function processFilterRules($filterRules) {
266
		$ns = '{' . $this::NS_OWNCLOUD . '}';
267
		$resultFileIds = null;
268
		$systemTagIds = [];
269
		$circlesIds = [];
270
		$favoriteFilter = null;
271
		foreach ($filterRules as $filterRule) {
272
			if ($filterRule['name'] === $ns . 'systemtag') {
273
				$systemTagIds[] = $filterRule['value'];
274
			}
275
			if ($filterRule['name'] === self::CIRCLE_PROPERTYNAME) {
276
				$circlesIds[] = $filterRule['value'];
277
			}
278
			if ($filterRule['name'] === $ns . 'favorite') {
279
				$favoriteFilter = true;
280
			}
281
282
		}
283
284
		if ($favoriteFilter !== null) {
285
			$resultFileIds = $this->fileTagger->load('files')->getFavorites();
286
			if (empty($resultFileIds)) {
287
				return [];
288
			}
289
		}
290
291
		if (!empty($systemTagIds)) {
292
			$fileIds = $this->getSystemTagFileIds($systemTagIds);
293
			if (empty($resultFileIds)) {
294
				$resultFileIds = $fileIds;
295
			} else {
296
				$resultFileIds = array_intersect($fileIds, $resultFileIds);
297
			}
298
		}
299
300
		if (!empty($circlesIds)) {
301
			$fileIds = $this->getCirclesFileIds($circlesIds);
302
			if (empty($resultFileIds)) {
303
				$resultFileIds = $fileIds;
304
			} else {
305
				$resultFileIds = array_intersect($fileIds, $resultFileIds);
306
			}
307
		}
308
309
		return $resultFileIds;
310
	}
311
312
	private function getSystemTagFileIds($systemTagIds) {
313
		$resultFileIds = null;
314
315
		// check user permissions, if applicable
316
		if (!$this->isAdmin()) {
317
			// check visibility/permission
318
			$tags = $this->tagManager->getTagsByIds($systemTagIds);
319
			$unknownTagIds = [];
320
			foreach ($tags as $tag) {
321
				if (!$tag->isUserVisible()) {
322
					$unknownTagIds[] = $tag->getId();
323
				}
324
			}
325
326
			if (!empty($unknownTagIds)) {
327
				throw new TagNotFoundException('Tag with ids ' . implode(', ', $unknownTagIds) . ' not found');
328
			}
329
		}
330
331
		// fetch all file ids and intersect them
332
		foreach ($systemTagIds as $systemTagId) {
333
			$fileIds = $this->tagMapper->getObjectIdsForTags($systemTagId, 'files');
334
335
			if (empty($fileIds)) {
336
				// This tag has no files, nothing can ever show up
337
				return [];
338
			}
339
340
			// first run ?
341
			if ($resultFileIds === null) {
342
				$resultFileIds = $fileIds;
343
			} else {
344
				$resultFileIds = array_intersect($resultFileIds, $fileIds);
345
			}
346
347
			if (empty($resultFileIds)) {
348
				// Empty intersection, nothing can show up anymore
349
				return [];
350
			}
351
		}
352
		return $resultFileIds;
353
	}
354
355
	/**
356
	 * @suppress PhanUndeclaredClassMethod
357
	 * @param array $circlesIds
358
	 * @return array
359
	 */
360
	private function getCirclesFileIds(array $circlesIds) {
361
		if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
362
			return [];
363
		}
364
		return \OCA\Circles\Api\v1\Circles::getFilesForCircles($circlesIds);
0 ignored issues
show
Bug introduced by
The type OCA\Circles\Api\v1\Circles was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
365
	}
366
367
368
	/**
369
	 * Prepare propfind response for the given nodes
370
	 *
371
	 * @param string $filesUri $filesUri URI leading to root of the files URI,
372
	 * with a leading slash but no trailing slash
373
	 * @param string[] $requestedProps requested properties
374
	 * @param Node[] nodes nodes for which to fetch and prepare responses
0 ignored issues
show
Bug introduced by
The type OCA\DAV\Connector\Sabre\nodes was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
375
	 * @return Response[]
376
	 */
377
	public function prepareResponses($filesUri, $requestedProps, $nodes) {
378
		$responses = [];
379
		foreach ($nodes as $node) {
380
			$propFind = new PropFind($filesUri . $node->getPath(), $requestedProps);
381
382
			$this->server->getPropertiesByNode($propFind, $node);
383
			// copied from Sabre Server's getPropertiesForPath
384
			$result = $propFind->getResultForMultiStatus();
385
			$result['href'] = $propFind->getPath();
386
387
			$resourceType = $this->server->getResourceTypeForNode($node);
388
			if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) {
389
				$result['href'] .= '/';
390
			}
391
392
			$responses[] = new Response(
393
				rtrim($this->server->getBaseUri(), '/') . $filesUri . $node->getPath(),
394
				$result,
395
				200
396
			);
397
		}
398
		return $responses;
399
	}
400
401
	/**
402
	 * Find Sabre nodes by file ids
403
	 *
404
	 * @param Node $rootNode root node for search
405
	 * @param array $fileIds file ids
406
	 * @return Node[] array of Sabre nodes
407
	 */
408
	public function findNodesByFileIds($rootNode, $fileIds) {
409
		$folder = $this->userFolder;
410
		if (trim($rootNode->getPath(), '/') !== '') {
411
			$folder = $folder->get($rootNode->getPath());
412
		}
413
414
		$results = [];
415
		foreach ($fileIds as $fileId) {
416
			$entry = $folder->getById($fileId);
0 ignored issues
show
Bug introduced by
The method getById() does not exist on OCP\Files\Node. Did you maybe mean getId()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

416
			/** @scrutinizer ignore-call */ 
417
   $entry = $folder->getById($fileId);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
417
			if ($entry) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $entry of type OCP\Files\Node[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
418
				$entry = current($entry);
419
				if ($entry instanceof \OCP\Files\File) {
420
					$results[] = new File($this->fileView, $entry);
421
				} else if ($entry instanceof \OCP\Files\Folder) {
422
					$results[] = new Directory($this->fileView, $entry);
423
				}
424
			}
425
		}
426
427
		return $results;
428
	}
429
430
	/**
431
	 * Returns whether the currently logged in user is an administrator
432
	 */
433
	private function isAdmin() {
434
		$user = $this->userSession->getUser();
435
		if ($user !== null) {
436
			return $this->groupManager->isAdmin($user->getUID());
437
		}
438
		return false;
439
	}
440
}
441