Passed
Push — master ( e17684...b6c034 )
by Blizzz
35:05 queued 17:14
created

SystemTagPlugin::handleUpdateProperties()   C

Complexity

Conditions 12
Paths 2

Size

Total Lines 59
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 38
nc 2
nop 2
dl 0
loc 59
rs 6.9666
c 0
b 0
f 0

How to fix   Long Method    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) 2016, ownCloud, Inc.
4
 *
5
 * @author Christoph Wurst <[email protected]>
6
 * @author Lukas Reschke <[email protected]>
7
 * @author Roeland Jago Douma <[email protected]>
8
 * @author Thomas Müller <[email protected]>
9
 * @author Vincent Petry <[email protected]>
10
 *
11
 * @license AGPL-3.0
12
 *
13
 * This code is free software: you can redistribute it and/or modify
14
 * it under the terms of the GNU Affero General Public License, version 3,
15
 * as published by the Free Software Foundation.
16
 *
17
 * This program is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
 * GNU Affero General Public License for more details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License, version 3,
23
 * along with this program. If not, see <http://www.gnu.org/licenses/>
24
 *
25
 */
26
namespace OCA\DAV\SystemTag;
27
28
use OCA\DAV\Connector\Sabre\Directory;
29
use OCA\DAV\Connector\Sabre\Node;
30
use OCP\IGroupManager;
31
use OCP\IUser;
32
use OCP\IUserSession;
33
use OCP\SystemTag\ISystemTag;
34
use OCP\SystemTag\ISystemTagManager;
35
use OCP\SystemTag\ISystemTagObjectMapper;
36
use OCP\SystemTag\TagAlreadyExistsException;
37
use OCP\Util;
38
use Sabre\DAV\Exception\BadRequest;
39
use Sabre\DAV\Exception\Conflict;
40
use Sabre\DAV\Exception\Forbidden;
41
use Sabre\DAV\Exception\UnsupportedMediaType;
42
use Sabre\DAV\PropFind;
43
use Sabre\DAV\PropPatch;
44
use Sabre\HTTP\RequestInterface;
45
use Sabre\HTTP\ResponseInterface;
46
47
/**
48
 * Sabre plugin to handle system tags:
49
 *
50
 * - makes it possible to create new tags with POST operation
51
 * - get/set Webdav properties for tags
52
 *
53
 */
54
class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
55
56
	// namespace
57
	public const NS_OWNCLOUD = 'http://owncloud.org/ns';
58
	public const ID_PROPERTYNAME = '{http://owncloud.org/ns}id';
59
	public const DISPLAYNAME_PROPERTYNAME = '{http://owncloud.org/ns}display-name';
60
	public const USERVISIBLE_PROPERTYNAME = '{http://owncloud.org/ns}user-visible';
61
	public const USERASSIGNABLE_PROPERTYNAME = '{http://owncloud.org/ns}user-assignable';
62
	public const GROUPS_PROPERTYNAME = '{http://owncloud.org/ns}groups';
63
	public const CANASSIGN_PROPERTYNAME = '{http://owncloud.org/ns}can-assign';
64
	public const SYSTEM_TAGS_PROPERTYNAME = '{http://nextcloud.org/ns}system-tags';
65
	public const NUM_FILES_PROPERTYNAME = '{http://nextcloud.org/ns}files-assigned';
66
	public const FILEID_PROPERTYNAME = '{http://nextcloud.org/ns}reference-fileid';
67
68
	/**
69
	 * @var \Sabre\DAV\Server $server
70
	 */
71
	private $server;
72
73
	/**
74
	 * @var ISystemTagManager
75
	 */
76
	protected $tagManager;
77
78
	/**
79
	 * @var IUserSession
80
	 */
81
	protected $userSession;
82
83
	/**
84
	 * @var IGroupManager
85
	 */
86
	protected $groupManager;
87
88
	/** @var array<int, string[]> */
89
	private array $cachedTagMappings = [];
90
	/** @var array<string, ISystemTag> */
91
	private array $cachedTags = [];
92
93
	private ISystemTagObjectMapper $tagMapper;
