From 771939e6cdcdebf762b4f409cb35aa56fd637f9a Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 13 Nov 2024 18:51:02 +0100 Subject: [PATCH 1/9] fix: fix missing dot in unique name when restoring from trash Signed-off-by: Robin Appelman --- lib/Trash/TrashBackend.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Trash/TrashBackend.php b/lib/Trash/TrashBackend.php index 3ba55adbe..85b506228 100644 --- a/lib/Trash/TrashBackend.php +++ b/lib/Trash/TrashBackend.php @@ -159,7 +159,7 @@ public function restoreItem(ITrashItem $item) { $target .= ' (' . $i . ')'; if (isset($info['extension'])) { - $target .= $info['extension']; + $target .= '.' . $info['extension']; } return $target; From 2652ce8ce25dc4633935e5579120922ea358fa0a Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 13 Nov 2024 18:53:38 +0100 Subject: [PATCH 2/9] fix: remove manual un-jailing when moving to trash as the storage now handles this properly* *: expect for the mentioned bug Signed-off-by: Robin Appelman --- lib/Trash/TrashBackend.php | 49 ++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/lib/Trash/TrashBackend.php b/lib/Trash/TrashBackend.php index 85b506228..0cf01e614 100644 --- a/lib/Trash/TrashBackend.php +++ b/lib/Trash/TrashBackend.php @@ -21,7 +21,7 @@ namespace OCA\GroupFolders\Trash; -use OC\Files\Storage\Wrapper\Jail; +use OC\Files\Storage\Wrapper\Encryption; use OCA\Files_Trashbin\Expiration; use OCA\Files_Trashbin\Trash\ITrashBackend; use OCA\Files_Trashbin\Trash\ITrashItem; @@ -230,12 +230,17 @@ public function moveToTrash(IStorage $storage, string $internalPath): bool { $trashStorage = $trashFolder->getStorage(); $time = time(); $trashName = $name . '.d' . $time; - [$unJailedStorage, $unJailedInternalPath] = $this->unwrapJails($storage, $internalPath); $targetInternalPath = $trashFolder->getInternalPath() . '/' . $trashName; - if ($trashStorage->moveFromStorage($unJailedStorage, $unJailedInternalPath, $targetInternalPath)) { + // until the fix from https://github.com/nextcloud/server/pull/49262 is in all versions we support we need to manually disable the optimization + if ($storage->instanceOfStorage(Encryption::class)) { + $result = $this->moveFromEncryptedStorage($storage, $trashStorage, $internalPath, $targetInternalPath); + } else { + $result = $trashStorage->moveFromStorage($storage, $internalPath, $targetInternalPath); + } + if ($result) { $this->trashManager->addTrashItem($folderId, $name, $time, $internalPath, $fileEntry->getId()); if ($trashStorage->getCache()->getId($targetInternalPath) !== $fileEntry->getId()) { - $trashStorage->getCache()->moveFromCache($unJailedStorage->getCache(), $unJailedInternalPath, $targetInternalPath); + $trashStorage->getCache()->moveFromCache($storage->getCache(), $internalPath, $targetInternalPath); } } else { throw new \Exception("Failed to move groupfolder item to trash"); @@ -246,16 +251,36 @@ public function moveToTrash(IStorage $storage, string $internalPath): bool { } } - private function unwrapJails(IStorage $storage, string $internalPath): array { - $unJailedInternalPath = $internalPath; - $unJailedStorage = $storage; - while ($unJailedStorage->instanceOfStorage(Jail::class)) { - $unJailedStorage = $unJailedStorage->getWrapperStorage(); - if ($unJailedStorage instanceof Jail) { - $unJailedInternalPath = $unJailedStorage->getUnjailedPath($unJailedInternalPath); + /** + * move from storage when we can't just move within the storage + * + * This is copied from the fallback implementation from Common::moveFromStorage + */ + private function moveFromEncryptedStorage(IStorage $sourceStorage, IStorage $targetStorage, string $sourceInternalPath, string $targetInternalPath): bool { + if (!$sourceStorage->isDeletable($sourceInternalPath)) { + return false; + } + + $result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, true); + if ($result) { + if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { + /** @var ObjectStoreStorage $sourceStorage */ + $sourceStorage->setPreserveCacheOnDelete(true); + } + try { + if ($sourceStorage->is_dir($sourceInternalPath)) { + $result = $sourceStorage->rmdir($sourceInternalPath); + } else { + $result = $sourceStorage->unlink($sourceInternalPath); + } + } finally { + if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { + /** @var ObjectStoreStorage $sourceStorage */ + $sourceStorage->setPreserveCacheOnDelete(false); + } } } - return [$unJailedStorage, $unJailedInternalPath]; + return $result; } private function userHasAccessToFolder(IUser $user, int $folderId): bool { From 29ca0d2b04675f980839999f04c2e7339d885bd3 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 13 Nov 2024 19:01:57 +0100 Subject: [PATCH 3/9] fix: use "full" target folder from user when restoring trash items Signed-off-by: Robin Appelman --- lib/Trash/TrashBackend.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Trash/TrashBackend.php b/lib/Trash/TrashBackend.php index 0cf01e614..d622bbd73 100644 --- a/lib/Trash/TrashBackend.php +++ b/lib/Trash/TrashBackend.php @@ -120,6 +120,7 @@ public function restoreItem(ITrashItem $item) { throw new \LogicException('Trying to restore normal trash item in group folder trash backend'); } $user = $item->getUser(); + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); [, $folderId] = explode('/', $item->getTrashPath()); $node = $this->getNodeForTrashItem($user, $item); if ($node === null) { @@ -135,7 +136,7 @@ public function restoreItem(ITrashItem $item) { $trashStorage = $node->getStorage(); /** @var Folder $targetFolder */ - $targetFolder = $this->mountProvider->getFolder((int)$folderId); + $targetFolder = $userFolder->get($item->getGroupFolderMountPoint()); $originalLocation = $item->getInternalOriginalLocation(); $parent = dirname($originalLocation); if ($parent === '.') { From aa6fe34757e59f7e0be9638c6e1b4cdcdba99615 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 18 Nov 2024 14:42:57 +0100 Subject: [PATCH 4/9] fix: make root cache entry optional for groupfolder storage Signed-off-by: Robin Appelman --- lib/Mount/GroupFolderStorage.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/Mount/GroupFolderStorage.php b/lib/Mount/GroupFolderStorage.php index 7d1449a61..ca5fcd440 100644 --- a/lib/Mount/GroupFolderStorage.php +++ b/lib/Mount/GroupFolderStorage.php @@ -31,7 +31,7 @@ class GroupFolderStorage extends Quota { private int $folderId; - private ICacheEntry $rootEntry; + private ?ICacheEntry $rootEntry; private IUserSession $userSession; private ?IUser $mountOwner = null; /** @var RootEntryCache|null */ @@ -68,7 +68,11 @@ public function getCache($path = '', $storage = null) { $storage = $this; } - $this->cache = new RootEntryCache(parent::getCache($path, $storage), $this->rootEntry); + $cache = parent::getCache($path, $storage); + if ($this->rootEntry !== null) { + $cache = new RootEntryCache($cache, $this->rootEntry);} + $this->cache = $cache; + return $this->cache; } From 8adb8e92a14da2946ac2cb7e5477a8871368c364 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 18 Nov 2024 14:43:56 +0100 Subject: [PATCH 5/9] fix: create a separate mount for trashbin to make encryption work Signed-off-by: Robin Appelman --- lib/Mount/MountProvider.php | 125 +++++++++++++++++++++++++---------- lib/Trash/GroupTrashItem.php | 13 +--- lib/Trash/TrashBackend.php | 38 ++++++++--- 3 files changed, 119 insertions(+), 57 deletions(-) diff --git a/lib/Mount/MountProvider.php b/lib/Mount/MountProvider.php index 7f0fd491c..8a0fb4625 100644 --- a/lib/Mount/MountProvider.php +++ b/lib/Mount/MountProvider.php @@ -140,7 +140,7 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader) { $aclManager = $this->aclManagerFactory->getACLManager($user, $this->getRootStorageId()); $rootRules = $aclManager->getRelevantRulesForPath($aclRootPaths); - return array_values(array_filter(array_map(function ($folder) use ($user, $loader, $conflicts, $aclManager, $rootRules) { + return array_merge(...array_filter(array_map(function (array $folder) use ($user, $loader, $conflicts, $aclManager, $rootRules): ?array { // check for existing files in the user home and rename them if needed $originalFolderName = $folder['mount_point']; if (in_array($originalFolderName, $conflicts)) { @@ -159,7 +159,7 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader) { $userStorage->getPropagator()->propagateChange("files/$folderName", time()); } - return $this->getMount( + $mount = $this->getMount( $folder['folder_id'], '/' . $user->getUID() . '/files/' . $folder['mount_point'], $folder['permissions'], @@ -171,6 +171,22 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader) { $aclManager, $rootRules ); + if (!$mount) { + return null; + } + $trashMount = $this->getTrashMount( + $folder['folder_id'], + '/' . $user->getUID() . '/files_trashbin/groupfolders/' . $folder['folder_id'], + $folder['quota'], + $loader, + $user + ); + + return [ + $mount, + $trashMount, + ]; + }, $folders))); } @@ -193,16 +209,16 @@ private function getCurrentUID(): ?string { } public function getMount( - int $id, - string $mountPoint, - int $permissions, - int $quota, - ?ICacheEntry $cacheEntry = null, + int $id, + string $mountPoint, + int $permissions, + int $quota, + ?ICacheEntry $cacheEntry = null, ?IStorageFactory $loader = null, - bool $acl = false, - ?IUser $user = null, - ?ACLManager $aclManager = null, - array $rootRules = [] + bool $acl = false, + ?IUser $user = null, + ?ACLManager $aclManager = null, + array $rootRules = [] ): ?IMountPoint { if (!$cacheEntry) { // trigger folder creation @@ -230,52 +246,91 @@ public function getMount( $cacheEntry['permissions'] &= $aclRootPermissions; } + $quotaStorage = $this->getGroupFolderStorage($id, $storage, $user, $rootPath, $quota, $cacheEntry); + + $maskedStore = new PermissionsMask([ + 'storage' => $quotaStorage, + 'mask' => $permissions, + ]); + + if (!$this->allowRootShare) { + $maskedStore = new RootPermissionsMask([ + 'storage' => $maskedStore, + 'mask' => Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE, + ]); + } + + return new GroupMountPoint( + $id, + $maskedStore, + $mountPoint, + null, + $loader + ); + } + + public function getTrashMount( + int $id, + string $mountPoint, + int $quota, + IStorageFactory $loader, + IUser $user, + ): IMountPoint { + + $storage = $this->getRootFolder()->getStorage(); + + $storage->setOwner($user?->getUID()); + + $trashPath = $this->getRootFolder()->getInternalPath() . '/trash/' . $id; + + $trashStorage = $this->getGroupFolderStorage($id, $storage, $user, $trashPath, $quota, null); + + return new GroupMountPoint( + $id, + $trashStorage, + $mountPoint, + null, + $loader + ); + } + + public function getGroupFolderStorage( + int $id, + IStorage $rootStorage, + ?IUser $user, + string $rootPath, + int $quota, + ?ICacheEntry $rootCacheEntry, + ): IStorage { if ($this->enableEncryption) { $baseStorage = new GroupFolderEncryptionJail([ - 'storage' => $storage, - 'root' => $rootPath + 'storage' => $rootStorage, + 'root' => $rootPath, ]); $quotaStorage = new GroupFolderStorage([ 'storage' => $baseStorage, 'quota' => $quota, 'folder_id' => $id, - 'rootCacheEntry' => $cacheEntry, + 'rootCacheEntry' => $rootCacheEntry, 'userSession' => $this->userSession, 'mountOwner' => $user, ]); } else { $baseStorage = new Jail([ - 'storage' => $storage, - 'root' => $rootPath + 'storage' => $rootStorage, + 'root' => $rootPath, ]); $quotaStorage = new GroupFolderNoEncryptionStorage([ 'storage' => $baseStorage, 'quota' => $quota, 'folder_id' => $id, - 'rootCacheEntry' => $cacheEntry, + 'rootCacheEntry' => $rootCacheEntry, 'userSession' => $this->userSession, 'mountOwner' => $user, ]); } - $maskedStore = new PermissionsMask([ - 'storage' => $quotaStorage, - 'mask' => $permissions - ]); - - if (!$this->allowRootShare) { - $maskedStore = new RootPermissionsMask([ - 'storage' => $maskedStore, - 'mask' => Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE, - ]); - } - return new GroupMountPoint( - $id, - $maskedStore, - $mountPoint, - null, - $loader - ); + return $quotaStorage; } public function getJailPath(int $folderId): string { diff --git a/lib/Trash/GroupTrashItem.php b/lib/Trash/GroupTrashItem.php index 285fc5a42..beeeff0cd 100644 --- a/lib/Trash/GroupTrashItem.php +++ b/lib/Trash/GroupTrashItem.php @@ -59,18 +59,7 @@ public function getTitle(): string { return $this->getGroupFolderMountPoint() . '/' . $this->getOriginalLocation(); } - public function getStorage() { - // get the unjailed storage, since the trash item is outside the jail - // (the internal path is also unjailed) - $groupFolderStorage = parent::getStorage(); - if ($groupFolderStorage->instanceOfStorage(Jail::class)) { - /** @var Jail $groupFolderStorage */ - return $groupFolderStorage->getUnjailedStorage(); - } - return $groupFolderStorage; - } - - public function getMtime() { + public function getMtime(): int { // trashbin is currently (incorrectly) assuming these to be the same return $this->getDeletedTime(); } diff --git a/lib/Trash/TrashBackend.php b/lib/Trash/TrashBackend.php index d622bbd73..8d4e384f0 100644 --- a/lib/Trash/TrashBackend.php +++ b/lib/Trash/TrashBackend.php @@ -89,14 +89,16 @@ public function listTrashFolder(ITrashItem $trashItem): array { return []; } $user = $trashItem->getUser(); - $folder = $this->getNodeForTrashItem($user, $trashItem); - if (!$folder instanceof Folder) { + $folderNode = $this->getNodeForTrashItem($user, $trashItem); + if (!$folderNode instanceof Folder) { return []; } - $content = $folder->getDirectoryListing(); + + $content = $folderNode->getDirectoryListing(); $this->aclManagerFactory->getACLManager($user)->preloadRulesForFolder($trashItem->getPath()); - return array_values(array_filter(array_map(function (Node $node) use ($trashItem, $user) { - if (!$this->userHasAccessToPath($user, $trashItem->getPath() . '/' . $node->getName())) { + + return array_values(array_filter(array_map(function (Node $node) use ($trashItem, $user): ?GroupTrashItem { + if (!$this->userHasAccessToPath($user, $this->getUnJailedPath($node))) { return null; } return new GroupTrashItem( @@ -304,10 +306,11 @@ private function userHasAccessToPath( private function getNodeForTrashItem(IUser $user, ITrashItem $trashItem): ?Node { [, $folderId, $path] = explode('/', $trashItem->getTrashPath(), 3); + $folderId = (int)$folderId; $folders = $this->folderManager->getFoldersForUser($user); foreach ($folders as $groupFolder) { - if ($groupFolder['folder_id'] === (int)$folderId) { - $trashRoot = $this->getTrashFolder((int)$folderId); + if ($groupFolder['folder_id'] === $folderId) { + $trashRoot = $this->rootFolder->get('/' . $user->getUID() . '/files_trashbin/groupfolders/' . $folderId); try { $node = $trashRoot->get($path); if (!$this->userHasAccessToPath($user, $trashItem->getPath())) { @@ -341,6 +344,17 @@ private function getTrashFolder(int $folderId): Folder { } } + private function getUnJailedPath(Node $node): string { + $storage = $node->getStorage(); + $path = $node->getInternalPath(); + while ($storage->instanceOfStorage(Jail::class)) { + /** @var Jail $storage */ + $path = $storage->getUnjailedPath($path); + $storage = $storage->getUnjailedStorage(); + } + return $path; + } + /** * @return list */ @@ -358,16 +372,20 @@ private function getTrashForFolders(IUser $user, array $folders): array { foreach ($folders as $folder) { $folderId = $folder['folder_id']; $mountPoint = $folder['mount_point']; - $trashFolder = $this->getTrashFolder($folderId); + + // ensure the trash folder exists + $this->getTrashFolder($folderId); + + $trashFolder = $this->rootFolder->get('/' . $user->getUID() . '/files_trashbin/groupfolders/' . $folderId); $content = $trashFolder->getDirectoryListing(); - $this->aclManagerFactory->getACLManager($user)->preloadRulesForFolder($trashFolder->getPath()); + $this->aclManagerFactory->getACLManager($user)->preloadRulesForFolder($this->getUnJailedPath($trashFolder)); foreach ($content as $item) { /** @var \OC\Files\Node\Node $item */ $pathParts = pathinfo($item->getName()); $timestamp = (int)substr($pathParts['extension'], 1); $name = $pathParts['filename']; $key = $folderId . '/' . $name . '/' . $timestamp; - if (!$this->userHasAccessToPath($user, $item->getPath())) { + if (!$this->userHasAccessToPath($user, $this->getUnJailedPath($item))) { continue; } $info = $item->getFileInfo(); From 8e116252249fa9ac6759903a979aa48aff02f14e Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 18 Nov 2024 14:44:07 +0100 Subject: [PATCH 6/9] fix: fix moving files to trash Signed-off-by: Robin Appelman --- lib/Trash/TrashBackend.php | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/Trash/TrashBackend.php b/lib/Trash/TrashBackend.php index 8d4e384f0..96661e9ff 100644 --- a/lib/Trash/TrashBackend.php +++ b/lib/Trash/TrashBackend.php @@ -23,6 +23,7 @@ use OC\Files\Storage\Wrapper\Encryption; use OCA\Files_Trashbin\Expiration; +use OCA\Files_Trashbin\Storage; use OCA\Files_Trashbin\Trash\ITrashBackend; use OCA\Files_Trashbin\Trash\ITrashItem; use OCA\GroupFolders\ACL\ACLManagerFactory; @@ -229,7 +230,14 @@ public function moveToTrash(IStorage $storage, string $internalPath): bool { $name = basename($internalPath); $fileEntry = $storage->getCache()->get($internalPath); $folderId = $storage->getFolderId(); - $trashFolder = $this->getTrashFolder($folderId); + $user = $this->userSession->getUser(); + if (!$user) { + throw new \Exception("file moved to trash with no user in context"); + } + // ensure the folder exists + $this->getTrashFolder($folderId); + + $trashFolder = $this->rootFolder->get('/' . $user->getUID() . '/files_trashbin/groupfolders/' . $folderId); $trashStorage = $trashFolder->getStorage(); $time = time(); $trashName = $name . '.d' . $time; @@ -242,8 +250,15 @@ public function moveToTrash(IStorage $storage, string $internalPath): bool { } if ($result) { $this->trashManager->addTrashItem($folderId, $name, $time, $internalPath, $fileEntry->getId()); - if ($trashStorage->getCache()->getId($targetInternalPath) !== $fileEntry->getId()) { + + // some storage backends (object/encryption) can either already move the cache item or cause the target to be scanned + // so we only conditionally do the cache move here + if (!$trashStorage->getCache()->inCache($targetInternalPath)) { + // doesn't exist in target yet, do the move $trashStorage->getCache()->moveFromCache($storage->getCache(), $internalPath, $targetInternalPath); + } elseif ($storage->getCache()->inCache($internalPath)) { + // exists in both source and target, cleanup source + $storage->getCache()->remove($internalPath); } } else { throw new \Exception("Failed to move groupfolder item to trash"); @@ -264,6 +279,11 @@ private function moveFromEncryptedStorage(IStorage $sourceStorage, IStorage $tar return false; } + // the trash should be the top wrapper, remove it to prevent recursive attempts to move to trash + if ($sourceStorage instanceof Storage) { + $sourceStorage = $sourceStorage->getWrapperStorage(); + } + $result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, true); if ($result) { if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { From c2c264b09dd0ebc2a8aaceadab7898f45fbac8a9 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 27 Nov 2024 18:59:39 +0100 Subject: [PATCH 7/9] fix: add fallback behavior for pre-fix trashbin restore Signed-off-by: Robin Appelman --- lib/Trash/TrashBackend.php | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/Trash/TrashBackend.php b/lib/Trash/TrashBackend.php index 96661e9ff..328663cce 100644 --- a/lib/Trash/TrashBackend.php +++ b/lib/Trash/TrashBackend.php @@ -21,7 +21,9 @@ namespace OCA\GroupFolders\Trash; +use OC\Encryption\Exceptions\DecryptionFailedException; use OC\Files\Storage\Wrapper\Encryption; +use OC\Files\Storage\Wrapper\Jail; use OCA\Files_Trashbin\Expiration; use OCA\Files_Trashbin\Storage; use OCA\Files_Trashbin\Trash\ITrashBackend; @@ -176,8 +178,19 @@ public function restoreItem(ITrashItem $item) { } $targetLocation = $targetFolder->getInternalPath() . '/' . $originalLocation; - $targetFolder->getStorage()->moveFromStorage($trashStorage, $node->getInternalPath(), $targetLocation); - $targetFolder->getStorage()->getUpdater()->renameFromStorage($trashStorage, $node->getInternalPath(), $targetLocation); + $targetStorage = $targetFolder->getStorage(); + $trashLocation = $node->getInternalPath(); + try { + $targetStorage->moveFromStorage($trashStorage, $trashLocation, $targetLocation); + $targetStorage->getUpdater()->renameFromStorage($trashStorage, $trashLocation, $targetLocation); + } catch (DecryptionFailedException $e) { + // Before https://github.com/nextcloud/groupfolders/pull/3425 the key would be in the wrong place, leading to the decryption failure. + // for those we fall back to the old restore behavior + [$unwrappedTargetStorage, $unwrappedTargetLocation] = $this->unwrapJails($targetStorage, $targetLocation); + [$unwrappedTrashStorage, $unwrappedTrashLocation] = $this->unwrapJails($trashStorage, $trashLocation); + $unwrappedTargetStorage->moveFromStorage($unwrappedTrashStorage, $unwrappedTrashLocation, $unwrappedTargetLocation); + $unwrappedTargetStorage->getUpdater()->renameFromStorage($unwrappedTrashStorage, $unwrappedTrashLocation, $unwrappedTargetLocation); + } $this->trashManager->removeItem((int)$folderId, $item->getName(), $item->getDeletedTime()); \OCP\Util::emitHook( '\OCA\Files_Trashbin\Trashbin', @@ -189,6 +202,18 @@ public function restoreItem(ITrashItem $item) { ); } + private function unwrapJails(IStorage $storage, string $internalPath): array { + $unJailedInternalPath = $internalPath; + $unJailedStorage = $storage; + while ($unJailedStorage->instanceOfStorage(Jail::class)) { + $unJailedStorage = $unJailedStorage->getWrapperStorage(); + if ($unJailedStorage instanceof Jail) { + $unJailedInternalPath = $unJailedStorage->getUnjailedPath($unJailedInternalPath); + } + } + return [$unJailedStorage, $unJailedInternalPath]; + } + /** * @return void * @throw \LogicException From 4d0e5a1f8462f1c15c995d864352e3de3210eeb3 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 11 Dec 2024 18:08:51 +0100 Subject: [PATCH 8/9] chore: formatting Signed-off-by: Robin Appelman --- lib/Mount/GroupFolderStorage.php | 3 ++- lib/Trash/GroupTrashItem.php | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Mount/GroupFolderStorage.php b/lib/Mount/GroupFolderStorage.php index ca5fcd440..26518772f 100644 --- a/lib/Mount/GroupFolderStorage.php +++ b/lib/Mount/GroupFolderStorage.php @@ -70,7 +70,8 @@ public function getCache($path = '', $storage = null) { $cache = parent::getCache($path, $storage); if ($this->rootEntry !== null) { - $cache = new RootEntryCache($cache, $this->rootEntry);} + $cache = new RootEntryCache($cache, $this->rootEntry); + } $this->cache = $cache; return $this->cache; diff --git a/lib/Trash/GroupTrashItem.php b/lib/Trash/GroupTrashItem.php index beeeff0cd..a62e0bce8 100644 --- a/lib/Trash/GroupTrashItem.php +++ b/lib/Trash/GroupTrashItem.php @@ -21,7 +21,6 @@ namespace OCA\GroupFolders\Trash; -use OC\Files\Storage\Wrapper\Jail; use OCA\Files_Trashbin\Trash\ITrashBackend; use OCA\Files_Trashbin\Trash\TrashItem; use OCP\Files\FileInfo; From 1366597df1fb0dec49a1081e297666d10db66d03 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 28 Nov 2024 15:05:06 +0100 Subject: [PATCH 9/9] chore: psalm fixes Signed-off-by: Robin Appelman --- lib/AppInfo/Application.php | 3 +- lib/Mount/GroupFolderStorage.php | 5 +- lib/Mount/MountProvider.php | 2 +- lib/Trash/TrashBackend.php | 16 +- tests/stub.phpstub | 798 +++++++++++++++++++++++++++---- 5 files changed, 716 insertions(+), 108 deletions(-) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index a27ab8fd8..742ba1a07 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -142,7 +142,8 @@ public function register(IRegistrationContext $context): void { $c->get(MountProvider::class), $c->get(ACLManagerFactory::class), $c->get(IRootFolder::class), - $c->get(LoggerInterface::class) + $c->get(LoggerInterface::class), + $c->get(IUserSession::class), ); $hasVersionApp = interface_exists(\OCA\Files_Versions\Versions\IVersionBackend::class); if ($hasVersionApp) { diff --git a/lib/Mount/GroupFolderStorage.php b/lib/Mount/GroupFolderStorage.php index 26518772f..8a650ffc5 100644 --- a/lib/Mount/GroupFolderStorage.php +++ b/lib/Mount/GroupFolderStorage.php @@ -25,6 +25,7 @@ use OC\Files\ObjectStore\ObjectStoreScanner; use OC\Files\ObjectStore\ObjectStoreStorage; use OC\Files\Storage\Wrapper\Quota; +use OCP\Files\Cache\ICache; use OCP\Files\Cache\ICacheEntry; use OCP\IUser; use OCP\IUserSession; @@ -33,8 +34,8 @@ class GroupFolderStorage extends Quota { private int $folderId; private ?ICacheEntry $rootEntry; private IUserSession $userSession; - private ?IUser $mountOwner = null; - /** @var RootEntryCache|null */ + private ?IUser $mountOwner; + /** @var ICache|null */ public $cache = null; public function __construct($parameters) { diff --git a/lib/Mount/MountProvider.php b/lib/Mount/MountProvider.php index 8a0fb4625..e60df4943 100644 --- a/lib/Mount/MountProvider.php +++ b/lib/Mount/MountProvider.php @@ -279,7 +279,7 @@ public function getTrashMount( $storage = $this->getRootFolder()->getStorage(); - $storage->setOwner($user?->getUID()); + $storage->setOwner($user->getUID()); $trashPath = $this->getRootFolder()->getInternalPath() . '/trash/' . $id; diff --git a/lib/Trash/TrashBackend.php b/lib/Trash/TrashBackend.php index 328663cce..2d3425453 100644 --- a/lib/Trash/TrashBackend.php +++ b/lib/Trash/TrashBackend.php @@ -22,6 +22,7 @@ namespace OCA\GroupFolders\Trash; use OC\Encryption\Exceptions\DecryptionFailedException; +use OC\Files\ObjectStore\ObjectStoreStorage; use OC\Files\Storage\Wrapper\Encryption; use OC\Files\Storage\Wrapper\Jail; use OCA\Files_Trashbin\Expiration; @@ -41,6 +42,7 @@ use OCP\Files\NotPermittedException; use OCP\Files\Storage\IStorage; use OCP\IUser; +use OCP\IUserSession; use Psr\Log\LoggerInterface; class TrashBackend implements ITrashBackend { @@ -53,6 +55,7 @@ class TrashBackend implements ITrashBackend { private $versionsBackend = null; private IRootFolder $rootFolder; private LoggerInterface $logger; + private IUserSession $userSession; public function __construct( FolderManager $folderManager, @@ -61,7 +64,8 @@ public function __construct( MountProvider $mountProvider, ACLManagerFactory $aclManagerFactory, IRootFolder $rootFolder, - LoggerInterface $logger + LoggerInterface $logger, + IUserSession $userSession, ) { $this->folderManager = $folderManager; $this->trashManager = $trashManager; @@ -70,6 +74,7 @@ public function __construct( $this->aclManagerFactory = $aclManagerFactory; $this->rootFolder = $rootFolder; $this->logger = $logger; + $this->userSession = $userSession; } public function setVersionsBackend(VersionsBackend $versionsBackend): void { @@ -309,9 +314,11 @@ private function moveFromEncryptedStorage(IStorage $sourceStorage, IStorage $tar $sourceStorage = $sourceStorage->getWrapperStorage(); } + /** @psalm-suppress TooManyArguments */ $result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, true); if ($result) { - if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { + // hacky workaround to make sure we don't rely on a newer minor version + if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class) && is_callable([$sourceStorage, 'setPreserveCacheOnDelete'])) { /** @var ObjectStoreStorage $sourceStorage */ $sourceStorage->setPreserveCacheOnDelete(true); } @@ -322,7 +329,7 @@ private function moveFromEncryptedStorage(IStorage $sourceStorage, IStorage $tar $result = $sourceStorage->unlink($sourceInternalPath); } } finally { - if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { + if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class) && is_callable([$sourceStorage, 'setPreserveCacheOnDelete'])) { /** @var ObjectStoreStorage $sourceStorage */ $sourceStorage->setPreserveCacheOnDelete(false); } @@ -355,6 +362,7 @@ private function getNodeForTrashItem(IUser $user, ITrashItem $trashItem): ?Node $folders = $this->folderManager->getFoldersForUser($user); foreach ($folders as $groupFolder) { if ($groupFolder['folder_id'] === $folderId) { + /** @var Folder $trashRoot */ $trashRoot = $this->rootFolder->get('/' . $user->getUID() . '/files_trashbin/groupfolders/' . $folderId); try { $node = $trashRoot->get($path); @@ -421,6 +429,8 @@ private function getTrashForFolders(IUser $user, array $folders): array { // ensure the trash folder exists $this->getTrashFolder($folderId); + + /** @var Folder $trashFolder */ $trashFolder = $this->rootFolder->get('/' . $user->getUID() . '/files_trashbin/groupfolders/' . $folderId); $content = $trashFolder->getDirectoryListing(); $this->aclManagerFactory->getACLManager($user)->preloadRulesForFolder($this->getUnJailedPath($trashFolder)); diff --git a/tests/stub.phpstub b/tests/stub.phpstub index 4b7c5644d..eb5a07b6c 100644 --- a/tests/stub.phpstub +++ b/tests/stub.phpstub @@ -279,6 +279,9 @@ namespace OCA\Files_Trashbin { */ public function isExpired($timestamp, $quotaExceeded = false) {} } + + class Storage extends \OC\Files\Storage\Wrapper\Wrapper { + } } @@ -1500,7 +1503,7 @@ namespace OC\Files\Storage\Wrapper{ * @param string $targetInternalPath * @return bool */ - public function copyFromStorage(\OCP\Files\Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) + public function copyFromStorage(\OCP\Files\Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) { } /** @@ -1561,109 +1564,702 @@ namespace OC\Files\Storage\Wrapper{ class Jail extends Wrapper { protected $rootPath; - public function getUnjailedPath(string $path): string {} - public function getUnjailedStorage(): IStorage {} - } - - class Quota extends Wrapper { - public function getQuota() {} - } - - class PermissionsMask extends Wrapper { - public function getQuota() {} - } -} - -namespace OC\Files\ObjectStore { - use OC\Files\Storage\Wrapper\Wrapper; - class ObjectStoreStorage extends Wrapper {} -} - -namespace OCA\Circles { - use OCA\Circles\Model\Circle; - use OCA\Circles\Model\FederatedUser; - use OCA\Circles\Model\Probes\CircleProbe; - use OCA\Circles\Model\Probes\DataProbe; - use OCP\DB\QueryBuilder\ICompositeExpression; - use OCP\DB\QueryBuilder\IQueryBuilder; - - interface IFederatedUser { - - } - - class CirclesManager { - public function startSession(?FederatedUser $federatedUser = null): void {} - public function probeCircles(?CircleProbe $circleProbe = null, ?DataProbe $dataProbe = null): array {} - public function getQueryHelper(): CirclesQueryHelper {} - public function getLocalFederatedUser(string $userId): FederatedUser {} - public function startSuperSession(): void {} - public function stopSession(): void {} - public function getCircle(string $singleId, ?CircleProbe $probe = null): Circle {} - } - - class CirclesQueryHelper{ - public function addCircleDetails(string $alias, string $field): void {} - public function getQueryBuilder(): IQueryBuilder {} - public function extractCircle(array $data): Circle {} - public function limitToInheritedMembers(string $alias, string $field, IFederatedUser $federatedUser, bool $fullDetails = false): ICompositeExpression {} - } -} - -namespace OCA\Circles\Exceptions { - class CircleNotFoundException extends \Exception { - } -} - -namespace OCA\Circles\Model { - use OCA\Circles\IFederatedUser; - - class FederatedUser implements IFederatedUser { - } - class CircleProbe { - } - class DataProbe { - } - class Circle { - public function getDisplayName(): string {} - public function getSingleId(): string {} - } -} - -namespace OCA\Circles\Model\Probes { - class CircleProbe { - public function includeSystemCircles(bool $include = true): self {} - public function includeSingleCircles(bool $include = true): self {} - } -} - -namespace OCA\Circles\Events { - use OCA\Circles\Model\Circle; - - class CircleDestroyedEvent extends \OCP\EventDispatcher\Event { - public function getCircle(): Circle {} - } -} - - -namespace OCA\DAV\Connector\Sabre\Exception { - class Forbidden extends \Sabre\DAV\Exception\Forbidden { - public const NS_OWNCLOUD = 'http://owncloud.org/ns'; - /** - * @param string $message - * @param bool $retry - * @param \Exception $previous + * @param array $arguments ['storage' => $storage, 'root' => $root] + * + * $storage: The storage that will be wrapper + * $root: The folder in the wrapped storage that will become the root folder of the wrapped storage */ - public function __construct($message, $retry = false, \Exception $previous = null) {} - + public function __construct($arguments) + { + } + public function getUnjailedPath($path) + { + } /** - * This method allows the exception to include additional information - * into the WebDAV error response + * This is separate from Wrapper::getWrapperStorage so we can get the jailed storage consistently even if the jail is inside another wrapper + */ + public function getUnjailedStorage() + { + } + public function getJailedPath($path) + { + } + public function getId() + { + } + /** + * see https://www.php.net/manual/en/function.mkdir.php * - * @param \Sabre\DAV\Server $server - * @param \DOMElement $errorNode - * @return void + * @param string $path + * @return bool */ - public function serialize(\Sabre\DAV\Server $server, \DOMElement $errorNode) {} - } + public function mkdir($path) + { + } + /** + * see https://www.php.net/manual/en/function.rmdir.php + * + * @param string $path + * @return bool + */ + public function rmdir($path) + { + } + /** + * see https://www.php.net/manual/en/function.opendir.php + * + * @param string $path + * @return resource|false + */ + public function opendir($path) + { + } + /** + * see https://www.php.net/manual/en/function.is_dir.php + * + * @param string $path + * @return bool + */ + public function is_dir($path) + { + } + /** + * see https://www.php.net/manual/en/function.is_file.php + * + * @param string $path + * @return bool + */ + public function is_file($path) + { + } + /** + * see https://www.php.net/manual/en/function.stat.php + * only the following keys are required in the result: size and mtime + * + * @param string $path + * @return array|bool + */ + public function stat($path) + { + } + /** + * see https://www.php.net/manual/en/function.filetype.php + * + * @param string $path + * @return bool + */ + public function filetype($path) + { + } + /** + * see https://www.php.net/manual/en/function.filesize.php + * The result for filesize when called on a folder is required to be 0 + */ + public function filesize($path) : false|int|float + { + } + /** + * check if a file can be created in $path + * + * @param string $path + * @return bool + */ + public function isCreatable($path) + { + } + /** + * check if a file can be read + * + * @param string $path + * @return bool + */ + public function isReadable($path) + { + } + /** + * check if a file can be written to + * + * @param string $path + * @return bool + */ + public function isUpdatable($path) + { + } + /** + * check if a file can be deleted + * + * @param string $path + * @return bool + */ + public function isDeletable($path) + { + } + /** + * check if a file can be shared + * + * @param string $path + * @return bool + */ + public function isSharable($path) + { + } + /** + * get the full permissions of a path. + * Should return a combination of the PERMISSION_ constants defined in lib/public/constants.php + * + * @param string $path + * @return int + */ + public function getPermissions($path) + { + } + /** + * see https://www.php.net/manual/en/function.file_exists.php + * + * @param string $path + * @return bool + */ + public function file_exists($path) + { + } + /** + * see https://www.php.net/manual/en/function.filemtime.php + * + * @param string $path + * @return int|bool + */ + public function filemtime($path) + { + } + /** + * see https://www.php.net/manual/en/function.file_get_contents.php + * + * @param string $path + * @return string|false + */ + public function file_get_contents($path) + { + } + /** + * see https://www.php.net/manual/en/function.file_put_contents.php + * + * @param string $path + * @param mixed $data + * @return int|float|false + */ + public function file_put_contents($path, $data) + { + } + /** + * see https://www.php.net/manual/en/function.unlink.php + * + * @param string $path + * @return bool + */ + public function unlink($path) + { + } + /** + * see https://www.php.net/manual/en/function.rename.php + * + * @param string $source + * @param string $target + * @return bool + */ + public function rename($source, $target) + { + } + /** + * see https://www.php.net/manual/en/function.copy.php + * + * @param string $source + * @param string $target + * @return bool + */ + public function copy($source, $target) + { + } + /** + * see https://www.php.net/manual/en/function.fopen.php + * + * @param string $path + * @param string $mode + * @return resource|bool + */ + public function fopen($path, $mode) + { + } + /** + * get the mimetype for a file or folder + * The mimetype for a folder is required to be "httpd/unix-directory" + * + * @param string $path + * @return string|bool + */ + public function getMimeType($path) + { + } + /** + * see https://www.php.net/manual/en/function.hash.php + * + * @param string $type + * @param string $path + * @param bool $raw + * @return string|bool + */ + public function hash($type, $path, $raw = false) + { + } + /** + * see https://www.php.net/manual/en/function.free_space.php + * + * @param string $path + * @return int|float|bool + */ + public function free_space($path) + { + } + /** + * search for occurrences of $query in file names + * + * @param string $query + * @return array|bool + */ + public function search($query) + { + } + /** + * see https://www.php.net/manual/en/function.touch.php + * If the backend does not support the operation, false should be returned + * + * @param string $path + * @param int $mtime + * @return bool + */ + public function touch($path, $mtime = null) + { + } + /** + * get the path to a local version of the file. + * The local version of the file can be temporary and doesn't have to be persistent across requests + * + * @param string $path + * @return string|false + */ + public function getLocalFile($path) + { + } + /** + * check if a file or folder has been updated since $time + * + * @param string $path + * @param int $time + * @return bool + * + * hasUpdated for folders should return at least true if a file inside the folder is add, removed or renamed. + * returning true for other changes in the folder is optional + */ + public function hasUpdated($path, $time) + { + } + /** + * get a cache instance for the storage + * + * @param string $path + * @param \OC\Files\Storage\Storage|null (optional) the storage to pass to the cache + * @return \OC\Files\Cache\Cache + */ + public function getCache($path = '', $storage = null) + { + } + /** + * get the user id of the owner of a file or folder + * + * @param string $path + * @return string + */ + public function getOwner($path) + { + } + /** + * get a watcher instance for the cache + * + * @param string $path + * @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher + * @return \OC\Files\Cache\Watcher + */ + public function getWatcher($path = '', $storage = null) + { + } + /** + * get the ETag for a file or folder + * + * @param string $path + * @return string|false + */ + public function getETag($path) + { + } + public function getMetaData($path) + { + } + /** + * @param string $path + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + * @throws \OCP\Lock\LockedException + */ + public function acquireLock($path, $type, \OCP\Lock\ILockingProvider $provider) + { + } + /** + * @param string $path + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + */ + public function releaseLock($path, $type, \OCP\Lock\ILockingProvider $provider) + { + } + /** + * @param string $path + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + */ + public function changeLock($path, $type, \OCP\Lock\ILockingProvider $provider) + { + } + /** + * Resolve the path for the source of the share + * + * @param string $path + * @return array + */ + public function resolvePath($path) + { + } + /** + * @param IStorage $sourceStorage + * @param string $sourceInternalPath + * @param string $targetInternalPath + * @return bool + */ + public function copyFromStorage(\OCP\Files\Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) + { + } + /** + * @param IStorage $sourceStorage + * @param string $sourceInternalPath + * @param string $targetInternalPath + * @return bool + */ + public function moveFromStorage(\OCP\Files\Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) + { + } + public function getPropagator($storage = null) + { + } + public function writeStream(string $path, $stream, ?int $size = null) : int + { + } + public function getDirectoryContent($directory) : \Traversable + { + } + } + class Quota extends \OC\Files\Storage\Wrapper\Wrapper + { + /** @var callable|null */ + protected $quotaCallback; + /** @var int|float|null int on 64bits, float on 32bits for bigint */ + protected int|float|null $quota; + protected string $sizeRoot; + private \OC\SystemConfig $config; + private bool $quotaIncludeExternalStorage; + /** + * @param array $parameters + */ + public function __construct($parameters) + { + } + /** + * @return int|float quota value + */ + public function getQuota() : int|float + { + } + private function hasQuota() : bool + { + } + /** + * @param string $path + * @param IStorage $storage + * @return int|float + */ + protected function getSize($path, $storage = null) + { + } + /** + * Get free space as limited by the quota + * + * @param string $path + * @return int|float|bool + */ + public function free_space($path) + { + } + /** + * see https://www.php.net/manual/en/function.file_put_contents.php + * + * @param string $path + * @param mixed $data + * @return int|float|false + */ + public function file_put_contents($path, $data) + { + } + /** + * see https://www.php.net/manual/en/function.copy.php + * + * @param string $source + * @param string $target + * @return bool + */ + public function copy($source, $target) + { + } + /** + * see https://www.php.net/manual/en/function.fopen.php + * + * @param string $path + * @param string $mode + * @return resource|bool + */ + public function fopen($path, $mode) + { + } + /** + * Checks whether the given path is a part file + * + * @param string $path Path that may identify a .part file + * @return bool + * @note this is needed for reusing keys + */ + private function isPartFile($path) + { + } + /** + * Only apply quota for files, not metadata, trash or others + */ + private function shouldApplyQuota(string $path) : bool + { + } + /** + * @param IStorage $sourceStorage + * @param string $sourceInternalPath + * @param string $targetInternalPath + * @return bool + */ + public function copyFromStorage(\OCP\Files\Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) + { + } + /** + * @param IStorage $sourceStorage + * @param string $sourceInternalPath + * @param string $targetInternalPath + * @return bool + */ + public function moveFromStorage(\OCP\Files\Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) + { + } + public function mkdir($path) + { + } + public function touch($path, $mtime = null) + { + } + } + /** + * Mask the permissions of a storage + * + * This can be used to restrict update, create, delete and/or share permissions of a storage + * + * Note that the read permissions can't be masked + */ + class PermissionsMask extends \OC\Files\Storage\Wrapper\Wrapper + { + /** + * @var int the permissions bits we want to keep + */ + private $mask; + /** + * @param array $arguments ['storage' => $storage, 'mask' => $mask] + * + * $storage: The storage the permissions mask should be applied on + * $mask: The permission bits that should be kept, a combination of the \OCP\Constant::PERMISSION_ constants + */ + public function __construct($arguments) + { + } + private function checkMask($permissions) + { + } + public function isUpdatable($path) + { + } + public function isCreatable($path) + { + } + public function isDeletable($path) + { + } + public function isSharable($path) + { + } + public function getPermissions($path) + { + } + public function rename($source, $target) + { + } + public function copy($source, $target) + { + } + public function touch($path, $mtime = null) + { + } + public function mkdir($path) + { + } + public function rmdir($path) + { + } + public function unlink($path) + { + } + public function file_put_contents($path, $data) + { + } + public function fopen($path, $mode) + { + } + /** + * get a cache instance for the storage + * + * @param string $path + * @param \OC\Files\Storage\Storage (optional) the storage to pass to the cache + * @return \OC\Files\Cache\Cache + */ + public function getCache($path = '', $storage = null) + { + } + public function getMetaData($path) + { + } + public function getScanner($path = '', $storage = null) + { + } + public function getDirectoryContent($directory) : \Traversable + { + } + } + class Encryption extends \OC\Files\Storage\Wrapper\Wrapper { + } +} + +namespace OC\Files\ObjectStore { + use OC\Files\Storage\Wrapper\Wrapper; + class ObjectStoreStorage extends Wrapper {} +} + +namespace OCA\Circles { + use OCA\Circles\Model\Circle; + use OCA\Circles\Model\FederatedUser; + use OCA\Circles\Model\Probes\CircleProbe; + use OCA\Circles\Model\Probes\DataProbe; + use OCP\DB\QueryBuilder\ICompositeExpression; + use OCP\DB\QueryBuilder\IQueryBuilder; + + interface IFederatedUser { + + } + + class CirclesManager { + public function startSession(?FederatedUser $federatedUser = null): void {} + public function probeCircles(?CircleProbe $circleProbe = null, ?DataProbe $dataProbe = null): array {} + public function getQueryHelper(): CirclesQueryHelper {} + public function getLocalFederatedUser(string $userId): FederatedUser {} + public function startSuperSession(): void {} + public function stopSession(): void {} + public function getCircle(string $singleId, ?CircleProbe $probe = null): Circle {} + } + + class CirclesQueryHelper{ + public function addCircleDetails(string $alias, string $field): void {} + public function getQueryBuilder(): IQueryBuilder {} + public function extractCircle(array $data): Circle {} + public function limitToInheritedMembers(string $alias, string $field, IFederatedUser $federatedUser, bool $fullDetails = false): ICompositeExpression {} + } +} + +namespace OCA\Circles\Exceptions { + class CircleNotFoundException extends \Exception { + } +} + +namespace OCA\Circles\Model { + use OCA\Circles\IFederatedUser; + + class FederatedUser implements IFederatedUser { + } + class CircleProbe { + } + class DataProbe { + } + class Circle { + public function getDisplayName(): string {} + public function getSingleId(): string {} + } +} + +namespace OCA\Circles\Model\Probes { + class CircleProbe { + public function includeSystemCircles(bool $include = true): self {} + public function includeSingleCircles(bool $include = true): self {} + } +} + +namespace OCA\Circles\Events { + use OCA\Circles\Model\Circle; + + class CircleDestroyedEvent extends \OCP\EventDispatcher\Event { + public function getCircle(): Circle {} + } +} + + +namespace OCA\DAV\Connector\Sabre\Exception { + class Forbidden extends \Sabre\DAV\Exception\Forbidden { + public const NS_OWNCLOUD = 'http://owncloud.org/ns'; + + /** + * @param string $message + * @param bool $retry + * @param \Exception $previous + */ + public function __construct($message, $retry = false, \Exception $previous = null) {} + + /** + * This method allows the exception to include additional information + * into the WebDAV error response + * + * @param \Sabre\DAV\Server $server + * @param \DOMElement $errorNode + * @return void + */ + public function serialize(\Sabre\DAV\Server $server, \DOMElement $errorNode) {} + } +} + +namespace OC\Encryption\Exceptions { + class DecryptionFailedException extends \Exception {} }