diff options
Diffstat (limited to 'apps/files_trashbin/lib/Storage.php')
-rw-r--r-- | apps/files_trashbin/lib/Storage.php | 368 |
1 files changed, 128 insertions, 240 deletions
diff --git a/apps/files_trashbin/lib/Storage.php b/apps/files_trashbin/lib/Storage.php index 5eaf502f236..82b7af5a934 100644 --- a/apps/files_trashbin/lib/Storage.php +++ b/apps/files_trashbin/lib/Storage.php @@ -1,242 +1,102 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Vincent Petry <pvince81@owncloud.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_Trashbin; use OC\Files\Filesystem; use OC\Files\Storage\Wrapper\Wrapper; -use OC\Files\View; use OCA\Files_Trashbin\Events\MoveToTrashEvent; +use OCA\Files_Trashbin\Trash\ITrashManager; +use OCP\App\IAppManager; use OCP\Encryption\Exceptions\GenericEncryptionException; +use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\IRootFolder; use OCP\Files\Node; -use OCP\ILogger; +use OCP\Files\Storage\IStorage; +use OCP\IRequest; use OCP\IUserManager; -use Symfony\Component\EventDispatcher\EventDispatcher; +use OCP\Server; +use Psr\Log\LoggerInterface; class Storage extends Wrapper { - - private $mountPoint; - // remember already deleted files to avoid infinite loops if the trash bin - // move files across storages - private $deletedFiles = array(); - - /** - * Disable trash logic - * - * @var bool - */ - private static $disableTrash = false; - - /** - * remember which file/folder was moved out of s shared folder - * in this case we want to add a copy to the owners trash bin - * - * @var array - */ - private static $moveOutOfSharedFolder = []; - - /** @var IUserManager */ - private $userManager; - - /** @var ILogger */ - private $logger; - - /** @var EventDispatcher */ - private $eventDispatcher; - - /** @var IRootFolder */ - private $rootFolder; + private string $mountPoint; + private bool $trashEnabled = true; /** * Storage constructor. - * * @param array $parameters - * @param IUserManager|null $userManager - * @param ILogger|null $logger - * @param EventDispatcher|null $eventDispatcher - * @param IRootFolder|null $rootFolder */ - public function __construct($parameters, - IUserManager $userManager = null, - ILogger $logger = null, - EventDispatcher $eventDispatcher = null, - IRootFolder $rootFolder = null) { + public function __construct( + $parameters, + private ?ITrashManager $trashManager = null, + private ?IUserManager $userManager = null, + private ?LoggerInterface $logger = null, + private ?IEventDispatcher $eventDispatcher = null, + private ?IRootFolder $rootFolder = null, + private ?IRequest $request = null, + ) { $this->mountPoint = $parameters['mountPoint']; - $this->userManager = $userManager; - $this->logger = $logger; - $this->eventDispatcher = $eventDispatcher; - $this->rootFolder = $rootFolder; parent::__construct($parameters); } - /** - * @internal - */ - public static function preRenameHook($params) { - // in cross-storage cases, a rename is a copy + unlink, - // that last unlink must not go to trash, only exception: - // if the file was moved from a shared storage to a local folder, - // in this case the owner should get a copy in his trash bin so that - // they can restore the files again - - $oldPath = $params['oldpath']; - $newPath = dirname($params['newpath']); - $currentUser = \OC::$server->getUserSession()->getUser(); - - $fileMovedOutOfSharedFolder = false; - - try { - if ($currentUser) { - $currentUserId = $currentUser->getUID(); - - $view = new View($currentUserId . '/files'); - $fileInfo = $view->getFileInfo($oldPath); - if ($fileInfo) { - $sourceStorage = $fileInfo->getStorage(); - $sourceOwner = $view->getOwner($oldPath); - $targetOwner = $view->getOwner($newPath); - - if ($sourceOwner !== $targetOwner - && $sourceStorage->instanceOfStorage('OCA\Files_Sharing\SharedStorage') - ) { - $fileMovedOutOfSharedFolder = true; - } - } + public function unlink(string $path): bool { + if ($this->trashEnabled) { + try { + return $this->doDelete($path, 'unlink'); + } catch (GenericEncryptionException $e) { + // in case of a encryption exception we delete the file right away + $this->logger->info( + "Can't move file " . $path + . ' to the trash bin, therefore it was deleted right away'); + + return $this->storage->unlink($path); } - } catch (\Exception $e) { - // do nothing, in this case we just disable the trashbin and continue - $logger = \OC::$server->getLogger(); - $logger->debug('Trashbin storage could not check if a file was moved out of a shared folder: ' . $e->getMessage()); - } - - if($fileMovedOutOfSharedFolder) { - self::$moveOutOfSharedFolder['/' . $currentUserId . '/files' . $oldPath] = true; } else { - self::$disableTrash = true; - } - - } - - /** - * @internal - */ - public static function postRenameHook($params) { - self::$disableTrash = false; - } - - /** - * Rename path1 to path2 by calling the wrapped storage. - * - * @param string $path1 first path - * @param string $path2 second path - * @return bool - */ - public function rename($path1, $path2) { - $result = $this->storage->rename($path1, $path2); - if ($result === false) { - // when rename failed, the post_rename hook isn't triggered, - // but we still want to reenable the trash logic - self::$disableTrash = false; + return $this->storage->unlink($path); } - return $result; } - /** - * Deletes the given file by moving it into the trashbin. - * - * @param string $path path of file or folder to delete - * - * @return bool true if the operation succeeded, false otherwise - */ - public function unlink($path) { - try { - if (isset(self::$moveOutOfSharedFolder[$this->mountPoint . $path])) { - $result = $this->doDelete($path, 'unlink', true); - unset(self::$moveOutOfSharedFolder[$this->mountPoint . $path]); - } else { - $result = $this->doDelete($path, 'unlink'); - } - } catch (GenericEncryptionException $e) { - // in case of a encryption exception we delete the file right away - $this->logger->info( - "Can't move file" . $path . - "to the trash bin, therefore it was deleted right away"); - - $result = $this->storage->unlink($path); - } - - return $result; - } - - /** - * Deletes the given folder by moving it into the trashbin. - * - * @param string $path path of folder to delete - * - * @return bool true if the operation succeeded, false otherwise - */ - public function rmdir($path) { - if (isset(self::$moveOutOfSharedFolder[$this->mountPoint . $path])) { - $result = $this->doDelete($path, 'rmdir', true); - unset(self::$moveOutOfSharedFolder[$this->mountPoint . $path]); + public function rmdir(string $path): bool { + if ($this->trashEnabled) { + return $this->doDelete($path, 'rmdir'); } else { - $result = $this->doDelete($path, 'rmdir'); + return $this->storage->rmdir($path); } - - return $result; } /** * check if it is a file located in data/user/files only files in the * 'files' directory should be moved to the trash - * - * @param $path - * @return bool */ - protected function shouldMoveToTrash($path){ + protected function shouldMoveToTrash(string $path): bool { + $normalized = Filesystem::normalizePath($this->mountPoint . '/' . $path); + $parts = explode('/', $normalized); + if (count($parts) < 4 || strpos($normalized, '/appdata_') === 0) { + return false; + } // check if there is a app which want to disable the trash bin for this file $fileId = $this->storage->getCache()->getId($path); - $nodes = $this->rootFolder->getById($fileId); + $owner = $this->storage->getOwner($path); + if ($owner === false || $this->storage->instanceOfStorage(\OCA\Files_Sharing\External\Storage::class)) { + $nodes = $this->rootFolder->getById($fileId); + } else { + $nodes = $this->rootFolder->getUserFolder($owner)->getById($fileId); + } + foreach ($nodes as $node) { $event = $this->createMoveToTrashEvent($node); + $this->eventDispatcher->dispatchTyped($event); $this->eventDispatcher->dispatch('OCA\Files_Trashbin::moveToTrash', $event); if ($event->shouldMoveToTrashBin() === false) { return false; } } - $normalized = Filesystem::normalizePath($this->mountPoint . '/' . $path); - $parts = explode('/', $normalized); - if (count($parts) < 4) { - return false; - } - if ($parts[2] === 'files' && $this->userManager->userExists($parts[1])) { return true; } @@ -250,9 +110,8 @@ class Storage extends Wrapper { * @param Node $node * @return MoveToTrashEvent */ - protected function createMoveToTrashEvent(Node $node) { - $event = new MoveToTrashEvent($node); - return $event; + protected function createMoveToTrashEvent(Node $node): MoveToTrashEvent { + return new MoveToTrashEvent($node); } /** @@ -260,62 +119,91 @@ class Storage extends Wrapper { * * @param string $path path of file or folder to delete * @param string $method either "unlink" or "rmdir" - * @param bool $ownerOnly delete for owner only (if file gets moved out of a shared folder) * * @return bool true if the operation succeeded, false otherwise */ - private function doDelete($path, $method, $ownerOnly = false) { - if (self::$disableTrash - || !\OC_App::isEnabled('files_trashbin') - || (pathinfo($path, PATHINFO_EXTENSION) === 'part') - || $this->shouldMoveToTrash($path) === false - ) { - return call_user_func_array([$this->storage, $method], [$path]); - } - - // check permissions before we continue, this is especially important for - // shared files - if (!$this->isDeletable($path)) { - return false; - } + private function doDelete(string $path, string $method): bool { + $isTrashbinEnabled = Server::get(IAppManager::class)->isEnabledForUser('files_trashbin'); + $isPartFile = pathinfo($path, PATHINFO_EXTENSION) === 'part'; + $isSkipTrashHeaderSet = $this->request !== null && $this->request->getHeader('X-NC-Skip-Trashbin') === 'true'; + // We keep the shouldMoveToTrash call at the end to prevent emitting unnecessary event. + $shouldMoveToTrash = $isTrashbinEnabled && !$isPartFile && !$isSkipTrashHeaderSet && $this->shouldMoveToTrash($path); + + if ($shouldMoveToTrash) { + // check permissions before we continue, this is especially important for + // shared files + if (!$this->isDeletable($path)) { + return false; + } - $normalized = Filesystem::normalizePath($this->mountPoint . '/' . $path, true, false, true); - $result = true; - $view = Filesystem::getView(); - if (!isset($this->deletedFiles[$normalized]) && $view instanceof View) { - $this->deletedFiles[$normalized] = $normalized; - if ($filesPath = $view->getRelativePath($normalized)) { - $filesPath = trim($filesPath, '/'); - $result = \OCA\Files_Trashbin\Trashbin::move2trash($filesPath, $ownerOnly); - // in cross-storage cases the file will be copied - // but not deleted, so we delete it here - if ($result) { - call_user_func_array([$this->storage, $method], [$path]); - } - } else { - $result = call_user_func_array([$this->storage, $method], [$path]); + $isMovedToTrash = $this->trashManager->moveToTrash($this, $path); + if ($isMovedToTrash) { + return true; } - unset($this->deletedFiles[$normalized]); - } else if ($this->storage->file_exists($path)) { - $result = call_user_func_array([$this->storage, $method], [$path]); } - return $result; + return call_user_func([$this->storage, $method], $path); } /** - * Setup the storate wrapper callback + * Setup the storage wrapper callback */ - public static function setupStorage() { - \OC\Files\Filesystem::addStorageWrapper('oc_trashbin', function ($mountPoint, $storage) { - return new \OCA\Files_Trashbin\Storage( - array('storage' => $storage, 'mountPoint' => $mountPoint), - \OC::$server->getUserManager(), - \OC::$server->getLogger(), - \OC::$server->getEventDispatcher(), - \OC::$server->getLazyRootFolder() - ); - }, 1); + public static function setupStorage(): void { + $trashManager = Server::get(ITrashManager::class); + $userManager = Server::get(IUserManager::class); + $logger = Server::get(LoggerInterface::class); + $eventDispatcher = Server::get(IEventDispatcher::class); + $rootFolder = Server::get(IRootFolder::class); + $request = Server::get(IRequest::class); + Filesystem::addStorageWrapper( + 'oc_trashbin', + function (string $mountPoint, IStorage $storage) use ($trashManager, $userManager, $logger, $eventDispatcher, $rootFolder, $request) { + return new Storage( + ['storage' => $storage, 'mountPoint' => $mountPoint], + $trashManager, + $userManager, + $logger, + $eventDispatcher, + $rootFolder, + $request, + ); + }, + 1); + } + + public function getMountPoint() { + return $this->mountPoint; } + public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { + $sourceIsTrashbin = $sourceStorage->instanceOfStorage(Storage::class); + try { + // the fallback for moving between storage involves a copy+delete + // we don't want to trigger the trashbin when doing the delete + if ($sourceIsTrashbin) { + /** @var Storage $sourceStorage */ + $sourceStorage->disableTrash(); + } + $result = parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); + if ($sourceIsTrashbin) { + /** @var Storage $sourceStorage */ + $sourceStorage->enableTrash(); + } + return $result; + } catch (\Exception $e) { + if ($sourceIsTrashbin) { + /** @var Storage $sourceStorage */ + $sourceStorage->enableTrash(); + } + throw $e; + } + } + + protected function disableTrash(): void { + $this->trashEnabled = false; + } + + protected function enableTrash(): void { + $this->trashEnabled = true; + } } |