diff options
Diffstat (limited to 'apps/files_sharing/lib/Controller')
8 files changed, 234 insertions, 212 deletions
diff --git a/apps/files_sharing/lib/Controller/DeletedShareAPIController.php b/apps/files_sharing/lib/Controller/DeletedShareAPIController.php index fcd33bd88e6..2fa4d7c668f 100644 --- a/apps/files_sharing/lib/Controller/DeletedShareAPIController.php +++ b/apps/files_sharing/lib/Controller/DeletedShareAPIController.php @@ -8,6 +8,7 @@ declare(strict_types=1); */ namespace OCA\Files_Sharing\Controller; +use OCA\Deck\Sharing\ShareAPIHelper; use OCA\Files_Sharing\ResponseDefinitions; use OCP\App\IAppManager; use OCP\AppFramework\Http; @@ -22,8 +23,8 @@ use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; use OCP\IGroupManager; use OCP\IRequest; -use OCP\IServerContainer; use OCP\IUserManager; +use OCP\Server; use OCP\Share\Exceptions\GenericShareException; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager as ShareManager; @@ -43,7 +44,6 @@ class DeletedShareAPIController extends OCSController { private IGroupManager $groupManager, private IRootFolder $rootFolder, private IAppManager $appManager, - private IServerContainer $serverContainer, ) { parent::__construct($appName, $request); } @@ -142,11 +142,12 @@ class DeletedShareAPIController extends OCSController { #[NoAdminRequired] public function index(): DataResponse { $groupShares = $this->shareManager->getDeletedSharedWith($this->userId, IShare::TYPE_GROUP, null, -1, 0); + $teamShares = $this->shareManager->getDeletedSharedWith($this->userId, IShare::TYPE_CIRCLE, null, -1, 0); $roomShares = $this->shareManager->getDeletedSharedWith($this->userId, IShare::TYPE_ROOM, null, -1, 0); $deckShares = $this->shareManager->getDeletedSharedWith($this->userId, IShare::TYPE_DECK, null, -1, 0); $sciencemeshShares = $this->shareManager->getDeletedSharedWith($this->userId, IShare::TYPE_SCIENCEMESH, null, -1, 0); - $shares = array_merge($groupShares, $roomShares, $deckShares, $sciencemeshShares); + $shares = array_merge($groupShares, $teamShares, $roomShares, $deckShares, $sciencemeshShares); $shares = array_values(array_map(function (IShare $share) { return $this->formatShare($share); @@ -200,7 +201,7 @@ class DeletedShareAPIController extends OCSController { throw new QueryException(); } - return $this->serverContainer->get('\OCA\Talk\Share\Helper\DeletedShareAPIController'); + return Server::get('\OCA\Talk\Share\Helper\DeletedShareAPIController'); } /** @@ -209,7 +210,7 @@ class DeletedShareAPIController extends OCSController { * If the Deck application is not enabled or the helper is not available * a QueryException is thrown instead. * - * @return \OCA\Deck\Sharing\ShareAPIHelper + * @return ShareAPIHelper * @throws QueryException */ private function getDeckShareHelper() { @@ -217,7 +218,7 @@ class DeletedShareAPIController extends OCSController { throw new QueryException(); } - return $this->serverContainer->get('\OCA\Deck\Sharing\ShareAPIHelper'); + return Server::get('\OCA\Deck\Sharing\ShareAPIHelper'); } /** @@ -226,7 +227,7 @@ class DeletedShareAPIController extends OCSController { * If the sciencemesh application is not enabled or the helper is not available * a QueryException is thrown instead. * - * @return \OCA\Deck\Sharing\ShareAPIHelper + * @return ShareAPIHelper * @throws QueryException */ private function getSciencemeshShareHelper() { @@ -234,6 +235,6 @@ class DeletedShareAPIController extends OCSController { throw new QueryException(); } - return $this->serverContainer->get('\OCA\ScienceMesh\Sharing\ShareAPIHelper'); + return Server::get('\OCA\ScienceMesh\Sharing\ShareAPIHelper'); } } diff --git a/apps/files_sharing/lib/Controller/ExternalSharesController.php b/apps/files_sharing/lib/Controller/ExternalSharesController.php index 9d15b03c6cd..fa828a9d2c2 100644 --- a/apps/files_sharing/lib/Controller/ExternalSharesController.php +++ b/apps/files_sharing/lib/Controller/ExternalSharesController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/apps/files_sharing/lib/Controller/PublicPreviewController.php b/apps/files_sharing/lib/Controller/PublicPreviewController.php index 91dead57e80..d917f6e0ebb 100644 --- a/apps/files_sharing/lib/Controller/PublicPreviewController.php +++ b/apps/files_sharing/lib/Controller/PublicPreviewController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -11,6 +12,7 @@ use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\FileDisplayResponse; +use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\PublicShareController; use OCP\Constants; use OCP\Files\Folder; @@ -18,6 +20,7 @@ use OCP\Files\NotFoundException; use OCP\IPreview; use OCP\IRequest; use OCP\ISession; +use OCP\Preview\IMimeIconProvider; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager as ShareManager; use OCP\Share\IShare; @@ -33,6 +36,7 @@ class PublicPreviewController extends PublicShareController { private ShareManager $shareManager, ISession $session, private IPreview $previewManager, + private IMimeIconProvider $mimeIconProvider, ) { parent::__construct($appName, $request, $session); } @@ -63,9 +67,11 @@ class PublicPreviewController extends PublicShareController { * @param int $x Width of the preview * @param int $y Height of the preview * @param bool $a Whether to not crop the preview - * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}> + * @param bool $mimeFallback Whether to fallback to the mime icon if no preview is available + * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}> * * 200: Preview returned + * 303: Redirect to the mime icon url if mimeFallback is true * 400: Getting preview is not possible * 403: Getting preview is not allowed * 404: Share or preview not found @@ -79,6 +85,7 @@ class PublicPreviewController extends PublicShareController { int $x = 32, int $y = 32, $a = false, + bool $mimeFallback = false, ) { $cacheForSeconds = 60 * 60 * 24; // 1 day @@ -96,12 +103,12 @@ class PublicPreviewController extends PublicShareController { return new DataResponse([], Http::STATUS_FORBIDDEN); } - $attributes = $share->getAttributes(); // Only explicitly set to false will forbid the download! - $downloadForbidden = $attributes?->getAttribute('permissions', 'download') === false; + $downloadForbidden = !$share->canSeeContent(); + // Is this header is set it means our UI is doing a preview for no-download shares // we check a header so we at least prevent people from using the link directly (obfuscation) - $isPublicPreview = $this->request->getHeader('X-NC-Preview') === 'true'; + $isPublicPreview = $this->request->getHeader('x-nc-preview') === 'true'; if ($isPublicPreview && $downloadForbidden) { // Only cache for 15 minutes on public preview requests to quickly remove from cache @@ -124,6 +131,12 @@ class PublicPreviewController extends PublicShareController { $response->cacheFor($cacheForSeconds); return $response; } catch (NotFoundException $e) { + // If we have no preview enabled, we can redirect to the mime icon if any + if ($mimeFallback) { + if ($url = $this->mimeIconProvider->getMimeIconUrl($file->getMimeType())) { + return new RedirectResponse($url); + } + } return new DataResponse([], Http::STATUS_NOT_FOUND); } catch (\InvalidArgumentException $e) { return new DataResponse([], Http::STATUS_BAD_REQUEST); @@ -169,8 +182,7 @@ class PublicPreviewController extends PublicShareController { return new DataResponse([], Http::STATUS_FORBIDDEN); } - $attributes = $share->getAttributes(); - if ($attributes !== null && $attributes->getAttribute('permissions', 'download') === false) { + if (!$share->canSeeContent()) { return new DataResponse([], Http::STATUS_FORBIDDEN); } diff --git a/apps/files_sharing/lib/Controller/RemoteController.php b/apps/files_sharing/lib/Controller/RemoteController.php index e23ae51f219..8c15cd8463e 100644 --- a/apps/files_sharing/lib/Controller/RemoteController.php +++ b/apps/files_sharing/lib/Controller/RemoteController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. 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; + } } diff --git a/apps/files_sharing/lib/Controller/ShareController.php b/apps/files_sharing/lib/Controller/ShareController.php index 1c3c9534dde..5a776379fce 100644 --- a/apps/files_sharing/lib/Controller/ShareController.php +++ b/apps/files_sharing/lib/Controller/ShareController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -9,7 +10,6 @@ namespace OCA\Files_Sharing\Controller; use OC\Security\CSP\ContentSecurityPolicy; use OCA\DAV\Connector\Sabre\PublicAuth; use OCA\FederatedFileSharing\FederatedShareProvider; -use OCA\Files_Sharing\Activity\Providers\Downloads; use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent; use OCA\Files_Sharing\Event\ShareLinkAccessedEvent; use OCP\Accounts\IAccountManager; @@ -28,7 +28,6 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\File; use OCP\Files\Folder; use OCP\Files\IRootFolder; -use OCP\Files\Node; use OCP\Files\NotFoundException; use OCP\HintException; use OCP\IConfig; @@ -253,9 +252,9 @@ class ShareController extends AuthPublicShareController { * Emit a ShareLinkAccessedEvent event when a share is accessed, downloaded, auth... */ protected function emitShareAccessEvent(IShare $share, string $step = '', int $errorCode = 200, string $errorMessage = ''): void { - if ($step !== self::SHARE_ACCESS && - $step !== self::SHARE_AUTH && - $step !== self::SHARE_DOWNLOAD) { + if ($step !== self::SHARE_ACCESS + && $step !== self::SHARE_AUTH + && $step !== self::SHARE_DOWNLOAD) { return; } $this->eventDispatcher->dispatchTyped(new ShareLinkAccessedEvent($share, $step, $errorCode, $errorMessage)); @@ -359,19 +358,18 @@ class ShareController extends AuthPublicShareController { return new DataResponse('Share has no read permission'); } + $attributes = $share->getAttributes(); + if ($attributes?->getAttribute('permissions', 'download') === false) { + return new DataResponse('Share has no download permission'); + } + if (!$this->validateShare($share)) { throw new NotFoundException(); } - // Single file share - if ($share->getNode() instanceof File) { - // Single file download - $this->singleFileDownloaded($share, $share->getNode()); - } - // Directory share - else { - /** @var Folder $node */ - $node = $share->getNode(); + $node = $share->getNode(); + if ($node instanceof Folder) { + // Directory share // Try to get the path if ($path !== '') { @@ -386,22 +384,10 @@ class ShareController extends AuthPublicShareController { if ($node instanceof Folder) { if ($files === null || $files === '') { - // The folder is downloaded - $this->singleFileDownloaded($share, $share->getNode()); - } else { - $fileList = json_decode($files); - // in case we get only a single file - if (!is_array($fileList)) { - $fileList = [$fileList]; - } - foreach ($fileList as $file) { - $subNode = $node->get($file); - $this->singleFileDownloaded($share, $subNode); + if ($share->getHideDownload()) { + throw new NotFoundException('Downloading a folder'); } } - } else { - // Single file download - $this->singleFileDownloaded($share, $share->getNode()); } } @@ -414,77 +400,4 @@ class ShareController extends AuthPublicShareController { } return new RedirectResponse($this->urlGenerator->getAbsoluteURL($davUrl)); } - - /** - * create activity if a single file was downloaded from a link share - * - * @param Share\IShare $share - * @throws NotFoundException when trying to download a folder of a "hide download" share - */ - protected function singleFileDownloaded(IShare $share, Node $node) { - if ($share->getHideDownload() && $node instanceof Folder) { - throw new NotFoundException('Downloading a folder'); - } - - $fileId = $node->getId(); - - $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); - $userNode = $userFolder->getFirstNodeById($fileId); - $ownerFolder = $this->rootFolder->getUserFolder($share->getShareOwner()); - $userPath = $userFolder->getRelativePath($userNode->getPath()); - $ownerPath = $ownerFolder->getRelativePath($node->getPath()); - $remoteAddress = $this->request->getRemoteAddress(); - $dateTime = new \DateTime(); - $dateTime = $dateTime->format('Y-m-d H'); - $remoteAddressHash = md5($dateTime . '-' . $remoteAddress); - - $parameters = [$userPath]; - - if ($share->getShareType() === IShare::TYPE_EMAIL) { - if ($node instanceof File) { - $subject = Downloads::SUBJECT_SHARED_FILE_BY_EMAIL_DOWNLOADED; - } else { - $subject = Downloads::SUBJECT_SHARED_FOLDER_BY_EMAIL_DOWNLOADED; - } - $parameters[] = $share->getSharedWith(); - } else { - if ($node instanceof File) { - $subject = Downloads::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED; - $parameters[] = $remoteAddressHash; - } else { - $subject = Downloads::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED; - $parameters[] = $remoteAddressHash; - } - } - - $this->publishActivity($subject, $parameters, $share->getSharedBy(), $fileId, $userPath); - - if ($share->getShareOwner() !== $share->getSharedBy()) { - $parameters[0] = $ownerPath; - $this->publishActivity($subject, $parameters, $share->getShareOwner(), $fileId, $ownerPath); - } - } - - /** - * publish activity - * - * @param string $subject - * @param array $parameters - * @param string $affectedUser - * @param int $fileId - * @param string $filePath - */ - protected function publishActivity($subject, - array $parameters, - $affectedUser, - $fileId, - $filePath) { - $event = $this->activityManager->generateEvent(); - $event->setApp('files_sharing') - ->setType('public_links') - ->setSubject($subject, $parameters) - ->setAffectedUser($affectedUser) - ->setObject('files', $fileId, $filePath); - $this->activityManager->publish($event); - } } diff --git a/apps/files_sharing/lib/Controller/ShareInfoController.php b/apps/files_sharing/lib/Controller/ShareInfoController.php index 0a290d69c65..b7e79aec830 100644 --- a/apps/files_sharing/lib/Controller/ShareInfoController.php +++ b/apps/files_sharing/lib/Controller/ShareInfoController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files_sharing/lib/Controller/ShareesAPIController.php b/apps/files_sharing/lib/Controller/ShareesAPIController.php index 9a9e94a7ee2..0c458ce9662 100644 --- a/apps/files_sharing/lib/Controller/ShareesAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareesAPIController.php @@ -12,6 +12,7 @@ use Generator; use OC\Collaboration\Collaborators\SearchResult; use OC\Share\Share; use OCA\Files_Sharing\ResponseDefinitions; +use OCP\App\IAppManager; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\DataResponse; @@ -21,9 +22,11 @@ use OCP\Collaboration\Collaborators\ISearch; use OCP\Collaboration\Collaborators\ISearchResult; use OCP\Collaboration\Collaborators\SearchResultType; use OCP\Constants; +use OCP\GlobalScale\IConfig as GlobalScaleIConfig; use OCP\IConfig; use OCP\IRequest; use OCP\IURLGenerator; +use OCP\Server; use OCP\Share\IManager; use OCP\Share\IShare; use function array_slice; @@ -155,7 +158,7 @@ class ShareesAPIController extends OCSController { } // FIXME: DI - if (\OC::$server->getAppManager()->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) { + if (Server::get(IAppManager::class)->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) { $shareTypes[] = IShare::TYPE_CIRCLE; } @@ -173,15 +176,11 @@ class ShareesAPIController extends OCSController { $this->limit = $perPage; $this->offset = $perPage * ($page - 1); - // In global scale mode we always search the loogup server - if ($this->config->getSystemValueBool('gs.enabled', false)) { - $lookup = true; - $this->result['lookupEnabled'] = true; - } else { - $this->result['lookupEnabled'] = $this->config->getAppValue('files_sharing', 'lookupServerEnabled', 'yes') === 'yes'; - } + // In global scale mode we always search the lookup server + $this->result['lookupEnabled'] = Server::get(GlobalScaleIConfig::class)->isGlobalScaleEnabled(); + // TODO: Reconsider using lookup server for non-global-scale federation - [$result, $hasMoreResults] = $this->collaboratorSearch->search($search, $shareTypes, $lookup, $this->limit, $this->offset); + [$result, $hasMoreResults] = $this->collaboratorSearch->search($search, $shareTypes, $this->result['lookupEnabled'], $this->limit, $this->offset); // extra treatment for 'exact' subarray, with a single merge expected keys might be lost if (isset($result['exact'])) { @@ -328,7 +327,7 @@ class ShareesAPIController extends OCSController { } // FIXME: DI - if (\OC::$server->getAppManager()->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) { + if (Server::get(IAppManager::class)->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) { $shareTypes[] = IShare::TYPE_CIRCLE; } |