Completed
Push — master ( bed94c...faf16f )
by
unknown
56:38 queued 25:46
created
apps/dav/lib/Connector/Sabre/FilesPlugin.php 1 patch
Indentation   +698 added lines, -698 removed lines patch added patch discarded remove patch
@@ -40,702 +40,702 @@
 block discarded – undo
40 40
 use Sabre\HTTP\ResponseInterface;
41 41
 
42 42
 class FilesPlugin extends ServerPlugin {
43
-	// namespace
44
-	public const NS_OWNCLOUD = 'http://owncloud.org/ns';
45
-	public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
46
-	public const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id';
47
-	public const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid';
48
-	public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
49
-	public const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions';
50
-	public const OCM_SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-cloud-mesh.org/ns}share-permissions';
51
-	public const SHARE_ATTRIBUTES_PROPERTYNAME = '{http://nextcloud.org/ns}share-attributes';
52
-	public const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL';
53
-	public const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size';
54
-	public const GETETAG_PROPERTYNAME = '{DAV:}getetag';
55
-	public const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified';
56
-	public const CREATIONDATE_PROPERTYNAME = '{DAV:}creationdate';
57
-	public const DISPLAYNAME_PROPERTYNAME = '{DAV:}displayname';
58
-	public const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id';
59
-	public const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name';
60
-	public const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums';
61
-	public const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint';
62
-	public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
63
-	public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type';
64
-	public const MOUNT_ROOT_PROPERTYNAME = '{http://nextcloud.org/ns}is-mount-root';
65
-	public const IS_FEDERATED_PROPERTYNAME = '{http://nextcloud.org/ns}is-federated';
66
-	public const METADATA_ETAG_PROPERTYNAME = '{http://nextcloud.org/ns}metadata_etag';
67
-	public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time';
68
-	public const CREATION_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}creation_time';
69
-	public const SHARE_NOTE = '{http://nextcloud.org/ns}note';
70
-	public const SHARE_HIDE_DOWNLOAD_PROPERTYNAME = '{http://nextcloud.org/ns}hide-download';
71
-	public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count';
72
-	public const SUBFILE_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-file-count';
73
-	public const FILE_METADATA_PREFIX = '{http://nextcloud.org/ns}metadata-';
74
-	public const HIDDEN_PROPERTYNAME = '{http://nextcloud.org/ns}hidden';
75
-
76
-	/** Reference to main server object */
77
-	private ?Server $server = null;
78
-
79
-	/**
80
-	 * @param Tree $tree
81
-	 * @param IConfig $config
82
-	 * @param IRequest $request
83
-	 * @param IPreview $previewManager
84
-	 * @param IUserSession $userSession
85
-	 * @param bool $isPublic Whether this is public WebDAV. If true, some returned information will be stripped off.
86
-	 * @param bool $downloadAttachment
87
-	 * @return void
88
-	 */
89
-	public function __construct(
90
-		private Tree $tree,
91
-		private IConfig $config,
92
-		private IRequest $request,
93
-		private IPreview $previewManager,
94
-		private IUserSession $userSession,
95
-		private IFilenameValidator $validator,
96
-		private IAccountManager $accountManager,
97
-		private bool $isPublic = false,
98
-		private bool $downloadAttachment = true,
99
-	) {
100
-	}
101
-
102
-	/**
103
-	 * This initializes the plugin.
104
-	 *
105
-	 * This function is called by \Sabre\DAV\Server, after
106
-	 * addPlugin is called.
107
-	 *
108
-	 * This method should set up the required event subscriptions.
109
-	 *
110
-	 * @return void
111
-	 */
112
-	public function initialize(Server $server) {
113
-		$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
114
-		$server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';
115
-		$server->protectedProperties[] = self::FILEID_PROPERTYNAME;
116
-		$server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME;
117
-		$server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME;
118
-		$server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME;
119
-		$server->protectedProperties[] = self::OCM_SHARE_PERMISSIONS_PROPERTYNAME;
120
-		$server->protectedProperties[] = self::SHARE_ATTRIBUTES_PROPERTYNAME;
121
-		$server->protectedProperties[] = self::SIZE_PROPERTYNAME;
122
-		$server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME;
123
-		$server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME;
124
-		$server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME;
125
-		$server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME;
126
-		$server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME;
127
-		$server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME;
128
-		$server->protectedProperties[] = self::MOUNT_TYPE_PROPERTYNAME;
129
-		$server->protectedProperties[] = self::IS_FEDERATED_PROPERTYNAME;
130
-		$server->protectedProperties[] = self::SHARE_NOTE;
131
-
132
-		// normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH
133
-		$allowedProperties = ['{DAV:}getetag'];
134
-		$server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties);
135
-
136
-		$this->server = $server;
137
-		$this->server->on('propFind', [$this, 'handleGetProperties']);
138
-		$this->server->on('propPatch', [$this, 'handleUpdateProperties']);
139
-		$this->server->on('afterBind', [$this, 'sendFileIdHeader']);
140
-		$this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']);
141
-		$this->server->on('afterMethod:GET', [$this,'httpGet']);
142
-		$this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']);
143
-		$this->server->on('afterResponse', function ($request, ResponseInterface $response): void {
144
-			$body = $response->getBody();
145
-			if (is_resource($body)) {
146
-				fclose($body);
147
-			}
148
-		});
149
-		$this->server->on('beforeMove', [$this, 'checkMove']);
150
-		$this->server->on('beforeCopy', [$this, 'checkCopy']);
151
-	}
152
-
153
-	/**
154
-	 * Plugin that checks if a copy can actually be performed.
155
-	 *
156
-	 * @param string $source source path
157
-	 * @param string $target target path
158
-	 * @throws NotFound If the source does not exist
159
-	 * @throws InvalidPath If the target is invalid
160
-	 */
161
-	public function checkCopy($source, $target): void {
162
-		$sourceNode = $this->tree->getNodeForPath($source);
163
-		if (!$sourceNode instanceof Node) {
164
-			return;
165
-		}
166
-
167
-		// Ensure source exists
168
-		$sourceNodeFileInfo = $sourceNode->getFileInfo();
169
-		if ($sourceNodeFileInfo === null) {
170
-			throw new NotFound($source . ' does not exist');
171
-		}
172
-		// Ensure the target name is valid
173
-		try {
174
-			[$targetPath, $targetName] = \Sabre\Uri\split($target);
175
-			$this->validator->validateFilename($targetName);
176
-		} catch (InvalidPathException $e) {
177
-			throw new InvalidPath($e->getMessage(), false);
178
-		}
179
-		// Ensure the target path is valid
180
-		$segments = array_slice(explode('/', $targetPath), 2);
181
-		foreach ($segments as $segment) {
182
-			if ($this->validator->isFilenameValid($segment) === false) {
183
-				$l = \OCP\Server::get(IFactory::class)->get('dav');
184
-				throw new InvalidPath($l->t('Invalid target path'));
185
-			}
186
-		}
187
-	}
188
-
189
-	/**
190
-	 * Plugin that checks if a move can actually be performed.
191
-	 *
192
-	 * @param string $source source path
193
-	 * @param string $target target path
194
-	 * @throws Forbidden If the source is not deletable
195
-	 * @throws NotFound If the source does not exist
196
-	 * @throws InvalidPath If the target name is invalid
197
-	 */
198
-	public function checkMove(string $source, string $target): void {
199
-		$sourceNode = $this->tree->getNodeForPath($source);
200
-		if (!$sourceNode instanceof Node) {
201
-			return;
202
-		}
203
-
204
-		// First check copyable (move only needs additional delete permission)
205
-		$this->checkCopy($source, $target);
206
-
207
-		// The source needs to be deletable for moving
208
-		$sourceNodeFileInfo = $sourceNode->getFileInfo();
209
-		if (!$sourceNodeFileInfo->isDeletable()) {
210
-			throw new Forbidden($source . ' cannot be deleted');
211
-		}
212
-
213
-		// The source is not allowed to be the parent of the target
214
-		if (str_starts_with($source, $target . '/')) {
215
-			throw new Forbidden($source . ' cannot be moved to it\'s parent');
216
-		}
217
-	}
218
-
219
-	/**
220
-	 * This sets a cookie to be able to recognize the start of the download
221
-	 * the content must not be longer than 32 characters and must only contain
222
-	 * alphanumeric characters
223
-	 *
224
-	 * @param RequestInterface $request
225
-	 * @param ResponseInterface $response
226
-	 */
227
-	public function handleDownloadToken(RequestInterface $request, ResponseInterface $response) {
228
-		$queryParams = $request->getQueryParameters();
229
-
230
-		/**
231
-		 * this sets a cookie to be able to recognize the start of the download
232
-		 * the content must not be longer than 32 characters and must only contain
233
-		 * alphanumeric characters
234
-		 */
235
-		if (isset($queryParams['downloadStartSecret'])) {
236
-			$token = $queryParams['downloadStartSecret'];
237
-			if (!isset($token[32])
238
-				&& preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) {
239
-				// FIXME: use $response->setHeader() instead
240
-				setcookie('ocDownloadStarted', $token, time() + 20, '/');
241
-			}
242
-		}
243
-	}
244
-
245
-	/**
246
-	 * Add headers to file download
247
-	 *
248
-	 * @param RequestInterface $request
249
-	 * @param ResponseInterface $response
250
-	 */
251
-	public function httpGet(RequestInterface $request, ResponseInterface $response) {
252
-		// Only handle valid files
253
-		$node = $this->tree->getNodeForPath($request->getPath());
254
-		if (!($node instanceof IFile)) {
255
-			return;
256
-		}
257
-
258
-		// adds a 'Content-Disposition: attachment' header in case no disposition
259
-		// header has been set before
260
-		if ($this->downloadAttachment &&
261
-			$response->getHeader('Content-Disposition') === null) {
262
-			$filename = $node->getName();
263
-			if ($this->request->isUserAgent(
264
-				[
265
-					Request::USER_AGENT_IE,
266
-					Request::USER_AGENT_ANDROID_MOBILE_CHROME,
267
-					Request::USER_AGENT_FREEBOX,
268
-				])) {
269
-				$response->addHeader('Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"');
270
-			} else {
271
-				$response->addHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . rawurlencode($filename)
272
-													 . '; filename="' . rawurlencode($filename) . '"');
273
-			}
274
-		}
275
-
276
-		if ($node instanceof File) {
277
-			//Add OC-Checksum header
278
-			$checksum = $node->getChecksum();
279
-			if ($checksum !== null && $checksum !== '') {
280
-				$response->addHeader('OC-Checksum', $checksum);
281
-			}
282
-		}
283
-		$response->addHeader('X-Accel-Buffering', 'no');
284
-	}
285
-
286
-	/**
287
-	 * Adds all ownCloud-specific properties
288
-	 *
289
-	 * @param PropFind $propFind
290
-	 * @param \Sabre\DAV\INode $node
291
-	 * @return void
292
-	 */
293
-	public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
294
-		$httpRequest = $this->server->httpRequest;
295
-
296
-		if ($node instanceof Node) {
297
-			/**
298
-			 * This was disabled, because it made dir listing throw an exception,
299
-			 * so users were unable to navigate into folders where one subitem
300
-			 * is blocked by the files_accesscontrol app, see:
301
-			 * https://github.com/nextcloud/files_accesscontrol/issues/65
302
-			 * if (!$node->getFileInfo()->isReadable()) {
303
-			 *     // avoid detecting files through this means
304
-			 *     throw new NotFound();
305
-			 * }
306
-			 */
307
-
308
-			$propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node) {
309
-				return $node->getFileId();
310
-			});
311
-
312
-			$propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function () use ($node) {
313
-				return $node->getInternalFileId();
314
-			});
315
-
316
-			$propFind->handle(self::PERMISSIONS_PROPERTYNAME, function () use ($node) {
317
-				$perms = $node->getDavPermissions();
318
-				if ($this->isPublic) {
319
-					// remove mount information
320
-					$perms = str_replace(['S', 'M'], '', $perms);
321
-				}
322
-				return $perms;
323
-			});
324
-
325
-			$propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
326
-				$user = $this->userSession->getUser();
327
-				if ($user === null) {
328
-					return null;
329
-				}
330
-				return $node->getSharePermissions(
331
-					$user->getUID()
332
-				);
333
-			});
334
-
335
-			$propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest): ?string {
336
-				$user = $this->userSession->getUser();
337
-				if ($user === null) {
338
-					return null;
339
-				}
340
-				$ncPermissions = $node->getSharePermissions(
341
-					$user->getUID()
342
-				);
343
-				$ocmPermissions = $this->ncPermissions2ocmPermissions($ncPermissions);
344
-				return json_encode($ocmPermissions, JSON_THROW_ON_ERROR);
345
-			});
346
-
347
-			$propFind->handle(self::SHARE_ATTRIBUTES_PROPERTYNAME, function () use ($node, $httpRequest) {
348
-				return json_encode($node->getShareAttributes(), JSON_THROW_ON_ERROR);
349
-			});
350
-
351
-			$propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node): string {
352
-				return $node->getETag();
353
-			});
354
-
355
-			$propFind->handle(self::OWNER_ID_PROPERTYNAME, function () use ($node): ?string {
356
-				$owner = $node->getOwner();
357
-				if (!$owner) {
358
-					return null;
359
-				} else {
360
-					return $owner->getUID();
361
-				}
362
-			});
363
-			$propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function () use ($node): ?string {
364
-				$owner = $node->getOwner();
365
-				if (!$owner) {
366
-					return null;
367
-				}
368
-
369
-				// Get current user to see if we're in a public share or not
370
-				$user = $this->userSession->getUser();
371
-
372
-				// If the user is logged in, we can return the display name
373
-				if ($user !== null) {
374
-					return $owner->getDisplayName();
375
-				}
376
-
377
-				// Check if the user published their display name
378
-				try {
379
-					$ownerAccount = $this->accountManager->getAccount($owner);
380
-				} catch (NoUserException) {
381
-					// do not lock process if owner is not local
382
-					return null;
383
-				}
384
-
385
-				$ownerNameProperty = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME);
386
-
387
-				// Since we are not logged in, we need to have at least the published scope
388
-				if ($ownerNameProperty->getScope() === IAccountManager::SCOPE_PUBLISHED) {
389
-					return $owner->getDisplayName();
390
-				}
391
-
392
-				return null;
393
-			});
394
-
395
-			$propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
396
-				return json_encode($this->previewManager->isAvailable($node->getFileInfo()), JSON_THROW_ON_ERROR);
397
-			});
398
-			$propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node): int|float {
399
-				return $node->getSize();
400
-			});
401
-			$propFind->handle(self::MOUNT_TYPE_PROPERTYNAME, function () use ($node) {
402
-				return $node->getFileInfo()->getMountPoint()->getMountType();
403
-			});
404
-
405
-			/**
406
-			 * This is a special property which is used to determine if a node
407
-			 * is a mount root or not, e.g. a shared folder.
408
-			 * If so, then the node can only be unshared and not deleted.
409
-			 * @see https://github.com/nextcloud/server/blob/cc75294eb6b16b916a342e69998935f89222619d/lib/private/Files/View.php#L696-L698
410
-			 */
411
-			$propFind->handle(self::MOUNT_ROOT_PROPERTYNAME, function () use ($node) {
412
-				return $node->getNode()->getInternalPath() === '' ? 'true' : 'false';
413
-			});
414
-
415
-			$propFind->handle(self::SHARE_NOTE, function () use ($node): ?string {
416
-				$user = $this->userSession->getUser();
417
-				return $node->getNoteFromShare(
418
-					$user?->getUID()
419
-				);
420
-			});
421
-
422
-			$propFind->handle(self::SHARE_HIDE_DOWNLOAD_PROPERTYNAME, function () use ($node) {
423
-				$storage = $node->getNode()->getStorage();
424
-				if ($storage->instanceOfStorage(ISharedStorage::class)) {
425
-					/** @var ISharedStorage $storage */
426
-					return match($storage->getShare()->getHideDownload()) {
427
-						true => 'true',
428
-						false => 'false',
429
-					};
430
-				} else {
431
-					return null;
432
-				}
433
-			});
434
-
435
-			$propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () {
436
-				return $this->config->getSystemValue('data-fingerprint', '');
437
-			});
438
-			$propFind->handle(self::CREATIONDATE_PROPERTYNAME, function () use ($node) {
439
-				return (new \DateTimeImmutable())
440
-					->setTimestamp($node->getFileInfo()->getCreationTime())
441
-					->format(\DateTimeInterface::ATOM);
442
-			});
443
-			$propFind->handle(self::CREATION_TIME_PROPERTYNAME, function () use ($node) {
444
-				return $node->getFileInfo()->getCreationTime();
445
-			});
446
-
447
-			foreach ($node->getFileInfo()->getMetadata() as $metadataKey => $metadataValue) {
448
-				$propFind->handle(self::FILE_METADATA_PREFIX . $metadataKey, $metadataValue);
449
-			}
450
-
451
-			$propFind->handle(self::HIDDEN_PROPERTYNAME, function () use ($node) {
452
-				$isLivePhoto = isset($node->getFileInfo()->getMetadata()['files-live-photo']);
453
-				$isMovFile = $node->getFileInfo()->getMimetype() === 'video/quicktime';
454
-				return ($isLivePhoto && $isMovFile) ? 'true' : 'false';
455
-			});
456
-
457
-			/**
458
-			 * Return file/folder name as displayname. The primary reason to
459
-			 * implement it this way is to avoid costly fallback to
460
-			 * CustomPropertiesBackend (esp. visible when querying all files
461
-			 * in a folder).
462
-			 */
463
-			$propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function () use ($node) {
464
-				return $node->getName();
465
-			});
466
-
467
-			$propFind->handle(self::IS_FEDERATED_PROPERTYNAME, function () use ($node) {
468
-				return $node->getFileInfo()->getMountPoint()
469
-					instanceof SharingExternalMount;
470
-			});
471
-		}
472
-
473
-		if ($node instanceof File) {
474
-			$propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node) {
475
-				try {
476
-					$directDownloadUrl = $node->getDirectDownload();
477
-					if (isset($directDownloadUrl['url'])) {
478
-						return $directDownloadUrl['url'];
479
-					}
480
-				} catch (StorageNotAvailableException $e) {
481
-					return false;
482
-				} catch (ForbiddenException $e) {
483
-					return false;
484
-				}
485
-				return false;
486
-			});
487
-
488
-			$propFind->handle(self::CHECKSUMS_PROPERTYNAME, function () use ($node) {
489
-				$checksum = $node->getChecksum();
490
-				if ($checksum === null || $checksum === '') {
491
-					return null;
492
-				}
493
-
494
-				return new ChecksumList($checksum);
495
-			});
496
-
497
-			$propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function () use ($node) {
498
-				return $node->getFileInfo()->getUploadTime();
499
-			});
500
-		}
501
-
502
-		if ($node instanceof Directory) {
503
-			$propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
504
-				return $node->getSize();
505
-			});
506
-
507
-			$requestProperties = $propFind->getRequestedProperties();
508
-
509
-			if (in_array(self::SUBFILE_COUNT_PROPERTYNAME, $requestProperties, true)
510
-				|| in_array(self::SUBFOLDER_COUNT_PROPERTYNAME, $requestProperties, true)) {
511
-				$nbFiles = 0;
512
-				$nbFolders = 0;
513
-				foreach ($node->getChildren() as $child) {
514
-					if ($child instanceof File) {
515
-						$nbFiles++;
516
-					} elseif ($child instanceof Directory) {
517
-						$nbFolders++;
518
-					}
519
-				}
520
-
521
-				$propFind->handle(self::SUBFILE_COUNT_PROPERTYNAME, $nbFiles);
522
-				$propFind->handle(self::SUBFOLDER_COUNT_PROPERTYNAME, $nbFolders);
523
-			}
524
-		}
525
-	}
526
-
527
-	/**
528
-	 * translate Nextcloud permissions to OCM Permissions
529
-	 *
530
-	 * @param $ncPermissions
531
-	 * @return array
532
-	 */
533
-	protected function ncPermissions2ocmPermissions($ncPermissions) {
534
-		$ocmPermissions = [];
535
-
536
-		if ($ncPermissions & Constants::PERMISSION_SHARE) {
537
-			$ocmPermissions[] = 'share';
538
-		}
539
-
540
-		if ($ncPermissions & Constants::PERMISSION_READ) {
541
-			$ocmPermissions[] = 'read';
542
-		}
543
-
544
-		if (($ncPermissions & Constants::PERMISSION_CREATE) ||
545
-			($ncPermissions & Constants::PERMISSION_UPDATE)) {
546
-			$ocmPermissions[] = 'write';
547
-		}
548
-
549
-		return $ocmPermissions;
550
-	}
551
-
552
-	/**
553
-	 * Update ownCloud-specific properties
554
-	 *
555
-	 * @param string $path
556
-	 * @param PropPatch $propPatch
557
-	 *
558
-	 * @return void
559
-	 */
560
-	public function handleUpdateProperties($path, PropPatch $propPatch) {
561
-		$node = $this->tree->getNodeForPath($path);
562
-		if (!($node instanceof Node)) {
563
-			return;
564
-		}
565
-
566
-		$propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function ($time) use ($node) {
567
-			if (empty($time)) {
568
-				return false;
569
-			}
570
-			$node->touch($time);
571
-			return true;
572
-		});
573
-		$propPatch->handle(self::GETETAG_PROPERTYNAME, function ($etag) use ($node) {
574
-			if (empty($etag)) {
575
-				return false;
576
-			}
577
-			return $node->setEtag($etag) !== -1;
578
-		});
579
-		$propPatch->handle(self::CREATIONDATE_PROPERTYNAME, function ($time) use ($node) {
580
-			if (empty($time)) {
581
-				return false;
582
-			}
583
-			$dateTime = new \DateTimeImmutable($time);
584
-			$node->setCreationTime($dateTime->getTimestamp());
585
-			return true;
586
-		});
587
-		$propPatch->handle(self::CREATION_TIME_PROPERTYNAME, function ($time) use ($node) {
588
-			if (empty($time)) {
589
-				return false;
590
-			}
591
-			$node->setCreationTime((int)$time);
592
-			return true;
593
-		});
594
-
595
-		$this->handleUpdatePropertiesMetadata($propPatch, $node);
596
-
597
-		/**
598
-		 * Disable modification of the displayname property for files and
599
-		 * folders via PROPPATCH. See PROPFIND for more information.
600
-		 */
601
-		$propPatch->handle(self::DISPLAYNAME_PROPERTYNAME, function ($displayName) {
602
-			return 403;
603
-		});
604
-	}
605
-
606
-
607
-	/**
608
-	 * handle the update of metadata from PROPPATCH requests
609
-	 *
610
-	 * @param PropPatch $propPatch
611
-	 * @param Node $node
612
-	 *
613
-	 * @throws FilesMetadataException
614
-	 */
615
-	private function handleUpdatePropertiesMetadata(PropPatch $propPatch, Node $node): void {
616
-		$userId = $this->userSession->getUser()?->getUID();
617
-		if ($userId === null) {
618
-			return;
619
-		}
620
-
621
-		$accessRight = $this->getMetadataFileAccessRight($node, $userId);
622
-		$filesMetadataManager = $this->initFilesMetadataManager();
623
-		$knownMetadata = $filesMetadataManager->getKnownMetadata();
624
-
625
-		foreach ($propPatch->getRemainingMutations() as $mutation) {
626
-			if (!str_starts_with($mutation, self::FILE_METADATA_PREFIX)) {
627
-				continue;
628
-			}
629
-
630
-			$propPatch->handle(
631
-				$mutation,
632
-				function (mixed $value) use ($accessRight, $knownMetadata, $node, $mutation, $filesMetadataManager): bool {
633
-					/** @var FilesMetadata $metadata */
634
-					$metadata = $filesMetadataManager->getMetadata((int)$node->getFileId(), true);
635
-					$metadata->setStorageId($node->getNode()->getStorage()->getCache()->getNumericStorageId());
636
-					$metadataKey = substr($mutation, strlen(self::FILE_METADATA_PREFIX));
637
-
638
-					// confirm metadata key is editable via PROPPATCH
639
-					if ($knownMetadata->getEditPermission($metadataKey) < $accessRight) {
640
-						throw new FilesMetadataException('you do not have enough rights to update \'' . $metadataKey . '\' on this node');
641
-					}
642
-
643
-					if ($value === null) {
644
-						$metadata->unset($metadataKey);
645
-						$filesMetadataManager->saveMetadata($metadata);
646
-						return true;
647
-					}
648
-
649
-					// If the metadata is unknown, it defaults to string.
650
-					try {
651
-						$type = $knownMetadata->getType($metadataKey);
652
-					} catch (FilesMetadataNotFoundException) {
653
-						$type = IMetadataValueWrapper::TYPE_STRING;
654
-					}
655
-
656
-					switch ($type) {
657
-						case IMetadataValueWrapper::TYPE_STRING:
658
-							$metadata->setString($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
659
-							break;
660
-						case IMetadataValueWrapper::TYPE_INT:
661
-							$metadata->setInt($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
662
-							break;
663
-						case IMetadataValueWrapper::TYPE_FLOAT:
664
-							$metadata->setFloat($metadataKey, $value);
665
-							break;
666
-						case IMetadataValueWrapper::TYPE_BOOL:
667
-							$metadata->setBool($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
668
-							break;
669
-						case IMetadataValueWrapper::TYPE_ARRAY:
670
-							$metadata->setArray($metadataKey, $value);
671
-							break;
672
-						case IMetadataValueWrapper::TYPE_STRING_LIST:
673
-							$metadata->setStringList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
674
-							break;
675
-						case IMetadataValueWrapper::TYPE_INT_LIST:
676
-							$metadata->setIntList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
677
-							break;
678
-					}
679
-
680
-					$filesMetadataManager->saveMetadata($metadata);
681
-
682
-					return true;
683
-				}
684
-			);
685
-		}
686
-	}
687
-
688
-	/**
689
-	 * init default internal metadata
690
-	 *
691
-	 * @return IFilesMetadataManager
692
-	 */
693
-	private function initFilesMetadataManager(): IFilesMetadataManager {
694
-		/** @var IFilesMetadataManager $manager */
695
-		$manager = \OCP\Server::get(IFilesMetadataManager::class);
696
-		$manager->initMetadata('files-live-photo', IMetadataValueWrapper::TYPE_STRING, false, IMetadataValueWrapper::EDIT_REQ_OWNERSHIP);
697
-
698
-		return $manager;
699
-	}
700
-
701
-	/**
702
-	 * based on owner and shares, returns the bottom limit to update related metadata
703
-	 *
704
-	 * @param Node $node
705
-	 * @param string $userId
706
-	 *
707
-	 * @return int
708
-	 */
709
-	private function getMetadataFileAccessRight(Node $node, string $userId): int {
710
-		if ($node->getOwner()?->getUID() === $userId) {
711
-			return IMetadataValueWrapper::EDIT_REQ_OWNERSHIP;
712
-		} else {
713
-			$filePermissions = $node->getSharePermissions($userId);
714
-			if ($filePermissions & Constants::PERMISSION_UPDATE) {
715
-				return IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION;
716
-			}
717
-		}
718
-
719
-		return IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION;
720
-	}
721
-
722
-	/**
723
-	 * @param string $filePath
724
-	 * @param ?\Sabre\DAV\INode $node
725
-	 * @return void
726
-	 * @throws \Sabre\DAV\Exception\BadRequest
727
-	 */
728
-	public function sendFileIdHeader($filePath, ?\Sabre\DAV\INode $node = null) {
729
-		// we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
730
-		try {
731
-			$node = $this->server->tree->getNodeForPath($filePath);
732
-			if ($node instanceof Node) {
733
-				$fileId = $node->getFileId();
734
-				if (!is_null($fileId)) {
735
-					$this->server->httpResponse->setHeader('OC-FileId', $fileId);
736
-				}
737
-			}
738
-		} catch (NotFound) {
739
-		}
740
-	}
43
+    // namespace
44
+    public const NS_OWNCLOUD = 'http://owncloud.org/ns';
45
+    public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
46
+    public const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id';
47
+    public const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid';
48
+    public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
49
+    public const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions';
50
+    public const OCM_SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-cloud-mesh.org/ns}share-permissions';
51
+    public const SHARE_ATTRIBUTES_PROPERTYNAME = '{http://nextcloud.org/ns}share-attributes';
52
+    public const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL';
53
+    public const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size';
54
+    public const GETETAG_PROPERTYNAME = '{DAV:}getetag';
55
+    public const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified';
56
+    public const CREATIONDATE_PROPERTYNAME = '{DAV:}creationdate';
57
+    public const DISPLAYNAME_PROPERTYNAME = '{DAV:}displayname';
58
+    public const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id';
59
+    public const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name';
60
+    public const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums';
61
+    public const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint';
62
+    public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
63
+    public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type';
64
+    public const MOUNT_ROOT_PROPERTYNAME = '{http://nextcloud.org/ns}is-mount-root';
65
+    public const IS_FEDERATED_PROPERTYNAME = '{http://nextcloud.org/ns}is-federated';
66
+    public const METADATA_ETAG_PROPERTYNAME = '{http://nextcloud.org/ns}metadata_etag';
67
+    public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time';
68
+    public const CREATION_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}creation_time';
69
+    public const SHARE_NOTE = '{http://nextcloud.org/ns}note';
70
+    public const SHARE_HIDE_DOWNLOAD_PROPERTYNAME = '{http://nextcloud.org/ns}hide-download';
71
+    public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count';
72
+    public const SUBFILE_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-file-count';
73
+    public const FILE_METADATA_PREFIX = '{http://nextcloud.org/ns}metadata-';
74
+    public const HIDDEN_PROPERTYNAME = '{http://nextcloud.org/ns}hidden';
75
+
76
+    /** Reference to main server object */
77
+    private ?Server $server = null;
78
+
79
+    /**
80
+     * @param Tree $tree
81
+     * @param IConfig $config
82
+     * @param IRequest $request
83
+     * @param IPreview $previewManager
84
+     * @param IUserSession $userSession
85
+     * @param bool $isPublic Whether this is public WebDAV. If true, some returned information will be stripped off.
86
+     * @param bool $downloadAttachment
87
+     * @return void
88
+     */
89
+    public function __construct(
90
+        private Tree $tree,
91
+        private IConfig $config,
92
+        private IRequest $request,
93
+        private IPreview $previewManager,
94
+        private IUserSession $userSession,
95
+        private IFilenameValidator $validator,
96
+        private IAccountManager $accountManager,
97
+        private bool $isPublic = false,
98
+        private bool $downloadAttachment = true,
99
+    ) {
100
+    }
101
+
102
+    /**
103
+     * This initializes the plugin.
104
+     *
105
+     * This function is called by \Sabre\DAV\Server, after
106
+     * addPlugin is called.
107
+     *
108
+     * This method should set up the required event subscriptions.
109
+     *
110
+     * @return void
111
+     */
112
+    public function initialize(Server $server) {
113
+        $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
114
+        $server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';
115
+        $server->protectedProperties[] = self::FILEID_PROPERTYNAME;
116
+        $server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME;
117
+        $server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME;
118
+        $server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME;
119
+        $server->protectedProperties[] = self::OCM_SHARE_PERMISSIONS_PROPERTYNAME;
120
+        $server->protectedProperties[] = self::SHARE_ATTRIBUTES_PROPERTYNAME;
121
+        $server->protectedProperties[] = self::SIZE_PROPERTYNAME;
122
+        $server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME;
123
+        $server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME;
124
+        $server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME;
125
+        $server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME;
126
+        $server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME;
127
+        $server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME;
128
+        $server->protectedProperties[] = self::MOUNT_TYPE_PROPERTYNAME;
129
+        $server->protectedProperties[] = self::IS_FEDERATED_PROPERTYNAME;
130
+        $server->protectedProperties[] = self::SHARE_NOTE;
131
+
132
+        // normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH
133
+        $allowedProperties = ['{DAV:}getetag'];
134
+        $server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties);
135
+
136
+        $this->server = $server;
137
+        $this->server->on('propFind', [$this, 'handleGetProperties']);
138
+        $this->server->on('propPatch', [$this, 'handleUpdateProperties']);
139
+        $this->server->on('afterBind', [$this, 'sendFileIdHeader']);
140
+        $this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']);
141
+        $this->server->on('afterMethod:GET', [$this,'httpGet']);
142
+        $this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']);
143
+        $this->server->on('afterResponse', function ($request, ResponseInterface $response): void {
144
+            $body = $response->getBody();
145
+            if (is_resource($body)) {
146
+                fclose($body);
147
+            }
148
+        });
149
+        $this->server->on('beforeMove', [$this, 'checkMove']);
150
+        $this->server->on('beforeCopy', [$this, 'checkCopy']);
151
+    }
152
+
153
+    /**
154
+     * Plugin that checks if a copy can actually be performed.
155
+     *
156
+     * @param string $source source path
157
+     * @param string $target target path
158
+     * @throws NotFound If the source does not exist
159
+     * @throws InvalidPath If the target is invalid
160
+     */
161
+    public function checkCopy($source, $target): void {
162
+        $sourceNode = $this->tree->getNodeForPath($source);
163
+        if (!$sourceNode instanceof Node) {
164
+            return;
165
+        }
166
+
167
+        // Ensure source exists
168
+        $sourceNodeFileInfo = $sourceNode->getFileInfo();
169
+        if ($sourceNodeFileInfo === null) {
170
+            throw new NotFound($source . ' does not exist');
171
+        }
172
+        // Ensure the target name is valid
173
+        try {
174
+            [$targetPath, $targetName] = \Sabre\Uri\split($target);
175
+            $this->validator->validateFilename($targetName);
176
+        } catch (InvalidPathException $e) {
177
+            throw new InvalidPath($e->getMessage(), false);
178
+        }
179
+        // Ensure the target path is valid
180
+        $segments = array_slice(explode('/', $targetPath), 2);
181
+        foreach ($segments as $segment) {
182
+            if ($this->validator->isFilenameValid($segment) === false) {
183
+                $l = \OCP\Server::get(IFactory::class)->get('dav');
184
+                throw new InvalidPath($l->t('Invalid target path'));
185
+            }
186
+        }
187
+    }
188
+
189
+    /**
190
+     * Plugin that checks if a move can actually be performed.
191
+     *
192
+     * @param string $source source path
193
+     * @param string $target target path
194
+     * @throws Forbidden If the source is not deletable
195
+     * @throws NotFound If the source does not exist
196
+     * @throws InvalidPath If the target name is invalid
197
+     */
198
+    public function checkMove(string $source, string $target): void {
199
+        $sourceNode = $this->tree->getNodeForPath($source);
200
+        if (!$sourceNode instanceof Node) {
201
+            return;
202
+        }
203
+
204
+        // First check copyable (move only needs additional delete permission)
205
+        $this->checkCopy($source, $target);
206
+
207
+        // The source needs to be deletable for moving
208
+        $sourceNodeFileInfo = $sourceNode->getFileInfo();
209
+        if (!$sourceNodeFileInfo->isDeletable()) {
210
+            throw new Forbidden($source . ' cannot be deleted');
211
+        }
212
+
213
+        // The source is not allowed to be the parent of the target
214
+        if (str_starts_with($source, $target . '/')) {
215
+            throw new Forbidden($source . ' cannot be moved to it\'s parent');
216
+        }
217
+    }
218
+
219
+    /**
220
+     * This sets a cookie to be able to recognize the start of the download
221
+     * the content must not be longer than 32 characters and must only contain
222
+     * alphanumeric characters
223
+     *
224
+     * @param RequestInterface $request
225
+     * @param ResponseInterface $response
226
+     */
227
+    public function handleDownloadToken(RequestInterface $request, ResponseInterface $response) {
228
+        $queryParams = $request->getQueryParameters();
229
+
230
+        /**
231
+         * this sets a cookie to be able to recognize the start of the download
232
+         * the content must not be longer than 32 characters and must only contain
233
+         * alphanumeric characters
234
+         */
235
+        if (isset($queryParams['downloadStartSecret'])) {
236
+            $token = $queryParams['downloadStartSecret'];
237
+            if (!isset($token[32])
238
+                && preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) {
239
+                // FIXME: use $response->setHeader() instead
240
+                setcookie('ocDownloadStarted', $token, time() + 20, '/');
241
+            }
242
+        }
243
+    }
244
+
245
+    /**
246
+     * Add headers to file download
247
+     *
248
+     * @param RequestInterface $request
249
+     * @param ResponseInterface $response
250
+     */
251
+    public function httpGet(RequestInterface $request, ResponseInterface $response) {
252
+        // Only handle valid files
253
+        $node = $this->tree->getNodeForPath($request->getPath());
254
+        if (!($node instanceof IFile)) {
255
+            return;
256
+        }
257
+
258
+        // adds a 'Content-Disposition: attachment' header in case no disposition
259
+        // header has been set before
260
+        if ($this->downloadAttachment &&
261
+            $response->getHeader('Content-Disposition') === null) {
262
+            $filename = $node->getName();
263
+            if ($this->request->isUserAgent(
264
+                [
265
+                    Request::USER_AGENT_IE,
266
+                    Request::USER_AGENT_ANDROID_MOBILE_CHROME,
267
+                    Request::USER_AGENT_FREEBOX,
268
+                ])) {
269
+                $response->addHeader('Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"');
270
+            } else {
271
+                $response->addHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . rawurlencode($filename)
272
+                                                        . '; filename="' . rawurlencode($filename) . '"');
273
+            }
274
+        }
275
+
276
+        if ($node instanceof File) {
277
+            //Add OC-Checksum header
278
+            $checksum = $node->getChecksum();
279
+            if ($checksum !== null && $checksum !== '') {
280
+                $response->addHeader('OC-Checksum', $checksum);
281
+            }
282
+        }
283
+        $response->addHeader('X-Accel-Buffering', 'no');
284
+    }
285
+
286
+    /**
287
+     * Adds all ownCloud-specific properties
288
+     *
289
+     * @param PropFind $propFind
290
+     * @param \Sabre\DAV\INode $node
291
+     * @return void
292
+     */
293
+    public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
294
+        $httpRequest = $this->server->httpRequest;
295
+
296
+        if ($node instanceof Node) {
297
+            /**
298
+             * This was disabled, because it made dir listing throw an exception,
299
+             * so users were unable to navigate into folders where one subitem
300
+             * is blocked by the files_accesscontrol app, see:
301
+             * https://github.com/nextcloud/files_accesscontrol/issues/65
302
+             * if (!$node->getFileInfo()->isReadable()) {
303
+             *     // avoid detecting files through this means
304
+             *     throw new NotFound();
305
+             * }
306
+             */
307
+
308
+            $propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node) {
309
+                return $node->getFileId();
310
+            });
311
+
312
+            $propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function () use ($node) {
313
+                return $node->getInternalFileId();
314
+            });
315
+
316
+            $propFind->handle(self::PERMISSIONS_PROPERTYNAME, function () use ($node) {
317
+                $perms = $node->getDavPermissions();
318
+                if ($this->isPublic) {
319
+                    // remove mount information
320
+                    $perms = str_replace(['S', 'M'], '', $perms);
321
+                }
322
+                return $perms;
323
+            });
324
+
325
+            $propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
326
+                $user = $this->userSession->getUser();
327
+                if ($user === null) {
328
+                    return null;
329
+                }
330
+                return $node->getSharePermissions(
331
+                    $user->getUID()
332
+                );
333
+            });
334
+
335
+            $propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest): ?string {
336
+                $user = $this->userSession->getUser();
337
+                if ($user === null) {
338
+                    return null;
339
+                }
340
+                $ncPermissions = $node->getSharePermissions(
341
+                    $user->getUID()
342
+                );
343
+                $ocmPermissions = $this->ncPermissions2ocmPermissions($ncPermissions);
344
+                return json_encode($ocmPermissions, JSON_THROW_ON_ERROR);
345
+            });
346
+
347
+            $propFind->handle(self::SHARE_ATTRIBUTES_PROPERTYNAME, function () use ($node, $httpRequest) {
348
+                return json_encode($node->getShareAttributes(), JSON_THROW_ON_ERROR);
349
+            });
350
+
351
+            $propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node): string {
352
+                return $node->getETag();
353
+            });
354
+
355
+            $propFind->handle(self::OWNER_ID_PROPERTYNAME, function () use ($node): ?string {
356
+                $owner = $node->getOwner();
357
+                if (!$owner) {
358
+                    return null;
359
+                } else {
360
+                    return $owner->getUID();
361
+                }
362
+            });
363
+            $propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function () use ($node): ?string {
364
+                $owner = $node->getOwner();
365
+                if (!$owner) {
366
+                    return null;
367
+                }
368
+
369
+                // Get current user to see if we're in a public share or not
370
+                $user = $this->userSession->getUser();
371
+
372
+                // If the user is logged in, we can return the display name
373
+                if ($user !== null) {
374
+                    return $owner->getDisplayName();
375
+                }
376
+
377
+                // Check if the user published their display name
378
+                try {
379
+                    $ownerAccount = $this->accountManager->getAccount($owner);
380
+                } catch (NoUserException) {
381
+                    // do not lock process if owner is not local
382
+                    return null;
383
+                }
384
+
385
+                $ownerNameProperty = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME);
386
+
387
+                // Since we are not logged in, we need to have at least the published scope
388
+                if ($ownerNameProperty->getScope() === IAccountManager::SCOPE_PUBLISHED) {
389
+                    return $owner->getDisplayName();
390
+                }
391
+
392
+                return null;
393
+            });
394
+
395
+            $propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
396
+                return json_encode($this->previewManager->isAvailable($node->getFileInfo()), JSON_THROW_ON_ERROR);
397
+            });
398
+            $propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node): int|float {
399
+                return $node->getSize();
400
+            });
401
+            $propFind->handle(self::MOUNT_TYPE_PROPERTYNAME, function () use ($node) {
402
+                return $node->getFileInfo()->getMountPoint()->getMountType();
403
+            });
404
+
405
+            /**
406
+             * This is a special property which is used to determine if a node
407
+             * is a mount root or not, e.g. a shared folder.
408
+             * If so, then the node can only be unshared and not deleted.
409
+             * @see https://github.com/nextcloud/server/blob/cc75294eb6b16b916a342e69998935f89222619d/lib/private/Files/View.php#L696-L698
410
+             */
411
+            $propFind->handle(self::MOUNT_ROOT_PROPERTYNAME, function () use ($node) {
412
+                return $node->getNode()->getInternalPath() === '' ? 'true' : 'false';
413
+            });
414
+
415
+            $propFind->handle(self::SHARE_NOTE, function () use ($node): ?string {
416
+                $user = $this->userSession->getUser();
417
+                return $node->getNoteFromShare(
418
+                    $user?->getUID()
419
+                );
420
+            });
421
+
422
+            $propFind->handle(self::SHARE_HIDE_DOWNLOAD_PROPERTYNAME, function () use ($node) {
423
+                $storage = $node->getNode()->getStorage();
424
+                if ($storage->instanceOfStorage(ISharedStorage::class)) {
425
+                    /** @var ISharedStorage $storage */
426
+                    return match($storage->getShare()->getHideDownload()) {
427
+                        true => 'true',
428
+                        false => 'false',
429
+                    };
430
+                } else {
431
+                    return null;
432
+                }
433
+            });
434
+
435
+            $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () {
436
+                return $this->config->getSystemValue('data-fingerprint', '');
437
+            });
438
+            $propFind->handle(self::CREATIONDATE_PROPERTYNAME, function () use ($node) {
439
+                return (new \DateTimeImmutable())
440
+                    ->setTimestamp($node->getFileInfo()->getCreationTime())
441
+                    ->format(\DateTimeInterface::ATOM);
442
+            });
443
+            $propFind->handle(self::CREATION_TIME_PROPERTYNAME, function () use ($node) {
444
+                return $node->getFileInfo()->getCreationTime();
445
+            });
446
+
447
+            foreach ($node->getFileInfo()->getMetadata() as $metadataKey => $metadataValue) {
448
+                $propFind->handle(self::FILE_METADATA_PREFIX . $metadataKey, $metadataValue);
449
+            }
450
+
451
+            $propFind->handle(self::HIDDEN_PROPERTYNAME, function () use ($node) {
452
+                $isLivePhoto = isset($node->getFileInfo()->getMetadata()['files-live-photo']);
453
+                $isMovFile = $node->getFileInfo()->getMimetype() === 'video/quicktime';
454
+                return ($isLivePhoto && $isMovFile) ? 'true' : 'false';
455
+            });
456
+
457
+            /**
458
+             * Return file/folder name as displayname. The primary reason to
459
+             * implement it this way is to avoid costly fallback to
460
+             * CustomPropertiesBackend (esp. visible when querying all files
461
+             * in a folder).
462
+             */
463
+            $propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function () use ($node) {
464
+                return $node->getName();
465
+            });
466
+
467
+            $propFind->handle(self::IS_FEDERATED_PROPERTYNAME, function () use ($node) {
468
+                return $node->getFileInfo()->getMountPoint()
469
+                    instanceof SharingExternalMount;
470
+            });
471
+        }
472
+
473
+        if ($node instanceof File) {
474
+            $propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node) {
475
+                try {
476
+                    $directDownloadUrl = $node->getDirectDownload();
477
+                    if (isset($directDownloadUrl['url'])) {
478
+                        return $directDownloadUrl['url'];
479
+                    }
480
+                } catch (StorageNotAvailableException $e) {
481
+                    return false;
482
+                } catch (ForbiddenException $e) {
483
+                    return false;
484
+                }
485
+                return false;
486
+            });
487
+
488
+            $propFind->handle(self::CHECKSUMS_PROPERTYNAME, function () use ($node) {
489
+                $checksum = $node->getChecksum();
490
+                if ($checksum === null || $checksum === '') {
491
+                    return null;
492
+                }
493
+
494
+                return new ChecksumList($checksum);
495
+            });
496
+
497
+            $propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function () use ($node) {
498
+                return $node->getFileInfo()->getUploadTime();
499
+            });
500
+        }
501
+
502
+        if ($node instanceof Directory) {
503
+            $propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
504
+                return $node->getSize();
505
+            });
506
+
507
+            $requestProperties = $propFind->getRequestedProperties();
508
+
509
+            if (in_array(self::SUBFILE_COUNT_PROPERTYNAME, $requestProperties, true)
510
+                || in_array(self::SUBFOLDER_COUNT_PROPERTYNAME, $requestProperties, true)) {
511
+                $nbFiles = 0;
512
+                $nbFolders = 0;
513
+                foreach ($node->getChildren() as $child) {
514
+                    if ($child instanceof File) {
515
+                        $nbFiles++;
516
+                    } elseif ($child instanceof Directory) {
517
+                        $nbFolders++;
518
+                    }
519
+                }
520
+
521
+                $propFind->handle(self::SUBFILE_COUNT_PROPERTYNAME, $nbFiles);
522
+                $propFind->handle(self::SUBFOLDER_COUNT_PROPERTYNAME, $nbFolders);
523
+            }
524
+        }
525
+    }
526
+
527
+    /**
528
+     * translate Nextcloud permissions to OCM Permissions
529
+     *
530
+     * @param $ncPermissions
531
+     * @return array
532
+     */
533
+    protected function ncPermissions2ocmPermissions($ncPermissions) {
534
+        $ocmPermissions = [];
535
+
536
+        if ($ncPermissions & Constants::PERMISSION_SHARE) {
537
+            $ocmPermissions[] = 'share';
538
+        }
539
+
540
+        if ($ncPermissions & Constants::PERMISSION_READ) {
541
+            $ocmPermissions[] = 'read';
542
+        }
543
+
544
+        if (($ncPermissions & Constants::PERMISSION_CREATE) ||
545
+            ($ncPermissions & Constants::PERMISSION_UPDATE)) {
546
+            $ocmPermissions[] = 'write';
547
+        }
548
+
549
+        return $ocmPermissions;
550
+    }
551
+
552
+    /**
553
+     * Update ownCloud-specific properties
554
+     *
555
+     * @param string $path
556
+     * @param PropPatch $propPatch
557
+     *
558
+     * @return void
559
+     */
560
+    public function handleUpdateProperties($path, PropPatch $propPatch) {
561
+        $node = $this->tree->getNodeForPath($path);
562
+        if (!($node instanceof Node)) {
563
+            return;
564
+        }
565
+
566
+        $propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function ($time) use ($node) {
567
+            if (empty($time)) {
568
+                return false;
569
+            }
570
+            $node->touch($time);
571
+            return true;
572
+        });
573
+        $propPatch->handle(self::GETETAG_PROPERTYNAME, function ($etag) use ($node) {
574
+            if (empty($etag)) {
575
+                return false;
576
+            }
577
+            return $node->setEtag($etag) !== -1;
578
+        });
579
+        $propPatch->handle(self::CREATIONDATE_PROPERTYNAME, function ($time) use ($node) {
580
+            if (empty($time)) {
581
+                return false;
582
+            }
583
+            $dateTime = new \DateTimeImmutable($time);
584
+            $node->setCreationTime($dateTime->getTimestamp());
585
+            return true;
586
+        });
587
+        $propPatch->handle(self::CREATION_TIME_PROPERTYNAME, function ($time) use ($node) {
588
+            if (empty($time)) {
589
+                return false;
590
+            }
591
+            $node->setCreationTime((int)$time);
592
+            return true;
593
+        });
594
+
595
+        $this->handleUpdatePropertiesMetadata($propPatch, $node);
596
+
597
+        /**
598
+         * Disable modification of the displayname property for files and
599
+         * folders via PROPPATCH. See PROPFIND for more information.
600
+         */
601
+        $propPatch->handle(self::DISPLAYNAME_PROPERTYNAME, function ($displayName) {
602
+            return 403;
603
+        });
604
+    }
605
+
606
+
607
+    /**
608
+     * handle the update of metadata from PROPPATCH requests
609
+     *
610
+     * @param PropPatch $propPatch
611
+     * @param Node $node
612
+     *
613
+     * @throws FilesMetadataException
614
+     */
615
+    private function handleUpdatePropertiesMetadata(PropPatch $propPatch, Node $node): void {
616
+        $userId = $this->userSession->getUser()?->getUID();
617
+        if ($userId === null) {
618
+            return;
619
+        }
620
+
621
+        $accessRight = $this->getMetadataFileAccessRight($node, $userId);
622
+        $filesMetadataManager = $this->initFilesMetadataManager();
623
+        $knownMetadata = $filesMetadataManager->getKnownMetadata();
624
+
625
+        foreach ($propPatch->getRemainingMutations() as $mutation) {
626
+            if (!str_starts_with($mutation, self::FILE_METADATA_PREFIX)) {
627
+                continue;
628
+            }
629
+
630
+            $propPatch->handle(
631
+                $mutation,
632
+                function (mixed $value) use ($accessRight, $knownMetadata, $node, $mutation, $filesMetadataManager): bool {
633
+                    /** @var FilesMetadata $metadata */
634
+                    $metadata = $filesMetadataManager->getMetadata((int)$node->getFileId(), true);
635
+                    $metadata->setStorageId($node->getNode()->getStorage()->getCache()->getNumericStorageId());
636
+                    $metadataKey = substr($mutation, strlen(self::FILE_METADATA_PREFIX));
637
+
638
+                    // confirm metadata key is editable via PROPPATCH
639
+                    if ($knownMetadata->getEditPermission($metadataKey) < $accessRight) {
640
+                        throw new FilesMetadataException('you do not have enough rights to update \'' . $metadataKey . '\' on this node');
641
+                    }
642
+
643
+                    if ($value === null) {
644
+                        $metadata->unset($metadataKey);
645
+                        $filesMetadataManager->saveMetadata($metadata);
646
+                        return true;
647
+                    }
648
+
649
+                    // If the metadata is unknown, it defaults to string.
650
+                    try {
651
+                        $type = $knownMetadata->getType($metadataKey);
652
+                    } catch (FilesMetadataNotFoundException) {
653
+                        $type = IMetadataValueWrapper::TYPE_STRING;
654
+                    }
655
+
656
+                    switch ($type) {
657
+                        case IMetadataValueWrapper::TYPE_STRING:
658
+                            $metadata->setString($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
659
+                            break;
660
+                        case IMetadataValueWrapper::TYPE_INT:
661
+                            $metadata->setInt($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
662
+                            break;
663
+                        case IMetadataValueWrapper::TYPE_FLOAT:
664
+                            $metadata->setFloat($metadataKey, $value);
665
+                            break;
666
+                        case IMetadataValueWrapper::TYPE_BOOL:
667
+                            $metadata->setBool($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
668
+                            break;
669
+                        case IMetadataValueWrapper::TYPE_ARRAY:
670
+                            $metadata->setArray($metadataKey, $value);
671
+                            break;
672
+                        case IMetadataValueWrapper::TYPE_STRING_LIST:
673
+                            $metadata->setStringList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
674
+                            break;
675
+                        case IMetadataValueWrapper::TYPE_INT_LIST:
676
+                            $metadata->setIntList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
677
+                            break;
678
+                    }
679
+
680
+                    $filesMetadataManager->saveMetadata($metadata);
681
+
682
+                    return true;
683
+                }
684
+            );
685
+        }
686
+    }
687
+
688
+    /**
689
+     * init default internal metadata
690
+     *
691
+     * @return IFilesMetadataManager
692
+     */
693
+    private function initFilesMetadataManager(): IFilesMetadataManager {
694
+        /** @var IFilesMetadataManager $manager */
695
+        $manager = \OCP\Server::get(IFilesMetadataManager::class);
696
+        $manager->initMetadata('files-live-photo', IMetadataValueWrapper::TYPE_STRING, false, IMetadataValueWrapper::EDIT_REQ_OWNERSHIP);
697
+
698
+        return $manager;
699
+    }
700
+
701
+    /**
702
+     * based on owner and shares, returns the bottom limit to update related metadata
703
+     *
704
+     * @param Node $node
705
+     * @param string $userId
706
+     *
707
+     * @return int
708
+     */
709
+    private function getMetadataFileAccessRight(Node $node, string $userId): int {
710
+        if ($node->getOwner()?->getUID() === $userId) {
711
+            return IMetadataValueWrapper::EDIT_REQ_OWNERSHIP;
712
+        } else {
713
+            $filePermissions = $node->getSharePermissions($userId);
714
+            if ($filePermissions & Constants::PERMISSION_UPDATE) {
715
+                return IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION;
716
+            }
717
+        }
718
+
719
+        return IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION;
720
+    }
721
+
722
+    /**
723
+     * @param string $filePath
724
+     * @param ?\Sabre\DAV\INode $node
725
+     * @return void
726
+     * @throws \Sabre\DAV\Exception\BadRequest
727
+     */
728
+    public function sendFileIdHeader($filePath, ?\Sabre\DAV\INode $node = null) {
729
+        // we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
730
+        try {
731
+            $node = $this->server->tree->getNodeForPath($filePath);
732
+            if ($node instanceof Node) {
733
+                $fileId = $node->getFileId();
734
+                if (!is_null($fileId)) {
735
+                    $this->server->httpResponse->setHeader('OC-FileId', $fileId);
736
+                }
737
+            }
738
+        } catch (NotFound) {
739
+        }
740
+    }
741 741
 }
Please login to merge, or discard this patch.