diff options
Diffstat (limited to 'apps/files_sharing/lib/SharedStorage.php')
-rw-r--r-- | apps/files_sharing/lib/SharedStorage.php | 388 |
1 files changed, 217 insertions, 171 deletions
diff --git a/apps/files_sharing/lib/SharedStorage.php b/apps/files_sharing/lib/SharedStorage.php index 221d022bb1e..e310c5f3138 100644 --- a/apps/files_sharing/lib/SharedStorage.php +++ b/apps/files_sharing/lib/SharedStorage.php @@ -1,64 +1,59 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bart Visscher <bartv@thisnet.nl> - * @author Björn Schießle <bjoern@schiessle.org> - * @author J0WI <J0WI@users.noreply.github.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author scambra <sergio@entrecables.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files_Sharing; +use OC\Files\Cache\CacheDependencies; +use OC\Files\Cache\CacheEntry; use OC\Files\Cache\FailedCache; use OC\Files\Cache\NullWatcher; -use OC\Files\Filesystem; +use OC\Files\ObjectStore\HomeObjectStoreStorage; +use OC\Files\Storage\Common; use OC\Files\Storage\FailedStorage; +use OC\Files\Storage\Home; +use OC\Files\Storage\Storage; +use OC\Files\Storage\Wrapper\Jail; use OC\Files\Storage\Wrapper\PermissionsMask; +use OC\Files\Storage\Wrapper\Wrapper; +use OC\Files\View; +use OC\Share\Share; use OC\User\NoUserException; +use OCA\Files_Sharing\ISharedStorage as LegacyISharedStorage; use OCP\Constants; +use OCP\Files\Cache\ICache; use OCP\Files\Cache\ICacheEntry; +use OCP\Files\Cache\IScanner; +use OCP\Files\Cache\IWatcher; +use OCP\Files\Folder; +use OCP\Files\IHomeStorage; +use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; use OCP\Files\Storage\IDisableEncryptionStorage; +use OCP\Files\Storage\ILockingStorage; +use OCP\Files\Storage\ISharedStorage; use OCP\Files\Storage\IStorage; use OCP\Lock\ILockingProvider; +use OCP\Server; +use OCP\Share\IShare; +use OCP\Util; +use Psr\Log\LoggerInterface; /** * Convert target path to source path and pass the function call to the correct storage provider */ -class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedStorage, IDisableEncryptionStorage { - - /** @var \OCP\Share\IShare */ +class SharedStorage extends Jail implements LegacyISharedStorage, ISharedStorage, IDisableEncryptionStorage { + /** @var IShare */ private $superShare; - /** @var \OCP\Share\IShare[] */ + /** @var IShare[] */ private $groupedShares; /** - * @var \OC\Files\View + * @var View */ private $ownerView; @@ -72,29 +67,39 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto /** @var string */ private $user; - /** - * @var \OCP\ILogger - */ - private $logger; + private LoggerInterface $logger; - /** @var IStorage */ + /** @var IStorage */ private $nonMaskedStorage; - private $options; + private array $mountOptions = []; /** @var boolean */ private $sharingDisabledForUser; - public function __construct($arguments) { - $this->ownerView = $arguments['ownerView']; - $this->logger = \OC::$server->getLogger(); + /** @var ?Folder $ownerUserFolder */ + private $ownerUserFolder = null; - $this->superShare = $arguments['superShare']; - $this->groupedShares = $arguments['groupedShares']; + private string $sourcePath = ''; + + private static int $initDepth = 0; + + /** + * @psalm-suppress NonInvariantDocblockPropertyType + * @var ?Storage $storage + */ + protected $storage; - $this->user = $arguments['user']; - if (isset($arguments['sharingDisabledForUser'])) { - $this->sharingDisabledForUser = $arguments['sharingDisabledForUser']; + public function __construct(array $parameters) { + $this->ownerView = $parameters['ownerView']; + $this->logger = Server::get(LoggerInterface::class); + + $this->superShare = $parameters['superShare']; + $this->groupedShares = $parameters['groupedShares']; + + $this->user = $parameters['user']; + if (isset($parameters['sharingDisabledForUser'])) { + $this->sharingDisabledForUser = $parameters['sharingDisabledForUser']; } else { $this->sharingDisabledForUser = false; } @@ -120,20 +125,65 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto return $this->sourceRootInfo; } + /** + * @psalm-assert Storage $this->storage + */ private function init() { if ($this->initialized) { + if (!$this->storage) { + // marked as initialized but no storage set + // this is probably because some code path has caused recursion during the share setup + // we setup a "failed storage" so `getWrapperStorage` doesn't return null. + // If the share setup completes after this the "failed storage" will be overwritten by the correct one + $this->logger->warning('Possible share setup recursion detected'); + $this->storage = new FailedStorage(['exception' => new \Exception('Possible share setup recursion detected')]); + $this->cache = new FailedCache(); + $this->rootPath = ''; + } return; } + $this->initialized = true; + self::$initDepth++; + try { - Filesystem::initMountPoints($this->superShare->getShareOwner()); - $storageId = $this->superShare->getNodeCacheEntry() ? $this->superShare->getNodeCacheEntry()->getStorageId() : null; - $sourcePath = $this->ownerView->getPath($this->superShare->getNodeId(), $storageId); - [$this->nonMaskedStorage, $this->rootPath] = $this->ownerView->resolvePath($sourcePath); - $this->storage = new PermissionsMask([ - 'storage' => $this->nonMaskedStorage, - 'mask' => $this->superShare->getPermissions(), - ]); + if (self::$initDepth > 10) { + throw new \Exception('Maximum share depth reached'); + } + + /** @var IRootFolder $rootFolder */ + $rootFolder = Server::get(IRootFolder::class); + $this->ownerUserFolder = $rootFolder->getUserFolder($this->superShare->getShareOwner()); + $sourceId = $this->superShare->getNodeId(); + $ownerNodes = $this->ownerUserFolder->getById($sourceId); + + if (count($ownerNodes) === 0) { + $this->storage = new FailedStorage(['exception' => new NotFoundException("File by id $sourceId not found")]); + $this->cache = new FailedCache(); + $this->rootPath = ''; + } else { + foreach ($ownerNodes as $ownerNode) { + $nonMaskedStorage = $ownerNode->getStorage(); + + // check if potential source node would lead to a recursive share setup + if ($nonMaskedStorage instanceof Wrapper && $nonMaskedStorage->isWrapperOf($this)) { + continue; + } + $this->nonMaskedStorage = $nonMaskedStorage; + $this->sourcePath = $ownerNode->getPath(); + $this->rootPath = $ownerNode->getInternalPath(); + $this->cache = null; + break; + } + if (!$this->nonMaskedStorage) { + // all potential source nodes would have been recursive + throw new \Exception('recursive share detected'); + } + $this->storage = new PermissionsMask([ + 'storage' => $this->nonMaskedStorage, + 'mask' => $this->superShare->getPermissions(), + ]); + } } catch (NotFoundException $e) { // original file not accessible or deleted, set FailedStorage $this->storage = new FailedStorage(['exception' => $e]); @@ -148,22 +198,27 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto $this->storage = new FailedStorage(['exception' => $e]); $this->cache = new FailedCache(); $this->rootPath = ''; - $this->logger->logException($e); + $this->logger->error($e->getMessage(), ['exception' => $e]); } if (!$this->nonMaskedStorage) { $this->nonMaskedStorage = $this->storage; } + self::$initDepth--; } - /** - * @inheritdoc - */ - public function instanceOfStorage($class) { - if ($class === '\OC\Files\Storage\Common') { + public function instanceOfStorage(string $class): bool { + if ($class === '\OC\Files\Storage\Common' || $class == Common::class) { return true; } - if (in_array($class, ['\OC\Files\Storage\Home', '\OC\Files\ObjectStore\HomeObjectStoreStorage', '\OCP\Files\IHomeStorage'])) { + if (in_array($class, [ + '\OC\Files\Storage\Home', + '\OC\Files\ObjectStore\HomeObjectStoreStorage', + '\OCP\Files\IHomeStorage', + Home::class, + HomeObjectStoreStorage::class, + IHomeStorage::class + ])) { return false; } return parent::instanceOfStorage($class); @@ -176,48 +231,37 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto return $this->superShare->getId(); } - private function isValid() { + private function isValid(): bool { return $this->getSourceRootInfo() && ($this->getSourceRootInfo()->getPermissions() & Constants::PERMISSION_SHARE) === Constants::PERMISSION_SHARE; } - /** - * get id of the mount point - * - * @return string - */ - public function getId() { + public function getId(): string { return 'shared::' . $this->getMountPoint(); } - /** - * Get the permissions granted for a shared file - * - * @param string $target Shared target file path - * @return int CRUDS permissions granted - */ - public function getPermissions($target = '') { + public function getPermissions(string $path = ''): int { if (!$this->isValid()) { return 0; } - $permissions = parent::getPermissions($target) & $this->superShare->getPermissions(); + $permissions = parent::getPermissions($path) & $this->superShare->getPermissions(); // part files and the mount point always have delete permissions - if ($target === '' || pathinfo($target, PATHINFO_EXTENSION) === 'part') { - $permissions |= \OCP\Constants::PERMISSION_DELETE; + if ($path === '' || pathinfo($path, PATHINFO_EXTENSION) === 'part') { + $permissions |= Constants::PERMISSION_DELETE; } if ($this->sharingDisabledForUser) { - $permissions &= ~\OCP\Constants::PERMISSION_SHARE; + $permissions &= ~Constants::PERMISSION_SHARE; } return $permissions; } - public function isCreatable($path) { - return ($this->getPermissions($path) & \OCP\Constants::PERMISSION_CREATE); + public function isCreatable(string $path): bool { + return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE); } - public function isReadable($path) { + public function isReadable(string $path): bool { if (!$this->isValid()) { return false; } @@ -230,22 +274,22 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto return $storage->isReadable($internalPath); } - public function isUpdatable($path) { - return ($this->getPermissions($path) & \OCP\Constants::PERMISSION_UPDATE); + public function isUpdatable(string $path): bool { + return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE); } - public function isDeletable($path) { - return ($this->getPermissions($path) & \OCP\Constants::PERMISSION_DELETE); + public function isDeletable(string $path): bool { + return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE); } - public function isSharable($path) { - if (\OCP\Util::isSharingDisabledForUser() || !\OC\Share\Share::isResharingAllowed()) { + public function isSharable(string $path): bool { + if (Util::isSharingDisabledForUser() || !Share::isResharingAllowed()) { return false; } - return ($this->getPermissions($path) & \OCP\Constants::PERMISSION_SHARE); + return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE); } - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { $source = $this->getUnjailedPath($path); switch ($mode) { case 'r+': @@ -293,22 +337,15 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto 'source' => $source, 'mode' => $mode, ]; - \OCP\Util::emitHook('\OC\Files\Storage\Shared', 'fopen', $info); + Util::emitHook('\OC\Files\Storage\Shared', 'fopen', $info); return $this->nonMaskedStorage->fopen($this->getUnjailedPath($path), $mode); } - /** - * see https://www.php.net/manual/en/function.rename.php - * - * @param string $path1 - * @param string $path2 - * @return bool - */ - public function rename($path1, $path2) { + public function rename(string $source, string $target): bool { $this->init(); - $isPartFile = pathinfo($path1, PATHINFO_EXTENSION) === 'part'; - $targetExists = $this->file_exists($path2); - $sameFolder = dirname($path1) === dirname($path2); + $isPartFile = pathinfo($source, PATHINFO_EXTENSION) === 'part'; + $targetExists = $this->file_exists($target); + $sameFolder = dirname($source) === dirname($target); if ($targetExists || ($sameFolder && !$isPartFile)) { if (!$this->isUpdatable('')) { @@ -320,7 +357,7 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto } } - return $this->nonMaskedStorage->rename($this->getUnjailedPath($path1), $this->getUnjailedPath($path2)); + return $this->nonMaskedStorage->rename($this->getUnjailedPath($source), $this->getUnjailedPath($target)); } /** @@ -328,14 +365,11 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto * * @return string */ - public function getMountPoint() { + public function getMountPoint(): string { return $this->superShare->getTarget(); } - /** - * @param string $path - */ - public function setMountPoint($path) { + public function setMountPoint(string $path): void { $this->superShare->setTarget($path); foreach ($this->groupedShares as $share) { @@ -348,14 +382,11 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto * * @return string */ - public function getSharedFrom() { + public function getSharedFrom(): string { return $this->superShare->getShareOwner(); } - /** - * @return \OCP\Share\IShare - */ - public function getShare() { + public function getShare(): IShare { return $this->superShare; } @@ -364,16 +395,11 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto * * @return string */ - public function getItemType() { + public function getItemType(): string { return $this->superShare->getNodeType(); } - /** - * @param string $path - * @param null $storage - * @return Cache - */ - public function getCache($path = '', $storage = null) { + public function getCache(string $path = '', ?IStorage $storage = null): ICache { if ($this->cache) { return $this->cache; } @@ -385,24 +411,50 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto return new FailedCache(); } - $this->cache = new \OCA\Files_Sharing\Cache($storage, $sourceRoot, $this->superShare); + $this->cache = new Cache( + $storage, + $sourceRoot, + Server::get(CacheDependencies::class), + $this->getShare() + ); return $this->cache; } - public function getScanner($path = '', $storage = null) { + public function getScanner(string $path = '', ?IStorage $storage = null): IScanner { if (!$storage) { $storage = $this; } - return new \OCA\Files_Sharing\Scanner($storage); + return new Scanner($storage); } - public function getOwner($path) { + public function getOwner(string $path): string|false { return $this->superShare->getShareOwner(); } - public function getWatcher($path = '', $storage = null) { + public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher { + if ($this->watcher) { + return $this->watcher; + } + + // Get node information + $node = $this->getShare()->getNodeCacheEntry(); + if ($node instanceof CacheEntry) { + $storageId = $node->getData()['storage_string_id'] ?? null; + // for shares from the home storage we can rely on the home storage to keep itself up to date + // for other storages we need use the proper watcher + if ($storageId !== null && !(str_starts_with($storageId, 'home::') || str_starts_with($storageId, 'object::user'))) { + $cache = $this->getCache(); + $this->watcher = parent::getWatcher($path, $storage); + if ($cache instanceof Cache) { + $this->watcher->onUpdate($cache->markRootChanged(...)); + } + return $this->watcher; + } + } + // cache updating is handled by the share source - return new NullWatcher(); + $this->watcher = new NullWatcher(); + return $this->watcher; } /** @@ -410,61 +462,42 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto * * @return bool */ - public function unshareStorage() { + public function unshareStorage(): bool { foreach ($this->groupedShares as $share) { - \OC::$server->getShareManager()->deleteFromSelf($share, $this->user); + Server::get(\OCP\Share\IManager::class)->deleteFromSelf($share, $this->user); } return true; } - /** - * @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, ILockingProvider $provider) { - /** @var \OCP\Files\Storage $targetStorage */ + public function acquireLock(string $path, int $type, ILockingProvider $provider): void { + /** @var ILockingStorage $targetStorage */ [$targetStorage, $targetInternalPath] = $this->resolvePath($path); $targetStorage->acquireLock($targetInternalPath, $type, $provider); // lock the parent folders of the owner when locking the share as recipient if ($path === '') { - $sourcePath = $this->ownerView->getPath($this->superShare->getNodeId()); + $sourcePath = $this->ownerUserFolder->getRelativePath($this->sourcePath); $this->ownerView->lockFile(dirname($sourcePath), ILockingProvider::LOCK_SHARED, true); } } - /** - * @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, ILockingProvider $provider) { - /** @var \OCP\Files\Storage $targetStorage */ + public function releaseLock(string $path, int $type, ILockingProvider $provider): void { + /** @var ILockingStorage $targetStorage */ [$targetStorage, $targetInternalPath] = $this->resolvePath($path); $targetStorage->releaseLock($targetInternalPath, $type, $provider); // unlock the parent folders of the owner when unlocking the share as recipient if ($path === '') { - $sourcePath = $this->ownerView->getPath($this->superShare->getNodeId()); + $sourcePath = $this->ownerUserFolder->getRelativePath($this->sourcePath); $this->ownerView->unlockFile(dirname($sourcePath), ILockingProvider::LOCK_SHARED, true); } } - /** - * @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, ILockingProvider $provider) { - /** @var \OCP\Files\Storage $targetStorage */ + public function changeLock(string $path, int $type, ILockingProvider $provider): void { + /** @var ILockingStorage $targetStorage */ [$targetStorage, $targetInternalPath] = $this->resolvePath($path); $targetStorage->changeLock($targetInternalPath, $type, $provider); } - /** - * @return array [ available, last_checked ] - */ - public function getAvailability() { + public function getAvailability(): array { // shares do not participate in availability logic return [ 'available' => true, @@ -472,10 +505,7 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto ]; } - /** - * @param bool $available - */ - public function setAvailability($available) { + public function setAvailability(bool $isAvailable): void { // shares do not participate in availability logic } @@ -484,35 +514,51 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto return $this->nonMaskedStorage; } - public function getWrapperStorage() { + public function getWrapperStorage(): Storage { $this->init(); + + /** + * @psalm-suppress DocblockTypeContradiction + */ + if (!$this->storage) { + $message = 'no storage set after init for share ' . $this->getShareId(); + $this->logger->error($message); + $this->storage = new FailedStorage(['exception' => new \Exception($message)]); + } + return $this->storage; } - public function file_get_contents($path) { + public function file_get_contents(string $path): string|false { $info = [ 'target' => $this->getMountPoint() . '/' . $path, 'source' => $this->getUnjailedPath($path), ]; - \OCP\Util::emitHook('\OC\Files\Storage\Shared', 'file_get_contents', $info); + Util::emitHook('\OC\Files\Storage\Shared', 'file_get_contents', $info); return parent::file_get_contents($path); } - public function file_put_contents($path, $data) { + public function file_put_contents(string $path, mixed $data): int|float|false { $info = [ 'target' => $this->getMountPoint() . '/' . $path, 'source' => $this->getUnjailedPath($path), ]; - \OCP\Util::emitHook('\OC\Files\Storage\Shared', 'file_put_contents', $info); + Util::emitHook('\OC\Files\Storage\Shared', 'file_put_contents', $info); return parent::file_put_contents($path, $data); } - public function setMountOptions(array $options) { + public function setMountOptions(array $options): void { + /* Note: This value is never read */ $this->mountOptions = $options; } - public function getUnjailedPath($path) { + public function getUnjailedPath(string $path): string { $this->init(); return parent::getUnjailedPath($path); } + + public function getDirectDownload(string $path): array|false { + // disable direct download for shares + return []; + } } |