diff --git a/apps/files_sharing/appinfo/info.xml b/apps/files_sharing/appinfo/info.xml index 8eaacf1cb2025..cb643d8904069 100644 --- a/apps/files_sharing/appinfo/info.xml +++ b/apps/files_sharing/appinfo/info.xml @@ -14,7 +14,7 @@ Turning the feature off removes shared files and folders on the server for all share recipients, and also on the sync clients and mobile apps. More information is available in the Nextcloud Documentation. - 1.25.1 + 1.25.2 agpl Michael Gapczynski Bjoern Schiessle diff --git a/apps/files_sharing/lib/External/MountProvider.php b/apps/files_sharing/lib/External/MountProvider.php index 5e132c22903ce..4dbea308963d2 100644 --- a/apps/files_sharing/lib/External/MountProvider.php +++ b/apps/files_sharing/lib/External/MountProvider.php @@ -13,6 +13,7 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Federation\ICloudIdManager; use OCP\Files\Config\IMountProvider; +use OCP\Files\Config\IPartialMountProvider; use OCP\Files\Storage\IStorageFactory; use OCP\Http\Client\IClientService; use OCP\ICertificateManager; @@ -21,7 +22,7 @@ use OCP\Server; use OCP\Share\IShare; -class MountProvider implements IMountProvider { +class MountProvider implements IMountProvider, IPartialMountProvider { public const STORAGE = ExternalShareStorage::class; /** @@ -69,4 +70,54 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader): array { $result->closeCursor(); return $mounts; } + + public function getMountsForPath( + string $setupPathHint, + bool $forChildren, + array $mountProviderArgs, + IStorageFactory $loader, + ): array { + $user = $mountProviderArgs[0]->mountInfo->getUser(); + $userId = $user->getUID(); + + if (!$forChildren) { + // override path with mount point when fetching without children + $setupPathHint = $mountProviderArgs[0]->mountInfo->getMountPoint(); + } + + // remove /uid/files as the target is stored without + $setupPathHint = \substr($setupPathHint, \strlen('/' . $userId . '/files')); + // remove trailing slash + $setupPathHint = \rtrim($setupPathHint, '/'); + + // make sure trailing slash is present when loading children + if ($forChildren || $setupPathHint === '') { + $setupPathHint .= '/'; + } + + $qb = $this->connection->getQueryBuilder(); + $qb->select('id', 'remote', 'share_token', 'password', 'mountpoint', 'owner') + ->from('share_external') + ->where($qb->expr()->eq('user', $qb->createNamedParameter($user->getUID()))) + ->andWhere($qb->expr()->eq('accepted', $qb->createNamedParameter(IShare::STATUS_ACCEPTED, IQueryBuilder::PARAM_INT))); + + if ($forChildren) { + $qb->andWhere($qb->expr()->like('mountpoint', $qb->createNamedParameter($this->connection->escapeLikeParameter($setupPathHint) . '_%'))); + } else { + $qb->andWhere($qb->expr()->eq('mountpoint', $qb->createNamedParameter($setupPathHint))); + } + + $result = $qb->executeQuery(); + + $mounts = []; + while ($row = $result->fetchAssociative()) { + $row['manager'] = $this; + $row['token'] = $row['share_token']; + $mount = $this->getMount($user, $row, $loader); + $mounts[$mount->getMountPoint()] = $mount; + } + $result->closeCursor(); + + return $mounts; + } } diff --git a/apps/files_sharing/lib/Migration/Version11300Date20201120141438.php b/apps/files_sharing/lib/Migration/Version11300Date20201120141438.php index 5530a3ad8ab08..e97f198a19322 100644 --- a/apps/files_sharing/lib/Migration/Version11300Date20201120141438.php +++ b/apps/files_sharing/lib/Migration/Version11300Date20201120141438.php @@ -85,6 +85,7 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt ]); $table->setPrimaryKey(['id']); $table->addUniqueIndex(['user', 'mountpoint_hash'], 'sh_external_mp'); + $table->addIndex(['user', 'mountpoint'], 'user_mountpoint_index', [], ['lengths' => [null, 128]]); } else { $table = $schema->getTable('share_external'); $remoteIdColumn = $table->getColumn('remote_id'); diff --git a/apps/files_sharing/lib/MountProvider.php b/apps/files_sharing/lib/MountProvider.php index 1d9e16db5ef84..cc7ab6e827ce6 100644 --- a/apps/files_sharing/lib/MountProvider.php +++ b/apps/files_sharing/lib/MountProvider.php @@ -14,6 +14,7 @@ use OCP\Cache\CappedMemoryCache; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Config\IMountProvider; +use OCP\Files\Config\IPartialMountProvider; use OCP\Files\Mount\IMountManager; use OCP\Files\Mount\IMountPoint; use OCP\Files\Storage\IStorageFactory; @@ -24,9 +25,10 @@ use OCP\Share\IManager; use OCP\Share\IShare; use Psr\Log\LoggerInterface; + use function count; -class MountProvider implements IMountProvider { +class MountProvider implements IMountProvider, IPartialMountProvider { /** * @param IConfig $config * @param IManager $shareManager @@ -51,7 +53,7 @@ public function __construct( */ public function getMountsForUser(IUser $user, IStorageFactory $loader) { $userId = $user->getUID(); - $shares = array_merge( + $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), @@ -62,17 +64,24 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader) { $shares = $this->filterShares($shares, $userId); $superShares = $this->buildSuperShares($shares, $user); - return $this->getMountsFromSuperShares($userId, $superShares, $loader, $user); + return array_values( + $this->getMountsFromSuperShares( + $userId, + $superShares, + $loader, + $user, + ), + ); } /** * Groups shares by path (nodeId) and target path * - * @param IShare[] $shares + * @param iterable $shares * @return IShare[][] array of grouped shares, each element in the * array is a group which itself is an array of shares */ - private function groupShares(array $shares) { + private function groupShares(iterable $shares): array { $tmp = []; foreach ($shares as $share) { @@ -108,11 +117,11 @@ private function groupShares(array $shares) { * the shares in the group, forming the most permissive combination * possible. * - * @param IShare[] $allShares + * @param iterable $allShares * @param IUser $user user * @return list}> Tuple of [superShare, groupedShares] */ - private function buildSuperShares(array $allShares, IUser $user) { + private function buildSuperShares(iterable $allShares, IUser $user): array { $result = []; $groupedShares = $this->groupShares($allShares); @@ -237,8 +246,7 @@ private function adjustTarget( // null groups which usually appear with group backend // caching inconsistencies $this->logger->debug( - 'Could not adjust share target for share ' . $share->getId( - ) . ' to make it consistent: ' . $e->getMessage(), + 'Could not adjust share target for share ' . $share->getId() . ' to make it consistent: ' . $e->getMessage(), ['app' => 'files_sharing'] ); } @@ -248,7 +256,7 @@ private function adjustTarget( * @param array $superShares * @param IStorageFactory $loader * @param IUser $user - * @return array + * @return array IMountPoint indexed by mount point * @throws Exception */ private function getMountsFromSuperShares( @@ -261,13 +269,11 @@ private function getMountsFromSuperShares( $mounts = []; $view = new View('/' . $userId . '/files'); $ownerViews = []; - $sharingDisabledForUser - = $this->shareManager->sharingDisabledForUser($userId); + $sharingDisabledForUser = $this->shareManager->sharingDisabledForUser($userId); /** @var CappedMemoryCache $folderExistCache */ $foldersExistCache = new CappedMemoryCache(); - $validShareCache - = $this->cacheFactory->createLocal('share-valid-mountpoint-max'); + $validShareCache = $this->cacheFactory->createLocal('share-valid-mountpoint-max'); $maxValidatedShare = $validShareCache->get($userId) ?? 0; $newMaxValidatedShare = $maxValidatedShare; @@ -312,12 +318,10 @@ private function getMountsFromSuperShares( $event = new ShareMountedEvent($mount); $this->eventDispatcher->dispatchTyped($event); - $mounts[$mount->getMountPoint()] - = $allMounts[$mount->getMountPoint()] = $mount; + $mounts[$mount->getMountPoint()] = $allMounts[$mount->getMountPoint()] = $mount; foreach ($event->getAdditionalMounts() as $additionalMount) { - $allMounts[$additionalMount->getMountPoint()] - = $mounts[$additionalMount->getMountPoint()] - = $additionalMount; + $mounts[$additionalMount->getMountPoint()] = $additionalMount; + $allMounts[$additionalMount->getMountPoint()] = $additionalMount; } } catch (Exception $e) { $this->logger->error( @@ -333,24 +337,74 @@ private function getMountsFromSuperShares( $validShareCache->set($userId, $newMaxValidatedShare, 24 * 60 * 60); // array_filter removes the null values from the array - return array_values(array_filter($mounts)); + return array_filter($mounts); } /** * Filters out shares owned or shared by the user and ones for which the * user has no permissions. * - * @param IShare[] $shares - * @return IShare[] + * @param iterable $shares + * @return iterable */ - private function filterShares(array $shares, string $userId): array { - return array_filter( - $shares, - static function (IShare $share) use ($userId) { - return $share->getPermissions() > 0 - && $share->getShareOwner() !== $userId - && $share->getSharedBy() !== $userId; + private function filterShares(iterable $shares, string $userId): iterable { + foreach ($shares as $share) { + if ( + $share->getPermissions() > 0 + && $share->getShareOwner() !== $userId + && $share->getSharedBy() !== $userId + ) { + yield $share; } + } + } + + public function getMountsForPath( + string $setupPathHint, + bool $forChildren, + array $mountProviderArgs, + IStorageFactory $loader, + ): array { + $limit = -1; + $user = $mountProviderArgs[0]->mountInfo->getUser(); + $userId = $user->getUID(); + + if (!$forChildren) { + // override path with mount point when fetching without children + $setupPathHint = $mountProviderArgs[0]->mountInfo->getMountPoint(); + } + + // remove /uid/files as the target is stored without + $setupPathHint = \substr($setupPathHint, \strlen('/' . $userId . '/files')); + // remove trailing slash + $setupPathHint = \rtrim($setupPathHint, '/'); + + // make sure trailing slash is present when loading children + if ($forChildren || $setupPathHint === '') { + $setupPathHint .= '/'; + } + + $shares = $this->mergeIterables( + $this->shareManager->getSharedWithByPath($userId, IShare::TYPE_USER, $setupPathHint, $forChildren, $limit), + $this->shareManager->getSharedWithByPath($userId, IShare::TYPE_GROUP, $setupPathHint, $forChildren, $limit), + $this->shareManager->getSharedWithByPath($userId, IShare::TYPE_CIRCLE, $setupPathHint, $forChildren, $limit), + $this->shareManager->getSharedWithByPath($userId, IShare::TYPE_ROOM, $setupPathHint, $forChildren, $limit), + $this->shareManager->getSharedWithByPath($userId, IShare::TYPE_DECK, $setupPathHint, $forChildren, $limit), ); + + $shares = $this->filterShares($shares, $userId); + $superShares = $this->buildSuperShares($shares, $user); + + return $this->getMountsFromSuperShares($userId, $superShares, $loader, $user); + } + + /** + * @param iterable ...$iterables + * @return iterable + */ + private function mergeIterables(...$iterables): iterable { + foreach ($iterables as $iterable) { + yield from $iterable; + } } } diff --git a/apps/files_sharing/tests/MountProviderTest.php b/apps/files_sharing/tests/MountProviderTest.php index 20db33d62e8f7..d6be55c5f53ca 100644 --- a/apps/files_sharing/tests/MountProviderTest.php +++ b/apps/files_sharing/tests/MountProviderTest.php @@ -7,14 +7,17 @@ */ namespace OCA\Files_Sharing\Tests; -use OC\Memcache\NullCache; use OC\Share20\Share; use OCA\Files_Sharing\MountProvider; use OCA\Files_Sharing\SharedMount; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Cache\ICacheEntry; +use OCP\Files\Config\ICachedMountInfo; +use OCP\Files\Config\MountProviderArgs; use OCP\Files\IRootFolder; use OCP\Files\Mount\IMountManager; use OCP\Files\Storage\IStorageFactory; +use OCP\ICache; use OCP\ICacheFactory; use OCP\IConfig; use OCP\IUser; @@ -35,6 +38,7 @@ class MountProviderTest extends \Test\TestCase { protected IManager&MockObject $shareManager; protected IStorageFactory&MockObject $loader; protected LoggerInterface&MockObject $logger; + private ICache&MockObject $cache; protected function setUp(): void { parent::setUp(); @@ -45,9 +49,10 @@ protected function setUp(): void { $this->shareManager = $this->getMockBuilder(IManager::class)->getMock(); $this->logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); $eventDispatcher = $this->createMock(IEventDispatcher::class); + $this->cache = $this->createMock(ICache::class); + $this->cache->method('get')->willReturn(true); $cacheFactory = $this->createMock(ICacheFactory::class); - $cacheFactory->method('createLocal') - ->willReturn(new NullCache()); + $cacheFactory->method('createLocal')->willReturn($this->cache); $mountManager = $this->createMock(IMountManager::class); $this->provider = new MountProvider($this->config, $this->shareManager, $this->logger, $eventDispatcher, $cacheFactory, $mountManager); @@ -355,7 +360,6 @@ public function testMergeShares(array $userShares, array $groupShares, array $ex $circleShares = []; $roomShares = []; $deckShares = []; - $scienceMeshShares = []; $this->shareManager->expects($this->exactly(5)) ->method('getSharedWith') ->willReturnMap([ @@ -402,4 +406,90 @@ public function testMergeShares(array $userShares, array $groupShares, array $ex } } } + + #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'mergeSharesDataProvider')] + public function testMergeSharesInGetMountsForPath(array $userShares, array $groupShares, array $expectedShares, bool $moveFails = false): void { + $rootFolder = $this->createMock(IRootFolder::class); + $userManager = $this->createMock(IUserManager::class); + + $userShares = array_map(function ($shareSpec) { + return $this->makeMockShare($shareSpec[0], $shareSpec[1], $shareSpec[2], $shareSpec[3], $shareSpec[4], $shareSpec[5]); + }, $userShares); + $groupShares = array_map(function ($shareSpec) { + return $this->makeMockShare($shareSpec[0], $shareSpec[1], $shareSpec[2], $shareSpec[3], $shareSpec[4], $shareSpec[5]); + }, $groupShares); + + $this->user->expects($this->any()) + ->method('getUID') + ->willReturn('user1'); + + // tests regarding circles are made in the app itself. + $circleShares = []; + $roomShares = []; + $deckShares = []; + $path = '/'; + + // no expected shares? then no calls are performed to providers + $expectedProviderCalls = \count($expectedShares) ? 5 : 0; + $this->shareManager->expects($this->exactly($expectedProviderCalls)) + ->method('getSharedWithByPath') + ->willReturnMap([ + ['user1', IShare::TYPE_USER, $path, true, -1, 0, $userShares], + ['user1', IShare::TYPE_GROUP, $path, true, -1, 0, $groupShares], + ['user1', IShare::TYPE_CIRCLE, $path, true, -1, 0, $circleShares], + ['user1', IShare::TYPE_ROOM, $path, true, -1, 0, $roomShares], + ['user1', IShare::TYPE_DECK, $path, true, -1, 0, $deckShares], + ]); + + $this->shareManager->expects($this->any()) + ->method('newShare') + ->willReturnCallback(function () use ($rootFolder, $userManager) { + return new Share($rootFolder, $userManager); + }); + + if ($moveFails) { + $this->shareManager->expects($this->any()) + ->method('moveShare') + ->willThrowException(new \InvalidArgumentException()); + } + + $mountArgs = []; + foreach ($expectedShares as $share) { + $mountInfo = $this->createMock(ICachedMountInfo::class); + $mountInfo->method('getUser')->willReturn($this->user); + $mountInfo->method('getRootId')->willReturn($share[1]); + $rootCacheEntry = $this->createMock(ICacheEntry::class); + $mountArg = new MountProviderArgs($mountInfo, $rootCacheEntry); + $mountArgs[] = $mountArg; + } + + if (count($mountArgs) === 0) { + $mounts = []; + } else { + $mounts = $this->provider->getMountsForPath('/', true, $mountArgs, $this->loader); + } + + + $this->assertCount(\count($expectedShares), $mounts); + + foreach (array_values($mounts) as $index => $mount) { + $expectedShare = $expectedShares[$index]; + $this->assertInstanceOf('OCA\Files_Sharing\SharedMount', $mount); + + // supershare + /** @var SharedMount $mount */ + $share = $mount->getShare(); + + $this->assertEquals($expectedShare[0], $share->getId()); + $this->assertEquals($expectedShare[1], $share->getNodeId()); + $this->assertEquals($expectedShare[2], $share->getShareOwner()); + $this->assertEquals($expectedShare[3], $share->getTarget()); + $this->assertEquals($expectedShare[4], $share->getPermissions()); + if ($expectedShare[5] === null) { + $this->assertNull($share->getAttributes()); + } else { + $this->assertEquals($expectedShare[5], $share->getAttributes()->toArray()); + } + } + } } diff --git a/core/Listener/AddMissingIndicesListener.php b/core/Listener/AddMissingIndicesListener.php index 1130165d870d7..0b8b4f86f6e85 100644 --- a/core/Listener/AddMissingIndicesListener.php +++ b/core/Listener/AddMissingIndicesListener.php @@ -210,5 +210,19 @@ public function handle(Event $event): void { 'unique_category_per_user', ['uid', 'type', 'category'] ); + + $event->addMissingIndex( + 'share', + 'share_with_file_target_index', + ['share_with', 'file_target'], + ['lengths' => [null, 128]] + ); + + $event->addMissingIndex( + 'share_external', + 'user_mountpoint_index', + ['user', 'mountpoint'], + ['lengths' => [null, 128]] + ); } } diff --git a/core/Migrations/Version13000Date20170718121200.php b/core/Migrations/Version13000Date20170718121200.php index 9c58ba18b5174..e3ac88a3e31af 100644 --- a/core/Migrations/Version13000Date20170718121200.php +++ b/core/Migrations/Version13000Date20170718121200.php @@ -454,6 +454,7 @@ public function changeSchema(IOutput $output, \Closure $schemaClosure, array $op $table->addIndex(['file_source'], 'file_source_index'); $table->addIndex(['token'], 'token_index'); $table->addIndex(['share_with'], 'share_with_index'); + $table->addIndex(['share_with', 'file_target'], 'share_with_file_target_index', [], ['lengths' => [null, 128]]); $table->addIndex(['parent'], 'parent_index'); $table->addIndex(['uid_owner'], 'owner_index'); $table->addIndex(['uid_initiator'], 'initiator_index'); diff --git a/lib/private/Files/Config/MountProviderCollection.php b/lib/private/Files/Config/MountProviderCollection.php index 364ab003125cb..338c26350ea6d 100644 --- a/lib/private/Files/Config/MountProviderCollection.php +++ b/lib/private/Files/Config/MountProviderCollection.php @@ -111,6 +111,17 @@ public function getUserMountsFromProviderByPath( ); } + $userId = null; + $user = null; + foreach ($mountProviderArgs as $mountProviderArg) { + if ($userId === null) { + $user = $mountProviderArg->mountInfo->getUser(); + $userId = $user->getUID(); + } elseif ($userId !== $mountProviderArg->mountInfo->getUser()->getUID()) { + throw new \LogicException('Mounts must belong to the same user!'); + } + } + return $provider->getMountsForPath( $path, $forChildren, diff --git a/lib/private/Files/SetupManager.php b/lib/private/Files/SetupManager.php index 41803db86c763..393a6a7de0fc6 100644 --- a/lib/private/Files/SetupManager.php +++ b/lib/private/Files/SetupManager.php @@ -507,8 +507,7 @@ public function setupForPath(string $path, bool $includeChildren = false): void $mountProvider = $cachedMount->getMountProvider(); $mountPoint = $cachedMount->getMountPoint(); $isMountProviderSetup = in_array($mountProvider, $setupProviders); - $isPathSetupAsAuthoritative - = $this->isPathSetup($mountPoint); + $isPathSetupAsAuthoritative = $this->isPathSetup($mountPoint); if (!$isMountProviderSetup && !$isPathSetupAsAuthoritative) { if ($mountProvider === '') { $this->logger->debug('mount at ' . $cachedMount->getMountPoint() . ' has no provider set, performing full setup'); @@ -539,8 +538,7 @@ public function setupForPath(string $path, bool $includeChildren = false): void } else { $currentProviders[] = $mountProvider; $setupProviders[] = $mountProvider; - $fullProviderMounts[] - = $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$mountProvider]); + $fullProviderMounts[] = $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$mountProvider]); } } @@ -721,6 +719,7 @@ public function tearDown() { $this->fullSetupRequired = []; $this->rootSetup = false; $this->mountManager->clear(); + $this->userMountCache->clear(); $this->eventDispatcher->dispatchTyped(new FilesystemTornDownEvent()); } diff --git a/lib/private/Share20/DefaultShareProvider.php b/lib/private/Share20/DefaultShareProvider.php index 954c1544650de..46c2b94116217 100644 --- a/lib/private/Share20/DefaultShareProvider.php +++ b/lib/private/Share20/DefaultShareProvider.php @@ -964,12 +964,12 @@ private function _getSharedWith( $groups = array_filter($groups); - $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP))) - ->andWhere($qb->expr()->in('share_with', $qb->createNamedParameter( + $qb->andWhere($qb->expr()->eq('s.share_type', $qb->createNamedParameter(IShare::TYPE_GROUP))) + ->andWhere($qb->expr()->in('s.share_with', $qb->createNamedParameter( $groups, IQueryBuilder::PARAM_STR_ARRAY ))) - ->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))); + ->andWhere($qb->expr()->in('s.item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))); $cursor = $qb->executeQuery(); while ($data = $cursor->fetch()) { diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 466401d14377f..51aa9559d5a22 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -1299,16 +1299,19 @@ public function getSharedWithByPath(string $userId, int $shareType, string $path throw new \RuntimeException(\get_class($provider) . ' must implement IPartialShareProvider'); } - $shares = $provider->getSharedWithByPath($userId, + $shares = $provider->getSharedWithByPath( + $userId, $shareType, $path, $forChildren, $limit, - $offset + $offset, ); if (\is_array($shares)) { $shares = new ArrayIterator($shares); + } elseif (!$shares instanceof \Iterator) { + $shares = new \IteratorIterator($shares); } return new \CallbackFilterIterator($shares, function (IShare $share) {