From a6b0c0a7bae98ee9e4899383337cd88f03601f5a Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Wed, 4 Feb 2026 12:14:51 +0100 Subject: [PATCH 1/2] perf(sharing): Avoid loading all shares from all users when unsharing First check which users have a shares and for which providers and then only load these shares. Avoid doing at most 5 SQL queries for each users a share was shared with if there are no shares. Signed-off-by: Carl Schwan --- apps/files_sharing/tests/CapabilitiesTest.php | 2 + .../tests/Settings/Admin/SharingTest.php | 2 + lib/private/Share20/DefaultShareProvider.php | 5 +- lib/private/Share20/Manager.php | 43 ++++++++- tests/lib/Share20/LegacyHooksTest.php | 14 +-- tests/lib/Share20/ManagerTest.php | 92 ++++++++++++++++++- 6 files changed, 143 insertions(+), 15 deletions(-) diff --git a/apps/files_sharing/tests/CapabilitiesTest.php b/apps/files_sharing/tests/CapabilitiesTest.php index 252459eaaae75..787a31c0626f2 100644 --- a/apps/files_sharing/tests/CapabilitiesTest.php +++ b/apps/files_sharing/tests/CapabilitiesTest.php @@ -18,6 +18,7 @@ use OCP\IAppConfig; use OCP\IConfig; use OCP\IDateTimeZone; +use OCP\IDBConnection; use OCP\IGroupManager; use OCP\IUserManager; use OCP\IUserSession; @@ -92,6 +93,7 @@ private function getResults(array $map, array $typedMap = [], bool $federationEn $this->createMock(ShareDisableChecker::class), $this->createMock(IDateTimeZone::class), $appConfig, + $this->createMock(IDBConnection::class), ); $cap = new Capabilities($config, $appConfig, $shareManager, $appManager); diff --git a/apps/settings/tests/Settings/Admin/SharingTest.php b/apps/settings/tests/Settings/Admin/SharingTest.php index 4b9745ea5998d..0ab11e40ce687 100644 --- a/apps/settings/tests/Settings/Admin/SharingTest.php +++ b/apps/settings/tests/Settings/Admin/SharingTest.php @@ -16,9 +16,11 @@ use OCP\IL10N; use OCP\IURLGenerator; use OCP\Share\IManager; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; +#[Group(name: 'DB')] class SharingTest extends TestCase { private Sharing $admin; diff --git a/lib/private/Share20/DefaultShareProvider.php b/lib/private/Share20/DefaultShareProvider.php index 46c2b94116217..fa2445524d2dd 100644 --- a/lib/private/Share20/DefaultShareProvider.php +++ b/lib/private/Share20/DefaultShareProvider.php @@ -1033,11 +1033,10 @@ public function getShareByToken($token) { /** * Create a share object from a database row * - * @param mixed[] $data - * @return \OCP\Share\IShare + * @param array $data * @throws InvalidShare */ - private function createShare($data) { + private function createShare($data): IShare { $share = new Share($this->rootFolder, $this->userManager); $share->setId($data['id']) ->setShareType((int)$data['share_type']) diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 7a1b7045b3643..61778f12d6aac 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -16,6 +16,7 @@ use OCA\Files_Sharing\AppInfo\Application; use OCA\Files_Sharing\SharedStorage; use OCA\ShareByMail\ShareByMailProvider; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\File; @@ -29,6 +30,7 @@ use OCP\IAppConfig; use OCP\IConfig; use OCP\IDateTimeZone; +use OCP\IDBConnection; use OCP\IGroupManager; use OCP\IL10N; use OCP\IUser; @@ -85,6 +87,7 @@ public function __construct( private ShareDisableChecker $shareDisableChecker, private IDateTimeZone $dateTimeZone, private IAppConfig $appConfig, + private IDBConnection $connection, ) { $this->l = $this->l10nFactory->get('lib'); // The constructor of LegacyHooks registers the listeners of share events @@ -1033,14 +1036,50 @@ protected function promoteReshares(IShare $share): void { IShare::TYPE_EMAIL, ]; - foreach ($userIds as $userId) { + // Figure out which users has some shares with which providers + $qb = $this->connection->getQueryBuilder(); + $qb->select('uid_initiator', 'share_type') + ->from('share') + ->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))) + ->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter($shareTypes, IQueryBuilder::PARAM_INT_ARRAY))) + ->andWhere( + $qb->expr()->orX( + $qb->expr()->in('uid_initiator', $qb->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)), + // Special case for old shares created via the web UI + $qb->expr()->andX( + $qb->expr()->in('uid_owner', $qb->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)), + $qb->expr()->isNull('uid_initiator') + ) + ) + ); + + if (!$node instanceof Folder) { + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId(), IQueryBuilder::PARAM_INT))); + } + + $qb->orderBy('id'); + + $cursor = $qb->executeQuery(); + $rawShares = []; + while ($data = $cursor->fetch()) { + if (!isset($rawShares[$data['uid_initiator']])) { + $rawShares[$data['uid_initiator']] = []; + } + if (!in_array($data['share_type'], $rawShares[$data['uid_initiator']], true)) { + $rawShares[$data['uid_initiator']][] = $data['share_type']; + } + } + $cursor->closeCursor(); + + foreach ($rawShares as $userId => $shareTypes) { foreach ($shareTypes as $shareType) { try { $provider = $this->factory->getProviderForType($shareType); - } catch (ProviderException $e) { + } catch (ProviderException) { continue; } + if ($node instanceof Folder) { /* We need to get all shares by this user to get subshares */ $shares = $provider->getSharesBy($userId, $shareType, null, false, -1, 0); diff --git a/tests/lib/Share20/LegacyHooksTest.php b/tests/lib/Share20/LegacyHooksTest.php index 2ce72b3fc1ccf..091bd5fb8dca7 100644 --- a/tests/lib/Share20/LegacyHooksTest.php +++ b/tests/lib/Share20/LegacyHooksTest.php @@ -9,7 +9,6 @@ use OC\EventDispatcher\EventDispatcher; use OC\Share20\LegacyHooks; -use OC\Share20\Manager; use OCP\Constants; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Cache\ICacheEntry; @@ -24,6 +23,7 @@ use OCP\Share\IManager as IShareManager; use OCP\Share\IShare; use OCP\Util; +use PHPUnit\Framework\Attributes\Group; use Psr\Log\LoggerInterface; use Test\TestCase; @@ -40,15 +40,11 @@ public function pre() { } } +#[Group(name: 'DB')] class LegacyHooksTest extends TestCase { - /** @var LegacyHooks */ - private $hooks; - - /** @var IEventDispatcher */ - private $eventDispatcher; - - /** @var Manager */ - private $manager; + private LegacyHooks $hooks; + private IEventDispatcher $eventDispatcher; + private IShareManager $manager; protected function setUp(): void { parent::setUp(); diff --git a/tests/lib/Share20/ManagerTest.php b/tests/lib/Share20/ManagerTest.php index 2afb5e6c3b3b7..f5c77fd17fa3d 100644 --- a/tests/lib/Share20/ManagerTest.php +++ b/tests/lib/Share20/ManagerTest.php @@ -18,6 +18,9 @@ use OC\Share20\Share; use OC\Share20\ShareDisableChecker; use OCP\Constants; +use OCP\DB\IResult; +use OCP\DB\QueryBuilder\IExpressionBuilder; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\File; @@ -33,6 +36,7 @@ use OCP\IAppConfig; use OCP\IConfig; use OCP\IDateTimeZone; +use OCP\IDBConnection; use OCP\IGroup; use OCP\IGroupManager; use OCP\IL10N; @@ -59,6 +63,7 @@ use OCP\Share\IShareProviderSupportsAllSharesInFolder; use OCP\Util; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\MockObject\MockBuilder; use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; @@ -75,7 +80,7 @@ public function listener() { * * @package Test\Share20 */ -#[\PHPUnit\Framework\Attributes\Group('DB')] +#[Group(name: 'DB')] class ManagerTest extends \Test\TestCase { protected Manager $manager; protected LoggerInterface&MockObject $logger; @@ -97,6 +102,7 @@ class ManagerTest extends \Test\TestCase { private DateTimeZone $timezone; protected IDateTimeZone&MockObject $dateTimeZone; protected IAppConfig&MockObject $appConfig; + protected IDBConnection&MockObject $connection; protected function setUp(): void { $this->logger = $this->createMock(LoggerInterface::class); @@ -110,6 +116,7 @@ protected function setUp(): void { $this->dispatcher = $this->createMock(IEventDispatcher::class); $this->userSession = $this->createMock(IUserSession::class); $this->knownUserService = $this->createMock(KnownUserService::class); + $this->connection = $this->createMock(IDBConnection::class); $this->shareDisabledChecker = new ShareDisableChecker($this->config, $this->userManager, $this->groupManager); $this->dateTimeZone = $this->createMock(IDateTimeZone::class); @@ -157,6 +164,7 @@ private function createManager(IProviderFactory $factory): Manager { $this->shareDisabledChecker, $this->dateTimeZone, $this->appConfig, + $this->connection, ); } @@ -182,6 +190,7 @@ private function createManagerMock(): MockBuilder { $this->shareDisabledChecker, $this->dateTimeZone, $this->appConfig, + $this->connection, ]); } @@ -486,6 +495,26 @@ public function testPromoteReshareFile(): void { $manager->expects($this->exactly(1))->method('updateShare')->with($reShare)->willReturn($reShare); + $qb = $this->createMock(IQueryBuilder::class); + $result = $this->createMock(IResult::class); + $qb->method('select') + ->willReturn($qb); + $qb->method('from') + ->willReturn($qb); + $qb->method('andWhere') + ->willReturn($qb); + $qb->method('expr') + ->willReturn($this->createMock(IExpressionBuilder::class)); + $qb->method('executeQuery') + ->willReturn($result); + $this->connection->method('getQueryBuilder') + ->willReturn($qb); + $result->method('fetch') + ->willReturnOnConsecutiveCalls( + ['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER], + false, + ); + self::invokePrivate($manager, 'promoteReshares', [$share]); } @@ -546,6 +575,26 @@ public function testPromoteReshare(): void { return $expected; }); + $qb = $this->createMock(IQueryBuilder::class); + $result = $this->createMock(IResult::class); + $qb->method('select') + ->willReturn($qb); + $qb->method('from') + ->willReturn($qb); + $qb->method('andWhere') + ->willReturn($qb); + $qb->method('expr') + ->willReturn($this->createMock(IExpressionBuilder::class)); + $qb->method('executeQuery') + ->willReturn($result); + $this->connection->method('getQueryBuilder') + ->willReturn($qb); + $result->method('fetch') + ->willReturnOnConsecutiveCalls( + ['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER], + false, + ); + self::invokePrivate($manager, 'promoteReshares', [$share]); } @@ -574,6 +623,26 @@ public function testPromoteReshareWhenUserHasAnotherShare(): void { /* No share is promoted because generalCreateChecks does not throw */ $manager->expects($this->never())->method('updateShare'); + $qb = $this->createMock(IQueryBuilder::class); + $result = $this->createMock(IResult::class); + $qb->method('select') + ->willReturn($qb); + $qb->method('from') + ->willReturn($qb); + $qb->method('andWhere') + ->willReturn($qb); + $qb->method('expr') + ->willReturn($this->createMock(IExpressionBuilder::class)); + $qb->method('executeQuery') + ->willReturn($result); + $this->connection->method('getQueryBuilder') + ->willReturn($qb); + $result->method('fetch') + ->willReturnOnConsecutiveCalls( + ['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER], + false, + ); + self::invokePrivate($manager, 'promoteReshares', [$share]); } @@ -641,6 +710,27 @@ public function testPromoteReshareOfUsersInGroupShare(): void { return $expected; }); + $qb = $this->createMock(IQueryBuilder::class); + $result = $this->createMock(IResult::class); + $qb->method('select') + ->willReturn($qb); + $qb->method('from') + ->willReturn($qb); + $qb->method('andWhere') + ->willReturn($qb); + $qb->method('expr') + ->willReturn($this->createMock(IExpressionBuilder::class)); + $qb->method('executeQuery') + ->willReturn($result); + $this->connection->method('getQueryBuilder') + ->willReturn($qb); + $result->method('fetch') + ->willReturnOnConsecutiveCalls( + ['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER], + ['uid_initiator' => 'userC', 'share_type' => IShare::TYPE_USER], + false, + ); + self::invokePrivate($manager, 'promoteReshares', [$share]); } From a0a194a24203541e34ce3c6ac4433c07244b9d79 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Mon, 16 Feb 2026 16:26:15 +0100 Subject: [PATCH 2/2] perf(share): Refactor share APIs to be more efficient - Fetch share from various providers at once by providing a new interface for providers to create their share based on raw data from the database - Use more efficient key-based pagination instead of offset-based pagination Signed-off-by: Carl Schwan --- apps/dav/lib/Connector/Sabre/SharesPlugin.php | 45 +-- .../unit/Connector/Sabre/SharesPluginTest.php | 6 +- .../lib/FederatedShareProvider.php | 197 +++------- .../lib/Service/OwnershipTransferService.php | 12 +- .../files_sharing/lib/AppInfo/Application.php | 4 +- .../Controller/DeletedShareAPIController.php | 9 +- .../lib/Controller/ShareAPIController.php | 107 ++--- .../lib/Listener/UserAddedToGroupListener.php | 3 +- apps/files_sharing/lib/MountProvider.php | 10 +- .../lib/Notification/Listener.php | 11 +- apps/sharebymail/lib/ShareByMailProvider.php | 211 ++-------- lib/private/Share20/DefaultShareProvider.php | 166 +++----- lib/private/Share20/Manager.php | 364 +++++++++++++++--- lib/public/PaginationParameters.php | 73 ++++ lib/public/Share/ICreateShareProvider.php | 42 ++ lib/public/Share/IManager.php | 39 +- lib/public/Share/IShareProvider.php | 7 +- tests/lib/Share/Backend.php | 6 +- 18 files changed, 688 insertions(+), 624 deletions(-) create mode 100644 lib/public/PaginationParameters.php create mode 100644 lib/public/Share/ICreateShareProvider.php diff --git a/apps/dav/lib/Connector/Sabre/SharesPlugin.php b/apps/dav/lib/Connector/Sabre/SharesPlugin.php index 5a4f187ef3c94..df195e0b527ac 100644 --- a/apps/dav/lib/Connector/Sabre/SharesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/SharesPlugin.php @@ -15,6 +15,7 @@ use OCP\Files\NotFoundException; use OCP\Files\Storage\ISharedStorage; use OCP\IUserSession; +use OCP\PaginationParameters; use OCP\Share\IManager; use OCP\Share\IShare; use Sabre\DAV\Exception\NotFound; @@ -79,10 +80,9 @@ public function initialize(Server $server) { } /** - * @param Node $node - * @return IShare[] + * @return list */ - private function getShare(Node $node): array { + private function getShare(Node $node, PaginationParameters $paginationParameters): array { $result = []; $requestedShareTypes = [ IShare::TYPE_USER, @@ -95,26 +95,23 @@ private function getShare(Node $node): array { IShare::TYPE_DECK, ]; - foreach ($requestedShareTypes as $requestedShareType) { - $result[] = $this->shareManager->getSharesBy( + $result[] = $this->shareManager->getAllSharesBy( + $this->userId, + $node, + $paginationParameters, + false, + ); + + // Also check for shares where the user is the recipient + try { + $result[] = $this->shareManager->getAllSharedWith( $this->userId, - $requestedShareType, + $requestedShareTypes, $node, - false, - -1 + $paginationParameters, ); - - // Also check for shares where the user is the recipient - try { - $result[] = $this->shareManager->getSharedWith( - $this->userId, - $requestedShareType, - $node, - -1 - ); - } catch (BackendError $e) { - // ignore - } + } catch (BackendError $e) { + // ignore } return array_merge(...$result); @@ -155,7 +152,7 @@ private function getShares(DavNode $sabreNode): array { return []; } - $shares = $this->getShare($node); + $shares = $this->getShare($node, new PaginationParameters(limit: null)); $this->cachedShares[$sabreNode->getId()] = $shares; return $shares; } @@ -235,9 +232,9 @@ public function validateMoveOrCopy(string $source, string $target): bool { return true; } - $targetShares = $this->getShare($targetNode->getNode()); + $targetShares = $this->getShare($targetNode->getNode(), new PaginationParameters(limit: null)); if (empty($targetShares)) { - // Target is not a share so no re-sharing inprogress + // Target is not a share so no re-sharing in-progress return true; } @@ -253,7 +250,7 @@ public function validateMoveOrCopy(string $source, string $target): bool { } } - // if the share recipient is allow to delete from the share, they are allowed to move the file out of the share + // if the share recipient is allowed to delete from the share, they are allowed to move the file out of the share // the user moving the file out of the share to their home storage would give them share permissions and allow moving into the share // // since the 2-step move is allowed, we also allow both steps at once diff --git a/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php index ff6d7f3c3dd42..b18c0af7dba85 100644 --- a/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php @@ -87,12 +87,11 @@ public function testGetProperties(array $shareTypes): void { }); $this->shareManager->expects($this->any()) - ->method('getSharedWith') + ->method('getAllSharedWith') ->with( $this->equalTo('user1'), $this->anything(), $this->equalTo($node), - $this->equalTo(-1) ) ->willReturn([]); @@ -183,12 +182,11 @@ public function testPreloadThenGetProperties(array $shareTypes): void { }); $this->shareManager->expects($this->any()) - ->method('getSharedWith') + ->method('getAllSharedWith') ->with( $this->equalTo('user1'), $this->anything(), $this->equalTo($node), - $this->equalTo(-1) ) ->willReturn([]); diff --git a/apps/federatedfilesharing/lib/FederatedShareProvider.php b/apps/federatedfilesharing/lib/FederatedShareProvider.php index 1d1c00772c5bf..10cd2bb540127 100644 --- a/apps/federatedfilesharing/lib/FederatedShareProvider.php +++ b/apps/federatedfilesharing/lib/FederatedShareProvider.php @@ -24,42 +24,38 @@ use OCP\IUserManager; use OCP\Share\Exceptions\GenericShareException; use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\ICreateShareProvider; use OCP\Share\IShare; use OCP\Share\IShareProvider; use OCP\Share\IShareProviderSupportsAllSharesInFolder; use Override; use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Exception\LogicException; /** * Class FederatedShareProvider * * @package OCA\FederatedFileSharing */ -class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAllSharesInFolder { - public const SHARE_TYPE_REMOTE = 6; +class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAllSharesInFolder, ICreateShareProvider { + private string $externalShareTable = 'share_external'; - /** @var string */ - private $externalShareTable = 'share_external'; + /** @var list list of supported share types */ + private array $supportedShareType = [IShare::TYPE_REMOTE_GROUP, IShare::TYPE_REMOTE, IShare::TYPE_CIRCLE]; - /** @var array list of supported share types */ - private $supportedShareType = [IShare::TYPE_REMOTE_GROUP, IShare::TYPE_REMOTE, IShare::TYPE_CIRCLE]; - - /** - * DefaultShareProvider constructor. - */ public function __construct( - private IDBConnection $dbConnection, - private AddressHandler $addressHandler, - private Notifications $notifications, - private TokenHandler $tokenHandler, - private IL10N $l, - private IRootFolder $rootFolder, - private IConfig $config, - private IUserManager $userManager, - private ICloudIdManager $cloudIdManager, - private \OCP\GlobalScale\IConfig $gsConfig, - private ICloudFederationProviderManager $cloudFederationProviderManager, - private LoggerInterface $logger, + private readonly IDBConnection $dbConnection, + private readonly AddressHandler $addressHandler, + private readonly Notifications $notifications, + private readonly TokenHandler $tokenHandler, + private readonly IL10N $l, + private readonly IRootFolder $rootFolder, + private readonly IConfig $config, + private readonly IUserManager $userManager, + private readonly ICloudIdManager $cloudIdManager, + private readonly \OCP\GlobalScale\IConfig $gsConfig, + private readonly ICloudFederationProviderManager $cloudFederationProviderManager, + private readonly LoggerInterface $logger, ) { } @@ -68,6 +64,16 @@ public function identifier(): string { return 'ocFederatedSharing'; } + #[Override] + public function getShareTypes(): array { + return $this->supportedShareType; + } + + #[Override] + public function getTokenShareTypes(): array { + return $this->supportedShareType; + } + /** * Share a path * @@ -160,7 +166,7 @@ public function create(IShare $share): IShare { } $data = $this->getRawShare($shareId); - return $this->createShareObject($data); + return $this->createShare($data); } /** @@ -424,7 +430,7 @@ public function getChildren(IShare $parent): array { $cursor = $qb->executeQuery(); while ($data = $cursor->fetchAssociative()) { - $children[] = $this->createShareObject($data); + $children[] = $this->createShare($data); } $cursor->closeCursor(); @@ -571,104 +577,25 @@ private function getSharesInFolderInternal(?string $userId, Folder $node, ?bool $cursor = $qb->executeQuery(); $shares = []; while ($data = $cursor->fetchAssociative()) { - $shares[$data['fileid']][] = $this->createShareObject($data); + $shares[$data['fileid']][] = $this->createShare($data); } $cursor->closeCursor(); return $shares; } - /** - * @inheritdoc - */ + #[Override] public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) { - $qb = $this->dbConnection->getQueryBuilder(); - $qb->select('*') - ->from('share'); - - $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter($shareType))); - - /** - * Reshares for this user are shares where they are the owner. - */ - if ($reshares === false) { - //Special case for old shares created via the web UI - $or1 = $qb->expr()->andX( - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), - $qb->expr()->isNull('uid_initiator') - ); - - $qb->andWhere( - $qb->expr()->orX( - $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)), - $or1 - ) - ); - } else { - $qb->andWhere( - $qb->expr()->orX( - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), - $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) - ) - ); - } - - if ($node !== null) { - $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); - } - - if ($limit !== -1) { - $qb->setMaxResults($limit); - } - - $qb->setFirstResult($offset); - $qb->orderBy('id'); - - $cursor = $qb->executeQuery(); - $shares = []; - while ($data = $cursor->fetchAssociative()) { - $shares[] = $this->createShareObject($data); - } - $cursor->closeCursor(); - - return $shares; + throw new LogicException('Is no longer used'); } - /** - * @inheritdoc - */ + #[Override] public function getShareById($id, $recipientId = null) { - $qb = $this->dbConnection->getQueryBuilder(); - - $qb->select('*') - ->from('share') - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) - ->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter($this->supportedShareType, IQueryBuilder::PARAM_INT_ARRAY))); - - $cursor = $qb->executeQuery(); - $data = $cursor->fetchAssociative(); - $cursor->closeCursor(); - - if ($data === false) { - throw new ShareNotFound('Can not find share with ID: ' . $id); - } - - try { - $share = $this->createShareObject($data); - } catch (InvalidShare $e) { - throw new ShareNotFound(); - } - - return $share; + throw new LogicException('Is no longer used'); } - /** - * Get shares for a given path - * - * @param Node $path - * @return IShare[] - */ - public function getSharesByPath(Node $path) { + #[Override] + public function getSharesByPath(Node $path): array { $qb = $this->dbConnection->getQueryBuilder(); // get federated user shares @@ -680,17 +607,15 @@ public function getSharesByPath(Node $path) { $shares = []; while ($data = $cursor->fetchAssociative()) { - $shares[] = $this->createShareObject($data); + $shares[] = $this->createShare($data); } $cursor->closeCursor(); return $shares; } - /** - * @inheritdoc - */ - public function getSharedWith($userId, $shareType, $node, $limit, $offset) { + #[Override] + public function getSharedWith(string $userId, int $shareType, ?Node $node, int $limit, int $offset): array { /** @var IShare[] $shares */ $shares = []; @@ -719,7 +644,7 @@ public function getSharedWith($userId, $shareType, $node, $limit, $offset) { $cursor = $qb->executeQuery(); while ($data = $cursor->fetchAssociative()) { - $shares[] = $this->createShareObject($data); + $shares[] = $this->createShare($data); } $cursor->closeCursor(); @@ -743,13 +668,7 @@ public function getShareByToken(string $token): IShare { throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); } - try { - $share = $this->createShareObject($data); - } catch (InvalidShare $e) { - throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); - } - - return $share; + return $this->createShare($data); } /** @@ -777,15 +696,8 @@ private function getRawShare($id) { return $data; } - /** - * Create a share object from an database row - * - * @param array $data - * @return IShare - * @throws InvalidShare - * @throws ShareNotFound - */ - private function createShareObject($data): IShare { + #[Override] + public function createShare(array $data): IShare { $share = new Share($this->rootFolder, $this->userManager); $share->setId((string)$data['id']) ->setShareType((int)$data['share_type']) @@ -1038,25 +950,8 @@ public function getAccessList($nodes, $currentAccess) { return ['remote' => $remote]; } + #[Override] public function getAllShares(): iterable { - $qb = $this->dbConnection->getQueryBuilder(); - - $qb->select('*') - ->from('share') - ->where($qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_REMOTE_GROUP, IShare::TYPE_REMOTE], IQueryBuilder::PARAM_INT_ARRAY))); - - $cursor = $qb->executeQuery(); - while ($data = $cursor->fetchAssociative()) { - try { - $share = $this->createShareObject($data); - } catch (InvalidShare $e) { - continue; - } catch (ShareNotFound $e) { - continue; - } - - yield $share; - } - $cursor->closeCursor(); + throw new \LogicException('getAllShare in DefaultShareProvider should no longer be used'); } } diff --git a/apps/files/lib/Service/OwnershipTransferService.php b/apps/files/lib/Service/OwnershipTransferService.php index e4a4e8f595aef..031f8e18243a1 100644 --- a/apps/files/lib/Service/OwnershipTransferService.php +++ b/apps/files/lib/Service/OwnershipTransferService.php @@ -29,6 +29,7 @@ use OCP\IUser; use OCP\IUserManager; use OCP\L10N\IFactory; +use OCP\PaginationParameters; use OCP\Server; use OCP\Share\IManager as IShareManager; use OCP\Share\IShare; @@ -384,16 +385,19 @@ private function collectIncomingShares( $progress = new ProgressBar($output); $normalizedPath = Filesystem::normalizePath($path); - $offset = 0; + $maxShareId = null; while (true) { - $sharePage = $this->shareManager->getSharedWith($sourceUid, IShare::TYPE_USER, null, 50, $offset); + /** @var list $sharePage */ + $sharePage = $this->shareManager->getAllSharedWith($sourceUid, [IShare::TYPE_USER], null, new PaginationParameters(limit: 50, maxId: $maxShareId)); $progress->advance(count($sharePage)); if (empty($sharePage)) { break; } + $maxShareId = end($sharePage)->getId(); + if ($path !== null && $path !== "$sourceUid/files") { - $sharePage = array_filter($sharePage, static function (IShare $share) use ($sourceUid, $normalizedPath) { + $sharePage = array_filter($sharePage, static function (IShare $share) use ($sourceUid, $normalizedPath): bool { try { return str_starts_with(Filesystem::normalizePath($sourceUid . '/files' . $share->getTarget() . '/', false), $normalizedPath . '/'); } catch (Exception) { @@ -405,8 +409,6 @@ private function collectIncomingShares( foreach ($sharePage as $share) { $shares[$share->getNodeId()] = $share; } - - $offset += 50; } diff --git a/apps/files_sharing/lib/AppInfo/Application.php b/apps/files_sharing/lib/AppInfo/Application.php index 4f0848b60bd33..0aaca666e2476 100644 --- a/apps/files_sharing/lib/AppInfo/Application.php +++ b/apps/files_sharing/lib/AppInfo/Application.php @@ -155,7 +155,7 @@ public function registerEventsScripts(IEventDispatcher $dispatcher): void { // notifications api to accept incoming user shares $dispatcher->addListener(ShareCreatedEvent::class, function (ShareCreatedEvent $event): void { /** @var Listener $listener */ - $listener = $this->getContainer()->query(Listener::class); + $listener = $this->getContainer()->get(Listener::class); $listener->shareNotification($event); }); $dispatcher->addListener(IGroup::class . '::postAddUser', function ($event): void { @@ -163,7 +163,7 @@ public function registerEventsScripts(IEventDispatcher $dispatcher): void { return; } /** @var Listener $listener */ - $listener = $this->getContainer()->query(Listener::class); + $listener = $this->getContainer()->get(Listener::class); $listener->userAddedToGroup($event); }); } diff --git a/apps/files_sharing/lib/Controller/DeletedShareAPIController.php b/apps/files_sharing/lib/Controller/DeletedShareAPIController.php index e7ddab8a40bf2..ee082fa35cceb 100644 --- a/apps/files_sharing/lib/Controller/DeletedShareAPIController.php +++ b/apps/files_sharing/lib/Controller/DeletedShareAPIController.php @@ -23,6 +23,7 @@ use OCP\IGroupManager; use OCP\IRequest; use OCP\IUserManager; +use OCP\PaginationParameters; use OCP\Server; use OCP\Share\Exceptions\GenericShareException; use OCP\Share\Exceptions\ShareNotFound; @@ -135,13 +136,9 @@ private function formatShare(IShare $share): array { */ #[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); + $shares = $this->shareManager->getAllDeletedSharedWith($this->userId, [IShare::TYPE_GROUP, IShare::TYPE_CIRCLE, IShare::TYPE_ROOM, IShare::TYPE_DECK], null, new PaginationParameters(limit: null)); - $shares = array_merge($groupShares, $teamShares, $roomShares, $deckShares); - $shares = array_values(array_map(fn (IShare $share): array => $this->formatShare($share), $shares)); + $shares = array_map(fn (IShare $share): array => $this->formatShare($share), $shares); return new DataResponse($shares); } diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index 8ed52a1de41d6..d446f0571f2a4 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -57,6 +57,7 @@ use OCP\Lock\LockedException; use OCP\Mail\IEmailValidator; use OCP\Mail\IMailer; +use OCP\PaginationParameters; use OCP\Server; use OCP\Share\Exceptions\GenericShareException; use OCP\Share\Exceptions\ShareNotFound; @@ -845,20 +846,11 @@ public function createShare( * @return list */ private function getSharedWithMe($node, bool $includeTags): array { - $userShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_USER, $node, -1, 0); - $groupShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_GROUP, $node, -1, 0); - $circleShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_CIRCLE, $node, -1, 0); - $roomShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_ROOM, $node, -1, 0); - $deckShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_DECK, $node, -1, 0); - - $shares = array_merge($userShares, $groupShares, $circleShares, $roomShares, $deckShares); - - $filteredShares = array_filter($shares, function (IShare $share) { - return $share->getShareOwner() !== $this->userId && $share->getSharedBy() !== $this->userId; - }); + $shareTypes = [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_CIRCLE, IShare::TYPE_ROOM, IShare::TYPE_DECK]; + $shares = $this->shareManager->getAllSharedWith($this->userId, $shareTypes, $node, new PaginationParameters(limit: null), ignoreWithSelf: true); $formatted = []; - foreach ($filteredShares as $share) { + foreach ($shares as $share) { if ($this->canAccessShare($share)) { try { $formatted[] = $this->formatShare($share); @@ -887,38 +879,30 @@ private function getSharesInDir(Node $folder): array { throw new OCSBadRequestException($this->l->t('Not a directory')); } - $nodes = $folder->getDirectoryListing(); - - /** @var IShare[] $shares */ - $shares = array_reduce($nodes, function ($carry, $node) { - $carry = array_merge($carry, $this->getAllShares($node, true)); - return $carry; - }, []); - // filter out duplicate shares - $known = []; - $formatted = $miniFormatted = []; $resharingRight = false; $known = []; - foreach ($shares as $share) { - if (in_array($share->getId(), $known) || $share->getSharedWith() === $this->userId) { - continue; - } + foreach ($folder->getDirectoryListing() as $node) { + foreach ($this->getAllShares($node, true) as $share) { + if (in_array($share->getId(), $known) || $share->getSharedWith() === $this->userId) { + continue; + } - try { - $format = $this->formatShare($share); + try { + $format = $this->formatShare($share); - $known[] = $share->getId(); - $formatted[] = $format; - if ($share->getSharedBy() === $this->userId) { - $miniFormatted[] = $format; - } - if (!$resharingRight && $this->shareProviderResharingRights($this->userId, $share, $folder)) { - $resharingRight = true; + $known[] = $share->getId(); + $formatted[] = $format; + if ($share->getSharedBy() === $this->userId) { + $miniFormatted[] = $format; + } + if (!$resharingRight && $this->shareProviderResharingRights($this->userId, $share, $folder)) { + $resharingRight = true; + } + } catch (\Exception $e) { + //Ignore this share } - } catch (\Exception $e) { - //Ignore this share } } @@ -1424,17 +1408,15 @@ public function pendingShares(): DataResponse { IShare::TYPE_GROUP ]; - foreach ($shareTypes as $shareType) { - $shares = $this->shareManager->getSharedWith($this->userId, $shareType, null, -1, 0); + $shares = $this->shareManager->getAllSharedWith($this->userId, $shareTypes, null, new PaginationParameters(limit: null)); - foreach ($shares as $share) { - if ($share->getStatus() === IShare::STATUS_PENDING || $share->getStatus() === IShare::STATUS_REJECTED) { - $pendingShares[] = $share; - } + foreach ($shares as $share) { + if ($share->getStatus() === IShare::STATUS_PENDING || $share->getStatus() === IShare::STATUS_REJECTED) { + $pendingShares[] = $share; } } - $result = array_values(array_filter(array_map(function (IShare $share) { + $result = array_values(array_filter(array_map(function (IShare $share): ?array { $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); $node = $userFolder->getFirstNodeById($share->getNodeId()); if (!$node) { @@ -1453,7 +1435,7 @@ public function pendingShares(): DataResponse { } catch (NotFoundException $e) { return null; } - }, $pendingShares), function ($entry) { + }, $pendingShares), function (?array $entry): bool { return $entry !== null; })); @@ -1975,39 +1957,10 @@ private function shareProviderResharingRights(string $userId, IShare $share, $no * * @param Node|null $path * @param boolean $reshares - * @return IShare[] + * @return list */ - private function getAllShares(?Node $path = null, bool $reshares = false) { - // Get all shares - $userShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_USER, $path, $reshares, -1, 0); - $groupShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_GROUP, $path, $reshares, -1, 0); - $linkShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_LINK, $path, $reshares, -1, 0); - - // EMAIL SHARES - $mailShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_EMAIL, $path, $reshares, -1, 0); - - // TEAM SHARES - $circleShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_CIRCLE, $path, $reshares, -1, 0); - - // TALK SHARES - $roomShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_ROOM, $path, $reshares, -1, 0); - - // DECK SHARES - $deckShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_DECK, $path, $reshares, -1, 0); - - // FEDERATION - if ($this->shareManager->outgoingServer2ServerSharesAllowed()) { - $federatedShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_REMOTE, $path, $reshares, -1, 0); - } else { - $federatedShares = []; - } - if ($this->shareManager->outgoingServer2ServerGroupSharesAllowed()) { - $federatedGroupShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_REMOTE_GROUP, $path, $reshares, -1, 0); - } else { - $federatedGroupShares = []; - } - - return array_merge($userShares, $groupShares, $linkShares, $mailShares, $circleShares, $roomShares, $deckShares, $federatedShares, $federatedGroupShares); + private function getAllShares(?Node $path = null, bool $reshares = false): array { + return $this->shareManager->getAllSharesBy($this->userId, $path, new PaginationParameters(limit: null), $reshares); } diff --git a/apps/files_sharing/lib/Listener/UserAddedToGroupListener.php b/apps/files_sharing/lib/Listener/UserAddedToGroupListener.php index 281c96ca5e7c0..448fcf0560cb6 100644 --- a/apps/files_sharing/lib/Listener/UserAddedToGroupListener.php +++ b/apps/files_sharing/lib/Listener/UserAddedToGroupListener.php @@ -13,6 +13,7 @@ use OCP\EventDispatcher\IEventListener; use OCP\Group\Events\UserAddedEvent; use OCP\IConfig; +use OCP\PaginationParameters; use OCP\Share\IManager; use OCP\Share\IShare; @@ -39,7 +40,7 @@ public function handle(Event $event): void { } // Get all group shares this user has access to now to filter later - $shares = $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_GROUP, null, -1); + $shares = $this->shareManager->getAllSharedWith($user->getUID(), [IShare::TYPE_GROUP], null, new PaginationParameters()); foreach ($shares as $share) { // If this is not the new group we can skip it diff --git a/apps/files_sharing/lib/MountProvider.php b/apps/files_sharing/lib/MountProvider.php index e9575dbac26c3..4d6167dbe9da8 100644 --- a/apps/files_sharing/lib/MountProvider.php +++ b/apps/files_sharing/lib/MountProvider.php @@ -21,6 +21,7 @@ use OCP\ICacheFactory; use OCP\IConfig; use OCP\IUser; +use OCP\PaginationParameters; use OCP\Share\IAttributes; use OCP\Share\IManager; use OCP\Share\IShare; @@ -61,14 +62,9 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader) { */ public function getSuperSharesForUser(IUser $user): array { $userId = $user->getUID(); - $shares = $this->mergeIterables( - $this->shareManager->getSharedWith($userId, IShare::TYPE_USER, null, -1), - $this->shareManager->getSharedWith($userId, IShare::TYPE_GROUP, null, -1), - $this->shareManager->getSharedWith($userId, IShare::TYPE_CIRCLE, null, -1), - $this->shareManager->getSharedWith($userId, IShare::TYPE_ROOM, null, -1), - $this->shareManager->getSharedWith($userId, IShare::TYPE_DECK, null, -1), - ); + $shareTypes = [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_CIRCLE, IShare::TYPE_ROOM, IShare::TYPE_DECK]; + $shares = $this->shareManager->getAllSharedWith($userId, $shareTypes, null, new PaginationParameters(limit: null), ignoreWithSelf: true); $shares = $this->filterShares($shares, $userId); return $this->buildSuperShares($shares, $user); } diff --git a/apps/files_sharing/lib/Notification/Listener.php b/apps/files_sharing/lib/Notification/Listener.php index 1cf0f845e7a51..7bfbfcad282aa 100644 --- a/apps/files_sharing/lib/Notification/Listener.php +++ b/apps/files_sharing/lib/Notification/Listener.php @@ -13,6 +13,7 @@ use OCP\IUser; use OCP\Notification\IManager as INotificationManager; use OCP\Notification\INotification; +use OCP\PaginationParameters; use OCP\Share\Events\ShareCreatedEvent; use OCP\Share\IManager as IShareManager; use OCP\Share\IShare; @@ -60,12 +61,17 @@ public function userAddedToGroup(GenericEvent $event): void { /** @var IUser $user */ $user = $event->getArgument('user'); - $offset = 0; + $sinceShareId = null; + $paginationParameters = new PaginationParameters( + limit: 50, + maxId: null, + ); while (true) { - $shares = $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_GROUP, null, 50, $offset); + $shares = $this->shareManager->getAllSharedWith($user->getUID(), [IShare::TYPE_GROUP], null, $paginationParameters); if (empty($shares)) { break; } + $paginationParameters->maxId = end($shares)->getId(); foreach ($shares as $share) { if ($share->getSharedWith() !== $group->getGID()) { @@ -82,7 +88,6 @@ public function userAddedToGroup(GenericEvent $event): void { ->setUser($user->getUID()); $this->notificationManager->notify($notification); } - $offset += 50; } } diff --git a/apps/sharebymail/lib/ShareByMailProvider.php b/apps/sharebymail/lib/ShareByMailProvider.php index ebbe56e52dfa8..8fae5f7110653 100644 --- a/apps/sharebymail/lib/ShareByMailProvider.php +++ b/apps/sharebymail/lib/ShareByMailProvider.php @@ -38,7 +38,9 @@ use OCP\Share\IShare; use OCP\Share\IShareProviderWithNotification; use OCP\Util; +use Override; use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Exception\LogicException; /** * Class ShareByMail @@ -46,41 +48,42 @@ * @package OCA\ShareByMail */ class ShareByMailProvider extends DefaultShareProvider implements IShareProviderWithNotification { - /** - * Return the identifier of this provider. - * - * @return string Containing only [a-zA-Z0-9] - */ + public function __construct( + private readonly IConfig $config, + private readonly IDBConnection $dbConnection, + private readonly ISecureRandom $secureRandom, + private readonly IUserManager $userManager, + private readonly IRootFolder $rootFolder, + private readonly IL10N $l, + private readonly LoggerInterface $logger, + private readonly IMailer $mailer, + private readonly IURLGenerator $urlGenerator, + private readonly IManager $activityManager, + private readonly SettingsManager $settingsManager, + private readonly Defaults $defaults, + private readonly IHasher $hasher, + private readonly IEventDispatcher $eventDispatcher, + private readonly IShareManager $shareManager, + private readonly IEmailValidator $emailValidator, + ) { + } + + #[Override] public function identifier(): string { return 'ocMailShare'; } - public function __construct( - private IConfig $config, - private IDBConnection $dbConnection, - private ISecureRandom $secureRandom, - private IUserManager $userManager, - private IRootFolder $rootFolder, - private IL10N $l, - private LoggerInterface $logger, - private IMailer $mailer, - private IURLGenerator $urlGenerator, - private IManager $activityManager, - private SettingsManager $settingsManager, - private Defaults $defaults, - private IHasher $hasher, - private IEventDispatcher $eventDispatcher, - private IShareManager $shareManager, - private IEmailValidator $emailValidator, - ) { + #[Override] + public function getShareTypes(): array { + return [IShare::TYPE_EMAIL]; } - /** - * Share a path - * - * @throws ShareNotFound - * @throws \Exception - */ + #[Override] + public function getTokenShareTypes(): array { + return [IShare::TYPE_EMAIL]; + } + + #[Override] public function create(IShare $share): IShare { $shareWith = $share->getSharedWith(); // Check if file is not already shared with the given email, @@ -797,97 +800,17 @@ public function restore(IShare $share, string $recipient): IShare { throw new GenericShareException('not implemented'); } - /** - * @inheritdoc - */ + #[Override] public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset): array { - $qb = $this->dbConnection->getQueryBuilder(); - $qb->select('*') - ->from('share'); - - $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))); - - /** - * Reshares for this user are shares where they are the owner. - */ - if ($reshares === false) { - //Special case for old shares created via the web UI - $or1 = $qb->expr()->andX( - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), - $qb->expr()->isNull('uid_initiator') - ); - - $qb->andWhere( - $qb->expr()->orX( - $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)), - $or1 - ) - ); - } elseif ($node === null) { - $qb->andWhere( - $qb->expr()->orX( - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), - $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) - ) - ); - } - - if ($node !== null) { - $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); - } - - if ($limit !== -1) { - $qb->setMaxResults($limit); - } - - $qb->setFirstResult($offset); - $qb->orderBy('id'); - - $cursor = $qb->executeQuery(); - $shares = []; - while ($data = $cursor->fetchAssociative()) { - $shares[] = $this->createShareObject($data); - } - $cursor->closeCursor(); - - return $shares; + throw new LogicException('Is no longer used'); } - /** - * @inheritdoc - */ + #[Override] public function getShareById($id, $recipientId = null): IShare { - $qb = $this->dbConnection->getQueryBuilder(); - - $qb->select('*') - ->from('share') - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) - ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))); - - $cursor = $qb->executeQuery(); - $data = $cursor->fetchAssociative(); - $cursor->closeCursor(); - - if ($data === false) { - throw new ShareNotFound(); - } - - $data['id'] = (string)$data['id']; - - try { - $share = $this->createShareObject($data); - } catch (InvalidShare $e) { - throw new ShareNotFound(); - } - - return $share; + throw new LogicException('Is no longer used'); } - /** - * Get shares for a given path - * - * @return IShare[] - */ + #[Override] public function getSharesByPath(Node $path): array { $qb = $this->dbConnection->getQueryBuilder(); @@ -907,9 +830,7 @@ public function getSharesByPath(Node $path): array { return $shares; } - /** - * @inheritdoc - */ + #[Override] public function getSharedWith($userId, $shareType, $node, $limit, $offset): array { /** @var IShare[] $shares */ $shares = []; @@ -947,35 +868,9 @@ public function getSharedWith($userId, $shareType, $node, $limit, $offset): arra return $shares; } - /** - * Get a share by token - * - * @throws ShareNotFound - */ - public function getShareByToken($token): IShare { - $qb = $this->dbConnection->getQueryBuilder(); - - $cursor = $qb->select('*') - ->from('share') - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))) - ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token))) - ->executeQuery(); - - $data = $cursor->fetchAssociative(); - - if ($data === false) { - throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); - } - - $data['id'] = (string)$data['id']; - - try { - $share = $this->createShareObject($data); - } catch (InvalidShare $e) { - throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); - } - - return $share; + #[Override] + public function getShareByToken(string $token): never { + throw new LogicException('Is no longer used'); } /** @@ -1202,29 +1097,7 @@ public function getAccessList($nodes, $currentAccess): array { } public function getAllShares(): iterable { - $qb = $this->dbConnection->getQueryBuilder(); - - $qb->select('*') - ->from('share') - ->where( - $qb->expr()->orX( - $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)) - ) - ); - - $cursor = $qb->executeQuery(); - while ($data = $cursor->fetchAssociative()) { - try { - $share = $this->createShareObject($data); - } catch (InvalidShare $e) { - continue; - } catch (ShareNotFound $e) { - continue; - } - - yield $share; - } - $cursor->closeCursor(); + throw new \LogicException('getAllShare in DefaultShareProvider should no longer be used'); } /** diff --git a/lib/private/Share20/DefaultShareProvider.php b/lib/private/Share20/DefaultShareProvider.php index fa2445524d2dd..4bfac0f593c2f 100644 --- a/lib/private/Share20/DefaultShareProvider.php +++ b/lib/private/Share20/DefaultShareProvider.php @@ -18,6 +18,7 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Defaults; use OCP\Files\Folder; +use OCP\Files\IMimeTypeLoader; use OCP\Files\IRootFolder; use OCP\Files\Node; use OCP\IConfig; @@ -29,8 +30,10 @@ use OCP\IUserManager; use OCP\L10N\IFactory; use OCP\Mail\IMailer; +use OCP\Server; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IAttributes; +use OCP\Share\ICreateShareProvider; use OCP\Share\IManager; use OCP\Share\IPartialShareProvider; use OCP\Share\IShare; @@ -38,6 +41,7 @@ use OCP\Share\IShareProviderSupportsAccept; use OCP\Share\IShareProviderSupportsAllSharesInFolder; use OCP\Share\IShareProviderWithNotification; +use Override; use Psr\Log\LoggerInterface; use function str_starts_with; @@ -51,41 +55,41 @@ class DefaultShareProvider implements IShareProviderSupportsAccept, IShareProviderSupportsAllSharesInFolder, IShareProviderGetUsers, - IPartialShareProvider { + IPartialShareProvider, + ICreateShareProvider { public function __construct( - private IDBConnection $dbConn, - private IUserManager $userManager, - private IGroupManager $groupManager, - private IRootFolder $rootFolder, - private IMailer $mailer, - private Defaults $defaults, - private IFactory $l10nFactory, - private IURLGenerator $urlGenerator, - private ITimeFactory $timeFactory, - private LoggerInterface $logger, - private IManager $shareManager, - private IConfig $config, + private readonly IDBConnection $dbConn, + private readonly IUserManager $userManager, + private readonly IGroupManager $groupManager, + private readonly IRootFolder $rootFolder, + private readonly IMailer $mailer, + private readonly Defaults $defaults, + private readonly IFactory $l10nFactory, + private readonly IURLGenerator $urlGenerator, + private readonly ITimeFactory $timeFactory, + private readonly LoggerInterface $logger, + private readonly IManager $shareManager, + private readonly IConfig $config, ) { } - /** - * Return the identifier of this provider. - * - * @return string Containing only [a-zA-Z0-9] - */ - public function identifier() { + #[Override] + public function identifier(): string { return 'ocinternal'; } - /** - * Share a path - * - * @param \OCP\Share\IShare $share - * @return \OCP\Share\IShare The share object - * @throws ShareNotFound - * @throws \Exception - */ - public function create(\OCP\Share\IShare $share) { + #[Override] + public function getShareTypes(): array { + return [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK]; + } + + #[Override] + public function getTokenShareTypes(): array { + return [IShare::TYPE_LINK]; + } + + #[Override] + public function create(IShare $share): IShare { $qb = $this->dbConn->getQueryBuilder(); $qb->insert('share'); @@ -195,16 +199,8 @@ public function create(\OCP\Share\IShare $share) { return $share; } - /** - * Update a share - * - * @param \OCP\Share\IShare $share - * @return \OCP\Share\IShare The share object - * @throws ShareNotFound - * @throws \OCP\Files\InvalidPathException - * @throws \OCP\Files\NotFoundException - */ - public function update(\OCP\Share\IShare $share) { + #[Override] + public function update(IShare $share): IShare { $originalShare = $this->getShareById($share->getId()); $shareAttributes = $this->formatShareAttributes($share->getAttributes()); @@ -301,14 +297,7 @@ public function update(\OCP\Share\IShare $share) { return $share; } - /** - * Accept a share. - * - * @param IShare $share - * @param string $recipient - * @return IShare The share object - * @since 9.0.0 - */ + #[Override] public function acceptShare(IShare $share, string $recipient): IShare { if ($share->getShareType() === IShare::TYPE_GROUP) { $group = $this->groupManager->get($share->getSharedWith()); @@ -392,12 +381,8 @@ public function getChildren(IShare $parent): array { return $children; } - /** - * Delete a share - * - * @param \OCP\Share\IShare $share - */ - public function delete(\OCP\Share\IShare $share) { + #[Override] + public function delete(IShare $share) { $qb = $this->dbConn->getQueryBuilder(); $qb->delete('share') ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))); @@ -549,7 +534,7 @@ public function restore(IShare $share, string $recipient): IShare { /** * @inheritdoc */ - public function move(\OCP\Share\IShare $share, $recipient) { + public function move(IShare $share, $recipient) { if ($share->getShareType() === IShare::TYPE_USER) { // Just update the target $qb = $this->dbConn->getQueryBuilder(); @@ -688,9 +673,7 @@ private function getSharesInFolderInternal(?string $userId, Folder $node, ?bool return $shares; } - /** - * @inheritdoc - */ + #[Override] public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) { $qb = $this->dbConn->getQueryBuilder(); $qb->select('*') @@ -736,10 +719,8 @@ public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offs return $shares; } - /** - * @inheritdoc - */ - public function getShareById($id, $recipientId = null) { + #[Override] + public function getShareById(string $id, $recipientId = null): IShare { $qb = $this->dbConn->getQueryBuilder(); $qb->select('*') @@ -783,7 +764,7 @@ public function getShareById($id, $recipientId = null) { * Get shares for a given path * * @param \OCP\Files\Node $path - * @return \OCP\Share\IShare[] + * @return IShare[] */ public function getSharesByPath(Node $path) { $qb = $this->dbConn->getQueryBuilder(); @@ -998,45 +979,13 @@ private function _getSharedWith( return $shares; } - /** - * Get a share by token - * - * @param string $token - * @return \OCP\Share\IShare - * @throws ShareNotFound - */ - public function getShareByToken($token) { - $qb = $this->dbConn->getQueryBuilder(); - - $cursor = $qb->select('*') - ->from('share') - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_LINK))) - ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token))) - ->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))) - ->executeQuery(); - - $data = $cursor->fetch(); - - if ($data === false) { - throw new ShareNotFound(); - } - - try { - $share = $this->createShare($data); - } catch (InvalidShare $e) { - throw new ShareNotFound(); - } - - return $share; + #[Override] + public function getShareByToken(string $token): never { + throw new \LogicException('Should no longer be called directly, instead use IManager::getShareByToken'); } - /** - * Create a share object from a database row - * - * @param array $data - * @throws InvalidShare - */ - private function createShare($data): IShare { + #[Override] + public function createShare(array $data): IShare { $share = new Share($this->rootFolder, $this->userManager); $share->setId($data['id']) ->setShareType((int)$data['share_type']) @@ -1087,7 +1036,7 @@ private function createShare($data): IShare { $entryData['permissions'] = $entryData['f_permissions']; $entryData['parent'] = $entryData['f_parent']; $share->setNodeCacheEntry(Cache::cacheEntryFromData($entryData, - \OC::$server->getMimeTypeLoader())); + Server::get(IMimeTypeLoader::class))); } $share->setProviderId($this->identifier()); @@ -1664,24 +1613,9 @@ private function sendNote(array $recipients, IShare $share) { } } + #[Override] public function getAllShares(): iterable { - $qb = $this->dbConn->getQueryBuilder(); - - $qb->select('*') - ->from('share') - ->where($qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK], IQueryBuilder::PARAM_INT_ARRAY))); - - $cursor = $qb->executeQuery(); - while ($data = $cursor->fetch()) { - try { - $share = $this->createShare($data); - } catch (InvalidShare $e) { - continue; - } - - yield $share; - } - $cursor->closeCursor(); + throw new \LogicException('getAllShare in DefaultShareProvider should no longer be used'); } /** diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 61778f12d6aac..ac062294a2e4e 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -37,6 +37,7 @@ use OCP\IUserManager; use OCP\IUserSession; use OCP\L10N\IFactory; +use OCP\PaginationParameters; use OCP\Security\Events\ValidatePasswordPolicyEvent; use OCP\Security\IHasher; use OCP\Security\ISecureRandom; @@ -51,6 +52,7 @@ use OCP\Share\Exceptions\GenericShareException; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\Exceptions\ShareTokenException; +use OCP\Share\ICreateShareProvider; use OCP\Share\IManager; use OCP\Share\IPartialShareProvider; use OCP\Share\IProviderFactory; @@ -195,8 +197,8 @@ protected function generalCreateChecks(IShare $share, bool $isUpdate = false): v } // And it should be a file or a folder - if (!($share->getNode() instanceof \OCP\Files\File) - && !($share->getNode() instanceof \OCP\Files\Folder)) { + if (!($share->getNode() instanceof File) + && !($share->getNode() instanceof Folder)) { throw new \InvalidArgumentException($this->l->t('Shared path must be either a file or a folder')); } @@ -258,7 +260,7 @@ protected function generalCreateChecks(IShare $share, bool $isUpdate = false): v throw new \InvalidArgumentException($this->l->t('Shares need at least read permissions')); } - if ($share->getNode() instanceof \OCP\Files\File) { + if ($share->getNode() instanceof File) { if ($share->getPermissions() & \OCP\Constants::PERMISSION_DELETE) { throw new GenericShareException($this->l->t('Files cannot be shared with delete permissions')); } @@ -585,7 +587,7 @@ protected function setLinkParent(IShare $share): void { protected function pathCreateChecks(Node $path): void { // Make sure that we do not share a path that contains a shared mountpoint - if ($path instanceof \OCP\Files\Folder) { + if ($path instanceof Folder) { $mounts = $this->mountManager->findIn($path->getPath()); foreach ($mounts as $mount) { if ($mount->getStorage()->instanceOfStorage('\OCA\Files_Sharing\ISharedStorage')) { @@ -853,7 +855,7 @@ public function updateShare(IShare $share, bool $onlyValid = true): IShare { if ($expirationDateUpdated === true) { \OC_Hook::emit(Share::class, 'post_set_expiration_date', [ - 'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder', + 'itemType' => $share->getNode() instanceof File ? 'file' : 'folder', 'itemSource' => $share->getNode()->getId(), 'date' => $share->getExpirationDate(), 'uidOwner' => $share->getSharedBy(), @@ -862,7 +864,7 @@ public function updateShare(IShare $share, bool $onlyValid = true): IShare { if ($share->getPassword() !== $originalShare->getPassword()) { \OC_Hook::emit(Share::class, 'post_update_password', [ - 'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder', + 'itemType' => $share->getNode() instanceof File ? 'file' : 'folder', 'itemSource' => $share->getNode()->getId(), 'uidOwner' => $share->getSharedBy(), 'token' => $share->getToken(), @@ -877,7 +879,7 @@ public function updateShare(IShare $share, bool $onlyValid = true): IShare { $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); } \OC_Hook::emit(Share::class, 'post_update_permissions', [ - 'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder', + 'itemType' => $share->getNode() instanceof File ? 'file' : 'folder', 'itemSource' => $share->getNode()->getId(), 'shareType' => $share->getShareType(), 'shareWith' => $share->getSharedWith(), @@ -1222,11 +1224,95 @@ public function getSharesInFolder($userId, Folder $node, bool $reshares = false, return $shares; } + #[Override] + public function getAllSharesBy(string $userId, ?Node $node = null, PaginationParameters $paginationParameters, bool $reshares = false, bool $onlyValid = true): array { + if ($node !== null && !$node instanceof File && !$node instanceof Folder) { + throw new \InvalidArgumentException($this->l->t('Invalid path')); + } + + // Get all shares from the providers supporting createShare + $providers = $this->factory->getAllProviders(); + $createShareProviders = array_filter($providers, fn (IShareProvider $provider) => $provider instanceof ICreateShareProvider); + $shareTypes = array_unique(array_merge(...array_map(fn (ICreateShareProvider $provider): array => $provider->getTokenShareTypes(), $createShareProviders))); + + if ($node?->getMountPoint() instanceof IShareOwnerlessMount) { + return $this->getSharesByPath($node, $shareTypes, $paginationParameters); + } + + $qb = $this->connection->getQueryBuilder(); + $qb->select('*') + ->from('share') + ->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter($shareTypes, IQueryBuilder::PARAM_INT_ARRAY))); + + /** + * Reshares for this user are shares where they are the owner. + */ + if ($reshares === false) { + //Special case for old shares created via the web UI + $or1 = $qb->expr()->andX( + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), + $qb->expr()->isNull('uid_initiator') + ); + + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)), + $or1 + ) + ); + } elseif ($node === null) { + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) + ) + ); + } + + if ($node !== null) { + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); + } + + $paginationParameters->fillQuery($qb, 'id'); + + $cursor = $qb->executeQuery(); + while ($data = $cursor->fetchAssociative()) { + $provider = $this->factory->getProviderForType((int)$data['share_type']); + if ($provider instanceof ICreateShareProvider) { + throw new \LogicException('Share type ' . $data['share_type'] . " doesn't have a corresponding ICreateShareProvider."); + } + $share = $provider->createShare($data); + try { + $this->checkShare($share, $added); + } catch (ShareNotFound $e) { + // Ignore since this basically means the share is deleted + continue; + } + } + $cursor->closeCursor(); + + // Get all the other shares from the providers not supporting createShare + $shares = []; + foreach ([IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_EMAIL, IShare::TYPE_CIRCLE, IShare::TYPE_ROOM, IShare::TYPE_DECK] as $shareType) { + if (!in_array($shareType, $shareTypes)) { + $shares = array_merge($shares, $this->getSharesBy($userId, $shareType, $node, $reshares, -1, 0)); + } + } + + // FEDERATION + if ($this->outgoingServer2ServerSharesAllowed() && !in_array(IShare::TYPE_REMOTE, $shareTypes)) { + $shares = array_merge($shares, $this->getSharesBy($userId, IShare::TYPE_REMOTE, $node, $reshares, -1, 0)); + } + if ($this->outgoingServer2ServerGroupSharesAllowed() && !in_array(IShare::TYPE_REMOTE_GROUP, $shareTypes)) { + $shares = array_merge($shares, $this->getSharesBy($userId, IShare::TYPE_REMOTE_GROUP, $node, $reshares, -1, 0)); + } + + return $shares; + } + #[Override] public function getSharesBy(string $userId, int $shareType, ?Node $path = null, bool $reshares = false, int $limit = 50, int $offset = 0, bool $onlyValid = true): array { - if ($path !== null - && !($path instanceof \OCP\Files\File) - && !($path instanceof \OCP\Files\Folder)) { + if ($path !== null && !($path instanceof File) && !($path instanceof Folder)) { throw new \InvalidArgumentException($this->l->t('Invalid path')); } @@ -1299,8 +1385,70 @@ public function getSharesBy(string $userId, int $shareType, ?Node $path = null, } } - $shares = $shares2; + return $shares2; + } + + #[Override] + public function getAllSharedWith(string $userId, array $shareTypes, ?Node $node, PaginationParameters $paginationParameters, bool $ignoreWithSelf = false): array { + $shareTypes = []; + $noCreateShareProvider = []; + foreach ($this->factory->getAllProviders() as $provider) { + if ($provider instanceof ICreateShareProvider) { + $shareTypes = array_merge($provider->getShareTypes(), $shareTypes); + } else { + $noCreateShareProvider[] = $provider; + } + } + $shareTypes = array_unique($shareTypes); + + // Get shares directly with this user + $qb = $this->connection->getQueryBuilder(); + $qb->select('*') + ->from('share'); + + $qb->where($qb->expr()->in('share_type', $qb->createNamedParameter($shareTypes, IQueryBuilder::PARAM_INT_ARRAY))); + $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId))); + if ($node !== null) { + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); + } + + if ($ignoreWithSelf) { + $qb->andWhere($qb->expr()->neq('uid_owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))); + $qb->andWhere($qb->expr()->neq('uid_initiator', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))); + } + + $paginationParameters->fillQuery($qb, 'id'); + + $result = $qb->executeQuery(); + $shares = []; + foreach ($result->fetchAssociative() as $data) { + try { + $provider = $this->factory->getProviderForType($data['share_type']); + } catch (ProviderException $e) { + continue; + } + if (!$provider instanceof ICreateShareProvider) { + throw new \LogicException($this->l->t('Invalid provider')); + } + $shares[] = $provider->createShare($data); + } + + // Legacy for providers what don't support ICreateShareProvider + foreach ($noCreateShareProvider as $shareType) { + // TODO fix pagination + $unverifiedShares = $provider->getSharedWith($userId, $shareType, $node, 0, 0); + + // remove all shares which are already expired + foreach ($unverifiedShares as $key => $share) { + try { + $this->checkShare($share); + } catch (ShareNotFound $e) { + unset($shares[$key]); + } + $shares[] = $share; + } + } return $shares; } @@ -1326,9 +1474,7 @@ public function getSharedWith(string $userId, int $shareType, ?Node $node = null return $shares; } - /** - * @inheritDoc - */ + #[Override] public function getSharedWithByPath(string $userId, int $shareType, string $path, bool $forChildren, int $limit = 50, int $offset = 0): iterable { try { $provider = $this->factory->getProviderForType($shareType); @@ -1368,6 +1514,7 @@ public function getSharedWithByPath(string $userId, int $shareType, string $path #[Override] public function getDeletedSharedWith(string $userId, int $shareType, ?Node $node = null, int $limit = 50, int $offset = 0): array { + // TODO: Remove, it is no longer used. $shares = $this->getSharedWith($userId, $shareType, $node, $limit, $offset); // Only get shares deleted shares and where the owner still exists @@ -1376,11 +1523,16 @@ public function getDeletedSharedWith(string $userId, int $shareType, ?Node $node } #[Override] - public function getShareById($id, $recipient = null, bool $onlyValid = true): IShare { - if ($id === null) { - throw new ShareNotFound(); - } + public function getAllDeletedSharedWith(string $userId, array $shareTypes, ?Node $node = null, PaginationParameters $paginationParameters): array { + $shares = $this->getAllSharedWith($userId, $shareTypes, $node, $paginationParameters); + + // Only get shares deleted shares and where the owner still exists + return array_filter($shares, fn (IShare $share): bool => $share->getPermissions() === 0 + && $this->userManager->userExists($share->getShareOwner())); + } + #[Override] + public function getShareById(string $id, ?string $recipient = null, bool $onlyValid = true): IShare { [$providerId, $id] = $this->splitFullId($id); try { @@ -1389,6 +1541,31 @@ public function getShareById($id, $recipient = null, bool $onlyValid = true): IS throw new ShareNotFound(); } + if ($provider instanceof ICreateShareProvider) { + $qb = $this->connection->getQueryBuilder(); + + $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter($provider->getShareTypes(), IQueryBuilder::PARAM_INT_ARRAY))); + + $cursor = $qb->executeQuery(); + $data = $cursor->fetchAssociative(); + $cursor->closeCursor(); + + if ($data === false) { + throw new ShareNotFound('Can not find share with ID: ' . $id); + } + + $share = $provider->createShare($data); + + if ($onlyValid) { + $this->checkShare($share); + } + + return $share; + } + $share = $provider->getShareById($id, $recipient); if ($onlyValid) { @@ -1398,24 +1575,87 @@ public function getShareById($id, $recipient = null, bool $onlyValid = true): IS return $share; } + /** + * @param list $shareTypes + * @return list + */ + private function getSharesByPath(Node $path, array $shareTypes, PaginationParameters $paginationParameters): array { + $qb = $this->connection->getQueryBuilder(); + + $qb = $qb->select('*') + ->from('share') + ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId()))) + ->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter($shareTypes, IQueryBuilder::PARAM_INT_ARRAY))); + $paginationParameters->fillQuery($qb, 'id'); + $cursor = $qb->executeQuery(); + + $shares = []; + while ($data = $cursor->fetchAssociative()) { + $provider = $this->factory->getProvider((int)$data['share_type']); + if ($provider instanceof ICreateShareProvider) { + $shares[] = $provider->createShare($data); + } else { + $shares[] = $provider->getShareById((int)$data['id']); + } + } + $cursor->closeCursor(); + return $shares; + } + + /** + * @return array{?IShare, list} + */ + private function getShareByTokenOptimized(string $token): array { + $providers = $this->factory->getAllProviders(); + // Get all shares from the providers supporting createShare + $createShareProviders = array_filter($providers, fn (IShareProvider $provider) => $provider instanceof ICreateShareProvider); + $shareTypes = array_unique(array_merge(...array_map(fn (ICreateShareProvider $provider): array => $provider->getTokenShareTypes(), $createShareProviders))); + if (!$this->appConfig->getValueBool('core', 'shareapi_allow_links', true)) { + $shareTypes = array_filter($shareTypes, fn (int $shareType): bool => $shareType !== IShare::TYPE_LINK); + } + $qb = $this->connection->getQueryBuilder(); + $result = $qb->select('*') + ->from('share') + ->where($qb->expr()->in('share_type', $qb->createNamedParameter($shareTypes))) + ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token))) + ->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))) + ->executeQuery(); + + $data = $result->fetch(); + if ($data === false) { + return [null, $shareTypes]; + } + $provider = $this->factory->getProviderForType((int)$data['share_type']); + if (!$provider instanceof ICreateShareProvider) { + throw new \LogicException('Share type ' . $data['share_type'] . " doesn't have a corresponding ICreateShareProvider."); + } + $data['id'] = (string)$data['id']; + $share = $provider->createShare($data); + return [$share, $shareTypes]; + } + #[Override] public function getShareByToken(string $token): IShare { // tokens cannot be valid local usernames if ($this->userManager->userExists($token)) { throw new ShareNotFound(); } - $share = null; - try { - if ($this->config->getAppValue('core', 'shareapi_allow_links', 'yes') === 'yes') { + + [$share, $testedShareTypes] = $this->getShareByTokenOptimized($token); + if ($share !== null) { + return $share; + } + + if (!in_array(IShare::TYPE_LINK, $testedShareTypes) && $this->appConfig->getValueBool('core', 'shareapi_allow_links', true)) { + try { $provider = $this->factory->getProviderForType(IShare::TYPE_LINK); $share = $provider->getShareByToken($token); + } catch (ProviderException|ShareNotFound) { } - } catch (ProviderException|ShareNotFound) { } - // If it is not a link share try to fetch a federated share by token - if ($share === null) { + if ($share === null && !in_array(IShare::TYPE_REMOTE, $testedShareTypes)) { try { $provider = $this->factory->getProviderForType(IShare::TYPE_REMOTE); $share = $provider->getShareByToken($token); @@ -1424,7 +1664,7 @@ public function getShareByToken(string $token): IShare { } // If it is not a link share try to fetch a mail share by token - if ($share === null && $this->shareProviderExists(IShare::TYPE_EMAIL)) { + if ($share === null && !in_array(IShare::TYPE_REMOTE, $testedShareTypes) && $this->shareProviderExists(IShare::TYPE_EMAIL)) { try { $provider = $this->factory->getProviderForType(IShare::TYPE_EMAIL); $share = $provider->getShareByToken($token); @@ -1432,7 +1672,7 @@ public function getShareByToken(string $token): IShare { } } - if ($share === null && $this->shareProviderExists(IShare::TYPE_CIRCLE)) { + if ($share === null && !in_array(IShare::TYPE_REMOTE, $testedShareTypes) && $this->shareProviderExists(IShare::TYPE_CIRCLE)) { try { $provider = $this->factory->getProviderForType(IShare::TYPE_CIRCLE); $share = $provider->getShareByToken($token); @@ -1440,7 +1680,7 @@ public function getShareByToken(string $token): IShare { } } - if ($share === null && $this->shareProviderExists(IShare::TYPE_ROOM)) { + if ($share === null && !in_array(IShare::TYPE_REMOTE, $testedShareTypes) && $this->shareProviderExists(IShare::TYPE_ROOM)) { try { $provider = $this->factory->getProviderForType(IShare::TYPE_ROOM); $share = $provider->getShareByToken($token); @@ -1671,21 +1911,21 @@ public function newShare(): IShare { #[Override] public function shareApiEnabled(): bool { - return $this->config->getAppValue('core', 'shareapi_enabled', 'yes') === 'yes'; + return $this->appConfig->getValueBool('core', 'shareapi_enabled', true); } #[Override] public function shareApiAllowLinks(?IUser $user = null): bool { - if ($this->config->getAppValue('core', 'shareapi_allow_links', 'yes') !== 'yes') { + if (!$this->appConfig->getValueBool('core', 'shareapi_allow_links', true)) { return false; } $user = $user ?? $this->userSession->getUser(); if ($user) { - $excludedGroups = json_decode($this->config->getAppValue('core', 'shareapi_allow_links_exclude_groups', '[]')); + $excludedGroups = $this->appConfig->getValueArray('core', 'shareapi_allow_links_exclude_groups'); if ($excludedGroups) { $userGroups = $this->groupManager->getUserGroupIds($user); - return !(bool)array_intersect($excludedGroups, $userGroups); + return !array_intersect($excludedGroups, $userGroups); } } @@ -1704,13 +1944,12 @@ protected function userCanCreateLinkShares(IUser $user): bool { #[Override] public function shareApiLinkEnforcePassword(bool $checkGroupMembership = true): bool { - $excludedGroups = $this->config->getAppValue('core', 'shareapi_enforce_links_password_excluded_groups', ''); - if ($excludedGroups !== '' && $checkGroupMembership) { - $excludedGroups = json_decode($excludedGroups); + $excludedGroups = $this->appConfig->getValueArray('core', 'shareapi_enforce_links_password_excluded_groups'); + if ($excludedGroups !== [] && $checkGroupMembership) { $user = $this->userSession->getUser(); if ($user) { $userGroups = $this->groupManager->getUserGroupIds($user); - if ((bool)array_intersect($excludedGroups, $userGroups)) { + if (array_intersect($excludedGroups, $userGroups)) { return false; } } @@ -1731,49 +1970,49 @@ public function shareApiLinkDefaultExpireDateEnforced(): bool { #[Override] public function shareApiLinkDefaultExpireDays(): int { - return (int)$this->config->getAppValue('core', 'shareapi_expire_after_n_days', '7'); + return $this->appConfig->getValueInt('core', 'shareapi_expire_after_n_days', 7); } #[Override] public function shareApiInternalDefaultExpireDate(): bool { - return $this->config->getAppValue('core', 'shareapi_default_internal_expire_date', 'no') === 'yes'; + return $this->appConfig->getValueBool('core', 'shareapi_default_internal_expire_date'); } #[Override] public function shareApiRemoteDefaultExpireDate(): bool { - return $this->config->getAppValue('core', 'shareapi_default_remote_expire_date', 'no') === 'yes'; + return $this->appConfig->getValueBool('core', 'shareapi_default_remote_expire_date'); } #[Override] public function shareApiInternalDefaultExpireDateEnforced(): bool { return $this->shareApiInternalDefaultExpireDate() - && $this->config->getAppValue('core', 'shareapi_enforce_internal_expire_date', 'no') === 'yes'; + && $this->appConfig->getValueBool('core', 'shareapi_enforce_internal_expire_date'); } #[Override] public function shareApiRemoteDefaultExpireDateEnforced(): bool { return $this->shareApiRemoteDefaultExpireDate() - && $this->config->getAppValue('core', 'shareapi_enforce_remote_expire_date', 'no') === 'yes'; + && $this->appConfig->getValueBool('core', 'shareapi_enforce_remote_expire_date'); } #[Override] public function shareApiInternalDefaultExpireDays(): int { - return (int)$this->config->getAppValue('core', 'shareapi_internal_expire_after_n_days', '7'); + return $this->appConfig->getValueInt('core', 'shareapi_internal_expire_after_n_days', 7); } #[Override] public function shareApiRemoteDefaultExpireDays(): int { - return (int)$this->config->getAppValue('core', 'shareapi_remote_expire_after_n_days', '7'); + return $this->appConfig->getValueInt('core', 'shareapi_remote_expire_after_n_days', 7); } #[Override] public function shareApiLinkAllowPublicUpload(): bool { - return $this->config->getAppValue('core', 'shareapi_allow_public_upload', 'yes') === 'yes'; + return $this->appConfig->getValueBool('core', 'shareapi_allow_public_upload', true); } #[Override] public function shareWithGroupMembersOnly(): bool { - return $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes'; + return $this->appConfig->getValueBool('core', 'shareapi_only_share_with_group_members'); } #[Override] @@ -1787,29 +2026,29 @@ public function shareWithGroupMembersOnlyExcludeGroupsList(): array { #[Override] public function allowGroupSharing(): bool { - return $this->config->getAppValue('core', 'shareapi_allow_group_sharing', 'yes') === 'yes'; + return $this->appConfig->getValueBool('core', 'shareapi_allow_group_sharing', true); } #[Override] public function allowEnumeration(): bool { - return $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; + return $this->appConfig->getValueBool('core', 'shareapi_allow_share_dialog_user_enumeration', true); } #[Override] public function limitEnumerationToGroups(): bool { return $this->allowEnumeration() - && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; + && $this->appConfig->getValueBool('core', 'shareapi_restrict_user_enumeration_to_group'); } #[Override] public function limitEnumerationToPhone(): bool { return $this->allowEnumeration() - && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes'; + && $this->appConfig->getValueBool('core', 'shareapi_restrict_user_enumeration_to_phone'); } #[Override] public function allowEnumerationFullMatch(): bool { - return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes'; + return $this->appConfig->getValueBool('core', 'shareapi_restrict_user_enumeration_full_match', true); } #[Override] @@ -1912,7 +2151,32 @@ public function registerShareProvider(string $shareProviderClass): void { public function getAllShares(): iterable { $providers = $this->factory->getAllProviders(); - foreach ($providers as $provider) { + // Get all shares from the providers supporting createShare + $createShareProviders = array_filter($providers, fn (IShareProvider $provider) => $provider instanceof ICreateShareProvider); + $shareTypes = array_unique(array_merge(...array_map(fn (ICreateShareProvider $provider): array => $provider->getShareTypes(), $createShareProviders))); + $qb = $this->connection->getQueryBuilder(); + $result = $qb->select('*') + ->from('share') + ->where($qb->expr()->in('share_type', $qb->createNamedParameter($shareTypes, IQueryBuilder::PARAM_INT_ARRAY))) + ->executeQuery(); + /** @var array $providers */ + $providers = []; + foreach ($result->iterateAssociative() as $row) { + if (!isset($providers[$row['share_type']])) { + $providers[$row['share_type']] = $this->factory->getProviderForType($row['share_type']); + } + + $provider = $providers[$row['share_type']]; + if (!$provider instanceof ICreateShareProvider) { + throw new \LogicException('Share type ' . $row['share_type'] . " doesn't have a corresponding ICreateShareProvider."); + } + + yield $provider->createShare($row); + } + + // Get all shares from the other providers + $noCreateShareProviders = array_filter($providers, fn (IShareProvider $provider) => !$provider instanceof ICreateShareProvider); + foreach ($noCreateShareProviders as $provider) { yield from $provider->getAllShares(); } } diff --git a/lib/public/PaginationParameters.php b/lib/public/PaginationParameters.php new file mode 100644 index 0000000000000..b8dc2656bcb73 --- /dev/null +++ b/lib/public/PaginationParameters.php @@ -0,0 +1,73 @@ +minId !== null) { + // paginate by min id (last entries added in the table last) + $qb->andWhere($qb->expr()->gt($idColumn, $this->minId)); + if ($this->maxId !== null) { + $qb->andWhere($qb->expr()->lt($idColumn, $this->maxId)); + } + $qb->orderBy($idColumn, 'ASC'); + $qb->setMaxResults($this->limit); + return $qb; + } else { + // paginate by max id (last entries added in the table first) + if ($this->maxId !== null) { + $qb->andWhere($qb->expr()->lt($idColumn, $this->maxId)); + } + if ($this->sinceId !== null) { + $qb->andWhere($qb->expr()->gt($idColumn, $this->sinceId)); + } + $qb->orderBy($idColumn, 'DESC'); + $qb->setMaxResults($this->limit); + return $qb; + } + } +} diff --git a/lib/public/Share/ICreateShareProvider.php b/lib/public/Share/ICreateShareProvider.php new file mode 100644 index 0000000000000..6674ebfbe3447 --- /dev/null +++ b/lib/public/Share/ICreateShareProvider.php @@ -0,0 +1,42 @@ + $data + * @return IShare $share + * @since 34.0.0 + */ + public function createShare(array $data): IShare; + + /** + * @return list + * @since 34.0.0 + */ + public function getShareTypes(): array; + + /** + * @return list + * @since 34.0.0 + */ + public function getTokenShareTypes(): array; +} diff --git a/lib/public/Share/IManager.php b/lib/public/Share/IManager.php index 9abaff980f27f..4fa021c4512b7 100644 --- a/lib/public/Share/IManager.php +++ b/lib/public/Share/IManager.php @@ -12,6 +12,7 @@ use OCP\Files\Node; use OCP\IUser; +use OCP\PaginationParameters; use OCP\Share\Exceptions\GenericShareException; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\Exceptions\ShareTokenException; @@ -115,11 +116,23 @@ public function getSharesInFolder(string $userId, Folder $node, bool $reshares = * @param int $limit The maximum number of returned results, -1 for all results * @param int $offset * @param bool $onlyValid Only returns valid shares, invalid shares will be deleted automatically and are not returned - * @return IShare[] + * @return list * @since 9.0.0 */ public function getSharesBy(string $userId, int $shareType, ?Node $path = null, bool $reshares = false, int $limit = 50, int $offset = 0, bool $onlyValid = true): array; + /** + * Get all shares shared by (initiated) by the provided user. + * + * @param string $userId + * @param Node|null $path + * @param bool $reshares + * @param bool $onlyValid Only returns valid shares, invalid shares will be deleted automatically and are not returned + * @return list + * @since 34.0.0 + */ + public function getAllSharesBy(string $userId, ?Node $node, PaginationParameters $paginationParameters, bool $reshares = false, bool $onlyValid = true): array; + /** * Get shares shared with $user. * Filter by $node if provided @@ -131,9 +144,19 @@ public function getSharesBy(string $userId, int $shareType, ?Node $path = null, * @param int $offset * @return IShare[] * @since 9.0.0 + * @deprecated 34.0.0 Use getAllSharedWith instead, it's more efficient */ public function getSharedWith(string $userId, int $shareType, ?Node $node = null, int $limit = 50, int $offset = 0): array; + /** + * @param string $userId + * @param list $shareTypes + * @param Node|null $node + * @return list + * @since 34.0.0 + */ + public function getAllSharedWith(string $userId, array $shareTypes, ?Node $node, PaginationParameters $paginationParameters, bool $ignoreWithSelf = false): array; + /** * Get shares shared with a $user filtering by $path. * @@ -152,11 +175,23 @@ public function getSharedWithByPath(string $userId, int $shareType, string $path * * @param IShare::TYPE_* $shareType * @param int $limit The maximum number of shares returned, -1 for all - * @return IShare[] + * @return list * @since 14.0.0 + * @deprecated 34.0.0 Use getAllDeletedSharedWith instead */ public function getDeletedSharedWith(string $userId, int $shareType, ?Node $node = null, int $limit = 50, int $offset = 0): array; + /** + * Get all the deleted shares shared with $user. + * + * Additionally filter by $node if provided + * + * @param list $shareTypes + * @return list + * @since 34.0.0 + */ + public function getAllDeletedSharedWith(string $userId, array $shareTypes, ?Node $node = null, PaginationParameters $paginationParameters): array; + /** * Retrieve a share by the share id. * If the recipient is set make sure to retrieve the file for that user. diff --git a/lib/public/Share/IShareProvider.php b/lib/public/Share/IShareProvider.php index c76040fccc5a2..eb2ae63a02924 100644 --- a/lib/public/Share/IShareProvider.php +++ b/lib/public/Share/IShareProvider.php @@ -129,7 +129,7 @@ public function getShareById($id, $recipientId = null); * Get shares for a given path * * @param Node $path - * @return \OCP\Share\IShare[] + * @return list<\OCP\Share\IShare> * @since 9.0.0 */ public function getSharesByPath(Node $path); @@ -138,14 +138,14 @@ public function getSharesByPath(Node $path); * Get shared with the given user * * @param string $userId get shares where this user is the recipient - * @param int $shareType + * @param IShare::TYPE_* $shareType * @param Node|null $node * @param int $limit The max number of entries returned, -1 for all * @param int $offset * @return \OCP\Share\IShare[] * @since 9.0.0 */ - public function getSharedWith($userId, $shareType, $node, $limit, $offset); + public function getSharedWith(string $userId, int $shareType, ?Node $node, int $limit, int $offset); /** * Get a share by token @@ -206,6 +206,7 @@ public function getAccessList($nodes, $currentAccess); * * @return iterable * @since 18.0.0 + * @deprecated 34.0.0 This is no longer needed when implementing ICreateShareProvider */ public function getAllShares(): iterable; diff --git a/tests/lib/Share/Backend.php b/tests/lib/Share/Backend.php index 94ac25111c228..d5b4ef4684cf2 100644 --- a/tests/lib/Share/Backend.php +++ b/tests/lib/Share/Backend.php @@ -9,6 +9,7 @@ namespace Test\Share; use OC\Share20\Manager; +use OCP\PaginationParameters; use OCP\Server; use OCP\Share\IShare; use OCP\Share_Backend; @@ -39,10 +40,7 @@ public function generateTarget($itemSource, $shareWith, $exclude = null) { $shareManager = Server::get(Manager::class); - $shares = array_merge( - $shareManager->getSharedWith($shareWith, IShare::TYPE_USER), - $shareManager->getSharedWith($shareWith, IShare::TYPE_GROUP), - ); + $shares = $shareManager->getAllSharedWith($shareWith, [IShare::TYPE_USER, IShare::TYPE_GROUP], null, new PaginationParameters(limit: null)); $knownTargets = []; foreach ($shares as $share) {