Completed
Push — master ( f6593a...d907a7 )
by John
29:27
created
apps/dav/lib/SystemTag/SystemTagPlugin.php 2 patches
Indentation   +517 added lines, -517 removed lines patch added patch discarded remove patch
@@ -42,521 +42,521 @@
 block discarded – undo
42 42
  */
43 43
 class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
44 44
 
45
-	// namespace
46
-	public const NS_OWNCLOUD = 'http://owncloud.org/ns';
47
-	public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
48
-	public const ID_PROPERTYNAME = '{http://owncloud.org/ns}id';
49
-	public const DISPLAYNAME_PROPERTYNAME = '{http://owncloud.org/ns}display-name';
50
-	public const USERVISIBLE_PROPERTYNAME = '{http://owncloud.org/ns}user-visible';
51
-	public const USERASSIGNABLE_PROPERTYNAME = '{http://owncloud.org/ns}user-assignable';
52
-	public const GROUPS_PROPERTYNAME = '{http://owncloud.org/ns}groups';
53
-	public const CANASSIGN_PROPERTYNAME = '{http://owncloud.org/ns}can-assign';
54
-	public const SYSTEM_TAGS_PROPERTYNAME = '{http://nextcloud.org/ns}system-tags';
55
-	public const NUM_FILES_PROPERTYNAME = '{http://nextcloud.org/ns}files-assigned';
56
-	public const REFERENCE_FILEID_PROPERTYNAME = '{http://nextcloud.org/ns}reference-fileid';
57
-	public const OBJECTIDS_PROPERTYNAME = '{http://nextcloud.org/ns}object-ids';
58
-	public const COLOR_PROPERTYNAME = '{http://nextcloud.org/ns}color';
59
-
60
-	/**
61
-	 * @var \Sabre\DAV\Server $server
62
-	 */
63
-	private $server;
64
-
65
-	/** @var array<int, string[]> */
66
-	private array $cachedTagMappings = [];
67
-	/** @var array<string, ISystemTag> */
68
-	private array $cachedTags = [];
69
-
70
-	public function __construct(
71
-		protected ISystemTagManager $tagManager,
72
-		protected IGroupManager $groupManager,
73
-		protected IUserSession $userSession,
74
-		protected IRootFolder $rootFolder,
75
-		protected ISystemTagObjectMapper $tagMapper,
76
-	) {
77
-	}
78
-
79
-	/**
80
-	 * This initializes the plugin.
81
-	 *
82
-	 * This function is called by \Sabre\DAV\Server, after
83
-	 * addPlugin is called.
84
-	 *
85
-	 * This method should set up the required event subscriptions.
86
-	 *
87
-	 * @param \Sabre\DAV\Server $server
88
-	 * @return void
89
-	 */
90
-	public function initialize(\Sabre\DAV\Server $server) {
91
-		$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
92
-		$server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';
93
-
94
-		$server->xml->elementMap[self::OBJECTIDS_PROPERTYNAME] = SystemTagsObjectList::class;
95
-
96
-		$server->protectedProperties[] = self::ID_PROPERTYNAME;
97
-
98
-		$server->on('preloadCollection', $this->preloadCollection(...));
99
-		$server->on('propFind', [$this, 'handleGetProperties']);
100
-		$server->on('propPatch', [$this, 'handleUpdateProperties']);
101
-		$server->on('method:POST', [$this, 'httpPost']);
102
-
103
-		$this->server = $server;
104
-	}
105
-
106
-	/**
107
-	 * POST operation on system tag collections
108
-	 *
109
-	 * @param RequestInterface $request request object
110
-	 * @param ResponseInterface $response response object
111
-	 * @return null|false
112
-	 */
113
-	public function httpPost(RequestInterface $request, ResponseInterface $response) {
114
-		$path = $request->getPath();
115
-
116
-		// Making sure the node exists
117
-		$node = $this->server->tree->getNodeForPath($path);
118
-		if ($node instanceof SystemTagsByIdCollection || $node instanceof SystemTagsObjectMappingCollection) {
119
-			$data = $request->getBodyAsString();
120
-
121
-			$tag = $this->createTag($data, $request->getHeader('Content-Type'));
122
-
123
-			if ($node instanceof SystemTagsObjectMappingCollection) {
124
-				// also add to collection
125
-				$node->createFile($tag->getId());
126
-				$url = $request->getBaseUrl() . 'systemtags/';
127
-			} else {
128
-				$url = $request->getUrl();
129
-			}
130
-
131
-			if ($url[strlen($url) - 1] !== '/') {
132
-				$url .= '/';
133
-			}
134
-
135
-			$response->setHeader('Content-Location', $url . $tag->getId());
136
-
137
-			// created
138
-			$response->setStatus(Http::STATUS_CREATED);
139
-			return false;
140
-		}
141
-	}
142
-
143
-	/**
144
-	 * Creates a new tag
145
-	 *
146
-	 * @param string $data JSON encoded string containing the properties of the tag to create
147
-	 * @param string $contentType content type of the data
148
-	 * @return ISystemTag newly created system tag
149
-	 *
150
-	 * @throws BadRequest if a field was missing
151
-	 * @throws Conflict if a tag with the same properties already exists
152
-	 * @throws UnsupportedMediaType if the content type is not supported
153
-	 */
154
-	private function createTag($data, $contentType = 'application/json') {
155
-		if (explode(';', $contentType)[0] === 'application/json') {
156
-			$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
157
-		} else {
158
-			throw new UnsupportedMediaType();
159
-		}
160
-
161
-		if (!isset($data['name'])) {
162
-			throw new BadRequest('Missing "name" attribute');
163
-		}
164
-
165
-		$tagName = $data['name'];
166
-		$userVisible = true;
167
-		$userAssignable = true;
168
-
169
-		if (isset($data['userVisible'])) {
170
-			$userVisible = (bool)$data['userVisible'];
171
-		}
172
-
173
-		if (isset($data['userAssignable'])) {
174
-			$userAssignable = (bool)$data['userAssignable'];
175
-		}
176
-
177
-		$groups = [];
178
-		if (isset($data['groups'])) {
179
-			$groups = $data['groups'];
180
-			if (is_string($groups)) {
181
-				$groups = explode('|', $groups);
182
-			}
183
-		}
184
-
185
-		if ($userVisible === false || $userAssignable === false || !empty($groups)) {
186
-			if (!$this->userSession->isLoggedIn() || !$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) {
187
-				throw new BadRequest('Not sufficient permissions');
188
-			}
189
-		}
190
-
191
-		try {
192
-			$tag = $this->tagManager->createTag($tagName, $userVisible, $userAssignable);
193
-			if (!empty($groups)) {
194
-				$this->tagManager->setTagGroups($tag, $groups);
195
-			}
196
-			return $tag;
197
-		} catch (TagAlreadyExistsException $e) {
198
-			throw new Conflict('Tag already exists', 0, $e);
199
-		} catch (TagCreationForbiddenException $e) {
200
-			throw new Forbidden('You don’t have permissions to create tags', 0, $e);
201
-		}
202
-	}
203
-
204
-	private function preloadCollection(
205
-		PropFind $propFind,
206
-		ICollection $collection,
207
-	): void {
208
-		if (!$collection instanceof Node) {
209
-			return;
210
-		}
211
-
212
-		if ($collection instanceof Directory
213
-			&& !isset($this->cachedTagMappings[$collection->getId()])
214
-			&& $propFind->getStatus(
215
-				self::SYSTEM_TAGS_PROPERTYNAME
216
-			) !== null) {
217
-			$fileIds = [$collection->getId()];
218
-
219
-			// note: pre-fetching only supported for depth <= 1
220
-			$folderContent = $collection->getChildren();
221
-			foreach ($folderContent as $info) {
222
-				if ($info instanceof Node) {
223
-					$fileIds[] = $info->getId();
224
-				}
225
-			}
226
-
227
-			$tags = $this->tagMapper->getTagIdsForObjects($fileIds, 'files');
228
-
229
-			$this->cachedTagMappings += $tags;
230
-			$emptyFileIds = array_diff($fileIds, array_keys($tags));
231
-
232
-			// also cache the ones that were not found
233
-			foreach ($emptyFileIds as $fileId) {
234
-				$this->cachedTagMappings[$fileId] = [];
235
-			}
236
-		}
237
-	}
238
-
239
-	/**
240
-	 * Retrieves system tag properties
241
-	 *
242
-	 * @param PropFind $propFind
243
-	 * @param \Sabre\DAV\INode $node
244
-	 *
245
-	 * @return void
246
-	 */
247
-	public function handleGetProperties(
248
-		PropFind $propFind,
249
-		\Sabre\DAV\INode $node,
250
-	) {
251
-		if ($node instanceof Node) {
252
-			$this->propfindForFile($propFind, $node);
253
-			return;
254
-		}
255
-
256
-		if (!$node instanceof SystemTagNode && !$node instanceof SystemTagMappingNode && !$node instanceof SystemTagObjectType) {
257
-			return;
258
-		}
259
-
260
-		// child nodes from systemtags-assigned should point to normal tag endpoint
261
-		if (preg_match('/^systemtags-assigned\/[0-9]+/', $propFind->getPath())) {
262
-			$propFind->setPath(str_replace('systemtags-assigned/', 'systemtags/', $propFind->getPath()));
263
-		}
264
-
265
-		$propFind->handle(FilesPlugin::GETETAG_PROPERTYNAME, function () use ($node): string {
266
-			return '"' . ($node->getSystemTag()->getETag() ?? '') . '"';
267
-		});
268
-
269
-		$propFind->handle(self::ID_PROPERTYNAME, function () use ($node) {
270
-			return $node->getSystemTag()->getId();
271
-		});
272
-
273
-		$propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function () use ($node) {
274
-			return $node->getSystemTag()->getName();
275
-		});
276
-
277
-		$propFind->handle(self::USERVISIBLE_PROPERTYNAME, function () use ($node) {
278
-			return $node->getSystemTag()->isUserVisible() ? 'true' : 'false';
279
-		});
280
-
281
-		$propFind->handle(self::USERASSIGNABLE_PROPERTYNAME, function () use ($node) {
282
-			// this is the tag's inherent property "is user assignable"
283
-			return $node->getSystemTag()->isUserAssignable() ? 'true' : 'false';
284
-		});
285
-
286
-		$propFind->handle(self::CANASSIGN_PROPERTYNAME, function () use ($node) {
287
-			// this is the effective permission for the current user
288
-			return $this->tagManager->canUserAssignTag($node->getSystemTag(), $this->userSession->getUser()) ? 'true' : 'false';
289
-		});
290
-
291
-		$propFind->handle(self::COLOR_PROPERTYNAME, function () use ($node) {
292
-			return $node->getSystemTag()->getColor() ?? '';
293
-		});
294
-
295
-		$propFind->handle(self::GROUPS_PROPERTYNAME, function () use ($node) {
296
-			if (!$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) {
297
-				// property only available for admins
298
-				throw new Forbidden();
299
-			}
300
-			$groups = [];
301
-			// no need to retrieve groups for namespaces that don't qualify
302
-			if ($node->getSystemTag()->isUserVisible() && !$node->getSystemTag()->isUserAssignable()) {
303
-				$groups = $this->tagManager->getTagGroups($node->getSystemTag());
304
-			}
305
-			return implode('|', $groups);
306
-		});
307
-
308
-		if ($node instanceof SystemTagNode) {
309
-			$propFind->handle(self::NUM_FILES_PROPERTYNAME, function () use ($node): int {
310
-				return $node->getNumberOfFiles();
311
-			});
312
-
313
-			$propFind->handle(self::REFERENCE_FILEID_PROPERTYNAME, function () use ($node): int {
314
-				return $node->getReferenceFileId();
315
-			});
316
-
317
-			$propFind->handle(self::OBJECTIDS_PROPERTYNAME, function () use ($node): SystemTagsObjectList {
318
-				$objectTypes = $this->tagMapper->getAvailableObjectTypes();
319
-				$objects = [];
320
-				foreach ($objectTypes as $type) {
321
-					$systemTagObjectType = new SystemTagObjectType($node->getSystemTag(), $type, $this->tagManager, $this->tagMapper);
322
-					$objects = array_merge($objects, array_fill_keys($systemTagObjectType->getObjectsIds(), $type));
323
-				}
324
-				return new SystemTagsObjectList($objects);
325
-			});
326
-		}
327
-
328
-		if ($node instanceof SystemTagObjectType) {
329
-			$propFind->handle(self::OBJECTIDS_PROPERTYNAME, function () use ($node): SystemTagsObjectList {
330
-				return new SystemTagsObjectList(array_fill_keys($node->getObjectsIds(), $node->getName()));
331
-			});
332
-		}
333
-	}
334
-
335
-	private function propfindForFile(PropFind $propFind, Node $node): void {
336
-
337
-		$propFind->handle(self::SYSTEM_TAGS_PROPERTYNAME, function () use ($node) {
338
-			$user = $this->userSession->getUser();
339
-
340
-			$tags = $this->getTagsForFile($node->getId(), $user);
341
-			usort($tags, function (ISystemTag $tagA, ISystemTag $tagB): int {
342
-				return Util::naturalSortCompare($tagA->getName(), $tagB->getName());
343
-			});
344
-			return new SystemTagList($tags, $this->tagManager, $user);
345
-		});
346
-	}
347
-
348
-	/**
349
-	 * @param int $fileId
350
-	 * @return ISystemTag[]
351
-	 */
352
-	private function getTagsForFile(int $fileId, ?IUser $user): array {
353
-		if (isset($this->cachedTagMappings[$fileId])) {
354
-			$tagIds = $this->cachedTagMappings[$fileId];
355
-		} else {
356
-			$tags = $this->tagMapper->getTagIdsForObjects([$fileId], 'files');
357
-			$fileTags = current($tags);
358
-			if ($fileTags) {
359
-				$tagIds = $fileTags;
360
-			} else {
361
-				$tagIds = [];
362
-			}
363
-		}
364
-
365
-		$tags = array_filter(array_map(function (string $tagId) {
366
-			return $this->cachedTags[$tagId] ?? null;
367
-		}, $tagIds));
368
-
369
-		$uncachedTagIds = array_filter($tagIds, function (string $tagId): bool {
370
-			return !isset($this->cachedTags[$tagId]);
371
-		});
372
-
373
-		if (count($uncachedTagIds)) {
374
-			$retrievedTags = $this->tagManager->getTagsByIds($uncachedTagIds);
375
-			foreach ($retrievedTags as $tag) {
376
-				$this->cachedTags[$tag->getId()] = $tag;
377
-			}
378
-			$tags += $retrievedTags;
379
-		}
380
-
381
-		return array_filter($tags, function (ISystemTag $tag) use ($user) {
382
-			return $this->tagManager->canUserSeeTag($tag, $user);
383
-		});
384
-	}
385
-
386
-	/**
387
-	 * Updates tag attributes
388
-	 *
389
-	 * @param string $path
390
-	 * @param PropPatch $propPatch
391
-	 *
392
-	 * @return void
393
-	 */
394
-	public function handleUpdateProperties($path, PropPatch $propPatch) {
395
-		$node = $this->server->tree->getNodeForPath($path);
396
-		if (!$node instanceof SystemTagNode && !$node instanceof SystemTagObjectType) {
397
-			return;
398
-		}
399
-
400
-		$propPatch->handle([self::OBJECTIDS_PROPERTYNAME], function ($props) use ($node) {
401
-			if (!$node instanceof SystemTagObjectType) {
402
-				return false;
403
-			}
404
-
405
-			if (isset($props[self::OBJECTIDS_PROPERTYNAME])) {
406
-				$user = $this->userSession->getUser();
407
-				if (!$user) {
408
-					throw new Forbidden('You don’t have permissions to update tags');
409
-				}
410
-
411
-				$propValue = $props[self::OBJECTIDS_PROPERTYNAME];
412
-				if (!$propValue instanceof SystemTagsObjectList || count($propValue->getObjects()) === 0) {
413
-					throw new BadRequest('Invalid object-ids property');
414
-				}
415
-
416
-				$objects = $propValue->getObjects();
417
-				$objectTypes = array_unique(array_values($objects));
418
-
419
-				if (count($objectTypes) !== 1 || $objectTypes[0] !== $node->getName()) {
420
-					throw new BadRequest('Invalid object-ids property. All object types must be of the same type: ' . $node->getName());
421
-				}
422
-
423
-				// Only files are supported at the moment
424
-				// Also see SystemTagsRelationsCollection file
425
-				if ($objectTypes[0] !== 'files') {
426
-					throw new BadRequest('Invalid object-ids property type. Only files are supported');
427
-				}
428
-
429
-				// Get all current tagged objects
430
-				$taggedObjects = $this->tagMapper->getObjectIdsForTags([$node->getSystemTag()->getId()], 'files');
431
-				$toAddObjects = array_map(fn ($value) => (string)$value, array_keys($objects));
432
-
433
-				// Compute the tags to add and remove
434
-				$addedObjects = array_values(array_diff($toAddObjects, $taggedObjects));
435
-				$removedObjects = array_values(array_diff($taggedObjects, $toAddObjects));
436
-
437
-				// Check permissions for each object to be freshly tagged or untagged
438
-				if (!$this->canUpdateTagForFileIds(array_merge($addedObjects, $removedObjects))) {
439
-					throw new Forbidden('You don’t have permissions to update tags');
440
-				}
441
-
442
-				$this->tagMapper->setObjectIdsForTag($node->getSystemTag()->getId(), $node->getName(), array_keys($objects));
443
-			}
444
-
445
-			if ($props[self::OBJECTIDS_PROPERTYNAME] === null) {
446
-				// Check the user have permissions to remove the tag from all currently tagged objects
447
-				$taggedObjects = $this->tagMapper->getObjectIdsForTags([$node->getSystemTag()->getId()], 'files');
448
-				if (!$this->canUpdateTagForFileIds($taggedObjects)) {
449
-					throw new Forbidden('You don’t have permissions to update tags');
450
-				}
451
-
452
-				$this->tagMapper->setObjectIdsForTag($node->getSystemTag()->getId(), $node->getName(), []);
453
-			}
454
-
455
-			return true;
456
-		});
457
-
458
-		$propPatch->handle([
459
-			self::DISPLAYNAME_PROPERTYNAME,
460
-			self::USERVISIBLE_PROPERTYNAME,
461
-			self::USERASSIGNABLE_PROPERTYNAME,
462
-			self::GROUPS_PROPERTYNAME,
463
-			self::NUM_FILES_PROPERTYNAME,
464
-			self::REFERENCE_FILEID_PROPERTYNAME,
465
-			self::COLOR_PROPERTYNAME,
466
-		], function ($props) use ($node) {
467
-			if (!$node instanceof SystemTagNode) {
468
-				return false;
469
-			}
470
-
471
-			$tag = $node->getSystemTag();
472
-			$name = $tag->getName();
473
-			$userVisible = $tag->isUserVisible();
474
-			$userAssignable = $tag->isUserAssignable();
475
-			$color = $tag->getColor();
476
-
477
-			$updateTag = false;
478
-
479
-			if (isset($props[self::DISPLAYNAME_PROPERTYNAME])) {
480
-				$name = $props[self::DISPLAYNAME_PROPERTYNAME];
481
-				$updateTag = true;
482
-			}
483
-
484
-			if (isset($props[self::USERVISIBLE_PROPERTYNAME])) {
485
-				$propValue = $props[self::USERVISIBLE_PROPERTYNAME];
486
-				$userVisible = ($propValue !== 'false' && $propValue !== '0');
487
-				$updateTag = true;
488
-			}
489
-
490
-			if (isset($props[self::USERASSIGNABLE_PROPERTYNAME])) {
491
-				$propValue = $props[self::USERASSIGNABLE_PROPERTYNAME];
492
-				$userAssignable = ($propValue !== 'false' && $propValue !== '0');
493
-				$updateTag = true;
494
-			}
495
-
496
-			if (isset($props[self::COLOR_PROPERTYNAME])) {
497
-				$propValue = $props[self::COLOR_PROPERTYNAME];
498
-				if ($propValue === '' || $propValue === 'null') {
499
-					$propValue = null;
500
-				}
501
-				$color = $propValue;
502
-				$updateTag = true;
503
-			}
504
-
505
-			if (isset($props[self::GROUPS_PROPERTYNAME])) {
506
-				if (!$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) {
507
-					// property only available for admins
508
-					throw new Forbidden();
509
-				}
510
-
511
-				$propValue = $props[self::GROUPS_PROPERTYNAME];
512
-				$groupIds = explode('|', $propValue);
513
-				$this->tagManager->setTagGroups($tag, $groupIds);
514
-			}
515
-
516
-			if (isset($props[self::NUM_FILES_PROPERTYNAME]) || isset($props[self::REFERENCE_FILEID_PROPERTYNAME])) {
517
-				// read-only properties
518
-				throw new Forbidden();
519
-			}
520
-
521
-			if ($updateTag) {
522
-				try {
523
-					$node->update($name, $userVisible, $userAssignable, $color);
524
-				} catch (TagUpdateForbiddenException $e) {
525
-					throw new Forbidden('You don’t have permissions to update tags', 0, $e);
526
-				}
527
-			}
528
-
529
-			return true;
530
-		});
531
-	}
532
-
533
-	/**
534
-	 * Check if the user can update the tag for the given file ids
535
-	 *
536
-	 * @param list<string> $fileIds
537
-	 * @return bool
538
-	 */
539
-	private function canUpdateTagForFileIds(array $fileIds): bool {
540
-		$user = $this->userSession->getUser();
541
-		$userFolder = $this->rootFolder->getUserFolder($user->getUID());
542
-
543
-		foreach ($fileIds as $fileId) {
544
-			try {
545
-				$nodes = $userFolder->getById((int)$fileId);
546
-				if (empty($nodes)) {
547
-					return false;
548
-				}
549
-
550
-				foreach ($nodes as $node) {
551
-					if (($node->getPermissions() & Constants::PERMISSION_UPDATE) !== Constants::PERMISSION_UPDATE) {
552
-						return false;
553
-					}
554
-				}
555
-			} catch (\Exception $e) {
556
-				return false;
557
-			}
558
-		}
559
-
560
-		return true;
561
-	}
45
+    // namespace
46
+    public const NS_OWNCLOUD = 'http://owncloud.org/ns';
47
+    public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
48
+    public const ID_PROPERTYNAME = '{http://owncloud.org/ns}id';
49
+    public const DISPLAYNAME_PROPERTYNAME = '{http://owncloud.org/ns}display-name';
50
+    public const USERVISIBLE_PROPERTYNAME = '{http://owncloud.org/ns}user-visible';
51
+    public const USERASSIGNABLE_PROPERTYNAME = '{http://owncloud.org/ns}user-assignable';
52
+    public const GROUPS_PROPERTYNAME = '{http://owncloud.org/ns}groups';
53
+    public const CANASSIGN_PROPERTYNAME = '{http://owncloud.org/ns}can-assign';
54
+    public const SYSTEM_TAGS_PROPERTYNAME = '{http://nextcloud.org/ns}system-tags';
55
+    public const NUM_FILES_PROPERTYNAME = '{http://nextcloud.org/ns}files-assigned';
56
+    public const REFERENCE_FILEID_PROPERTYNAME = '{http://nextcloud.org/ns}reference-fileid';
57
+    public const OBJECTIDS_PROPERTYNAME = '{http://nextcloud.org/ns}object-ids';
58
+    public const COLOR_PROPERTYNAME = '{http://nextcloud.org/ns}color';
59
+
60
+    /**
61
+     * @var \Sabre\DAV\Server $server
62
+     */
63
+    private $server;
64
+
65
+    /** @var array<int, string[]> */
66
+    private array $cachedTagMappings = [];
67
+    /** @var array<string, ISystemTag> */
68
+    private array $cachedTags = [];
69
+
70
+    public function __construct(
71
+        protected ISystemTagManager $tagManager,
72
+        protected IGroupManager $groupManager,
73
+        protected IUserSession $userSession,
74
+        protected IRootFolder $rootFolder,
75
+        protected ISystemTagObjectMapper $tagMapper,
76
+    ) {
77
+    }
78
+
79
+    /**
80
+     * This initializes the plugin.
81
+     *
82
+     * This function is called by \Sabre\DAV\Server, after
83
+     * addPlugin is called.
84
+     *
85
+     * This method should set up the required event subscriptions.
86
+     *
87
+     * @param \Sabre\DAV\Server $server
88
+     * @return void
89
+     */
90
+    public function initialize(\Sabre\DAV\Server $server) {
91
+        $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
92
+        $server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';
93
+
94
+        $server->xml->elementMap[self::OBJECTIDS_PROPERTYNAME] = SystemTagsObjectList::class;
95
+
96
+        $server->protectedProperties[] = self::ID_PROPERTYNAME;
97
+
98
+        $server->on('preloadCollection', $this->preloadCollection(...));
99
+        $server->on('propFind', [$this, 'handleGetProperties']);
100
+        $server->on('propPatch', [$this, 'handleUpdateProperties']);
101
+        $server->on('method:POST', [$this, 'httpPost']);
102
+
103
+        $this->server = $server;
104
+    }
105
+
106
+    /**
107
+     * POST operation on system tag collections
108
+     *
109
+     * @param RequestInterface $request request object
110
+     * @param ResponseInterface $response response object
111
+     * @return null|false
112
+     */
113
+    public function httpPost(RequestInterface $request, ResponseInterface $response) {
114
+        $path = $request->getPath();
115
+
116
+        // Making sure the node exists
117
+        $node = $this->server->tree->getNodeForPath($path);
118
+        if ($node instanceof SystemTagsByIdCollection || $node instanceof SystemTagsObjectMappingCollection) {
119
+            $data = $request->getBodyAsString();
120
+
121
+            $tag = $this->createTag($data, $request->getHeader('Content-Type'));
122
+
123
+            if ($node instanceof SystemTagsObjectMappingCollection) {
124
+                // also add to collection
125
+                $node->createFile($tag->getId());
126
+                $url = $request->getBaseUrl() . 'systemtags/';
127
+            } else {
128
+                $url = $request->getUrl();
129
+            }
130
+
131
+            if ($url[strlen($url) - 1] !== '/') {
132
+                $url .= '/';
133
+            }
134
+
135
+            $response->setHeader('Content-Location', $url . $tag->getId());
136
+
137
+            // created
138
+            $response->setStatus(Http::STATUS_CREATED);
139
+            return false;
140
+        }
141
+    }
142
+
143
+    /**
144
+     * Creates a new tag
145
+     *
146
+     * @param string $data JSON encoded string containing the properties of the tag to create
147
+     * @param string $contentType content type of the data
148
+     * @return ISystemTag newly created system tag
149
+     *
150
+     * @throws BadRequest if a field was missing
151
+     * @throws Conflict if a tag with the same properties already exists
152
+     * @throws UnsupportedMediaType if the content type is not supported
153
+     */
154
+    private function createTag($data, $contentType = 'application/json') {
155
+        if (explode(';', $contentType)[0] === 'application/json') {
156
+            $data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
157
+        } else {
158
+            throw new UnsupportedMediaType();
159
+        }
160
+
161
+        if (!isset($data['name'])) {
162
+            throw new BadRequest('Missing "name" attribute');
163
+        }
164
+
165
+        $tagName = $data['name'];
166
+        $userVisible = true;
167
+        $userAssignable = true;
168
+
169
+        if (isset($data['userVisible'])) {
170
+            $userVisible = (bool)$data['userVisible'];
171
+        }
172
+
173
+        if (isset($data['userAssignable'])) {
174
+            $userAssignable = (bool)$data['userAssignable'];
175
+        }
176
+
177
+        $groups = [];
178
+        if (isset($data['groups'])) {
179
+            $groups = $data['groups'];
180
+            if (is_string($groups)) {
181
+                $groups = explode('|', $groups);
182
+            }
183
+        }
184
+
185
+        if ($userVisible === false || $userAssignable === false || !empty($groups)) {
186
+            if (!$this->userSession->isLoggedIn() || !$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) {
187
+                throw new BadRequest('Not sufficient permissions');
188
+            }
189
+        }
190
+
191
+        try {
192
+            $tag = $this->tagManager->createTag($tagName, $userVisible, $userAssignable);
193
+            if (!empty($groups)) {
194
+                $this->tagManager->setTagGroups($tag, $groups);
195
+            }
196
+            return $tag;
197
+        } catch (TagAlreadyExistsException $e) {
198
+            throw new Conflict('Tag already exists', 0, $e);
199
+        } catch (TagCreationForbiddenException $e) {
200
+            throw new Forbidden('You don’t have permissions to create tags', 0, $e);
201
+        }
202
+    }
203
+
204
+    private function preloadCollection(
205
+        PropFind $propFind,
206
+        ICollection $collection,
207
+    ): void {
208
+        if (!$collection instanceof Node) {
209
+            return;
210
+        }
211
+
212
+        if ($collection instanceof Directory
213
+            && !isset($this->cachedTagMappings[$collection->getId()])
214
+            && $propFind->getStatus(
215
+                self::SYSTEM_TAGS_PROPERTYNAME
216
+            ) !== null) {
217
+            $fileIds = [$collection->getId()];
218
+
219
+            // note: pre-fetching only supported for depth <= 1
220
+            $folderContent = $collection->getChildren();
221
+            foreach ($folderContent as $info) {
222
+                if ($info instanceof Node) {
223
+                    $fileIds[] = $info->getId();
224
+                }
225
+            }
226
+
227
+            $tags = $this->tagMapper->getTagIdsForObjects($fileIds, 'files');
228
+
229
+            $this->cachedTagMappings += $tags;
230
+            $emptyFileIds = array_diff($fileIds, array_keys($tags));
231
+
232
+            // also cache the ones that were not found
233
+            foreach ($emptyFileIds as $fileId) {
234
+                $this->cachedTagMappings[$fileId] = [];
235
+            }
236
+        }
237
+    }
238
+
239
+    /**
240
+     * Retrieves system tag properties
241
+     *
242
+     * @param PropFind $propFind
243
+     * @param \Sabre\DAV\INode $node
244
+     *
245
+     * @return void
246
+     */
247
+    public function handleGetProperties(
248
+        PropFind $propFind,
249
+        \Sabre\DAV\INode $node,
250
+    ) {
251
+        if ($node instanceof Node) {
252
+            $this->propfindForFile($propFind, $node);
253
+            return;
254
+        }
255
+
256
+        if (!$node instanceof SystemTagNode && !$node instanceof SystemTagMappingNode && !$node instanceof SystemTagObjectType) {
257
+            return;
258
+        }
259
+
260
+        // child nodes from systemtags-assigned should point to normal tag endpoint
261
+        if (preg_match('/^systemtags-assigned\/[0-9]+/', $propFind->getPath())) {
262
+            $propFind->setPath(str_replace('systemtags-assigned/', 'systemtags/', $propFind->getPath()));
263
+        }
264
+
265
+        $propFind->handle(FilesPlugin::GETETAG_PROPERTYNAME, function () use ($node): string {
266
+            return '"' . ($node->getSystemTag()->getETag() ?? '') . '"';
267
+        });
268
+
269
+        $propFind->handle(self::ID_PROPERTYNAME, function () use ($node) {
270
+            return $node->getSystemTag()->getId();
271
+        });
272
+
273
+        $propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function () use ($node) {
274
+            return $node->getSystemTag()->getName();
275
+        });
276
+
277
+        $propFind->handle(self::USERVISIBLE_PROPERTYNAME, function () use ($node) {
278
+            return $node->getSystemTag()->isUserVisible() ? 'true' : 'false';
279
+        });
280
+
281
+        $propFind->handle(self::USERASSIGNABLE_PROPERTYNAME, function () use ($node) {
282
+            // this is the tag's inherent property "is user assignable"
283
+            return $node->getSystemTag()->isUserAssignable() ? 'true' : 'false';
284
+        });
285
+
286
+        $propFind->handle(self::CANASSIGN_PROPERTYNAME, function () use ($node) {
287
+            // this is the effective permission for the current user
288
+            return $this->tagManager->canUserAssignTag($node->getSystemTag(), $this->userSession->getUser()) ? 'true' : 'false';
289
+        });
290
+
291
+        $propFind->handle(self::COLOR_PROPERTYNAME, function () use ($node) {
292
+            return $node->getSystemTag()->getColor() ?? '';
293
+        });
294
+
295
+        $propFind->handle(self::GROUPS_PROPERTYNAME, function () use ($node) {
296
+            if (!$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) {
297
+                // property only available for admins
298
+                throw new Forbidden();
299
+            }
300
+            $groups = [];
301
+            // no need to retrieve groups for namespaces that don't qualify
302
+            if ($node->getSystemTag()->isUserVisible() && !$node->getSystemTag()->isUserAssignable()) {
303
+                $groups = $this->tagManager->getTagGroups($node->getSystemTag());
304
+            }
305
+            return implode('|', $groups);
306
+        });
307
+
308
+        if ($node instanceof SystemTagNode) {
309
+            $propFind->handle(self::NUM_FILES_PROPERTYNAME, function () use ($node): int {
310
+                return $node->getNumberOfFiles();
311
+            });
312
+
313
+            $propFind->handle(self::REFERENCE_FILEID_PROPERTYNAME, function () use ($node): int {
314
+                return $node->getReferenceFileId();
315
+            });
316
+
317
+            $propFind->handle(self::OBJECTIDS_PROPERTYNAME, function () use ($node): SystemTagsObjectList {
318
+                $objectTypes = $this->tagMapper->getAvailableObjectTypes();
319
+                $objects = [];
320
+                foreach ($objectTypes as $type) {
321
+                    $systemTagObjectType = new SystemTagObjectType($node->getSystemTag(), $type, $this->tagManager, $this->tagMapper);
322
+                    $objects = array_merge($objects, array_fill_keys($systemTagObjectType->getObjectsIds(), $type));
323
+                }
324
+                return new SystemTagsObjectList($objects);
325
+            });
326
+        }
327
+
328
+        if ($node instanceof SystemTagObjectType) {
329
+            $propFind->handle(self::OBJECTIDS_PROPERTYNAME, function () use ($node): SystemTagsObjectList {
330
+                return new SystemTagsObjectList(array_fill_keys($node->getObjectsIds(), $node->getName()));
331
+            });
332
+        }
333
+    }
334
+
335
+    private function propfindForFile(PropFind $propFind, Node $node): void {
336
+
337
+        $propFind->handle(self::SYSTEM_TAGS_PROPERTYNAME, function () use ($node) {
338
+            $user = $this->userSession->getUser();
339
+
340
+            $tags = $this->getTagsForFile($node->getId(), $user);
341
+            usort($tags, function (ISystemTag $tagA, ISystemTag $tagB): int {
342
+                return Util::naturalSortCompare($tagA->getName(), $tagB->getName());
343
+            });
344
+            return new SystemTagList($tags, $this->tagManager, $user);
345
+        });
346
+    }
347
+
348
+    /**
349
+     * @param int $fileId
350
+     * @return ISystemTag[]
351
+     */
352
+    private function getTagsForFile(int $fileId, ?IUser $user): array {
353
+        if (isset($this->cachedTagMappings[$fileId])) {
354
+            $tagIds = $this->cachedTagMappings[$fileId];
355
+        } else {
356
+            $tags = $this->tagMapper->getTagIdsForObjects([$fileId], 'files');
357
+            $fileTags = current($tags);
358
+            if ($fileTags) {
359
+                $tagIds = $fileTags;
360
+            } else {
361
+                $tagIds = [];
362
+            }
363
+        }
364
+
365
+        $tags = array_filter(array_map(function (string $tagId) {
366
+            return $this->cachedTags[$tagId] ?? null;
367
+        }, $tagIds));
368
+
369
+        $uncachedTagIds = array_filter($tagIds, function (string $tagId): bool {
370
+            return !isset($this->cachedTags[$tagId]);
371
+        });
372
+
373
+        if (count($uncachedTagIds)) {
374
+            $retrievedTags = $this->tagManager->getTagsByIds($uncachedTagIds);
375
+            foreach ($retrievedTags as $tag) {
376
+                $this->cachedTags[$tag->getId()] = $tag;
377
+            }
378
+            $tags += $retrievedTags;
379
+        }
380
+
381
+        return array_filter($tags, function (ISystemTag $tag) use ($user) {
382
+            return $this->tagManager->canUserSeeTag($tag, $user);
383
+        });
384
+    }
385
+
386
+    /**
387
+     * Updates tag attributes
388
+     *
389
+     * @param string $path
390
+     * @param PropPatch $propPatch
391
+     *
392
+     * @return void
393
+     */
394
+    public function handleUpdateProperties($path, PropPatch $propPatch) {
395
+        $node = $this->server->tree->getNodeForPath($path);
396
+        if (!$node instanceof SystemTagNode && !$node instanceof SystemTagObjectType) {
397
+            return;
398
+        }
399
+
400
+        $propPatch->handle([self::OBJECTIDS_PROPERTYNAME], function ($props) use ($node) {
401
+            if (!$node instanceof SystemTagObjectType) {
402
+                return false;
403
+            }
404
+
405
+            if (isset($props[self::OBJECTIDS_PROPERTYNAME])) {
406
+                $user = $this->userSession->getUser();
407
+                if (!$user) {
408
+                    throw new Forbidden('You don’t have permissions to update tags');
409
+                }
410
+
411
+                $propValue = $props[self::OBJECTIDS_PROPERTYNAME];
412
+                if (!$propValue instanceof SystemTagsObjectList || count($propValue->getObjects()) === 0) {
413
+                    throw new BadRequest('Invalid object-ids property');
414
+                }
415
+
416
+                $objects = $propValue->getObjects();
417
+                $objectTypes = array_unique(array_values($objects));
418
+
419
+                if (count($objectTypes) !== 1 || $objectTypes[0] !== $node->getName()) {
420
+                    throw new BadRequest('Invalid object-ids property. All object types must be of the same type: ' . $node->getName());
421
+                }
422
+
423
+                // Only files are supported at the moment
424
+                // Also see SystemTagsRelationsCollection file
425
+                if ($objectTypes[0] !== 'files') {
426
+                    throw new BadRequest('Invalid object-ids property type. Only files are supported');
427
+                }
428
+
429
+                // Get all current tagged objects
430
+                $taggedObjects = $this->tagMapper->getObjectIdsForTags([$node->getSystemTag()->getId()], 'files');
431
+                $toAddObjects = array_map(fn ($value) => (string)$value, array_keys($objects));
432
+
433
+                // Compute the tags to add and remove
434
+                $addedObjects = array_values(array_diff($toAddObjects, $taggedObjects));
435
+                $removedObjects = array_values(array_diff($taggedObjects, $toAddObjects));
436
+
437
+                // Check permissions for each object to be freshly tagged or untagged
438
+                if (!$this->canUpdateTagForFileIds(array_merge($addedObjects, $removedObjects))) {
439
+                    throw new Forbidden('You don’t have permissions to update tags');
440
+                }
441
+
442
+                $this->tagMapper->setObjectIdsForTag($node->getSystemTag()->getId(), $node->getName(), array_keys($objects));
443
+            }
444
+
445
+            if ($props[self::OBJECTIDS_PROPERTYNAME] === null) {
446
+                // Check the user have permissions to remove the tag from all currently tagged objects
447
+                $taggedObjects = $this->tagMapper->getObjectIdsForTags([$node->getSystemTag()->getId()], 'files');
448
+                if (!$this->canUpdateTagForFileIds($taggedObjects)) {
449
+                    throw new Forbidden('You don’t have permissions to update tags');
450
+                }
451
+
452
+                $this->tagMapper->setObjectIdsForTag($node->getSystemTag()->getId(), $node->getName(), []);
453
+            }
454
+
455
+            return true;
456
+        });
457
+
458
+        $propPatch->handle([
459
+            self::DISPLAYNAME_PROPERTYNAME,
460
+            self::USERVISIBLE_PROPERTYNAME,
461
+            self::USERASSIGNABLE_PROPERTYNAME,
462
+            self::GROUPS_PROPERTYNAME,
463
+            self::NUM_FILES_PROPERTYNAME,
464
+            self::REFERENCE_FILEID_PROPERTYNAME,
465
+            self::COLOR_PROPERTYNAME,
466
+        ], function ($props) use ($node) {
467
+            if (!$node instanceof SystemTagNode) {
468
+                return false;
469
+            }
470
+
471
+            $tag = $node->getSystemTag();
472
+            $name = $tag->getName();
473
+            $userVisible = $tag->isUserVisible();
474
+            $userAssignable = $tag->isUserAssignable();
475
+            $color = $tag->getColor();
476
+
477
+            $updateTag = false;
478
+
479
+            if (isset($props[self::DISPLAYNAME_PROPERTYNAME])) {
480
+                $name = $props[self::DISPLAYNAME_PROPERTYNAME];
481
+                $updateTag = true;
482
+            }
483
+
484
+            if (isset($props[self::USERVISIBLE_PROPERTYNAME])) {
485
+                $propValue = $props[self::USERVISIBLE_PROPERTYNAME];
486
+                $userVisible = ($propValue !== 'false' && $propValue !== '0');
487
+                $updateTag = true;
488
+            }
489
+
490
+            if (isset($props[self::USERASSIGNABLE_PROPERTYNAME])) {
491
+                $propValue = $props[self::USERASSIGNABLE_PROPERTYNAME];
492
+                $userAssignable = ($propValue !== 'false' && $propValue !== '0');
493
+                $updateTag = true;
494
+            }
495
+
496
+            if (isset($props[self::COLOR_PROPERTYNAME])) {
497
+                $propValue = $props[self::COLOR_PROPERTYNAME];
498
+                if ($propValue === '' || $propValue === 'null') {
499
+                    $propValue = null;
500
+                }
501
+                $color = $propValue;
502
+                $updateTag = true;
503
+            }
504
+
505
+            if (isset($props[self::GROUPS_PROPERTYNAME])) {
506
+                if (!$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) {
507
+                    // property only available for admins
508
+                    throw new Forbidden();
509
+                }
510
+
511
+                $propValue = $props[self::GROUPS_PROPERTYNAME];
512
+                $groupIds = explode('|', $propValue);
513
+                $this->tagManager->setTagGroups($tag, $groupIds);
514
+            }
515
+
516
+            if (isset($props[self::NUM_FILES_PROPERTYNAME]) || isset($props[self::REFERENCE_FILEID_PROPERTYNAME])) {
517
+                // read-only properties
518
+                throw new Forbidden();
519
+            }
520
+
521
+            if ($updateTag) {
522
+                try {
523
+                    $node->update($name, $userVisible, $userAssignable, $color);
524
+                } catch (TagUpdateForbiddenException $e) {
525
+                    throw new Forbidden('You don’t have permissions to update tags', 0, $e);
526
+                }
527
+            }
528
+
529
+            return true;
530
+        });
531
+    }
532
+
533
+    /**
534
+     * Check if the user can update the tag for the given file ids
535
+     *
536
+     * @param list<string> $fileIds
537
+     * @return bool
538
+     */
539
+    private function canUpdateTagForFileIds(array $fileIds): bool {
540
+        $user = $this->userSession->getUser();
541
+        $userFolder = $this->rootFolder->getUserFolder($user->getUID());
542
+
543
+        foreach ($fileIds as $fileId) {
544
+            try {
545
+                $nodes = $userFolder->getById((int)$fileId);
546
+                if (empty($nodes)) {
547
+                    return false;
548
+                }
549
+
550
+                foreach ($nodes as $node) {
551
+                    if (($node->getPermissions() & Constants::PERMISSION_UPDATE) !== Constants::PERMISSION_UPDATE) {
552
+                        return false;
553
+                    }
554
+                }
555
+            } catch (\Exception $e) {
556
+                return false;
557
+            }
558
+        }
559
+
560
+        return true;
561
+    }
562 562
 }
