FilesPlugin.php 23 KB

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