diff options
Diffstat (limited to 'apps/files_sharing/lib/Controller/ShareAPIController.php')
-rw-r--r-- | apps/files_sharing/lib/Controller/ShareAPIController.php | 268 |
1 files changed, 181 insertions, 87 deletions
diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index 1fb62480049..095a8a75963 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -10,8 +10,12 @@ declare(strict_types=1); namespace OCA\Files_Sharing\Controller; use Exception; +use OC\Core\AppInfo\ConfigLexicon; +use OC\Files\FileInfo; use OC\Files\Storage\Wrapper\Wrapper; use OCA\Circles\Api\v1\Circles; +use OCA\Deck\Sharing\ShareAPIHelper; +use OCA\Federation\TrustedServers; use OCA\Files\Helper; use OCA\Files_Sharing\Exceptions\SharingRightsException; use OCA\Files_Sharing\External\Storage; @@ -39,18 +43,21 @@ use OCP\Files\Mount\IShareOwnerlessMount; use OCP\Files\Node; use OCP\Files\NotFoundException; use OCP\HintException; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IDateTimeZone; use OCP\IGroupManager; use OCP\IL10N; use OCP\IPreview; use OCP\IRequest; +use OCP\ITagManager; use OCP\IURLGenerator; use OCP\IUserManager; use OCP\Lock\ILockingProvider; use OCP\Lock\LockedException; use OCP\Mail\IMailer; use OCP\Server; +use OCP\Share\Exceptions\GenericShareException; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\Exceptions\ShareTokenException; use OCP\Share\IManager; @@ -70,6 +77,7 @@ use Psr\Log\LoggerInterface; class ShareAPIController extends OCSController { private ?Node $lockedNode = null; + private array $trustedServerCache = []; /** * Share20OCS constructor. @@ -84,6 +92,7 @@ class ShareAPIController extends OCSController { private IURLGenerator $urlGenerator, private IL10N $l, private IConfig $config, + private IAppConfig $appConfig, private IAppManager $appManager, private ContainerInterface $serverContainer, private IUserStatusManager $userStatusManager, @@ -92,6 +101,8 @@ class ShareAPIController extends OCSController { private LoggerInterface $logger, private IProviderFactory $factory, private IMailer $mailer, + private ITagManager $tagManager, + private ?TrustedServers $trustedServers, private ?string $userId = null, ) { parent::__construct($appName, $request); @@ -194,6 +205,32 @@ class ShareAPIController extends OCSController { $result['item_size'] = $node->getSize(); $result['item_mtime'] = $node->getMTime(); + if ($this->trustedServers !== null && in_array($share->getShareType(), [IShare::TYPE_REMOTE, IShare::TYPE_REMOTE_GROUP], true)) { + $result['is_trusted_server'] = false; + $sharedWith = $share->getSharedWith(); + $remoteIdentifier = is_string($sharedWith) ? strrchr($sharedWith, '@') : false; + if ($remoteIdentifier !== false) { + $remote = substr($remoteIdentifier, 1); + + if (isset($this->trustedServerCache[$remote])) { + $result['is_trusted_server'] = $this->trustedServerCache[$remote]; + } else { + try { + $isTrusted = $this->trustedServers->isTrustedServer($remote); + $this->trustedServerCache[$remote] = $isTrusted; + $result['is_trusted_server'] = $isTrusted; + } catch (\Exception $e) { + // Server not found or other issue, we consider it not trusted + $this->trustedServerCache[$remote] = false; + $this->logger->error( + 'Error checking if remote server is trusted (treating as untrusted): ' . $e->getMessage(), + ['exception' => $e] + ); + } + } + } + } + $expiration = $share->getExpirationDate(); if ($expiration !== null) { $expiration->setTimezone($this->dateTimeZone->getTimeZone()); @@ -280,7 +317,7 @@ class ShareAPIController extends OCSController { /** @var array{share_with_displayname: string, share_with_link: string, share_with?: string, token?: string} $roomShare */ $roomShare = $this->getRoomShareHelper()->formatShare($share); $result = array_merge($result, $roomShare); - } catch (QueryException $e) { + } catch (ContainerExceptionInterface $e) { } } elseif ($share->getShareType() === IShare::TYPE_DECK) { $result['share_with'] = $share->getSharedWith(); @@ -290,7 +327,7 @@ class ShareAPIController extends OCSController { /** @var array{share_with: string, share_with_displayname: string, share_with_link: string} $deckShare */ $deckShare = $this->getDeckShareHelper()->formatShare($share); $result = array_merge($result, $deckShare); - } catch (QueryException $e) { + } catch (ContainerExceptionInterface $e) { } } elseif ($share->getShareType() === IShare::TYPE_SCIENCEMESH) { $result['share_with'] = $share->getSharedWith(); @@ -300,7 +337,7 @@ class ShareAPIController extends OCSController { /** @var array{share_with: string, share_with_displayname: string, token: string} $scienceMeshShare */ $scienceMeshShare = $this->getSciencemeshShareHelper()->formatShare($share); $result = array_merge($result, $scienceMeshShare); - } catch (QueryException $e) { + } catch (ContainerExceptionInterface $e) { } } @@ -327,7 +364,7 @@ class ShareAPIController extends OCSController { private function getDisplayNameFromAddressBook(string $query, string $property): string { // FIXME: If we inject the contacts manager it gets initialized before any address books are registered try { - $result = \OC::$server->getContactsManager()->search($query, [$property], [ + $result = Server::get(\OCP\Contacts\IManager::class)->search($query, [$property], [ 'limit' => 1, 'enumeration' => false, 'strict_search' => true, @@ -407,7 +444,7 @@ class ShareAPIController extends OCSController { private function retrieveFederatedDisplayName(array $userIds, bool $cacheOnly = false): array { // check if gss is enabled and available if (count($userIds) === 0 - || !$this->appManager->isInstalled('globalsiteselector') + || !$this->appManager->isEnabledForAnyone('globalsiteselector') || !class_exists('\OCA\GlobalSiteSelector\Service\SlaveService')) { return []; } @@ -470,7 +507,7 @@ class ShareAPIController extends OCSController { $share = $this->formatShare($share); if ($include_tags) { - $share = Helper::populateTags([$share], \OC::$server->getTagManager()); + $share = $this->populateTags([$share]); } else { $share = [$share]; } @@ -556,6 +593,7 @@ class ShareAPIController extends OCSController { * 200: Share created */ #[NoAdminRequired] + #[UserRateLimit(limit: 20, period: 600)] public function createShare( ?string $path = null, ?int $permissions = null, @@ -592,7 +630,7 @@ class ShareAPIController extends OCSController { // combine all permissions to determine if the user can share this file $nodes = $userFolder->getById($node->getId()); foreach ($nodes as $nodeById) { - /** @var \OC\Files\FileInfo $fileInfo */ + /** @var FileInfo $fileInfo */ $fileInfo = $node->getFileInfo(); $fileInfo['permissions'] |= $nodeById->getPermissions(); } @@ -637,7 +675,9 @@ class ShareAPIController extends OCSController { $share = $this->setShareAttributes($share, $attributes); } - // Expire date + // Expire date checks + // Normally, null means no expiration date but we still set the default for backwards compatibility + // If the client sends an empty string, we set noExpirationDate to true if ($expireDate !== null) { if ($expireDate !== '') { try { @@ -654,7 +694,6 @@ class ShareAPIController extends OCSController { } $share->setSharedBy($this->userId); - $this->checkInheritedAttributes($share); // Handle mail send if (is_null($sendMail)) { @@ -752,7 +791,7 @@ class ShareAPIController extends OCSController { $share->setSharedWith($shareWith); $share->setPermissions($permissions); } elseif ($shareType === IShare::TYPE_CIRCLE) { - if (!\OC::$server->getAppManager()->isEnabledForUser('circles') || !class_exists('\OCA\Circles\ShareByCircleProvider')) { + if (!Server::get(IAppManager::class)->isEnabledForUser('circles') || !class_exists('\OCA\Circles\ShareByCircleProvider')) { throw new OCSNotFoundException($this->l->t('You cannot share to a Team if the app is not enabled')); } @@ -767,19 +806,19 @@ class ShareAPIController extends OCSController { } elseif ($shareType === IShare::TYPE_ROOM) { try { $this->getRoomShareHelper()->createShare($share, $shareWith, $permissions, $expireDate ?? ''); - } catch (QueryException $e) { + } catch (ContainerExceptionInterface $e) { throw new OCSForbiddenException($this->l->t('Sharing %s failed because the back end does not support room shares', [$node->getPath()])); } } elseif ($shareType === IShare::TYPE_DECK) { try { $this->getDeckShareHelper()->createShare($share, $shareWith, $permissions, $expireDate ?? ''); - } catch (QueryException $e) { + } catch (ContainerExceptionInterface $e) { throw new OCSForbiddenException($this->l->t('Sharing %s failed because the back end does not support room shares', [$node->getPath()])); } } elseif ($shareType === IShare::TYPE_SCIENCEMESH) { try { $this->getSciencemeshShareHelper()->createShare($share, $shareWith, $permissions, $expireDate ?? ''); - } catch (QueryException $e) { + } catch (ContainerExceptionInterface $e) { throw new OCSForbiddenException($this->l->t('Sharing %s failed because the back end does not support ScienceMesh shares', [$node->getPath()])); } } else { @@ -787,6 +826,7 @@ class ShareAPIController extends OCSController { } $share->setShareType($shareType); + $this->checkInheritedAttributes($share); if ($note !== '') { $share->setNote($note); @@ -797,6 +837,9 @@ class ShareAPIController extends OCSController { } catch (HintException $e) { $code = $e->getCode() === 0 ? 403 : $e->getCode(); throw new OCSException($e->getHint(), $code); + } catch (GenericShareException|\InvalidArgumentException $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new OCSForbiddenException($e->getMessage(), $e); } catch (\Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); throw new OCSForbiddenException('Failed to create share.', $e); @@ -839,7 +882,7 @@ class ShareAPIController extends OCSController { } if ($includeTags) { - $formatted = Helper::populateTags($formatted, \OC::$server->getTagManager()); + $formatted = $this->populateTags($formatted); } return $formatted; @@ -958,9 +1001,9 @@ class ShareAPIController extends OCSController { : Constants::PERMISSION_READ; } - // TODO: It might make sense to have a dedicated setting to allow/deny converting link shares into federated ones if ($this->hasPermission($permissions, Constants::PERMISSION_READ) - && $this->shareManager->outgoingServer2ServerSharesAllowed()) { + && $this->shareManager->outgoingServer2ServerSharesAllowed() + && $this->appConfig->getValueBool('core', ConfigLexicon::SHAREAPI_ALLOW_FEDERATION_ON_PUBLIC_SHARES)) { $permissions |= Constants::PERMISSION_SHARE; } @@ -1092,8 +1135,7 @@ class ShareAPIController extends OCSController { $formatted = $this->fixMissingDisplayName($formatted); if ($includeTags) { - $formatted = - Helper::populateTags($formatted, \OC::$server->getTagManager()); + $formatted = $this->populateTags($formatted); } return $formatted; @@ -1250,17 +1292,17 @@ class ShareAPIController extends OCSController { } if ( - $permissions === null && - $password === null && - $sendPasswordByTalk === null && - $publicUpload === null && - $expireDate === null && - $note === null && - $label === null && - $hideDownload === null && - $attributes === null && - $sendMail === null && - $token === null + $permissions === null + && $password === null + && $sendPasswordByTalk === null + && $publicUpload === null + && $expireDate === null + && $note === null + && $label === null + && $hideDownload === null + && $attributes === null + && $sendMail === null + && $token === null ) { throw new OCSBadRequestException($this->l->t('Wrong or no update parameter given')); } @@ -1272,7 +1314,6 @@ class ShareAPIController extends OCSController { if ($attributes !== null) { $share = $this->setShareAttributes($share, $attributes); } - $this->checkInheritedAttributes($share); // Handle mail send if ($sendMail === 'true' || $sendMail === 'false') { @@ -1286,16 +1327,11 @@ class ShareAPIController extends OCSController { || $share->getShareType() === IShare::TYPE_EMAIL) { // Update hide download state - $attributes = $share->getAttributes() ?? $share->newAttributes(); if ($hideDownload === 'true') { $share->setHideDownload(true); - $attributes->setAttribute('permissions', 'download', false); } elseif ($hideDownload === 'false') { $share->setHideDownload(false); - $attributes->setAttribute('permissions', 'download', true); } - $share->setAttributes($attributes); - // If either manual permissions are specified or publicUpload // then we need to also update the permissions of the share @@ -1359,6 +1395,7 @@ class ShareAPIController extends OCSController { } try { + $this->checkInheritedAttributes($share); $share = $this->shareManager->updateShare($share); } catch (HintException $e) { $code = $e->getCode() === 0 ? 403 : $e->getCode(); @@ -1522,7 +1559,7 @@ class ShareAPIController extends OCSController { if ($share->getShareType() === IShare::TYPE_ROOM) { try { return $this->getRoomShareHelper()->canAccessShare($share, $this->userId); - } catch (QueryException $e) { + } catch (ContainerExceptionInterface $e) { return false; } } @@ -1530,7 +1567,7 @@ class ShareAPIController extends OCSController { if ($share->getShareType() === IShare::TYPE_DECK) { try { return $this->getDeckShareHelper()->canAccessShare($share, $this->userId); - } catch (QueryException $e) { + } catch (ContainerExceptionInterface $e) { return false; } } @@ -1538,7 +1575,7 @@ class ShareAPIController extends OCSController { if ($share->getShareType() === IShare::TYPE_SCIENCEMESH) { try { return $this->getSciencemeshShareHelper()->canAccessShare($share, $this->userId); - } catch (QueryException $e) { + } catch (ContainerExceptionInterface $e) { return false; } } @@ -1560,8 +1597,8 @@ class ShareAPIController extends OCSController { // The owner of the file and the creator of the share // can always edit the share - if ($share->getShareOwner() === $this->userId || - $share->getSharedBy() === $this->userId + if ($share->getShareOwner() === $this->userId + || $share->getSharedBy() === $this->userId ) { return true; } @@ -1593,16 +1630,16 @@ class ShareAPIController extends OCSController { // if the user is the recipient, i can unshare // the share with self - if ($share->getShareType() === IShare::TYPE_USER && - $share->getSharedWith() === $this->userId + if ($share->getShareType() === IShare::TYPE_USER + && $share->getSharedWith() === $this->userId ) { return true; } // The owner of the file and the creator of the share // can always delete the share - if ($share->getShareOwner() === $this->userId || - $share->getSharedBy() === $this->userId + if ($share->getShareOwner() === $this->userId + || $share->getSharedBy() === $this->userId ) { return true; } @@ -1629,16 +1666,16 @@ class ShareAPIController extends OCSController { * @suppress PhanUndeclaredClassMethod */ protected function canDeleteShareFromSelf(IShare $share): bool { - if ($share->getShareType() !== IShare::TYPE_GROUP && - $share->getShareType() !== IShare::TYPE_ROOM && - $share->getShareType() !== IShare::TYPE_DECK && - $share->getShareType() !== IShare::TYPE_SCIENCEMESH + if ($share->getShareType() !== IShare::TYPE_GROUP + && $share->getShareType() !== IShare::TYPE_ROOM + && $share->getShareType() !== IShare::TYPE_DECK + && $share->getShareType() !== IShare::TYPE_SCIENCEMESH ) { return false; } - if ($share->getShareOwner() === $this->userId || - $share->getSharedBy() === $this->userId + if ($share->getShareOwner() === $this->userId + || $share->getSharedBy() === $this->userId ) { // Delete the whole share, not just for self return false; @@ -1656,7 +1693,7 @@ class ShareAPIController extends OCSController { if ($share->getShareType() === IShare::TYPE_ROOM) { try { return $this->getRoomShareHelper()->canAccessShare($share, $this->userId); - } catch (QueryException $e) { + } catch (ContainerExceptionInterface $e) { return false; } } @@ -1664,7 +1701,7 @@ class ShareAPIController extends OCSController { if ($share->getShareType() === IShare::TYPE_DECK) { try { return $this->getDeckShareHelper()->canAccessShare($share, $this->userId); - } catch (QueryException $e) { + } catch (ContainerExceptionInterface $e) { return false; } } @@ -1672,7 +1709,7 @@ class ShareAPIController extends OCSController { if ($share->getShareType() === IShare::TYPE_SCIENCEMESH) { try { return $this->getSciencemeshShareHelper()->canAccessShare($share, $this->userId); - } catch (QueryException $e) { + } catch (ContainerExceptionInterface $e) { return false; } } @@ -1798,10 +1835,10 @@ class ShareAPIController extends OCSController { * Returns the helper of ShareAPIController for room shares. * * If the Talk application is not enabled or the helper is not available - * a QueryException is thrown instead. + * a ContainerExceptionInterface is thrown instead. * * @return \OCA\Talk\Share\Helper\ShareAPIController - * @throws QueryException + * @throws ContainerExceptionInterface */ private function getRoomShareHelper() { if (!$this->appManager->isEnabledForUser('spreed')) { @@ -1815,10 +1852,10 @@ class ShareAPIController extends OCSController { * Returns the helper of ShareAPIHelper for deck shares. * * If the Deck application is not enabled or the helper is not available - * a QueryException is thrown instead. + * a ContainerExceptionInterface is thrown instead. * - * @return \OCA\Deck\Sharing\ShareAPIHelper - * @throws QueryException + * @return ShareAPIHelper + * @throws ContainerExceptionInterface */ private function getDeckShareHelper() { if (!$this->appManager->isEnabledForUser('deck')) { @@ -1832,10 +1869,10 @@ class ShareAPIController extends OCSController { * Returns the helper of ShareAPIHelper for sciencemesh shares. * * If the sciencemesh application is not enabled or the helper is not available - * a QueryException is thrown instead. + * a ContainerExceptionInterface is thrown instead. * - * @return \OCA\Deck\Sharing\ShareAPIHelper - * @throws QueryException + * @return ShareAPIHelper + * @throws ContainerExceptionInterface */ private function getSciencemeshShareHelper() { if (!$this->appManager->isEnabledForUser('sciencemesh')) { @@ -1871,8 +1908,8 @@ class ShareAPIController extends OCSController { continue; } - $providerShares = - $this->shareManager->getSharesBy($viewer, $provider, $node, $reShares, -1, 0); + $providerShares + = $this->shareManager->getSharesBy($viewer, $provider, $node, $reShares, -1, 0); $shares = array_merge($shares, $providerShares); } @@ -1968,7 +2005,7 @@ class ShareAPIController extends OCSController { return true; } - if ($share->getShareType() === IShare::TYPE_CIRCLE && \OC::$server->getAppManager()->isEnabledForUser('circles') + if ($share->getShareType() === IShare::TYPE_CIRCLE && Server::get(IAppManager::class)->isEnabledForUser('circles') && class_exists('\OCA\Circles\Api\v1\Circles')) { $hasCircleId = (str_ends_with($share->getSharedWith(), ']')); $shareWithStart = ($hasCircleId ? strrpos($share->getSharedWith(), '[') + 1 : 0); @@ -1984,7 +2021,7 @@ class ShareAPIController extends OCSController { return true; } return false; - } catch (QueryException $e) { + } catch (ContainerExceptionInterface $e) { return false; } } @@ -2083,30 +2120,50 @@ class ShareAPIController extends OCSController { if (!$share->getSharedBy()) { return; // Probably in a test } + + $canDownload = false; + $hideDownload = true; + $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); - $node = $userFolder->getFirstNodeById($share->getNodeId()); - if (!$node) { - return; - } - if ($node->getStorage()->instanceOfStorage(SharedStorage::class)) { - $storage = $node->getStorage(); - if ($storage instanceof Wrapper) { - $storage = $storage->getInstanceOfStorage(SharedStorage::class); - if ($storage === null) { - throw new \RuntimeException('Should not happen, instanceOfStorage but getInstanceOfStorage return null'); - } - } else { - throw new \RuntimeException('Should not happen, instanceOfStorage but not a wrapper'); + $nodes = $userFolder->getById($share->getNodeId()); + foreach ($nodes as $node) { + // Owner always can download it - so allow it and break + if ($node->getOwner()?->getUID() === $share->getSharedBy()) { + $canDownload = true; + $hideDownload = false; + break; } - /** @var SharedStorage $storage */ - $inheritedAttributes = $storage->getShare()->getAttributes(); - if ($inheritedAttributes !== null && $inheritedAttributes->getAttribute('permissions', 'download') === false) { - $share->setHideDownload(true); - $attributes = $share->getAttributes(); - if ($attributes) { - $attributes->setAttribute('permissions', 'download', false); - $share->setAttributes($attributes); + + if ($node->getStorage()->instanceOfStorage(SharedStorage::class)) { + $storage = $node->getStorage(); + if ($storage instanceof Wrapper) { + $storage = $storage->getInstanceOfStorage(SharedStorage::class); + if ($storage === null) { + throw new \RuntimeException('Should not happen, instanceOfStorage but getInstanceOfStorage return null'); + } + } else { + throw new \RuntimeException('Should not happen, instanceOfStorage but not a wrapper'); } + + /** @var SharedStorage $storage */ + $originalShare = $storage->getShare(); + $inheritedAttributes = $originalShare->getAttributes(); + // hide if hidden and also the current share enforces hide (can only be false if one share is false or user is owner) + $hideDownload = $hideDownload && $originalShare->getHideDownload(); + // allow download if already allowed by previous share or when the current share allows downloading + $canDownload = $canDownload || $inheritedAttributes === null || $inheritedAttributes->getAttribute('permissions', 'download') !== false; + } elseif ($node->getStorage()->instanceOfStorage(Storage::class)) { + $canDownload = true; // in case of federation storage, we can expect the download to be activated by default + } + } + + if ($hideDownload || !$canDownload) { + $share->setHideDownload(true); + + if (!$canDownload) { + $attributes = $share->getAttributes() ?? $share->newAttributes(); + $attributes->setAttribute('permissions', 'download', false); + $share->setAttributes($attributes); } } } @@ -2125,7 +2182,7 @@ class ShareAPIController extends OCSController { * 200: The email notification was sent successfully */ #[NoAdminRequired] - #[UserRateLimit(limit: 5, period: 120)] + #[UserRateLimit(limit: 10, period: 600)] public function sendShareEmail(string $id, $password = ''): DataResponse { try { $share = $this->getShareById($id); @@ -2198,4 +2255,41 @@ class ShareAPIController extends OCSController { throw new OCSException($this->l->t('Failed to generate a unique token')); } } + + /** + * Populate the result set with file tags + * + * @psalm-template T of array{tags?: list<string>, file_source: int, ...array<string, mixed>} + * @param list<T> $fileList + * @return list<T> file list populated with tags + */ + private function populateTags(array $fileList): array { + $tagger = $this->tagManager->load('files'); + $tags = $tagger->getTagsForObjects(array_map(static fn (array $fileData) => $fileData['file_source'], $fileList)); + + if (!is_array($tags)) { + throw new \UnexpectedValueException('$tags must be an array'); + } + + // Set empty tag array + foreach ($fileList as &$fileData) { + $fileData['tags'] = []; + } + unset($fileData); + + if (!empty($tags)) { + foreach ($tags as $fileId => $fileTags) { + foreach ($fileList as &$fileData) { + if ($fileId !== $fileData['file_source']) { + continue; + } + + $fileData['tags'] = $fileTags; + } + unset($fileData); + } + } + + return $fileList; + } } |