94
95
	public function __construct(
96
		ISystemTagManager $tagManager,
97
		IGroupManager $groupManager,
98
		IUserSession $userSession,
99
		ISystemTagObjectMapper $tagMapper,
100
	) {
101
		$this->tagManager = $tagManager;
102
		$this->userSession = $userSession;
103
		$this->groupManager = $groupManager;
104
		$this->tagMapper = $tagMapper;
105
	}
106
107
	/**
108
	 * This initializes the plugin.
109
	 *
110
	 * This function is called by \Sabre\DAV\Server, after
111
	 * addPlugin is called.
112
	 *
113
	 * This method should set up the required event subscriptions.
114
	 *
115
	 * @param \Sabre\DAV\Server $server
116
	 * @return void
117
	 */
118
	public function initialize(\Sabre\DAV\Server $server) {
119
		$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
120
121
		$server->protectedProperties[] = self::ID_PROPERTYNAME;
122
123
		$server->on('propFind', [$this, 'handleGetProperties']);
124
		$server->on('propPatch', [$this, 'handleUpdateProperties']);
125
		$server->on('method:POST', [$this, 'httpPost']);
126
127
		$this->server = $server;
128
	}
129
130
	/**
131
	 * POST operation on system tag collections
132
	 *
133
	 * @param RequestInterface $request request object
134
	 * @param ResponseInterface $response response object
135
	 * @return null|false
136
	 */
137
	public function httpPost(RequestInterface $request, ResponseInterface $response) {
138
		$path = $request->getPath();
139
140
		// Making sure the node exists
141
		$node = $this->server->tree->getNodeForPath($path);
142
		if ($node instanceof SystemTagsByIdCollection || $node instanceof SystemTagsObjectMappingCollection) {
143
			$data = $request->getBodyAsString();
144
145
			$tag = $this->createTag($data, $request->getHeader('Content-Type'));
146
147
			if ($node instanceof SystemTagsObjectMappingCollection) {
148
				// also add to collection
149
				$node->createFile($tag->getId());
150
				$url = $request->getBaseUrl() . 'systemtags/';
151
			} else {
152
				$url = $request->getUrl();
153
			}
154
155
			if ($url[strlen($url) - 1] !== '/') {
156
				$url .= '/';
157
			}
158
159
			$response->setHeader('Content-Location', $url . $tag->getId());
160
161
			// created
162
			$response->setStatus(201);
163
			return false;
164
		}
165
	}
166
167
	/**
168
	 * Creates a new tag
169
	 *
170
	 * @param string $data JSON encoded string containing the properties of the tag to create
171
	 * @param string $contentType content type of the data
172
	 * @return ISystemTag newly created system tag
173
	 *
174
	 * @throws BadRequest if a field was missing
175
	 * @throws Conflict if a tag with the same properties already exists
176
	 * @throws UnsupportedMediaType if the content type is not supported
177
	 */
178
	private function createTag($data, $contentType = 'application/json') {
179
		if (explode(';', $contentType)[0] === 'application/json') {
180
			$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
181
		} else {
182
			throw new UnsupportedMediaType();
183
		}
184
185
		if (!isset($data['name'])) {
186
			throw new BadRequest('Missing "name" attribute');
187
		}
188
189
		$tagName = $data['name'];
190
		$userVisible = true;
191
		$userAssignable = true;
192
193
		if (isset($data['userVisible'])) {
194
			$userVisible = (bool)$data['userVisible'];
195
		}
196
197
		if (isset($data['userAssignable'])) {
198
			$userAssignable = (bool)$data['userAssignable'];
199
		}
200
201
		$groups = [];
202
		if (isset($data['groups'])) {
203
			$groups = $data['groups'];
204
			if (is_string($groups)) {
205
				$groups = explode('|', $groups);
206
			}
207
		}
208
209
		if ($userVisible === false || $userAssignable === false || !empty($groups)) {
210
			if (!$this->userSession->isLoggedIn() || !$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) {
211
				throw new BadRequest('Not sufficient permissions');
212
			}
213
		}
214
215
		try {
216
			$tag = $this->tagManager->createTag($tagName, $userVisible, $userAssignable);
217
			if (!empty($groups)) {
218
				$this->tagManager->setTagGroups($tag, $groups);
219
			}
220
			return $tag;
221
		} catch (TagAlreadyExistsException $e) {
222
			throw new Conflict('Tag already exists', 0, $e);
223
		}
224
	}
