FilesPlugin.php 23 KB

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