diff options
Diffstat (limited to 'apps/files_trashbin/lib/Trashbin.php')
-rw-r--r-- | apps/files_trashbin/lib/Trashbin.php | 705 |
1 files changed, 449 insertions, 256 deletions
diff --git a/apps/files_trashbin/lib/Trashbin.php b/apps/files_trashbin/lib/Trashbin.php index d61881ce3b1..667066c2fca 100644 --- a/apps/files_trashbin/lib/Trashbin.php +++ b/apps/files_trashbin/lib/Trashbin.php @@ -1,78 +1,69 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bart Visscher <bartv@thisnet.nl> - * @author Bastien Ho <bastienho@urbancube.fr> - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Florin Peter <github@florin-peter.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Juan Pablo Villafáñez <jvillafanez@solidgear.es> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Qingping Hou <dave2008713@gmail.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sjors van der Pluijm <sjors@desjors.nl> - * @author Steven Bühner <buehner@me.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Victor Dubiniuk <dubiniuk@owncloud.com> - * @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 Exception; +use OC\Files\Cache\Cache; +use OC\Files\Cache\CacheEntry; +use OC\Files\Cache\CacheQueryBuilder; use OC\Files\Filesystem; +use OC\Files\Node\NonExistingFile; +use OC\Files\Node\NonExistingFolder; use OC\Files\View; +use OC\User\NoUserException; +use OC_User; use OCA\Files_Trashbin\AppInfo\Application; use OCA\Files_Trashbin\Command\Expire; +use OCA\Files_Trashbin\Events\BeforeNodeRestoredEvent; +use OCA\Files_Trashbin\Events\NodeRestoredEvent; +use OCA\Files_Trashbin\Exceptions\CopyRecursiveException; +use OCA\Files_Versions\Storage; +use OCP\App\IAppManager; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Command\IBus; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Events\Node\BeforeNodeDeletedEvent; use OCP\Files\File; use OCP\Files\Folder; +use OCP\Files\IMimeTypeLoader; +use OCP\Files\IRootFolder; +use OCP\Files\Node; use OCP\Files\NotFoundException; -use OCP\User; - -class Trashbin { - +use OCP\Files\NotPermittedException; +use OCP\Files\Storage\ILockingStorage; +use OCP\Files\Storage\IStorage; +use OCP\FilesMetadata\IFilesMetadataManager; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\Lock\ILockingProvider; +use OCP\Lock\LockedException; +use OCP\Server; +use OCP\Util; +use Psr\Log\LoggerInterface; + +/** @template-implements IEventListener<BeforeNodeDeletedEvent> */ +class Trashbin implements IEventListener { // unit: percentage; 50% of available disk space/quota - const DEFAULTMAXSIZE = 50; - - /** - * Whether versions have already be rescanned during this PHP request - * - * @var bool - */ - private static $scannedVersions = false; + public const DEFAULTMAXSIZE = 50; /** * Ensure we don't need to scan the file during the move to trash * by triggering the scan in the pre-hook - * - * @param array $params */ - public static function ensureFileScannedHook($params) { + public static function ensureFileScannedHook(Node $node): void { try { - self::getUidAndFilename($params['path']); + self::getUidAndFilename($node->getPath()); } catch (NotFoundException $e) { - // nothing to scan for non existing files + // Nothing to scan for non existing files } } @@ -82,23 +73,23 @@ class Trashbin { * * @param string $filename * @return array - * @throws \OC\User\NoUserException + * @throws NoUserException */ public static function getUidAndFilename($filename) { $uid = Filesystem::getOwner($filename); - $userManager = \OC::$server->getUserManager(); + $userManager = Server::get(IUserManager::class); // if the user with the UID doesn't exists, e.g. because the UID points // to a remote user with a federated cloud ID we use the current logged-in // user. We need a valid local user to move the file to the right trash bin if (!$userManager->userExists($uid)) { - $uid = User::getUser(); + $uid = OC_User::getUser(); } if (!$uid) { // no owner, usually because of share link from ext storage return [null, null]; } Filesystem::initMountPoints($uid); - if ($uid !== User::getUser()) { + if ($uid !== OC_User::getUser()) { $info = Filesystem::getFileInfo($filename); $ownerView = new View('/' . $uid . '/files'); try { @@ -111,23 +102,25 @@ class Trashbin { } /** - * get original location of files for user + * get original location and deleted by of files for user * * @param string $user - * @return array (filename => array (timestamp => original location)) - */ - public static function getLocations($user) { - $query = \OC_DB::prepare('SELECT `id`, `timestamp`, `location`' - . ' FROM `*PREFIX*files_trash` WHERE `user`=?'); - $result = $query->execute(array($user)); - $array = array(); - while ($row = $result->fetchRow()) { - if (isset($array[$row['id']])) { - $array[$row['id']][$row['timestamp']] = $row['location']; - } else { - $array[$row['id']] = array($row['timestamp'] => $row['location']); - } + * @return array<string, array<string, array{location: string, deletedBy: string}>> + */ + public static function getExtraData($user) { + $query = Server::get(IDBConnection::class)->getQueryBuilder(); + $query->select('id', 'timestamp', 'location', 'deleted_by') + ->from('files_trash') + ->where($query->expr()->eq('user', $query->createNamedParameter($user))); + $result = $query->executeQuery(); + $array = []; + while ($row = $result->fetch()) { + $array[$row['id']][$row['timestamp']] = [ + 'location' => (string)$row['location'], + 'deletedBy' => (string)$row['deleted_by'], + ]; } + $result->closeCursor(); return $array; } @@ -137,20 +130,29 @@ class Trashbin { * @param string $user * @param string $filename * @param string $timestamp - * @return string original location + * @return string|false original location */ public static function getLocation($user, $filename, $timestamp) { - $query = \OC_DB::prepare('SELECT `location` FROM `*PREFIX*files_trash`' - . ' WHERE `user`=? AND `id`=? AND `timestamp`=?'); - $result = $query->execute(array($user, $filename, $timestamp))->fetchAll(); - if (isset($result[0]['location'])) { - return $result[0]['location']; + $query = Server::get(IDBConnection::class)->getQueryBuilder(); + $query->select('location') + ->from('files_trash') + ->where($query->expr()->eq('user', $query->createNamedParameter($user))) + ->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename))) + ->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp))); + + $result = $query->executeQuery(); + $row = $result->fetch(); + $result->closeCursor(); + + if (isset($row['location'])) { + return $row['location']; } else { return false; } } - private static function setUpTrash($user) { + /** @param string $user */ + private static function setUpTrash($user): void { $view = new View('/' . $user); if (!$view->is_dir('files_trashbin')) { $view->mkdir('files_trashbin'); @@ -173,10 +175,10 @@ class Trashbin { * @param string $sourcePath * @param string $owner * @param string $targetPath - * @param $user - * @param integer $timestamp + * @param string $user + * @param int $timestamp */ - private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp) { + private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp): void { self::setUpTrash($owner); $targetFilename = basename($targetPath); @@ -186,16 +188,27 @@ class Trashbin { $view = new View('/'); - $target = $user . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp; - $source = $owner . '/files_trashbin/files/' . $sourceFilename . '.d' . $timestamp; - self::copy_recursive($source, $target, $view); + $target = $user . '/files_trashbin/files/' . static::getTrashFilename($targetFilename, $timestamp); + $source = $owner . '/files_trashbin/files/' . static::getTrashFilename($sourceFilename, $timestamp); + $free = $view->free_space($target); + $isUnknownOrUnlimitedFreeSpace = $free < 0; + $isEnoughFreeSpaceLeft = $view->filesize($source) < $free; + if ($isUnknownOrUnlimitedFreeSpace || $isEnoughFreeSpaceLeft) { + self::copy_recursive($source, $target, $view); + } if ($view->file_exists($target)) { - $query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)"); - $result = $query->execute(array($targetFilename, $timestamp, $targetLocation, $user)); + $query = Server::get(IDBConnection::class)->getQueryBuilder(); + $query->insert('files_trash') + ->setValue('id', $query->createNamedParameter($targetFilename)) + ->setValue('timestamp', $query->createNamedParameter($timestamp)) + ->setValue('location', $query->createNamedParameter($targetLocation)) + ->setValue('user', $query->createNamedParameter($user)) + ->setValue('deleted_by', $query->createNamedParameter($user)); + $result = $query->executeStatement(); if (!$result) { - \OCP\Util::writeLog('files_trashbin', 'trash bin database couldn\'t be updated for the files owner', \OCP\Util::ERROR); + Server::get(LoggerInterface::class)->error('trash bin database couldn\'t be updated for the files owner', ['app' => 'files_trashbin']); } } } @@ -212,8 +225,8 @@ class Trashbin { public static function move2trash($file_path, $ownerOnly = false) { // get the user for which the filesystem is setup $root = Filesystem::getRoot(); - list(, $user) = explode('/', $root); - list($owner, $ownerPath) = self::getUidAndFilename($file_path); + [, $user] = explode('/', $root); + [$owner, $ownerPath] = self::getUidAndFilename($file_path); // if no owner found (ex: ext storage + share link), will use the current user's trashbin then if (is_null($owner)) { @@ -222,8 +235,15 @@ class Trashbin { } $ownerView = new View('/' . $owner); + // file has been deleted in between - if (is_null($ownerPath) || $ownerPath === '' || !$ownerView->file_exists('/files/' . $ownerPath)) { + if (is_null($ownerPath) || $ownerPath === '') { + return true; + } + + $sourceInfo = $ownerView->getFileInfo('/files/' . $ownerPath); + + if ($sourceInfo === false) { return true; } @@ -237,27 +257,57 @@ class Trashbin { $filename = $path_parts['basename']; $location = $path_parts['dirname']; - $timestamp = time(); + /** @var ITimeFactory $timeFactory */ + $timeFactory = Server::get(ITimeFactory::class); + $timestamp = $timeFactory->getTime(); + + $lockingProvider = Server::get(ILockingProvider::class); // disable proxy to prevent recursive calls - $trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp; + $trashPath = '/files_trashbin/files/' . static::getTrashFilename($filename, $timestamp); + $gotLock = false; + + do { + /** @var ILockingStorage & IStorage $trashStorage */ + [$trashStorage, $trashInternalPath] = $ownerView->resolvePath($trashPath); + try { + $trashStorage->acquireLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider); + $gotLock = true; + } catch (LockedException $e) { + // a file with the same name is being deleted concurrently + // nudge the timestamp a bit to resolve the conflict + + $timestamp = $timestamp + 1; + + $trashPath = '/files_trashbin/files/' . static::getTrashFilename($filename, $timestamp); + } + } while (!$gotLock); + + $sourceStorage = $sourceInfo->getStorage(); + $sourceInternalPath = $sourceInfo->getInternalPath(); + + if ($trashStorage->file_exists($trashInternalPath)) { + $trashStorage->unlink($trashInternalPath); + } + + $configuredTrashbinSize = static::getConfiguredTrashbinSize($owner); + if ($configuredTrashbinSize >= 0 && $sourceInfo->getSize() >= $configuredTrashbinSize) { + return false; + } - /** @var \OC\Files\Storage\Storage $trashStorage */ - list($trashStorage, $trashInternalPath) = $ownerView->resolvePath($trashPath); - /** @var \OC\Files\Storage\Storage $sourceStorage */ - list($sourceStorage, $sourceInternalPath) = $ownerView->resolvePath('/files/' . $ownerPath); try { $moveSuccessful = true; - if ($trashStorage->file_exists($trashInternalPath)) { - $trashStorage->unlink($trashInternalPath); - } + $trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath); - } catch (\OCA\Files_Trashbin\Exceptions\CopyRecursiveException $e) { + if ($sourceStorage->getCache()->inCache($sourceInternalPath)) { + $trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath); + } + } catch (CopyRecursiveException $e) { $moveSuccessful = false; if ($trashStorage->file_exists($trashInternalPath)) { $trashStorage->unlink($trashInternalPath); } - \OCP\Util::writeLog('files_trashbin', 'Couldn\'t move ' . $file_path . ' to the trash bin', \OCP\Util::ERROR); + Server::get(LoggerInterface::class)->error('Couldn\'t move ' . $file_path . ' to the trash bin', ['app' => 'files_trashbin']); } if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort @@ -266,19 +316,30 @@ class Trashbin { } else { $sourceStorage->unlink($sourceInternalPath); } + + if ($sourceStorage->file_exists($sourceInternalPath)) { + // undo the cache move + $sourceStorage->getUpdater()->renameFromStorage($trashStorage, $trashInternalPath, $sourceInternalPath); + } else { + $trashStorage->getUpdater()->remove($trashInternalPath); + } return false; } - $trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath); - if ($moveSuccessful) { - $query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)"); - $result = $query->execute(array($filename, $timestamp, $location, $owner)); + $query = Server::get(IDBConnection::class)->getQueryBuilder(); + $query->insert('files_trash') + ->setValue('id', $query->createNamedParameter($filename)) + ->setValue('timestamp', $query->createNamedParameter($timestamp)) + ->setValue('location', $query->createNamedParameter($location)) + ->setValue('user', $query->createNamedParameter($owner)) + ->setValue('deleted_by', $query->createNamedParameter($user)); + $result = $query->executeStatement(); if (!$result) { - \OCP\Util::writeLog('files_trashbin', 'trash bin database couldn\'t be updated', \OCP\Util::ERROR); + Server::get(LoggerInterface::class)->error('trash bin database couldn\'t be updated', ['app' => 'files_trashbin']); } - \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', array('filePath' => Filesystem::normalizePath($file_path), - 'trashPath' => Filesystem::normalizePath($filename . '.d' . $timestamp))); + Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', ['filePath' => Filesystem::normalizePath($file_path), + 'trashPath' => Filesystem::normalizePath(static::getTrashFilename($filename, $timestamp))]); self::retainVersions($filename, $owner, $ownerPath, $timestamp); @@ -288,6 +349,8 @@ class Trashbin { } } + $trashStorage->releaseLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider); + self::scheduleExpire($user); // if owner !== user we also need to update the owners trash size @@ -298,32 +361,43 @@ class Trashbin { return $moveSuccessful; } + private static function getConfiguredTrashbinSize(string $user): int|float { + $config = Server::get(IConfig::class); + $userTrashbinSize = $config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1'); + if (is_numeric($userTrashbinSize) && ($userTrashbinSize > -1)) { + return Util::numericToNumber($userTrashbinSize); + } + $systemTrashbinSize = $config->getAppValue('files_trashbin', 'trashbin_size', '-1'); + if (is_numeric($systemTrashbinSize)) { + return Util::numericToNumber($systemTrashbinSize); + } + return -1; + } + /** * Move file versions to trash so that they can be restored later * * @param string $filename of deleted file * @param string $owner owner user id * @param string $ownerPath path relative to the owner's home storage - * @param integer $timestamp when the file was deleted + * @param int $timestamp when the file was deleted */ private static function retainVersions($filename, $owner, $ownerPath, $timestamp) { - if (\OCP\App::isEnabled('files_versions') && !empty($ownerPath)) { - - $user = User::getUser(); + if (Server::get(IAppManager::class)->isEnabledForUser('files_versions') && !empty($ownerPath)) { + $user = OC_User::getUser(); $rootView = new View('/'); if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) { if ($owner !== $user) { - self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . basename($ownerPath) . '.d' . $timestamp, $rootView); + self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . static::getTrashFilename(basename($ownerPath), $timestamp), $rootView); } - self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . $filename . '.d' . $timestamp); - } else if ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) { - + self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . static::getTrashFilename($filename, $timestamp)); + } elseif ($versions = Storage::getVersions($owner, $ownerPath)) { foreach ($versions as $v) { if ($owner !== $user) { - self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp); + self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . static::getTrashFilename($v['name'] . '.v' . $v['version'], $timestamp)); } - self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp); + self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v['version'], $timestamp)); } } } @@ -339,9 +413,9 @@ class Trashbin { */ private static function move(View $view, $source, $target) { /** @var \OC\Files\Storage\Storage $sourceStorage */ - list($sourceStorage, $sourceInternalPath) = $view->resolvePath($source); + [$sourceStorage, $sourceInternalPath] = $view->resolvePath($source); /** @var \OC\Files\Storage\Storage $targetStorage */ - list($targetStorage, $targetInternalPath) = $view->resolvePath($target); + [$targetStorage, $targetInternalPath] = $view->resolvePath($target); /** @var \OC\Files\Storage\Storage $ownerTrashStorage */ $result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); @@ -361,9 +435,9 @@ class Trashbin { */ private static function copy(View $view, $source, $target) { /** @var \OC\Files\Storage\Storage $sourceStorage */ - list($sourceStorage, $sourceInternalPath) = $view->resolvePath($source); + [$sourceStorage, $sourceInternalPath] = $view->resolvePath($source); /** @var \OC\Files\Storage\Storage $targetStorage */ - list($targetStorage, $targetInternalPath) = $view->resolvePath($target); + [$targetStorage, $targetInternalPath] = $view->resolvePath($target); /** @var \OC\Files\Storage\Storage $ownerTrashStorage */ $result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); @@ -377,26 +451,26 @@ class Trashbin { * Restore a file or folder from trash bin * * @param string $file path to the deleted file/folder relative to "files_trashbin/files/", - * including the timestamp suffix ".d12345678" + * including the timestamp suffix ".d12345678" * @param string $filename name of the file/folder * @param int $timestamp time when the file/folder was deleted * * @return bool true on success, false otherwise */ public static function restore($file, $filename, $timestamp) { - $user = User::getUser(); + $user = OC_User::getUser(); $view = new View('/' . $user); $location = ''; if ($timestamp) { $location = self::getLocation($user, $filename, $timestamp); if ($location === false) { - \OCP\Util::writeLog('files_trashbin', 'trash bin database inconsistent! ($user: ' . $user . ' $filename: ' . $filename . ', $timestamp: ' . $timestamp . ')', \OCP\Util::ERROR); + Server::get(LoggerInterface::class)->error('trash bin database inconsistent! ($user: ' . $user . ' $filename: ' . $filename . ', $timestamp: ' . $timestamp . ')', ['app' => 'files_trashbin']); } else { // if location no longer exists, restore file in the root directory - if ($location !== '/' && - (!$view->is_dir('files/' . $location) || - !$view->isCreatable('files/' . $location)) + if ($location !== '/' + && (!$view->is_dir('files/' . $location) + || !$view->isCreatable('files/' . $location)) ) { $location = ''; } @@ -414,6 +488,24 @@ class Trashbin { $mtime = $view->filemtime($source); // restore file + if (!$view->isCreatable(dirname($target))) { + throw new NotPermittedException("Can't restore trash item because the target folder is not writable"); + } + + $sourcePath = Filesystem::normalizePath($file); + $targetPath = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename); + + $sourceNode = self::getNodeForPath($sourcePath); + $targetNode = self::getNodeForPath($targetPath); + $run = true; + $event = new BeforeNodeRestoredEvent($sourceNode, $targetNode, $run); + $dispatcher = Server::get(IEventDispatcher::class); + $dispatcher->dispatchTyped($event); + + if (!$run) { + return false; + } + $restoreResult = $view->rename($source, $target); // handle the restore result @@ -422,14 +514,23 @@ class Trashbin { $view->chroot('/' . $user . '/files'); $view->touch('/' . $location . '/' . $uniqueFilename, $mtime); $view->chroot($fakeRoot); - \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', array('filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename), - 'trashPath' => Filesystem::normalizePath($file))); + Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => $targetPath, 'trashPath' => $sourcePath]); + + $sourceNode = self::getNodeForPath($sourcePath); + $targetNode = self::getNodeForPath($targetPath); + $event = new NodeRestoredEvent($sourceNode, $targetNode); + $dispatcher = Server::get(IEventDispatcher::class); + $dispatcher->dispatchTyped($event); self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp); if ($timestamp) { - $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?'); - $query->execute(array($user, $filename, $timestamp)); + $query = Server::get(IDBConnection::class)->getQueryBuilder(); + $query->delete('files_trash') + ->where($query->expr()->eq('user', $query->createNamedParameter($user))) + ->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename))) + ->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp))); + $query->executeStatement(); } return true; @@ -450,15 +551,13 @@ class Trashbin { * @return false|null */ private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) { - - if (\OCP\App::isEnabled('files_versions')) { - - $user = User::getUser(); + if (Server::get(IAppManager::class)->isEnabledForUser('files_versions')) { + $user = OC_User::getUser(); $rootView = new View('/'); $target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename); - list($owner, $ownerPath) = self::getUidAndFilename($target); + [$owner, $ownerPath] = self::getUidAndFilename($target); // file has been deleted in between if (empty($ownerPath)) { @@ -473,10 +572,10 @@ class Trashbin { if ($view->is_dir('/files_trashbin/versions/' . $file)) { $rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath)); - } else if ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) { + } elseif ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) { foreach ($versions as $v) { if ($timestamp) { - $rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v . '.d' . $timestamp, $owner . '/files_versions/' . $ownerPath . '.v' . $v); + $rootView->rename($user . '/files_trashbin/versions/' . static::getTrashFilename($versionedFile . '.v' . $v, $timestamp), $owner . '/files_versions/' . $ownerPath . '.v' . $v); } else { $rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v); } @@ -489,7 +588,7 @@ class Trashbin { * delete all files from the trash */ public static function deleteAll() { - $user = User::getUser(); + $user = OC_User::getUser(); $userRoot = \OC::$server->getUserFolder($user)->getParent(); $view = new View('/' . $user); $fileInfos = $view->getDirectoryContent('files_trashbin/files'); @@ -501,30 +600,33 @@ class Trashbin { } // Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore) - $filePaths = array(); - foreach($fileInfos as $fileInfo){ + $filePaths = []; + foreach ($fileInfos as $fileInfo) { $filePaths[] = $view->getRelativePath($fileInfo->getPath()); } unset($fileInfos); // save memory // Bulk PreDelete-Hook - \OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', array('paths' => $filePaths)); + \OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', ['paths' => $filePaths]); // Single-File Hooks - foreach($filePaths as $path){ + foreach ($filePaths as $path) { self::emitTrashbinPreDelete($path); } // actual file deletion $trash->delete(); - $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?'); - $query->execute(array($user)); + + $query = Server::get(IDBConnection::class)->getQueryBuilder(); + $query->delete('files_trash') + ->where($query->expr()->eq('user', $query->createNamedParameter($user))); + $query->executeStatement(); // Bulk PostDelete-Hook - \OC_Hook::emit('\OCP\Trashbin', 'deleteAll', array('paths' => $filePaths)); + \OC_Hook::emit('\OCP\Trashbin', 'deleteAll', ['paths' => $filePaths]); // Single-File Hooks - foreach($filePaths as $path){ + foreach ($filePaths as $path) { self::emitTrashbinPostDelete($path); } @@ -536,18 +638,20 @@ class Trashbin { /** * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted + * * @param string $path */ - protected static function emitTrashbinPreDelete($path){ - \OC_Hook::emit('\OCP\Trashbin', 'preDelete', array('path' => $path)); + protected static function emitTrashbinPreDelete($path) { + \OC_Hook::emit('\OCP\Trashbin', 'preDelete', ['path' => $path]); } /** * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted + * * @param string $path */ - protected static function emitTrashbinPostDelete($path){ - \OC_Hook::emit('\OCP\Trashbin', 'delete', array('path' => $path)); + protected static function emitTrashbinPostDelete($path) { + \OC_Hook::emit('\OCP\Trashbin', 'delete', ['path' => $path]); } /** @@ -557,7 +661,7 @@ class Trashbin { * @param string $user * @param int $timestamp of deletion time * - * @return int size of deleted files + * @return int|float size of deleted files */ public static function delete($filename, $user, $timestamp = null) { $userRoot = \OC::$server->getUserFolder($user)->getParent(); @@ -565,9 +669,14 @@ class Trashbin { $size = 0; if ($timestamp) { - $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?'); - $query->execute(array($user, $filename, $timestamp)); - $file = $filename . '.d' . $timestamp; + $query = Server::get(IDBConnection::class)->getQueryBuilder(); + $query->delete('files_trash') + ->where($query->expr()->eq('user', $query->createNamedParameter($user))) + ->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename))) + ->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp))); + $query->executeStatement(); + + $file = static::getTrashFilename($filename, $timestamp); } else { $file = $filename; } @@ -582,7 +691,7 @@ class Trashbin { if ($node instanceof Folder) { $size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file)); - } else if ($node instanceof File) { + } elseif ($node instanceof File) { $size += $view->filesize('/files_trashbin/files/' . $file); } @@ -594,24 +703,21 @@ class Trashbin { } /** - * @param View $view * @param string $file * @param string $filename - * @param integer|null $timestamp - * @param string $user - * @return int + * @param ?int $timestamp */ - private static function deleteVersions(View $view, $file, $filename, $timestamp, $user) { + private static function deleteVersions(View $view, $file, $filename, $timestamp, string $user): int|float { $size = 0; - if (\OCP\App::isEnabled('files_versions')) { + if (Server::get(IAppManager::class)->isEnabledForUser('files_versions')) { if ($view->is_dir('files_trashbin/versions/' . $file)) { $size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file)); $view->unlink('files_trashbin/versions/' . $file); - } else if ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) { + } elseif ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) { foreach ($versions as $v) { if ($timestamp) { - $size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp); - $view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp); + $size += $view->filesize('/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v, $timestamp)); + $view->unlink('/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v, $timestamp)); } else { $size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v); $view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v); @@ -630,13 +736,11 @@ class Trashbin { * @return bool true if file exists, otherwise false */ public static function file_exists($filename, $timestamp = null) { - $user = User::getUser(); + $user = OC_User::getUser(); $view = new View('/' . $user); if ($timestamp) { - $filename = $filename . '.d' . $timestamp; - } else { - $filename = $filename; + $filename = static::getTrashFilename($filename, $timestamp); } $target = Filesystem::normalizePath('files_trashbin/files/' . $filename); @@ -650,23 +754,30 @@ class Trashbin { * @return bool result of db delete operation */ public static function deleteUser($uid) { - $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?'); - return $query->execute(array($uid)); + $query = Server::get(IDBConnection::class)->getQueryBuilder(); + $query->delete('files_trash') + ->where($query->expr()->eq('user', $query->createNamedParameter($uid))); + return (bool)$query->executeStatement(); } /** * calculate remaining free space for trash bin * - * @param integer $trashbinSize current size of the trash bin + * @param int|float $trashbinSize current size of the trash bin * @param string $user - * @return int available free space for trash bin + * @return int|float available free space for trash bin */ - private static function calculateFreeSpace($trashbinSize, $user) { - $softQuota = true; - $userObject = \OC::$server->getUserManager()->get($user); - if(is_null($userObject)) { + private static function calculateFreeSpace(int|float $trashbinSize, string $user): int|float { + $configuredTrashbinSize = static::getConfiguredTrashbinSize($user); + if ($configuredTrashbinSize > -1) { + return $configuredTrashbinSize - $trashbinSize; + } + + $userObject = Server::get(IUserManager::class)->get($user); + if (is_null($userObject)) { return 0; } + $softQuota = true; $quota = $userObject->getQuota(); if ($quota === null || $quota === 'none') { $quota = Filesystem::free_space('/'); @@ -676,17 +787,21 @@ class Trashbin { $quota = PHP_INT_MAX; } } else { - $quota = \OCP\Util::computerFileSize($quota); + $quota = Util::computerFileSize($quota); + // invalid quota + if ($quota === false) { + $quota = PHP_INT_MAX; + } } // calculate available space for trash bin // subtract size of files and current trash bin size from quota if ($softQuota) { $userFolder = \OC::$server->getUserFolder($user); - if(is_null($userFolder)) { + if (is_null($userFolder)) { return 0; } - $free = $quota - $userFolder->getSize(); // remaining free space for user + $free = $quota - $userFolder->getSize(false); // remaining free space for user if ($free > 0) { $availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions } else { @@ -696,7 +811,7 @@ class Trashbin { $availableSpace = $quota; } - return $availableSpace; + return Util::numericToNumber($availableSpace); } /** @@ -705,7 +820,6 @@ class Trashbin { * @param string $user user id */ public static function resizeTrash($user) { - $size = self::getTrashbinSize($user); $freeSpace = self::calculateFreeSpace($size, $user); @@ -727,7 +841,7 @@ class Trashbin { $dirContent = Helper::getTrashFiles('/', $user, 'mtime'); // delete all files older then $retention_obligation - list($delSize, $count) = self::deleteExpiredFiles($dirContent, $user); + [$delSize, $count] = self::deleteExpiredFiles($dirContent, $user); $availableSpace += $delSize; @@ -740,10 +854,11 @@ class Trashbin { */ private static function scheduleExpire($user) { // let the admin disable auto expire - $application = new Application(); + /** @var Application $application */ + $application = Server::get(Application::class); $expiration = $application->getContainer()->query('Expiration'); if ($expiration->isEnabled()) { - \OC::$server->getCommandBus()->push(new Expire($user)); + Server::get(IBus::class)->push(new Expire($user)); } } @@ -753,11 +868,12 @@ class Trashbin { * * @param array $files * @param string $user - * @param int $availableSpace available disc space - * @return int size of deleted files + * @param int|float $availableSpace available disc space + * @return int|float size of deleted files */ - protected static function deleteFiles($files, $user, $availableSpace) { - $application = new Application(); + protected static function deleteFiles(array $files, string $user, int|float $availableSpace): int|float { + /** @var Application $application */ + $application = Server::get(Application::class); $expiration = $application->getContainer()->query('Expiration'); $size = 0; @@ -765,7 +881,13 @@ class Trashbin { foreach ($files as $file) { if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) { $tmp = self::delete($file['name'], $user, $file['mtime']); - \OCP\Util::writeLog('files_trashbin', 'remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota)', \OCP\Util::INFO); + Server::get(LoggerInterface::class)->info( + 'remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota) for user "{user}"', + [ + 'app' => 'files_trashbin', + 'user' => $user, + ] + ); $availableSpace += $tmp; $size += $tmp; } else { @@ -781,41 +903,54 @@ class Trashbin { * * @param array $files list of files sorted by mtime * @param string $user - * @return integer[] size of deleted files and number of deleted files + * @return array{int|float, int} size of deleted files and number of deleted files */ public static function deleteExpiredFiles($files, $user) { - $application = new Application(); - $expiration = $application->getContainer()->query('Expiration'); + /** @var Expiration $expiration */ + $expiration = Server::get(Expiration::class); $size = 0; $count = 0; foreach ($files as $file) { $timestamp = $file['mtime']; $filename = $file['name']; if ($expiration->isExpired($timestamp)) { - $count++; - $size += self::delete($filename, $user, $timestamp); - \OC::$server->getLogger()->info( - 'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.', - ['app' => 'files_trashbin'] + try { + $size += self::delete($filename, $user, $timestamp); + $count++; + } catch (NotPermittedException $e) { + Server::get(LoggerInterface::class)->warning('Removing "' . $filename . '" from trashbin failed for user "{user}"', + [ + 'exception' => $e, + 'app' => 'files_trashbin', + 'user' => $user, + ] + ); + } + Server::get(LoggerInterface::class)->info( + 'Remove "' . $filename . '" from trashbin for user "{user}" because it exceeds max retention obligation term.', + [ + 'app' => 'files_trashbin', + 'user' => $user, + ], ); } else { break; } } - return array($size, $count); + return [$size, $count]; } /** * recursive copy to copy a whole directory * * @param string $source source path, relative to the users files directory - * @param string $destination destination path relative to the users root directoy + * @param string $destination destination path relative to the users root directory * @param View $view file view for the users root directory - * @return int + * @return int|float * @throws Exceptions\CopyRecursiveException */ - private static function copy_recursive($source, $destination, View $view) { + private static function copy_recursive($source, $destination, View $view): int|float { $size = 0; if ($view->is_dir($source)) { $view->mkdir($destination); @@ -828,7 +963,7 @@ class Trashbin { $size += $view->filesize($pathDir); $result = $view->copy($pathDir, $destination . '/' . $i['name']); if (!$result) { - throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException(); + throw new CopyRecursiveException(); } $view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir)); } @@ -837,7 +972,7 @@ class Trashbin { $size += $view->filesize($source); $result = $view->copy($source, $destination); if (!$result) { - throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException(); + throw new CopyRecursiveException(); } $view->touch($destination, $view->filemtime($source)); } @@ -849,39 +984,60 @@ class Trashbin { * * @param string $filename name of the file which should be restored * @param int $timestamp timestamp when the file was deleted - * @return array */ - private static function getVersionsFromTrash($filename, $timestamp, $user) { + private static function getVersionsFromTrash($filename, $timestamp, string $user): array { $view = new View('/' . $user . '/files_trashbin/versions'); - $versions = array(); + $versions = []; - //force rescan of versions, local storage may not have updated the cache - if (!self::$scannedVersions) { - /** @var \OC\Files\Storage\Storage $storage */ - list($storage,) = $view->resolvePath('/'); - $storage->getScanner()->scan('files_trashbin/versions'); - self::$scannedVersions = true; - } + /** @var \OC\Files\Storage\Storage $storage */ + [$storage,] = $view->resolvePath('/'); + $pattern = Server::get(IDBConnection::class)->escapeLikeParameter(basename($filename)); if ($timestamp) { // fetch for old versions - $matches = $view->searchRaw($filename . '.v%.d' . $timestamp); - $offset = -strlen($timestamp) - 2; + $escapedTimestamp = Server::get(IDBConnection::class)->escapeLikeParameter((string)$timestamp); + $pattern .= '.v%.d' . $escapedTimestamp; + $offset = -strlen($escapedTimestamp) - 2; } else { - $matches = $view->searchRaw($filename . '.v%'); + $pattern .= '.v%'; } - if (is_array($matches)) { - foreach ($matches as $ma) { - if ($timestamp) { - $parts = explode('.v', substr($ma['path'], 0, $offset)); - $versions[] = (end($parts)); - } else { - $parts = explode('.v', $ma); - $versions[] = (end($parts)); - } + // Manually fetch all versions from the file cache to be able to filter them by their parent + $cache = $storage->getCache(''); + $query = new CacheQueryBuilder( + Server::get(IDBConnection::class)->getQueryBuilder(), + Server::get(IFilesMetadataManager::class), + ); + $normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/' . $filename)), '/'); + $parentId = $cache->getId($normalizedParentPath); + if ($parentId === -1) { + return []; + } + + $query->selectFileCache() + ->whereStorageId($cache->getNumericStorageId()) + ->andWhere($query->expr()->eq('parent', $query->createNamedParameter($parentId))) + ->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern))); + + $result = $query->executeQuery(); + $entries = $result->fetchAll(); + $result->closeCursor(); + + /** @var CacheEntry[] $matches */ + $matches = array_map(function (array $data) { + return Cache::cacheEntryFromData($data, Server::get(IMimeTypeLoader::class)); + }, $entries); + + foreach ($matches as $ma) { + if ($timestamp) { + $parts = explode('.v', substr($ma['path'], 0, $offset)); + $versions[] = end($parts); + } else { + $parts = explode('.v', $ma['path']); + $versions[] = end($parts); } } + return $versions; } @@ -896,7 +1052,7 @@ class Trashbin { private static function getUniqueFilename($location, $filename, View $view) { $ext = pathinfo($filename, PATHINFO_EXTENSION); $name = pathinfo($filename, PATHINFO_FILENAME); - $l = \OC::$server->getL10N('files_trashbin'); + $l = Util::getL10N('files_trashbin'); $location = '/' . trim($location, '/'); @@ -907,9 +1063,9 @@ class Trashbin { if ($view->file_exists('files' . $location . '/' . $filename)) { $i = 2; - $uniqueName = $name . " (" . $l->t("restored") . ")" . $ext; + $uniqueName = $name . ' (' . $l->t('restored') . ')' . $ext; while ($view->file_exists('files' . $location . '/' . $uniqueName)) { - $uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext; + $uniqueName = $name . ' (' . $l->t('restored') . ' ' . $i . ')' . $ext; $i++; } @@ -923,10 +1079,10 @@ class Trashbin { * get the size from a given root folder * * @param View $view file view on the root folder - * @return integer size of the folder + * @return int|float size of the folder */ - private static function calculateSize($view) { - $root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath(''); + private static function calculateSize(View $view): int|float { + $root = Server::get(IConfig::class)->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath(''); if (!file_exists($root)) { return 0; } @@ -954,41 +1110,24 @@ class Trashbin { * get current size of trash bin from a given user * * @param string $user user who owns the trash bin - * @return integer trash bin size + * @return int|float trash bin size */ - private static function getTrashbinSize($user) { + private static function getTrashbinSize(string $user): int|float { $view = new View('/' . $user); $fileInfo = $view->getFileInfo('/files_trashbin'); return isset($fileInfo['size']) ? $fileInfo['size'] : 0; } /** - * register hooks - */ - public static function registerHooks() { - // create storage wrapper on setup - \OCP\Util::connectHook('OC_Filesystem', 'preSetup', 'OCA\Files_Trashbin\Storage', 'setupStorage'); - //Listen to delete user signal - \OCP\Util::connectHook('OC_User', 'pre_deleteUser', 'OCA\Files_Trashbin\Hooks', 'deleteUser_hook'); - //Listen to post write hook - \OCP\Util::connectHook('OC_Filesystem', 'post_write', 'OCA\Files_Trashbin\Hooks', 'post_write_hook'); - // pre and post-rename, disable trash logic for the copy+unlink case - \OCP\Util::connectHook('OC_Filesystem', 'delete', 'OCA\Files_Trashbin\Trashbin', 'ensureFileScannedHook'); - \OCP\Util::connectHook('OC_Filesystem', 'rename', 'OCA\Files_Trashbin\Storage', 'preRenameHook'); - \OCP\Util::connectHook('OC_Filesystem', 'post_rename', 'OCA\Files_Trashbin\Storage', 'postRenameHook'); - } - - /** * check if trash bin is empty for a given user * * @param string $user * @return bool */ public static function isEmpty($user) { - $view = new View('/' . $user . '/files_trashbin'); if ($view->is_dir('/files') && $dh = $view->opendir('/files')) { - while ($file = readdir($dh)) { + while (($file = readdir($dh)) !== false) { if (!Filesystem::isIgnoredDir($file)) { return false; } @@ -1002,6 +1141,60 @@ class Trashbin { * @return string */ public static function preview_icon($path) { - return \OCP\Util::linkToRoute('core_ajax_trashbin_preview', array('x' => 32, 'y' => 32, 'file' => $path)); + return Server::get(IURLGenerator::class)->linkToRoute('core_ajax_trashbin_preview', ['x' => 32, 'y' => 32, 'file' => $path]); + } + + /** + * Return the filename used in the trash bin + */ + public static function getTrashFilename(string $filename, int $timestamp): string { + $trashFilename = $filename . '.d' . $timestamp; + $length = strlen($trashFilename); + // oc_filecache `name` column has a limit of 250 chars + $maxLength = 250; + if ($length > $maxLength) { + $trashFilename = substr_replace( + $trashFilename, + '', + $maxLength / 2, + $length - $maxLength + ); + } + return $trashFilename; + } + + private static function getNodeForPath(string $path): Node { + $user = OC_User::getUser(); + $rootFolder = Server::get(IRootFolder::class); + + if ($user !== false) { + $userFolder = $rootFolder->getUserFolder($user); + /** @var Folder */ + $trashFolder = $userFolder->getParent()->get('files_trashbin/files'); + try { + return $trashFolder->get($path); + } catch (NotFoundException $ex) { + } + } + + $view = Server::get(View::class); + $fsView = Filesystem::getView(); + if ($fsView === null) { + throw new Exception('View should not be null'); + } + + $fullPath = $fsView->getAbsolutePath($path); + + if (Filesystem::is_dir($path)) { + return new NonExistingFolder($rootFolder, $view, $fullPath); + } else { + return new NonExistingFile($rootFolder, $view, $fullPath); + } + } + + public function handle(Event $event): void { + if ($event instanceof BeforeNodeDeletedEvent) { + self::ensureFileScannedHook($event->getNode()); + } } } |