Passed
Push — master ( a9798a...74f31b )
by John
14:36 queued 12s
created

SystemTagPlugin::handleGetProperties()   B

Complexity

Conditions 10
Paths 3

Size

Total Lines 46
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 22
c 0
b 0
f 0
nc 3
nop 2
dl 0
loc 46
rs 7.6666

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