You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2265 lines
74 KiB

9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  6. * SPDX-License-Identifier: AGPL-3.0-only
  7. */
  8. namespace OCA\Files_Sharing\Controller;
  9. use Exception;
  10. use OC\Core\AppInfo\ConfigLexicon;
  11. use OC\Files\FileInfo;
  12. use OC\Files\Storage\Wrapper\Wrapper;
  13. use OCA\Circles\Api\v1\Circles;
  14. use OCA\Deck\Sharing\ShareAPIHelper;
  15. use OCA\Federation\TrustedServers;
  16. use OCA\Files\Helper;
  17. use OCA\Files_Sharing\Exceptions\SharingRightsException;
  18. use OCA\Files_Sharing\External\Storage;
  19. use OCA\Files_Sharing\ResponseDefinitions;
  20. use OCA\Files_Sharing\SharedStorage;
  21. use OCA\GlobalSiteSelector\Service\SlaveService;
  22. use OCP\App\IAppManager;
  23. use OCP\AppFramework\Http;
  24. use OCP\AppFramework\Http\Attribute\ApiRoute;
  25. use OCP\AppFramework\Http\Attribute\NoAdminRequired;
  26. use OCP\AppFramework\Http\Attribute\UserRateLimit;
  27. use OCP\AppFramework\Http\DataResponse;
  28. use OCP\AppFramework\OCS\OCSBadRequestException;
  29. use OCP\AppFramework\OCS\OCSException;
  30. use OCP\AppFramework\OCS\OCSForbiddenException;
  31. use OCP\AppFramework\OCS\OCSNotFoundException;
  32. use OCP\AppFramework\OCSController;
  33. use OCP\AppFramework\QueryException;
  34. use OCP\Constants;
  35. use OCP\Files\File;
  36. use OCP\Files\Folder;
  37. use OCP\Files\InvalidPathException;
  38. use OCP\Files\IRootFolder;
  39. use OCP\Files\Mount\IShareOwnerlessMount;
  40. use OCP\Files\Node;
  41. use OCP\Files\NotFoundException;
  42. use OCP\HintException;
  43. use OCP\IAppConfig;
  44. use OCP\IConfig;
  45. use OCP\IDateTimeZone;
  46. use OCP\IGroupManager;
  47. use OCP\IL10N;
  48. use OCP\IPreview;
  49. use OCP\IRequest;
  50. use OCP\ITagManager;
  51. use OCP\IURLGenerator;
  52. use OCP\IUserManager;
  53. use OCP\Lock\ILockingProvider;
  54. use OCP\Lock\LockedException;
  55. use OCP\Mail\IMailer;
  56. use OCP\Server;
  57. use OCP\Share\Exceptions\GenericShareException;
  58. use OCP\Share\Exceptions\ShareNotFound;
  59. use OCP\Share\Exceptions\ShareTokenException;
  60. use OCP\Share\IManager;
  61. use OCP\Share\IProviderFactory;
  62. use OCP\Share\IShare;
  63. use OCP\Share\IShareProviderWithNotification;
  64. use OCP\UserStatus\IManager as IUserStatusManager;
  65. use Psr\Container\ContainerExceptionInterface;
  66. use Psr\Container\ContainerInterface;
  67. use Psr\Log\LoggerInterface;
  68. /**
  69. * @package OCA\Files_Sharing\API
  70. *
  71. * @psalm-import-type Files_SharingShare from ResponseDefinitions
  72. */
  73. class ShareAPIController extends OCSController {
  74. private ?Node $lockedNode = null;
  75. private array $trustedServerCache = [];
  76. /**
  77. * Share20OCS constructor.
  78. */
  79. public function __construct(
  80. string $appName,
  81. IRequest $request,
  82. private IManager $shareManager,
  83. private IGroupManager $groupManager,
  84. private IUserManager $userManager,
  85. private IRootFolder $rootFolder,
  86. private IURLGenerator $urlGenerator,
  87. private IL10N $l,
  88. private IConfig $config,
  89. private IAppConfig $appConfig,
  90. private IAppManager $appManager,
  91. private ContainerInterface $serverContainer,
  92. private IUserStatusManager $userStatusManager,
  93. private IPreview $previewManager,
  94. private IDateTimeZone $dateTimeZone,
  95. private LoggerInterface $logger,
  96. private IProviderFactory $factory,
  97. private IMailer $mailer,
  98. private ITagManager $tagManager,
  99. private ?TrustedServers $trustedServers,
  100. private ?string $userId = null,
  101. ) {
  102. parent::__construct($appName, $request);
  103. }
  104. /**
  105. * Convert an IShare to an array for OCS output
  106. *
  107. * @param IShare $share
  108. * @param Node|null $recipientNode
  109. * @return Files_SharingShare
  110. * @throws NotFoundException In case the node can't be resolved.
  111. *
  112. * @suppress PhanUndeclaredClassMethod
  113. */
  114. protected function formatShare(IShare $share, ?Node $recipientNode = null): array {
  115. $sharedBy = $this->userManager->get($share->getSharedBy());
  116. $shareOwner = $this->userManager->get($share->getShareOwner());
  117. $isOwnShare = false;
  118. if ($shareOwner !== null) {
  119. $isOwnShare = $shareOwner->getUID() === $this->userId;
  120. }
  121. $result = [
  122. 'id' => $share->getId(),
  123. 'share_type' => $share->getShareType(),
  124. 'uid_owner' => $share->getSharedBy(),
  125. 'displayname_owner' => $sharedBy !== null ? $sharedBy->getDisplayName() : $share->getSharedBy(),
  126. // recipient permissions
  127. 'permissions' => $share->getPermissions(),
  128. // current user permissions on this share
  129. 'can_edit' => $this->canEditShare($share),
  130. 'can_delete' => $this->canDeleteShare($share),
  131. 'stime' => $share->getShareTime()->getTimestamp(),
  132. 'parent' => null,
  133. 'expiration' => null,
  134. 'token' => null,
  135. 'uid_file_owner' => $share->getShareOwner(),
  136. 'note' => $share->getNote(),
  137. 'label' => $share->getLabel(),
  138. 'displayname_file_owner' => $shareOwner !== null ? $shareOwner->getDisplayName() : $share->getShareOwner(),
  139. ];
  140. $userFolder = $this->rootFolder->getUserFolder($this->userId);
  141. if ($recipientNode) {
  142. $node = $recipientNode;
  143. } else {
  144. $node = $userFolder->getFirstNodeById($share->getNodeId());
  145. if (!$node) {
  146. // fallback to guessing the path
  147. $node = $userFolder->get($share->getTarget());
  148. if ($node === null || $share->getTarget() === '') {
  149. throw new NotFoundException();
  150. }
  151. }
  152. }
  153. $result['path'] = $userFolder->getRelativePath($node->getPath());
  154. if ($node instanceof Folder) {
  155. $result['item_type'] = 'folder';
  156. } else {
  157. $result['item_type'] = 'file';
  158. }
  159. // Get the original node permission if the share owner is the current user
  160. if ($isOwnShare) {
  161. $result['item_permissions'] = $node->getPermissions();
  162. }
  163. // If we're on the recipient side, the node permissions
  164. // are bound to the share permissions. So we need to
  165. // adjust the permissions to the share permissions if necessary.
  166. if (!$isOwnShare) {
  167. $result['item_permissions'] = $share->getPermissions();
  168. // For some reason, single files share are forbidden to have the delete permission
  169. // since we have custom methods to check those, let's adjust straight away.
  170. // DAV permissions does not have that issue though.
  171. if ($this->canDeleteShare($share) || $this->canDeleteShareFromSelf($share)) {
  172. $result['item_permissions'] |= Constants::PERMISSION_DELETE;
  173. }
  174. if ($this->canEditShare($share)) {
  175. $result['item_permissions'] |= Constants::PERMISSION_UPDATE;
  176. }
  177. }
  178. // See MOUNT_ROOT_PROPERTYNAME dav property
  179. $result['is-mount-root'] = $node->getInternalPath() === '';
  180. $result['mount-type'] = $node->getMountPoint()->getMountType();
  181. $result['mimetype'] = $node->getMimetype();
  182. $result['has_preview'] = $this->previewManager->isAvailable($node);
  183. $result['storage_id'] = $node->getStorage()->getId();
  184. $result['storage'] = $node->getStorage()->getCache()->getNumericStorageId();
  185. $result['item_source'] = $node->getId();
  186. $result['file_source'] = $node->getId();
  187. $result['file_parent'] = $node->getParent()->getId();
  188. $result['file_target'] = $share->getTarget();
  189. $result['item_size'] = $node->getSize();
  190. $result['item_mtime'] = $node->getMTime();
  191. if ($this->trustedServers !== null && in_array($share->getShareType(), [IShare::TYPE_REMOTE, IShare::TYPE_REMOTE_GROUP], true)) {
  192. $result['is_trusted_server'] = false;
  193. $sharedWith = $share->getSharedWith();
  194. $remoteIdentifier = is_string($sharedWith) ? strrchr($sharedWith, '@') : false;
  195. if ($remoteIdentifier !== false) {
  196. $remote = substr($remoteIdentifier, 1);
  197. if (isset($this->trustedServerCache[$remote])) {
  198. $result['is_trusted_server'] = $this->trustedServerCache[$remote];
  199. } else {
  200. try {
  201. $isTrusted = $this->trustedServers->isTrustedServer($remote);
  202. $this->trustedServerCache[$remote] = $isTrusted;
  203. $result['is_trusted_server'] = $isTrusted;
  204. } catch (\Exception $e) {
  205. // Server not found or other issue, we consider it not trusted
  206. $this->trustedServerCache[$remote] = false;
  207. $this->logger->error(
  208. 'Error checking if remote server is trusted (treating as untrusted): ' . $e->getMessage(),
  209. ['exception' => $e]
  210. );
  211. }
  212. }
  213. }
  214. }
  215. $expiration = $share->getExpirationDate();
  216. if ($expiration !== null) {
  217. $expiration->setTimezone($this->dateTimeZone->getTimeZone());
  218. $result['expiration'] = $expiration->format('Y-m-d 00:00:00');
  219. }
  220. if ($share->getShareType() === IShare::TYPE_USER) {
  221. $sharedWith = $this->userManager->get($share->getSharedWith());
  222. $result['share_with'] = $share->getSharedWith();
  223. $result['share_with_displayname'] = $sharedWith !== null ? $sharedWith->getDisplayName() : $share->getSharedWith();
  224. $result['share_with_displayname_unique'] = $sharedWith !== null ? (
  225. !empty($sharedWith->getSystemEMailAddress()) ? $sharedWith->getSystemEMailAddress() : $sharedWith->getUID()
  226. ) : $share->getSharedWith();
  227. $userStatuses = $this->userStatusManager->getUserStatuses([$share->getSharedWith()]);
  228. $userStatus = array_shift($userStatuses);
  229. if ($userStatus) {
  230. $result['status'] = [
  231. 'status' => $userStatus->getStatus(),
  232. 'message' => $userStatus->getMessage(),
  233. 'icon' => $userStatus->getIcon(),
  234. 'clearAt' => $userStatus->getClearAt()
  235. ? (int)$userStatus->getClearAt()->format('U')
  236. : null,
  237. ];
  238. }
  239. } elseif ($share->getShareType() === IShare::TYPE_GROUP) {
  240. $group = $this->groupManager->get($share->getSharedWith());
  241. $result['share_with'] = $share->getSharedWith();
  242. $result['share_with_displayname'] = $group !== null ? $group->getDisplayName() : $share->getSharedWith();
  243. } elseif ($share->getShareType() === IShare::TYPE_LINK) {
  244. // "share_with" and "share_with_displayname" for passwords of link
  245. // shares was deprecated in Nextcloud 15, use "password" instead.
  246. $result['share_with'] = $share->getPassword();
  247. $result['share_with_displayname'] = '(' . $this->l->t('Shared link') . ')';
  248. $result['password'] = $share->getPassword();
  249. $result['send_password_by_talk'] = $share->getSendPasswordByTalk();
  250. $result['token'] = $share->getToken();
  251. $result['url'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $share->getToken()]);
  252. } elseif ($share->getShareType() === IShare::TYPE_REMOTE) {
  253. $result['share_with'] = $share->getSharedWith();
  254. $result['share_with_displayname'] = $this->getCachedFederatedDisplayName($share->getSharedWith());
  255. $result['token'] = $share->getToken();
  256. } elseif ($share->getShareType() === IShare::TYPE_REMOTE_GROUP) {
  257. $result['share_with'] = $share->getSharedWith();
  258. $result['share_with_displayname'] = $this->getDisplayNameFromAddressBook($share->getSharedWith(), 'CLOUD');
  259. $result['token'] = $share->getToken();
  260. } elseif ($share->getShareType() === IShare::TYPE_EMAIL) {
  261. $result['share_with'] = $share->getSharedWith();
  262. $result['password'] = $share->getPassword();
  263. $result['password_expiration_time'] = $share->getPasswordExpirationTime() !== null ? $share->getPasswordExpirationTime()->format(\DateTime::ATOM) : null;
  264. $result['send_password_by_talk'] = $share->getSendPasswordByTalk();
  265. $result['share_with_displayname'] = $this->getDisplayNameFromAddressBook($share->getSharedWith(), 'EMAIL');
  266. $result['token'] = $share->getToken();
  267. } elseif ($share->getShareType() === IShare::TYPE_CIRCLE) {
  268. // getSharedWith() returns either "name (type, owner)" or
  269. // "name (type, owner) [id]", depending on the Teams app version.
  270. $hasCircleId = (substr($share->getSharedWith(), -1) === ']');
  271. $result['share_with_displayname'] = $share->getSharedWithDisplayName();
  272. if (empty($result['share_with_displayname'])) {
  273. $displayNameLength = ($hasCircleId ? strrpos($share->getSharedWith(), ' ') : strlen($share->getSharedWith()));
  274. $result['share_with_displayname'] = substr($share->getSharedWith(), 0, $displayNameLength);
  275. }
  276. $result['share_with_avatar'] = $share->getSharedWithAvatar();
  277. $shareWithStart = ($hasCircleId ? strrpos($share->getSharedWith(), '[') + 1 : 0);
  278. $shareWithLength = ($hasCircleId ? -1 : strpos($share->getSharedWith(), ' '));
  279. if ($shareWithLength === false) {
  280. $result['share_with'] = substr($share->getSharedWith(), $shareWithStart);
  281. } else {
  282. $result['share_with'] = substr($share->getSharedWith(), $shareWithStart, $shareWithLength);
  283. }
  284. } elseif ($share->getShareType() === IShare::TYPE_ROOM) {
  285. $result['share_with'] = $share->getSharedWith();
  286. $result['share_with_displayname'] = '';
  287. try {
  288. /** @var array{share_with_displayname: string, share_with_link: string, share_with?: string, token?: string} $roomShare */
  289. $roomShare = $this->getRoomShareHelper()->formatShare($share);
  290. $result = array_merge($result, $roomShare);
  291. } catch (ContainerExceptionInterface $e) {
  292. }
  293. } elseif ($share->getShareType() === IShare::TYPE_DECK) {
  294. $result['share_with'] = $share->getSharedWith();
  295. $result['share_with_displayname'] = '';
  296. try {
  297. /** @var array{share_with: string, share_with_displayname: string, share_with_link: string} $deckShare */
  298. $deckShare = $this->getDeckShareHelper()->formatShare($share);
  299. $result = array_merge($result, $deckShare);
  300. } catch (ContainerExceptionInterface $e) {
  301. }
  302. } elseif ($share->getShareType() === IShare::TYPE_SCIENCEMESH) {
  303. $result['share_with'] = $share->getSharedWith();
  304. $result['share_with_displayname'] = '';
  305. try {
  306. /** @var array{share_with: string, share_with_displayname: string, token: string} $scienceMeshShare */
  307. $scienceMeshShare = $this->getSciencemeshShareHelper()->formatShare($share);
  308. $result = array_merge($result, $scienceMeshShare);
  309. } catch (ContainerExceptionInterface $e) {
  310. }
  311. }
  312. $result['mail_send'] = $share->getMailSend() ? 1 : 0;
  313. $result['hide_download'] = $share->getHideDownload() ? 1 : 0;
  314. $result['attributes'] = null;
  315. if ($attributes = $share->getAttributes()) {
  316. $result['attributes'] = (string)\json_encode($attributes->toArray());
  317. }
  318. return $result;
  319. }
  320. /**
  321. * Check if one of the users address books knows the exact property, if
  322. * not we return the full name.
  323. *
  324. * @param string $query
  325. * @param string $property
  326. * @return string
  327. */
  328. private function getDisplayNameFromAddressBook(string $query, string $property): string {
  329. // FIXME: If we inject the contacts manager it gets initialized before any address books are registered
  330. try {
  331. $result = Server::get(\OCP\Contacts\IManager::class)->search($query, [$property], [
  332. 'limit' => 1,
  333. 'enumeration' => false,
  334. 'strict_search' => true,
  335. ]);
  336. } catch (Exception $e) {
  337. $this->logger->error(
  338. $e->getMessage(),
  339. ['exception' => $e]
  340. );
  341. return $query;
  342. }
  343. foreach ($result as $r) {
  344. foreach ($r[$property] as $value) {
  345. if ($value === $query && $r['FN']) {
  346. return $r['FN'];
  347. }
  348. }
  349. }
  350. return $query;
  351. }
  352. /**
  353. * @param list<Files_SharingShare> $shares
  354. * @param array<string, string>|null $updatedDisplayName
  355. *
  356. * @return list<Files_SharingShare>
  357. */
  358. private function fixMissingDisplayName(array $shares, ?array $updatedDisplayName = null): array {
  359. $userIds = $updated = [];
  360. foreach ($shares as $share) {
  361. // share is federated and share have no display name yet
  362. if ($share['share_type'] === IShare::TYPE_REMOTE
  363. && ($share['share_with'] ?? '') !== ''
  364. && ($share['share_with_displayname'] ?? '') === '') {
  365. $userIds[] = $userId = $share['share_with'];
  366. if ($updatedDisplayName !== null && array_key_exists($userId, $updatedDisplayName)) {
  367. $share['share_with_displayname'] = $updatedDisplayName[$userId];
  368. }
  369. }
  370. // prepping userIds with displayName to be updated
  371. $updated[] = $share;
  372. }
  373. // if $updatedDisplayName is not null, it means we should have already fixed displayNames of the shares
  374. if ($updatedDisplayName !== null) {
  375. return $updated;
  376. }
  377. // get displayName for the generated list of userId with no displayName
  378. $displayNames = $this->retrieveFederatedDisplayName($userIds);
  379. // if no displayName are updated, we exit
  380. if (empty($displayNames)) {
  381. return $updated;
  382. }
  383. // let's fix missing display name and returns all shares
  384. return $this->fixMissingDisplayName($shares, $displayNames);
  385. }
  386. /**
  387. * get displayName of a list of userIds from the lookup-server; through the globalsiteselector app.
  388. * returns an array with userIds as keys and displayName as values.
  389. *
  390. * @param array $userIds
  391. * @param bool $cacheOnly - do not reach LUS, get data from cache.
  392. *
  393. * @return array
  394. * @throws ContainerExceptionInterface
  395. */
  396. private function retrieveFederatedDisplayName(array $userIds, bool $cacheOnly = false): array {
  397. // check if gss is enabled and available
  398. if (count($userIds) === 0
  399. || !$this->appManager->isEnabledForAnyone('globalsiteselector')
  400. || !class_exists('\OCA\GlobalSiteSelector\Service\SlaveService')) {
  401. return [];
  402. }
  403. try {
  404. $slaveService = Server::get(SlaveService::class);
  405. } catch (\Throwable $e) {
  406. $this->logger->error(
  407. $e->getMessage(),
  408. ['exception' => $e]
  409. );
  410. return [];
  411. }
  412. return $slaveService->getUsersDisplayName($userIds, $cacheOnly);
  413. }
  414. /**
  415. * retrieve displayName from cache if available (should be used on federated shares)
  416. * if not available in cache/lus, try for get from address-book, else returns empty string.
  417. *
  418. * @param string $userId
  419. * @param bool $cacheOnly if true will not reach the lus but will only get data from cache
  420. *
  421. * @return string
  422. */
  423. private function getCachedFederatedDisplayName(string $userId, bool $cacheOnly = true): string {
  424. $details = $this->retrieveFederatedDisplayName([$userId], $cacheOnly);
  425. if (array_key_exists($userId, $details)) {
  426. return $details[$userId];
  427. }
  428. $displayName = $this->getDisplayNameFromAddressBook($userId, 'CLOUD');
  429. return ($displayName === $userId) ? '' : $displayName;
  430. }
  431. /**
  432. * Get a specific share by id
  433. *
  434. * @param string $id ID of the share
  435. * @param bool $include_tags Include tags in the share
  436. * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}>
  437. * @throws OCSNotFoundException Share not found
  438. *
  439. * 200: Share returned
  440. */
  441. #[NoAdminRequired]
  442. public function getShare(string $id, bool $include_tags = false): DataResponse {
  443. try {
  444. $share = $this->getShareById($id);
  445. } catch (ShareNotFound $e) {
  446. throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
  447. }
  448. try {
  449. if ($this->canAccessShare($share)) {
  450. $share = $this->formatShare($share);
  451. if ($include_tags) {
  452. $share = $this->populateTags([$share]);
  453. } else {
  454. $share = [$share];
  455. }
  456. return new DataResponse($share);
  457. }
  458. } catch (NotFoundException $e) {
  459. // Fall through
  460. }
  461. throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
  462. }
  463. /**
  464. * Delete a share
  465. *
  466. * @param string $id ID of the share
  467. * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
  468. * @throws OCSNotFoundException Share not found
  469. * @throws OCSForbiddenException Missing permissions to delete the share
  470. *
  471. * 200: Share deleted successfully
  472. */
  473. #[NoAdminRequired]
  474. public function deleteShare(string $id): DataResponse {
  475. try {
  476. $share = $this->getShareById($id);
  477. } catch (ShareNotFound $e) {
  478. throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
  479. }
  480. try {
  481. $this->lock($share->getNode());
  482. } catch (LockedException $e) {
  483. throw new OCSNotFoundException($this->l->t('Could not delete share'));
  484. }
  485. if (!$this->canAccessShare($share)) {
  486. throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
  487. }
  488. // if it's a group share or a room share
  489. // we don't delete the share, but only the
  490. // mount point. Allowing it to be restored
  491. // from the deleted shares
  492. if ($this->canDeleteShareFromSelf($share)) {
  493. $this->shareManager->deleteFromSelf($share, $this->userId);
  494. } else {
  495. if (!$this->canDeleteShare($share)) {
  496. throw new OCSForbiddenException($this->l->t('Could not delete share'));
  497. }
  498. $this->shareManager->deleteShare($share);
  499. }
  500. return new DataResponse();
  501. }
  502. /**
  503. * Create a share
  504. *
  505. * @param string|null $path Path of the share
  506. * @param int|null $permissions Permissions for the share
  507. * @param int $shareType Type of the share
  508. * @param ?string $shareWith The entity this should be shared with
  509. * @param 'true'|'false'|null $publicUpload If public uploading is allowed (deprecated)
  510. * @param string $password Password for the share
  511. * @param string|null $sendPasswordByTalk Send the password for the share over Talk
  512. * @param ?string $expireDate The expiry date of the share in the user's timezone at 00:00.
  513. * If $expireDate is not supplied or set to `null`, the system default will be used.
  514. * @param string $note Note for the share
  515. * @param string $label Label for the share (only used in link and email)
  516. * @param string|null $attributes Additional attributes for the share
  517. * @param 'false'|'true'|null $sendMail Send a mail to the recipient
  518. *
  519. * @return DataResponse<Http::STATUS_OK, Files_SharingShare, array{}>
  520. * @throws OCSBadRequestException Unknown share type
  521. * @throws OCSException
  522. * @throws OCSForbiddenException Creating the share is not allowed
  523. * @throws OCSNotFoundException Creating the share failed
  524. * @suppress PhanUndeclaredClassMethod
  525. *
  526. * 200: Share created
  527. */
  528. #[NoAdminRequired]
  529. #[UserRateLimit(limit: 20, period: 600)]
  530. public function createShare(
  531. ?string $path = null,
  532. ?int $permissions = null,
  533. int $shareType = -1,
  534. ?string $shareWith = null,
  535. ?string $publicUpload = null,
  536. string $password = '',
  537. ?string $sendPasswordByTalk = null,
  538. ?string $expireDate = null,
  539. string $note = '',
  540. string $label = '',
  541. ?string $attributes = null,
  542. ?string $sendMail = null,
  543. ): DataResponse {
  544. assert($this->userId !== null);
  545. $share = $this->shareManager->newShare();
  546. $hasPublicUpload = $this->getLegacyPublicUpload($publicUpload);
  547. // Verify path
  548. if ($path === null) {
  549. throw new OCSNotFoundException($this->l->t('Please specify a file or folder path'));
  550. }
  551. $userFolder = $this->rootFolder->getUserFolder($this->userId);
  552. try {
  553. /** @var \OC\Files\Node\Node $node */
  554. $node = $userFolder->get($path);
  555. } catch (NotFoundException $e) {
  556. throw new OCSNotFoundException($this->l->t('Wrong path, file/folder does not exist'));
  557. }
  558. // a user can have access to a file through different paths, with differing permissions
  559. // combine all permissions to determine if the user can share this file
  560. $nodes = $userFolder->getById($node->getId());
  561. foreach ($nodes as $nodeById) {
  562. /** @var FileInfo $fileInfo */
  563. $fileInfo = $node->getFileInfo();
  564. $fileInfo['permissions'] |= $nodeById->getPermissions();
  565. }
  566. $share->setNode($node);
  567. try {
  568. $this->lock($share->getNode());
  569. } catch (LockedException $e) {
  570. throw new OCSNotFoundException($this->l->t('Could not create share'));
  571. }
  572. // Set permissions
  573. if ($shareType === IShare::TYPE_LINK || $shareType === IShare::TYPE_EMAIL) {
  574. $permissions = $this->getLinkSharePermissions($permissions, $hasPublicUpload);
  575. $this->validateLinkSharePermissions($node, $permissions, $hasPublicUpload);
  576. } else {
  577. // Use default permissions only for non-link shares to keep legacy behavior
  578. if ($permissions === null) {
  579. $permissions = (int)$this->config->getAppValue('core', 'shareapi_default_permissions', (string)Constants::PERMISSION_ALL);
  580. }
  581. // Non-link shares always require read permissions (link shares could be file drop)
  582. $permissions |= Constants::PERMISSION_READ;
  583. }
  584. // For legacy reasons the API allows to pass PERMISSIONS_ALL even for single file shares (I look at you Talk)
  585. if ($node instanceof File) {
  586. // if this is a single file share we remove the DELETE and CREATE permissions
  587. $permissions = $permissions & ~(Constants::PERMISSION_DELETE | Constants::PERMISSION_CREATE);
  588. }
  589. /**
  590. * Hack for https://github.com/owncloud/core/issues/22587
  591. * We check the permissions via webdav. But the permissions of the mount point
  592. * do not equal the share permissions. Here we fix that for federated mounts.
  593. */
  594. if ($node->getStorage()->instanceOfStorage(Storage::class)) {
  595. $permissions &= ~($permissions & ~$node->getPermissions());
  596. }
  597. if ($attributes !== null) {
  598. $share = $this->setShareAttributes($share, $attributes);
  599. }
  600. // Expire date checks
  601. // Normally, null means no expiration date but we still set the default for backwards compatibility
  602. // If the client sends an empty string, we set noExpirationDate to true
  603. if ($expireDate !== null) {
  604. if ($expireDate !== '') {
  605. try {
  606. $expireDateTime = $this->parseDate($expireDate);
  607. $share->setExpirationDate($expireDateTime);
  608. } catch (\Exception $e) {
  609. throw new OCSNotFoundException($e->getMessage(), $e);
  610. }
  611. } else {
  612. // Client sent empty string for expire date.
  613. // Set noExpirationDate to true so overwrite is prevented.
  614. $share->setNoExpirationDate(true);
  615. }
  616. }
  617. $share->setSharedBy($this->userId);
  618. // Handle mail send
  619. if (is_null($sendMail)) {
  620. $allowSendMail = $this->config->getSystemValueBool('sharing.enable_share_mail', true);
  621. if ($allowSendMail !== true || $shareType === IShare::TYPE_EMAIL) {
  622. // Define a default behavior when sendMail is not provided
  623. // For email shares with a valid recipient, the default is to send the mail
  624. // For all other share types, the default is to not send the mail
  625. $allowSendMail = ($shareType === IShare::TYPE_EMAIL && $shareWith !== null && $shareWith !== '');
  626. }
  627. $share->setMailSend($allowSendMail);
  628. } else {
  629. $share->setMailSend($sendMail === 'true');
  630. }
  631. if ($shareType === IShare::TYPE_USER) {
  632. // Valid user is required to share
  633. if ($shareWith === null || !$this->userManager->userExists($shareWith)) {
  634. throw new OCSNotFoundException($this->l->t('Please specify a valid account to share with'));
  635. }
  636. $share->setSharedWith($shareWith);
  637. $share->setPermissions($permissions);
  638. } elseif ($shareType === IShare::TYPE_GROUP) {
  639. if (!$this->shareManager->allowGroupSharing()) {
  640. throw new OCSNotFoundException($this->l->t('Group sharing is disabled by the administrator'));
  641. }
  642. // Valid group is required to share
  643. if ($shareWith === null || !$this->groupManager->groupExists($shareWith)) {
  644. throw new OCSNotFoundException($this->l->t('Please specify a valid group'));
  645. }
  646. $share->setSharedWith($shareWith);
  647. $share->setPermissions($permissions);
  648. } elseif ($shareType === IShare::TYPE_LINK
  649. || $shareType === IShare::TYPE_EMAIL) {
  650. // Can we even share links?
  651. if (!$this->shareManager->shareApiAllowLinks()) {
  652. throw new OCSNotFoundException($this->l->t('Public link sharing is disabled by the administrator'));
  653. }
  654. $this->validateLinkSharePermissions($node, $permissions, $hasPublicUpload);
  655. $share->setPermissions($permissions);
  656. // Set password
  657. if ($password !== '') {
  658. $share->setPassword($password);
  659. }
  660. // Only share by mail have a recipient
  661. if (is_string($shareWith) && $shareType === IShare::TYPE_EMAIL) {
  662. // If sending a mail have been requested, validate the mail address
  663. if ($share->getMailSend() && !$this->mailer->validateMailAddress($shareWith)) {
  664. throw new OCSNotFoundException($this->l->t('Please specify a valid email address'));
  665. }
  666. $share->setSharedWith($shareWith);
  667. }
  668. // If we have a label, use it
  669. if ($label !== '') {
  670. if (strlen($label) > 255) {
  671. throw new OCSBadRequestException('Maximum label length is 255');
  672. }
  673. $share->setLabel($label);
  674. }
  675. if ($sendPasswordByTalk === 'true') {
  676. if (!$this->appManager->isEnabledForUser('spreed')) {
  677. throw new OCSForbiddenException($this->l->t('Sharing %s sending the password by Nextcloud Talk failed because Nextcloud Talk is not enabled', [$node->getPath()]));
  678. }
  679. $share->setSendPasswordByTalk(true);
  680. }
  681. } elseif ($shareType === IShare::TYPE_REMOTE) {
  682. if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) {
  683. throw new OCSForbiddenException($this->l->t('Sharing %1$s failed because the back end does not allow shares from type %2$s', [$node->getPath(), $shareType]));
  684. }
  685. if ($shareWith === null) {
  686. throw new OCSNotFoundException($this->l->t('Please specify a valid federated account ID'));
  687. }
  688. $share->setSharedWith($shareWith);
  689. $share->setPermissions($permissions);
  690. $share->setSharedWithDisplayName($this->getCachedFederatedDisplayName($shareWith, false));
  691. } elseif ($shareType === IShare::TYPE_REMOTE_GROUP) {
  692. if (!$this->shareManager->outgoingServer2ServerGroupSharesAllowed()) {
  693. throw new OCSForbiddenException($this->l->t('Sharing %1$s failed because the back end does not allow shares from type %2$s', [$node->getPath(), $shareType]));
  694. }
  695. if ($shareWith === null) {
  696. throw new OCSNotFoundException($this->l->t('Please specify a valid federated group ID'));
  697. }
  698. $share->setSharedWith($shareWith);
  699. $share->setPermissions($permissions);
  700. } elseif ($shareType === IShare::TYPE_CIRCLE) {
  701. if (!Server::get(IAppManager::class)->isEnabledForUser('circles') || !class_exists('\OCA\Circles\ShareByCircleProvider')) {
  702. throw new OCSNotFoundException($this->l->t('You cannot share to a Team if the app is not enabled'));
  703. }
  704. $circle = Circles::detailsCircle($shareWith);
  705. // Valid team is required to share
  706. if ($circle === null) {
  707. throw new OCSNotFoundException($this->l->t('Please specify a valid team'));
  708. }
  709. $share->setSharedWith($shareWith);
  710. $share->setPermissions($permissions);
  711. } elseif ($shareType === IShare::TYPE_ROOM) {
  712. try {
  713. $this->getRoomShareHelper()->createShare($share, $shareWith, $permissions, $expireDate ?? '');
  714. } catch (ContainerExceptionInterface $e) {
  715. throw new OCSForbiddenException($this->l->t('Sharing %s failed because the back end does not support room shares', [$node->getPath()]));
  716. }
  717. } elseif ($shareType === IShare::TYPE_DECK) {
  718. try {
  719. $this->getDeckShareHelper()->createShare($share, $shareWith, $permissions, $expireDate ?? '');
  720. } catch (ContainerExceptionInterface $e) {
  721. throw new OCSForbiddenException($this->l->t('Sharing %s failed because the back end does not support room shares', [$node->getPath()]));
  722. }
  723. } elseif ($shareType === IShare::TYPE_SCIENCEMESH) {
  724. try {
  725. $this->getSciencemeshShareHelper()->createShare($share, $shareWith, $permissions, $expireDate ?? '');
  726. } catch (ContainerExceptionInterface $e) {
  727. throw new OCSForbiddenException($this->l->t('Sharing %s failed because the back end does not support ScienceMesh shares', [$node->getPath()]));
  728. }
  729. } else {
  730. throw new OCSBadRequestException($this->l->t('Unknown share type'));
  731. }
  732. $share->setShareType($shareType);
  733. $this->checkInheritedAttributes($share);
  734. if ($note !== '') {
  735. $share->setNote($note);
  736. }
  737. try {
  738. $share = $this->shareManager->createShare($share);
  739. } catch (HintException $e) {
  740. $code = $e->getCode() === 0 ? 403 : $e->getCode();
  741. throw new OCSException($e->getHint(), $code);
  742. } catch (GenericShareException|\InvalidArgumentException $e) {
  743. $this->logger->error($e->getMessage(), ['exception' => $e]);
  744. throw new OCSForbiddenException($e->getMessage(), $e);
  745. } catch (\Exception $e) {
  746. $this->logger->error($e->getMessage(), ['exception' => $e]);
  747. throw new OCSForbiddenException('Failed to create share.', $e);
  748. }
  749. $output = $this->formatShare($share);
  750. return new DataResponse($output);
  751. }
  752. /**
  753. * @param null|Node $node
  754. * @param boolean $includeTags
  755. *
  756. * @return list<Files_SharingShare>
  757. */
  758. private function getSharedWithMe($node, bool $includeTags): array {
  759. $userShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_USER, $node, -1, 0);
  760. $groupShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_GROUP, $node, -1, 0);
  761. $circleShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_CIRCLE, $node, -1, 0);
  762. $roomShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_ROOM, $node, -1, 0);
  763. $deckShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_DECK, $node, -1, 0);
  764. $sciencemeshShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_SCIENCEMESH, $node, -1, 0);
  765. $shares = array_merge($userShares, $groupShares, $circleShares, $roomShares, $deckShares, $sciencemeshShares);
  766. $filteredShares = array_filter($shares, function (IShare $share) {
  767. return $share->getShareOwner() !== $this->userId && $share->getSharedBy() !== $this->userId;
  768. });
  769. $formatted = [];
  770. foreach ($filteredShares as $share) {
  771. if ($this->canAccessShare($share)) {
  772. try {
  773. $formatted[] = $this->formatShare($share);
  774. } catch (NotFoundException $e) {
  775. // Ignore this share
  776. }
  777. }
  778. }
  779. if ($includeTags) {
  780. $formatted = $this->populateTags($formatted);
  781. }
  782. return $formatted;
  783. }
  784. /**
  785. * @param Node $folder
  786. *
  787. * @return list<Files_SharingShare>
  788. * @throws OCSBadRequestException
  789. * @throws NotFoundException
  790. */
  791. private function getSharesInDir(Node $folder): array {
  792. if (!($folder instanceof Folder)) {
  793. throw new OCSBadRequestException($this->l->t('Not a directory'));
  794. }
  795. $nodes = $folder->getDirectoryListing();
  796. /** @var IShare[] $shares */
  797. $shares = array_reduce($nodes, function ($carry, $node) {
  798. $carry = array_merge($carry, $this->getAllShares($node, true));
  799. return $carry;
  800. }, []);
  801. // filter out duplicate shares
  802. $known = [];
  803. $formatted = $miniFormatted = [];
  804. $resharingRight = false;
  805. $known = [];
  806. foreach ($shares as $share) {
  807. if (in_array($share->getId(), $known) || $share->getSharedWith() === $this->userId) {
  808. continue;
  809. }
  810. try {
  811. $format = $this->formatShare($share);
  812. $known[] = $share->getId();
  813. $formatted[] = $format;
  814. if ($share->getSharedBy() === $this->userId) {
  815. $miniFormatted[] = $format;
  816. }
  817. if (!$resharingRight && $this->shareProviderResharingRights($this->userId, $share, $folder)) {
  818. $resharingRight = true;
  819. }
  820. } catch (\Exception $e) {
  821. //Ignore this share
  822. }
  823. }
  824. if (!$resharingRight) {
  825. $formatted = $miniFormatted;
  826. }
  827. return $formatted;
  828. }
  829. /**
  830. * Get shares of the current user
  831. *
  832. * @param string $shared_with_me Only get shares with the current user
  833. * @param string $reshares Only get shares by the current user and reshares
  834. * @param string $subfiles Only get all shares in a folder
  835. * @param string $path Get shares for a specific path
  836. * @param string $include_tags Include tags in the share
  837. *
  838. * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}>
  839. * @throws OCSNotFoundException The folder was not found or is inaccessible
  840. *
  841. * 200: Shares returned
  842. */
  843. #[NoAdminRequired]
  844. public function getShares(
  845. string $shared_with_me = 'false',
  846. string $reshares = 'false',
  847. string $subfiles = 'false',
  848. string $path = '',
  849. string $include_tags = 'false',
  850. ): DataResponse {
  851. $node = null;
  852. if ($path !== '') {
  853. $userFolder = $this->rootFolder->getUserFolder($this->userId);
  854. try {
  855. $node = $userFolder->get($path);
  856. $this->lock($node);
  857. } catch (NotFoundException $e) {
  858. throw new OCSNotFoundException(
  859. $this->l->t('Wrong path, file/folder does not exist')
  860. );
  861. } catch (LockedException $e) {
  862. throw new OCSNotFoundException($this->l->t('Could not lock node'));
  863. }
  864. }
  865. $shares = $this->getFormattedShares(
  866. $this->userId,
  867. $node,
  868. ($shared_with_me === 'true'),
  869. ($reshares === 'true'),
  870. ($subfiles === 'true'),
  871. ($include_tags === 'true')
  872. );
  873. return new DataResponse($shares);
  874. }
  875. private function getLinkSharePermissions(?int $permissions, ?bool $legacyPublicUpload): int {
  876. $permissions = $permissions ?? Constants::PERMISSION_READ;
  877. // Legacy option handling
  878. if ($legacyPublicUpload !== null) {
  879. $permissions = $legacyPublicUpload
  880. ? (Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE)
  881. : Constants::PERMISSION_READ;
  882. }
  883. if ($this->hasPermission($permissions, Constants::PERMISSION_READ)
  884. && $this->shareManager->outgoingServer2ServerSharesAllowed()
  885. && $this->appConfig->getValueBool('core', ConfigLexicon::SHAREAPI_ALLOW_FEDERATION_ON_PUBLIC_SHARES)) {
  886. $permissions |= Constants::PERMISSION_SHARE;
  887. }
  888. return $permissions;
  889. }
  890. /**
  891. * Helper to check for legacy "publicUpload" handling.
  892. * If the value is set to `true` or `false` then true or false are returned.
  893. * Otherwise null is returned to indicate that the option was not (or wrong) set.
  894. *
  895. * @param null|string $legacyPublicUpload The value of `publicUpload`
  896. */
  897. private function getLegacyPublicUpload(?string $legacyPublicUpload): ?bool {
  898. if ($legacyPublicUpload === 'true') {
  899. return true;
  900. } elseif ($legacyPublicUpload === 'false') {
  901. return false;
  902. }
  903. // Not set at all
  904. return null;
  905. }
  906. /**
  907. * For link and email shares validate that only allowed combinations are set.
  908. *
  909. * @throw OCSBadRequestException If permission combination is invalid.
  910. * @throw OCSForbiddenException If public upload was forbidden by the administrator.
  911. */
  912. private function validateLinkSharePermissions(Node $node, int $permissions, ?bool $legacyPublicUpload): void {
  913. if ($legacyPublicUpload && ($node instanceof File)) {
  914. throw new OCSBadRequestException($this->l->t('Public upload is only possible for publicly shared folders'));
  915. }
  916. // We need at least READ or CREATE (file drop)
  917. if (!$this->hasPermission($permissions, Constants::PERMISSION_READ)
  918. && !$this->hasPermission($permissions, Constants::PERMISSION_CREATE)) {
  919. throw new OCSBadRequestException($this->l->t('Share must at least have READ or CREATE permissions'));
  920. }
  921. // UPDATE and DELETE require a READ permission
  922. if (!$this->hasPermission($permissions, Constants::PERMISSION_READ)
  923. && ($this->hasPermission($permissions, Constants::PERMISSION_UPDATE) || $this->hasPermission($permissions, Constants::PERMISSION_DELETE))) {
  924. throw new OCSBadRequestException($this->l->t('Share must have READ permission if UPDATE or DELETE permission is set'));
  925. }
  926. // Check if public uploading was disabled
  927. if ($this->hasPermission($permissions, Constants::PERMISSION_CREATE)
  928. && !$this->shareManager->shareApiLinkAllowPublicUpload()) {
  929. throw new OCSForbiddenException($this->l->t('Public upload disabled by the administrator'));
  930. }
  931. }
  932. /**
  933. * @param string $viewer
  934. * @param Node $node
  935. * @param bool $sharedWithMe
  936. * @param bool $reShares
  937. * @param bool $subFiles
  938. * @param bool $includeTags
  939. *
  940. * @return list<Files_SharingShare>
  941. * @throws NotFoundException
  942. * @throws OCSBadRequestException
  943. */
  944. private function getFormattedShares(
  945. string $viewer,
  946. $node = null,
  947. bool $sharedWithMe = false,
  948. bool $reShares = false,
  949. bool $subFiles = false,
  950. bool $includeTags = false,
  951. ): array {
  952. if ($sharedWithMe) {
  953. return $this->getSharedWithMe($node, $includeTags);
  954. }
  955. if ($subFiles) {
  956. return $this->getSharesInDir($node);
  957. }
  958. $shares = $this->getSharesFromNode($viewer, $node, $reShares);
  959. $known = $formatted = $miniFormatted = [];
  960. $resharingRight = false;
  961. foreach ($shares as $share) {
  962. try {
  963. $share->getNode();
  964. } catch (NotFoundException $e) {
  965. /*
  966. * Ignore shares where we can't get the node
  967. * For example deleted shares
  968. */
  969. continue;
  970. }
  971. if (in_array($share->getId(), $known)
  972. || ($share->getSharedWith() === $this->userId && $share->getShareType() === IShare::TYPE_USER)) {
  973. continue;
  974. }
  975. $known[] = $share->getId();
  976. try {
  977. /** @var IShare $share */
  978. $format = $this->formatShare($share, $node);
  979. $formatted[] = $format;
  980. // let's also build a list of shares created
  981. // by the current user only, in case
  982. // there is no resharing rights
  983. if ($share->getSharedBy() === $this->userId) {
  984. $miniFormatted[] = $format;
  985. }
  986. // check if one of those share is shared with me
  987. // and if I have resharing rights on it
  988. if (!$resharingRight && $this->shareProviderResharingRights($this->userId, $share, $node)) {
  989. $resharingRight = true;
  990. }
  991. } catch (InvalidPathException|NotFoundException $e) {
  992. }
  993. }
  994. if (!$resharingRight) {
  995. $formatted = $miniFormatted;
  996. }
  997. // fix eventual missing display name from federated shares
  998. $formatted = $this->fixMissingDisplayName($formatted);
  999. if ($includeTags) {
  1000. $formatted = $this->populateTags($formatted);
  1001. }
  1002. return $formatted;
  1003. }
  1004. /**
  1005. * Get all shares relative to a file, including parent folders shares rights
  1006. *
  1007. * @param string $path Path all shares will be relative to
  1008. *
  1009. * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}>
  1010. * @throws InvalidPathException
  1011. * @throws NotFoundException
  1012. * @throws OCSNotFoundException The given path is invalid
  1013. * @throws SharingRightsException
  1014. *
  1015. * 200: Shares returned
  1016. */
  1017. #[NoAdminRequired]
  1018. public function getInheritedShares(string $path): DataResponse {
  1019. // get Node from (string) path.
  1020. $userFolder = $this->rootFolder->getUserFolder($this->userId);
  1021. try {
  1022. $node = $userFolder->get($path);
  1023. $this->lock($node);
  1024. } catch (NotFoundException $e) {
  1025. throw new OCSNotFoundException($this->l->t('Wrong path, file/folder does not exist'));
  1026. } catch (LockedException $e) {
  1027. throw new OCSNotFoundException($this->l->t('Could not lock path'));
  1028. }
  1029. if (!($node->getPermissions() & Constants::PERMISSION_SHARE)) {
  1030. throw new SharingRightsException($this->l->t('no sharing rights on this item'));
  1031. }
  1032. // The current top parent we have access to
  1033. $parent = $node;
  1034. // initiate real owner.
  1035. $owner = $node->getOwner()
  1036. ->getUID();
  1037. if (!$this->userManager->userExists($owner)) {
  1038. return new DataResponse([]);
  1039. }
  1040. // get node based on the owner, fix owner in case of external storage
  1041. $userFolder = $this->rootFolder->getUserFolder($owner);
  1042. if ($node->getId() !== $userFolder->getId() && !$userFolder->isSubNode($node)) {
  1043. $owner = $node->getOwner()
  1044. ->getUID();
  1045. $userFolder = $this->rootFolder->getUserFolder($owner);
  1046. $node = $userFolder->getFirstNodeById($node->getId());
  1047. }
  1048. $basePath = $userFolder->getPath();
  1049. // generate node list for each parent folders
  1050. /** @var Node[] $nodes */
  1051. $nodes = [];
  1052. while (true) {
  1053. $node = $node->getParent();
  1054. if ($node->getPath() === $basePath) {
  1055. break;
  1056. }
  1057. $nodes[] = $node;
  1058. }
  1059. // The user that is requesting this list
  1060. $currentUserFolder = $this->rootFolder->getUserFolder($this->userId);
  1061. // for each nodes, retrieve shares.
  1062. $shares = [];
  1063. foreach ($nodes as $node) {
  1064. $getShares = $this->getFormattedShares($owner, $node, false, true);
  1065. $currentUserNode = $currentUserFolder->getFirstNodeById($node->getId());
  1066. if ($currentUserNode) {
  1067. $parent = $currentUserNode;
  1068. }
  1069. $subPath = $currentUserFolder->getRelativePath($parent->getPath());
  1070. foreach ($getShares as &$share) {
  1071. $share['via_fileid'] = $parent->getId();
  1072. $share['via_path'] = $subPath;
  1073. }
  1074. $this->mergeFormattedShares($shares, $getShares);
  1075. }
  1076. return new DataResponse(array_values($shares));
  1077. }
  1078. /**
  1079. * Check whether a set of permissions contains the permissions to check.
  1080. */
  1081. private function hasPermission(int $permissionsSet, int $permissionsToCheck): bool {
  1082. return ($permissionsSet & $permissionsToCheck) === $permissionsToCheck;
  1083. }
  1084. /**
  1085. * Update a share
  1086. *
  1087. * @param string $id ID of the share
  1088. * @param int|null $permissions New permissions
  1089. * @param string|null $password New password
  1090. * @param string|null $sendPasswordByTalk New condition if the password should be send over Talk
  1091. * @param string|null $publicUpload New condition if public uploading is allowed
  1092. * @param string|null $expireDate New expiry date
  1093. * @param string|null $note New note
  1094. * @param string|null $label New label
  1095. * @param string|null $hideDownload New condition if the download should be hidden
  1096. * @param string|null $attributes New additional attributes
  1097. * @param string|null $sendMail if the share should be send by mail.
  1098. * Considering the share already exists, no mail will be send after the share is updated.
  1099. * You will have to use the sendMail action to send the mail.
  1100. * @param string|null $shareWith New recipient for email shares
  1101. * @param string|null $token New token
  1102. * @return DataResponse<Http::STATUS_OK, Files_SharingShare, array{}>
  1103. * @throws OCSBadRequestException Share could not be updated because the requested changes are invalid
  1104. * @throws OCSForbiddenException Missing permissions to update the share
  1105. * @throws OCSNotFoundException Share not found
  1106. *
  1107. * 200: Share updated successfully
  1108. */
  1109. #[NoAdminRequired]
  1110. public function updateShare(
  1111. string $id,
  1112. ?int $permissions = null,
  1113. ?string $password = null,
  1114. ?string $sendPasswordByTalk = null,
  1115. ?string $publicUpload = null,
  1116. ?string $expireDate = null,
  1117. ?string $note = null,
  1118. ?string $label = null,
  1119. ?string $hideDownload = null,
  1120. ?string $attributes = null,
  1121. ?string $sendMail = null,
  1122. ?string $token = null,
  1123. ): DataResponse {
  1124. try {
  1125. $share = $this->getShareById($id);
  1126. } catch (ShareNotFound $e) {
  1127. throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
  1128. }
  1129. $this->lock($share->getNode());
  1130. if (!$this->canAccessShare($share, false)) {
  1131. throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
  1132. }
  1133. if (!$this->canEditShare($share)) {
  1134. throw new OCSForbiddenException($this->l->t('You are not allowed to edit incoming shares'));
  1135. }
  1136. if (
  1137. $permissions === null
  1138. && $password === null
  1139. && $sendPasswordByTalk === null
  1140. && $publicUpload === null
  1141. && $expireDate === null
  1142. && $note === null
  1143. && $label === null
  1144. && $hideDownload === null
  1145. && $attributes === null
  1146. && $sendMail === null
  1147. && $token === null
  1148. ) {
  1149. throw new OCSBadRequestException($this->l->t('Wrong or no update parameter given'));
  1150. }
  1151. if ($note !== null) {
  1152. $share->setNote($note);
  1153. }
  1154. if ($attributes !== null) {
  1155. $share = $this->setShareAttributes($share, $attributes);
  1156. }
  1157. // Handle mail send
  1158. if ($sendMail === 'true' || $sendMail === 'false') {
  1159. $share->setMailSend($sendMail === 'true');
  1160. }
  1161. /**
  1162. * expiration date, password and publicUpload only make sense for link shares
  1163. */
  1164. if ($share->getShareType() === IShare::TYPE_LINK
  1165. || $share->getShareType() === IShare::TYPE_EMAIL) {
  1166. // Update hide download state
  1167. if ($hideDownload === 'true') {
  1168. $share->setHideDownload(true);
  1169. } elseif ($hideDownload === 'false') {
  1170. $share->setHideDownload(false);
  1171. }
  1172. // If either manual permissions are specified or publicUpload
  1173. // then we need to also update the permissions of the share
  1174. if ($permissions !== null || $publicUpload !== null) {
  1175. $hasPublicUpload = $this->getLegacyPublicUpload($publicUpload);
  1176. $permissions = $this->getLinkSharePermissions($permissions ?? Constants::PERMISSION_READ, $hasPublicUpload);
  1177. $this->validateLinkSharePermissions($share->getNode(), $permissions, $hasPublicUpload);
  1178. $share->setPermissions($permissions);
  1179. }
  1180. if ($password === '') {
  1181. $share->setPassword(null);
  1182. } elseif ($password !== null) {
  1183. $share->setPassword($password);
  1184. }
  1185. if ($label !== null) {
  1186. if (strlen($label) > 255) {
  1187. throw new OCSBadRequestException('Maximum label length is 255');
  1188. }
  1189. $share->setLabel($label);
  1190. }
  1191. if ($sendPasswordByTalk === 'true') {
  1192. if (!$this->appManager->isEnabledForUser('spreed')) {
  1193. throw new OCSForbiddenException($this->l->t('"Sending the password by Nextcloud Talk" for sharing a file or folder failed because Nextcloud Talk is not enabled.'));
  1194. }
  1195. $share->setSendPasswordByTalk(true);
  1196. } elseif ($sendPasswordByTalk !== null) {
  1197. $share->setSendPasswordByTalk(false);
  1198. }
  1199. if ($token !== null) {
  1200. if (!$this->shareManager->allowCustomTokens()) {
  1201. throw new OCSForbiddenException($this->l->t('Custom share link tokens have been disabled by the administrator'));
  1202. }
  1203. if (!$this->validateToken($token)) {
  1204. throw new OCSBadRequestException($this->l->t('Tokens must contain at least 1 character and may only contain letters, numbers, or a hyphen'));
  1205. }
  1206. $share->setToken($token);
  1207. }
  1208. }
  1209. // NOT A LINK SHARE
  1210. else {
  1211. if ($permissions !== null) {
  1212. $share->setPermissions($permissions);
  1213. }
  1214. }
  1215. if ($expireDate === '') {
  1216. $share->setExpirationDate(null);
  1217. } elseif ($expireDate !== null) {
  1218. try {
  1219. $expireDateTime = $this->parseDate($expireDate);
  1220. $share->setExpirationDate($expireDateTime);
  1221. } catch (\Exception $e) {
  1222. throw new OCSBadRequestException($e->getMessage(), $e);
  1223. }
  1224. }
  1225. try {
  1226. $this->checkInheritedAttributes($share);
  1227. $share = $this->shareManager->updateShare($share);
  1228. } catch (HintException $e) {
  1229. $code = $e->getCode() === 0 ? 403 : $e->getCode();
  1230. throw new OCSException($e->getHint(), (int)$code);
  1231. } catch (\Exception $e) {
  1232. $this->logger->error($e->getMessage(), ['exception' => $e]);
  1233. throw new OCSBadRequestException('Failed to update share.', $e);
  1234. }
  1235. return new DataResponse($this->formatShare($share));
  1236. }
  1237. private function validateToken(string $token): bool {
  1238. if (mb_strlen($token) === 0) {
  1239. return false;
  1240. }
  1241. if (!preg_match('/^[a-z0-9-]+$/i', $token)) {
  1242. return false;
  1243. }
  1244. return true;
  1245. }
  1246. /**
  1247. * Get all shares that are still pending
  1248. *
  1249. * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}>
  1250. *
  1251. * 200: Pending shares returned
  1252. */
  1253. #[NoAdminRequired]
  1254. public function pendingShares(): DataResponse {
  1255. $pendingShares = [];
  1256. $shareTypes = [
  1257. IShare::TYPE_USER,
  1258. IShare::TYPE_GROUP
  1259. ];
  1260. foreach ($shareTypes as $shareType) {
  1261. $shares = $this->shareManager->getSharedWith($this->userId, $shareType, null, -1, 0);
  1262. foreach ($shares as $share) {
  1263. if ($share->getStatus() === IShare::STATUS_PENDING || $share->getStatus() === IShare::STATUS_REJECTED) {
  1264. $pendingShares[] = $share;
  1265. }
  1266. }
  1267. }
  1268. $result = array_values(array_filter(array_map(function (IShare $share) {
  1269. $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
  1270. $node = $userFolder->getFirstNodeById($share->getNodeId());
  1271. if (!$node) {
  1272. // fallback to guessing the path
  1273. $node = $userFolder->get($share->getTarget());
  1274. if ($node === null || $share->getTarget() === '') {
  1275. return null;
  1276. }
  1277. }
  1278. try {
  1279. $formattedShare = $this->formatShare($share, $node);
  1280. $formattedShare['path'] = '/' . $share->getNode()->getName();
  1281. $formattedShare['permissions'] = 0;
  1282. return $formattedShare;
  1283. } catch (NotFoundException $e) {
  1284. return null;
  1285. }
  1286. }, $pendingShares), function ($entry) {
  1287. return $entry !== null;
  1288. }));
  1289. return new DataResponse($result);
  1290. }
  1291. /**
  1292. * Accept a share
  1293. *
  1294. * @param string $id ID of the share
  1295. * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
  1296. * @throws OCSNotFoundException Share not found
  1297. * @throws OCSException
  1298. * @throws OCSBadRequestException Share could not be accepted
  1299. *
  1300. * 200: Share accepted successfully
  1301. */
  1302. #[NoAdminRequired]
  1303. public function acceptShare(string $id): DataResponse {
  1304. try {
  1305. $share = $this->getShareById($id);
  1306. } catch (ShareNotFound $e) {
  1307. throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
  1308. }
  1309. if (!$this->canAccessShare($share)) {
  1310. throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
  1311. }
  1312. try {
  1313. $this->shareManager->acceptShare($share, $this->userId);
  1314. } catch (HintException $e) {
  1315. $code = $e->getCode() === 0 ? 403 : $e->getCode();
  1316. throw new OCSException($e->getHint(), (int)$code);
  1317. } catch (\Exception $e) {
  1318. $this->logger->error($e->getMessage(), ['exception' => $e]);
  1319. throw new OCSBadRequestException('Failed to accept share.', $e);
  1320. }
  1321. return new DataResponse();
  1322. }
  1323. /**
  1324. * Does the user have read permission on the share
  1325. *
  1326. * @param IShare $share the share to check
  1327. * @param boolean $checkGroups check groups as well?
  1328. * @return boolean
  1329. * @throws NotFoundException
  1330. *
  1331. * @suppress PhanUndeclaredClassMethod
  1332. */
  1333. protected function canAccessShare(IShare $share, bool $checkGroups = true): bool {
  1334. // A file with permissions 0 can't be accessed by us. So Don't show it
  1335. if ($share->getPermissions() === 0) {
  1336. return false;
  1337. }
  1338. // Owner of the file and the sharer of the file can always get share
  1339. if ($share->getShareOwner() === $this->userId
  1340. || $share->getSharedBy() === $this->userId) {
  1341. return true;
  1342. }
  1343. // If the share is shared with you, you can access it!
  1344. if ($share->getShareType() === IShare::TYPE_USER
  1345. && $share->getSharedWith() === $this->userId) {
  1346. return true;
  1347. }
  1348. // Have reshare rights on the shared file/folder ?
  1349. // Does the currentUser have access to the shared file?
  1350. $userFolder = $this->rootFolder->getUserFolder($this->userId);
  1351. $file = $userFolder->getFirstNodeById($share->getNodeId());
  1352. if ($file && $this->shareProviderResharingRights($this->userId, $share, $file)) {
  1353. return true;
  1354. }
  1355. // If in the recipient group, you can see the share
  1356. if ($checkGroups && $share->getShareType() === IShare::TYPE_GROUP) {
  1357. $sharedWith = $this->groupManager->get($share->getSharedWith());
  1358. $user = $this->userManager->get($this->userId);
  1359. if ($user !== null && $sharedWith !== null && $sharedWith->inGroup($user)) {
  1360. return true;
  1361. }
  1362. }
  1363. if ($share->getShareType() === IShare::TYPE_CIRCLE) {
  1364. // TODO: have a sanity check like above?
  1365. return true;
  1366. }
  1367. if ($share->getShareType() === IShare::TYPE_ROOM) {
  1368. try {
  1369. return $this->getRoomShareHelper()->canAccessShare($share, $this->userId);
  1370. } catch (ContainerExceptionInterface $e) {
  1371. return false;
  1372. }
  1373. }
  1374. if ($share->getShareType() === IShare::TYPE_DECK) {
  1375. try {
  1376. return $this->getDeckShareHelper()->canAccessShare($share, $this->userId);
  1377. } catch (ContainerExceptionInterface $e) {
  1378. return false;
  1379. }
  1380. }
  1381. if ($share->getShareType() === IShare::TYPE_SCIENCEMESH) {
  1382. try {
  1383. return $this->getSciencemeshShareHelper()->canAccessShare($share, $this->userId);
  1384. } catch (ContainerExceptionInterface $e) {
  1385. return false;
  1386. }
  1387. }
  1388. return false;
  1389. }
  1390. /**
  1391. * Does the user have edit permission on the share
  1392. *
  1393. * @param IShare $share the share to check
  1394. * @return boolean
  1395. */
  1396. protected function canEditShare(IShare $share): bool {
  1397. // A file with permissions 0 can't be accessed by us. So Don't show it
  1398. if ($share->getPermissions() === 0) {
  1399. return false;
  1400. }
  1401. // The owner of the file and the creator of the share
  1402. // can always edit the share
  1403. if ($share->getShareOwner() === $this->userId
  1404. || $share->getSharedBy() === $this->userId
  1405. ) {
  1406. return true;
  1407. }
  1408. $userFolder = $this->rootFolder->getUserFolder($this->userId);
  1409. $file = $userFolder->getFirstNodeById($share->getNodeId());
  1410. if ($file?->getMountPoint() instanceof IShareOwnerlessMount && $this->shareProviderResharingRights($this->userId, $share, $file)) {
  1411. return true;
  1412. }
  1413. //! we do NOT support some kind of `admin` in groups.
  1414. //! You cannot edit shares shared to a group you're
  1415. //! a member of if you're not the share owner or the file owner!
  1416. return false;
  1417. }
  1418. /**
  1419. * Does the user have delete permission on the share
  1420. *
  1421. * @param IShare $share the share to check
  1422. * @return boolean
  1423. */
  1424. protected function canDeleteShare(IShare $share): bool {
  1425. // A file with permissions 0 can't be accessed by us. So Don't show it
  1426. if ($share->getPermissions() === 0) {
  1427. return false;
  1428. }
  1429. // if the user is the recipient, i can unshare
  1430. // the share with self
  1431. if ($share->getShareType() === IShare::TYPE_USER
  1432. && $share->getSharedWith() === $this->userId
  1433. ) {
  1434. return true;
  1435. }
  1436. // The owner of the file and the creator of the share
  1437. // can always delete the share
  1438. if ($share->getShareOwner() === $this->userId
  1439. || $share->getSharedBy() === $this->userId
  1440. ) {
  1441. return true;
  1442. }
  1443. $userFolder = $this->rootFolder->getUserFolder($this->userId);
  1444. $file = $userFolder->getFirstNodeById($share->getNodeId());
  1445. if ($file?->getMountPoint() instanceof IShareOwnerlessMount && $this->shareProviderResharingRights($this->userId, $share, $file)) {
  1446. return true;
  1447. }
  1448. return false;
  1449. }
  1450. /**
  1451. * Does the user have delete permission on the share
  1452. * This differs from the canDeleteShare function as it only
  1453. * remove the share for the current user. It does NOT
  1454. * completely delete the share but only the mount point.
  1455. * It can then be restored from the deleted shares section.
  1456. *
  1457. * @param IShare $share the share to check
  1458. * @return boolean
  1459. *
  1460. * @suppress PhanUndeclaredClassMethod
  1461. */
  1462. protected function canDeleteShareFromSelf(IShare $share): bool {
  1463. if ($share->getShareType() !== IShare::TYPE_GROUP
  1464. && $share->getShareType() !== IShare::TYPE_ROOM
  1465. && $share->getShareType() !== IShare::TYPE_DECK
  1466. && $share->getShareType() !== IShare::TYPE_SCIENCEMESH
  1467. ) {
  1468. return false;
  1469. }
  1470. if ($share->getShareOwner() === $this->userId
  1471. || $share->getSharedBy() === $this->userId
  1472. ) {
  1473. // Delete the whole share, not just for self
  1474. return false;
  1475. }
  1476. // If in the recipient group, you can delete the share from self
  1477. if ($share->getShareType() === IShare::TYPE_GROUP) {
  1478. $sharedWith = $this->groupManager->get($share->getSharedWith());
  1479. $user = $this->userManager->get($this->userId);
  1480. if ($user !== null && $sharedWith !== null && $sharedWith->inGroup($user)) {
  1481. return true;
  1482. }
  1483. }
  1484. if ($share->getShareType() === IShare::TYPE_ROOM) {
  1485. try {
  1486. return $this->getRoomShareHelper()->canAccessShare($share, $this->userId);
  1487. } catch (ContainerExceptionInterface $e) {
  1488. return false;
  1489. }
  1490. }
  1491. if ($share->getShareType() === IShare::TYPE_DECK) {
  1492. try {
  1493. return $this->getDeckShareHelper()->canAccessShare($share, $this->userId);
  1494. } catch (ContainerExceptionInterface $e) {
  1495. return false;
  1496. }
  1497. }
  1498. if ($share->getShareType() === IShare::TYPE_SCIENCEMESH) {
  1499. try {
  1500. return $this->getSciencemeshShareHelper()->canAccessShare($share, $this->userId);
  1501. } catch (ContainerExceptionInterface $e) {
  1502. return false;
  1503. }
  1504. }
  1505. return false;
  1506. }
  1507. /**
  1508. * Make sure that the passed date is valid ISO 8601
  1509. * So YYYY-MM-DD
  1510. * If not throw an exception
  1511. *
  1512. * @param string $expireDate
  1513. *
  1514. * @throws \Exception
  1515. * @return \DateTime
  1516. */
  1517. private function parseDate(string $expireDate): \DateTime {
  1518. try {
  1519. $date = new \DateTime(trim($expireDate, '"'), $this->dateTimeZone->getTimeZone());
  1520. // Make sure it expires at midnight in owner timezone
  1521. $date->setTime(0, 0, 0);
  1522. } catch (\Exception $e) {
  1523. throw new \Exception($this->l->t('Invalid date. Format must be YYYY-MM-DD'));
  1524. }
  1525. return $date;
  1526. }
  1527. /**
  1528. * Since we have multiple providers but the OCS Share API v1 does
  1529. * not support this we need to check all backends.
  1530. *
  1531. * @param string $id
  1532. * @return IShare
  1533. * @throws ShareNotFound
  1534. */
  1535. private function getShareById(string $id): IShare {
  1536. $providers = [
  1537. 'ocinternal' => null, // No type check needed
  1538. 'ocCircleShare' => IShare::TYPE_CIRCLE,
  1539. 'ocMailShare' => IShare::TYPE_EMAIL,
  1540. 'ocRoomShare' => null,
  1541. 'deck' => IShare::TYPE_DECK,
  1542. 'sciencemesh' => IShare::TYPE_SCIENCEMESH,
  1543. ];
  1544. // Add federated sharing as a provider only if it's allowed
  1545. if ($this->shareManager->outgoingServer2ServerSharesAllowed()) {
  1546. $providers['ocFederatedSharing'] = null; // No type check needed
  1547. }
  1548. foreach ($providers as $prefix => $type) {
  1549. try {
  1550. if ($type === null || $this->shareManager->shareProviderExists($type)) {
  1551. return $this->shareManager->getShareById($prefix . ':' . $id, $this->userId);
  1552. }
  1553. } catch (ShareNotFound $e) {
  1554. // Do nothing, continue to next provider
  1555. } catch (\Exception $e) {
  1556. $this->logger->warning('Unexpected error in share provider', [
  1557. 'shareId' => $id,
  1558. 'provider' => $prefix,
  1559. 'exception' => $e,
  1560. ]);
  1561. }
  1562. }
  1563. throw new ShareNotFound();
  1564. }
  1565. /**
  1566. * Lock a Node
  1567. *
  1568. * @param Node $node
  1569. * @throws LockedException
  1570. */
  1571. private function lock(Node $node) {
  1572. $node->lock(ILockingProvider::LOCK_SHARED);
  1573. $this->lockedNode = $node;
  1574. }
  1575. /**
  1576. * Cleanup the remaining locks
  1577. * @throws LockedException
  1578. */
  1579. public function cleanup() {
  1580. if ($this->lockedNode !== null) {
  1581. $this->lockedNode->unlock(ILockingProvider::LOCK_SHARED);
  1582. }
  1583. }
  1584. /**
  1585. * Returns the helper of ShareAPIController for room shares.
  1586. *
  1587. * If the Talk application is not enabled or the helper is not available
  1588. * a ContainerExceptionInterface is thrown instead.
  1589. *
  1590. * @return \OCA\Talk\Share\Helper\ShareAPIController
  1591. * @throws ContainerExceptionInterface
  1592. */
  1593. private function getRoomShareHelper() {
  1594. if (!$this->appManager->isEnabledForUser('spreed')) {
  1595. throw new QueryException();
  1596. }
  1597. return $this->serverContainer->get('\OCA\Talk\Share\Helper\ShareAPIController');
  1598. }
  1599. /**
  1600. * Returns the helper of ShareAPIHelper for deck shares.
  1601. *
  1602. * If the Deck application is not enabled or the helper is not available
  1603. * a ContainerExceptionInterface is thrown instead.
  1604. *
  1605. * @return ShareAPIHelper
  1606. * @throws ContainerExceptionInterface
  1607. */
  1608. private function getDeckShareHelper() {
  1609. if (!$this->appManager->isEnabledForUser('deck')) {
  1610. throw new QueryException();
  1611. }
  1612. return $this->serverContainer->get('\OCA\Deck\Sharing\ShareAPIHelper');
  1613. }
  1614. /**
  1615. * Returns the helper of ShareAPIHelper for sciencemesh shares.
  1616. *
  1617. * If the sciencemesh application is not enabled or the helper is not available
  1618. * a ContainerExceptionInterface is thrown instead.
  1619. *
  1620. * @return ShareAPIHelper
  1621. * @throws ContainerExceptionInterface
  1622. */
  1623. private function getSciencemeshShareHelper() {
  1624. if (!$this->appManager->isEnabledForUser('sciencemesh')) {
  1625. throw new QueryException();
  1626. }
  1627. return $this->serverContainer->get('\OCA\ScienceMesh\Sharing\ShareAPIHelper');
  1628. }
  1629. /**
  1630. * @param string $viewer
  1631. * @param Node $node
  1632. * @param bool $reShares
  1633. *
  1634. * @return IShare[]
  1635. */
  1636. private function getSharesFromNode(string $viewer, $node, bool $reShares): array {
  1637. $providers = [
  1638. IShare::TYPE_USER,
  1639. IShare::TYPE_GROUP,
  1640. IShare::TYPE_LINK,
  1641. IShare::TYPE_EMAIL,
  1642. IShare::TYPE_CIRCLE,
  1643. IShare::TYPE_ROOM,
  1644. IShare::TYPE_DECK,
  1645. IShare::TYPE_SCIENCEMESH
  1646. ];
  1647. // Should we assume that the (currentUser) viewer is the owner of the node !?
  1648. $shares = [];
  1649. foreach ($providers as $provider) {
  1650. if (!$this->shareManager->shareProviderExists($provider)) {
  1651. continue;
  1652. }
  1653. $providerShares
  1654. = $this->shareManager->getSharesBy($viewer, $provider, $node, $reShares, -1, 0);
  1655. $shares = array_merge($shares, $providerShares);
  1656. }
  1657. if ($this->shareManager->outgoingServer2ServerSharesAllowed()) {
  1658. $federatedShares = $this->shareManager->getSharesBy(
  1659. $this->userId, IShare::TYPE_REMOTE, $node, $reShares, -1, 0
  1660. );
  1661. $shares = array_merge($shares, $federatedShares);
  1662. }
  1663. if ($this->shareManager->outgoingServer2ServerGroupSharesAllowed()) {
  1664. $federatedShares = $this->shareManager->getSharesBy(
  1665. $this->userId, IShare::TYPE_REMOTE_GROUP, $node, $reShares, -1, 0
  1666. );
  1667. $shares = array_merge($shares, $federatedShares);
  1668. }
  1669. return $shares;
  1670. }
  1671. /**
  1672. * @param Node $node
  1673. *
  1674. * @throws SharingRightsException
  1675. */
  1676. private function confirmSharingRights(Node $node): void {
  1677. if (!$this->hasResharingRights($this->userId, $node)) {
  1678. throw new SharingRightsException($this->l->t('No sharing rights on this item'));
  1679. }
  1680. }
  1681. /**
  1682. * @param string $viewer
  1683. * @param Node $node
  1684. *
  1685. * @return bool
  1686. */
  1687. private function hasResharingRights($viewer, $node): bool {
  1688. if ($viewer === $node->getOwner()->getUID()) {
  1689. return true;
  1690. }
  1691. foreach ([$node, $node->getParent()] as $node) {
  1692. $shares = $this->getSharesFromNode($viewer, $node, true);
  1693. foreach ($shares as $share) {
  1694. try {
  1695. if ($this->shareProviderResharingRights($viewer, $share, $node)) {
  1696. return true;
  1697. }
  1698. } catch (InvalidPathException|NotFoundException $e) {
  1699. }
  1700. }
  1701. }
  1702. return false;
  1703. }
  1704. /**
  1705. * Returns if we can find resharing rights in an IShare object for a specific user.
  1706. *
  1707. * @suppress PhanUndeclaredClassMethod
  1708. *
  1709. * @param string $userId
  1710. * @param IShare $share
  1711. * @param Node $node
  1712. *
  1713. * @return bool
  1714. * @throws NotFoundException
  1715. * @throws InvalidPathException
  1716. */
  1717. private function shareProviderResharingRights(string $userId, IShare $share, $node): bool {
  1718. if ($share->getShareOwner() === $userId) {
  1719. return true;
  1720. }
  1721. // we check that current user have parent resharing rights on the current file
  1722. if ($node !== null && ($node->getPermissions() & Constants::PERMISSION_SHARE) !== 0) {
  1723. return true;
  1724. }
  1725. if ((Constants::PERMISSION_SHARE & $share->getPermissions()) === 0) {
  1726. return false;
  1727. }
  1728. if ($share->getShareType() === IShare::TYPE_USER && $share->getSharedWith() === $userId) {
  1729. return true;
  1730. }
  1731. if ($share->getShareType() === IShare::TYPE_GROUP && $this->groupManager->isInGroup($userId, $share->getSharedWith())) {
  1732. return true;
  1733. }
  1734. if ($share->getShareType() === IShare::TYPE_CIRCLE && Server::get(IAppManager::class)->isEnabledForUser('circles')
  1735. && class_exists('\OCA\Circles\Api\v1\Circles')) {
  1736. $hasCircleId = (str_ends_with($share->getSharedWith(), ']'));
  1737. $shareWithStart = ($hasCircleId ? strrpos($share->getSharedWith(), '[') + 1 : 0);
  1738. $shareWithLength = ($hasCircleId ? -1 : strpos($share->getSharedWith(), ' '));
  1739. if ($shareWithLength === false) {
  1740. $sharedWith = substr($share->getSharedWith(), $shareWithStart);
  1741. } else {
  1742. $sharedWith = substr($share->getSharedWith(), $shareWithStart, $shareWithLength);
  1743. }
  1744. try {
  1745. $member = Circles::getMember($sharedWith, $userId, 1);
  1746. if ($member->getLevel() >= 4) {
  1747. return true;
  1748. }
  1749. return false;
  1750. } catch (ContainerExceptionInterface $e) {
  1751. return false;
  1752. }
  1753. }
  1754. return false;
  1755. }
  1756. /**
  1757. * Get all the shares for the current user
  1758. *
  1759. * @param Node|null $path
  1760. * @param boolean $reshares
  1761. * @return IShare[]
  1762. */
  1763. private function getAllShares(?Node $path = null, bool $reshares = false) {
  1764. // Get all shares
  1765. $userShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_USER, $path, $reshares, -1, 0);
  1766. $groupShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_GROUP, $path, $reshares, -1, 0);
  1767. $linkShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_LINK, $path, $reshares, -1, 0);
  1768. // EMAIL SHARES
  1769. $mailShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_EMAIL, $path, $reshares, -1, 0);
  1770. // TEAM SHARES
  1771. $circleShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_CIRCLE, $path, $reshares, -1, 0);
  1772. // TALK SHARES
  1773. $roomShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_ROOM, $path, $reshares, -1, 0);
  1774. // DECK SHARES
  1775. $deckShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_DECK, $path, $reshares, -1, 0);
  1776. // SCIENCEMESH SHARES
  1777. $sciencemeshShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_SCIENCEMESH, $path, $reshares, -1, 0);
  1778. // FEDERATION
  1779. if ($this->shareManager->outgoingServer2ServerSharesAllowed()) {
  1780. $federatedShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_REMOTE, $path, $reshares, -1, 0);
  1781. } else {
  1782. $federatedShares = [];
  1783. }
  1784. if ($this->shareManager->outgoingServer2ServerGroupSharesAllowed()) {
  1785. $federatedGroupShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_REMOTE_GROUP, $path, $reshares, -1, 0);
  1786. } else {
  1787. $federatedGroupShares = [];
  1788. }
  1789. return array_merge($userShares, $groupShares, $linkShares, $mailShares, $circleShares, $roomShares, $deckShares, $sciencemeshShares, $federatedShares, $federatedGroupShares);
  1790. }
  1791. /**
  1792. * merging already formatted shares.
  1793. * We'll make an associative array to easily detect duplicate Ids.
  1794. * Keys _needs_ to be removed after all shares are retrieved and merged.
  1795. *
  1796. * @param array $shares
  1797. * @param array $newShares
  1798. */
  1799. private function mergeFormattedShares(array &$shares, array $newShares) {
  1800. foreach ($newShares as $newShare) {
  1801. if (!array_key_exists($newShare['id'], $shares)) {
  1802. $shares[$newShare['id']] = $newShare;
  1803. }
  1804. }
  1805. }
  1806. /**
  1807. * @param IShare $share
  1808. * @param string|null $attributesString
  1809. * @return IShare modified share
  1810. */
  1811. private function setShareAttributes(IShare $share, ?string $attributesString) {
  1812. $newShareAttributes = null;
  1813. if ($attributesString !== null) {
  1814. $newShareAttributes = $this->shareManager->newShare()->newAttributes();
  1815. $formattedShareAttributes = \json_decode($attributesString, true);
  1816. if (is_array($formattedShareAttributes)) {
  1817. foreach ($formattedShareAttributes as $formattedAttr) {
  1818. $newShareAttributes->setAttribute(
  1819. $formattedAttr['scope'],
  1820. $formattedAttr['key'],
  1821. $formattedAttr['value'],
  1822. );
  1823. }
  1824. } else {
  1825. throw new OCSBadRequestException($this->l->t('Invalid share attributes provided: "%s"', [$attributesString]));
  1826. }
  1827. }
  1828. $share->setAttributes($newShareAttributes);
  1829. return $share;
  1830. }
  1831. private function checkInheritedAttributes(IShare $share): void {
  1832. if (!$share->getSharedBy()) {
  1833. return; // Probably in a test
  1834. }
  1835. $canDownload = false;
  1836. $hideDownload = true;
  1837. $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
  1838. $nodes = $userFolder->getById($share->getNodeId());
  1839. foreach ($nodes as $node) {
  1840. // Owner always can download it - so allow it and break
  1841. if ($node->getOwner()?->getUID() === $share->getSharedBy()) {
  1842. $canDownload = true;
  1843. $hideDownload = false;
  1844. break;
  1845. }
  1846. if ($node->getStorage()->instanceOfStorage(SharedStorage::class)) {
  1847. $storage = $node->getStorage();
  1848. if ($storage instanceof Wrapper) {
  1849. $storage = $storage->getInstanceOfStorage(SharedStorage::class);
  1850. if ($storage === null) {
  1851. throw new \RuntimeException('Should not happen, instanceOfStorage but getInstanceOfStorage return null');
  1852. }
  1853. } else {
  1854. throw new \RuntimeException('Should not happen, instanceOfStorage but not a wrapper');
  1855. }
  1856. /** @var SharedStorage $storage */
  1857. $originalShare = $storage->getShare();
  1858. $inheritedAttributes = $originalShare->getAttributes();
  1859. // hide if hidden and also the current share enforces hide (can only be false if one share is false or user is owner)
  1860. $hideDownload = $hideDownload && $originalShare->getHideDownload();
  1861. // allow download if already allowed by previous share or when the current share allows downloading
  1862. $canDownload = $canDownload || $inheritedAttributes === null || $inheritedAttributes->getAttribute('permissions', 'download') !== false;
  1863. } elseif ($node->getStorage()->instanceOfStorage(Storage::class)) {
  1864. $canDownload = true; // in case of federation storage, we can expect the download to be activated by default
  1865. }
  1866. }
  1867. if ($hideDownload || !$canDownload) {
  1868. $share->setHideDownload(true);
  1869. if (!$canDownload) {
  1870. $attributes = $share->getAttributes() ?? $share->newAttributes();
  1871. $attributes->setAttribute('permissions', 'download', false);
  1872. $share->setAttributes($attributes);
  1873. }
  1874. }
  1875. }
  1876. /**
  1877. * Send a mail notification again for a share.
  1878. * The mail_send option must be enabled for the given share.
  1879. * @param string $id the share ID
  1880. * @param string $password the password to check against. Necessary for password protected shares.
  1881. * @throws OCSNotFoundException Share not found
  1882. * @throws OCSForbiddenException You are not allowed to send mail notifications
  1883. * @throws OCSBadRequestException Invalid request or wrong password
  1884. * @throws OCSException Error while sending mail notification
  1885. * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
  1886. *
  1887. * 200: The email notification was sent successfully
  1888. */
  1889. #[NoAdminRequired]
  1890. #[UserRateLimit(limit: 10, period: 600)]
  1891. public function sendShareEmail(string $id, $password = ''): DataResponse {
  1892. try {
  1893. $share = $this->getShareById($id);
  1894. if (!$this->canAccessShare($share, false)) {
  1895. throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
  1896. }
  1897. if (!$this->canEditShare($share)) {
  1898. throw new OCSForbiddenException($this->l->t('You are not allowed to send mail notifications'));
  1899. }
  1900. // For mail and link shares, the user must be
  1901. // the owner of the share, not only the file owner.
  1902. if ($share->getShareType() === IShare::TYPE_EMAIL
  1903. || $share->getShareType() === IShare::TYPE_LINK) {
  1904. if ($share->getSharedBy() !== $this->userId) {
  1905. throw new OCSForbiddenException($this->l->t('You are not allowed to send mail notifications'));
  1906. }
  1907. }
  1908. try {
  1909. $provider = $this->factory->getProviderForType($share->getShareType());
  1910. if (!($provider instanceof IShareProviderWithNotification)) {
  1911. throw new OCSBadRequestException($this->l->t('No mail notification configured for this share type'));
  1912. }
  1913. // Circumvent the password encrypted data by
  1914. // setting the password clear. We're not storing
  1915. // the password clear, it is just a temporary
  1916. // object manipulation. The password will stay
  1917. // encrypted in the database.
  1918. if ($share->getPassword() !== null && $share->getPassword() !== $password) {
  1919. if (!$this->shareManager->checkPassword($share, $password)) {
  1920. throw new OCSBadRequestException($this->l->t('Wrong password'));
  1921. }
  1922. $share = $share->setPassword($password);
  1923. }
  1924. $provider->sendMailNotification($share);
  1925. return new DataResponse();
  1926. } catch (Exception $e) {
  1927. $this->logger->error($e->getMessage(), ['exception' => $e]);
  1928. throw new OCSException($this->l->t('Error while sending mail notification'));
  1929. }
  1930. } catch (ShareNotFound $e) {
  1931. throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
  1932. }
  1933. }
  1934. /**
  1935. * Get a unique share token
  1936. *
  1937. * @throws OCSException Failed to generate a unique token
  1938. *
  1939. * @return DataResponse<Http::STATUS_OK, array{token: string}, array{}>
  1940. *
  1941. * 200: Token generated successfully
  1942. */
  1943. #[ApiRoute(verb: 'GET', url: '/api/v1/token')]
  1944. #[NoAdminRequired]
  1945. public function generateToken(): DataResponse {
  1946. try {
  1947. $token = $this->shareManager->generateToken();
  1948. return new DataResponse([
  1949. 'token' => $token,
  1950. ]);
  1951. } catch (ShareTokenException $e) {
  1952. throw new OCSException($this->l->t('Failed to generate a unique token'));
  1953. }
  1954. }
  1955. /**
  1956. * Populate the result set with file tags
  1957. *
  1958. * @psalm-template T of array{tags?: list<string>, file_source: int, ...array<string, mixed>}
  1959. * @param list<T> $fileList
  1960. * @return list<T> file list populated with tags
  1961. */
  1962. private function populateTags(array $fileList): array {
  1963. $tagger = $this->tagManager->load('files');
  1964. $tags = $tagger->getTagsForObjects(array_map(static fn (array $fileData) => $fileData['file_source'], $fileList));
  1965. if (!is_array($tags)) {
  1966. throw new \UnexpectedValueException('$tags must be an array');
  1967. }
  1968. // Set empty tag array
  1969. foreach ($fileList as &$fileData) {
  1970. $fileData['tags'] = [];
  1971. }
  1972. unset($fileData);
  1973. if (!empty($tags)) {
  1974. foreach ($tags as $fileId => $fileTags) {
  1975. foreach ($fileList as &$fileData) {
  1976. if ($fileId !== $fileData['file_source']) {
  1977. continue;
  1978. }
  1979. $fileData['tags'] = $fileTags;
  1980. }
  1981. unset($fileData);
  1982. }
  1983. }
  1984. return $fileList;
  1985. }
  1986. }