Completed
Push — master ( e4992c...6d0a35 )
by
unknown
10:42
created

FilesReportPlugin   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 354
Duplicated Lines 4.52 %

Coupling/Cohesion

Components 1
Dependencies 21

Importance

Changes 0
Metric Value
dl 16
loc 354
rs 9.1199
c 0
b 0
f 0
wmc 41
lcom 1
cbo 21

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 18 1
A initialize() 8 8 1
A getSupportedReportSet() 0 3 1
B onReport() 0 45 7
A slice() 0 8 2
A getFilesBaseUri() 0 14 3
A processFilterRules() 0 23 5
B getSystemTagFileIds() 0 42 9
A prepareResponses() 0 14 2
A findNodesByFileIds() 0 20 5
A makeSabreNode() 0 8 3
A isAdmin() 7 7 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like FilesReportPlugin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FilesReportPlugin, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @author Joas Schilling <[email protected]>
4
 * @author Thomas Müller <[email protected]>
5
 * @author Vincent Petry <[email protected]>
6
 *
7
 * @copyright Copyright (c) 2018, ownCloud GmbH
8
 * @license AGPL-3.0
9
 *
10
 * This code is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU Affero General Public License, version 3,
12
 * as published by the Free Software Foundation.
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, version 3,
20
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
21
 *
22
 */