Please login to merge, or discard this patch.
Spacing   +27 added lines, -27 removed lines patch added patch discarded remove patch
@@ -123,7 +123,7 @@  discard block
 block discarded – undo
123 123
 			if ($node instanceof SystemTagsObjectMappingCollection) {
124 124
 				// also add to collection
125 125
 				$node->createFile($tag->getId());
126
-				$url = $request->getBaseUrl() . 'systemtags/';
126
+				$url = $request->getBaseUrl().'systemtags/';
127 127
 			} else {
128 128
 				$url = $request->getUrl();
129 129
 			}
@@ -132,7 +132,7 @@  discard block
 block discarded – undo
132 132
 				$url .= '/';
133 133
 			}
134 134
 
135
-			$response->setHeader('Content-Location', $url . $tag->getId());
135
+			$response->setHeader('Content-Location', $url.$tag->getId());
136 136
 
137 137
 			// created
138 138
 			$response->setStatus(Http::STATUS_CREATED);
@@ -167,11 +167,11 @@  discard block
 block discarded – undo
167 167
 		$userAssignable = true;
168 168
 
169 169
 		if (isset($data['userVisible'])) {
170
-			$userVisible = (bool)$data['userVisible'];
170
+			$userVisible = (bool) $data['userVisible'];
171 171
 		}