225
226
227
	/**
228
	 * Retrieves system tag properties
229
	 *
230
	 * @param PropFind $propFind
231
	 * @param \Sabre\DAV\INode $node
232
	 *
233
	 * @return void
234
	 */
235
	public function handleGetProperties(
236
		PropFind $propFind,
237
		\Sabre\DAV\INode $node
238
	) {
239
		if ($node instanceof Node) {
240
			$this->propfindForFile($propFind, $node);
241
			return;
242
		}
243
244
		if (!($node instanceof SystemTagNode) && !($node instanceof SystemTagMappingNode)) {
245
			return;
246
		}
247
248
		// child nodes from systemtags-assigned should point to normal tag endpoint
249
		if (preg_match('/^systemtags-assigned\/[0-9]+/', $propFind->getPath())) {
250
			$propFind->setPath(str_replace('systemtags-assigned/', 'systemtags/', $propFind->getPath()));
251
		}
252
253
		$propFind->handle(self::ID_PROPERTYNAME, function () use ($node) {
254
			return $node->getSystemTag()->getId();
255
		});
256
257
		$propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function () use ($node) {
258
			return $node->getSystemTag()->getName();
259
		});
260
261
		$propFind->handle(self::USERVISIBLE_PROPERTYNAME, function () use ($node) {
262
			return $node->getSystemTag()->isUserVisible() ? 'true' : 'false';
263
		});
264
265
		$propFind->handle(self::USERASSIGNABLE_PROPERTYNAME, function () use ($node) {
266
			// this is the tag's inherent property "is user assignable"
267
			return $node->getSystemTag()->isUserAssignable() ? 'true' : 'false';
268
		});
269
270
		$propFind->handle(self::CANASSIGN_PROPERTYNAME, function () use ($node) {
271
			// this is the effective permission for the current user
272
			return $this->tagManager->canUserAssignTag($node->getSystemTag(), $this->userSession->getUser()) ? 'true' : 'false';
273
		});
274
275
		$propFind->handle(self::GROUPS_PROPERTYNAME, function () use ($node) {
276
			if (!$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) {
277
				// property only available for admins
278
				throw new Forbidden();
279
			}
280
			$groups = [];
281
			// no need to retrieve groups for namespaces that don't qualify
282
			if ($node->getSystemTag()->isUserVisible() && !$node->getSystemTag()->isUserAssignable()) {
283
				$groups = $this->tagManager->getTagGroups($node->getSystemTag());
284
			}
285
			return implode('|', $groups);
286
		});
287
288
		if ($node instanceof SystemTagNode) {
289
			$propFind->handle(self::NUM_FILES_PROPERTYNAME, function () use ($node): int {
290
				return $node->getNumberOfFiles();
291
			});
292
293
			$propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node): int {
294
				return $node->getReferenceFileId();
295
			});
296
		}
297
	}
298
299
	private function propfindForFile(PropFind $propFind, Node $node): void {
300
		if ($node instanceof Directory
301
			&& $propFind->getDepth() !== 0
302
			&& !is_null($propFind->getStatus(self::SYSTEM_TAGS_PROPERTYNAME))) {
303
			$fileIds = [$node->getId()];
304
305
			// note: pre-fetching only supported for depth <= 1
306
			$folderContent = $node->getNode()->getDirectoryListing();
307
			foreach ($folderContent as $info) {
308
				$fileIds[] = $info->getId();
309
			}
310
311
			$tags = $this->tagMapper->getTagIdsForObjects($fileIds, 'files');
312
313
			$this->cachedTagMappings = $this->cachedTagMappings + $tags;
314
			$emptyFileIds = array_diff($fileIds, array_keys($tags));
315
316
			// also cache the ones that were not found
317
			foreach ($emptyFileIds as $fileId) {
318
				$this->cachedTagMappings[$fileId] = [];
319
			}
320
		}
321
322
		$propFind->handle(self::SYSTEM_TAGS_PROPERTYNAME, function () use ($node) {
323
			$user = $this->userSession->getUser();
324
			if ($user === null) {
325
				return;
326
			}
327
328
			$tags = $this->getTagsForFile($node->getId(), $user);
329
			usort($tags, function (ISystemTag $tagA, ISystemTag $tagB): int {
330
				return Util::naturalSortCompare($tagA->getName(), $tagB->getName());
331
			});
332
			return new SystemTagList($tags, $this->tagManager, $user);
333
		});
334
	}