23
24
namespace OCA\DAV\Connector\Sabre;
25
26
use OC\Files\View;
27
use OCA\DAV\Files\Xml\FilterRequest;
28
use OCP\Files\Folder;
29
use OCP\IGroupManager;
30
use OCP\ITagManager;
31
use OCP\IUserSession;
32
use OCP\SystemTag\ISystemTagManager;
33
use OCP\SystemTag\ISystemTagObjectMapper;
34
use OCP\SystemTag\TagNotFoundException;
35
use Sabre\DAV\Exception\BadRequest;
36
use Sabre\DAV\Exception\PreconditionFailed;
37
use Sabre\DAV\PropFind;
38
use Sabre\DAV\ServerPlugin;
39
use Sabre\DAV\Tree;
40
use Sabre\DAV\Xml\Element\Response;
41
42
class FilesReportPlugin extends ServerPlugin {
43
44
	// namespace
45
	const NS_OWNCLOUD = 'http://owncloud.org/ns';
46
	const REPORT_NAME            = '{http://owncloud.org/ns}filter-files';
47
	const SYSTEMTAG_PROPERTYNAME = '{http://owncloud.org/ns}systemtag';
48
49
	/**
50
	 * Reference to main server object
51
	 *
52
	 * @var \Sabre\DAV\Server
53
	 */
54
	private $server;
55
56
	/**
57
	 * @var Tree
58
	 */
59
	private $tree;
60
61
	/**
62
	 * @var View
63
	 */
64
	private $fileView;
65
66
	/**
67
	 * @var ISystemTagManager
68
	 */
69
	private $tagManager;
70
71
	/**
72
	 * @var ISystemTagObjectMapper
73
	 */
74
	private $tagMapper;
75
76
	/**
77
	 * Manager for private tags
78
	 *
79
	 * @var ITagManager
80
	 */
81
	private $fileTagger;
82
83
	/**
84
	 * @var IUserSession
85
	 */
86
	private $userSession;
87
88
	/**
89
	 * @var IGroupManager
90
	 */
91
	private $groupManager;
92
93
	/**
94
	 * @var Folder
95
	 */
96
	private $userFolder;
97
98
	/**
99
	 * @param Tree $tree
100
	 * @param View $view
101
	 * @param ISystemTagManager $tagManager
102
	 * @param ISystemTagObjectMapper $tagMapper
103
	 * @param ITagManager $fileTagger manager for private tags
104
	 * @param IUserSession $userSession
105
	 * @param IGroupManager $groupManager
106
	 * @param Folder $userFolder
107
	 */
108
	public function __construct(Tree $tree,
109
								View $view,
110
								ISystemTagManager $tagManager,
111
								ISystemTagObjectMapper $tagMapper,
112
								ITagManager $fileTagger,
113
								IUserSession $userSession,
114
								IGroupManager $groupManager,
115
								Folder $userFolder
116
	) {
117
		$this->tree = $tree;
118
		$this->fileView = $view;
119
		$this->tagManager = $tagManager;
120
		$this->tagMapper = $tagMapper;
121
		$this->fileTagger = $fileTagger;
122
		$this->userSession = $userSession;
123
		$this->groupManager = $groupManager;
124
		$this->userFolder = $userFolder;
125
	}
126
127
	/**
128
	 * This initializes the plugin.
129
	 *
130
	 * This function is called by \Sabre\DAV\Server, after
131
	 * addPlugin is called.
132
	 *
133
	 * This method should set up the required event subscriptions.
134
	 *
135
	 * @param \Sabre\DAV\Server $server
136
	 * @return void
137
	 */
138 View Code Duplication
	public function initialize(\Sabre\DAV\Server $server) {
139
		$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
140
141
		$server->xml->elementMap[self::REPORT_NAME] = FilterRequest::class;
142
143
		$this->server = $server;
144
		$this->server->on('report', [$this, 'onReport']);
145
	}
146
147
	/**
148
	 * Returns a list of reports this plugin supports.
149
	 *
150
	 * This will be used in the {DAV:}supported-report-set property.
151
	 *
152
	 * @param string $uri
153
	 * @return array
154
	 */
155
	public function getSupportedReportSet($uri) {
156
		return [self::REPORT_NAME];
157
	}
158
159
	/**
160
	 * REPORT operations to look for files
161
	 *
162
	 * @param string $reportName
163
	 * @param mixed $report
164
	 * @param string $uri
165
	 * @return bool
166
	 * @throws BadRequest
167
	 * @throws PreconditionFailed
168
	 * @internal param $ [] $report
169
	 */
170
	public function onReport($reportName, $report, $uri) {
171
		$reportTargetNode = $this->server->tree->getNodeForPath($uri);
172
		if (!$reportTargetNode instanceof Directory || $reportName !== self::REPORT_NAME) {
173
			return;
174
		}
175
176
		$requestedProps = $report->properties;
177
		$filterRules = $report->filters;
178
179
		// "systemtag" is always an array of tags, favorite a string/int/null
180
		if (empty($filterRules['systemtag']) && $filterRules['favorite'] === null) {
181
			// FIXME: search currently not possible because results are missing properties!
182
			throw new BadRequest('No filter criteria specified');
183
		} else {
184
			if (isset($report->search['pattern'])) {
185
				// TODO: implement this at some point...
186
				throw new BadRequest('Search pattern cannot be combined with filter');
187
			}
188
189
			// gather all file ids matching filter
190
			try {
191
				$resultFileIds = $this->processFilterRules($filterRules);
192
			} catch (TagNotFoundException $e) {
193
				throw new PreconditionFailed('Cannot filter by non-existing tag', 0, $e);
0 ignored issues
show
Unused Code introduced by
The call to PreconditionFailed::__construct() has too many arguments starting with $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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
194
			}
195
196
			// pre-slice the results if needed for pagination to not waste
197
			// time resolving nodes that will not be returned anyway
198
			$resultFileIds = $this->slice($resultFileIds, $report);
199
200
			// find sabre nodes by file id, restricted to the root node path
201
			$results = $this->findNodesByFileIds($reportTargetNode, $resultFileIds);
202
		}
203
204
		$filesUri = $this->getFilesBaseUri($uri, $reportTargetNode->getPath());
205
		$results = $this->prepareResponses($filesUri, $requestedProps, $results);
206
207
		$xml = $this->server->generateMultiStatus($results);
208
209
		$this->server->httpResponse->setStatus(207);
210
		$this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
211
		$this->server->httpResponse->setBody($xml);
212
213
		return false;
214
	}
215
216
	private function slice($results, $report) {
217
		if ($report->search !== null) {
218
			$length = $report->search['limit'];
219
			$offset = $report->search['offset'];
220
			$results = \array_slice($results, $offset, $length);
221
		}
222
		return $results;
223
	}
224
225
	/**
226
	 * Returns the base uri of the files root by removing
227
	 * the subpath from the URI
228
	 *
229
	 * @param string $uri URI from this request
230
	 * @param string $subPath subpath to remove from the URI
231
	 *
232
	 * @return string files base uri
233
	 */
234
	private function getFilesBaseUri($uri, $subPath) {
235
		$uri = \trim($uri, '/');
236
		$subPath = \trim($subPath, '/');
237
		if (empty($subPath)) {
238
			$filesUri = $uri;
239
		} else {
240
			$filesUri = \substr($uri, 0, \strlen($uri) - \strlen($subPath));
241
		}
242
		$filesUri = \trim($filesUri, '/');
243
		if (empty($filesUri)) {
244
			return '';
245
		}
246
		return '/' . $filesUri;
247
	}
248
249
	/**
250
	 * Find file ids matching the given filter rules
251
	 *
252
	 * @param array $filterRules
253
	 * @return array array of unique file id results
254
	 *
255
	 * @throws TagNotFoundException whenever a tag was not found
256
	 */
257
	protected function processFilterRules($filterRules) {
258
		$resultFileIds = null;
259
		$systemTagIds = $filterRules['systemtag'];
260
		$favoriteFilter = $filterRules['favorite'];
261
262
		if ($favoriteFilter !== null) {
263
			$resultFileIds = $this->fileTagger->load('files')->getFavorites();
264
			if (empty($resultFileIds)) {
265
				return [];
266
			}
267
		}
268
269
		if (!empty($systemTagIds)) {
270
			$fileIds = $this->getSystemTagFileIds($systemTagIds);
271
			if (empty($resultFileIds)) {
272
				$resultFileIds = $fileIds;
273
			} else {
274
				$resultFileIds = \array_intersect($fileIds, $resultFileIds);
275
			}
276
		}
277
278
		return $resultFileIds;
279
	}
280
281
	private function getSystemTagFileIds($systemTagIds) {
282
		$resultFileIds = null;
283
284
		// check user permissions, if applicable
285
		if (!$this->isAdmin()) {
286
			// check visibility/permission
287
			$tags = $this->tagManager->getTagsByIds($systemTagIds);
288
			$unknownTagIds = [];
289
			foreach ($tags as $tag) {
290
				if (!$tag->isUserVisible()) {
291
					$unknownTagIds[] = $tag->getId();
292
				}
293
			}
294
295
			if (!empty($unknownTagIds)) {
296
				throw new TagNotFoundException('Tag with ids ' . \implode(', ', $unknownTagIds) . ' not found');
297
			}
298
		}
299
300
		// fetch all file ids and intersect them
301
		foreach ($systemTagIds as $systemTagId) {
302
			$fileIds = $this->tagMapper->getObjectIdsForTags($systemTagId, 'files');
303
304
			if (empty($fileIds)) {
305
				// This tag has no files, nothing can ever show up
306
				return [];
307
			}
308
309
			// first run ?
310
			if ($resultFileIds === null) {
311
				$resultFileIds = $fileIds;
312
			} else {
313
				$resultFileIds = \array_intersect($resultFileIds, $fileIds);
314
			}
315
316
			if (empty($resultFileIds)) {
317
				// Empty intersection, nothing can show up anymore
318
				return [];
319
			}
320
		}
321
		return $resultFileIds;
322
	}
323
324
	/**
325
	 * Prepare propfind response for the given nodes
326
	 *
327
	 * @param string $filesUri $filesUri URI leading to root of the files URI,
328
	 * with a leading slash but no trailing slash
329
	 * @param string[] $requestedProps requested properties
330
	 * @param Node[] nodes nodes for which to fetch and prepare responses
331
	 * @return Response[]
332
	 */
333
	public function prepareResponses($filesUri, $requestedProps, $nodes) {
334
		$results = [];
335
		foreach ($nodes as $node) {
336
			$propFind = new PropFind($filesUri . $node->getPath(), $requestedProps);
337
338
			$this->server->getPropertiesByNode($propFind, $node);
339
			// copied from Sabre Server's getPropertiesForPath
340
			$result = $propFind->getResultForMultiStatus();
341
			$result['href'] = $propFind->getPath();
342
343
			$results[] = $result;
344
		}
345
		return $results;
346
	}
347
348
	/**
349
	 * Find Sabre nodes by file ids
350
	 *
351
	 * @param Node $rootNode root node for search
352
	 * @param array $fileIds file ids
353
	 * @return Node[] array of Sabre nodes
354
	 */
355
	public function findNodesByFileIds($rootNode, $fileIds) {
356
		$folder = $this->userFolder;
357
		if (\trim($rootNode->getPath(), '/') !== '') {
358
			$folder = $folder->get($rootNode->getPath());
359
		}
360
361
		$results = [];
362
		foreach ($fileIds as $fileId) {
363
			$entry = $folder->getById($fileId);
364
			if ($entry) {
365
				$entry = \current($entry);
366
				$node = $this->makeSabreNode($entry);
367
				if ($node) {
368
					$results[] = $node;
369
				}
370
			}
371
		}
372
373
		return $results;
374
	}
375
376
	private function makeSabreNode(\OCP\Files\Node $filesNode) {
377
		if ($filesNode instanceof \OCP\Files\File) {
378
			return new File($this->fileView, $filesNode);
379
		} elseif ($filesNode instanceof \OCP\Files\Folder) {
380
			return new Directory($this->fileView, $filesNode);
381
		}
382
		throw new \Exception('Unrecognized Files API node returned, aborting');
383
	}
384
385
	/**
386
	 * Returns whether the currently logged in user is an administrator
387
	 */
388 View Code Duplication
	private function isAdmin() {
389
		$user = $this->userSession->getUser();
390
		if ($user !== null) {
391
			return $this->groupManager->isAdmin($user->getUID());
392
		}
393
		return false;
394
	}
395
}
396