diff --git a/lib/Connector/Sabre/APlugin.php b/lib/Connector/Sabre/APlugin.php index 7bec7cf..a77008e 100644 --- a/lib/Connector/Sabre/APlugin.php +++ b/lib/Connector/Sabre/APlugin.php @@ -26,6 +26,7 @@ use OCP\AppFramework\Http; use OCA\DAV\Connector\Sabre\Directory; use OCA\DAV\Connector\Sabre\Exception\Forbidden; use OCA\DAV\Connector\Sabre\File; +use OCA\EndToEndEncryption\E2EEnabledPathCache; use OCP\Files\FileInfo; use OCP\Files\IRootFolder; use OCP\Files\Node; @@ -41,6 +42,7 @@ abstract class APlugin extends ServerPlugin { protected ?Server $server = null; protected IRootFolder $rootFolder; protected IUserSession $userSession; + protected E2EEnabledPathCache $pathCache; /** * Should plugin be applied to the current node? @@ -50,14 +52,15 @@ abstract class APlugin extends ServerPlugin { /** * APlugin constructor. - * - * @param IRootFolder $rootFolder - * @param IUserSession $userSession */ - public function __construct(IRootFolder $rootFolder, - IUserSession $userSession) { + public function __construct( + IRootFolder $rootFolder, + IUserSession $userSession, + E2EEnabledPathCache $pathCache + ) { $this->rootFolder = $rootFolder; $this->userSession = $userSession; + $this->pathCache = $pathCache; } /** @@ -117,6 +120,18 @@ abstract class APlugin extends ServerPlugin { } } + /** + * Checks if the path is an E2E folder or inside an E2E folder + */ + protected function isE2EEnabledPath(string $path): bool { + try { + $node = $this->getFileNode($path); + } catch (NotFound $e) { + return false; + } + return $this->pathCache->isE2EEnabledPath($node, $path); + } + /** * Check if we process a file or directory. This plugin should ignore calendars * and contacts @@ -132,27 +147,4 @@ abstract class APlugin extends ServerPlugin { return $this->applyPlugin[$url]; } - /** - * Checks if the path is an E2E folder or inside an E2E folder - */ - protected function isE2EEnabledPath(string $path):bool { - try { - $node = $this->getFileNode($path); - } catch (NotFound $e) { - return false; - } - - while ($node->isEncrypted() === false || $node->getType() === FileInfo::TYPE_FILE) { - $node = $node->getParent(); - - // Nitpick: This doesn't check if root is E2E, - // but that's not supported at the moment anyway - if ($node->getPath() === '/') { - // top-level folder reached - return false; - } - } - - return true; - } } diff --git a/lib/Connector/Sabre/LockPlugin.php b/lib/Connector/Sabre/LockPlugin.php index 5c58bf0..a84f7f9 100644 --- a/lib/Connector/Sabre/LockPlugin.php +++ b/lib/Connector/Sabre/LockPlugin.php @@ -39,6 +39,7 @@ use Sabre\DAV\Exception\NotFound; use Sabre\DAV\INode; use Sabre\DAV\Server; use Sabre\HTTP\RequestInterface; +use OCA\EndToEndEncryption\E2EEnabledPathCache; class LockPlugin extends APlugin { private LockManager $lockManager; @@ -47,8 +48,9 @@ class LockPlugin extends APlugin { public function __construct(IRootFolder $rootFolder, IUserSession $userSession, LockManager $lockManager, - UserAgentManager $userAgentManager) { - parent::__construct($rootFolder, $userSession); + UserAgentManager $userAgentManager, + E2EEnabledPathCache $pathCache) { + parent::__construct($rootFolder, $userSession, $pathCache); $this->lockManager = $lockManager; $this->userAgentManager = $userAgentManager; } diff --git a/lib/Connector/Sabre/PropFindPlugin.php b/lib/Connector/Sabre/PropFindPlugin.php index a1c897e..ee1b81a 100644 --- a/lib/Connector/Sabre/PropFindPlugin.php +++ b/lib/Connector/Sabre/PropFindPlugin.php @@ -27,6 +27,7 @@ namespace OCA\EndToEndEncryption\Connector\Sabre; use OCA\DAV\Connector\Sabre\Directory; use OCA\DAV\Connector\Sabre\Exception\Forbidden; use OCA\EndToEndEncryption\UserAgentManager; +use OCA\EndToEndEncryption\E2EEnabledPathCache; use OCP\Files\IRootFolder; use OCP\IRequest; use OCP\IUserSession; @@ -43,8 +44,9 @@ class PropFindPlugin extends APlugin { public function __construct(IRootFolder $rootFolder, IUserSession $userSession, UserAgentManager $userAgentManager, - IRequest $request) { - parent::__construct($rootFolder, $userSession); + IRequest $request, + E2EEnabledPathCache $pathCache) { + parent::__construct($rootFolder, $userSession, $pathCache); $this->userAgentManager = $userAgentManager; $this->request = $request; } diff --git a/lib/Connector/Sabre/RedirectRequestPlugin.php b/lib/Connector/Sabre/RedirectRequestPlugin.php index e9e8d9c..ab3eb29 100644 --- a/lib/Connector/Sabre/RedirectRequestPlugin.php +++ b/lib/Connector/Sabre/RedirectRequestPlugin.php @@ -31,6 +31,7 @@ use Sabre\DAV\PropFind; use Sabre\DAV\Server; use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; +use OCA\EndToEndEncryption\E2EEnabledPathCache; /** * Class WritePlugin diff --git a/lib/E2EEnabledPathCache.php b/lib/E2EEnabledPathCache.php new file mode 100644 index 0000000..d75b1bd --- /dev/null +++ b/lib/E2EEnabledPathCache.php @@ -0,0 +1,102 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OCA\EndToEndEncryption; + +use Sabre\DAV\INode; +use OCP\Files\Node; +use OCP\Files\IHomeStorage; +use OCP\Files\Cache\ICache; +use OC\Files\Storage\Wrapper\Wrapper; +use OCA\Files_Sharing\SharedStorage; + +class E2EEnabledPathCache { + /** + * @psalm-type FileId=int + * + * @psalm-type EncryptedState=array{0: FileId, 1: bool} + * + * @psalm-type Path=string + * + * @psalm-type StorageId=string|int + */ + + /** @var array> */ + protected $cacheEntries; + + /** + * Checks if the path is an E2E folder or inside an E2E folder + * + * @param INode&Node $node + */ + public function isE2EEnabledPath($node, string $path): bool { + $storage = $node->getStorage(); + $cache = $storage->getCache(); + $encryptedStates = $this->getEncryptedStates($cache, $path, $storage, !$storage->instanceOfStorage(IHomeStorage::class) || $storage->instanceOfStorage(SharedStorage::class)); + foreach ($encryptedStates as [$fileid, $encryptedState]) { + if ($encryptedState) { + return true; + } + } + return false; + } + + /** + * Get the file ids of the given path and its parents + * + * @return array + */ + protected function getEncryptedStates(ICache $cache, string $path, $storage, bool $isExternalStorage): array { + /** @psalm-suppress InvalidArgument */ + if ($storage->instanceOfStorage(\OCA\GroupFolders\Mount\GroupFolderStorage::class)) { + // Special implementation for groupfolder since all groupfolders share the same storage + // id so add the group folder id in the cache key too. + $groupFolderStorage = $storage; + if ($this->storage instanceof Wrapper) { + $groupFolderStorage = $getInstanceOfStorage(\OCA\GroupFolders\Mount\GroupFolderStorage::class); + } + if ($groupFolderStorage === null) { + throw new \LogicException('Should not happen: Storage is instance of GroupFolderStorage but no group folder storage found while unwrapping.'); + } + /** + * @psalm-suppress UndefinedDocblockClass + * @psalm-suppress UndefinedInterfaceMethod + */ + $cacheId = $cache->getNumericStorageId() . '/' . $groupFolderStorage->getFolderId(); + } else { + $cacheId = $cache->getNumericStorageId(); + } + if (isset($this->cacheEntries[$cacheId][$path])) { + return $this->cacheEntries[$cacheId][$path]; + } + + $parentIds = []; + if ($path !== $this->dirname($path)) { + $cacheEntries = []; + $cacheEntry = $cache->get($path); + if ($cacheEntry !== false) { + $cacheEntries[] = [$cacheEntry->getId(), $cacheEntry->isEncrypted()]; + if ($cacheEntry->isEncrypted()) { + // no need to go further down in the tree + $this->cacheEntries[$cacheId][$path] = $parentEntries; + return $cacheEntry; + } + } + $cacheEntries = array_merge($this->getEncryptedStates($cache, $this->dirname($path), $storage, $isExternalStorage), $cacheEntries); + } elseif (!$isExternalStorage) { + return []; + } + + $this->cacheEntries[$cacheId][$path] = $cacheEntries; + return $cacheEntries; + } + + protected function dirname(string $path): string { + $dir = dirname($path); + return $dir === '.' ? '' : $dir; + } +} diff --git a/tests/Unit/Connector/Sabre/LockPluginTest.php b/tests/Unit/Connector/Sabre/LockPluginTest.php index 72e49da..0d3b79e 100644 --- a/tests/Unit/Connector/Sabre/LockPluginTest.php +++ b/tests/Unit/Connector/Sabre/LockPluginTest.php @@ -31,6 +31,7 @@ use OCA\DAV\Upload\FutureFile; use OCA\EndToEndEncryption\Connector\Sabre\LockPlugin; use OCA\EndToEndEncryption\LockManager; use OCA\EndToEndEncryption\UserAgentManager; +use OCA\EndToEndEncryption\E2EEnabledPathCache; use OCP\Files\FileInfo; use OCP\Files\IRootFolder; use OCP\Files\Node; @@ -56,8 +57,10 @@ class LockPluginTest extends TestCase { /** @var UserAgentManager|\PHPUnit\Framework\MockObject\MockObject */ private $userAgentManager; - /** @var LockPlugin */ - private $plugin; + /** @var E2EEnabledPathCache|\PHPUnit\Framework\MockObject\MockObject */ + private $pathCache; + + private LockPlugin $plugin; protected function setUp(): void { parent::setUp(); @@ -66,9 +69,10 @@ class LockPluginTest extends TestCase { $this->userSession = $this->createMock(IUserSession::class); $this->lockManager = $this->createMock(LockManager::class); $this->userAgentManager = $this->createMock(UserAgentManager::class); + $this->pathCache = $this->createMock(E2EEnabledPathCache::class); $this->plugin = new LockPlugin($this->rootFolder, $this->userSession, - $this->lockManager, $this->userAgentManager); + $this->lockManager, $this->userAgentManager, $this->pathCache); } public function testInitialize(): void { diff --git a/tests/Unit/Connector/Sabre/RedirectRequestPluginTest.php b/tests/Unit/Connector/Sabre/RedirectRequestPluginTest.php index 3688107..889f789 100644 --- a/tests/Unit/Connector/Sabre/RedirectRequestPluginTest.php +++ b/tests/Unit/Connector/Sabre/RedirectRequestPluginTest.php @@ -25,6 +25,7 @@ namespace OCA\EndToEndEncryption\Tests\Unit\Connector\Sabre; use OCA\DAV\Connector\Sabre\File; use OCA\EndToEndEncryption\Connector\Sabre\RedirectRequestPlugin; +use OCA\EndToEndEncryption\E2EEnabledPathCache; use OCP\Files\IRootFolder; use OCP\IUserSession; use Sabre\DAV\Server; @@ -39,16 +40,19 @@ class RedirectRequestPluginTest extends TestCase { /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */ private $userSession; - /** @var RedirectRequestPlugin */ - private $plugin; + /** @var E2EEnabledPathCache|\PHPUnit\Framework\MockObject\MockObject */ + private $pathCache; + + private RedirectRequestPlugin $plugin; protected function setUp(): void { parent::setUp(); $this->rootFolder = $this->createMock(IRootFolder::class); $this->userSession = $this->createMock(IUserSession::class); + $this->pathCache = $this->createMock(E2EEnabledPathCache::class); - $this->plugin = new RedirectRequestPlugin($this->rootFolder, $this->userSession); + $this->plugin = new RedirectRequestPlugin($this->rootFolder, $this->userSession, $this->pathCache); } public function testInitialize(): void {