diff --git a/apps/files_sharing/lib/Repair/CleanupShareTarget.php b/apps/files_sharing/lib/Repair/CleanupShareTarget.php index 872fbb70930b0..459dd6194035e 100644 --- a/apps/files_sharing/lib/Repair/CleanupShareTarget.php +++ b/apps/files_sharing/lib/Repair/CleanupShareTarget.php @@ -124,10 +124,11 @@ public function run(IOutput $output) { $userMounts[$newMountPoint] = $mount; unset($userMounts[$oldMountPoint]); - $this->userMountCache->removeMount($oldMountPoint); + $this->userMountCache->removeMount($oldMountPoint, $recipient); $this->userMountCache->addMount($recipient, $newMountPoint, new CacheEntry([ 'fileid' => $mount->getRootId(), 'storage' => $mount->getStorageId(), + 'path' => $mount->getRootInternalPath(), ]), $mount->getMountProvider(), $mount->getMountId()); } } diff --git a/apps/files_sharing/tests/ShareMountEventsTest.php b/apps/files_sharing/tests/ShareMountEventsTest.php new file mode 100644 index 0000000000000..5cd8820fd5266 --- /dev/null +++ b/apps/files_sharing/tests/ShareMountEventsTest.php @@ -0,0 +1,134 @@ + */ + private array $events = []; + /** @var \Closure(Event): void */ + private \Closure $listener; + + protected function setUp(): void { + parent::setUp(); + + $this->eventDispatcher = Server::get(IEventDispatcher::class); + $this->listener = function (Event $event): void { + $this->events[] = $event; + }; + $this->eventDispatcher->addListener(UserMountAddedEvent::class, $this->listener); + $this->eventDispatcher->addListener(UserMountRemovedEvent::class, $this->listener); + + $this->rootFolder->getUserFolder(self::TEST_FILES_SHARING_API_USER1)->newFolder(self::FOLDER); + + $this->loginHelper(self::TEST_FILES_SHARING_API_USER1); + } + + protected function tearDown(): void { + $this->eventDispatcher->removeListener(UserMountAddedEvent::class, $this->listener); + $this->eventDispatcher->removeListener(UserMountRemovedEvent::class, $this->listener); + Server::get(SharesUpdatedListener::class)->setCutOffMarkTime(-1); + $this->events = []; + + parent::tearDown(); + } + + /** + * @return array + */ + public static function shareProvider(): array { + // A cutoff time of -1 updates the share mounts of recipients immediately, + // 0 marks recipients so their share mounts are updated on their next setup + return [ + 'user share, immediate update' => [IShare::TYPE_USER, self::TEST_FILES_SHARING_API_USER2, -1], + 'user share, deferred update' => [IShare::TYPE_USER, self::TEST_FILES_SHARING_API_USER2, 0], + 'group share, immediate update' => [IShare::TYPE_GROUP, self::TEST_FILES_SHARING_API_GROUP1, -1], + 'group share, deferred update' => [IShare::TYPE_GROUP, self::TEST_FILES_SHARING_API_GROUP1, 0], + ]; + } + + #[DataProvider(methodName: 'shareProvider')] + public function testMountAddedEventForCreatedShare(int $shareType, string $recipient, float $cutOffTime): void { + Server::get(SharesUpdatedListener::class)->setCutOffMarkTime($cutOffTime); + + $this->share($shareType, self::FOLDER, self::TEST_FILES_SHARING_API_USER1, $recipient, Constants::PERMISSION_ALL); + $this->getUserFolder(self::TEST_FILES_SHARING_API_USER2)->get(self::FOLDER); + + $this->assertContains( + '/' . self::TEST_FILES_SHARING_API_USER2 . '/files/' . self::FOLDER . '/', + $this->getMountPointsOfEvents(UserMountAddedEvent::class, self::TEST_FILES_SHARING_API_USER2), + 'A UserMountAddedEvent should be dispatched for the share mount of the recipient', + ); + } + + #[DataProvider(methodName: 'shareProvider')] + public function testMountRemovedEventForDeletedShare(int $shareType, string $recipient, float $cutOffTime): void { + $share = $this->share($shareType, self::FOLDER, self::TEST_FILES_SHARING_API_USER1, $recipient, Constants::PERMISSION_ALL); + $this->getUserFolder(self::TEST_FILES_SHARING_API_USER2)->get(self::FOLDER); + + Server::get(SharesUpdatedListener::class)->setCutOffMarkTime($cutOffTime); + $this->shareManager->deleteShare($share); + $this->assertFalse( + $this->getUserFolder(self::TEST_FILES_SHARING_API_USER2)->nodeExists(self::FOLDER), + 'The share should no longer be mounted for the recipient', + ); + + $this->assertEquals( + ['/' . self::TEST_FILES_SHARING_API_USER2 . '/files/' . self::FOLDER . '/'], + $this->getMountPointsOfEvents(UserMountRemovedEvent::class, self::TEST_FILES_SHARING_API_USER2), + 'A UserMountRemovedEvent should be dispatched for the share mount of the recipient', + ); + } + + /** + * Get the user folder of the user as a new request by them would + * + * Logging in tears down the file system, so the mounts of the user are set up again. + * Otherwise the mounts already set up for the user earlier in this process are + * reused and changes to the shares of the user are not picked up. + */ + private function getUserFolder(string $userId): Folder { + $this->loginHelper($userId); + return $this->rootFolder->getUserFolder($userId); + } + + /** + * @param class-string $eventClass + * @return list + */ + private function getMountPointsOfEvents(string $eventClass, string $userId): array { + $mountPoints = []; + foreach ($this->events as $event) { + if ($event instanceof $eventClass && $event->mountPoint->getUser()->getUID() === $userId) { + $mountPoints[] = $event->mountPoint->getMountPoint(); + } + } + return $mountPoints; + } +} diff --git a/lib/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php index 1430ab469eff3..df89c4c84966a 100644 --- a/lib/private/Files/Config/UserMountCache.php +++ b/lib/private/Files/Config/UserMountCache.php @@ -557,8 +557,26 @@ public function getMountsInPath(IUser $user, string $path): array { return $result; } + private function loadMountInfo(IUser $user, string $mountPoint): ?CachedMountInfo { + $builder = $this->connection->getQueryBuilder(); + $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class') + ->from('mounts', 'm') + ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid')) + ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID()))) + ->andWhere($builder->expr()->eq('mount_point_hash', $builder->createNamedParameter(hash('xxh128', $mountPoint), IQueryBuilder::PARAM_STR))); + + $row = $query->executeQuery()->fetch(); + return $row ? $this->dbRowToMountInfo($row) : null; + } + #[\Override] public function removeMount(string $mountPoint, ?IUser $user = null): void { + if ($user) { + $mount = $this->loadMountInfo($user, $mountPoint); + } else { + $mount = null; + } + $query = $this->connection->getQueryBuilder(); $query->delete('mounts') ->where($query->expr()->eq('mount_point_hash', $query->createNamedParameter(hash('xxh128', $mountPoint)))); @@ -572,6 +590,10 @@ public function removeMount(string $mountPoint, ?IUser $user = null): void { [, $userId] = $parts; unset($this->mountsForUsers[$userId]); } + + if ($user && $mount) { + $this->eventDispatcher->dispatchTyped(new UserMountRemovedEvent($mount)); + } } #[\Override] @@ -596,12 +618,24 @@ public function addMount( try { $query->executeStatement(); + unset($this->mountsForUsers[$user->getUID()]); } catch (DbalException $e) { if ($e->getReason() !== DbalException::REASON_UNIQUE_CONSTRAINT_VIOLATION) { throw $e; } } + + $cachedMount = new CachedMountInfo( + $user, + $rootCacheEntry->getStorageId(), + $rootCacheEntry->getId(), + $mountPoint, + $mountProvider, + $mountId, + $rootCacheEntry->getPath(), + ); + $this->eventDispatcher->dispatchTyped(new UserMountAddedEvent($cachedMount)); } /**