335
336
	/**
337
	 * @param int $fileId
338
	 * @return ISystemTag[]
339
	 */
340
	private function getTagsForFile(int $fileId, IUser $user): array {
341
342
		if (isset($this->cachedTagMappings[$fileId])) {
343
			$tagIds = $this->cachedTagMappings[$fileId];
344
		} else {
345
			$tags = $this->tagMapper->getTagIdsForObjects([$fileId], 'files');
346
			$fileTags = current($tags);
347
			if ($fileTags) {
348
				$tagIds = $fileTags;
349
			} else {
350
				$tagIds = [];
351
			}
352
		}
353
354
		$tags = array_filter(array_map(function(string $tagId) {
355
			return $this->cachedTags[$tagId] ?? null;
356
		}, $tagIds));
357
358
		$uncachedTagIds = array_filter($tagIds, function(string $tagId): bool {
359
			return !isset($this->cachedTags[$tagId]);
360
		});
361
362
		if (count($uncachedTagIds)) {
363
			$retrievedTags = $this->tagManager->getTagsByIds($uncachedTagIds);
364
			foreach ($retrievedTags as $tag) {
365
				$this->cachedTags[$tag->getId()] = $tag;
366
			}
367
			$tags += $retrievedTags;
368
		}
369
370
		return array_filter($tags, function(ISystemTag $tag) use ($user) {
371
			return $this->tagManager->canUserSeeTag($tag, $user);
372
		});
373
	}
374
375
	/**
376
	 * Updates tag attributes
377
	 *
378
	 * @param string $path
379
	 * @param PropPatch $propPatch
380
	 *
381
	 * @return void
382
	 */
383
	public function handleUpdateProperties($path, PropPatch $propPatch) {
384
		$node = $this->server->tree->getNodeForPath($path);
385
		if (!($node instanceof SystemTagNode)) {
386
			return;
387
		}
388
389
		$propPatch->handle([
390
			self::DISPLAYNAME_PROPERTYNAME,
391
			self::USERVISIBLE_PROPERTYNAME,
392
			self::USERASSIGNABLE_PROPERTYNAME,
393
			self::GROUPS_PROPERTYNAME,
394
			self::NUM_FILES_PROPERTYNAME,
395
			self::FILEID_PROPERTYNAME,
396
		], function ($props) use ($node) {
397
			$tag = $node->getSystemTag();
398
			$name = $tag->getName();
399
			$userVisible = $tag->isUserVisible();
400
			$userAssignable = $tag->isUserAssignable();
401
402
			$updateTag = false;
403
404
			if (isset($props[self::DISPLAYNAME_PROPERTYNAME])) {
405
				$name = $props[self::DISPLAYNAME_PROPERTYNAME];
406
				$updateTag = true;
407
			}
408
409
			if (isset($props[self::USERVISIBLE_PROPERTYNAME])) {
410
				$propValue = $props[self::USERVISIBLE_PROPERTYNAME];
411
				$userVisible = ($propValue !== 'false' && $propValue !== '0');
412
				$updateTag = true;
413
			}
414
415
			if (isset($props[self::USERASSIGNABLE_PROPERTYNAME])) {
416
				$propValue = $props[self::USERASSIGNABLE_PROPERTYNAME];
417
				$userAssignable = ($propValue !== 'false' && $propValue !== '0');
418
				$updateTag = true;
419
			}
420
421
			if (isset($props[self::GROUPS_PROPERTYNAME])) {
422
				if (!$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) {
423
					// property only available for admins
424
					throw new Forbidden();
425
				}
426
427
				$propValue = $props[self::GROUPS_PROPERTYNAME];
428
				$groupIds = explode('|', $propValue);
429
				$this->tagManager->setTagGroups($tag, $groupIds);
430
			}
431
432
			if (isset($props[self::NUM_FILES_PROPERTYNAME]) || isset($props[self::FILEID_PROPERTYNAME])) {
433
				// read-only properties
434
				throw new Forbidden();
435
			}
436
437
			if ($updateTag) {
438
				$node->update($name, $userVisible, $userAssignable);
439
			}
440
441
			return true;
442
		});
443
	}
444
}
445