FilesPlugin.php 24 KB

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