172 172
 
173 173
 		if (isset($data['userAssignable'])) {
174
-			$userAssignable = (bool)$data['userAssignable'];
174
+			$userAssignable = (bool) $data['userAssignable'];
175 175
 		}
176 176
 
177 177
 		$groups = [];
@@ -262,37 +262,37 @@  discard block
 block discarded – undo
262 262
 			$propFind->setPath(str_replace('systemtags-assigned/', 'systemtags/', $propFind->getPath()));
263 263
 		}
264 264
 
265
-		$propFind->handle(FilesPlugin::GETETAG_PROPERTYNAME, function () use ($node): string {
266
-			return '"' . ($node->getSystemTag()->getETag() ?? '') . '"';
265
+		$propFind->handle(FilesPlugin::GETETAG_PROPERTYNAME, function() use ($node): string {
266
+			return '"'.($node->getSystemTag()->getETag() ?? '').'"';
267 267
 		});
268 268
 
269
-		$propFind->handle(self::ID_PROPERTYNAME, function () use ($node) {
269
+		$propFind->handle(self::ID_PROPERTYNAME, function() use ($node) {
270 270
 			return $node->getSystemTag()->getId();
271 271
 		});
272 272
 
273
-		$propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function () use ($node) {
273
+		$propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function() use ($node) {
274 274
 			return $node->getSystemTag()->getName();
275 275
 		});
276 276
 
277
-		$propFind->handle(self::USERVISIBLE_PROPERTYNAME, function () use ($node) {
277
+		$propFind->handle(self::USERVISIBLE_PROPERTYNAME, function() use ($node) {
278 278
 			return $node->getSystemTag()->isUserVisible() ? 'true' : 'false';
279 279
 		});
280 280
 
281
-		$propFind->handle(self::USERASSIGNABLE_PROPERTYNAME, function () use ($node) {
281
+		$propFind->handle(self::USERASSIGNABLE_PROPERTYNAME, function() use ($node) {
282 282
 			// this is the tag's inherent property "is user assignable"
283 283
 			return $node->getSystemTag()->isUserAssignable() ? 'true' : 'false';
284 284
 		});
285 285
 
286
-		$propFind->handle(self::CANASSIGN_PROPERTYNAME, function () use ($node) {
286
+		$propFind->handle(self::CANASSIGN_PROPERTYNAME, function() use ($node) {
287 287
 			// this is the effective permission for the current user
288 288
 			return $this->tagManager->canUserAssignTag($node->getSystemTag(), $this->userSession->getUser()) ? 'true' : 'false';
289 289
 		});
290 290
 
291
-		$propFind->handle(self::COLOR_PROPERTYNAME, function () use ($node) {
291
+		$propFind->handle(self::COLOR_PROPERTYNAME, function() use ($node) {
292 292
 			return $node->getSystemTag()->getColor() ?? '';
293 293
 		});
294 294
 
295
-		$propFind->handle(self::GROUPS_PROPERTYNAME, function () use ($node) {
295
+		$propFind->handle(self::GROUPS_PROPERTYNAME, function() use ($node) {
296 296
 			if (!$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) {
297 297
 				// property only available for admins
298 298
 				throw new Forbidden();
@@ -306,15 +306,15 @@  discard block
 block discarded – undo
306 306
 		});
307 307
 
308 308
 		if ($node instanceof SystemTagNode) {
309
-			$propFind->handle(self::NUM_FILES_PROPERTYNAME, function () use ($node): int {
309
+			$propFind->handle(self::NUM_FILES_PROPERTYNAME, function() use ($node): int {
310 310
 				return $node->getNumberOfFiles();
311 311
 			});
312 312
 
313
-			$propFind->handle(self::REFERENCE_FILEID_PROPERTYNAME, function () use ($node): int {
313
+			$propFind->handle(self::REFERENCE_FILEID_PROPERTYNAME, function() use ($node): int {
314 314
 				return $node->getReferenceFileId();
315 315
 			});
316 316
 
317
-			$propFind->handle(self::OBJECTIDS_PROPERTYNAME, function () use ($node): SystemTagsObjectList {
317
+			$propFind->handle(self::OBJECTIDS_PROPERTYNAME, function() use ($node): SystemTagsObjectList {
318 318
 				$objectTypes = $this->tagMapper->getAvailableObjectTypes();
319 319
 				$objects = [];
320 320
 				foreach ($objectTypes as $type) {
@@ -326,7 +326,7 @@  discard block
 block discarded – undo
326 326
 		}
327 327
 
328 328
 		if ($node instanceof SystemTagObjectType) {
329
-			$propFind->handle(self::OBJECTIDS_PROPERTYNAME, function () use ($node): SystemTagsObjectList {
329
+			$propFind->handle(self::OBJECTIDS_PROPERTYNAME, function() use ($node): SystemTagsObjectList {
330 330
 				return new SystemTagsObjectList(array_fill_keys($node->getObjectsIds(), $node->getName()));
331 331
 			});
332 332
 		}
@@ -334,11 +334,11 @@  discard block
 block discarded – undo
334 334
 
335 335
 	private function propfindForFile(PropFind $propFind, Node $node): void {
336 336
 
337
-		$propFind->handle(self::SYSTEM_TAGS_PROPERTYNAME, function () use ($node) {
337
+		$propFind->handle(self::SYSTEM_TAGS_PROPERTYNAME, function() use ($node) {
338 338
 			$user = $this->userSession->getUser();
339 339
 
340 340
 			$tags = $this->getTagsForFile($node->getId(), $user);
341
-			usort($tags, function (ISystemTag $tagA, ISystemTag $tagB): int {
341
+			usort($tags, function(ISystemTag $tagA, ISystemTag $tagB): int {
342 342
 				return Util::naturalSortCompare($tagA->getName(), $tagB->getName());
343 343
 			});
344 344
 			return new SystemTagList($tags, $this->tagManager, $user);
@@ -362,11 +362,11 @@  discard block
 block discarded – undo
362 362
 			}
363 363
 		}
364 364
 
365
-		$tags = array_filter(array_map(function (string $tagId) {
365
+		$tags = array_filter(array_map(function(string $tagId) {
366 366
 			return $this->cachedTags[$tagId] ?? null;
367 367
 		}, $tagIds));
368 368
 
369
-		$uncachedTagIds = array_filter($tagIds, function (string $tagId): bool {
369
+		$uncachedTagIds = array_filter($tagIds, function(string $tagId): bool {
370 370
 			return !isset($this->cachedTags[$tagId]);
371 371
 		});
372 372
 
@@ -378,7 +378,7 @@  discard block
 block discarded – undo
378 378
 			$tags += $retrievedTags;
379 379
 		}
380 380
 
381
-		return array_filter($tags, function (ISystemTag $tag) use ($user) {
381
+		return array_filter($tags, function(ISystemTag $tag) use ($user) {
382 382
 			return $this->tagManager->canUserSeeTag($tag, $user);
383 383
 		});
384 384
 	}
@@ -397,7 +397,7 @@  discard block
 block discarded – undo
397 397
 			return;
398 398
 		}
399 399
 
400
-		$propPatch->handle([self::OBJECTIDS_PROPERTYNAME], function ($props) use ($node) {
400
+		$propPatch->handle([self::OBJECTIDS_PROPERTYNAME], function($props) use ($node) {
401 401
 			if (!$node instanceof SystemTagObjectType) {
402 402
 				return false;
403 403
 			}
@@ -417,7 +417,7 @@  discard block
 block discarded – undo
417 417
 				$objectTypes = array_unique(array_values($objects));
418 418
 
419 419
 				if (count($objectTypes) !== 1 || $objectTypes[0] !== $node->getName()) {
420
-					throw new BadRequest('Invalid object-ids property. All object types must be of the same type: ' . $node->getName());
420
+					throw new BadRequest('Invalid object-ids property. All object types must be of the same type: '.$node->getName());
421 421
 				}
422 422
 
423 423
 				// Only files are supported at the moment
@@ -428,7 +428,7 @@  discard block
 block discarded – undo
428 428
 
429 429
 				// Get all current tagged objects
430 430
 				$taggedObjects = $this->tagMapper->getObjectIdsForTags([$node->getSystemTag()->getId()], 'files');
431
-				$toAddObjects = array_map(fn ($value) => (string)$value, array_keys($objects));
431
+				$toAddObjects = array_map(fn ($value) => (string) $value, array_keys($objects));
432 432
 
433 433
 				// Compute the tags to add and remove
434 434
 				$addedObjects = array_values(array_diff($toAddObjects, $taggedObjects));
@@ -463,7 +463,7 @@  discard block
 block discarded – undo
463 463
 			self::NUM_FILES_PROPERTYNAME,
464 464
 			self::REFERENCE_FILEID_PROPERTYNAME,
465 465
 			self::COLOR_PROPERTYNAME,
466
-		], function ($props) use ($node) {
466
+		], function($props) use ($node) {
467 467
 			if (!$node instanceof SystemTagNode) {
468 468
 				return false;
469 469
 			}
@@ -542,7 +542,7 @@  discard block
 block discarded – undo
542 542
 
543 543
 		foreach ($fileIds as $fileId) {
544 544
 			try {
545
-				$nodes = $userFolder->getById((int)$fileId);
545
+				$nodes = $userFolder->getById((int) $fileId);
546 546
 				if (empty($nodes)) {
547 547
 					return false;
548 548
 				}
Please login to merge, or discard this patch.