From 032be69b2689b01f17326b71da529f398477c5b3 Mon Sep 17 00:00:00 2001 From: Luka Trovic Date: Mon, 22 Jan 2024 15:23:54 +0100 Subject: [PATCH] feat: add command to fix broken shares after transfer ownership Signed-off-by: Luka Trovic --- .github/workflows/phpunit-mysql.yml | 2 +- apps/files_sharing/appinfo/info.xml | 1 + .../composer/composer/ClassLoader.php | 96 +++++------ .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + .../lib/Command/FixBrokenShares.php | 59 +++++++ apps/files_sharing/lib/OrphanHelper.php | 28 +++- .../tests/EtagPropagationTest.php | 3 +- lib/private/Share20/Manager.php | 116 +++++++++++++- tests/lib/Share20/ManagerTest.php | 151 +++++++++++++++++- 10 files changed, 402 insertions(+), 56 deletions(-) create mode 100644 apps/files_sharing/lib/Command/FixBrokenShares.php diff --git a/.github/workflows/phpunit-mysql.yml b/.github/workflows/phpunit-mysql.yml index cecc22e1c0005..20b2f3a52747e 100644 --- a/.github/workflows/phpunit-mysql.yml +++ b/.github/workflows/phpunit-mysql.yml @@ -89,7 +89,7 @@ jobs: submodules: true - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@4bd44f22a98a19e0950cbad5f31095157cc9621b # v2 + uses: shivammathur/setup-php@c665c7a15b5295c2488ac8a87af9cb806cd72198 # v2 with: php-version: ${{ matrix.php-versions }} # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation diff --git a/apps/files_sharing/appinfo/info.xml b/apps/files_sharing/appinfo/info.xml index 68b0e0eb81e0e..210757f9bee37 100644 --- a/apps/files_sharing/appinfo/info.xml +++ b/apps/files_sharing/appinfo/info.xml @@ -48,6 +48,7 @@ Turning the feature off removes shared files and folders on the server for all s OCA\Files_Sharing\Command\CleanupRemoteStorages OCA\Files_Sharing\Command\ExiprationNotification OCA\Files_Sharing\Command\DeleteOrphanShares + OCA\Files_Sharing\Command\FixBrokenShares diff --git a/apps/files_sharing/composer/composer/ClassLoader.php b/apps/files_sharing/composer/composer/ClassLoader.php index 7824d8f7eafe8..a72151c77c8eb 100644 --- a/apps/files_sharing/composer/composer/ClassLoader.php +++ b/apps/files_sharing/composer/composer/ClassLoader.php @@ -45,34 +45,35 @@ class ClassLoader /** @var \Closure(string):void */ private static $includeFile; - /** @var string|null */ + /** @var ?string */ private $vendorDir; // PSR-4 /** - * @var array> + * @var array[] + * @psalm-var array> */ private $prefixLengthsPsr4 = array(); /** - * @var array> + * @var array[] + * @psalm-var array> */ private $prefixDirsPsr4 = array(); /** - * @var list + * @var array[] + * @psalm-var array */ private $fallbackDirsPsr4 = array(); // PSR-0 /** - * List of PSR-0 prefixes - * - * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) - * - * @var array>> + * @var array[] + * @psalm-var array> */ private $prefixesPsr0 = array(); /** - * @var list + * @var array[] + * @psalm-var array */ private $fallbackDirsPsr0 = array(); @@ -80,7 +81,8 @@ class ClassLoader private $useIncludePath = false; /** - * @var array + * @var string[] + * @psalm-var array */ private $classMap = array(); @@ -88,20 +90,21 @@ class ClassLoader private $classMapAuthoritative = false; /** - * @var array + * @var bool[] + * @psalm-var array */ private $missingClasses = array(); - /** @var string|null */ + /** @var ?string */ private $apcuPrefix; /** - * @var array + * @var self[] */ private static $registeredLoaders = array(); /** - * @param string|null $vendorDir + * @param ?string $vendorDir */ public function __construct($vendorDir = null) { @@ -110,7 +113,7 @@ public function __construct($vendorDir = null) } /** - * @return array> + * @return string[] */ public function getPrefixes() { @@ -122,7 +125,8 @@ public function getPrefixes() } /** - * @return array> + * @return array[] + * @psalm-return array> */ public function getPrefixesPsr4() { @@ -130,7 +134,8 @@ public function getPrefixesPsr4() } /** - * @return list + * @return array[] + * @psalm-return array */ public function getFallbackDirs() { @@ -138,7 +143,8 @@ public function getFallbackDirs() } /** - * @return list + * @return array[] + * @psalm-return array */ public function getFallbackDirsPsr4() { @@ -146,7 +152,8 @@ public function getFallbackDirsPsr4() } /** - * @return array Array of classname => path + * @return string[] Array of classname => path + * @psalm-return array */ public function getClassMap() { @@ -154,7 +161,8 @@ public function getClassMap() } /** - * @param array $classMap Class to filename map + * @param string[] $classMap Class to filename map + * @psalm-param array $classMap * * @return void */ @@ -171,25 +179,24 @@ public function addClassMap(array $classMap) * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * - * @param string $prefix The prefix - * @param list|string $paths The PSR-0 root directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories * * @return void */ public function add($prefix, $paths, $prepend = false) { - $paths = (array) $paths; if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( - $paths, + (array) $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, - $paths + (array) $paths ); } @@ -198,19 +205,19 @@ public function add($prefix, $paths, $prepend = false) $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { - $this->prefixesPsr0[$first][$prefix] = $paths; + $this->prefixesPsr0[$first][$prefix] = (array) $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( - $paths, + (array) $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], - $paths + (array) $paths ); } } @@ -219,9 +226,9 @@ public function add($prefix, $paths, $prepend = false) * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param list|string $paths The PSR-4 base directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException * @@ -229,18 +236,17 @@ public function add($prefix, $paths, $prepend = false) */ public function addPsr4($prefix, $paths, $prepend = false) { - $paths = (array) $paths; if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( - $paths, + (array) $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, - $paths + (array) $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { @@ -250,18 +256,18 @@ public function addPsr4($prefix, $paths, $prepend = false) throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; - $this->prefixDirsPsr4[$prefix] = $paths; + $this->prefixDirsPsr4[$prefix] = (array) $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( - $paths, + (array) $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], - $paths + (array) $paths ); } } @@ -270,8 +276,8 @@ public function addPsr4($prefix, $paths, $prepend = false) * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * - * @param string $prefix The prefix - * @param list|string $paths The PSR-0 base directories + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 base directories * * @return void */ @@ -288,8 +294,8 @@ public function set($prefix, $paths) * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param list|string $paths The PSR-4 base directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException * @@ -475,9 +481,9 @@ public function findFile($class) } /** - * Returns the currently registered loaders keyed by their corresponding vendor directories. + * Returns the currently registered loaders indexed by their corresponding vendor directories. * - * @return array + * @return self[] */ public static function getRegisteredLoaders() { diff --git a/apps/files_sharing/composer/composer/autoload_classmap.php b/apps/files_sharing/composer/composer/autoload_classmap.php index e1abddb3a6450..87c935dcbab82 100644 --- a/apps/files_sharing/composer/composer/autoload_classmap.php +++ b/apps/files_sharing/composer/composer/autoload_classmap.php @@ -26,6 +26,7 @@ 'OCA\\Files_Sharing\\Command\\CleanupRemoteStorages' => $baseDir . '/../lib/Command/CleanupRemoteStorages.php', 'OCA\\Files_Sharing\\Command\\DeleteOrphanShares' => $baseDir . '/../lib/Command/DeleteOrphanShares.php', 'OCA\\Files_Sharing\\Command\\ExiprationNotification' => $baseDir . '/../lib/Command/ExiprationNotification.php', + 'OCA\\Files_Sharing\\Command\\FixBrokenShares' => $baseDir . '/../lib/Command/FixBrokenShares.php', 'OCA\\Files_Sharing\\Controller\\AcceptController' => $baseDir . '/../lib/Controller/AcceptController.php', 'OCA\\Files_Sharing\\Controller\\DeletedShareAPIController' => $baseDir . '/../lib/Controller/DeletedShareAPIController.php', 'OCA\\Files_Sharing\\Controller\\ExternalSharesController' => $baseDir . '/../lib/Controller/ExternalSharesController.php', diff --git a/apps/files_sharing/composer/composer/autoload_static.php b/apps/files_sharing/composer/composer/autoload_static.php index 5d2fb3bac2a47..c77a2af82b278 100644 --- a/apps/files_sharing/composer/composer/autoload_static.php +++ b/apps/files_sharing/composer/composer/autoload_static.php @@ -41,6 +41,7 @@ class ComposerStaticInitFiles_Sharing 'OCA\\Files_Sharing\\Command\\CleanupRemoteStorages' => __DIR__ . '/..' . '/../lib/Command/CleanupRemoteStorages.php', 'OCA\\Files_Sharing\\Command\\DeleteOrphanShares' => __DIR__ . '/..' . '/../lib/Command/DeleteOrphanShares.php', 'OCA\\Files_Sharing\\Command\\ExiprationNotification' => __DIR__ . '/..' . '/../lib/Command/ExiprationNotification.php', + 'OCA\\Files_Sharing\\Command\\FixBrokenShares' => __DIR__ . '/..' . '/../lib/Command/FixBrokenShares.php', 'OCA\\Files_Sharing\\Controller\\AcceptController' => __DIR__ . '/..' . '/../lib/Controller/AcceptController.php', 'OCA\\Files_Sharing\\Controller\\DeletedShareAPIController' => __DIR__ . '/..' . '/../lib/Controller/DeletedShareAPIController.php', 'OCA\\Files_Sharing\\Controller\\ExternalSharesController' => __DIR__ . '/..' . '/../lib/Controller/ExternalSharesController.php', diff --git a/apps/files_sharing/lib/Command/FixBrokenShares.php b/apps/files_sharing/lib/Command/FixBrokenShares.php new file mode 100644 index 0000000000000..e9b21a10e311d --- /dev/null +++ b/apps/files_sharing/lib/Command/FixBrokenShares.php @@ -0,0 +1,59 @@ +setName('sharing:fix-broken-shares') + ->setDescription('Fix broken shares after transfer ownership') + ->addOption( + 'dry-run', + null, + InputOption::VALUE_NONE, + 'only show which shares would be updated' + ); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $shares = $this->orphanHelper->getAllShares(); + $dryRun = $input->getOption('dry-run'); + + foreach ($shares as $share) { + if ($this->orphanHelper->isShareValid($share['owner'], $share['fileid']) || !$this->orphanHelper->fileExists($share['fileid'])) { + continue; + } + + $owner = $this->orphanHelper->findOwner($share['fileid']); + + if ($owner !== null) { + if ($dryRun) { + $output->writeln("Share {$share['id']} can be updated to owner $owner"); + } else { + $this->orphanHelper->updateShareOwner($share['id'], $owner); + $output->writeln("Share {$share['id']} updated to owner $owner"); + } + } + } + + return static::SUCCESS; + } +} diff --git a/apps/files_sharing/lib/OrphanHelper.php b/apps/files_sharing/lib/OrphanHelper.php index 94fe4f0831838..cbf594db16692 100644 --- a/apps/files_sharing/lib/OrphanHelper.php +++ b/apps/files_sharing/lib/OrphanHelper.php @@ -10,19 +10,23 @@ use OC\User\NoUserException; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Files\Config\IUserMountCache; use OCP\Files\IRootFolder; use OCP\IDBConnection; class OrphanHelper { private IDBConnection $connection; private IRootFolder $rootFolder; + private IUserMountCache $userMountCache; public function __construct( IDBConnection $connection, - IRootFolder $rootFolder + IRootFolder $rootFolder, + IUserMountCache $userMountCache ) { $this->connection = $connection; $this->rootFolder = $rootFolder; + $this->userMountCache = $userMountCache; } public function isShareValid(string $owner, int $fileId): bool { @@ -73,4 +77,26 @@ public function getAllShares() { ]; } } + + public function findOwner(int $fileId): ?string { + $mounts = $this->userMountCache->getMountsForFileId($fileId); + if (!$mounts) { + return null; + } + foreach ($mounts as $mount) { + // Only the mount of owner has the internal path value + if ($mount->getInternalPath()) { + return $mount->getUser()->getUID(); + } + } + return null; + } + + public function updateShareOwner(int $shareId, string $owner): void { + $query = $this->connection->getQueryBuilder(); + $query->update('share') + ->set('uid_owner', $query->createNamedParameter($owner)) + ->where($query->expr()->eq('id', $query->createNamedParameter($shareId, IQueryBuilder::PARAM_INT))); + $query->executeStatement(); + } } diff --git a/apps/files_sharing/tests/EtagPropagationTest.php b/apps/files_sharing/tests/EtagPropagationTest.php index 3f9ddfc413dc7..327726acc9774 100644 --- a/apps/files_sharing/tests/EtagPropagationTest.php +++ b/apps/files_sharing/tests/EtagPropagationTest.php @@ -277,7 +277,8 @@ public function testOwnerUnshares() { self::TEST_FILES_SHARING_API_USER2, ]); - $this->assertAllUnchanged(); + $this->assertEtagsNotChanged([self::TEST_FILES_SHARING_API_USER1, self::TEST_FILES_SHARING_API_USER2, self::TEST_FILES_SHARING_API_USER3]); + $this->assertEtagsChanged([self::TEST_FILES_SHARING_API_USER4]); } public function testOwnerUnsharesFlatReshares() { diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 75eeb082b2fba..1b52ecfce46c2 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -17,6 +17,7 @@ use OCP\Files\IRootFolder; use OCP\Files\Mount\IMountManager; use OCP\Files\Node; +use OCP\Files\NotFoundException; use OCP\HintException; use OCP\IConfig; use OCP\IDateTimeZone; @@ -360,7 +361,7 @@ protected function validateExpirationDateLink(IShare $share) { if ($expirationDate !== null) { $expirationDate->setTimezone($this->dateTimeZone->getTimeZone()); $expirationDate->setTime(0, 0, 0); - + $date = new \DateTime('now', $this->dateTimeZone->getTimeZone()); $date->setTime(0, 0, 0); if ($date >= $expirationDate) { @@ -376,24 +377,24 @@ protected function validateExpirationDateLink(IShare $share) { } catch (\UnexpectedValueException $e) { // This is a new share } - + if ($fullId === null && $expirationDate === null && $this->shareApiLinkDefaultExpireDate()) { $expirationDate = new \DateTime('now', $this->dateTimeZone->getTimeZone()); $expirationDate->setTime(0, 0, 0); - + $days = (int)$this->config->getAppValue('core', 'link_defaultExpDays', (string)$this->shareApiLinkDefaultExpireDays()); if ($days > $this->shareApiLinkDefaultExpireDays()) { $days = $this->shareApiLinkDefaultExpireDays(); } $expirationDate->add(new \DateInterval('P' . $days . 'D')); } - + // If we enforce the expiration date check that is does not exceed if ($isEnforced) { if (empty($expirationDate)) { throw new \InvalidArgumentException('Expiration date is enforced'); } - + $date = new \DateTime('now', $this->dateTimeZone->getTimeZone()); $date->setTime(0, 0, 0); $date->add(new \DateInterval('P' . $this->shareApiLinkDefaultExpireDays() . 'D')); @@ -1117,6 +1118,108 @@ protected function deleteChildren(IShare $share) { return $deletedShares; } + public function deleteReshare(IShare $share) { + // Skip if node not found + try { + $node = $share->getNode(); + } catch (NotFoundException) { + return; + } + + // If the user has another shares, we don't delete the shares by this user + if ($share->getShareType() === IShare::TYPE_USER) { + $groupShares = $this->getSharedWith($share->getSharedWith(), IShare::TYPE_GROUP, $node, -1, 0); + + if (count($groupShares) !== 0) { + return; + } + + // Check shares of parent folders + try { + $parentNode = $node->getParent(); + while ($parentNode) { + $groupShares = $this->getSharedWith($share->getSharedWith(), IShare::TYPE_GROUP, $parentNode, -1, 0); + $userShares = $this->getSharedWith($share->getSharedWith(), IShare::TYPE_USER, $parentNode, -1, 0); + + if (count($groupShares) !== 0 || count($userShares) !== 0) { + return; + } + + $parentNode = $parentNode->getParent(); + } + } catch (NotFoundException) { + } + } + + // Delete re-share records (shared by "share with user") inside folder + if ($share->getNodeType() === 'folder' && $share->getShareType() === IShare::TYPE_USER) { + $sharesInFolder = $this->getSharesInFolder($share->getSharedWith(), $node, true); + + foreach ($sharesInFolder as $shares) { + foreach ($shares as $child) { + $this->deleteShare($child); + } + } + } + + $shareTypes = [ + IShare::TYPE_GROUP, + IShare::TYPE_USER, + IShare::TYPE_LINK, + IShare::TYPE_REMOTE, + IShare::TYPE_EMAIL + ]; + + // Delete re-share records which shared by "share with user" + if ($share->getShareType() === IShare::TYPE_USER || $share->getShareType() === IShare::TYPE_USERGROUP) { + foreach ($shareTypes as $shareType) { + $provider = $this->factory->getProviderForType($shareType); + $shares = $provider->getSharesBy($share->getSharedWith(), $shareType, $node, false, -1, 0); + foreach ($shares as $child) { + $this->deleteShare($child); + } + } + } + + // Delete re-share records which shared by users in "share with group" + if ($share->getShareType() === IShare::TYPE_GROUP) { + $group = $this->groupManager->get($share->getSharedWith()); + $users = $group->getUsers(); + + foreach ($users as $user) { + if ($user->getUID() === $share->getShareOwner()) { + continue; + } + + $anotherShares = $this->getSharedWith($user->getUID(), IShare::TYPE_USER, $node, -1, 0); + $groupShares = $this->getSharedWith($user->getUID(), IShare::TYPE_GROUP, $node, -1, 0); + + // If the user has another shares, we don't delete the shares by this user + if (count($anotherShares) !== 0 || count($groupShares) > 1) { + continue; + } + + if ($share->getNodeType() === 'folder') { + $sharesInFolder = $this->getSharesInFolder($user->getUID(), $node, true); + + foreach ($sharesInFolder as $shares) { + foreach ($shares as $child) { + $this->deleteShare($child); + } + } + } else { + foreach ($shareTypes as $shareType) { + $provider = $this->factory->getProviderForType($shareType); + $shares = $provider->getSharesBy($user->getUID(), $shareType, $node, false, -1, 0); + foreach ($shares as $child) { + $this->deleteShare($child); + } + } + } + } + } + } + /** * Delete a share * @@ -1133,6 +1236,9 @@ public function deleteShare(IShare $share) { $this->dispatcher->dispatchTyped(new BeforeShareDeletedEvent($share)); + // Delete shares that shared by the "share with user/group" + $this->deleteReshare($share); + // Get all children and delete them as well $this->deleteChildren($share); diff --git a/tests/lib/Share20/ManagerTest.php b/tests/lib/Share20/ManagerTest.php index 15ecc839451a4..b4f61ba4b2498 100644 --- a/tests/lib/Share20/ManagerTest.php +++ b/tests/lib/Share20/ManagerTest.php @@ -227,7 +227,7 @@ public function dataTestDelete() { */ public function testDelete($shareType, $sharedWith) { $manager = $this->createManagerMock() - ->setMethods(['getShareById', 'deleteChildren']) + ->setMethods(['getShareById', 'deleteChildren', 'deleteReshare']) ->getMock(); $manager->method('deleteChildren')->willReturn([]); @@ -245,6 +245,7 @@ public function testDelete($shareType, $sharedWith) { ->setTarget('myTarget'); $manager->expects($this->once())->method('deleteChildren')->with($share); + $manager->expects($this->once())->method('deleteReshare')->with($share); $this->defaultProvider ->expects($this->once()) @@ -269,7 +270,7 @@ public function testDelete($shareType, $sharedWith) { public function testDeleteLazyShare() { $manager = $this->createManagerMock() - ->setMethods(['getShareById', 'deleteChildren']) + ->setMethods(['getShareById', 'deleteChildren', 'deleteReshare']) ->getMock(); $manager->method('deleteChildren')->willReturn([]); @@ -288,6 +289,7 @@ public function testDeleteLazyShare() { $this->rootFolder->expects($this->never())->method($this->anything()); $manager->expects($this->once())->method('deleteChildren')->with($share); + $manager->expects($this->once())->method('deleteReshare')->with($share); $this->defaultProvider ->expects($this->once()) @@ -312,7 +314,7 @@ public function testDeleteLazyShare() { public function testDeleteNested() { $manager = $this->createManagerMock() - ->setMethods(['getShareById']) + ->setMethods(['getShareById', 'deleteReshare']) ->getMock(); $path = $this->createMock(File::class); @@ -469,6 +471,149 @@ public function testDeleteChildren() { $this->assertSame($shares, $result); } + public function testDeleteReshareWhenUserHasOneShare() { + $manager = $this->createManagerMock() + ->setMethods(['deleteShare', 'getSharesInFolder', 'getSharedWith']) + ->getMock(); + + $folder = $this->createMock(Folder::class); + $folder->method('getParent')->willReturn(null); + + $share = $this->createMock(IShare::class); + $share->method('getShareType')->willReturn(IShare::TYPE_USER); + $share->method('getNodeType')->willReturn('folder'); + $share->method('getSharedWith')->willReturn('UserB'); + $share->method('getNode')->willReturn($folder); + + $reShare = $this->createMock(IShare::class); + $reShare->method('getSharedBy')->willReturn('UserB'); + + $reShareInSubFolder = $this->createMock(IShare::class); + $reShareInSubFolder->method('getSharedBy')->willReturn('UserB'); + + $manager->method('getSharedWith')->willReturn([]); + $manager->method('getSharesInFolder')->willReturn([$reShareInSubFolder]); + + $this->defaultProvider->method('getSharesBy') + ->willReturn([$reShare]); + + $manager->expects($this->atLeast(2))->method('deleteShare')->withConsecutive([$reShare], [$reShareInSubFolder]); + + $manager->deleteReshare($share); + } + + public function testDeleteReshareWhenUserHasAnotherShare() { + $manager = $this->createManagerMock() + ->setMethods(['deleteShare', 'getSharesInFolder', 'getSharedWith']) + ->getMock(); + + $folder = $this->createMock(Folder::class); + $folder->method('getParent')->willReturn(null); + + $share = $this->createMock(IShare::class); + $share->method('getShareType')->willReturn(IShare::TYPE_USER); + $share->method('getNodeType')->willReturn('folder'); + $share->method('getSharedWith')->willReturn('UserB'); + $share->method('getNode')->willReturn($folder); + + $reShare = $this->createMock(IShare::class); + $reShare->method('getShareType')->willReturn(IShare::TYPE_USER); + $reShare->method('getNodeType')->willReturn('folder'); + $reShare->method('getSharedBy')->willReturn('UserB'); + $reShare->method('getNode')->willReturn($folder); + + $manager->method('getSharedWith') + ->with('UserB', IShare::TYPE_GROUP, $folder, -1, 0) + ->willReturn([1]); + + $manager->expects($this->never())->method('deleteShare'); + + $manager->deleteReshare($share); + } + + public function testDeleteReshareWhenUserHasAnotherShareFromParentFolder() { + $manager = $this->createManagerMock() + ->setMethods(['deleteShare', 'getSharesInFolder', 'getSharedWith']) + ->getMock(); + + $parentFolder = $this->createMock(Folder::class); + $parentFolder->method('getParent')->willReturn(null); + + $folder = $this->createMock(Folder::class); + $folder->method('getParent')->willReturn($parentFolder); + + $share = $this->createMock(IShare::class); + $share->method('getShareType')->willReturn(IShare::TYPE_USER); + $share->method('getNodeType')->willReturn('folder'); + $share->method('getSharedWith')->willReturn('UserB'); + $share->method('getNode')->willReturn($folder); + + $reShare = $this->createMock(IShare::class); + $reShare->method('getShareType')->willReturn(IShare::TYPE_USER); + $reShare->method('getNodeType')->willReturn('folder'); + $reShare->method('getSharedBy')->willReturn('UserB'); + $reShare->method('getNode')->willReturn($folder); + + $manager->expects($this->exactly(3))->method('getSharedWith')->willReturnOnConsecutiveCalls([], [1], []); + $manager->method('getSharesInFolder')->willReturn([]); + + $this->defaultProvider->method('getSharesBy') + ->willReturn([$reShare]); + + $manager->expects($this->never())->method('deleteShare'); + + $manager->deleteReshare($share); + } + + public function testDeleteReshareOfUsersInGroupShare() { + $manager = $this->createManagerMock() + ->setMethods(['deleteShare', 'getSharesInFolder', 'getSharedWith']) + ->getMock(); + + $parentFolder = $this->createMock(Folder::class); + $parentFolder->method('getParent')->willReturn(null); + + $folder = $this->createMock(Folder::class); + $folder->method('getParent')->willReturn($parentFolder); + + $userA = $this->createMock(IUser::class); + $userA->method('getUID')->willReturn('userA'); + + $share = $this->createMock(IShare::class); + $share->method('getShareType')->willReturn(IShare::TYPE_GROUP); + $share->method('getNodeType')->willReturn('folder'); + $share->method('getSharedWith')->willReturn('Group'); + $share->method('getNode')->willReturn($folder); + $share->method('getShareOwner')->willReturn($userA); + + $reShare1 = $this->createMock(IShare::class); + $reShare1->method('getShareType')->willReturn(IShare::TYPE_USER); + $reShare1->method('getNodeType')->willReturn('folder'); + $reShare1->method('getSharedBy')->willReturn('UserB'); + $reShare1->method('getNode')->willReturn($folder); + + $reShare2 = $this->createMock(IShare::class); + $reShare2->method('getShareType')->willReturn(IShare::TYPE_USER); + $reShare2->method('getNodeType')->willReturn('folder'); + $reShare2->method('getSharedBy')->willReturn('UserC'); + $reShare2->method('getNode')->willReturn($folder); + + $userB = $this->createMock(IUser::class); + $userB->method('getUID')->willReturn('userB'); + $userC = $this->createMock(IUser::class); + $userC->method('getUID')->willReturn('userC'); + $group = $this->createMock(IGroup::class); + $group->method('getUsers')->willReturn([$userB, $userC]); + $this->groupManager->method('get')->with('Group')->willReturn($group); + + $manager->method('getSharedWith')->willReturn([]); + $manager->expects($this->exactly(2))->method('getSharesInFolder')->willReturnOnConsecutiveCalls([[$reShare1]], [[$reShare2]]); + + $manager->expects($this->exactly(2))->method('deleteShare')->withConsecutive([$reShare1], [$reShare2]); + + $manager->deleteReshare($share); + } + public function testGetShareById() { $share = $this->createMock(IShare::class);