diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php index d7511e772abbf..d612e541f3f82 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -78,6 +78,7 @@ 'OCA\\Files\\Listener\\NodeAddedToFavoriteListener' => $baseDir . '/../lib/Listener/NodeAddedToFavoriteListener.php', 'OCA\\Files\\Listener\\NodeRemovedFromFavoriteListener' => $baseDir . '/../lib/Listener/NodeRemovedFromFavoriteListener.php', 'OCA\\Files\\Listener\\RenderReferenceEventListener' => $baseDir . '/../lib/Listener/RenderReferenceEventListener.php', + 'OCA\\Files\\Listener\\RestrictInteractionListener' => $baseDir . '/../lib/Listener/RestrictInteractionListener.php', 'OCA\\Files\\Listener\\SyncLivePhotosListener' => $baseDir . '/../lib/Listener/SyncLivePhotosListener.php', 'OCA\\Files\\Listener\\UserFirstTimeLoggedInListener' => $baseDir . '/../lib/Listener/UserFirstTimeLoggedInListener.php', 'OCA\\Files\\Migration\\Version11301Date20191205150729' => $baseDir . '/../lib/Migration/Version11301Date20191205150729.php', diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php index ab2b19e7772c1..bb9e0e2431fd4 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.php @@ -93,6 +93,7 @@ class ComposerStaticInitFiles 'OCA\\Files\\Listener\\NodeAddedToFavoriteListener' => __DIR__ . '/..' . '/../lib/Listener/NodeAddedToFavoriteListener.php', 'OCA\\Files\\Listener\\NodeRemovedFromFavoriteListener' => __DIR__ . '/..' . '/../lib/Listener/NodeRemovedFromFavoriteListener.php', 'OCA\\Files\\Listener\\RenderReferenceEventListener' => __DIR__ . '/..' . '/../lib/Listener/RenderReferenceEventListener.php', + 'OCA\\Files\\Listener\\RestrictInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/RestrictInteractionListener.php', 'OCA\\Files\\Listener\\SyncLivePhotosListener' => __DIR__ . '/..' . '/../lib/Listener/SyncLivePhotosListener.php', 'OCA\\Files\\Listener\\UserFirstTimeLoggedInListener' => __DIR__ . '/..' . '/../lib/Listener/UserFirstTimeLoggedInListener.php', 'OCA\\Files\\Migration\\Version11301Date20191205150729' => __DIR__ . '/..' . '/../lib/Migration/Version11301Date20191205150729.php', diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php index 54e6b2c678ad0..c7000e3c8c975 100644 --- a/apps/files/lib/AppInfo/Application.php +++ b/apps/files/lib/AppInfo/Application.php @@ -23,6 +23,7 @@ use OCA\Files\Listener\NodeAddedToFavoriteListener; use OCA\Files\Listener\NodeRemovedFromFavoriteListener; use OCA\Files\Listener\RenderReferenceEventListener; +use OCA\Files\Listener\RestrictInteractionListener; use OCA\Files\Listener\SyncLivePhotosListener; use OCA\Files\Listener\UserFirstTimeLoggedInListener; use OCA\Files\Notification\Notifier; @@ -40,6 +41,7 @@ use OCP\Files\Events\Node\NodeCopiedEvent; use OCP\Files\Events\NodeAddedToFavorite; use OCP\Files\Events\NodeRemovedFromFavorite; +use OCP\Interaction\RestrictInteractionEvent; use OCP\User\Events\UserFirstTimeLoggedInEvent; use OCP\Util; @@ -78,6 +80,7 @@ public function register(IRegistrationContext $context): void { $context->registerConfigLexicon(ConfigLexicon::class); + $context->registerEventListener(RestrictInteractionEvent::class, RestrictInteractionListener::class); } #[\Override] diff --git a/apps/files/lib/Listener/RestrictInteractionListener.php b/apps/files/lib/Listener/RestrictInteractionListener.php new file mode 100644 index 0000000000000..37e81856f4c88 --- /dev/null +++ b/apps/files/lib/Listener/RestrictInteractionListener.php @@ -0,0 +1,43 @@ + + */ +final readonly class RestrictInteractionListener implements IEventListener { + private IL10N $l10n; + + public function __construct( + IFactory $l10nFactory, + ) { + $this->l10n = $l10nFactory->get(Application::APP_ID); + } + + /** + * @param RestrictInteractionEvent $event + */ + #[\Override] + public function handle(Event $event): void { + if ($event->resource instanceof NodeResource && ($event->resource->getNodePermissions() & Constants::PERMISSION_READ) !== Constants::PERMISSION_READ) { + throw new InteractionRestrictedException('No read permission on the node.', $this->l10n->t('No read permission on file.')); + } + } +} diff --git a/apps/files/tests/Listener/RestrictInteractionListenerTest.php b/apps/files/tests/Listener/RestrictInteractionListenerTest.php new file mode 100644 index 0000000000000..80fd39dd4ba4b --- /dev/null +++ b/apps/files/tests/Listener/RestrictInteractionListenerTest.php @@ -0,0 +1,60 @@ +createUser('user', 'password'); + $this->assertNotFalse($user); + $this->user = $user; + + Server::get(ISetupManager::class)->setupForUser($user); + } + + #[\Override] + protected function tearDown(): void { + Server::get(ISetupManager::class)->tearDown(); + + $this->assertTrue($this->user->delete()); + + parent::tearDown(); + } + + public function testNodeResourceShareActionMissingReadPermission(): void { + $userFolder = Server::get(IRootFolder::class)->getUserFolder($this->user->getUID()); + + $fileNode = $userFolder->newFile('foo.txt', 'bar'); + $fileNode->getStorage()->getCache()->update($fileNode->getId(), ['permissions' => Constants::PERMISSION_ALL & ~Constants::PERMISSION_READ]); + + $folderNode = $userFolder->newFolder('foo'); + $folderNode->getStorage()->getCache()->update($folderNode->getId(), ['permissions' => Constants::PERMISSION_ALL & ~Constants::PERMISSION_READ]); + + foreach ([$fileNode, $folderNode] as $node) { + $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, new NodeResource($node->getId(), $this->user->getUID(), $node), null, null); + $this->assertEquals('No read permission on file.', $event->isInteractionRestricted()); + } + } +} diff --git a/apps/files_sharing/composer/composer/autoload_classmap.php b/apps/files_sharing/composer/composer/autoload_classmap.php index 8140fc52f1628..ffd26eab072d6 100644 --- a/apps/files_sharing/composer/composer/autoload_classmap.php +++ b/apps/files_sharing/composer/composer/autoload_classmap.php @@ -70,6 +70,7 @@ 'OCA\\Files_Sharing\\Listener\\LoadAdditionalListener' => $baseDir . '/../lib/Listener/LoadAdditionalListener.php', 'OCA\\Files_Sharing\\Listener\\LoadPublicFileRequestAuthListener' => $baseDir . '/../lib/Listener/LoadPublicFileRequestAuthListener.php', 'OCA\\Files_Sharing\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php', + 'OCA\\Files_Sharing\\Listener\\RestrictInteractionListener' => $baseDir . '/../lib/Listener/RestrictInteractionListener.php', 'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => $baseDir . '/../lib/Listener/ShareInteractionListener.php', 'OCA\\Files_Sharing\\Listener\\SharesUpdatedListener' => $baseDir . '/../lib/Listener/SharesUpdatedListener.php', 'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => $baseDir . '/../lib/Listener/UserAddedToGroupListener.php', diff --git a/apps/files_sharing/composer/composer/autoload_static.php b/apps/files_sharing/composer/composer/autoload_static.php index fd2189f481888..c6cfeb8700a4e 100644 --- a/apps/files_sharing/composer/composer/autoload_static.php +++ b/apps/files_sharing/composer/composer/autoload_static.php @@ -85,6 +85,7 @@ class ComposerStaticInitFiles_Sharing 'OCA\\Files_Sharing\\Listener\\LoadAdditionalListener' => __DIR__ . '/..' . '/../lib/Listener/LoadAdditionalListener.php', 'OCA\\Files_Sharing\\Listener\\LoadPublicFileRequestAuthListener' => __DIR__ . '/..' . '/../lib/Listener/LoadPublicFileRequestAuthListener.php', 'OCA\\Files_Sharing\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php', + 'OCA\\Files_Sharing\\Listener\\RestrictInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/RestrictInteractionListener.php', 'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/ShareInteractionListener.php', 'OCA\\Files_Sharing\\Listener\\SharesUpdatedListener' => __DIR__ . '/..' . '/../lib/Listener/SharesUpdatedListener.php', 'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => __DIR__ . '/..' . '/../lib/Listener/UserAddedToGroupListener.php', diff --git a/apps/files_sharing/lib/AppInfo/Application.php b/apps/files_sharing/lib/AppInfo/Application.php index 824a7f8b640ae..8bf70d8cdbc92 100644 --- a/apps/files_sharing/lib/AppInfo/Application.php +++ b/apps/files_sharing/lib/AppInfo/Application.php @@ -23,6 +23,7 @@ use OCA\Files_Sharing\Listener\LoadAdditionalListener; use OCA\Files_Sharing\Listener\LoadPublicFileRequestAuthListener; use OCA\Files_Sharing\Listener\LoadSidebarListener; +use OCA\Files_Sharing\Listener\RestrictInteractionListener; use OCA\Files_Sharing\Listener\ShareInteractionListener; use OCA\Files_Sharing\Listener\SharesUpdatedListener; use OCA\Files_Sharing\Listener\UserAddedToGroupListener; @@ -55,6 +56,7 @@ use OCP\IConfig; use OCP\IDBConnection; use OCP\IGroup; +use OCP\Interaction\RestrictInteractionEvent; use OCP\Share\Events\BeforeShareDeletedEvent; use OCP\Share\Events\ShareCreatedEvent; use OCP\Share\Events\ShareMovedEvent; @@ -131,6 +133,8 @@ function () use ($c) { $context->registerEventListener(UserHomeSetupEvent::class, UserHomeSetupListener::class); $context->registerConfigLexicon(ConfigLexicon::class); + + $context->registerEventListener(RestrictInteractionEvent::class, RestrictInteractionListener::class); } #[\Override] diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index 943510edc8699..1c82c46f9fb82 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -724,10 +724,6 @@ public function createShare( $share->setSharedWith($shareWith); $share->setPermissions($permissions); } elseif ($shareType === IShare::TYPE_GROUP) { - if (!$this->shareManager->allowGroupSharing()) { - throw new OCSNotFoundException($this->l->t('Group sharing is disabled by the administrator')); - } - // Valid group is required to share if ($shareWith === null || !$this->groupManager->groupExists($shareWith)) { throw new OCSNotFoundException($this->l->t('Please specify a valid group')); @@ -737,11 +733,6 @@ public function createShare( } elseif ($shareType === IShare::TYPE_LINK || $shareType === IShare::TYPE_EMAIL) { - // Can we even share links? - if (!$this->shareManager->shareApiAllowLinks()) { - throw new OCSNotFoundException($this->l->t('Public link sharing is disabled by the administrator')); - } - $this->validateLinkSharePermissions($node, $permissions, $hasPublicUpload); $share->setPermissions($permissions); @@ -775,10 +766,6 @@ public function createShare( $share->setSendPasswordByTalk(true); } } elseif ($shareType === IShare::TYPE_REMOTE) { - if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) { - throw new OCSForbiddenException($this->l->t('Sharing %1$s failed because the back end does not allow shares from type %2$s', [$node->getPath(), $shareType])); - } - if ($shareWith === null) { throw new OCSNotFoundException($this->l->t('Please specify a valid federated account ID')); } @@ -787,10 +774,6 @@ public function createShare( $share->setPermissions($permissions); $share->setSharedWithDisplayName($this->getCachedFederatedDisplayName($shareWith, false)); } elseif ($shareType === IShare::TYPE_REMOTE_GROUP) { - if (!$this->shareManager->outgoingServer2ServerGroupSharesAllowed()) { - throw new OCSForbiddenException($this->l->t('Sharing %1$s failed because the back end does not allow shares from type %2$s', [$node->getPath(), $shareType])); - } - if ($shareWith === null) { throw new OCSNotFoundException($this->l->t('Please specify a valid federated group ID')); } @@ -1049,12 +1032,6 @@ private function validateLinkSharePermissions(Node $node, int $permissions, ?boo && ($this->hasPermission($permissions, Constants::PERMISSION_UPDATE) || $this->hasPermission($permissions, Constants::PERMISSION_DELETE))) { throw new OCSBadRequestException($this->l->t('Share must have READ permission if UPDATE or DELETE permission is set')); } - - // Check if public uploading was disabled - if ($this->hasPermission($permissions, Constants::PERMISSION_CREATE) - && !$this->shareManager->shareApiLinkAllowPublicUpload()) { - throw new OCSForbiddenException($this->l->t('Public upload disabled by the administrator')); - } } /** diff --git a/apps/files_sharing/lib/Listener/RestrictInteractionListener.php b/apps/files_sharing/lib/Listener/RestrictInteractionListener.php new file mode 100644 index 0000000000000..9f84a0620de14 --- /dev/null +++ b/apps/files_sharing/lib/Listener/RestrictInteractionListener.php @@ -0,0 +1,89 @@ + + */ +final class RestrictInteractionListener implements IEventListener { + private readonly IL10N $l10n; + + public function __construct( + private IRootFolder $rootFolder, + private IManager $manager, + IFactory $l10nFactory, + ) { + $this->l10n = $l10nFactory->get(Application::APP_ID); + } + + /** + * @param RestrictInteractionEvent $event + */ + #[\Override] + public function handle(Event $event): void { + if ($event->resource instanceof NodeResource && $event->action instanceof ShareAction) { + if (!$event->resource->getNode()->isShareable()) { + throw new InteractionRestrictedException('Node is not shareable.', $this->l10n->t('You are not allowed to share "%s".', [$event->resource->getNode()->getName()])); + } + + $userFolder = $this->rootFolder->getUserFolder($event->userId); + if ($event->resource->nodeId === $userFolder->getId()) { + throw new InteractionRestrictedException('Cannot share home folder node.', $this->l10n->t('You cannot share your home folder.')); + } + + if ($event->action->filesSharingPermissions !== null) { + if (($event->action->filesSharingPermissions & ~$event->resource->getNodePermissions()) !== 0) { + $path = $userFolder->getRelativePath($event->resource->getNode()->getPath()); + throw new InteractionRestrictedException('Cannot share node with more permissions than the node already has.', $this->l10n->t('You cannot share "%s" with more permission than you have yourself.', [$path])); + } + + if ($event->resource->getNode() instanceof File) { + if (($event->action->filesSharingPermissions & Constants::PERMISSION_DELETE) === Constants::PERMISSION_DELETE) { + throw new InteractionRestrictedException('Cannot share file node with delete permission.', $this->l10n->t('File cannot be shared with delete permission.')); + } + + if (($event->action->filesSharingPermissions & Constants::PERMISSION_CREATE) === Constants::PERMISSION_CREATE) { + throw new InteractionRestrictedException('Cannot share file node with create permission.', $this->l10n->t('File cannot be shared with create permission.')); + } + } + + if (!$event->receiver instanceof LinkReceiver + && !$event->receiver instanceof EmailReceiver + && ($event->action->filesSharingPermissions & Constants::PERMISSION_READ) !== Constants::PERMISSION_READ) { + throw new InteractionRestrictedException('No read permission on the share.', $this->l10n->t('File share needs at least read permission.')); + } + + if (($event->receiver instanceof LinkReceiver || $event->receiver instanceof EmailReceiver) + && $event->resource->getNode() instanceof Folder + && ($event->action->filesSharingPermissions & (Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE)) !== 0 + && !$this->manager->shareApiLinkAllowPublicUpload()) { + throw new InteractionRestrictedException('Public upload is not allowed.', $this->l10n->t('Public upload is not allowed.')); + } + } + } + } +} diff --git a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php index 89c2a7a7028f5..6aef44468f161 100644 --- a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php @@ -2040,7 +2040,6 @@ public function testCreateShareGroupNoValidShareWith(): void { $share = $this->newShare(); $this->shareManager->method('newShare')->willReturn($share); $this->shareManager->method('createShare')->willReturnArgument(0); - $this->shareManager->method('allowGroupSharing')->willReturn(true); [$userFolder, $path] = $this->getNonSharedUserFile(); $this->rootFolder->method('getUserFolder') @@ -2061,241 +2060,6 @@ public function testCreateShareGroupNoValidShareWith(): void { $this->ocs->createShare('valid-path', Constants::PERMISSION_ALL, IShare::TYPE_GROUP, 'invalidGroup'); } - public function testCreateShareGroup(): void { - $share = $this->newShare(); - $this->shareManager->method('newShare')->willReturn($share); - - /** @var ShareAPIController&MockObject $ocs */ - $ocs = $this->getMockBuilder(ShareAPIController::class) - ->setConstructorArgs([ - $this->appName, - $this->request, - $this->shareManager, - $this->groupManager, - $this->userManager, - $this->rootFolder, - $this->urlGenerator, - $this->l, - $this->config, - $this->appConfig, - $this->appManager, - $this->serverContainer, - $this->userStatusManager, - $this->previewManager, - $this->dateTimeZone, - $this->logger, - $this->factory, - $this->mailer, - $this->tagManager, - $this->getEmailValidatorWithStrictEmailCheck(), - $this->trustedServers, - $this->currentUser, - ])->onlyMethods(['formatShare']) - ->getMock(); - - $this->request - ->method('getParam') - ->willReturnMap([ - ['path', null, 'valid-path'], - ['permissions', null, Constants::PERMISSION_ALL], - ['shareType', '-1', IShare::TYPE_GROUP], - ['shareWith', null, 'validGroup'], - ]); - - [$userFolder, $path] = $this->getNonSharedUserFolder(); - $this->rootFolder->expects($this->exactly(2)) - ->method('getUserFolder') - ->with('currentUser') - ->willReturn($userFolder); - - $userFolder->expects($this->once()) - ->method('get') - ->with('valid-path') - ->willReturn($path); - $userFolder->method('getById') - ->willReturn([]); - - $this->groupManager->method('groupExists')->with('validGroup')->willReturn(true); - - $this->shareManager->expects($this->once()) - ->method('allowGroupSharing') - ->willReturn(true); - - $path->expects($this->once()) - ->method('lock') - ->with(ILockingProvider::LOCK_SHARED); - - $this->shareManager->method('createShare') - ->with($this->callback(function (IShare $share) use ($path) { - return $share->getNode() === $path - && $share->getPermissions() === Constants::PERMISSION_ALL - && $share->getShareType() === IShare::TYPE_GROUP - && $share->getSharedWith() === 'validGroup' - && $share->getSharedBy() === 'currentUser'; - })) - ->willReturnArgument(0); - - $expected = new DataResponse([]); - $result = $ocs->createShare('valid-path', Constants::PERMISSION_ALL, IShare::TYPE_GROUP, 'validGroup'); - - $this->assertInstanceOf(get_class($expected), $result); - $this->assertEquals($expected->getData(), $result->getData()); - } - - - public function testCreateShareGroupNotAllowed(): void { - $this->expectException(OCSNotFoundException::class); - $this->expectExceptionMessage('Group sharing is disabled by the administrator'); - - $share = $this->newShare(); - $this->shareManager->method('newShare')->willReturn($share); - - [$userFolder, $path] = $this->getNonSharedUserFolder(); - $this->rootFolder->method('getUserFolder') - ->with('currentUser') - ->willReturn($userFolder); - - $userFolder->expects($this->once()) - ->method('get') - ->with('valid-path') - ->willReturn($path); - $userFolder->method('getById') - ->willReturn([]); - - $this->groupManager->method('groupExists')->with('validGroup')->willReturn(true); - - $this->shareManager->expects($this->once()) - ->method('allowGroupSharing') - ->willReturn(false); - - $this->ocs->createShare('valid-path', Constants::PERMISSION_ALL, IShare::TYPE_GROUP, 'invalidGroup'); - } - - - public function testCreateShareLinkNoLinksAllowed(): void { - $this->expectException(OCSNotFoundException::class); - $this->expectExceptionMessage('Public link sharing is disabled by the administrator'); - - $this->request - ->method('getParam') - ->willReturnMap([ - ['path', null, 'valid-path'], - ['shareType', '-1', IShare::TYPE_LINK], - ]); - - $path = $this->getMockBuilder(Folder::class)->getMock(); - $path->method('getId')->willReturn(42); - $storage = $this->createMock(IStorage::class); - $storage->method('instanceOfStorage') - ->willReturnMap([ - ['OCA\Files_Sharing\External\Storage', false], - ['OCA\Files_Sharing\SharedStorage', false], - ]); - $path->method('getStorage')->willReturn($storage); - $this->rootFolder->method('getUserFolder')->with($this->currentUser)->willReturnSelf(); - $this->rootFolder->method('get')->with('valid-path')->willReturn($path); - $this->rootFolder->method('getById') - ->willReturn([]); - - $this->shareManager->method('newShare')->willReturn(Server::get(IManager::class)->newShare()); - $this->shareManager->method('shareApiLinkAllowPublicUpload')->willReturn(true); - $this->shareManager->method('shareApiAllowLinks')->willReturn(false); - - $this->ocs->createShare('valid-path', Constants::PERMISSION_ALL, IShare::TYPE_LINK); - } - - - public function testCreateShareLinkNoPublicUpload(): void { - $this->expectException(OCSForbiddenException::class); - $this->expectExceptionMessage('Public upload disabled by the administrator'); - - $path = $this->getMockBuilder(Folder::class)->getMock(); - $path->method('getId')->willReturn(42); - $storage = $this->createMock(IStorage::class); - $storage->method('instanceOfStorage') - ->willReturnMap([ - ['OCA\Files_Sharing\External\Storage', false], - ['OCA\Files_Sharing\SharedStorage', false], - ]); - $path->method('getStorage')->willReturn($storage); - $this->rootFolder->method('getUserFolder')->with($this->currentUser)->willReturnSelf(); - $this->rootFolder->method('get')->with('valid-path')->willReturn($path); - $this->rootFolder->method('getById') - ->willReturn([]); - - $this->shareManager->method('newShare')->willReturn(Server::get(IManager::class)->newShare()); - $this->shareManager->method('shareApiAllowLinks')->willReturn(true); - - $this->ocs->createShare('valid-path', Constants::PERMISSION_ALL, IShare::TYPE_LINK, null, 'true'); - } - - - public function testCreateShareLinkPublicUploadFile(): void { - $this->expectException(OCSBadRequestException::class); - $this->expectExceptionMessage('Public upload is only possible for publicly shared folders'); - - $storage = $this->createMock(IStorage::class); - $storage->method('instanceOfStorage') - ->willReturnMap([ - ['OCA\Files_Sharing\External\Storage', false], - ['OCA\Files_Sharing\SharedStorage', false], - ]); - - $file = $this->createMock(File::class); - $file->method('getId')->willReturn(42); - $file->method('getStorage')->willReturn($storage); - - $this->rootFolder->method('getUserFolder')->with($this->currentUser)->willReturnSelf(); - $this->rootFolder->method('get')->with('valid-path')->willReturn($file); - $this->rootFolder->method('getById') - ->willReturn([]); - - $this->shareManager->method('newShare')->willReturn(Server::get(IManager::class)->newShare()); - $this->shareManager->method('shareApiAllowLinks')->willReturn(true); - $this->shareManager->method('shareApiLinkAllowPublicUpload')->willReturn(true); - - $this->ocs->createShare('valid-path', Constants::PERMISSION_ALL, IShare::TYPE_LINK, null, 'true'); - } - - public function testCreateShareLinkPublicUploadFolder(): void { - $ocs = $this->mockFormatShare(); - - $path = $this->getMockBuilder(Folder::class)->getMock(); - $path->method('getId')->willReturn(1); - $storage = $this->createMock(IStorage::class); - $storage->method('instanceOfStorage') - ->willReturnMap([ - ['OCA\Files_Sharing\External\Storage', false], - ['OCA\Files_Sharing\SharedStorage', false], - ]); - $path->method('getStorage')->willReturn($storage); - $this->rootFolder->method('getUserFolder')->with($this->currentUser)->willReturnSelf(); - $this->rootFolder->method('get')->with('valid-path')->willReturn($path); - $this->rootFolder->method('getById') - ->willReturn([]); - - $this->shareManager->method('newShare')->willReturn(Server::get(IManager::class)->newShare()); - $this->shareManager->method('shareApiAllowLinks')->willReturn(true); - $this->shareManager->method('shareApiLinkAllowPublicUpload')->willReturn(true); - - $this->shareManager->expects($this->once())->method('createShare')->with( - $this->callback(function (IShare $share) use ($path) { - return $share->getNode() === $path - && $share->getShareType() === IShare::TYPE_LINK - && $share->getPermissions() === (Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE) - && $share->getSharedBy() === 'currentUser' - && $share->getPassword() === null - && $share->getExpirationDate() === null; - }) - )->willReturnArgument(0); - - $expected = new DataResponse([]); - $result = $ocs->createShare('valid-path', Constants::PERMISSION_ALL, IShare::TYPE_LINK, null, 'true', '', null, ''); - - $this->assertInstanceOf(get_class($expected), $result); - $this->assertEquals($expected->getData(), $result->getData()); - } - public function testCreateShareLinkPassword(): void { $ocs = $this->mockFormatShare(); @@ -2314,8 +2078,6 @@ public function testCreateShareLinkPassword(): void { ->willReturn([]); $this->shareManager->method('newShare')->willReturn(Server::get(IManager::class)->newShare()); - $this->shareManager->method('shareApiAllowLinks')->willReturn(true); - $this->shareManager->method('shareApiLinkAllowPublicUpload')->willReturn(true); $this->shareManager->expects($this->once())->method('createShare')->with( $this->callback(function (IShare $share) use ($path) { @@ -2353,8 +2115,6 @@ public function testCreateShareLinkSendPasswordByTalk(): void { ->willReturn([]); $this->shareManager->method('newShare')->willReturn(Server::get(IManager::class)->newShare()); - $this->shareManager->method('shareApiAllowLinks')->willReturn(true); - $this->shareManager->method('shareApiLinkAllowPublicUpload')->willReturn(true); $this->appManager->method('isEnabledForUser')->with('spreed')->willReturn(true); @@ -2400,8 +2160,6 @@ public function testCreateShareLinkSendPasswordByTalkWithTalkDisabled(): void { ->willReturn([]); $this->shareManager->method('newShare')->willReturn(Server::get(IManager::class)->newShare()); - $this->shareManager->method('shareApiAllowLinks')->willReturn(true); - $this->shareManager->method('shareApiLinkAllowPublicUpload')->willReturn(true); $this->appManager->method('isEnabledForUser')->with('spreed')->willReturn(false); @@ -2438,8 +2196,6 @@ public function testCreateShareValidExpireDate(): void { ->willReturn([]); $this->shareManager->method('newShare')->willReturn(Server::get(IManager::class)->newShare()); - $this->shareManager->method('shareApiAllowLinks')->willReturn(true); - $this->shareManager->method('shareApiLinkAllowPublicUpload')->willReturn(true); $this->shareManager->expects($this->once())->method('createShare')->with( $this->callback(function (IShare $share) use ($path) { @@ -2484,8 +2240,6 @@ public function testCreateShareInvalidExpireDate(): void { ->willReturn([]); $this->shareManager->method('newShare')->willReturn(Server::get(IManager::class)->newShare()); - $this->shareManager->method('shareApiAllowLinks')->willReturn(true); - $this->shareManager->method('shareApiLinkAllowPublicUpload')->willReturn(true); $ocs->createShare('valid-path', Constants::PERMISSION_ALL, IShare::TYPE_LINK, null, 'false', '', null, 'a1b2d3'); } @@ -2555,8 +2309,6 @@ public function testCreateShareRemote(): void { })) ->willReturnArgument(0); - $this->shareManager->method('outgoingServer2ServerSharesAllowed')->willReturn(true); - $expected = new DataResponse([]); $result = $ocs->createShare('valid-path', Constants::PERMISSION_ALL, IShare::TYPE_REMOTE, 'user@example.org'); @@ -2629,8 +2381,6 @@ public function testCreateShareRemoteGroup(): void { })) ->willReturnArgument(0); - $this->shareManager->method('outgoingServer2ServerGroupSharesAllowed')->willReturn(true); - $expected = new DataResponse([]); $result = $ocs->createShare('valid-path', Constants::PERMISSION_ALL, IShare::TYPE_REMOTE_GROUP, 'group@example.org'); @@ -3004,7 +2754,6 @@ public function testUpdateLinkShareSet(): void { ->setNode($folder); $this->shareManager->method('getShareById')->with('ocinternal:42')->willReturn($share); - $this->shareManager->method('shareApiLinkAllowPublicUpload')->willReturn(true); $this->shareManager->expects($this->once())->method('updateShare')->with( $this->callback(function (IShare $share) { @@ -3044,55 +2793,6 @@ public function testUpdateLinkShareSet(): void { $this->assertEquals($expected->getData(), $result->getData()); } - #[DataProvider(methodName: 'publicUploadParamsProvider')] - public function testUpdateLinkShareEnablePublicUpload($permissions, $publicUpload, $expireDate, $password): void { - $ocs = $this->mockFormatShare(); - - [$userFolder, $folder] = $this->getNonSharedUserFolder(); - $folder->method('getId') - ->willReturn(42); - - $share = Server::get(IManager::class)->newShare(); - $share->setPermissions(Constants::PERMISSION_ALL) - ->setSharedBy($this->currentUser) - ->setShareType(IShare::TYPE_LINK) - ->setPassword('password') - ->setNode($folder); - - $this->shareManager->method('getShareById')->with('ocinternal:42')->willReturn($share); - $this->shareManager->method('shareApiLinkAllowPublicUpload')->willReturn(true); - $this->shareManager->method('getSharedWith')->willReturn([]); - - $this->shareManager->expects($this->once())->method('updateShare')->with( - $this->callback(function (IShare $share) { - return $share->getPermissions() === (Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE) - && $share->getPassword() === 'password' - && $share->getExpirationDate() === null; - }) - )->willReturnArgument(0); - - $this->rootFolder->method('getUserFolder') - ->with($this->currentUser) - ->willReturn($userFolder); - - $userFolder->method('getById') - ->with(42) - ->willReturn([$folder]); - - $mountPoint = $this->createMock(IMountPoint::class); - $folder->method('getMountPoint') - ->willReturn($mountPoint); - $mountPoint->method('getStorageRootId') - ->willReturn(42); - - $expected = new DataResponse([]); - $result = $ocs->updateShare(42, $permissions, $password, null, $publicUpload, $expireDate); - - $this->assertInstanceOf(get_class($expected), $result); - $this->assertEquals($expected->getData(), $result->getData()); - } - - public static function publicLinkValidPermissionsProvider() { return [ [Constants::PERMISSION_CREATE], @@ -3119,7 +2819,6 @@ public function testUpdateLinkShareSetCRUDPermissions($permissions): void { ->setNode($folder); $this->shareManager->method('getShareById')->with('ocinternal:42')->willReturn($share); - $this->shareManager->method('shareApiLinkAllowPublicUpload')->willReturn(true); $this->shareManager->method('getSharedWith')->willReturn([]); $this->shareManager @@ -3202,56 +2901,10 @@ public function testUpdateLinkShareInvalidDate(): void { ->setNode($folder); $this->shareManager->method('getShareById')->with('ocinternal:42')->willReturn($share); - $this->shareManager->method('shareApiLinkAllowPublicUpload')->willReturn(true); $ocs->updateShare(42, null, 'password', null, 'true', '2000-01-a'); } - public static function publicUploadParamsProvider() { - return [ - [null, 'true', null, 'password'], - // legacy had no delete - [ - Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE, - 'true', null, 'password' - ], - // correct - [ - Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE, - null, null, 'password' - ], - ]; - } - - #[DataProvider(methodName: 'publicUploadParamsProvider')] - public function testUpdateLinkSharePublicUploadNotAllowed($permissions, $publicUpload, $expireDate, $password): void { - $this->expectException(OCSForbiddenException::class); - $this->expectExceptionMessage('Public upload disabled by the administrator'); - - $ocs = $this->mockFormatShare(); - [$userFolder, $folder] = $this->getNonSharedUserFolder(); - $userFolder->method('getById') - ->with(42) - ->willReturn([$folder]); - $this->rootFolder->method('getUserFolder') - ->with($this->currentUser) - ->willReturn($userFolder); - - $folder->method('getId')->willReturn(42); - - $share = Server::get(IManager::class)->newShare(); - $share->setPermissions(Constants::PERMISSION_ALL) - ->setSharedBy($this->currentUser) - ->setShareType(IShare::TYPE_LINK) - ->setNode($folder); - - $this->shareManager->method('getShareById')->with('ocinternal:42')->willReturn($share); - $this->shareManager->method('shareApiLinkAllowPublicUpload')->willReturn(false); - - $ocs->updateShare(42, $permissions, $password, null, $publicUpload, $expireDate); - } - - public function testUpdateLinkSharePublicUploadOnFile(): void { $this->expectException(OCSBadRequestException::class); $this->expectExceptionMessage('Public upload is only possible for publicly shared folders'); @@ -3279,9 +2932,6 @@ public function testUpdateLinkSharePublicUploadOnFile(): void { ->method('getShareById') ->with('ocinternal:42') ->willReturn($share); - $this->shareManager - ->method('shareApiLinkAllowPublicUpload') - ->willReturn(true); $this->shareManager ->method('updateShare') ->with($share) @@ -3649,7 +3299,6 @@ public function testUpdateLinkSharePublicUploadDoesNotChangeOther(): void { ->setNode($folder); $this->shareManager->method('getShareById')->with('ocinternal:42')->willReturn($share); - $this->shareManager->method('shareApiLinkAllowPublicUpload')->willReturn(true); $this->shareManager->expects($this->once())->method('updateShare')->with( $this->callback(function (IShare $share) use ($date) { @@ -3710,7 +3359,6 @@ public function testUpdateLinkSharePermissions(): void { ->setNode($folder); $this->shareManager->method('getShareById')->with('ocinternal:42')->willReturn($share); - $this->shareManager->method('shareApiLinkAllowPublicUpload')->willReturn(true); $this->shareManager->expects($this->once())->method('updateShare')->with( $this->callback(function (IShare $share) use ($date): bool { @@ -3770,7 +3418,6 @@ public function testUpdateLinkSharePermissionsShare(): void { ->setNode($folder); $this->shareManager->method('getShareById')->with('ocinternal:42')->willReturn($share); - $this->shareManager->method('shareApiLinkAllowPublicUpload')->willReturn(true); $this->shareManager->expects($this->once()) ->method('updateShare') @@ -3823,7 +3470,6 @@ public function testUpdateOtherPermissions(): void { ->setNode($file); $this->shareManager->method('getShareById')->with('ocinternal:42')->willReturn($share); - $this->shareManager->method('shareApiLinkAllowPublicUpload')->willReturn(true); $this->shareManager->expects($this->once())->method('updateShare')->with( $this->callback(function (IShare $share) { diff --git a/apps/files_sharing/tests/Listener/RestrictInteractionListenerTest.php b/apps/files_sharing/tests/Listener/RestrictInteractionListenerTest.php new file mode 100644 index 0000000000000..2cfba75691cc0 --- /dev/null +++ b/apps/files_sharing/tests/Listener/RestrictInteractionListenerTest.php @@ -0,0 +1,175 @@ +createUser('user', 'password'); + $this->assertNotFalse($user); + $this->user = $user; + + Server::get(ISetupManager::class)->setupForUser($user); + } + + #[\Override] + protected function tearDown(): void { + Server::get(ISetupManager::class)->tearDown(); + + $this->assertTrue($this->user->delete()); + + parent::tearDown(); + } + + public function testNodeResourceShareActionMissingSharePermission(): void { + $userFolder = Server::get(IRootFolder::class)->getUserFolder($this->user->getUID()); + + $fileNode = $userFolder->newFile('foo.txt', 'bar'); + $fileNode->getStorage()->getCache()->update($fileNode->getId(), ['permissions' => Constants::PERMISSION_ALL & ~Constants::PERMISSION_SHARE]); + $fileNode = $userFolder->getFirstNodeById($fileNode->getId()); + $this->assertNotNull($fileNode); + + $folderNode = $userFolder->newFolder('foo'); + $folderNode->getStorage()->getCache()->update($folderNode->getId(), ['permissions' => Constants::PERMISSION_ALL & ~Constants::PERMISSION_SHARE]); + $folderNode = $userFolder->getFirstNodeById($folderNode->getId()); + $this->assertNotNull($folderNode); + + foreach ([$fileNode, $folderNode] as $node) { + $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, new NodeResource($node->getId(), $this->user->getUID(), $node), new ShareAction(), null); + $this->assertEquals('You are not allowed to share "' . $node->getName() . '".', $event->isInteractionRestricted()); + } + } + + public function testNodeResourceShareActionNotHomeFolder(): void { + $userFolder = Server::get(IRootFolder::class)->getUserFolder($this->user->getUID()); + + $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, new NodeResource($userFolder->getId(), $this->user->getUID(), $userFolder), new ShareAction(), null); + $this->assertEquals('You cannot share your home folder.', $event->isInteractionRestricted()); + } + + public function testNodeResourceShareActionIncreasePermission(): void { + $userFolder = Server::get(IRootFolder::class)->getUserFolder($this->user->getUID()); + + $fileNode = $userFolder->newFile('foo.txt', 'bar'); + $fileNode->getStorage()->getCache()->update($fileNode->getId(), ['permissions' => Constants::PERMISSION_READ | Constants::PERMISSION_SHARE]); + + $folderNode = $userFolder->newFolder('foo'); + $folderNode->getStorage()->getCache()->update($folderNode->getId(), ['permissions' => Constants::PERMISSION_READ | Constants::PERMISSION_SHARE]); + + foreach ([$fileNode, $folderNode] as $node) { + $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, new NodeResource($node->getId(), $this->user->getUID(), $node), new ShareAction(Constants::PERMISSION_READ | Constants::PERMISSION_SHARE | Constants::PERMISSION_UPDATE), null); + $this->assertEquals('You cannot share "/' . $node->getName() . '" with more permission than you have yourself.', $event->isInteractionRestricted()); + } + } + + public function testNodeResourceShareActionFileHasDeletePermission(): void { + $userFolder = Server::get(IRootFolder::class)->getUserFolder($this->user->getUID()); + + $node = $userFolder->newFile('foo.txt', 'bar'); + $node->getStorage()->getCache()->update($node->getId(), ['permissions' => Constants::PERMISSION_ALL]); + + $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, new NodeResource($node->getId(), $this->user->getUID(), $node), new ShareAction(Constants::PERMISSION_DELETE), null); + $this->assertEquals('File cannot be shared with delete permission.', $event->isInteractionRestricted()); + } + + public function testNodeResourceShareActionFileHasCreatePermission(): void { + $userFolder = Server::get(IRootFolder::class)->getUserFolder($this->user->getUID()); + + $node = $userFolder->newFile('foo.txt', 'bar'); + $node->getStorage()->getCache()->update($node->getId(), ['permissions' => Constants::PERMISSION_ALL]); + + $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, new NodeResource($node->getId(), $this->user->getUID(), $node), new ShareAction(Constants::PERMISSION_CREATE), null); + $this->assertEquals('File cannot be shared with create permission.', $event->isInteractionRestricted()); + } + + /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ + public function testNodeResourceShareActionNoLinkEmailReceiverMissingReadPermission(): void { + $config = Server::get(IConfig::class); + // Defaults to disabled, so we need to enable it to test the RemoteGroupReceiver. + $config->setAppValue('files_sharing', 'outgoing_server2server_group_share_enabled', 'yes'); + + $userFolder = Server::get(IRootFolder::class)->getUserFolder($this->user->getUID()); + + $node = $userFolder->newFolder('foo'); + $node->getStorage()->getCache()->update($node->getId(), ['permissions' => Constants::PERMISSION_ALL]); + + $resource = new NodeResource($node->getId(), $this->user->getUID(), $node); + + foreach ([ + new CircleReceiver(''), + new DeckReceiver(0), + new GroupReceiver(''), + new RemoteGroupReceiver(''), + new RemoteUserReceiver(''), + new RoomReceiver(''), + new UserReceiver(''), + ] as $receiver) { + $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, $resource, new ShareAction(Constants::PERMISSION_ALL & ~Constants::PERMISSION_READ), $receiver); + $this->assertEquals('File share needs at least read permission.', $event->isInteractionRestricted()); + } + + $config->deleteAppValue('files_sharing', 'outgoing_server2server_group_share_enabled'); + } + + /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ + public function testNodeResourceShareActionLinkEmailReceiverPublicUploadDisabled(): void { + $config = Server::get(IConfig::class); + $config->setAppValue('core', 'shareapi_allow_public_upload', 'no'); + + $userFolder = Server::get(IRootFolder::class)->getUserFolder($this->user->getUID()); + + $node = $userFolder->newFolder('foo'); + $node->getStorage()->getCache()->update($node->getId(), ['permissions' => Constants::PERMISSION_ALL]); + + $resource = new NodeResource($node->getId(), $this->user->getUID(), $node); + + foreach ([ + new LinkReceiver(), + new EmailReceiver('test@example.org'), + ] as $receiver) { + foreach ([ + Constants::PERMISSION_CREATE, + Constants::PERMISSION_UPDATE, + Constants::PERMISSION_DELETE, + ] as $permissions) { + $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, $resource, new ShareAction($permissions), $receiver); + $this->assertEquals('Public upload is not allowed.', $event->isInteractionRestricted()); + } + } + + $config->deleteAppValue('core', 'shareapi_allow_public_upload'); + } +} diff --git a/build/integration/sharing_features/sharing-v1-part2.feature b/build/integration/sharing_features/sharing-v1-part2.feature index 02bb84750f278..aa9a2390b6e39 100644 --- a/build/integration/sharing_features/sharing-v1-part2.feature +++ b/build/integration/sharing_features/sharing-v1-part2.feature @@ -657,7 +657,7 @@ Scenario: getting all shares of a file with a received share after revoking the | shareType | 0 | | shareWith | user2 | | permissions | 31 | - Then the OCS status code should be "404" + Then the OCS status code should be "403" And the HTTP status code should be "200" Scenario: User is allowed to reshare file with more permissions if shares of same file to same user have them @@ -708,7 +708,7 @@ Scenario: getting all shares of a file with a received share after revoking the | shareType | 0 | | shareWith | user2 | | permissions | 31 | - Then the OCS status code should be "404" + Then the OCS status code should be "403" And the HTTP status code should be "200" Scenario: User is not allowed to reshare file with more permissions even if shares of same file to other users have them @@ -737,7 +737,7 @@ Scenario: getting all shares of a file with a received share after revoking the | shareType | 0 | | shareWith | user2 | | permissions | 19 | - Then the OCS status code should be "404" + Then the OCS status code should be "403" And the HTTP status code should be "200" Scenario: User is not allowed to reshare file with more permissions even if shares of other files from same user have them @@ -765,7 +765,7 @@ Scenario: getting all shares of a file with a received share after revoking the | shareType | 0 | | shareWith | user2 | | permissions | 19 | - Then the OCS status code should be "404" + Then the OCS status code should be "403" And the HTTP status code should be "200" Scenario: User is not allowed to reshare file with more permissions even if shares of other files from other users have them @@ -794,7 +794,7 @@ Scenario: getting all shares of a file with a received share after revoking the | shareType | 0 | | shareWith | user2 | | permissions | 19 | - Then the OCS status code should be "404" + Then the OCS status code should be "403" And the HTTP status code should be "200" Scenario: download restrictions can not be dropped @@ -888,7 +888,7 @@ Scenario: getting all shares of a file with a received share after revoking the | shareType | 0 | | shareWith | user2 | | permissions | 25 | - Then the OCS status code should be "404" + Then the OCS status code should be "403" And the HTTP status code should be "200" Scenario: User is not allowed to reshare file with additional delete permissions for files @@ -1197,7 +1197,7 @@ Scenario: getting all shares of a file with a received share after revoking the | permissions | 21 | When Updating last share with | permissions | 31 | - Then the OCS status code should be "404" + Then the OCS status code should be "403" And the HTTP status code should be "200" Scenario: Do not allow reshare to exceed permissions even if shares of same file to other users have them @@ -1229,7 +1229,7 @@ Scenario: getting all shares of a file with a received share after revoking the | permissions | 21 | When Updating last share with | permissions | 31 | - Then the OCS status code should be "404" + Then the OCS status code should be "403" And the HTTP status code should be "200" Scenario: Do not allow reshare to exceed permissions even if shares of other files from same user have them @@ -1260,7 +1260,7 @@ Scenario: getting all shares of a file with a received share after revoking the | permissions | 21 | When Updating last share with | permissions | 31 | - Then the OCS status code should be "404" + Then the OCS status code should be "403" And the HTTP status code should be "200" Scenario: Do not allow reshare to exceed permissions even if shares of other files from other users have them @@ -1292,7 +1292,7 @@ Scenario: getting all shares of a file with a received share after revoking the | permissions | 21 | When Updating last share with | permissions | 31 | - Then the OCS status code should be "404" + Then the OCS status code should be "403" And the HTTP status code should be "200" Scenario: Do not allow sub reshare to exceed permissions @@ -1316,7 +1316,7 @@ Scenario: getting all shares of a file with a received share after revoking the | permissions | 21 | When Updating last share with | permissions | 31 | - Then the OCS status code should be "404" + Then the OCS status code should be "403" And the HTTP status code should be "200" Scenario: Only allow 1 link share per file/folder diff --git a/build/integration/sharing_features/sharing-v1-part3.feature b/build/integration/sharing_features/sharing-v1-part3.feature index 1fe545ed1a80b..8f04b78249ebb 100644 --- a/build/integration/sharing_features/sharing-v1-part3.feature +++ b/build/integration/sharing_features/sharing-v1-part3.feature @@ -449,7 +449,7 @@ Feature: sharing | shareType | 3 | And Updating last share with | publicUpload | true | - Then the OCS status code should be "404" + Then the OCS status code should be "403" And the HTTP status code should be "200" Scenario: do not allow to increase link share permissions on sub reshare @@ -471,7 +471,7 @@ Feature: sharing | shareType | 3 | And Updating last share with | publicUpload | true | - Then the OCS status code should be "404" + Then the OCS status code should be "403" And the HTTP status code should be "200" Scenario: deleting file out of a share as recipient creates a backup for the owner diff --git a/build/rector-strict.php b/build/rector-strict.php index 1a948753acb49..c7a4528ce9c92 100644 --- a/build/rector-strict.php +++ b/build/rector-strict.php @@ -5,6 +5,9 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +use Rector\Php81\Rector\Property\ReadOnlyPropertyRector; +use Rector\Php82\Rector\Class_\ReadOnlyClassRector; + $nextcloudDir = dirname(__DIR__); return (require __DIR__ . '/rector-shared.php') @@ -25,6 +28,14 @@ $nextcloudDir . '/build/psalm/ITypedQueryBuilderTest.php', $nextcloudDir . '/lib/private/DB/QueryBuilder/TypedQueryBuilder.php', $nextcloudDir . '/lib/public/DB/QueryBuilder/ITypedQueryBuilder.php', + $nextcloudDir . '/lib/public/Interaction', + $nextcloudDir . '/tests/lib/Interaction', + $nextcloudDir . '/apps/files/lib/Listener/RestrictInteractionListener.php', + $nextcloudDir . '/apps/files/tests/Listener/RestrictInteractionListenerTest.php', + $nextcloudDir . '/apps/files_sharing/lib/Listener/RestrictInteractionListener.php', + $nextcloudDir . '/apps/files_sharing/tests/Listener/RestrictInteractionListenerTest.php', + $nextcloudDir . '/core/Listener/RestrictInteractionListener.php', + $nextcloudDir . '/tests/Core/Listener/RestrictInteractionListenerTest.php', ]) ->withAutoloadPaths([ // ensure rector properly autoload the public interfaces @@ -46,4 +57,13 @@ symfonyConfigs: true, )->withPhpSets( php82: true, - ); + )->withSkip([ + ReadOnlyPropertyRector::class => [ + $nextcloudDir . '/core/Listener/RestrictInteractionListener.php', + $nextcloudDir . '/apps/files_sharing/lib/Listener/RestrictInteractionListener.php', + ], + ReadOnlyClassRector::class => [ + $nextcloudDir . '/core/Listener/RestrictInteractionListener.php', + $nextcloudDir . '/apps/files_sharing/lib/Listener/RestrictInteractionListener.php', + ], + ]); diff --git a/core/AppInfo/Application.php b/core/AppInfo/Application.php index 15cf42c4a5505..08e1ff95a3dc7 100644 --- a/core/AppInfo/Application.php +++ b/core/AppInfo/Application.php @@ -21,6 +21,7 @@ use OC\Core\Listener\AddMissingPrimaryKeyListener; use OC\Core\Listener\BeforeTemplateRenderedListener; use OC\Core\Listener\PasswordUpdatedListener; +use OC\Core\Listener\RestrictInteractionListener; use OC\Core\Notification\CoreNotifier; use OC\OCM\OCMDiscoveryHandler; use OC\TagManager; @@ -32,6 +33,7 @@ use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; use OCP\DB\Events\AddMissingIndicesEvent; use OCP\DB\Events\AddMissingPrimaryKeyEvent; +use OCP\Interaction\RestrictInteractionEvent; use OCP\User\Events\BeforeUserDeletedEvent; use OCP\User\Events\PasswordUpdatedEvent; use OCP\User\Events\UserDeletedEvent; @@ -89,6 +91,8 @@ public function register(IRegistrationContext $context): void { $context->registerWellKnownHandler(OCMDiscoveryHandler::class); $context->registerCapability(Capabilities::class); + + $context->registerEventListener(RestrictInteractionEvent::class, RestrictInteractionListener::class); } #[\Override] diff --git a/core/Listener/RestrictInteractionListener.php b/core/Listener/RestrictInteractionListener.php new file mode 100644 index 0000000000000..1836b366b3b2f --- /dev/null +++ b/core/Listener/RestrictInteractionListener.php @@ -0,0 +1,93 @@ + + */ +final class RestrictInteractionListener implements IEventListener { + private readonly IL10N $l10n; + + public function __construct( + private IManager $manager, + private IGroupManager $groupManager, + IFactory $l10nFactory, + ) { + $this->l10n = $l10nFactory->get(Application::APP_ID); + } + + /** + * @param RestrictInteractionEvent $event + */ + #[\Override] + public function handle(Event $event): void { + if ($event->action instanceof ShareAction) { + if (!$this->manager->shareApiEnabled()) { + throw new InteractionRestrictedException('Sharing is not allowed.', $this->l10n->t('Sharing is not allowed.')); + } + + if ($this->manager->sharingDisabledForUser($event->userId)) { + throw new InteractionRestrictedException('Sharing is not allowed for the user.', $this->l10n->t('Sharing is not allowed for you.')); + } + + if ($this->manager->shareWithGroupMembersOnly()) { + if ($event->receiver instanceof UserReceiver) { + $groups = array_intersect( + $this->groupManager->getUserGroupIds($event->getUser()), + $this->groupManager->getUserGroupIds($event->receiver->getUser()), + ); + + $groups = array_diff($groups, $this->manager->shareWithGroupMembersOnlyExcludeGroupsList()); + + if ($groups === []) { + throw new InteractionRestrictedException('Sharing is only allowed with group members.', $this->l10n->t('Sharing is only allowed with group members.')); + } + } + + if ($event->receiver instanceof GroupReceiver && (!$event->receiver->getGroup()->inGroup($event->getUser()) || in_array($event->receiver->getGroup()->getGID(), $this->manager->shareWithGroupMembersOnlyExcludeGroupsList(), true))) { + throw new InteractionRestrictedException('Sharing is only allowed to the groups the user is a member of.', $this->l10n->t('Sharing is only allowed within your own groups.')); + } + } + + if ($event->receiver instanceof GroupReceiver && !$this->manager->allowGroupSharing()) { + throw new InteractionRestrictedException('Group sharing is not allowed.', $this->l10n->t('Group sharing is not allowed.')); + } + + if (($event->receiver instanceof LinkReceiver || $event->receiver instanceof EmailReceiver) && !$this->manager->shareApiAllowLinks($event->getUser())) { + throw new InteractionRestrictedException('Public link sharing is not allowed.', $this->l10n->t('Public link sharing is not allowed.')); + } + + if ($event->receiver instanceof RemoteUserReceiver && !$this->manager->outgoingServer2ServerSharesAllowed()) { + throw new InteractionRestrictedException('Sharing to remote users is not allowed.', $this->l10n->t('Sharing to remote users is not allowed.')); + } + + if ($event->receiver instanceof RemoteGroupReceiver && !$this->manager->outgoingServer2ServerGroupSharesAllowed()) { + throw new InteractionRestrictedException('Sharing to remote groups is not allowed.', $this->l10n->t('Sharing to remote groups is not allowed.')); + } + } + } +} diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 1f29368f8ab38..720a423e73c93 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -665,6 +665,22 @@ 'OCP\\IUserSession' => $baseDir . '/lib/public/IUserSession.php', 'OCP\\Image' => $baseDir . '/lib/public/Image.php', 'OCP\\Install\\Events\\InstallationCompletedEvent' => $baseDir . '/lib/public/Install/Events/InstallationCompletedEvent.php', + 'OCP\\Interaction\\Actions\\ShareAction' => $baseDir . '/lib/public/Interaction/Actions/ShareAction.php', + 'OCP\\Interaction\\InteractionAction' => $baseDir . '/lib/public/Interaction/InteractionAction.php', + 'OCP\\Interaction\\InteractionReceiver' => $baseDir . '/lib/public/Interaction/InteractionReceiver.php', + 'OCP\\Interaction\\InteractionResource' => $baseDir . '/lib/public/Interaction/InteractionResource.php', + 'OCP\\Interaction\\InteractionRestrictedException' => $baseDir . '/lib/public/Interaction/InteractionRestrictedException.php', + 'OCP\\Interaction\\Receivers\\CircleReceiver' => $baseDir . '/lib/public/Interaction/Receivers/CircleReceiver.php', + 'OCP\\Interaction\\Receivers\\DeckReceiver' => $baseDir . '/lib/public/Interaction/Receivers/DeckReceiver.php', + 'OCP\\Interaction\\Receivers\\EmailReceiver' => $baseDir . '/lib/public/Interaction/Receivers/EmailReceiver.php', + 'OCP\\Interaction\\Receivers\\GroupReceiver' => $baseDir . '/lib/public/Interaction/Receivers/GroupReceiver.php', + 'OCP\\Interaction\\Receivers\\LinkReceiver' => $baseDir . '/lib/public/Interaction/Receivers/LinkReceiver.php', + 'OCP\\Interaction\\Receivers\\RemoteGroupReceiver' => $baseDir . '/lib/public/Interaction/Receivers/RemoteGroupReceiver.php', + 'OCP\\Interaction\\Receivers\\RemoteUserReceiver' => $baseDir . '/lib/public/Interaction/Receivers/RemoteUserReceiver.php', + 'OCP\\Interaction\\Receivers\\RoomReceiver' => $baseDir . '/lib/public/Interaction/Receivers/RoomReceiver.php', + 'OCP\\Interaction\\Receivers\\UserReceiver' => $baseDir . '/lib/public/Interaction/Receivers/UserReceiver.php', + 'OCP\\Interaction\\Resources\\NodeResource' => $baseDir . '/lib/public/Interaction/Resources/NodeResource.php', + 'OCP\\Interaction\\RestrictInteractionEvent' => $baseDir . '/lib/public/Interaction/RestrictInteractionEvent.php', 'OCP\\L10N\\IFactory' => $baseDir . '/lib/public/L10N/IFactory.php', 'OCP\\L10N\\ILanguageIterator' => $baseDir . '/lib/public/L10N/ILanguageIterator.php', 'OCP\\LDAP\\Exceptions\\MultipleUsersReturnedException' => $baseDir . '/lib/public/LDAP/Exceptions/MultipleUsersReturnedException.php', @@ -1523,6 +1539,7 @@ 'OC\\Core\\Listener\\BeforeTemplateRenderedListener' => $baseDir . '/core/Listener/BeforeTemplateRenderedListener.php', 'OC\\Core\\Listener\\FeedBackHandler' => $baseDir . '/core/Listener/FeedBackHandler.php', 'OC\\Core\\Listener\\PasswordUpdatedListener' => $baseDir . '/core/Listener/PasswordUpdatedListener.php', + 'OC\\Core\\Listener\\RestrictInteractionListener' => $baseDir . '/core/Listener/RestrictInteractionListener.php', 'OC\\Core\\Middleware\\TwoFactorMiddleware' => $baseDir . '/core/Middleware/TwoFactorMiddleware.php', 'OC\\Core\\Migrations\\Version13000Date20170705121758' => $baseDir . '/core/Migrations/Version13000Date20170705121758.php', 'OC\\Core\\Migrations\\Version13000Date20170718121200' => $baseDir . '/core/Migrations/Version13000Date20170718121200.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 25b1877dade90..1dcb34f99402c 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -706,6 +706,22 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\IUserSession' => __DIR__ . '/../../..' . '/lib/public/IUserSession.php', 'OCP\\Image' => __DIR__ . '/../../..' . '/lib/public/Image.php', 'OCP\\Install\\Events\\InstallationCompletedEvent' => __DIR__ . '/../../..' . '/lib/public/Install/Events/InstallationCompletedEvent.php', + 'OCP\\Interaction\\Actions\\ShareAction' => __DIR__ . '/../../..' . '/lib/public/Interaction/Actions/ShareAction.php', + 'OCP\\Interaction\\InteractionAction' => __DIR__ . '/../../..' . '/lib/public/Interaction/InteractionAction.php', + 'OCP\\Interaction\\InteractionReceiver' => __DIR__ . '/../../..' . '/lib/public/Interaction/InteractionReceiver.php', + 'OCP\\Interaction\\InteractionResource' => __DIR__ . '/../../..' . '/lib/public/Interaction/InteractionResource.php', + 'OCP\\Interaction\\InteractionRestrictedException' => __DIR__ . '/../../..' . '/lib/public/Interaction/InteractionRestrictedException.php', + 'OCP\\Interaction\\Receivers\\CircleReceiver' => __DIR__ . '/../../..' . '/lib/public/Interaction/Receivers/CircleReceiver.php', + 'OCP\\Interaction\\Receivers\\DeckReceiver' => __DIR__ . '/../../..' . '/lib/public/Interaction/Receivers/DeckReceiver.php', + 'OCP\\Interaction\\Receivers\\EmailReceiver' => __DIR__ . '/../../..' . '/lib/public/Interaction/Receivers/EmailReceiver.php', + 'OCP\\Interaction\\Receivers\\GroupReceiver' => __DIR__ . '/../../..' . '/lib/public/Interaction/Receivers/GroupReceiver.php', + 'OCP\\Interaction\\Receivers\\LinkReceiver' => __DIR__ . '/../../..' . '/lib/public/Interaction/Receivers/LinkReceiver.php', + 'OCP\\Interaction\\Receivers\\RemoteGroupReceiver' => __DIR__ . '/../../..' . '/lib/public/Interaction/Receivers/RemoteGroupReceiver.php', + 'OCP\\Interaction\\Receivers\\RemoteUserReceiver' => __DIR__ . '/../../..' . '/lib/public/Interaction/Receivers/RemoteUserReceiver.php', + 'OCP\\Interaction\\Receivers\\RoomReceiver' => __DIR__ . '/../../..' . '/lib/public/Interaction/Receivers/RoomReceiver.php', + 'OCP\\Interaction\\Receivers\\UserReceiver' => __DIR__ . '/../../..' . '/lib/public/Interaction/Receivers/UserReceiver.php', + 'OCP\\Interaction\\Resources\\NodeResource' => __DIR__ . '/../../..' . '/lib/public/Interaction/Resources/NodeResource.php', + 'OCP\\Interaction\\RestrictInteractionEvent' => __DIR__ . '/../../..' . '/lib/public/Interaction/RestrictInteractionEvent.php', 'OCP\\L10N\\IFactory' => __DIR__ . '/../../..' . '/lib/public/L10N/IFactory.php', 'OCP\\L10N\\ILanguageIterator' => __DIR__ . '/../../..' . '/lib/public/L10N/ILanguageIterator.php', 'OCP\\LDAP\\Exceptions\\MultipleUsersReturnedException' => __DIR__ . '/../../..' . '/lib/public/LDAP/Exceptions/MultipleUsersReturnedException.php', @@ -1564,6 +1580,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Listener\\BeforeTemplateRenderedListener' => __DIR__ . '/../../..' . '/core/Listener/BeforeTemplateRenderedListener.php', 'OC\\Core\\Listener\\FeedBackHandler' => __DIR__ . '/../../..' . '/core/Listener/FeedBackHandler.php', 'OC\\Core\\Listener\\PasswordUpdatedListener' => __DIR__ . '/../../..' . '/core/Listener/PasswordUpdatedListener.php', + 'OC\\Core\\Listener\\RestrictInteractionListener' => __DIR__ . '/../../..' . '/core/Listener/RestrictInteractionListener.php', 'OC\\Core\\Middleware\\TwoFactorMiddleware' => __DIR__ . '/../../..' . '/core/Middleware/TwoFactorMiddleware.php', 'OC\\Core\\Migrations\\Version13000Date20170705121758' => __DIR__ . '/../../..' . '/core/Migrations/Version13000Date20170705121758.php', 'OC\\Core\\Migrations\\Version13000Date20170718121200' => __DIR__ . '/../../..' . '/core/Migrations/Version13000Date20170718121200.php', diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 9d68aa23653c5..44026cdf1dd7e 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -25,7 +25,6 @@ use OCP\Files\Folder; use OCP\Files\IRootFolder; use OCP\Files\Mount\IMountManager; -use OCP\Files\Mount\IMovableMount; use OCP\Files\Mount\IShareOwnerlessMount; use OCP\Files\Node; use OCP\Files\NotFoundException; @@ -36,6 +35,18 @@ use OCP\IDBConnection; use OCP\IGroupManager; use OCP\IL10N; +use OCP\Interaction\Actions\ShareAction; +use OCP\Interaction\Receivers\CircleReceiver; +use OCP\Interaction\Receivers\DeckReceiver; +use OCP\Interaction\Receivers\EmailReceiver; +use OCP\Interaction\Receivers\GroupReceiver; +use OCP\Interaction\Receivers\LinkReceiver; +use OCP\Interaction\Receivers\RemoteGroupReceiver; +use OCP\Interaction\Receivers\RemoteUserReceiver; +use OCP\Interaction\Receivers\RoomReceiver; +use OCP\Interaction\Receivers\UserReceiver; +use OCP\Interaction\Resources\NodeResource; +use OCP\Interaction\RestrictInteractionEvent; use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; @@ -145,7 +156,7 @@ protected function verifyPassword(?string $password): void { * * @suppress PhanUndeclaredClassMethod */ - protected function generalCreateChecks(IShare $share, bool $isUpdate = false): void { + protected function generalChecks(IShare $share): void { if ($share->getShareType() === IShare::TYPE_USER) { // We expect a valid user as sharedWith for user shares if (!$this->userManager->userExists($share->getSharedWith())) { @@ -208,21 +219,6 @@ protected function generalCreateChecks(IShare $share, bool $isUpdate = false): v throw new \InvalidArgumentException($this->l->t('Shared path must be either a file or a folder')); } - // And you cannot share your rootfolder - if ($this->userManager->userExists($share->getSharedBy())) { - $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); - } else { - $userFolder = $this->rootFolder->getUserFolder($share->getShareOwner()); - } - if ($userFolder->getId() === $share->getNode()->getId()) { - throw new \InvalidArgumentException($this->l->t('You cannot share your root folder')); - } - - // Check if we actually have share permissions - if (!$share->getNode()->isShareable()) { - throw new GenericShareException($this->l->t('You are not allowed to share %s', [$share->getNode()->getName()]), code: 404); - } - // Permissions should be set if ($share->getPermissions() === null) { throw new \InvalidArgumentException($this->l->t('Valid permissions are required for sharing')); @@ -233,45 +229,42 @@ protected function generalCreateChecks(IShare $share, bool $isUpdate = false): v throw new \InvalidArgumentException($this->l->t('Valid permissions are required for sharing')); } - // Single file shares should never have delete or create permissions - if (($share->getNode() instanceof File) - && (($share->getPermissions() & (Constants::PERMISSION_CREATE | Constants::PERMISSION_DELETE)) !== 0)) { - throw new \InvalidArgumentException($this->l->t('File shares cannot have create or delete permissions')); - } + $action = new ShareAction($share->getPermissions()); + $receiver = match ($share->getShareType()) { + IShare::TYPE_USER => new UserReceiver($share->getSharedWith()), + IShare::TYPE_GROUP => new GroupReceiver($share->getSharedWith()), + IShare::TYPE_LINK => new LinkReceiver(), + IShare::TYPE_EMAIL => new EmailReceiver($share->getSharedWith()), + IShare::TYPE_REMOTE => new RemoteUserReceiver($share->getSharedWith()), + IShare::TYPE_CIRCLE => new CircleReceiver($share->getSharedWith()), + IShare::TYPE_REMOTE_GROUP => new RemoteGroupReceiver($share->getSharedWith()), + IShare::TYPE_ROOM => new RoomReceiver($share->getSharedWith()), + IShare::TYPE_DECK => new DeckReceiver((int)$share->getSharedWith()), + default => throw new \InvalidArgumentException('Unknown share type.'), + }; - $permissions = 0; - $nodesForUser = $userFolder->getById($share->getNodeId()); - foreach ($nodesForUser as $node) { - if ($node->getInternalPath() === '' && !$node->getMountPoint() instanceof IMovableMount) { - // for the root of non-movable mount, the permissions we see if limited by the mount itself, - // so we instead use the "raw" permissions from the storage - $permissions |= $node->getStorage()->getPermissions(''); - } else { - $permissions |= $node->getPermissions(); - } + $users = []; + if ($share->getShareOwner() !== null && ($user = $this->userManager->get($share->getShareOwner())) !== null) { + $users[] = $user; } - - // Check that we do not share with more permissions than we have - if ($share->getPermissions() & ~$permissions) { - $path = $userFolder->getRelativePath($share->getNode()->getPath()); - throw new GenericShareException($this->l->t('Cannot increase permissions of %s', [$path]), code: 404); + if ($share->getSharedBy() !== $share->getShareOwner() && ($user = $this->userManager->get($share->getSharedBy())) !== null) { + $users[] = $user; } - // Check that read permissions are always set - // Link shares are allowed to have no read permissions to allow upload to hidden folders - $noReadPermissionRequired = $share->getShareType() === IShare::TYPE_LINK - || $share->getShareType() === IShare::TYPE_EMAIL; - if (!$noReadPermissionRequired - && ($share->getPermissions() & Constants::PERMISSION_READ) === 0) { - throw new \InvalidArgumentException($this->l->t('Shares need at least read permissions')); + if ($users === []) { + throw new \RuntimeException('Cannot check sharing restrictions.'); } - if ($share->getNode() instanceof File) { - if ($share->getPermissions() & Constants::PERMISSION_DELETE) { - throw new GenericShareException($this->l->t('Files cannot be shared with delete permissions')); - } - if ($share->getPermissions() & Constants::PERMISSION_CREATE) { - throw new GenericShareException($this->l->t('Files cannot be shared with create permissions')); + foreach ($users as $user) { + $resource = new NodeResource($share->getNodeId(), $user->getUID()); + $event = new RestrictInteractionEvent($user->getUID(), $user, $resource, $action, $receiver); + try { + $isRestricted = $event->isInteractionRestricted(); + if ($isRestricted !== false) { + throw new GenericShareException($isRestricted, code: 403); + } + } catch (\Exception $exception) { + throw new GenericShareException($exception->getMessage(), $exception instanceof HintException ? $exception->getHint() : '', code: 403); } } } @@ -456,25 +449,6 @@ protected function validateExpirationDateLink(IShare $share): IShare { * @throws \Exception */ protected function userCreateChecks(IShare $share): void { - // Check if we can share with group members only - if ($this->shareWithGroupMembersOnly()) { - $sharedBy = $this->userManager->get($share->getSharedBy()); - $sharedWith = $this->userManager->get($share->getSharedWith()); - // Verify we can share with this user - $groups = array_intersect( - $this->groupManager->getUserGroupIds($sharedBy), - $this->groupManager->getUserGroupIds($sharedWith) - ); - - // optional excluded groups - $excludedGroups = $this->shareWithGroupMembersOnlyExcludeGroupsList(); - $groups = array_diff($groups, $excludedGroups); - - if (empty($groups)) { - throw new \Exception($this->l->t('Sharing is only allowed with group members')); - } - } - /* * TODO: Could be costly, fix * @@ -517,23 +491,6 @@ protected function userCreateChecks(IShare $share): void { * @throws \Exception */ protected function groupCreateChecks(IShare $share): void { - // Verify group shares are allowed - if (!$this->allowGroupSharing()) { - throw new \Exception($this->l->t('Group sharing is now allowed')); - } - - // Verify if the user can share with this group - if ($this->shareWithGroupMembersOnly()) { - $sharedBy = $this->userManager->get($share->getSharedBy()); - $sharedWith = $this->groupManager->get($share->getSharedWith()); - - // optional excluded groups - $excludedGroups = $this->shareWithGroupMembersOnlyExcludeGroupsList(); - if (is_null($sharedWith) || in_array($share->getSharedWith(), $excludedGroups) || !$sharedWith->inGroup($sharedBy)) { - throw new \Exception($this->l->t('Sharing is only allowed within your own groups')); - } - } - /* * TODO: Could be costly, fix * @@ -556,24 +513,6 @@ protected function groupCreateChecks(IShare $share): void { } } - /** - * Check for pre share requirements for link shares - * - * @throws \Exception - */ - protected function linkCreateChecks(IShare $share): void { - // Are link shares allowed? - if (!$this->shareApiAllowLinks()) { - throw new \Exception($this->l->t('Link sharing is not allowed')); - } - - // Check if public upload is allowed - if ($share->getNodeType() === 'folder' && !$this->shareApiLinkAllowPublicUpload() - && ($share->getPermissions() & (Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE))) { - throw new \InvalidArgumentException($this->l->t('Public upload is not allowed')); - } - } - /** * To make sure we don't get invisible link shares we set the parent * of a link if it is a reshare. This is a quick word around @@ -607,27 +546,9 @@ protected function pathCreateChecks(Node $path): void { } } - /** - * Check if the user that is sharing can actually share - * - * @throws \Exception - */ - protected function canShare(IShare $share): void { - if (!$this->shareApiEnabled()) { - throw new \Exception($this->l->t('Sharing is disabled')); - } - - if ($this->sharingDisabledForUser($share->getSharedBy())) { - throw new \Exception($this->l->t('Sharing is disabled for you')); - } - } - #[Override] public function createShare(IShare $share): IShare { - // TODO: handle link share permissions or check them - $this->canShare($share); - - $this->generalCreateChecks($share); + $this->generalChecks($share); // Verify if there are any issues with the path $this->pathCreateChecks($share->getNode()); @@ -668,7 +589,6 @@ public function createShare(IShare $share): IShare { $share = $this->validateExpirationDateInternal($share); } elseif ($share->getShareType() === IShare::TYPE_LINK || $share->getShareType() === IShare::TYPE_EMAIL) { - $this->linkCreateChecks($share); $this->setLinkParent($share); $token = $this->generateToken(); @@ -761,8 +681,6 @@ public function createShare(IShare $share): IShare { public function updateShare(IShare $share, bool $onlyValid = true): IShare { $expirationDateUpdated = false; - $this->canShare($share); - try { $originalShare = $this->getShareById($share->getFullId(), onlyValid: $onlyValid); } catch (\UnexpectedValueException $e) { @@ -786,7 +704,7 @@ public function updateShare(IShare $share, bool $onlyValid = true): IShare { throw new \InvalidArgumentException($this->l->t('Cannot share with the share owner')); } - $this->generalCreateChecks($share, true); + $this->generalChecks($share); if ($share->getShareType() === IShare::TYPE_USER) { $this->userCreateChecks($share); @@ -806,7 +724,6 @@ public function updateShare(IShare $share, bool $onlyValid = true): IShare { } } elseif ($share->getShareType() === IShare::TYPE_LINK || $share->getShareType() === IShare::TYPE_EMAIL) { - $this->linkCreateChecks($share); // The new password is not set again if it is the same as the old // one, unless when switching from sending by Talk to sending by @@ -1121,7 +1038,7 @@ protected function promoteReshares(IShare $share): void { foreach ($reshareRecords as $child) { try { /* Check if the share is still valid (means the resharer still has access to the file through another mean) */ - $this->generalCreateChecks($child); + $this->generalChecks($child); } catch (GenericShareException $e) { /* The check is invalid, promote it to a direct share from the sharer of parent share */ $this->logger->debug('Promote reshare because of exception ' . $e->getMessage(), ['exception' => $e, 'fullId' => $child->getFullId()]); @@ -1500,7 +1417,7 @@ private function checkShare(IShare $share, int &$added = 1): void { throw new ShareNotFound($this->l->t('The requested share does not exist anymore')); } if ($this->config->getAppValue('files_sharing', 'hide_disabled_user_shares', 'no') === 'yes') { - $uids = array_unique([$share->getShareOwner(),$share->getSharedBy()]); + $uids = array_unique([$share->getShareOwner(), $share->getSharedBy()]); foreach ($uids as $uid) { $user = $this->userManager->get($uid); if ($user?->isEnabled() === false) { diff --git a/lib/public/Interaction/Actions/ShareAction.php b/lib/public/Interaction/Actions/ShareAction.php new file mode 100644 index 0000000000000..382fa4e5d167c --- /dev/null +++ b/lib/public/Interaction/Actions/ShareAction.php @@ -0,0 +1,31 @@ + */ + public ?int $filesSharingPermissions = null, + ) { + } +} diff --git a/lib/public/Interaction/InteractionAction.php b/lib/public/Interaction/InteractionAction.php new file mode 100644 index 0000000000000..d4b14a3b9d8d3 --- /dev/null +++ b/lib/public/Interaction/InteractionAction.php @@ -0,0 +1,19 @@ +circleId; + } +} diff --git a/lib/public/Interaction/Receivers/DeckReceiver.php b/lib/public/Interaction/Receivers/DeckReceiver.php new file mode 100644 index 0000000000000..87ac527ad1ac7 --- /dev/null +++ b/lib/public/Interaction/Receivers/DeckReceiver.php @@ -0,0 +1,35 @@ +cardId; + } +} diff --git a/lib/public/Interaction/Receivers/EmailReceiver.php b/lib/public/Interaction/Receivers/EmailReceiver.php new file mode 100644 index 0000000000000..c01c04a7a6235 --- /dev/null +++ b/lib/public/Interaction/Receivers/EmailReceiver.php @@ -0,0 +1,35 @@ +email; + } +} diff --git a/lib/public/Interaction/Receivers/GroupReceiver.php b/lib/public/Interaction/Receivers/GroupReceiver.php new file mode 100644 index 0000000000000..d073fd673a565 --- /dev/null +++ b/lib/public/Interaction/Receivers/GroupReceiver.php @@ -0,0 +1,56 @@ +group instanceof IGroup) { + return $this->group; + } + + $group = Server::get(IGroupManager::class)->get($this->groupId); + if ($group === null) { + throw new RuntimeException('Group does not exist: ' . $this->groupId); + } + + return $this->group = $group; + } + + /** + * @since 34.0.2 + */ + #[\Override] + public function getID(): string { + return $this->groupId; + } +} diff --git a/lib/public/Interaction/Receivers/LinkReceiver.php b/lib/public/Interaction/Receivers/LinkReceiver.php new file mode 100644 index 0000000000000..3099350be070d --- /dev/null +++ b/lib/public/Interaction/Receivers/LinkReceiver.php @@ -0,0 +1,27 @@ +cloudId ??= Server::get(ICloudIdManager::class)->resolveCloudId($this->cloudIdString); + } + + /** + * @since 34.0.2 + */ + #[\Override] + public function getID(): string { + return $this->cloudIdString; + } +} diff --git a/lib/public/Interaction/Receivers/RemoteUserReceiver.php b/lib/public/Interaction/Receivers/RemoteUserReceiver.php new file mode 100644 index 0000000000000..6d58e3e5108fc --- /dev/null +++ b/lib/public/Interaction/Receivers/RemoteUserReceiver.php @@ -0,0 +1,46 @@ +cloudId ??= Server::get(ICloudIdManager::class)->resolveCloudId($this->cloudIdString); + } + + /** + * @since 34.0.2 + */ + #[\Override] + public function getID(): string { + return $this->cloudIdString; + } +} diff --git a/lib/public/Interaction/Receivers/RoomReceiver.php b/lib/public/Interaction/Receivers/RoomReceiver.php new file mode 100644 index 0000000000000..817db4bbf187b --- /dev/null +++ b/lib/public/Interaction/Receivers/RoomReceiver.php @@ -0,0 +1,35 @@ +roomToken; + } +} diff --git a/lib/public/Interaction/Receivers/UserReceiver.php b/lib/public/Interaction/Receivers/UserReceiver.php new file mode 100644 index 0000000000000..c222ee7552f23 --- /dev/null +++ b/lib/public/Interaction/Receivers/UserReceiver.php @@ -0,0 +1,56 @@ +user instanceof IUser) { + return $this->user; + } + + $user = Server::get(IUserManager::class)->get($this->userId); + if ($user === null) { + throw new RuntimeException('User does not exist: ' . $this->userId); + } + + return $this->user = $user; + } + + /** + * @since 34.0.2 + */ + #[\Override] + public function getID(): string { + return $this->userId; + } +} diff --git a/lib/public/Interaction/Resources/NodeResource.php b/lib/public/Interaction/Resources/NodeResource.php new file mode 100644 index 0000000000000..5a4e0b6215580 --- /dev/null +++ b/lib/public/Interaction/Resources/NodeResource.php @@ -0,0 +1,91 @@ + $nodePermissions */ + private ?int $nodePermissions = null, + ) { + } + + /** + * If you need to check the node permissions, use {@see getNodePermissions} instead. + * + * @since 34.0.2 + */ + public function getNode(): Node { + if ($this->node instanceof Node) { + return $this->node; + } + + $node = Server::get(IRootFolder::class)->getUserFolder($this->userId)->getFirstNodeById($this->nodeId); + if ($node === null) { + throw new RuntimeException('Node does not exist: ' . $this->nodeId); + } + + return $this->node = $node; + } + + /** + * Returns the merged permissions of all node instances the user has access to. + * + * @return int-mask-of + * @since 34.0.2 + */ + public function getNodePermissions(): int { + if ($this->nodePermissions !== null) { + return $this->nodePermissions; + } + + $nodes = Server::get(IRootFolder::class)->getUserFolder($this->userId)->getById($this->nodeId); + if ($nodes === []) { + throw new RuntimeException('Node does not exist: ' . $this->nodeId); + } + + /** @var int-mask-of $nodePermissions */ + $nodePermissions = array_reduce( + $nodes, + static fn (int $nodePermissions, Node $node): int => $nodePermissions | ($node->getInternalPath() === '' && !$node->getMountPoint() instanceof IMovableMount ? $node->getStorage()->getPermissions('') : $node->getPermissions()), + 0, + ); + + return $this->nodePermissions = $nodePermissions; + } + + /** + * @since 34.0.2 + */ + #[\Override] + public function getID(): string { + return (string)$this->nodeId; + } +} diff --git a/lib/public/Interaction/RestrictInteractionEvent.php b/lib/public/Interaction/RestrictInteractionEvent.php new file mode 100644 index 0000000000000..3ba77c025b152 --- /dev/null +++ b/lib/public/Interaction/RestrictInteractionEvent.php @@ -0,0 +1,94 @@ +user instanceof IUser) { + return $this->user; + } + + $user = Server::get(IUserManager::class)->get($this->userId); + if ($user === null) { + throw new RuntimeException('User does not exist: ' . $this->userId); + } + + return $this->user = $user; + } + + /** + * @return false|string Returns a user friendly translated message if the interaction is restricted. + * @since 34.0.2 + */ + public function isInteractionRestricted(): false|string { + $eventDispatcher = Server::get(IEventDispatcher::class); + + $params = [ + $this->action instanceof InteractionAction ? $this->action::class : '?', + $this->userId, + $this->resource?->getID() ?? '?', + $this->receiver?->getID() ?? '?', + ]; + + try { + $eventDispatcher->dispatchTyped($this); + + $eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent( + 'Interaction "%s" from user "%s" on "%s" to "%s" is allowed.', + $params, + )); + + return false; + } catch (InteractionRestrictedException $interactionRestrictedException) { + $eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent( + 'Interaction "%s" from user "%s" on "%s" to "%s" is restricted: ' . $interactionRestrictedException->getMessage(), + $params, + )); + + return $interactionRestrictedException->getHint(); + } + } +} diff --git a/psalm-strict.xml b/psalm-strict.xml index 5d9d09162a046..b3515870cccb4 100644 --- a/psalm-strict.xml +++ b/psalm-strict.xml @@ -36,6 +36,14 @@ + + + + + + + + @@ -51,11 +59,15 @@ + + + + diff --git a/tests/Core/Listener/RestrictInteractionListenerTest.php b/tests/Core/Listener/RestrictInteractionListenerTest.php new file mode 100644 index 0000000000000..da7615456c868 --- /dev/null +++ b/tests/Core/Listener/RestrictInteractionListenerTest.php @@ -0,0 +1,223 @@ +createUser('user', 'password'); + $this->assertNotFalse($user); + $this->user = $user; + + Server::get(ISetupManager::class)->setupForUser($user); + + /** @var CappedMemoryCache $cache */ + $cache = self::invokePrivate(Server::get(ShareDisableChecker::class), 'sharingDisabledForUsersCache'); + $cache->clear(); + } + + #[\Override] + protected function tearDown(): void { + Server::get(ISetupManager::class)->tearDown(); + + $this->assertTrue($this->user->delete()); + + parent::tearDown(); + } + + /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ + public function testShareActionApiDisabled(): void { + $config = Server::get(IConfig::class); + $config->setAppValue('core', 'shareapi_enabled', 'no'); + + $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, null, new ShareAction(), null); + $this->assertEquals('Sharing is not allowed.', $event->isInteractionRestricted()); + + $config->deleteAppValue('core', 'shareapi_enabled'); + } + + /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ + public function testShareActionDisabledForUser(): void { + $group = Server::get(IGroupManager::class)->createGroup('group'); + $this->assertNotNull($group); + $group->addUser($this->user); + + $config = Server::get(IConfig::class); + $config->setAppValue('core', 'shareapi_exclude_groups', 'yes'); + $config->setAppValue('core', 'shareapi_exclude_groups_list', json_encode([$group->getGID()], JSON_THROW_ON_ERROR)); + + $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, null, new ShareAction(), null); + $this->assertEquals('Sharing is not allowed for you.', $event->isInteractionRestricted()); + + $config->deleteAppValue('core', 'shareapi_exclude_groups'); + $config->deleteAppValue('core', 'shareapi_exclude_groups_list'); + + $this->assertTrue($group->delete()); + } + + /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ + public function testShareActionUserReceiverGroupMembersOnly(): void { + $user2 = Server::get(IUserManager::class)->createUser('user2', 'password'); + $this->assertNotFalse($user2); + + $group = Server::get(IGroupManager::class)->createGroup('group'); + $this->assertNotNull($group); + $group->addUser($this->user); + + $config = Server::get(IConfig::class); + $config->setAppValue('core', 'shareapi_only_share_with_group_members', 'yes'); + + $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, null, new ShareAction(), new UserReceiver($user2->getUID(), $user2)); + $this->assertEquals('Sharing is only allowed with group members.', $event->isInteractionRestricted()); + + $config->deleteAppValue('core', 'shareapi_only_share_with_group_members'); + + $this->assertTrue($group->delete()); + $this->assertTrue($user2->delete()); + } + + /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ + public function testShareActionUserReceiverGroupMembersOnlyExclude(): void { + $user2 = Server::get(IUserManager::class)->createUser('user2', 'password'); + $this->assertNotFalse($user2); + + $group = Server::get(IGroupManager::class)->createGroup('group'); + $this->assertNotNull($group); + $group->addUser($this->user); + $group->addUser($user2); + + $config = Server::get(IConfig::class); + $config->setAppValue('core', 'shareapi_only_share_with_group_members', 'yes'); + $config->setAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list', json_encode([$group->getGID()], JSON_THROW_ON_ERROR)); + + $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, null, new ShareAction(), new UserReceiver($user2->getUID(), $user2)); + $this->assertEquals('Sharing is only allowed with group members.', $event->isInteractionRestricted()); + + $config->deleteAppValue('core', 'shareapi_only_share_with_group_members'); + $config->deleteAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list'); + + $this->assertTrue($group->delete()); + $this->assertTrue($user2->delete()); + } + + /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ + public function testShareActionGroupReceiverGroupMembersOnly(): void { + $group = Server::get(IGroupManager::class)->createGroup('group'); + $this->assertNotNull($group); + + $config = Server::get(IConfig::class); + $config->setAppValue('core', 'shareapi_only_share_with_group_members', 'yes'); + + $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, null, new ShareAction(), new GroupReceiver($group->getGID(), $group)); + $this->assertEquals('Sharing is only allowed within your own groups.', $event->isInteractionRestricted()); + + $config->deleteAppValue('core', 'shareapi_only_share_with_group_members'); + + $this->assertTrue($group->delete()); + } + + /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ + public function testShareActionGroupReceiverGroupMembersOnlyExclude(): void { + $group = Server::get(IGroupManager::class)->createGroup('group'); + $this->assertNotNull($group); + $group->addUser($this->user); + + $config = Server::get(IConfig::class); + $config->setAppValue('core', 'shareapi_only_share_with_group_members', 'yes'); + $config->setAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list', json_encode([$group->getGID()], JSON_THROW_ON_ERROR)); + + $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, null, new ShareAction(), new GroupReceiver($group->getGID(), $group)); + $this->assertEquals('Sharing is only allowed within your own groups.', $event->isInteractionRestricted()); + + $config->deleteAppValue('core', 'shareapi_only_share_with_group_members'); + $config->deleteAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list'); + + $this->assertTrue($group->delete()); + } + + /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ + public function testShareActionGroupReceiverGroupSharingDisabled(): void { + $group = Server::get(IGroupManager::class)->createGroup('group'); + $this->assertNotNull($group); + $group->addUser($this->user); + + $config = Server::get(IConfig::class); + $config->setAppValue('core', 'shareapi_allow_group_sharing', 'no'); + + $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, null, new ShareAction(), new GroupReceiver($group->getGID(), $group)); + $this->assertEquals('Group sharing is not allowed.', $event->isInteractionRestricted()); + + $config->deleteAppValue('core', 'shareapi_allow_group_sharing'); + + $this->assertTrue($group->delete()); + } + + /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ + public function testShareActionLinkEmailReceiverLinkSharingDisabled(): void { + $config = Server::get(IConfig::class); + $config->setAppValue('core', 'shareapi_allow_links', 'no'); + + foreach ([ + new LinkReceiver(), + new EmailReceiver(''), + ] as $receiver) { + $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, null, new ShareAction(), $receiver); + $this->assertEquals('Public link sharing is not allowed.', $event->isInteractionRestricted()); + } + + $config->deleteAppValue('core', 'shareapi_allow_links'); + } + + /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ + public function testShareActionRemoteUserReceiverServer2ServerSharingDisabled(): void { + $config = Server::get(IConfig::class); + $config->setAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'no'); + + $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, null, new ShareAction(), new RemoteUserReceiver('')); + $this->assertEquals('Sharing to remote users is not allowed.', $event->isInteractionRestricted()); + + $config->deleteAppValue('files_sharing', 'outgoing_server2server_share_enabled'); + } + + /** @psalm-suppress DeprecatedMethod The configs are not migrated to IAppConfig, so using deprecated IConfig is required for now. */ + public function testShareActionRemoteGroupReceiverServer2ServerGroupSharingDisabled(): void { + $config = Server::get(IConfig::class); + $config->setAppValue('files_sharing', 'outgoing_server2server_group_share_enabled', 'no'); + + $event = new RestrictInteractionEvent($this->user->getUID(), $this->user, null, new ShareAction(), new RemoteGroupReceiver('')); + $this->assertEquals('Sharing to remote groups is not allowed.', $event->isInteractionRestricted()); + + $config->deleteAppValue('files_sharing', 'outgoing_server2server_group_share_enabled'); + } +} diff --git a/tests/lib/Interaction/RestrictInteractionEventTest.php b/tests/lib/Interaction/RestrictInteractionEventTest.php new file mode 100644 index 0000000000000..c356719a7c925 --- /dev/null +++ b/tests/lib/Interaction/RestrictInteractionEventTest.php @@ -0,0 +1,101 @@ + + */ + public static function dataIsInteractionRestricted(): array { + return [ + [true], + [false], + ]; + } + + #[DataProvider('dataIsInteractionRestricted')] + public function testIsInteractionRestricted(bool $isRestricted): void { + $eventDispatcher = Server::get(IEventDispatcher::class); + + $auditEvents = []; + $auditEventListener = function (CriticalActionPerformedEvent $event) use (&$auditEvents): void { + $auditEvents[] = $event; + }; + $eventDispatcher->addListener(CriticalActionPerformedEvent::class, $auditEventListener); + + /** @psalm-suppress UnusedClosureParam */ + $restrictInteractionEventListener = function (RestrictInteractionEvent $event) use ($isRestricted): void { + if ($isRestricted) { + throw new InteractionRestrictedException('my restriction message', 'my restriction hint'); + } + }; + $eventDispatcher->addListener(RestrictInteractionEvent::class, $restrictInteractionEventListener); + + $user = $this->createMock(IUser::class); + $user + ->method('getUID') + ->willReturn('my-uid'); + + $resource = $this->createMock(InteractionResource::class); + $resource + ->method('getID') + ->willReturn('my-resource'); + + $action = $this->createStub(InteractionAction::class); + + $receiver = $this->createMock(InteractionReceiver::class); + $receiver + ->method('getID') + ->willReturn('my-receiver'); + + $event = new RestrictInteractionEvent( + $user->getUID(), + $user, + $resource, + $action, + $receiver, + ); + + if ($isRestricted) { + $this->assertEquals('my restriction hint', $event->isInteractionRestricted()); + } else { + $this->assertFalse($event->isInteractionRestricted()); + } + + $this->assertEquals([ + new CriticalActionPerformedEvent( + $isRestricted + ? 'Interaction "%s" from user "%s" on "%s" to "%s" is restricted: my restriction message' + : 'Interaction "%s" from user "%s" on "%s" to "%s" is allowed.', + [ + $action::class, + 'my-uid', + 'my-resource', + 'my-receiver', + ], + ), + ], $auditEvents); + + $eventDispatcher->removeListener(CriticalActionPerformedEvent::class, $auditEventListener); + $eventDispatcher->removeListener(RestrictInteractionEvent::class, $restrictInteractionEventListener); + } +} diff --git a/tests/lib/Share20/ManagerTest.php b/tests/lib/Share20/ManagerTest.php index 21098e7f38af9..6e7c6ae3340f1 100644 --- a/tests/lib/Share20/ManagerTest.php +++ b/tests/lib/Share20/ManagerTest.php @@ -16,6 +16,7 @@ use OC\Share20\Manager; use OC\Share20\Share; use OC\Share20\ShareDisableChecker; +use OCA\Files_Sharing\Listener\RestrictInteractionListener; use OCP\Constants; use OCP\DB\IResult; use OCP\DB\QueryBuilder\IExpressionBuilder; @@ -47,6 +48,7 @@ use OCP\Security\Events\ValidatePasswordPolicyEvent; use OCP\Security\IHasher; use OCP\Security\ISecureRandom; +use OCP\Server; use OCP\Share\Events\BeforeShareCreatedEvent; use OCP\Share\Events\BeforeShareDeletedEvent; use OCP\Share\Events\ShareCreatedEvent; @@ -71,6 +73,7 @@ class DummyShareManagerListener { public function post() { } + public function listener() { } } @@ -145,6 +148,27 @@ protected function setUp(): void { $this->defaultProvider = $this->createMock(DefaultShareProvider::class); $this->defaultProvider->method('identifier')->willReturn('default'); $this->factory->setProvider($this->defaultProvider); + + $this->overwriteService(IRootFolder::class, $this->rootFolder); + $this->overwriteService(IUserManager::class, $this->userManager); + $this->overwriteService(IGroupManager::class, $this->groupManager); + self::invokePrivate(Server::get(\OC\Core\Listener\RestrictInteractionListener::class), 'manager', [$this->manager]); + self::invokePrivate(Server::get(\OC\Core\Listener\RestrictInteractionListener::class), 'groupManager', [$this->groupManager]); + self::invokePrivate(Server::get(RestrictInteractionListener::class), 'manager', [$this->manager]); + self::invokePrivate(Server::get(RestrictInteractionListener::class), 'rootFolder', [$this->rootFolder]); + } + + #[\Override] + protected function tearDown(): void { + $this->restoreService(IRootFolder::class); + $this->restoreService(IUserManager::class); + $this->restoreService(IGroupManager::class); + self::invokePrivate(Server::get(\OC\Core\Listener\RestrictInteractionListener::class), 'manager', [Server::get(IManager::class)]); + self::invokePrivate(Server::get(\OC\Core\Listener\RestrictInteractionListener::class), 'groupManager', [Server::get(IGroupManager::class)]); + self::invokePrivate(Server::get(RestrictInteractionListener::class), 'manager', [Server::get(IManager::class)]); + self::invokePrivate(Server::get(RestrictInteractionListener::class), 'rootFolder', [Server::get(IRootFolder::class)]); + + parent::tearDown(); } private function createManager(IProviderFactory $factory): Manager { @@ -464,7 +488,7 @@ public function testDeleteChildren(): void { public function testPromoteReshareFile(): void { $manager = $this->createManagerMock() - ->onlyMethods(['updateShare', 'getSharesInFolder', 'generalCreateChecks']) + ->onlyMethods(['updateShare', 'getSharesInFolder', 'generalChecks']) ->getMock(); $file = $this->createMock(File::class); @@ -486,14 +510,14 @@ public function testPromoteReshareFile(): void { ->willReturnCallback(function ($userId, $shareType, $node, $reshares, $limit, $offset) use ($reShare, $file) { $this->assertEquals($file, $node); if ($shareType === IShare::TYPE_USER) { - return match($userId) { + return match ($userId) { 'userB' => [$reShare], }; } else { return []; } }); - $manager->method('generalCreateChecks')->willThrowException(new GenericShareException()); + $manager->method('generalChecks')->willThrowException(new GenericShareException()); $manager->expects($this->exactly(1))->method('updateShare')->with($reShare)->willReturn($reShare); @@ -529,7 +553,7 @@ public function testPromoteReshareFile(): void { public function testPromoteReshare(): void { $manager = $this->createManagerMock() - ->onlyMethods(['updateShare', 'getSharesInFolder', 'generalCreateChecks']) + ->onlyMethods(['updateShare', 'getSharesInFolder', 'generalChecks']) ->getMock(); $folder = $this->createFolderMock('/path/to/folder'); @@ -574,7 +598,7 @@ public function testPromoteReshare(): void { } $this->fail(); }); - $manager->method('generalCreateChecks')->willThrowException(new GenericShareException()); + $manager->method('generalChecks')->willThrowException(new GenericShareException()); $calls = [ $reShare, @@ -627,7 +651,7 @@ public function testPromoteReshare(): void { public function testPromoteReshareWhenUserHasAnotherShare(): void { $manager = $this->createManagerMock() - ->onlyMethods(['updateShare', 'getSharesInFolder', 'getSharedWith', 'generalCreateChecks']) + ->onlyMethods(['updateShare', 'getSharesInFolder', 'getSharedWith', 'generalChecks']) ->getMock(); $folder = $this->createFolderMock('/path/to/folder'); @@ -645,9 +669,9 @@ public function testPromoteReshareWhenUserHasAnotherShare(): void { $reShare->method('getNode')->willReturn($folder); $this->defaultProvider->method('getSharesBy')->willReturn([$reShare]); - $manager->method('generalCreateChecks'); + $manager->method('generalChecks'); - /* No share is promoted because generalCreateChecks does not throw */ + /* No share is promoted because generalChecks does not throw */ $manager->expects($this->never())->method('updateShare'); $this->userManager->method('userExists')->willReturn(true); @@ -682,7 +706,7 @@ public function testPromoteReshareWhenUserHasAnotherShare(): void { public function testPromoteReshareOfUsersInGroupShare(): void { $manager = $this->createManagerMock() - ->onlyMethods(['updateShare', 'getSharesInFolder', 'getSharedWith', 'generalCreateChecks']) + ->onlyMethods(['updateShare', 'getSharesInFolder', 'getSharedWith', 'generalChecks']) ->getMock(); $folder = $this->createFolderMock('/path/to/folder'); @@ -720,7 +744,7 @@ public function testPromoteReshareOfUsersInGroupShare(): void { $this->defaultProvider->method('getSharesBy') ->willReturnCallback(function ($userId, $shareType, $node, $reshares, $limit, $offset) use ($reShare1, $reShare2) { if ($shareType === IShare::TYPE_USER) { - return match($userId) { + return match ($userId) { 'userB' => [$reShare1], 'userC' => [$reShare2], }; @@ -728,7 +752,7 @@ public function testPromoteReshareOfUsersInGroupShare(): void { return []; } }); - $manager->method('generalCreateChecks')->willThrowException(new GenericShareException()); + $manager->method('generalChecks')->willThrowException(new GenericShareException()); $manager->method('getSharedWith')->willReturn([]); @@ -989,6 +1013,7 @@ public static function dataGeneralChecks(): array { [ 'getId' => 108, 'isShareable' => false, + 'getPermissions' => Constants::PERMISSION_READ, 'getPath' => 'path', 'getName' => 'name', 'getOwner' => $user0, @@ -996,11 +1021,11 @@ public static function dataGeneralChecks(): array { 'default', ]; - $data[] = [[null, IShare::TYPE_USER, $nonShareAble, $user2, $user0, $user0, 31, null, null], 'You are not allowed to share name', true]; - $data[] = [[null, IShare::TYPE_GROUP, $nonShareAble, $group0, $user0, $user0, 31, null, null], 'You are not allowed to share name', true]; - $data[] = [[null, IShare::TYPE_LINK, $nonShareAble, null, $user0, $user0, 31, null, null], 'You are not allowed to share name', true]; + $data[] = [[null, IShare::TYPE_USER, $nonShareAble, $user2, $user0, $user0, 31, null, null], 'You are not allowed to share "name".', true]; + $data[] = [[null, IShare::TYPE_GROUP, $nonShareAble, $group0, $user0, $user0, 31, null, null], 'You are not allowed to share "name".', true]; + $data[] = [[null, IShare::TYPE_LINK, $nonShareAble, null, $user0, $user0, 31, null, null], 'You are not allowed to share "name".', true]; - $limitedPermssions = [ + $limitedPermissions = [ File::class, [ 'isShareable' => true, @@ -1013,17 +1038,17 @@ public static function dataGeneralChecks(): array { 'default', ]; - $data[] = [[null, IShare::TYPE_USER, $limitedPermssions, $user2, $user0, $user0, null, null, null], 'Valid permissions are required for sharing', true]; - $data[] = [[null, IShare::TYPE_GROUP, $limitedPermssions, $group0, $user0, $user0, null, null, null], 'Valid permissions are required for sharing', true]; - $data[] = [[null, IShare::TYPE_LINK, $limitedPermssions, null, $user0, $user0, null, null, null], 'Valid permissions are required for sharing', true]; + $data[] = [[null, IShare::TYPE_USER, $limitedPermissions, $user2, $user0, $user0, null, null, null], 'Valid permissions are required for sharing', true]; + $data[] = [[null, IShare::TYPE_GROUP, $limitedPermissions, $group0, $user0, $user0, null, null, null], 'Valid permissions are required for sharing', true]; + $data[] = [[null, IShare::TYPE_LINK, $limitedPermissions, null, $user0, $user0, null, null, null], 'Valid permissions are required for sharing', true]; - $limitedPermssions[1]['getMountPoint'] = IMovableMount::class; + $limitedPermissions[1]['getMountPoint'] = IMovableMount::class; // increase permissions of a re-share - $data[] = [[null, IShare::TYPE_GROUP, $limitedPermssions, $group0, $user0, $user0, 17, null, null], 'Cannot increase permissions of path', true]; - $data[] = [[null, IShare::TYPE_USER, $limitedPermssions, $user2, $user0, $user0, 3, null, null], 'Cannot increase permissions of path', true]; + $data[] = [[null, IShare::TYPE_GROUP, $limitedPermissions, $group0, $user0, $user0, 17, null, null], 'You cannot share "path" with more permission than you have yourself.', true]; + $data[] = [[null, IShare::TYPE_USER, $limitedPermissions, $user2, $user0, $user0, 3, null, null], 'You cannot share "path" with more permission than you have yourself.', true]; - $nonMoveableMountPermssions = [ + $nonMoveableMountPermissions = [ Folder::class, [ 'isShareable' => true, @@ -1034,11 +1059,11 @@ public static function dataGeneralChecks(): array { 'getInternalPath' => '', 'getOwner' => $user0, ], - 'allPermssions', + 'allPermissions', ]; - $data[] = [[null, IShare::TYPE_USER, $nonMoveableMountPermssions, $user2, $user0, $user0, 11, null, null], 'Cannot increase permissions of path', false]; - $data[] = [[null, IShare::TYPE_GROUP, $nonMoveableMountPermssions, $group0, $user0, $user0, 11, null, null], 'Cannot increase permissions of path', false]; + $data[] = [[null, IShare::TYPE_USER, $nonMoveableMountPermissions, $user2, $user0, $user0, 11, null, null], 'You cannot share "path" with more permission than you have yourself.', false]; + $data[] = [[null, IShare::TYPE_GROUP, $nonMoveableMountPermissions, $group0, $user0, $user0, 11, null, null], 'You cannot share "path" with more permission than you have yourself.', false]; $rootFolder = [ Folder::class, @@ -1050,27 +1075,27 @@ public static function dataGeneralChecks(): array { 'none', ]; - $data[] = [[null, IShare::TYPE_USER, $rootFolder, $user2, $user0, $user0, 30, null, null], 'You cannot share your root folder', true]; - $data[] = [[null, IShare::TYPE_GROUP, $rootFolder, $group0, $user0, $user0, 2, null, null], 'You cannot share your root folder', true]; - $data[] = [[null, IShare::TYPE_LINK, $rootFolder, null, $user0, $user0, 16, null, null], 'You cannot share your root folder', true]; + $data[] = [[null, IShare::TYPE_USER, $rootFolder, $user2, $user0, $user0, 30, null, null], 'You are not allowed to share "".', true]; + $data[] = [[null, IShare::TYPE_GROUP, $rootFolder, $group0, $user0, $user0, 2, null, null], 'You are not allowed to share "".', true]; + $data[] = [[null, IShare::TYPE_LINK, $rootFolder, null, $user0, $user0, 16, null, null], 'You are not allowed to share "".', true]; - $allPermssionsFiles = [ + $allPermissionsFiles = [ File::class, [ 'isShareable' => true, 'getPermissions' => Constants::PERMISSION_ALL, - 'getId' => 187, + 'getId' => 108, 'getOwner' => $user0, ], 'default', ]; // test invalid CREATE or DELETE permissions - $data[] = [[null, IShare::TYPE_USER, $allPermssionsFiles, $user2, $user0, $user0, Constants::PERMISSION_ALL, null, null], 'File shares cannot have create or delete permissions', true]; - $data[] = [[null, IShare::TYPE_GROUP, $allPermssionsFiles, $group0, $user0, $user0, Constants::PERMISSION_READ | Constants::PERMISSION_CREATE, null, null], 'File shares cannot have create or delete permissions', true]; - $data[] = [[null, IShare::TYPE_LINK, $allPermssionsFiles, null, $user0, $user0, Constants::PERMISSION_READ | Constants::PERMISSION_DELETE, null, null], 'File shares cannot have create or delete permissions', true]; + $data[] = [[null, IShare::TYPE_USER, $allPermissionsFiles, $user2, $user0, $user0, Constants::PERMISSION_ALL, null, null], 'File cannot be shared with delete permission.', true]; + $data[] = [[null, IShare::TYPE_GROUP, $allPermissionsFiles, $group0, $user0, $user0, Constants::PERMISSION_READ | Constants::PERMISSION_CREATE, null, null], 'File cannot be shared with create permission.', true]; + $data[] = [[null, IShare::TYPE_LINK, $allPermissionsFiles, null, $user0, $user0, Constants::PERMISSION_READ | Constants::PERMISSION_DELETE, null, null], 'File cannot be shared with delete permission.', true]; - $allPermssions = [ + $allPermissions = [ Folder::class, [ 'isShareable' => true, @@ -1081,24 +1106,24 @@ public static function dataGeneralChecks(): array { 'default', ]; - $data[] = [[null, IShare::TYPE_USER, $allPermssions, $user2, $user0, $user0, 30, null, null], 'Shares need at least read permissions', true]; - $data[] = [[null, IShare::TYPE_GROUP, $allPermssions, $group0, $user0, $user0, 2, null, null], 'Shares need at least read permissions', true]; + $data[] = [[null, IShare::TYPE_USER, $allPermissions, $user2, $user0, $user0, 30, null, null], 'File share needs at least read permission.', true]; + $data[] = [[null, IShare::TYPE_GROUP, $allPermissions, $group0, $user0, $user0, 2, null, null], 'File share needs at least read permission.', true]; // test invalid permissions - $data[] = [[null, IShare::TYPE_USER, $allPermssions, $user2, $user0, $user0, 32, null, null], 'Valid permissions are required for sharing', true]; - $data[] = [[null, IShare::TYPE_GROUP, $allPermssions, $group0, $user0, $user0, 63, null, null], 'Valid permissions are required for sharing', true]; - $data[] = [[null, IShare::TYPE_LINK, $allPermssions, null, $user0, $user0, -1, null, null], 'Valid permissions are required for sharing', true]; + $data[] = [[null, IShare::TYPE_USER, $allPermissions, $user2, $user0, $user0, 32, null, null], 'Valid permissions are required for sharing', true]; + $data[] = [[null, IShare::TYPE_GROUP, $allPermissions, $group0, $user0, $user0, 63, null, null], 'Valid permissions are required for sharing', true]; + $data[] = [[null, IShare::TYPE_LINK, $allPermissions, null, $user0, $user0, -1, null, null], 'Valid permissions are required for sharing', true]; // working shares - $data[] = [[null, IShare::TYPE_USER, $allPermssions, $user2, $user0, $user0, 31, null, null], null, false]; - $data[] = [[null, IShare::TYPE_GROUP, $allPermssions, $group0, $user0, $user0, 3, null, null], null, false]; - $data[] = [[null, IShare::TYPE_LINK, $allPermssions, null, $user0, $user0, 17, null, null], null, false]; + $data[] = [[null, IShare::TYPE_USER, $allPermissions, $user2, $user0, $user0, 31, null, null], null, false]; + $data[] = [[null, IShare::TYPE_GROUP, $allPermissions, $group0, $user0, $user0, 3, null, null], null, false]; + $data[] = [[null, IShare::TYPE_LINK, $allPermissions, null, $user0, $user0, 17, null, null], null, false]; $remoteFile = [ Folder::class, [ 'isShareable' => true, - 'getPermissions' => Constants::PERMISSION_READ ^ Constants::PERMISSION_UPDATE, + 'getPermissions' => Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE, 'getId' => 108, 'getOwner' => $user0, ], @@ -1107,7 +1132,7 @@ public static function dataGeneralChecks(): array { $data[] = [[null, IShare::TYPE_REMOTE, $remoteFile, $user2, $user0, $user0, 1, null, null], null, false]; $data[] = [[null, IShare::TYPE_REMOTE, $remoteFile, $user2, $user0, $user0, 3, null, null], null, false]; - $data[] = [[null, IShare::TYPE_REMOTE, $remoteFile, $user2, $user0, $user0, 31, null, null], 'Cannot increase permissions of ', true]; + $data[] = [[null, IShare::TYPE_REMOTE, $remoteFile, $user2, $user0, $user0, 31, null, null], 'You cannot share "" with more permission than you have yourself.', true]; return $data; } @@ -1132,7 +1157,7 @@ private function createNodeMock(string $class, array $methods, string $storageTy ->with('\OCA\Files_Sharing\External\Storage') ->willReturn(false); break; - case 'allPermssions': + case 'allPermissions': $storage = $this->createMock(IStorage::class); $storage->method('instanceOfStorage') ->with('\OCA\Files_Sharing\External\Storage') @@ -1175,33 +1200,76 @@ public function testGeneralChecks(array $shareParams, ?string $exceptionMessage, ['user1', true], ]); + $user0 = $this->createMock(IUser::class); + $user0 + ->method('getUID') + ->willReturn('user0'); + $user1 = $this->createMock(IUser::class); + $user1 + ->method('getUID') + ->willReturn('user1'); + + $this->userManager->method('get')->willReturnMap([ + ['user0', $user0], + ['user1', $user1], + ]); + $this->groupManager->method('groupExists')->willReturnMap([ ['group0', true], ]); $userFolder = $this->createMock(Folder::class); - $userFolder->expects($this->any()) + + $userFolder ->method('getId') ->willReturn(42); + + $userFolder + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_ALL); + // Id 108 is used in the data to refer to the node of the share. try { $node = $share->getNode(); - $userFolder->method('getById') - ->with(108) - ->willReturn([$node]); } catch (NotFoundException $e) { - $userFolder->method('getById') - ->with(108) - ->willReturn([]); + $node = null; } - $userFolder->expects($this->any()) + + $userFolder + ->method('getById') + ->willReturnMap([ + [108, $node !== null ? [$node] : []], + [42, [$userFolder]], + ]); + + $userFolder + ->method('getFirstNodeById') + ->willReturnMap([ + [108, $node], + [42, $userFolder], + ]); + + $userFolder ->method('getRelativePath') ->willReturnArgument(0); - $this->rootFolder->method('getUserFolder')->willReturn($userFolder); + + $this->rootFolder + ->method('getUserFolder') + ->willReturn($userFolder); + + $this->config + ->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_enabled', 'yes', 'yes'], + ['core', 'shareapi_allow_group_sharing', 'yes', 'yes'], + ['core', 'shareapi_allow_links', 'yes', 'yes'], + ['core', 'shareapi_allow_links_exclude_groups', '[]', '[]'], + ['files_sharing', 'outgoing_server2server_share_enabled', 'yes', 'yes'], + ]); try { - self::invokePrivate($this->manager, 'generalCreateChecks', [$share]); + self::invokePrivate($this->manager, 'generalChecks', [$share]); $thrown = false; } catch (GenericShareException $e) { $this->assertEquals($exceptionMessage, $e->getHint()); @@ -1216,26 +1284,69 @@ public function testGeneralChecks(array $shareParams, ?string $exceptionMessage, public function testGeneralCheckShareRoot(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('You cannot share your root folder'); + $this->expectException(GenericShareException::class); + $this->expectExceptionMessage('You cannot share your home folder.'); $this->userManager->method('userExists')->willReturnMap([ ['user0', true], ['user1', true], ]); + $user0 = $this->createMock(IUser::class); + $user0 + ->method('getUID') + ->willReturn('user0'); + $user1 = $this->createMock(IUser::class); + $user1 + ->method('getUID') + ->willReturn('user1'); + + $this->userManager->method('get')->willReturnMap([ + ['user0', $user0], + ['user1', $user1], + ]); + $userFolder = $this->createMock(Folder::class); - $userFolder->method('isSubNode')->with($userFolder)->willReturn(false); - $this->rootFolder->method('getUserFolder')->willReturn($userFolder); + $userFolder + ->method('isSubNode') + ->with($userFolder) + ->willReturn(false); + $userFolder + ->method('getId') + ->willReturn(42); + $userFolder + ->method('getById') + ->with(42) + ->willReturn([$userFolder]); + $userFolder + ->method('getFirstNodeById') + ->with(42) + ->willReturn($userFolder); + $userFolder + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_ALL); + $userFolder + ->method('isShareable') + ->willReturn(true); - $share = $this->manager->newShare(); + $this->rootFolder + ->method('getUserFolder') + ->willReturn($userFolder); - $share->setShareType(IShare::TYPE_USER) + $this->config + ->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_enabled', 'yes', 'yes'], + ]); + + $share = $this->manager->newShare() + ->setShareType(IShare::TYPE_USER) ->setSharedWith('user0') ->setSharedBy('user1') - ->setNode($userFolder); + ->setNode($userFolder) + ->setPermissions(Constants::PERMISSION_ALL); - self::invokePrivate($this->manager, 'generalCreateChecks', [$share]); + self::invokePrivate($this->manager, 'generalChecks', [$share]); } public static function validateExpirationDateInternalProvider() { @@ -1989,14 +2100,47 @@ public function testValidateExpirationDateExistingShareNoDefault(): void { } public function testUserCreateChecksShareWithGroupMembersOnlyDifferentGroups(): void { - $this->expectException(\Exception::class); + $this->expectException(GenericShareException::class); $this->expectExceptionMessage('Sharing is only allowed with group members'); - $share = $this->manager->newShare(); + $node = $this->createMock(Folder::class); + $node + ->method('getId') + ->willReturn(108); + $node + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_ALL); + $node + ->method('isShareable') + ->willReturn(true); + + $userFolder = $this->createMock(Folder::class); + $userFolder + ->method('getById') + ->with(108) + ->willReturn([$node]); + $userFolder + ->method('getFirstNodeById') + ->with(108) + ->willReturn($node); + + $this->rootFolder + ->method('getUserFolder') + ->with('sharedBy') + ->willReturn($userFolder); + + $share = $this->manager->newShare() + ->setShareType(IShare::TYPE_USER) + ->setSharedBy('sharedBy') + ->setSharedWith('sharedWith') + ->setNode($node) + ->setPermissions(Constants::PERMISSION_ALL); $sharedBy = $this->createMock(IUser::class); + $sharedBy + ->method('getUID') + ->willReturn('sharedBy'); $sharedWith = $this->createMock(IUser::class); - $share->setSharedBy('sharedBy')->setSharedWith('sharedWith'); $this->groupManager ->method('getUserGroupIds') @@ -2007,6 +2151,11 @@ public function testUserCreateChecksShareWithGroupMembersOnlyDifferentGroups(): ] ); + $this->userManager->method('userExists')->willReturnMap([ + ['sharedBy', true], + ['sharedWith', true], + ]); + $this->userManager->method('get')->willReturnMap([ ['sharedBy', $sharedBy], ['sharedWith', $sharedWith], @@ -2015,22 +2164,53 @@ public function testUserCreateChecksShareWithGroupMembersOnlyDifferentGroups(): $this->config ->method('getAppValue') ->willReturnMap([ + ['core', 'shareapi_enabled', 'yes', 'yes'], ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], ]); - self::invokePrivate($this->manager, 'userCreateChecks', [$share]); + self::invokePrivate($this->manager, 'generalChecks', [$share]); } public function testUserCreateChecksShareWithGroupMembersOnlySharedGroup(): void { - $share = $this->manager->newShare(); + $node = $this->createMock(Folder::class); + $node + ->method('getId') + ->willReturn(108); + $node + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_ALL); + $node + ->method('isShareable') + ->willReturn(true); + + $userFolder = $this->createMock(Folder::class); + $userFolder + ->method('getById') + ->with(108) + ->willReturn([$node]); + $userFolder + ->method('getFirstNodeById') + ->with(108) + ->willReturn($node); + + $this->rootFolder + ->method('getUserFolder') + ->with('sharedBy') + ->willReturn($userFolder); + + $share = $this->manager->newShare() + ->setShareType(IShare::TYPE_USER) + ->setSharedBy('sharedBy') + ->setSharedWith('sharedWith') + ->setNode($node) + ->setPermissions(Constants::PERMISSION_ALL); $sharedBy = $this->createMock(IUser::class); + $sharedBy + ->method('getUID') + ->willReturn('sharedBy'); $sharedWith = $this->createMock(IUser::class); - $share->setSharedBy('sharedBy')->setSharedWith('sharedWith'); - - $path = $this->createMock(Node::class); - $share->setNode($path); $this->groupManager ->method('getUserGroupIds') @@ -2041,24 +2221,34 @@ public function testUserCreateChecksShareWithGroupMembersOnlySharedGroup(): void ] ); - $this->userManager->method('get')->willReturnMap([ - ['sharedBy', $sharedBy], - ['sharedWith', $sharedWith], - ]); + $this->userManager + ->method('get') + ->willReturnMap([ + ['sharedBy', $sharedBy], + ['sharedWith', $sharedWith], + ]); + + $this->userManager + ->method('userExists') + ->willReturnMap([ + ['sharedBy', true], + ['sharedWith', true], + ]); $this->config ->method('getAppValue') ->willReturnMap([ + ['core', 'shareapi_enabled', 'yes', 'yes'], ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], ]); $this->defaultProvider ->method('getSharesByPath') - ->with($path) + ->with($node) ->willReturn([]); - self::invokePrivate($this->manager, 'userCreateChecks', [$share]); + self::invokePrivate($this->manager, 'generalChecks', [$share]); } @@ -2170,16 +2360,61 @@ public function testUserCreateChecksIdenticalPathSharedViaDeletedGroup(): void { } public function testUserCreateChecksIdenticalPathNotSharedWithUser(): void { + $node = $this->createMock(Folder::class); + $node + ->method('getId') + ->willReturn(108); + $node + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_ALL); + $node + ->method('isShareable') + ->willReturn(true); + + $userFolder = $this->createMock(Folder::class); + $userFolder + ->method('getById') + ->with(108) + ->willReturn([$node]); + $userFolder + ->method('getFirstNodeById') + ->with(108) + ->willReturn($node); + + $this->rootFolder + ->method('getUserFolder') + ->with('sharedBy') + ->willReturn($userFolder); + $share = $this->manager->newShare(); - $sharedWith = $this->createMock(IUser::class); - $path = $this->createMock(Node::class); - $share->setSharedWith('sharedWith') - ->setNode($path) - ->setShareOwner('shareOwner') + $share + ->setShareType(IShare::TYPE_USER) + ->setSharedBy('sharedBy') + ->setSharedWith('sharedWith') + ->setNode($node) + ->setPermissions(Constants::PERMISSION_ALL) ->setProviderId('foo') ->setId('bar'); - $this->userManager->method('get')->with('sharedWith')->willReturn($sharedWith); + $sharedWith = $this->createMock(IUser::class); + $sharedBy = $this->createMock(IUser::class); + $sharedBy + ->method('getUID') + ->willReturn('sharedBy'); + + $this->userManager + ->method('get') + ->willReturnMap([ + ['sharedWith', $sharedWith], + ['sharedBy', $sharedBy], + ]); + + $this->userManager + ->method('userExists') + ->willReturnMap([ + ['sharedWith', true], + ['sharedBy', true], + ]); $share2 = $this->manager->newShare(); $share2->setShareType(IShare::TYPE_GROUP) @@ -2198,107 +2433,237 @@ public function testUserCreateChecksIdenticalPathNotSharedWithUser(): void { $this->defaultProvider ->method('getSharesByPath') - ->with($path) + ->with($node) ->willReturn([$share2]); - self::invokePrivate($this->manager, 'userCreateChecks', [$share]); - } - - - public function testGroupCreateChecksShareWithGroupMembersGroupSharingNotAllowed(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Group sharing is now allowed'); - - $share = $this->manager->newShare(); - $this->config ->method('getAppValue') ->willReturnMap([ - ['core', 'shareapi_allow_group_sharing', 'yes', 'no'], + ['core', 'shareapi_enabled', 'yes', 'yes'], ]); - self::invokePrivate($this->manager, 'groupCreateChecks', [$share]); + self::invokePrivate($this->manager, 'generalChecks', [$share]); } - - public function testGroupCreateChecksShareWithGroupMembersOnlyNotInGroup(): void { + public function testGroupCreateChecksShareWithGroupMembersGroupSharingNotAllowed(): void { $this->expectException(\Exception::class); - $this->expectExceptionMessage('Sharing is only allowed within your own groups'); + $this->expectExceptionMessage('Group sharing is not allowed'); - $share = $this->manager->newShare(); + $node = $this->createMock(Folder::class); + $node + ->method('getId') + ->willReturn(108); + $node + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_ALL); + $node + ->method('isShareable') + ->willReturn(true); - $user = $this->createMock(IUser::class); - $group = $this->createMock(IGroup::class); - $share->setSharedBy('user')->setSharedWith('group'); + $userFolder = $this->createMock(Folder::class); + $userFolder + ->method('getById') + ->with(108) + ->willReturn([$node]); + $userFolder + ->method('getFirstNodeById') + ->with(108) + ->willReturn($node); + + $this->rootFolder + ->method('getUserFolder') + ->with('user1') + ->willReturn($userFolder); - $group->method('inGroup')->with($user)->willReturn(false); + $this->userManager + ->method('userExists') + ->willReturnMap([ + ['user1', true], + ]); - $this->groupManager->method('get')->with('group')->willReturn($group); - $this->userManager->method('get')->with('user')->willReturn($user); + $user1 = $this->createMock(IUser::class); + $user1 + ->method('getUID') + ->willReturn('user1'); + + $this->userManager->method('get')->willReturnMap([ + ['user1', $user1], + ]); + + $this->groupManager + ->method('groupExists') + ->willReturnMap([ + ['group1', true], + ]); + + $share = $this->manager->newShare() + ->setShareType(IShare::TYPE_GROUP) + ->setSharedBy('user1') + ->setSharedWith('group1') + ->setNode($node) + ->setPermissions(Constants::PERMISSION_ALL); $this->config ->method('getAppValue') ->willReturnMap([ - ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], - ['core', 'shareapi_allow_group_sharing', 'yes', 'yes'], - ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], + ['core', 'shareapi_enabled', 'yes', 'yes'], + ['core', 'shareapi_allow_group_sharing', 'yes', 'no'], ]); - self::invokePrivate($this->manager, 'groupCreateChecks', [$share]); + self::invokePrivate($this->manager, 'generalChecks', [$share]); } - public function testGroupCreateChecksShareWithGroupMembersOnlyNullGroup(): void { + public function testGroupCreateChecksShareWithGroupMembersOnlyNotInGroup(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('Sharing is only allowed within your own groups'); - $share = $this->manager->newShare(); + $node = $this->createMock(Folder::class); + $node + ->method('getId') + ->willReturn(108); + $node + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_ALL); + $node + ->method('isShareable') + ->willReturn(true); + + $userFolder = $this->createMock(Folder::class); + $userFolder + ->method('getById') + ->with(108) + ->willReturn([$node]); + $userFolder + ->method('getFirstNodeById') + ->with(108) + ->willReturn($node); + + $this->rootFolder + ->method('getUserFolder') + ->with('user') + ->willReturn($userFolder); + + $share = $this->manager->newShare() + ->setShareType(IShare::TYPE_GROUP) + ->setSharedBy('user') + ->setSharedWith('group') + ->setNode($node) + ->setPermissions(Constants::PERMISSION_ALL); $user = $this->createMock(IUser::class); - $share->setSharedBy('user')->setSharedWith('group'); + $user + ->method('getUID') + ->willReturn('user'); - $this->groupManager->method('get')->with('group')->willReturn(null); - $this->userManager->method('get')->with('user')->willReturn($user); + $group = $this->createMock(IGroup::class); + $group + ->method('inGroup') + ->with($user) + ->willReturn(false); + + $this->groupManager + ->method('groupExists') + ->with('group') + ->willReturn(true); + + $this->groupManager + ->method('get') + ->with('group') + ->willReturn($group); + + $this->userManager + ->method('get') + ->with('user') + ->willReturn($user); $this->config ->method('getAppValue') ->willReturnMap([ + ['core', 'shareapi_enabled', 'yes', 'yes'], ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], ['core', 'shareapi_allow_group_sharing', 'yes', 'yes'], ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], ]); - $this->assertNull($this->invokePrivate($this->manager, 'groupCreateChecks', [$share])); + self::invokePrivate($this->manager, 'generalChecks', [$share]); } public function testGroupCreateChecksShareWithGroupMembersOnlyInGroup(): void { + $node = $this->createMock(Folder::class); + $node + ->method('getId') + ->willReturn(108); + $node + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_ALL); + $node + ->method('isShareable') + ->willReturn(true); + + $userFolder = $this->createMock(Folder::class); + $userFolder + ->method('getById') + ->with(108) + ->willReturn([$node]); + $userFolder + ->method('getFirstNodeById') + ->with(108) + ->willReturn($node); + + $this->rootFolder + ->method('getUserFolder') + ->with('user') + ->willReturn($userFolder); + $share = $this->manager->newShare(); + $share + ->setShareType(IShare::TYPE_GROUP) + ->setSharedBy('user') + ->setSharedWith('group') + ->setNode($node) + ->setPermissions(Constants::PERMISSION_ALL); $user = $this->createMock(IUser::class); + $user + ->method('getUID') + ->willReturn('user'); $group = $this->createMock(IGroup::class); - $share->setSharedBy('user')->setSharedWith('group'); - $this->userManager->method('get')->with('user')->willReturn($user); - $this->groupManager->method('get')->with('group')->willReturn($group); + $this->userManager + ->method('get') + ->with('user') + ->willReturn($user); - $group->method('inGroup')->with($user)->willReturn(true); + $this->groupManager + ->method('groupExists') + ->with('group') + ->willReturn(true); - $path = $this->createMock(Node::class); - $share->setNode($path); + $this->groupManager + ->method('get') + ->with('group') + ->willReturn($group); + + $group + ->method('inGroup') + ->with($user) + ->willReturn(true); $this->defaultProvider->method('getSharesByPath') - ->with($path) + ->with($node) ->willReturn([]); $this->config ->method('getAppValue') ->willReturnMap([ + ['core', 'shareapi_enabled', 'yes', 'yes'], ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], ['core', 'shareapi_allow_group_sharing', 'yes', 'yes'], ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], ]); - self::invokePrivate($this->manager, 'groupCreateChecks', [$share]); + self::invokePrivate($this->manager, 'generalChecks', [$share]); } @@ -2359,92 +2724,314 @@ public function testGroupCreateChecksPathAlreadySharedWithDifferentGroup(): void public function testLinkCreateChecksNoLinkSharesAllowed(): void { $this->expectException(\Exception::class); - $this->expectExceptionMessage('Link sharing is not allowed'); + $this->expectExceptionMessage('Public link sharing is not allowed.'); - $share = $this->manager->newShare(); + $node = $this->createMock(File::class); + $node + ->method('getId') + ->willReturn(108); + $node + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_ALL); + $node + ->method('isShareable') + ->willReturn(true); + + $userFolder = $this->createMock(Folder::class); + $userFolder + ->method('getById') + ->with(108) + ->willReturn([$node]); + $userFolder + ->method('getFirstNodeById') + ->with(108) + ->willReturn($node); + + $this->rootFolder + ->method('getUserFolder') + ->with('user1') + ->willReturn($userFolder); + + $this->userManager + ->method('userExists') + ->willReturnMap([ + ['user1', true], + ]); + + $user1 = $this->createMock(IUser::class); + $user1 + ->method('getUID') + ->willReturn('user1'); + + $this->userManager->method('get')->willReturnMap([ + ['user1', $user1], + ]); + + $share = $this->manager->newShare() + ->setShareType(IShare::TYPE_LINK) + ->setSharedBy('user1') + ->setNode($node) + ->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_SHARE); $this->config ->method('getAppValue') ->willReturnMap([ + ['core', 'shareapi_enabled', 'yes', 'yes'], ['core', 'shareapi_allow_links', 'yes', 'no'], ]); - self::invokePrivate($this->manager, 'linkCreateChecks', [$share]); + self::invokePrivate($this->manager, 'generalChecks', [$share]); } - - #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] public function testFileLinkCreateChecksNoPublicUpload(): void { - $share = $this->manager->newShare(); + $node = $this->createMock(File::class); + $node + ->method('getId') + ->willReturn(108); + $node + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_ALL); + $node + ->method('isShareable') + ->willReturn(true); + + $userFolder = $this->createMock(Folder::class); + $userFolder + ->method('getById') + ->with(108) + ->willReturn([$node]); + $userFolder + ->method('getFirstNodeById') + ->with(108) + ->willReturn($node); + + $this->rootFolder + ->method('getUserFolder') + ->with('user1') + ->willReturn($userFolder); + + $user1 = $this->createMock(IUser::class); + $user1 + ->method('getUID') + ->willReturn('user1'); + + $this->userManager + ->method('userExists') + ->willReturnMap([ + ['user1', true], + ]); + + $this->userManager + ->method('get') + ->willReturnMap([ + ['user1', $user1], + ]); - $share->setPermissions(Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE); - $share->setNodeType('file'); + $share = $this->manager->newShare() + ->setShareType(IShare::TYPE_LINK) + ->setSharedBy('user1') + ->setNode($node) + ->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE); $this->config ->method('getAppValue') ->willReturnMap([ + ['core', 'shareapi_enabled', 'yes', 'yes'], ['core', 'shareapi_allow_links', 'yes', 'yes'], + ['core', 'shareapi_allow_links_exclude_groups', '[]', '[]'], ['core', 'shareapi_allow_public_upload', 'yes', 'no'] ]); - self::invokePrivate($this->manager, 'linkCreateChecks', [$share]); + self::invokePrivate($this->manager, 'generalChecks', [$share]); } public function testFolderLinkCreateChecksNoPublicUpload(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Public upload is not allowed'); + $this->expectException(GenericShareException::class); + $this->expectExceptionMessage('Public upload is not allowed.'); - $share = $this->manager->newShare(); + $node = $this->createMock(Folder::class); + $node + ->method('getId') + ->willReturn(108); + $node + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_ALL); + $node + ->method('isShareable') + ->willReturn(true); + + $userFolder = $this->createMock(Folder::class); + $userFolder + ->method('getById') + ->with(108) + ->willReturn([$node]); + $userFolder + ->method('getFirstNodeById') + ->with(108) + ->willReturn($node); + + $this->rootFolder + ->method('getUserFolder') + ->with('user1') + ->willReturn($userFolder); + + $user1 = $this->createMock(IUser::class); + $user1 + ->method('getUID') + ->willReturn('user1'); + + $this->userManager + ->method('userExists') + ->willReturnMap([ + ['user1', true], + ]); + + $this->userManager + ->method('get') + ->willReturnMap([ + ['user1', $user1], + ]); - $share->setPermissions(Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE); - $share->setNodeType('folder'); + $share = $this->manager->newShare() + ->setShareType(IShare::TYPE_LINK) + ->setSharedBy('user1') + ->setNode($node) + ->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE); $this->config ->method('getAppValue') ->willReturnMap([ + ['core', 'shareapi_enabled', 'yes', 'yes'], ['core', 'shareapi_allow_links', 'yes', 'yes'], - ['core', 'shareapi_allow_public_upload', 'yes', 'no'] + ['core', 'shareapi_allow_links_exclude_groups', '[]', '[]'], + ['core', 'shareapi_allow_public_upload', 'yes', 'no'], ]); - self::invokePrivate($this->manager, 'linkCreateChecks', [$share]); + self::invokePrivate($this->manager, 'generalChecks', [$share]); } - #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] public function testLinkCreateChecksPublicUpload(): void { - $share = $this->manager->newShare(); + $node = $this->createMock(Folder::class); + $node + ->method('getId') + ->willReturn(108); + $node + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_ALL); + $node + ->method('isShareable') + ->willReturn(true); - $share->setPermissions(Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE); - $share->setSharedWith('sharedWith'); - $folder = $this->createMock(\OC\Files\Node\Folder::class); - $share->setNode($folder); + $userFolder = $this->createMock(Folder::class); + $userFolder + ->method('getById') + ->with(108) + ->willReturn([$node]); + $userFolder + ->method('getFirstNodeById') + ->with(108) + ->willReturn($node); + + $this->rootFolder + ->method('getUserFolder') + ->with('user1') + ->willReturn($userFolder); + + $user1 = $this->createMock(IUser::class); + $user1 + ->method('getUID') + ->willReturn('user1'); + + $this->userManager + ->method('userExists') + ->willReturnMap([ + ['user1', true], + ]); + + $this->userManager + ->method('get') + ->willReturnMap([ + ['user1', $user1], + ]); + + $share = $this->manager->newShare() + ->setShareType(IShare::TYPE_LINK) + ->setSharedBy('user1') + ->setNode($node) + ->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE); $this->config ->method('getAppValue') ->willReturnMap([ + ['core', 'shareapi_enabled', 'yes', 'yes'], ['core', 'shareapi_allow_links', 'yes', 'yes'], + ['core', 'shareapi_allow_links_exclude_groups', '[]', '[]'], ['core', 'shareapi_allow_public_upload', 'yes', 'yes'] ]); - self::invokePrivate($this->manager, 'linkCreateChecks', [$share]); + self::invokePrivate($this->manager, 'generalChecks', [$share]); } - #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions] public function testLinkCreateChecksReadOnly(): void { - $share = $this->manager->newShare(); + $node = $this->createMock(Folder::class); + $node + ->method('getId') + ->willReturn(108); + $node + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_ALL); + $node + ->method('isShareable') + ->willReturn(true); - $share->setPermissions(Constants::PERMISSION_READ); - $share->setSharedWith('sharedWith'); - $folder = $this->createMock(\OC\Files\Node\Folder::class); - $share->setNode($folder); + $userFolder = $this->createMock(Folder::class); + $userFolder + ->method('getById') + ->with(108) + ->willReturn([$node]); + $userFolder + ->method('getFirstNodeById') + ->with(108) + ->willReturn($node); + + $this->rootFolder + ->method('getUserFolder') + ->with('user1') + ->willReturn($userFolder); + + $this->userManager + ->method('userExists') + ->willReturnMap([ + ['user1', true], + ]); + + $user1 = $this->createMock(IUser::class); + $user1 + ->method('getUID') + ->willReturn('user1'); + + $this->userManager + ->method('get') + ->willReturnMap([ + ['user1', $user1], + ]); + + $share = $this->manager->newShare() + ->setShareType(IShare::TYPE_LINK) + ->setSharedBy('user1') + ->setNode($node) + ->setPermissions(Constants::PERMISSION_READ); $this->config ->method('getAppValue') ->willReturnMap([ + ['core', 'shareapi_enabled', 'yes', 'yes'], ['core', 'shareapi_allow_links', 'yes', 'yes'], - ['core', 'shareapi_allow_public_upload', 'yes', 'no'] + ['core', 'shareapi_allow_links_exclude_groups', '[]', '[]'], + ['core', 'shareapi_allow_public_upload', 'yes', 'yes'] ]); - self::invokePrivate($this->manager, 'linkCreateChecks', [$share]); + self::invokePrivate($this->manager, 'generalChecks', [$share]); } @@ -2593,26 +3180,79 @@ public function testCanShare($expected, $sharingEnabled, $disabledForUser): void ->getMock(); $manager->method('sharingDisabledForUser') - ->with('user') + ->with('user1') ->willReturn($disabledForUser); - $share = $this->manager->newShare(); - $share->setSharedBy('user'); + $this->overwriteService(IManager::class, $manager); + self::invokePrivate(Server::get(\OC\Core\Listener\RestrictInteractionListener::class), 'manager', [$manager]); + + $node = $this->createMock(File::class); + $node + ->method('getId') + ->willReturn(108); + $node + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_ALL); + $node + ->method('isShareable') + ->willReturn(true); + + $userFolder = $this->createMock(Folder::class); + $userFolder + ->method('getById') + ->with(108) + ->willReturn([$node]); + $userFolder + ->method('getFirstNodeById') + ->with(108) + ->willReturn($node); + + $this->rootFolder + ->method('getUserFolder') + ->with('user1') + ->willReturn($userFolder); + + $user1 = $this->createMock(IUser::class); + $user1 + ->method('getUID') + ->willReturn('user1'); + + $this->userManager + ->method('get') + ->with('user1') + ->willReturn($user1); + + $share = $this->manager->newShare() + ->setShareType(IShare::TYPE_USER) + ->setSharedBy('user1') + ->setSharedWith('user2') + ->setNode($node) + ->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_SHARE); + + $this->userManager + ->method('userExists') + ->willReturnMap([ + ['user1', true], + ['user2', true], + ]); $exception = false; try { - $res = self::invokePrivate($manager, 'canShare', [$share]); + $res = self::invokePrivate($manager, 'generalChecks', [$share]); } catch (\Exception $e) { $exception = true; } $this->assertEquals($expected, !$exception); + + $this->restoreService(IManager::class); + self::invokePrivate(Server::get(\OC\Core\Listener\RestrictInteractionListener::class), 'manager', [Server::get(IManager::class)]); } public function testCreateShareUser(): void { /** @var Manager&MockObject $manager */ $manager = $this->createManagerMock() - ->onlyMethods(['canShare', 'generalCreateChecks', 'userCreateChecks', 'pathCreateChecks', 'validateExpirationDateInternal']) + ->onlyMethods(['generalChecks', 'userCreateChecks', 'pathCreateChecks', 'validateExpirationDateInternal']) ->getMock(); $shareOwner = $this->createMock(IUser::class); @@ -2634,16 +3274,11 @@ public function testCreateShareUser(): void { Constants::PERMISSION_ALL); $manager->expects($this->once()) - ->method('canShare') - ->with($share); - $manager->expects($this->once()) - ->method('generalCreateChecks') + ->method('generalChecks') ->with($share); - ; $manager->expects($this->once()) ->method('userCreateChecks') ->with($share); - ; $manager->expects($this->once()) ->method('pathCreateChecks') ->with($path); @@ -2670,7 +3305,7 @@ public function testCreateShareUser(): void { public function testCreateShareGroup(): void { $manager = $this->createManagerMock() - ->onlyMethods(['canShare', 'generalCreateChecks', 'groupCreateChecks', 'pathCreateChecks', 'validateExpirationDateInternal']) + ->onlyMethods(['generalChecks', 'groupCreateChecks', 'pathCreateChecks', 'validateExpirationDateInternal']) ->getMock(); $shareOwner = $this->createMock(IUser::class); @@ -2692,16 +3327,11 @@ public function testCreateShareGroup(): void { Constants::PERMISSION_ALL); $manager->expects($this->once()) - ->method('canShare') + ->method('generalChecks') ->with($share); - $manager->expects($this->once()) - ->method('generalCreateChecks') - ->with($share); - ; $manager->expects($this->once()) ->method('groupCreateChecks') ->with($share); - ; $manager->expects($this->once()) ->method('pathCreateChecks') ->with($path); @@ -2729,9 +3359,7 @@ public function testCreateShareGroup(): void { public function testCreateShareLink(): void { $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', - 'generalCreateChecks', - 'linkCreateChecks', + 'generalChecks', 'pathCreateChecks', 'validateExpirationDateLink', 'verifyPassword', @@ -2760,16 +3388,8 @@ public function testCreateShareLink(): void { ->setPassword('password'); $manager->expects($this->once()) - ->method('canShare') - ->with($share); - $manager->expects($this->once()) - ->method('generalCreateChecks') + ->method('generalChecks') ->with($share); - ; - $manager->expects($this->once()) - ->method('linkCreateChecks') - ->with($share); - ; $manager->expects($this->once()) ->method('pathCreateChecks') ->with($path); @@ -2838,9 +3458,7 @@ public function testCreateShareLink(): void { public function testCreateShareMail(): void { $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', - 'generalCreateChecks', - 'linkCreateChecks', + 'generalChecks', 'pathCreateChecks', 'validateExpirationDateLink', 'verifyPassword', @@ -2865,14 +3483,9 @@ public function testCreateShareMail(): void { ->setPermissions(Constants::PERMISSION_ALL); $manager->expects($this->once()) - ->method('canShare') - ->with($share); - $manager->expects($this->once()) - ->method('generalCreateChecks') + ->method('generalChecks') ->with($share); - $manager->expects($this->once()) - ->method('linkCreateChecks'); $manager->expects($this->once()) ->method('pathCreateChecks') ->with($path); @@ -2936,8 +3549,7 @@ public function testCreateShareHookError(): void { $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', - 'generalCreateChecks', + 'generalChecks', 'userCreateChecks', 'pathCreateChecks', 'validateExpirationDateInternal', @@ -2963,16 +3575,11 @@ public function testCreateShareHookError(): void { Constants::PERMISSION_ALL); $manager->expects($this->once()) - ->method('canShare') - ->with($share); - $manager->expects($this->once()) - ->method('generalCreateChecks') + ->method('generalChecks') ->with($share); - ; $manager->expects($this->once()) ->method('userCreateChecks') ->with($share); - ; $manager->expects($this->once()) ->method('pathCreateChecks') ->with($path); @@ -3004,7 +3611,7 @@ public function testCreateShareHookError(): void { public function testCreateShareOfIncomingFederatedShare(): void { $manager = $this->createManagerMock() - ->onlyMethods(['canShare', 'generalCreateChecks', 'userCreateChecks', 'pathCreateChecks', 'validateExpirationDateInternal']) + ->onlyMethods(['generalChecks', 'userCreateChecks', 'pathCreateChecks', 'validateExpirationDateInternal']) ->getMock(); $shareOwner = $this->createMock(IUser::class); @@ -3045,16 +3652,11 @@ public function testCreateShareOfIncomingFederatedShare(): void { Constants::PERMISSION_ALL); $manager->expects($this->once()) - ->method('canShare') - ->with($share); - $manager->expects($this->once()) - ->method('generalCreateChecks') + ->method('generalChecks') ->with($share); - ; $manager->expects($this->once()) ->method('userCreateChecks') ->with($share); - ; $manager->expects($this->once()) ->method('pathCreateChecks') ->with($path); @@ -3618,7 +4220,6 @@ public function testUpdateShareCantChangeShareType(): void { $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', 'getShareById' ]) ->getMock(); @@ -3626,7 +4227,6 @@ public function testUpdateShareCantChangeShareType(): void { $originalShare = $this->manager->newShare(); $originalShare->setShareType(IShare::TYPE_GROUP); - $manager->expects($this->once())->method('canShare'); $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); $share = $this->manager->newShare(); @@ -3646,7 +4246,6 @@ public function testUpdateShareCantChangeRecipientForGroupShare(): void { $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', 'getShareById' ]) ->getMock(); @@ -3655,7 +4254,6 @@ public function testUpdateShareCantChangeRecipientForGroupShare(): void { $originalShare->setShareType(IShare::TYPE_GROUP) ->setSharedWith('origGroup'); - $manager->expects($this->once())->method('canShare'); $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); $share = $this->manager->newShare(); @@ -3674,7 +4272,6 @@ public function testUpdateShareCantShareWithOwner(): void { $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', 'getShareById' ]) ->getMock(); @@ -3683,7 +4280,6 @@ public function testUpdateShareCantShareWithOwner(): void { $originalShare->setShareType(IShare::TYPE_USER) ->setSharedWith('sharedWith'); - $manager->expects($this->once())->method('canShare'); $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); $share = $this->manager->newShare(); @@ -3701,9 +4297,8 @@ public function testUpdateShareUser(): void { $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', 'getShareById', - 'generalCreateChecks', + 'generalChecks', 'userCreateChecks', 'pathCreateChecks', ]) @@ -3718,7 +4313,6 @@ public function testUpdateShareUser(): void { $node->method('getId')->willReturn(100); $node->method('getPath')->willReturn('/newUser/files/myPath'); - $manager->expects($this->once())->method('canShare'); $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); $share = $this->manager->newShare(); @@ -3765,9 +4359,8 @@ public function testUpdateShareUser(): void { public function testUpdateShareGroup(): void { $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', 'getShareById', - 'generalCreateChecks', + 'generalChecks', 'groupCreateChecks', 'pathCreateChecks', ]) @@ -3778,7 +4371,6 @@ public function testUpdateShareGroup(): void { ->setSharedWith('origUser') ->setPermissions(31); - $manager->expects($this->once())->method('canShare'); $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); $node = $this->createMock(File::class); @@ -3811,10 +4403,8 @@ public function testUpdateShareGroup(): void { public function testUpdateShareLink(): void { $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', 'getShareById', - 'generalCreateChecks', - 'linkCreateChecks', + 'generalChecks', 'pathCreateChecks', 'verifyPassword', 'validateExpirationDateLink', @@ -3844,7 +4434,6 @@ public function testUpdateShareLink(): void { ->setNode($file) ->setPermissions(15); - $manager->expects($this->once())->method('canShare'); $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); $manager->expects($this->once())->method('validateExpirationDateLink')->with($share); $manager->expects($this->once())->method('verifyPassword')->with('password'); @@ -3892,10 +4481,8 @@ public function testUpdateShareLinkEnableSendPasswordByTalkWithNoPassword(): voi $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', 'getShareById', - 'generalCreateChecks', - 'linkCreateChecks', + 'generalChecks', 'pathCreateChecks', 'verifyPassword', 'validateExpirationDateLink', @@ -3926,10 +4513,8 @@ public function testUpdateShareLinkEnableSendPasswordByTalkWithNoPassword(): voi ->setNode($file) ->setPermissions(15); - $manager->expects($this->once())->method('canShare'); $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalCreateChecks')->with($share); - $manager->expects($this->once())->method('linkCreateChecks')->with($share); + $manager->expects($this->once())->method('generalChecks')->with($share); $manager->expects($this->never())->method('verifyPassword'); $manager->expects($this->never())->method('pathCreateChecks'); $manager->expects($this->never())->method('validateExpirationDateLink'); @@ -3958,12 +4543,10 @@ public function testUpdateShareLinkEnableSendPasswordByTalkWithNoPassword(): voi public function testUpdateShareMail(): void { $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', 'getShareById', - 'generalCreateChecks', + 'generalChecks', 'verifyPassword', 'pathCreateChecks', - 'linkCreateChecks', 'validateExpirationDateLink', ]) ->getMock(); @@ -3991,12 +4574,10 @@ public function testUpdateShareMail(): void { ->setNode($file) ->setPermissions(Constants::PERMISSION_ALL); - $manager->expects($this->once())->method('canShare'); $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalCreateChecks')->with($share); + $manager->expects($this->once())->method('generalChecks')->with($share); $manager->expects($this->once())->method('verifyPassword')->with('password'); $manager->expects($this->once())->method('pathCreateChecks')->with($file); - $manager->expects($this->once())->method('linkCreateChecks'); $manager->expects($this->once())->method('validateExpirationDateLink'); $this->hasher->expects($this->once()) @@ -4038,12 +4619,10 @@ public function testUpdateShareMail(): void { public function testUpdateShareMailEnableSendPasswordByTalk(): void { $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', 'getShareById', - 'generalCreateChecks', + 'generalChecks', 'verifyPassword', 'pathCreateChecks', - 'linkCreateChecks', 'validateExpirationDateLink', ]) ->getMock(); @@ -4074,12 +4653,10 @@ public function testUpdateShareMailEnableSendPasswordByTalk(): void { ->setNode($file) ->setPermissions(Constants::PERMISSION_ALL); - $manager->expects($this->once())->method('canShare'); $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalCreateChecks')->with($share); + $manager->expects($this->once())->method('generalChecks')->with($share); $manager->expects($this->once())->method('verifyPassword')->with('password'); $manager->expects($this->once())->method('pathCreateChecks')->with($file); - $manager->expects($this->once())->method('linkCreateChecks'); $manager->expects($this->once())->method('validateExpirationDateLink'); $this->hasher->expects($this->once()) @@ -4121,12 +4698,10 @@ public function testUpdateShareMailEnableSendPasswordByTalk(): void { public function testUpdateShareMailEnableSendPasswordByTalkWithDifferentPassword(): void { $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', 'getShareById', - 'generalCreateChecks', + 'generalChecks', 'verifyPassword', 'pathCreateChecks', - 'linkCreateChecks', 'validateExpirationDateLink', ]) ->getMock(); @@ -4157,12 +4732,10 @@ public function testUpdateShareMailEnableSendPasswordByTalkWithDifferentPassword ->setNode($file) ->setPermissions(Constants::PERMISSION_ALL); - $manager->expects($this->once())->method('canShare'); $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalCreateChecks')->with($share); + $manager->expects($this->once())->method('generalChecks')->with($share); $manager->expects($this->once())->method('verifyPassword')->with('password'); $manager->expects($this->once())->method('pathCreateChecks')->with($file); - $manager->expects($this->once())->method('linkCreateChecks'); $manager->expects($this->once())->method('validateExpirationDateLink'); $this->hasher->expects($this->once()) @@ -4212,12 +4785,10 @@ public function testUpdateShareMailEnableSendPasswordByTalkWithNoPassword(): voi $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', 'getShareById', - 'generalCreateChecks', + 'generalChecks', 'verifyPassword', 'pathCreateChecks', - 'linkCreateChecks', 'validateExpirationDateLink', ]) ->getMock(); @@ -4248,12 +4819,10 @@ public function testUpdateShareMailEnableSendPasswordByTalkWithNoPassword(): voi ->setNode($file) ->setPermissions(Constants::PERMISSION_ALL); - $manager->expects($this->once())->method('canShare'); $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalCreateChecks')->with($share); + $manager->expects($this->once())->method('generalChecks')->with($share); $manager->expects($this->never())->method('verifyPassword'); $manager->expects($this->never())->method('pathCreateChecks'); - $manager->expects($this->once())->method('linkCreateChecks'); $manager->expects($this->never())->method('validateExpirationDateLink'); // If the password is empty, we have nothing to hash @@ -4285,12 +4854,10 @@ public function testUpdateShareMailEnableSendPasswordByTalkRemovingPassword(): v $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', 'getShareById', - 'generalCreateChecks', + 'generalChecks', 'verifyPassword', 'pathCreateChecks', - 'linkCreateChecks', 'validateExpirationDateLink', ]) ->getMock(); @@ -4321,12 +4888,10 @@ public function testUpdateShareMailEnableSendPasswordByTalkRemovingPassword(): v ->setNode($file) ->setPermissions(Constants::PERMISSION_ALL); - $manager->expects($this->once())->method('canShare'); $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalCreateChecks')->with($share); + $manager->expects($this->once())->method('generalChecks')->with($share); $manager->expects($this->once())->method('verifyPassword'); $manager->expects($this->never())->method('pathCreateChecks'); - $manager->expects($this->once())->method('linkCreateChecks'); $manager->expects($this->never())->method('validateExpirationDateLink'); // If the password is empty, we have nothing to hash @@ -4358,12 +4923,10 @@ public function testUpdateShareMailEnableSendPasswordByTalkRemovingPasswordWithE $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', 'getShareById', - 'generalCreateChecks', + 'generalChecks', 'verifyPassword', 'pathCreateChecks', - 'linkCreateChecks', 'validateExpirationDateLink', ]) ->getMock(); @@ -4394,12 +4957,10 @@ public function testUpdateShareMailEnableSendPasswordByTalkRemovingPasswordWithE ->setNode($file) ->setPermissions(Constants::PERMISSION_ALL); - $manager->expects($this->once())->method('canShare'); $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalCreateChecks')->with($share); + $manager->expects($this->once())->method('generalChecks')->with($share); $manager->expects($this->once())->method('verifyPassword'); $manager->expects($this->never())->method('pathCreateChecks'); - $manager->expects($this->once())->method('linkCreateChecks'); $manager->expects($this->never())->method('validateExpirationDateLink'); // If the password is empty, we have nothing to hash @@ -4431,12 +4992,10 @@ public function testUpdateShareMailEnableSendPasswordByTalkWithPreviousPassword( $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', 'getShareById', - 'generalCreateChecks', + 'generalChecks', 'verifyPassword', 'pathCreateChecks', - 'linkCreateChecks', 'validateExpirationDateLink', ]) ->getMock(); @@ -4467,12 +5026,10 @@ public function testUpdateShareMailEnableSendPasswordByTalkWithPreviousPassword( ->setNode($file) ->setPermissions(Constants::PERMISSION_ALL); - $manager->expects($this->once())->method('canShare'); $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalCreateChecks')->with($share); + $manager->expects($this->once())->method('generalChecks')->with($share); $manager->expects($this->never())->method('verifyPassword'); $manager->expects($this->never())->method('pathCreateChecks'); - $manager->expects($this->once())->method('linkCreateChecks'); $manager->expects($this->never())->method('validateExpirationDateLink'); // If the old & new passwords are the same, we don't do anything @@ -4505,12 +5062,10 @@ public function testUpdateShareMailDisableSendPasswordByTalkWithPreviousPassword $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', 'getShareById', - 'generalCreateChecks', + 'generalChecks', 'verifyPassword', 'pathCreateChecks', - 'linkCreateChecks', 'validateExpirationDateLink', ]) ->getMock(); @@ -4541,12 +5096,10 @@ public function testUpdateShareMailDisableSendPasswordByTalkWithPreviousPassword ->setNode($file) ->setPermissions(Constants::PERMISSION_ALL); - $manager->expects($this->once())->method('canShare'); $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalCreateChecks')->with($share); + $manager->expects($this->once())->method('generalChecks')->with($share); $manager->expects($this->never())->method('verifyPassword'); $manager->expects($this->never())->method('pathCreateChecks'); - $manager->expects($this->once())->method('linkCreateChecks'); $manager->expects($this->never())->method('validateExpirationDateLink'); // If the old & new passwords are the same, we don't do anything @@ -4579,12 +5132,10 @@ public function testUpdateShareMailDisableSendPasswordByTalkWithoutChangingPassw $manager = $this->createManagerMock() ->onlyMethods([ - 'canShare', 'getShareById', - 'generalCreateChecks', + 'generalChecks', 'verifyPassword', 'pathCreateChecks', - 'linkCreateChecks', 'validateExpirationDateLink', ]) ->getMock(); @@ -4615,12 +5166,10 @@ public function testUpdateShareMailDisableSendPasswordByTalkWithoutChangingPassw ->setNode($file) ->setPermissions(Constants::PERMISSION_ALL); - $manager->expects($this->once())->method('canShare'); $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); - $manager->expects($this->once())->method('generalCreateChecks')->with($share); + $manager->expects($this->once())->method('generalChecks')->with($share); $manager->expects($this->never())->method('verifyPassword'); $manager->expects($this->never())->method('pathCreateChecks'); - $manager->expects($this->once())->method('linkCreateChecks'); $manager->expects($this->never())->method('validateExpirationDateLink'); // If the old & new passwords are the same, we don't do anything @@ -4936,7 +5485,7 @@ public function testGetAccessList(): void { ->willReturn($userFolder); $expected = [ - 'users' => ['owner', 'user1', 'user2', 'user3', '123456','user4', 'user5', '234567'], + 'users' => ['owner', 'user1', 'user2', 'user3', '123456', 'user4', 'user5', '234567'], 'remote' => true, 'public' => true, ];