From 116c3132a7ffd5d6dc613a5c6b2c9f9292b9336b Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 11 Nov 2025 20:24:15 +0100 Subject: [PATCH 1/8] fix: Revert "fix: ignore root mount when getting mount for node" This reverts commit 99992f39e90727132027c5a858c1572be11e6e97. Signed-off-by: Joas Schilling --- lib/Operation.php | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/lib/Operation.php b/lib/Operation.php index c849a872..09901088 100644 --- a/lib/Operation.php +++ b/lib/Operation.php @@ -18,6 +18,7 @@ use OCP\Files\ForbiddenException; use OCP\Files\IRootFolder; use OCP\Files\Mount\IMountManager; +use OCP\Files\Mount\IMountPoint; use OCP\Files\Node; use OCP\Files\NotFoundException; use OCP\Files\Storage\IStorage; @@ -292,16 +293,8 @@ public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatch * @param array|ICacheEntry|null $cacheEntry */ private function getNode(IStorage $storage, string $path, $cacheEntry = null): ?Node { - $mountPoint = null; - $mounts = $this->mountManager->findByStorageId($storage->getId()); - foreach ($mounts as $mount) { - // we don't want to root mount; - if (strlen($mount->getMountPoint()) > 2) { - $mountPoint = $mount; - break; - } - } - + /** @var IMountPoint|false $mountPoint */ + $mountPoint = current($this->mountManager->findByStorageId($storage->getId())); if (!$mountPoint) { return null; } From 98c685ba85d5dc929e75035c127d7cbb22f21569 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 8 Sep 2025 17:32:20 +0200 Subject: [PATCH 2/8] fix: reuse the mountpoint the wrapper was setup with Signed-off-by: Robin Appelman # Conflicts: # lib/StorageWrapper.php --- lib/AppInfo/Application.php | 4 +++- lib/Operation.php | 15 +++++++++++---- lib/StorageWrapper.php | 7 +++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 50ded6f6..a061e246 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -17,6 +17,7 @@ use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\Files\Mount\IMountPoint; use OCP\Files\Storage\IStorage; use OCP\Util; use OCP\WorkflowEngine\Events\RegisterOperationsEvent; @@ -40,13 +41,14 @@ public function addStorageWrapper(): void { * @param IStorage $storage * @return StorageWrapper|IStorage */ - public function addStorageWrapperCallback($mountPoint, IStorage $storage) { + public function addStorageWrapperCallback($mountPoint, IStorage $storage, IMountPoint $mount) { if (!OC::$CLI && $mountPoint !== '/') { /** @var Operation $operation */ $operation = $this->getContainer()->get(Operation::class); return new StorageWrapper([ 'storage' => $storage, 'mountPoint' => $mountPoint, + 'mount' => $mount, 'operation' => $operation, ]); } diff --git a/lib/Operation.php b/lib/Operation.php index 09901088..9a2f041e 100644 --- a/lib/Operation.php +++ b/lib/Operation.php @@ -293,10 +293,17 @@ public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatch * @param array|ICacheEntry|null $cacheEntry */ private function getNode(IStorage $storage, string $path, $cacheEntry = null): ?Node { - /** @var IMountPoint|false $mountPoint */ - $mountPoint = current($this->mountManager->findByStorageId($storage->getId())); - if (!$mountPoint) { - return null; + if ($storage->instanceOfStorage(StorageWrapper::class)) { + /** @var StorageWrapper $mountPoint */ + $mountPoint = $storage->getMount(); + } else { + // fairly sure this branch is never taken, but not 100% + + /** @var IMountPoint|false $mountPoint */ + $mountPoint = current($this->mountManager->findByStorageId($storage->getId())); + if (!$mountPoint) { + return null; + } } $fullPath = $mountPoint->getMountPoint() . $path; diff --git a/lib/StorageWrapper.php b/lib/StorageWrapper.php index 1093eac7..ef5a3f0a 100644 --- a/lib/StorageWrapper.php +++ b/lib/StorageWrapper.php @@ -13,6 +13,7 @@ use OC\Files\Storage\Wrapper\Wrapper; use OCP\Constants; use OCP\Files\ForbiddenException; +use OCP\Files\Mount\IMountPoint; use OCP\Files\Storage\IStorage; use OCP\Files\Storage\IWriteStreamStorage; @@ -20,6 +21,7 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage { protected readonly Operation $operation; public readonly string $mountPoint; protected readonly int $mask; + private readonly IMountPoint $mount; /** * @param array $parameters @@ -28,6 +30,7 @@ public function __construct($parameters) { parent::__construct($parameters); $this->operation = $parameters['operation']; $this->mountPoint = $parameters['mountPoint']; + $this->mount = $parameters['mount']; $this->mask = Constants::PERMISSION_ALL & ~Constants::PERMISSION_READ @@ -323,4 +326,8 @@ private function isPartFile(string $path): bool { $extension = pathinfo($path, PATHINFO_EXTENSION); return $extension === 'part'; } + + public function getMount(): IMountPoint { + return $this->mount; + } } From cbf79a5aa21bf097505cb97d3d63592f4b9917b6 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 8 Sep 2025 17:38:51 +0200 Subject: [PATCH 3/8] test: update test to new wrapper argument Signed-off-by: Robin Appelman --- tests/Unit/StorageWrapperTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/Unit/StorageWrapperTest.php b/tests/Unit/StorageWrapperTest.php index a6715e19..0822ed58 100644 --- a/tests/Unit/StorageWrapperTest.php +++ b/tests/Unit/StorageWrapperTest.php @@ -11,6 +11,7 @@ use OCA\FilesAccessControl\Operation; use OCA\FilesAccessControl\StorageWrapper; use OCP\Files\ForbiddenException; +use OCP\Files\Mount\IMountPoint; use OCP\Files\Storage\IStorage; use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; @@ -27,12 +28,16 @@ protected function setUp(): void { } protected function getInstance(array $methods = []): StorageWrapper&MockObject { + $mount = $this->createMock(IMountPoint::class); + $mount->method('getMountPoint') + ->willReturn('mountPoint'); return $this->getMockBuilder(StorageWrapper::class) ->setConstructorArgs([ [ 'storage' => $this->storage, 'mountPoint' => 'mountPoint', 'operation' => $this->operation, + 'mount' => $mount, ] ]) ->onlyMethods($methods) From 20e7f1accb979f2362bcbeac46bf894104a16021 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 8 Sep 2025 18:02:09 +0200 Subject: [PATCH 4/8] chore: psalm fixes Signed-off-by: Robin Appelman --- lib/Operation.php | 2 +- lib/StorageWrapper.php | 16 +- psalm.xml | 3 + tests/psalm-baseline.xml | 24 +- tests/stubs/oc_files_cache_cache.php | 77 +++++++ .../oc_files_cache_wrapper_cachewrapper.php | 21 ++ .../oc_files_storage_wrapper_wrapper.php | 211 ++++++++++++++++++ 7 files changed, 331 insertions(+), 23 deletions(-) create mode 100644 tests/stubs/oc_files_cache_cache.php create mode 100644 tests/stubs/oc_files_cache_wrapper_cachewrapper.php create mode 100644 tests/stubs/oc_files_storage_wrapper_wrapper.php diff --git a/lib/Operation.php b/lib/Operation.php index 9a2f041e..1a932590 100644 --- a/lib/Operation.php +++ b/lib/Operation.php @@ -294,7 +294,7 @@ public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatch */ private function getNode(IStorage $storage, string $path, $cacheEntry = null): ?Node { if ($storage->instanceOfStorage(StorageWrapper::class)) { - /** @var StorageWrapper $mountPoint */ + /** @var StorageWrapper $storage */ $mountPoint = $storage->getMount(); } else { // fairly sure this branch is never taken, but not 100% diff --git a/lib/StorageWrapper.php b/lib/StorageWrapper.php index ef5a3f0a..3f9d1e10 100644 --- a/lib/StorageWrapper.php +++ b/lib/StorageWrapper.php @@ -8,10 +8,10 @@ namespace OCA\FilesAccessControl; -use OC\Files\Cache\Cache; use OC\Files\Storage\Storage; use OC\Files\Storage\Wrapper\Wrapper; use OCP\Constants; +use OCP\Files\Cache\ICache; use OCP\Files\ForbiddenException; use OCP\Files\Mount\IMountPoint; use OCP\Files\Storage\IStorage; @@ -147,7 +147,7 @@ public function getPermissions($path) { * see http://php.net/manual/en/function.file_get_contents.php * * @param string $path - * @return string + * @return string|false * @throws ForbiddenException */ public function file_get_contents($path) { @@ -159,8 +159,8 @@ public function file_get_contents($path) { * see http://php.net/manual/en/function.file_put_contents.php * * @param string $path - * @param string $data - * @return bool + * @param mixed $data + * @return int|float|false * @throws ForbiddenException */ public function file_put_contents($path, $data) { @@ -215,7 +215,7 @@ public function copy($path1, $path2) { * * @param string $path * @param string $mode - * @return resource + * @return resource|bool * @throws ForbiddenException */ public function fopen($path, $mode) { @@ -242,7 +242,7 @@ public function touch($path, $mtime = null) { * * @param string $path * @param Storage (optional) the storage to pass to the cache - * @return Cache + * @return ICache */ public function getCache($path = '', $storage = null) { if (!$storage) { @@ -258,7 +258,7 @@ public function getCache($path = '', $storage = null) { * For now the returned array can hold the parameter url - in future more attributes might follow. * * @param string $path - * @return array + * @return array|bool * @throws ForbiddenException */ public function getDirectDownload($path) { @@ -306,7 +306,7 @@ public function writeStream(string $path, $stream, ?int $size = null): int { $this->checkFileAccess($path, false); } - $result = $this->storage->writeStream($path, $stream, $size); + $result = parent::writeStream($path, $stream, $size); if (!$this->isPartFile($path)) { return $result; } diff --git a/psalm.xml b/psalm.xml index 4e5bf0bb..5e7c69ec 100644 --- a/psalm.xml +++ b/psalm.xml @@ -26,6 +26,9 @@ + + + diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index b17e249b..1feed4d5 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -6,18 +6,10 @@ - - - - - fileEntity]]> - - - @@ -32,11 +24,15 @@ - - - - - - + + + + + + + + + + diff --git a/tests/stubs/oc_files_cache_cache.php b/tests/stubs/oc_files_cache_cache.php new file mode 100644 index 00000000..2a6c7a08 --- /dev/null +++ b/tests/stubs/oc_files_cache_cache.php @@ -0,0 +1,77 @@ +cache = $cache; + } + public function getNumericStorageId() { + } + public function getIncomplete() { + } + public function getPathById($id) { + } + public function getAll() { + } + public function get($file) { + } + public function getFolderContents($folder) { + } + public function getFolderContentsById($fileId) { + } + public function put($file, array $data) { + } + public function insert($file, array $data) { + } + public function update($id, array $data) { + } + public function getId($file) { + } + public function getParentId($file) { + } + public function inCache($file) { + } + public function remove($file) { + } + public function move($source, $target) { + } + public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) { + } + public function clear() { + } + public function getStatus($file) { + } + public function search($pattern) { + } + public function searchByMime($mimetype) { + } + public function searchQuery(ISearchQuery $query) { + } + public function correctFolderSize($path, $data = null, $isBackgroundScan = false) { + } + public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int { + } + public function normalize($path) { + } + public function getQueryFilterForStorage(): ISearchOperator { + } + public function getCacheEntryFromSearchResult(ICacheEntry $rawEntry): ?ICacheEntry { + } + public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) { + } + } +} diff --git a/tests/stubs/oc_files_cache_wrapper_cachewrapper.php b/tests/stubs/oc_files_cache_wrapper_cachewrapper.php new file mode 100644 index 00000000..2d99ba2e --- /dev/null +++ b/tests/stubs/oc_files_cache_wrapper_cachewrapper.php @@ -0,0 +1,21 @@ + $class + * @psalm-return T|null + */ + public function getInstanceOfStorage(string $class) { + } + + /** + * Pass any methods custom to specific storage implementations to the wrapped storage + * + * @param string $method + * @param array $args + * @return mixed + */ + public function __call($method, $args) { + } + + public function getDirectDownload($path) { + } + + public function getAvailability() { + } + + public function setAvailability($isAvailable) { + } + + public function verifyPath($path, $fileName) { + } + + public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + } + + public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + } + + public function getMetaData($path) { + } + + public function acquireLock($path, $type, ILockingProvider $provider) { + } + + public function releaseLock($path, $type, ILockingProvider $provider) { + } + + public function changeLock($path, $type, ILockingProvider $provider) { + } + + public function needsPartFile() { + } + + public function writeStream(string $path, $stream, ?int $size = null): int { + } + + public function getDirectoryContent($directory): \Traversable { + } + + public function isWrapperOf(IStorage $storage) { + } + + public function setOwner(?string $user): void { + } + } +} From 0d01495ce6475f6db04904c022bcf433f1e48900 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 18 Oct 2023 16:38:16 +0200 Subject: [PATCH 5/8] fix(storage): Pass around mountpoint for getNode Signed-off-by: Robin Appelman --- lib/CacheWrapper.php | 5 ++++- lib/Operation.php | 24 ++++-------------------- lib/StorageWrapper.php | 6 +++--- tests/Unit/StorageWrapperTest.php | 2 +- 4 files changed, 12 insertions(+), 25 deletions(-) diff --git a/lib/CacheWrapper.php b/lib/CacheWrapper.php index b0c3b45a..22ce75bb 100644 --- a/lib/CacheWrapper.php +++ b/lib/CacheWrapper.php @@ -12,14 +12,17 @@ use OCP\Constants; use OCP\Files\Cache\ICache; use OCP\Files\ForbiddenException; +use OCP\Files\Mount\IMountPoint; use OCP\Files\Storage\IStorage; class CacheWrapper extends Wrapper { protected readonly int $mask; + protected readonly ?IStorage $storage; public function __construct( ICache $cache, protected readonly IStorage $storage, + protected readonly IMountPoint $mountPoint, protected readonly Operation $operation, ) { parent::__construct($cache); @@ -33,7 +36,7 @@ public function __construct( protected function formatCacheEntry($entry) { if (isset($entry['path']) && isset($entry['permissions'])) { try { - $this->operation->checkFileAccess($this->storage, $entry['path'], $entry['mimetype'] === 'httpd/unix-directory', $entry); + $this->operation->checkFileAccess($this->storage, $entry['path'], $this->mountPoint, $entry['mimetype'] === 'httpd/unix-directory', $entry); } catch (ForbiddenException) { $entry['permissions'] &= $this->mask; } diff --git a/lib/Operation.php b/lib/Operation.php index 1a932590..d6c75da9 100644 --- a/lib/Operation.php +++ b/lib/Operation.php @@ -50,7 +50,7 @@ public function __construct( * @param array|ICacheEntry|null $cacheEntry * @throws ForbiddenException */ - public function checkFileAccess(IStorage $storage, string $path, bool $isDir = false, $cacheEntry = null): void { + public function checkFileAccess(IStorage $storage, string $path, IMountPoint $mountPoint, bool $isDir, $cacheEntry = null): void { if (!$this->isBlockablePath($storage, $path) || $this->isCreatingSkeletonFiles() || $this->nestingLevel !== 0) { // Allow creating skeletons and theming // https://github.com/nextcloud/files_accesscontrol/issues/5 @@ -63,7 +63,7 @@ public function checkFileAccess(IStorage $storage, string $path, bool $isDir = f $filePath = $this->translatePath($storage, $path); $ruleMatcher = $this->manager->getRuleMatcher(); $ruleMatcher->setFileInfo($storage, $filePath, $isDir); - $node = $this->getNode($storage, $path, $cacheEntry); + $node = $this->getNode($path, $mountPoint, $cacheEntry); if ($node !== null) { $ruleMatcher->setEntitySubject($this->fileEntity, $node); } @@ -289,23 +289,7 @@ public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatch // Noop } - /** - * @param array|ICacheEntry|null $cacheEntry - */ - private function getNode(IStorage $storage, string $path, $cacheEntry = null): ?Node { - if ($storage->instanceOfStorage(StorageWrapper::class)) { - /** @var StorageWrapper $storage */ - $mountPoint = $storage->getMount(); - } else { - // fairly sure this branch is never taken, but not 100% - - /** @var IMountPoint|false $mountPoint */ - $mountPoint = current($this->mountManager->findByStorageId($storage->getId())); - if (!$mountPoint) { - return null; - } - } - + private function getNode(string $path, IMountPoint $mountPoint, ICacheEntry|array|null $cacheEntry = null): ?Node { $fullPath = $mountPoint->getMountPoint() . $path; if ($cacheEntry) { // todo: LazyNode? @@ -320,7 +304,7 @@ private function getNode(IStorage $storage, string $path, $cacheEntry = null): ? } else { try { return $this->rootFolder->get($fullPath); - } catch (NotFoundException $e) { + } catch (NotFoundException) { return null; } } diff --git a/lib/StorageWrapper.php b/lib/StorageWrapper.php index 3f9d1e10..4cbde8ea 100644 --- a/lib/StorageWrapper.php +++ b/lib/StorageWrapper.php @@ -21,7 +21,7 @@ class StorageWrapper extends Wrapper implements IWriteStreamStorage { protected readonly Operation $operation; public readonly string $mountPoint; protected readonly int $mask; - private readonly IMountPoint $mount; + protected readonly IMountPoint $mount; /** * @param array $parameters @@ -43,7 +43,7 @@ public function __construct($parameters) { * @throws ForbiddenException */ protected function checkFileAccess(string $path, ?bool $isDir = null): void { - $this->operation->checkFileAccess($this, $path, is_bool($isDir) ? $isDir : $this->is_dir($path)); + $this->operation->checkFileAccess($this, $path, $this->mount, is_bool($isDir) ? $isDir : $this->is_dir($path)); } /* @@ -249,7 +249,7 @@ public function getCache($path = '', $storage = null) { $storage = $this; } $cache = $this->storage->getCache($path, $storage); - return new CacheWrapper($cache, $storage, $this->operation); + return new CacheWrapper($cache, $storage, $this->mount, $this->operation); } /** diff --git a/tests/Unit/StorageWrapperTest.php b/tests/Unit/StorageWrapperTest.php index 0822ed58..fed401b1 100644 --- a/tests/Unit/StorageWrapperTest.php +++ b/tests/Unit/StorageWrapperTest.php @@ -59,7 +59,7 @@ public function testCheckFileAccess(string $path, bool $isDir): void { $this->operation->expects($this->once()) ->method('checkFileAccess') - ->with($storage, $path); + ->with($storage, $path, $this->createMock(IMountPoint::class), false); self::invokePrivate($storage, 'checkFileAccess', [$path, $isDir]); } From eba662117b9af4e2f28136b4e319715526a7e256 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 18 Oct 2023 17:13:28 +0200 Subject: [PATCH 6/8] fix(operation): Improve getNode logic Signed-off-by: Robin Appelman --- lib/CacheWrapper.php | 11 +++- lib/Operation.php | 60 +++++++------------ lib/StorageWrapper.php | 4 +- psalm.xml | 1 + tests/Unit/StorageWrapperTest.php | 16 +++-- tests/stubs/oc_files_storage_wrapper_jail.php | 11 ++++ 6 files changed, 56 insertions(+), 47 deletions(-) create mode 100644 tests/stubs/oc_files_storage_wrapper_jail.php diff --git a/lib/CacheWrapper.php b/lib/CacheWrapper.php index 22ce75bb..0604ea7f 100644 --- a/lib/CacheWrapper.php +++ b/lib/CacheWrapper.php @@ -9,6 +9,7 @@ namespace OCA\FilesAccessControl; use OC\Files\Cache\Wrapper\CacheWrapper as Wrapper; +use OC\Files\Storage\Wrapper\Jail; use OCP\Constants; use OCP\Files\Cache\ICache; use OCP\Files\ForbiddenException; @@ -21,11 +22,11 @@ class CacheWrapper extends Wrapper { public function __construct( ICache $cache, - protected readonly IStorage $storage, protected readonly IMountPoint $mountPoint, protected readonly Operation $operation, ) { parent::__construct($cache); + $this->storage = $mountPoint->getStorage(); $this->mask = Constants::PERMISSION_ALL & ~Constants::PERMISSION_READ & ~Constants::PERMISSION_CREATE @@ -36,7 +37,13 @@ public function __construct( protected function formatCacheEntry($entry) { if (isset($entry['path']) && isset($entry['permissions'])) { try { - $this->operation->checkFileAccess($this->storage, $entry['path'], $this->mountPoint, $entry['mimetype'] === 'httpd/unix-directory', $entry); + $storage = $this->storage; + $path = $entry['path']; + if ($storage?->instanceOfStorage(Jail::class)) { + /** @var Jail $storage */ + $path = $storage->getJailedPath($path); + } + $this->operation->checkFileAccess($path, $this->mountPoint, $entry['mimetype'] === 'httpd/unix-directory', $entry); } catch (ForbiddenException) { $entry['permissions'] &= $this->mask; } diff --git a/lib/Operation.php b/lib/Operation.php index d6c75da9..717ad74d 100644 --- a/lib/Operation.php +++ b/lib/Operation.php @@ -20,7 +20,6 @@ use OCP\Files\Mount\IMountManager; use OCP\Files\Mount\IMountPoint; use OCP\Files\Node; -use OCP\Files\NotFoundException; use OCP\Files\Storage\IStorage; use OCP\IL10N; use OCP\IURLGenerator; @@ -29,7 +28,6 @@ use OCP\WorkflowEngine\IRuleMatcher; use OCP\WorkflowEngine\ISpecificOperation; use Psr\Log\LoggerInterface; -use ReflectionClass; use UnexpectedValueException; class Operation implements IComplexOperation, ISpecificOperation { @@ -50,13 +48,17 @@ public function __construct( * @param array|ICacheEntry|null $cacheEntry * @throws ForbiddenException */ - public function checkFileAccess(IStorage $storage, string $path, IMountPoint $mountPoint, bool $isDir, $cacheEntry = null): void { - if (!$this->isBlockablePath($storage, $path) || $this->isCreatingSkeletonFiles() || $this->nestingLevel !== 0) { + public function checkFileAccess(string $path, IMountPoint $mountPoint, bool $isDir, $cacheEntry = null): void { + if (!$this->isBlockablePath($mountPoint, $path) || $this->isCreatingSkeletonFiles() || $this->nestingLevel !== 0) { // Allow creating skeletons and theming // https://github.com/nextcloud/files_accesscontrol/issues/5 // https://github.com/nextcloud/files_accesscontrol/issues/12 return; } + $storage = $mountPoint->getStorage(); + if ($storage === null) { + return; + } $this->nestingLevel++; @@ -80,24 +82,8 @@ public function checkFileAccess(IStorage $storage, string $path, IMountPoint $mo } } - protected function isBlockablePath(IStorage $storage, string $path): bool { - if (property_exists($storage, 'mountPoint')) { - $hasMountPoint = $storage instanceof StorageWrapper; - if (!$hasMountPoint) { - $ref = new ReflectionClass($storage); - $prop = $ref->getProperty('mountPoint'); - $hasMountPoint = $prop->isPublic(); - } - - if ($hasMountPoint) { - /** @var StorageWrapper $storage */ - $fullPath = $storage->mountPoint . ltrim($path, '/'); - } else { - $fullPath = $path; - } - } else { - $fullPath = $path; - } + protected function isBlockablePath(IMountPoint $mountPoint, string $path): bool { + $fullPath = $mountPoint->getMountPoint() . ltrim($path, '/'); if (substr_count($fullPath, '/') < 3) { return false; @@ -291,22 +277,20 @@ public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatch private function getNode(string $path, IMountPoint $mountPoint, ICacheEntry|array|null $cacheEntry = null): ?Node { $fullPath = $mountPoint->getMountPoint() . $path; - if ($cacheEntry) { - // todo: LazyNode? - $info = new FileInfo($fullPath, $mountPoint->getStorage(), $path, $cacheEntry, $mountPoint); - $isDir = $info->getType() === \OCP\Files\FileInfo::TYPE_FOLDER; - $view = new View(''); - if ($isDir) { - return new Folder($this->rootFolder, $view, $path, $info); - } else { - return new \OC\Files\Node\File($this->rootFolder, $view, $path, $info); - } - } else { - try { - return $this->rootFolder->get($fullPath); - } catch (NotFoundException) { - return null; - } + if (!$cacheEntry) { + $cacheEntry = $mountPoint->getStorage()?->getCache()->get($path); + } + if (!$cacheEntry) { + return null; + } + + // todo: LazyNode? + $info = new FileInfo($fullPath, $mountPoint->getStorage(), $path, $cacheEntry, $mountPoint); + $isDir = $info->getType() === \OCP\Files\FileInfo::TYPE_FOLDER; + $view = new View(''); + if ($isDir) { + return new Folder($this->rootFolder, $view, $path, $info); } + return new \OC\Files\Node\File($this->rootFolder, $view, $path, $info); } } diff --git a/lib/StorageWrapper.php b/lib/StorageWrapper.php index 4cbde8ea..15a471e4 100644 --- a/lib/StorageWrapper.php +++ b/lib/StorageWrapper.php @@ -43,7 +43,7 @@ public function __construct($parameters) { * @throws ForbiddenException */ protected function checkFileAccess(string $path, ?bool $isDir = null): void { - $this->operation->checkFileAccess($this, $path, $this->mount, is_bool($isDir) ? $isDir : $this->is_dir($path)); + $this->operation->checkFileAccess($path, $this->mount, is_bool($isDir) ? $isDir : $this->is_dir($path)); } /* @@ -249,7 +249,7 @@ public function getCache($path = '', $storage = null) { $storage = $this; } $cache = $this->storage->getCache($path, $storage); - return new CacheWrapper($cache, $storage, $this->mount, $this->operation); + return new CacheWrapper($cache, $this->mount, $this->operation); } /** diff --git a/psalm.xml b/psalm.xml index 5e7c69ec..82e912be 100644 --- a/psalm.xml +++ b/psalm.xml @@ -30,5 +30,6 @@ + diff --git a/tests/Unit/StorageWrapperTest.php b/tests/Unit/StorageWrapperTest.php index fed401b1..8cde9a7a 100644 --- a/tests/Unit/StorageWrapperTest.php +++ b/tests/Unit/StorageWrapperTest.php @@ -20,24 +20,30 @@ class StorageWrapperTest extends TestCase { protected IStorage&MockObject $storage; protected Operation&MockObject $operation; + /** @var IMountPoint|MockObject */ + protected $mountPoint; + protected function setUp(): void { parent::setUp(); $this->storage = $this->createMock(IStorage::class); $this->operation = $this->createMock(Operation::class); + + $this->mountPoint = $this->createMock(IMountPoint::class); + $this->mountPoint->method('getMountPoint') + ->willReturn('mountPoint'); + $this->mountPoint->method('getStorage') + ->willReturn($this->storage); } protected function getInstance(array $methods = []): StorageWrapper&MockObject { - $mount = $this->createMock(IMountPoint::class); - $mount->method('getMountPoint') - ->willReturn('mountPoint'); return $this->getMockBuilder(StorageWrapper::class) ->setConstructorArgs([ [ 'storage' => $this->storage, 'mountPoint' => 'mountPoint', + 'mount' => $this->mountPoint, 'operation' => $this->operation, - 'mount' => $mount, ] ]) ->onlyMethods($methods) @@ -59,7 +65,7 @@ public function testCheckFileAccess(string $path, bool $isDir): void { $this->operation->expects($this->once()) ->method('checkFileAccess') - ->with($storage, $path, $this->createMock(IMountPoint::class), false); + ->with($path, $this->mountPoint, $isDir); self::invokePrivate($storage, 'checkFileAccess', [$path, $isDir]); } diff --git a/tests/stubs/oc_files_storage_wrapper_jail.php b/tests/stubs/oc_files_storage_wrapper_jail.php new file mode 100644 index 00000000..f30207fb --- /dev/null +++ b/tests/stubs/oc_files_storage_wrapper_jail.php @@ -0,0 +1,11 @@ + Date: Tue, 11 Nov 2025 10:50:22 +0100 Subject: [PATCH 7/8] fix(jailed-storage): Fix null returned by jailed storage Signed-off-by: Joas Schilling --- lib/CacheWrapper.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/CacheWrapper.php b/lib/CacheWrapper.php index 0604ea7f..8ed22666 100644 --- a/lib/CacheWrapper.php +++ b/lib/CacheWrapper.php @@ -41,7 +41,8 @@ protected function formatCacheEntry($entry) { $path = $entry['path']; if ($storage?->instanceOfStorage(Jail::class)) { /** @var Jail $storage */ - $path = $storage->getJailedPath($path); + $jailedPath = $storage->getJailedPath($path); + $path = $jailedPath ?? $path; } $this->operation->checkFileAccess($path, $this->mountPoint, $entry['mimetype'] === 'httpd/unix-directory', $entry); } catch (ForbiddenException) { From 355be4d5a5af99500d19d99904c402149cfedd99 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 16 Oct 2025 13:49:53 +0200 Subject: [PATCH 8/8] test: Add an integration test with richdocuments Signed-off-by: Joas Schilling --- .github/workflows/integration.yml | 17 ++++++++++ .github/workflows/phpunit-mariadb.yml | 24 ++++++++++--- .github/workflows/phpunit-mysql.yml | 14 +++++++- .github/workflows/phpunit-oci.yml | 13 +++++++ .github/workflows/phpunit-pgsql.yml | 13 +++++++ .github/workflows/phpunit-sqlite.yml | 13 +++++++ .../features/bootstrap/FeatureContext.php | 34 +++++++++++++++++++ .../Integration/features/bootstrap/WebDav.php | 34 +++++++++++++------ tests/Integration/features/mimetypes.feature | 2 +- .../Integration/features/sharing-user.feature | 32 +++++++++++++++++ tests/Integration/run.sh | 1 + 11 files changed, 180 insertions(+), 17 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index f9f7dc7e..039fa327 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -26,6 +26,7 @@ jobs: php-versions: ['8.1'] databases: ['sqlite', 'mysql', 'pgsql'] server-versions: ['stable30'] + richdocuments-versions: ['stable30'] primary-storage: ['local', 'minio'] name: php${{ matrix.php-versions }}-${{ matrix.databases }}-${{ matrix.server-versions }}-${{ matrix.primary-storage}} @@ -74,6 +75,14 @@ jobs: persist-credentials: false path: apps/${{ env.APP_NAME }} + - name: Checkout app (richdocuments) + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + path: apps/richdocuments + repository: nextcloud/richdocuments + ref: ${{ matrix.richdocuments-versions }} + - name: Set up php ${{ matrix.php-versions }} uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2.31.1 with: @@ -91,6 +100,14 @@ jobs: working-directory: apps/${{ env.APP_NAME }} run: composer i --no-dev + - name: Set up behat dependencies + working-directory: apps/${{ env.APP_NAME }}/tests/Integration + run: composer i + + - name: Set up dependencies (richdocuments) + working-directory: apps/richdocuments + run: composer i --no-dev + - name: Set up Nextcloud for S3 primary storage if: matrix.primary-storage == 'minio' run: | diff --git a/.github/workflows/phpunit-mariadb.yml b/.github/workflows/phpunit-mariadb.yml index d3cc10d2..2616d8e4 100644 --- a/.github/workflows/phpunit-mariadb.yml +++ b/.github/workflows/phpunit-mariadb.yml @@ -68,21 +68,23 @@ jobs: matrix: php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} - mariadb-versions: ['10.6', '10.11'] + mariadb-versions: ['10.6', '11.4'] + richdocuments-versions: ['stable30'] name: MariaDB ${{ matrix.mariadb-versions }} PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} services: mariadb: - image: ghcr.io/nextcloud/continuous-integration-mariadb-${{ matrix.mariadb-versions }}:latest + image: ghcr.io/nextcloud/continuous-integration-mariadb-${{ matrix.mariadb-versions }}:latest # zizmor: ignore[unpinned-images] ports: - 4444:3306/tcp env: - MYSQL_ROOT_PASSWORD: rootpassword - options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 5 + MARIADB_ROOT_PASSWORD: rootpassword + options: --health-cmd="mariadb-admin ping" --health-interval 5s --health-timeout 2s --health-retries 5 steps: - name: Set app env + if: ${{ env.APP_NAME == '' }} run: | # Split and keep last echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV @@ -99,8 +101,16 @@ jobs: with: path: apps/${{ env.APP_NAME }} + - name: Checkout app (richdocuments) + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + path: apps/richdocuments + repository: nextcloud/richdocuments + ref: ${{ matrix.richdocuments-versions }} + - name: Set up php ${{ matrix.php-versions }} - uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2.31.1 + uses: shivammathur/setup-php@0f7f1d08e3e32076e51cae65eb0b0c871405b16e # v2.34.1 with: php-version: ${{ matrix.php-versions }} # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation @@ -127,6 +137,10 @@ jobs: working-directory: apps/${{ env.APP_NAME }} run: composer i + - name: Set up dependencies (richdocuments) + working-directory: apps/richdocuments + run: composer i --no-dev + - name: Set up Nextcloud env: DB_PORT: 4444 diff --git a/.github/workflows/phpunit-mysql.yml b/.github/workflows/phpunit-mysql.yml index c0f4c69b..81565adb 100644 --- a/.github/workflows/phpunit-mysql.yml +++ b/.github/workflows/phpunit-mysql.yml @@ -30,7 +30,7 @@ jobs: id: versions uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1 with: - matrix: '{"mysql-versions": ["8.4"]}' + matrix: '{"mysql-versions": ["8.4"], "richdocuments-versions": ["stable30"]}' changes: runs-on: ubuntu-latest-low @@ -97,6 +97,14 @@ jobs: with: path: apps/${{ env.APP_NAME }} + - name: Checkout app (richdocuments) + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + path: apps/richdocuments + repository: nextcloud/richdocuments + ref: ${{ matrix.richdocuments-versions }} + - name: Set up php ${{ matrix.php-versions }} uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2.31.1 with: @@ -125,6 +133,10 @@ jobs: working-directory: apps/${{ env.APP_NAME }} run: composer i + - name: Set up dependencies (richdocuments) + working-directory: apps/richdocuments + run: composer i --no-dev + - name: Set up Nextcloud env: DB_PORT: 4444 diff --git a/.github/workflows/phpunit-oci.yml b/.github/workflows/phpunit-oci.yml index d03beb9d..f4d32bc5 100644 --- a/.github/workflows/phpunit-oci.yml +++ b/.github/workflows/phpunit-oci.yml @@ -68,6 +68,7 @@ jobs: matrix: php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} + richdocuments-versions: ['stable30'] name: OCI PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} @@ -110,6 +111,14 @@ jobs: with: path: apps/${{ env.APP_NAME }} + - name: Checkout app (richdocuments) + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + path: apps/richdocuments + repository: nextcloud/richdocuments + ref: ${{ matrix.richdocuments-versions }} + - name: Set up php ${{ matrix.php-versions }} uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2.31.1 with: @@ -133,6 +142,10 @@ jobs: working-directory: apps/${{ env.APP_NAME }} run: composer i + - name: Set up dependencies (richdocuments) + working-directory: apps/richdocuments + run: composer i --no-dev + - name: Set up Nextcloud env: DB_PORT: 1521 diff --git a/.github/workflows/phpunit-pgsql.yml b/.github/workflows/phpunit-pgsql.yml index 2a23e02e..6e1f32a4 100644 --- a/.github/workflows/phpunit-pgsql.yml +++ b/.github/workflows/phpunit-pgsql.yml @@ -68,6 +68,7 @@ jobs: matrix: php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} + richdocuments-versions: ['stable30'] name: PostgreSQL PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} @@ -100,6 +101,14 @@ jobs: with: path: apps/${{ env.APP_NAME }} + - name: Checkout app (richdocuments) + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + path: apps/richdocuments + repository: nextcloud/richdocuments + ref: ${{ matrix.richdocuments-versions }} + - name: Set up php ${{ matrix.php-versions }} uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2.31.1 with: @@ -123,6 +132,10 @@ jobs: working-directory: apps/${{ env.APP_NAME }} run: composer i + - name: Set up dependencies (richdocuments) + working-directory: apps/richdocuments + run: composer i --no-dev + - name: Set up Nextcloud env: DB_PORT: 4444 diff --git a/.github/workflows/phpunit-sqlite.yml b/.github/workflows/phpunit-sqlite.yml index be9e3324..7e751e18 100644 --- a/.github/workflows/phpunit-sqlite.yml +++ b/.github/workflows/phpunit-sqlite.yml @@ -68,6 +68,7 @@ jobs: matrix: php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} + richdocuments-versions: ['stable30'] name: SQLite PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} @@ -89,6 +90,14 @@ jobs: with: path: apps/${{ env.APP_NAME }} + - name: Checkout app (richdocuments) + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + path: apps/richdocuments + repository: nextcloud/richdocuments + ref: ${{ matrix.richdocuments-versions }} + - name: Set up php ${{ matrix.php-versions }} uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2.31.1 with: @@ -112,6 +121,10 @@ jobs: working-directory: apps/${{ env.APP_NAME }} run: composer i + - name: Set up dependencies (richdocuments) + working-directory: apps/richdocuments + run: composer i --no-dev + - name: Set up Nextcloud env: DB_PORT: 4444 diff --git a/tests/Integration/features/bootstrap/FeatureContext.php b/tests/Integration/features/bootstrap/FeatureContext.php index 41e87c81..be307711 100644 --- a/tests/Integration/features/bootstrap/FeatureContext.php +++ b/tests/Integration/features/bootstrap/FeatureContext.php @@ -45,6 +45,8 @@ class FeatureContext implements Context { protected string $tagId = ''; protected array $createdUsers = []; + protected array $changedConfigs = []; + /** * FeatureContext constructor. */ @@ -61,6 +63,12 @@ public function cleanUpBetweenTests() { $this->setCurrentUser('admin'); $this->sendingTo('DELETE', '/apps/files_accesscontrol_testing'); $this->assertStatusCode($this->response, 200); + + foreach ($this->changedConfigs as $appId => $configs) { + foreach ($configs as $config) { + $this->sendingTo('DELETE', '/apps/provisioning_api/api/v1/config/apps/' . $appId . '/' . $config); + } + } } /** @@ -133,6 +141,20 @@ public function userSharesFile(string $sharer, string $file, string $sharee): vo ]); } + /** + * @Given /^user "([^"]*)" shares file "([^"]*)" publicly$/ + */ + public function userSharesFilePublicly(string $sharer, string $file): void { + $this->setCurrentUser($sharer); + $this->sendingToWith('POST', '/apps/files_sharing/api/v1/shares', [ + 'path' => $file, + 'permissions' => 19, + 'shareType' => 3, + ]); + $responseBody = json_decode($this->response->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + $this->lastShareData = $responseBody['ocs']['data']; + } + // ChecksumsContext /** * @Then The webdav response should have a status code :statusCode @@ -150,6 +172,18 @@ public function theWebdavResponseShouldHaveAStatusCode($statusCode) { } } + + #[\Behat\Step\Given('the following :appId app config is set')] + public function setAppConfig(string $appId, TableNode $formData): void { + $this->setCurrentUser('admin'); + foreach ($formData->getRows() as $row) { + $this->sendingToWith('POST', '/apps/provisioning_api/api/v1/config/apps/' . $appId . '/' . $row[0], [ + 'value' => $row[1], + ]); + $this->changedConfigs[$appId][] = $row[0]; + } + } + /** * User management */ diff --git a/tests/Integration/features/bootstrap/WebDav.php b/tests/Integration/features/bootstrap/WebDav.php index ac1976d9..715a8b8e 100644 --- a/tests/Integration/features/bootstrap/WebDav.php +++ b/tests/Integration/features/bootstrap/WebDav.php @@ -19,6 +19,7 @@ trait WebDav { /** @var int */ private $storedFileID = null; private array $trashedFiles = []; + protected array $lastShareData = []; /** * @Given /^using dav path "([^"]*)"$/ @@ -140,17 +141,21 @@ public function downloadFileWithRange($fileSource, $range) { * @param string $range */ public function downloadPublicFileWithRange($range) { - $token = $this->lastShareData->data->token; - $fullUrl = $this->baseUrl . "public.php/webdav"; + $token = $this->lastShareData['token']; + $fullUrl = $this->baseUrl . 'public.php/webdav'; $client = new GClient(); $options = []; - $options['auth'] = [$token, ""]; + $options['auth'] = [$token, '']; $options['headers'] = [ 'Range' => $range ]; - $this->response = $client->request("GET", $fullUrl, $options); + try { + $this->response = $client->request('GET', $fullUrl, $options); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } } /** @@ -158,8 +163,8 @@ public function downloadPublicFileWithRange($range) { * @param string $range */ public function downloadPublicFileInsideAFolderWithRange($path, $range) { - $token = $this->lastShareData->data->token; - $fullUrl = $this->baseUrl . "public.php/webdav" . "$path"; + $token = $this->lastShareData['token']; + $fullUrl = $this->baseUrl . 'public.php/webdav' . "$path"; $client = new GClient(); $options = [ @@ -167,9 +172,13 @@ public function downloadPublicFileInsideAFolderWithRange($path, $range) { 'Range' => $range ] ]; - $options['auth'] = [$token, ""]; + $options['auth'] = [$token, '']; - $this->response = $client->request("GET", $fullUrl, $options); + try { + $this->response = $client->request('GET', $fullUrl, $options); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } } /** @@ -189,8 +198,13 @@ public function downloadedContentShouldBe($content) { */ public function checkPropForFile($file, $prefix, $prop, $value) { $elementList = $this->propfindFile($this->currentUser, $file, "<$prefix:$prop/>"); - $property = $elementList['/'.$this->getDavFilesPath($this->currentUser).$file][200]["{DAV:}$prop"]; - Assert::assertEquals($property, $value); + if ($prefix === 'oc') { + $prefix = '{http://owncloud.org/ns}'; + } else { + $prefix = '{DAV:}'; + } + $property = $elementList['/' . $this->getDavFilesPath($this->currentUser) . $file][200]["$prefix$prop"]; + Assert::assertEquals($value, $property); } /** diff --git a/tests/Integration/features/mimetypes.feature b/tests/Integration/features/mimetypes.feature index 63049fd5..dc05adf6 100644 --- a/tests/Integration/features/mimetypes.feature +++ b/tests/Integration/features/mimetypes.feature @@ -5,7 +5,7 @@ Given as user "test1" And using new dav path - Scenario: Can properly block path detected mimetypes for application/javscript + Scenario: Can properly block path detected mimetypes for application/javascript And user "admin" creates global flow with 200 | name | Admin flow | | class | OCA\FilesAccessControl\Operation | diff --git a/tests/Integration/features/sharing-user.feature b/tests/Integration/features/sharing-user.feature index 577988be..fe8d6418 100644 --- a/tests/Integration/features/sharing-user.feature +++ b/tests/Integration/features/sharing-user.feature @@ -172,3 +172,35 @@ Feature: Sharing user And The webdav response should have a status code "404" And user "test2" should see following elements | /nextcloud2.txt | + + Scenario: Downloading is still blocked when Secure View is enabled + Given the following files app config is set + | watermark_enabled | yes | + Given User "test1" uploads file "data/textfile.txt" to "/foobar.txt" + And The webdav response should have a status code "201" + And user "test1" shares file "/foobar.txt" with user "test2" + And as user "test2" + When File "/foobar.txt" should have prop "oc:permissions" equal to "SRGDNVW" + When Downloading file "/foobar.txt" + Then The webdav response should have a status code "200" + When Downloading file "/foobar.txt" with range "1-4" + Then The webdav response should have a status code "200" + And user "test1" shares file "/foobar.txt" publicly + And as user "test2" + When Downloading last public shared file with range "1-4" + Then The webdav response should have a status code "200" + And user "admin" creates global flow with 200 + | name | Admin flow | + | class | OCA\FilesAccessControl\Operation | + | entity | OCA\WorkflowEngine\Entity\File | + | events | [] | + | operation | deny | + | checks-0 | {"class":"OCA\\WorkflowEngine\\Check\\FileMimeType", "operator": "is", "value": "text/plain"} | + And as user "test2" + When File "/foobar.txt" should have prop "oc:permissions" equal to "SRD" + When Downloading file "/foobar.txt" + Then The webdav response should have a status code "404" + When Downloading file "/foobar.txt" with range "1-4" + Then The webdav response should have a status code "404" + When Downloading last public shared file with range "1-4" + Then The webdav response should have a status code "404" diff --git a/tests/Integration/run.sh b/tests/Integration/run.sh index a8d5910d..e6ab34b1 100755 --- a/tests/Integration/run.sh +++ b/tests/Integration/run.sh @@ -16,6 +16,7 @@ composer install cp -R ./app "../../../${APP_NAME}_testing" ${ROOT_DIR}/occ app:enable $APP_NAME ${ROOT_DIR}/occ app:enable --force "${APP_NAME}_testing" +${ROOT_DIR}/occ app:enable --force richdocuments ${ROOT_DIR}/occ app:list | grep $APP_NAME export TEST_SERVER_URL="http://localhost:8080/"