From 5c2bdb6689de4d79606d095a7d57f9f7bf8c192d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 5 Mar 2019 17:39:03 +0100 Subject: [PATCH 1/4] [stable10] Add webdav trash bin endpoint --- apps/dav/lib/Capabilities.php | 1 + .../Connector/Sabre/CopyEtagHeaderPlugin.php | 15 +- apps/dav/lib/Connector/Sabre/Directory.php | 4 + apps/dav/lib/RootCollection.php | 3 + apps/dav/lib/Server.php | 4 +- .../dav/lib/TrashBin/AbstractTrashBinNode.php | 153 ++++++++++++++++++ apps/dav/lib/TrashBin/ITrashBinNode.php | 51 ++++++ apps/dav/lib/TrashBin/RootCollection.php | 45 ++++++ apps/dav/lib/TrashBin/TrashBinFile.php | 35 ++++ apps/dav/lib/TrashBin/TrashBinFolder.php | 56 +++++++ apps/dav/lib/TrashBin/TrashBinHome.php | 69 ++++++++ apps/dav/lib/TrashBin/TrashBinManager.php | 90 +++++++++++ apps/dav/lib/TrashBin/TrashBinPlugin.php | 60 +++++++ .../unit/TrashBin/RootCollectionTest.php | 44 +++++ .../tests/unit/TrashBin/TrashBinFileTest.php | 65 ++++++++ .../unit/TrashBin/TrashBinFolderTest.php | 133 +++++++++++++++ .../tests/unit/TrashBin/TrashBinHomeTest.php | 100 ++++++++++++ .../unit/TrashBin/TrashBinPluginTest.php | 63 ++++++++ apps/files_trashbin/lib/Trashbin.php | 49 +++--- 19 files changed, 1011 insertions(+), 29 deletions(-) create mode 100644 apps/dav/lib/TrashBin/AbstractTrashBinNode.php create mode 100644 apps/dav/lib/TrashBin/ITrashBinNode.php create mode 100644 apps/dav/lib/TrashBin/RootCollection.php create mode 100644 apps/dav/lib/TrashBin/TrashBinFile.php create mode 100644 apps/dav/lib/TrashBin/TrashBinFolder.php create mode 100644 apps/dav/lib/TrashBin/TrashBinHome.php create mode 100644 apps/dav/lib/TrashBin/TrashBinManager.php create mode 100644 apps/dav/lib/TrashBin/TrashBinPlugin.php create mode 100644 apps/dav/tests/unit/TrashBin/RootCollectionTest.php create mode 100644 apps/dav/tests/unit/TrashBin/TrashBinFileTest.php create mode 100644 apps/dav/tests/unit/TrashBin/TrashBinFolderTest.php create mode 100644 apps/dav/tests/unit/TrashBin/TrashBinHomeTest.php create mode 100644 apps/dav/tests/unit/TrashBin/TrashBinPluginTest.php diff --git a/apps/dav/lib/Capabilities.php b/apps/dav/lib/Capabilities.php index e4c29cd3b610..71903b20f4d3 100644 --- a/apps/dav/lib/Capabilities.php +++ b/apps/dav/lib/Capabilities.php @@ -41,6 +41,7 @@ public function getCapabilities() { $cap = [ 'dav' => [ 'chunking' => '1.0', + 'trashbin' => '1.0', 'reports' => [ 'search-files', ] diff --git a/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php b/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php index 2d9b3553f53a..57a291408eb3 100644 --- a/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php +++ b/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php @@ -22,6 +22,7 @@ namespace OCA\DAV\Connector\Sabre; +use Sabre\DAV\Exception\NotFound; use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; @@ -71,11 +72,15 @@ public function afterMethod(RequestInterface $request, ResponseInterface $respon * @return void */ public function afterMove($source, $destination) { - $node = $this->server->tree->getNodeForPath($destination); - if ($node instanceof File) { - $eTag = $node->getETag(); - $this->server->httpResponse->setHeader('OC-ETag', $eTag); - $this->server->httpResponse->setHeader('ETag', $eTag); + try { + $node = $this->server->tree->getNodeForPath($destination); + if ($node instanceof File) { + $eTag = $node->getETag(); + $this->server->httpResponse->setHeader('OC-ETag', $eTag); + $this->server->httpResponse->setHeader('ETag', $eTag); + } + } catch (NotFound $ex) { + // nothing to do then .... } } } diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php index 214a5ebe4acf..e1a90ec4ed1f 100644 --- a/apps/dav/lib/Connector/Sabre/Directory.php +++ b/apps/dav/lib/Connector/Sabre/Directory.php @@ -36,6 +36,7 @@ use OCA\DAV\Connector\Sabre\Exception\FileLocked; use OCA\DAV\Connector\Sabre\Exception\Forbidden; use OCA\DAV\Connector\Sabre\Exception\InvalidPath; +use OCA\DAV\TrashBin\ITrashBinNode; use OCA\DAV\Upload\FutureFile; use OCP\Files\FileContentNotAllowedException; use OCP\Files\ForbiddenException; @@ -401,6 +402,9 @@ public function getQuotaInfo() { */ public function moveInto($targetName, $fullSourcePath, INode $sourceNode) { if (!$sourceNode instanceof Node) { + if ($sourceNode instanceof ITrashBinNode) { + return $sourceNode->restore($targetName); + } // it's a file of another kind, like FutureFile if ($sourceNode instanceof IFile) { // fallback to default copy+delete handling diff --git a/apps/dav/lib/RootCollection.php b/apps/dav/lib/RootCollection.php index 43eb9b178c03..4d10aaad038a 100644 --- a/apps/dav/lib/RootCollection.php +++ b/apps/dav/lib/RootCollection.php @@ -58,6 +58,8 @@ public function __construct() { $systemPrincipals->disableListing = $disableListing; $filesCollection = new Files\RootCollection($userPrincipalBackend, 'principals/users'); $filesCollection->disableListing = $disableListing; + $trashBinCollection = new TrashBin\RootCollection($userPrincipalBackend, 'principals/users'); + $trashBinCollection->disableListing = $disableListing; $caldavBackend = new CalDavBackend($db, $userPrincipalBackend, $groupPrincipalBackend, $random); $calendarRoot = new CalendarRoot($userPrincipalBackend, $caldavBackend, 'principals/users'); $calendarRoot->disableListing = $disableListing; @@ -100,6 +102,7 @@ public function __construct() { $groupPrincipals, $systemPrincipals]), $filesCollection, + $trashBinCollection, $calendarRoot, $publicCalendarRoot, new SimpleCollection('addressbooks', [ diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index be6e4826b818..66446254cfca 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -55,6 +55,7 @@ use OCA\DAV\Files\PreviewPlugin; use OCA\DAV\JobStatus\Entity\JobStatusMapper; use OCA\DAV\SystemTag\SystemTagPlugin; +use OCA\DAV\TrashBin\TrashBinPlugin; use OCA\DAV\Upload\ChunkingPlugin; use OCP\IRequest; use OCP\SabrePluginEvent; @@ -187,6 +188,7 @@ public function __construct(IRequest $request, $baseUri) { $this->server->addPlugin(new CopyEtagHeaderPlugin()); $this->server->addPlugin(new ChunkingPlugin()); + $this->server->addPlugin(new TrashBinPlugin()); // Allow view-only plugin for webdav requests $this->server->addPlugin(new ViewOnlyPlugin( @@ -215,7 +217,7 @@ public function __construct(IRequest $request, $baseUri) { ) ); - if ($this->isRequestForSubtree(['files', 'uploads'])) { + if ($this->isRequestForSubtree(['files', 'uploads', 'trash-bin'])) { //For files only $filePropertiesPlugin = new FileCustomPropertiesPlugin( new FileCustomPropertiesBackend( diff --git a/apps/dav/lib/TrashBin/AbstractTrashBinNode.php b/apps/dav/lib/TrashBin/AbstractTrashBinNode.php new file mode 100644 index 000000000000..4c12dc7f3451 --- /dev/null +++ b/apps/dav/lib/TrashBin/AbstractTrashBinNode.php @@ -0,0 +1,153 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @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 + * + */ + +namespace OCA\DAV\TrashBin; + +use OCA\Files_Trashbin\Trashbin; +use OCP\Files\FileInfo; +use Sabre\DAV\Exception\Forbidden; + +abstract class AbstractTrashBinNode implements ITrashBinNode { + + /** + * @var FileInfo + */ + protected $fileInfo; + /** + * @var TrashBinManager + */ + protected $trashBinManager; + /** + * @var string + */ + protected $user; + + public function __construct(string $user, FileInfo $fileInfo, TrashBinManager $trashBinManager) { + $this->fileInfo = $fileInfo; + $this->trashBinManager = $trashBinManager; + $this->user = $user; + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + public function getName() { + return (string)$this->fileInfo->getId(); + } + + /** + * Returns the mime-type for a file + * + * If null is returned, we'll assume application/octet-stream + * + * @return string|null + */ + public function getContentType() { + return $this->fileInfo->getMimetype(); + } + + public function getETag() { + return $this->fileInfo->getEtag(); + } + + public function getLastModified() { + return $this->fileInfo->getMtime(); + } + public function getSize() { + return $this->fileInfo->getSize(); + } + + public function getOriginalFileName() : string { + $path = $this->getPathInTrash(); + if (\count($path) === 1) { + $path = \end($path); + $pathInfo = \pathinfo($path); + return $pathInfo['filename']; + } + return \end($path); + } + + public function getOriginalLocation() : string { + $pathElements = $this->getPathInTrash(); + $path = $pathElements[0]; + $pathParts = \pathinfo($path); + $timestamp = (int)\substr($pathParts['extension'], 1); + + $pathElements[0] = $pathParts['filename']; + $originalPath = \implode('/', $pathElements); + + $location = $this->trashBinManager->getLocation($this->user, $pathParts['filename'], $timestamp); + if ($location !== '.') { + $originalPath = $location . '/' . $originalPath; + } + + return $originalPath; + } + + public function getDeleteTimestamp() : int { + $path = $this->getPathInTrash(); + $path = $path[0]; + $pathParts = \pathinfo($path); + return (int)\substr($pathParts['extension'], 1); + } + + /** + * @codeCoverageIgnore + * @param string $targetLocation + * @return bool + */ + public function restore(string $targetLocation): bool { + return $this->trashBinManager->restore($this->user, $this, $targetLocation); + } + + /** + * @codeCoverageIgnore + */ + public function delete() : void { + $path = $this->fileInfo->getPath(); + $path = \explode('/', $path); + $user = $path[1]; + $elements = \array_splice($path, 4); + $path = \implode('/', $elements); + + $delimiter = \strrpos($path, '.d'); + $path = \substr($path, 0, $delimiter); + + Trashbin::delete($path, $user, $this->getDeleteTimestamp()); + } + + /** + * @return array + */ + public function getPathInTrash() { + $path = $this->fileInfo->getPath(); + $path = \explode('/', $path); + return \array_splice($path, 4); + } + + public function setName($name) { + throw new Forbidden('Permission denied to rename this resource'); + } +} diff --git a/apps/dav/lib/TrashBin/ITrashBinNode.php b/apps/dav/lib/TrashBin/ITrashBinNode.php new file mode 100644 index 000000000000..30a674e85b99 --- /dev/null +++ b/apps/dav/lib/TrashBin/ITrashBinNode.php @@ -0,0 +1,51 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @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 + * + */ +namespace OCA\DAV\TrashBin; + +use Sabre\DAV\INode; + +/** + * Interface ITrashBinNode + * + * A common interface for all trash bin items + * + * @package OCA\DAV\TrashBin + */ +interface ITrashBinNode extends INode { + /** + * @return string + */ + public function getOriginalFileName() : string; + /** + * @return string + */ + public function getOriginalLocation() : string; + /** + * @return int + */ + public function getDeleteTimestamp() : int; + + /** + * @param string $targetLocation + * @return bool + */ + public function restore(string $targetLocation) : bool; +} diff --git a/apps/dav/lib/TrashBin/RootCollection.php b/apps/dav/lib/TrashBin/RootCollection.php new file mode 100644 index 000000000000..32e68a4f66bd --- /dev/null +++ b/apps/dav/lib/TrashBin/RootCollection.php @@ -0,0 +1,45 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @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 + * + */ + +namespace OCA\DAV\TrashBin; + +use Sabre\DAVACL\AbstractPrincipalCollection; + +class RootCollection extends AbstractPrincipalCollection { + + /** + * This method returns a node for a principal. + * + * The passed array contains principal information, and is guaranteed to + * at least contain a uri item. Other properties may or may not be + * supplied by the authentication backend. + * + * @param array $principalInfo + * @return TrashBinHome + */ + public function getChildForPrincipal(array $principalInfo) { + return new TrashBinHome($principalInfo, new TrashBinManager()); + } + + public function getName() { + return 'trash-bin'; + } +} diff --git a/apps/dav/lib/TrashBin/TrashBinFile.php b/apps/dav/lib/TrashBin/TrashBinFile.php new file mode 100644 index 000000000000..58b61db37cb2 --- /dev/null +++ b/apps/dav/lib/TrashBin/TrashBinFile.php @@ -0,0 +1,35 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @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 + * + */ + +namespace OCA\DAV\TrashBin; + +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\IFile; + +class TrashBinFile extends AbstractTrashBinNode implements IFile { + public function put($data) { + throw new Forbidden('Permission denied to write this file'); + } + + public function get() { + throw new Forbidden('Permission denied to read this file'); + } +} diff --git a/apps/dav/lib/TrashBin/TrashBinFolder.php b/apps/dav/lib/TrashBin/TrashBinFolder.php new file mode 100644 index 000000000000..44adb64d5c25 --- /dev/null +++ b/apps/dav/lib/TrashBin/TrashBinFolder.php @@ -0,0 +1,56 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @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 + * + */ + +namespace OCA\DAV\TrashBin; + +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\MethodNotAllowed; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\ICollection; + +class TrashBinFolder extends AbstractTrashBinNode implements ICollection { + public function getChild($name) { + return $this->trashBinManager->getChild($this->user, $name); + } + + public function getChildren() { + return $this->trashBinManager->getChildren($this->user, $this->getName()); + } + + public function createFile($name, $data = null) { + throw new Forbidden('Permission denied to create a file'); + } + + public function createDirectory($name) { + throw new Forbidden('Permission denied to create a folder'); + } + + public function childExists($name) { + try { + $ret = $this->getChild($name); + return $ret !== null; + } catch (NotFound $ex) { + return false; + } catch (MethodNotAllowed $ex) { + return false; + } + } +} diff --git a/apps/dav/lib/TrashBin/TrashBinHome.php b/apps/dav/lib/TrashBin/TrashBinHome.php new file mode 100644 index 000000000000..7899337e6c5c --- /dev/null +++ b/apps/dav/lib/TrashBin/TrashBinHome.php @@ -0,0 +1,69 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @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 + * + */ + +namespace OCA\DAV\TrashBin; + +use Sabre\DAV\Collection; +use Sabre\DAV\Exception\Forbidden; + +class TrashBinHome extends Collection { + + /** @var TrashBinManager */ + private $trashBinManager; + /** @var string */ + private $user; + + /** + * TrashBinHome constructor. + * + * @param array $principalInfo + * @param TrashBinManager $trashBinManager + */ + public function __construct(array $principalInfo, TrashBinManager $trashBinManager) { + $this->trashBinManager = $trashBinManager; + [, $name] = \Sabre\Uri\split($principalInfo['uri']); + $this->user = $name; + } + + public function getChild($name) { + return $this->trashBinManager->getChild($this->user, $name); + } + + public function getChildren() { + return $this->trashBinManager->getChildren($this->user); + } + + public function delete() { + $this->trashBinManager->deleteAll(); + } + + public function getName() { + return $this->user; + } + + public function setName($name) { + throw new Forbidden('Permission denied to rename this folder'); + } + + public function getLastModified() { + return null; + } +} diff --git a/apps/dav/lib/TrashBin/TrashBinManager.php b/apps/dav/lib/TrashBin/TrashBinManager.php new file mode 100644 index 000000000000..4fdf9f89c63f --- /dev/null +++ b/apps/dav/lib/TrashBin/TrashBinManager.php @@ -0,0 +1,90 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @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 + * + */ + +namespace OCA\DAV\TrashBin; + +use OC\Files\FileInfo; +use OC\Files\View; +use OCA\Files_Trashbin\Trashbin; +use OCP\Files\NotFoundException; +use Sabre\DAV\Exception\InvalidResourceType; +use Sabre\DAV\Exception\NotFound; + +/** + * Class TrashBinManager + * + * @package OCA\DAV\TrashBin + * @codeCoverageIgnore + */ +class TrashBinManager { + public function getChild(string $user, string $id) { + try { + $view = new View('/' . $user . '/files_trashbin/files'); + $path = $view->getPath($id); + $fileInfo = $view->getFileInfo($path); + return $this->nodeFactory($user, $fileInfo); + } catch (NotFoundException $ex) { + throw new NotFound(); + } + } + + public function getChildren(string $user, string $fileId = null) { + try { + $view = new View('/' . $user . '/files_trashbin/files'); + $path = '/'; + if ($fileId) { + $path = $view->getPath($fileId); + } + $fileInfo = $view->getFileInfo($path); + if ($fileInfo->getMimetype() !== 'httpd/unix-directory') { + throw new InvalidResourceType(); + } + $files = $view->getDirectoryContent($path); + return \array_map(function ($fileInfo) use ($user) { + return $this->nodeFactory($user, $fileInfo); + }, $files); + } catch (\Exception $exception) { + return []; + } + } + + private function nodeFactory(string $user, FileInfo $fileInfo) { + if ($fileInfo->getMimetype() === 'httpd/unix-directory') { + return new TrashBinFolder($user, $fileInfo, $this); + } + return new TrashBinFile($user, $fileInfo, $this); + } + + public function restore(string $user, AbstractTrashBinNode $trashItem, $targetLocation) { + $path = $trashItem->getPathInTrash(); + $path = \implode('/', $path); + return Trashbin::restore($path, + $trashItem->getOriginalFileName(), $trashItem->getDeleteTimestamp(), $targetLocation); + } + + public function deleteAll() { + return Trashbin::deleteAll(); + } + + public function getLocation(string $user, $filename, int $timestamp) { + return Trashbin::getLocation($user, $filename, $timestamp); + } +} diff --git a/apps/dav/lib/TrashBin/TrashBinPlugin.php b/apps/dav/lib/TrashBin/TrashBinPlugin.php new file mode 100644 index 000000000000..01b08765fc5d --- /dev/null +++ b/apps/dav/lib/TrashBin/TrashBinPlugin.php @@ -0,0 +1,60 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @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 + * + */ + +namespace OCA\DAV\TrashBin; + +use Sabre\DAV\INode; +use Sabre\DAV\PropFind; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; + +class TrashBinPlugin extends ServerPlugin { + public const TRASHBIN_ORIGINAL_FILENAME = '{http://owncloud.org/ns}trashbin-original-filename'; + public const TRASHBIN_ORIGINAL_LOCATION = '{http://owncloud.org/ns}trashbin-original-location'; + public const TRASHBIN_DELETE_TIMESTAMP = '{http://owncloud.org/ns}trashbin-delete-timestamp'; + + /** @var Server */ + private $server; + + public function initialize(Server $server) { + $this->server = $server; + + $this->server->on('propFind', [$this, 'propFind']); + } + + public function propFind(PropFind $propFind, INode $node) { + if (!($node instanceof ITrashBinNode)) { + return; + } + + $propFind->handle(self::TRASHBIN_ORIGINAL_FILENAME, static function () use ($node) { + return $node->getOriginalFileName(); + }); + + $propFind->handle(self::TRASHBIN_ORIGINAL_LOCATION, static function () use ($node) { + return $node->getOriginalLocation(); + }); + + $propFind->handle(self::TRASHBIN_DELETE_TIMESTAMP, static function () use ($node) { + return $node->getDeleteTimestamp(); + }); + } +} diff --git a/apps/dav/tests/unit/TrashBin/RootCollectionTest.php b/apps/dav/tests/unit/TrashBin/RootCollectionTest.php new file mode 100644 index 000000000000..b99c5e9203fd --- /dev/null +++ b/apps/dav/tests/unit/TrashBin/RootCollectionTest.php @@ -0,0 +1,44 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @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 + * + */ + +namespace OCA\DAV\Tests\Unit\TrashBin; + +use OCA\DAV\TrashBin\RootCollection; +use OCA\DAV\TrashBin\TrashBinHome; +use Sabre\DAVACL\PrincipalBackend\BackendInterface; +use Test\TestCase; + +class RootCollectionTest extends TestCase { + public function testGetName() { + $backEnd = $this->createMock(BackendInterface::class); + $collection = new RootCollection($backEnd); + self::assertEquals('trash-bin', $collection->getName()); + } + + public function testGetChildForPrincipal() { + $backEnd = $this->createMock(BackendInterface::class); + $collection = new RootCollection($backEnd); + $child = $collection->getChildForPrincipal([ + 'uri' => 'principals/alice' + ]); + self::assertInstanceOf(TrashBinHome::class, $child); + } +} diff --git a/apps/dav/tests/unit/TrashBin/TrashBinFileTest.php b/apps/dav/tests/unit/TrashBin/TrashBinFileTest.php new file mode 100644 index 000000000000..6221c8afa690 --- /dev/null +++ b/apps/dav/tests/unit/TrashBin/TrashBinFileTest.php @@ -0,0 +1,65 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @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 + * + */ + +namespace OCA\DAV\Tests\Unit\TrashBin; + +use OCA\DAV\TrashBin\TrashBinFile; +use OCA\DAV\TrashBin\TrashBinManager; +use OCP\Files\FileInfo; +use Test\TestCase; + +class TrashBinFileTest extends TestCase { + /** + * @var TrashBinFile + */ + private $trashBinFile; + + protected function setUp() { + parent::setUp(); + $fileInfo = $this->createMock(FileInfo::class); + $trashBinManager = $this->createMock(TrashBinManager::class); + $this->trashBinFile = new TrashBinFile('alice', $fileInfo, $trashBinManager); + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + * @expectedExceptionMessage Permission denied to write this file + */ + public function testPut() { + $this->trashBinFile->put(''); + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + * @expectedExceptionMessage Permission denied to read this file + */ + public function testGet() { + $this->trashBinFile->get(); + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + * @expectedExceptionMessage Permission denied to rename this resource + */ + public function testSetName() { + $this->trashBinFile->setName(''); + } +} diff --git a/apps/dav/tests/unit/TrashBin/TrashBinFolderTest.php b/apps/dav/tests/unit/TrashBin/TrashBinFolderTest.php new file mode 100644 index 000000000000..0b37040617ff --- /dev/null +++ b/apps/dav/tests/unit/TrashBin/TrashBinFolderTest.php @@ -0,0 +1,133 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @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 + * + */ + +namespace OCA\DAV\Tests\Unit\TrashBin; + +use OCA\DAV\TrashBin\TrashBinFolder; +use OCA\DAV\TrashBin\TrashBinManager; +use OCP\Files\FileInfo; +use Sabre\DAV\Exception\NotFound; +use Test\TestCase; + +class TrashBinFolderTest extends TestCase { + /** + * @var TrashBinFolder + */ + private $trashBinFolder; + /** + * @var TrashBinManager | \PHPUnit\Framework\MockObject\MockObject + */ + private $trashBinManager; + /** + * @var FileInfo | \PHPUnit\Framework\MockObject\MockObject + */ + private $fileInfo; + + protected function setUp() { + parent::setUp(); + $this->fileInfo = $this->createMock(FileInfo::class); + $this->trashBinManager = $this->createMock(TrashBinManager::class); + $this->trashBinFolder = new TrashBinFolder('alice', $this->fileInfo, $this->trashBinManager); + + $this->fileInfo->method('getId')->willReturn(666); + $this->fileInfo->method('getMimeType')->willReturn('foo'); + $this->fileInfo->method('getEtag')->willReturn('abcdefgh'); + $this->fileInfo->method('getMtime')->willReturn(789123456); + $this->fileInfo->method('getSize')->willReturn(12345678); + $this->fileInfo->method('getPath')->willReturn('/alice/files_trashbin/files/folder.d1561467869/foo'); + + $this->trashBinManager->method('getLocation')->willReturn('.'); + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + * @expectedExceptionMessage Permission denied to create a file + */ + public function testCreateFile() { + $this->trashBinFolder->createFile(''); + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + * @expectedExceptionMessage Permission denied to create a folder + */ + public function testCreateFolder() { + $this->trashBinFolder->createDirectory(''); + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + * @expectedExceptionMessage Permission denied to rename this resource + */ + public function testSetName() { + $this->trashBinFolder->setName(''); + } + + public function testGetChildren() { + $this->trashBinManager->method('getChildren') + ->with('alice', '666')->willReturn([ + 'dummy string' + ]); + $children = $this->trashBinFolder->getChildren(); + self::assertEquals(['dummy string'], $children); + } + + public function testGetChild() { + $this->trashBinManager->method('getChild') + ->with('alice', '777')->willReturn('dummy string'); + $child = $this->trashBinFolder->getChild(777); + self::assertEquals('dummy string', $child); + } + + public function testChildExists() { + $this->trashBinManager->method('getChild') + ->with('alice', '777')->willReturn('dummy string'); + $exists = $this->trashBinFolder->childExists(777); + self::assertTrue($exists); + } + + public function testChildDoesNotExists() { + $this->trashBinManager->method('getChild') + ->with('alice', '777')->willThrowException(new NotFound()); + $exists = $this->trashBinFolder->childExists(777); + self::assertFalse($exists); + } + + /** + * @dataProvider providesMethods + */ + public function testGetter($expectedValue, $method) { + self::assertEquals($expectedValue, $this->trashBinFolder->$method()); + } + + public function providesMethods() { + return [ + ['666', 'getName'], + ['foo', 'getContentType'], + ['abcdefgh', 'getEtag'], + [789123456, 'getLastModified'], + [12345678, 'getSize'], + ['foo', 'getOriginalFileName'], + ['folder/foo', 'getOriginalLocation'], + [1561467869, 'getDeleteTimestamp'], + ]; + } +} diff --git a/apps/dav/tests/unit/TrashBin/TrashBinHomeTest.php b/apps/dav/tests/unit/TrashBin/TrashBinHomeTest.php new file mode 100644 index 000000000000..adf90bdc3eda --- /dev/null +++ b/apps/dav/tests/unit/TrashBin/TrashBinHomeTest.php @@ -0,0 +1,100 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @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 + * + */ + +namespace OCA\DAV\Tests\Unit\TrashBin; + +use OCA\DAV\TrashBin\TrashBinHome; +use OCA\DAV\TrashBin\TrashBinManager; +use Sabre\DAV\Exception\NotFound; +use Test\TestCase; + +class TrashBinHomeTest extends TestCase { + /** + * @var TrashBinHome + */ + private $trashBinHome; + /** + * @var TrashBinManager | \PHPUnit\Framework\MockObject\MockObject + */ + private $trashBinManager; + + protected function setUp() { + parent::setUp(); + $this->trashBinManager = $this->createMock(TrashBinManager::class); + $this->trashBinHome = new TrashBinHome([ + 'uri' => 'principals/alice' + ], $this->trashBinManager); + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + * @expectedExceptionMessage Permission denied to rename this folder + */ + public function testSetName() { + $this->trashBinHome->setName(''); + } + + public function testGetName() { + self::assertEquals('alice', $this->trashBinHome->getName()); + } + + public function testGetLastModified() { + self::assertNull($this->trashBinHome->getLastModified()); + } + + public function testGetChildren() { + $this->trashBinManager->method('getChildren') + ->with('alice')->willReturn([ + 'dummy string' + ]); + $children = $this->trashBinHome->getChildren(); + self::assertEquals(['dummy string'], $children); + } + + public function testGetChild() { + $this->trashBinManager->method('getChild') + ->with('alice', '777')->willReturn('dummy string'); + $child = $this->trashBinHome->getChild(777); + self::assertEquals('dummy string', $child); + } + + public function testChildExists() { + $this->trashBinManager->method('getChild') + ->with('alice', '777')->willReturn('dummy string'); + $exists = $this->trashBinHome->childExists(777); + self::assertTrue($exists); + } + + public function testChildDoesNotExists() { + $this->trashBinManager->method('getChild') + ->with('alice', '777')->willThrowException(new NotFound()); + $exists = $this->trashBinHome->childExists(777); + self::assertFalse($exists); + } + + public function testDelete() { + $this->trashBinManager + ->expects($this->once()) + ->method('deleteAll') + ->willReturn(true); + $this->trashBinHome->delete(); + } +} diff --git a/apps/dav/tests/unit/TrashBin/TrashBinPluginTest.php b/apps/dav/tests/unit/TrashBin/TrashBinPluginTest.php new file mode 100644 index 000000000000..2e9081955403 --- /dev/null +++ b/apps/dav/tests/unit/TrashBin/TrashBinPluginTest.php @@ -0,0 +1,63 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @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 + * + */ + +namespace OCA\DAV\Tests\Unit\TrashBin; + +use OCA\DAV\TrashBin\ITrashBinNode; +use OCA\DAV\TrashBin\RootCollection; +use OCA\DAV\TrashBin\TrashBinHome; +use OCA\DAV\TrashBin\TrashBinPlugin; +use phpDocumentor\Reflection\Types\This; +use Sabre\DAV\PropFind; +use Sabre\DAV\Server; +use Sabre\DAVACL\PrincipalBackend\BackendInterface; +use Test\TestCase; + +class TrashBinPluginTest extends TestCase { + public function testInit() { + $server = $this->createMock(Server::class); + $server->expects($this->once())->method('on')->with('propFind'); + + $plugin = new TrashBinPlugin(); + $plugin->initialize($server); + } + + /** + * @dataProvider providesMethods + */ + public function testPropFind($expectedMethod, $expectedMethodReturn, $prop) { + $node = $this->createMock(ITrashBinNode::class); + $node->expects(self::once())->method($expectedMethod)->willReturn($expectedMethodReturn); + $propFind = new PropFind('', [$prop]); + $plugin = new TrashBinPlugin(); + $plugin->propFind($propFind, $node); + + self::assertEquals($expectedMethodReturn, $propFind->get($prop)); + } + + public function providesMethods() { + return [ + ['getOriginalFileName', 'bar.txt', TrashBinPlugin::TRASHBIN_ORIGINAL_FILENAME], + ['getOriginalLocation', 'foo/bar.txt', TrashBinPlugin::TRASHBIN_ORIGINAL_LOCATION], + ['getDeleteTimestamp', 123456, TrashBinPlugin::TRASHBIN_DELETE_TIMESTAMP] + ]; + } +} diff --git a/apps/files_trashbin/lib/Trashbin.php b/apps/files_trashbin/lib/Trashbin.php index 93e8fc5475c2..e8407eb22c9b 100644 --- a/apps/files_trashbin/lib/Trashbin.php +++ b/apps/files_trashbin/lib/Trashbin.php @@ -474,32 +474,35 @@ private static function copy(View $view, $source, $target) { * * @return bool true on success, false otherwise */ - public static function restore($file, $filename, $timestamp) { + public static function restore($file, $filename, $timestamp, $targetLocation = null) { $user = User::getUser(); $view = new View('/' . $user); - $location = ''; - if ($timestamp) { - $location = self::getLocation($user, $filename, $timestamp); - if ($location === false) { - \OCP\Util::writeLog('files_trashbin', 'Original location of file ' . $filename . - ' not found in database, hence restoring into user\'s root instead', \OCP\Util::DEBUG); - } else { - // if location no longer exists, restore file in the root directory - if ($location !== '/' && - (!$view->is_dir('files/' . $location) || - !$view->isCreatable('files/' . $location)) - ) { - $location = ''; + if ($targetLocation === null) { + $location = ''; + if ($timestamp) { + $location = self::getLocation($user, $filename, $timestamp); + if ($location === false) { + \OCP\Util::writeLog('files_trashbin', 'Original location of file ' . $filename . + ' not found in database, hence restoring into user\'s root instead', \OCP\Util::DEBUG); + } else { + // if location no longer exists, restore file in the root directory + if ($location !== '/' && + (!$view->is_dir('files/' . $location) || + !$view->isCreatable('files/' . $location)) + ) { + $location = ''; + } } } - } - // we need a extension in case a file/dir with the same name already exists - $uniqueFilename = self::getUniqueFilename($location, $filename, $view); + // we need a extension in case a file/dir with the same name already exists + $uniqueFilename = self::getUniqueFilename($location, $filename, $view); + $targetLocation = $location . '/' . $uniqueFilename; + } $source = Filesystem::normalizePath('files_trashbin/files/' . $file); - $target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename); + $target = Filesystem::normalizePath('files/' . $targetLocation); if (!$view->file_exists($source)) { return false; } @@ -512,12 +515,12 @@ public static function restore($file, $filename, $timestamp) { if ($restoreResult) { $fakeRoot = $view->getRoot(); $view->chroot('/' . $user . '/files'); - $view->touch('/' . $location . '/' . $uniqueFilename, $mtime); + $view->touch('/' . $targetLocation, $mtime); $view->chroot($fakeRoot); - \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename), + \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $targetLocation), 'trashPath' => Filesystem::normalizePath($file)]); - self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp); + self::restoreVersions($view, $file, $filename, $targetLocation, $timestamp); if ($timestamp) { $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?'); @@ -541,12 +544,12 @@ public static function restore($file, $filename, $timestamp) { * @param int $timestamp deletion time * @return false|null */ - private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) { + private static function restoreVersions(View $view, $file, $filename, $targetLocation, $timestamp) { if (\OCP\App::isEnabled('files_versions')) { $user = User::getUser(); $rootView = new View('/'); - $target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename); + $target = Filesystem::normalizePath('/' . $targetLocation); list($owner, $ownerPath) = self::getUidAndFilename($target); From a5cb737f6aee53e55ac2c62d38799b3ff51957d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Wed, 26 Jun 2019 16:44:46 +0200 Subject: [PATCH 2/4] [stable10] Adjust API acceptance tests to use the new webdav trash bin api --- apps/dav/lib/Connector/Sabre/Directory.php | 2 +- apps/dav/lib/TrashBin/TrashBinManager.php | 3 + .../apiTrashbin/trashbinRestore.feature | 17 -- .../features/bootstrap/TrashbinContext.php | 149 +++++++++++------- 4 files changed, 94 insertions(+), 77 deletions(-) diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php index e1a90ec4ed1f..58fd0dc5c96a 100644 --- a/apps/dav/lib/Connector/Sabre/Directory.php +++ b/apps/dav/lib/Connector/Sabre/Directory.php @@ -403,7 +403,7 @@ public function getQuotaInfo() { public function moveInto($targetName, $fullSourcePath, INode $sourceNode) { if (!$sourceNode instanceof Node) { if ($sourceNode instanceof ITrashBinNode) { - return $sourceNode->restore($targetName); + return $sourceNode->restore($this->path . '/' . $targetName); } // it's a file of another kind, like FutureFile if ($sourceNode instanceof IFile) { diff --git a/apps/dav/lib/TrashBin/TrashBinManager.php b/apps/dav/lib/TrashBin/TrashBinManager.php index 4fdf9f89c63f..a32e85cd4680 100644 --- a/apps/dav/lib/TrashBin/TrashBinManager.php +++ b/apps/dav/lib/TrashBin/TrashBinManager.php @@ -54,6 +54,9 @@ public function getChildren(string $user, string $fileId = null) { $path = $view->getPath($fileId); } $fileInfo = $view->getFileInfo($path); + if ($fileInfo === false) { + throw new NotFound(); + } if ($fileInfo->getMimetype() !== 'httpd/unix-directory') { throw new InvalidResourceType(); } diff --git a/tests/acceptance/features/apiTrashbin/trashbinRestore.feature b/tests/acceptance/features/apiTrashbin/trashbinRestore.feature index 6e3bcb8cada6..c301940073d4 100644 --- a/tests/acceptance/features/apiTrashbin/trashbinRestore.feature +++ b/tests/acceptance/features/apiTrashbin/trashbinRestore.feature @@ -68,22 +68,6 @@ Feature: Restore deleted files/folders | old | | new | - Scenario Outline: A file deleted from a folder is restored to root if the original folder does not exist - Given using DAV path - And user "user0" has been created with default attributes and skeleton files - And user "user0" has created folder "/new-folder" - And user "user0" has moved file "/textfile0.txt" to "/new-folder/new-file.txt" - And user "user0" has deleted file "/new-folder/new-file.txt" - And user "user0" has deleted folder "/new-folder" - And user "user0" has logged in to a web-style session - When user "user0" restores the file with original path "/new-folder/new-file.txt" using the trashbin API - Then as "user0" the file with original path "/new-folder/new-file.txt" should not exist in trash - And as "user0" file "/new-file.txt" should exist - Examples: - | dav-path | - | old | - | new | - Scenario Outline: A file deleted from a folder is restored to the original folder if the original folder was deleted and restored Given using DAV path And user "user0" has been created with default attributes and skeleton files @@ -175,4 +159,3 @@ Feature: Restore deleted files/folders When user "user0" restores the folder with original path "/local_storage/tmp/textfile0.txt" using the trashbin API Then as "user0" the folder with original path "/local_storage/tmp/textfile0.txt" should not exist in trash And the downloaded content when downloading file "/local_storage/tmp/textfile0.txt" for user "user0" with range "bytes=0-1" should be "AA" - \ No newline at end of file diff --git a/tests/acceptance/features/bootstrap/TrashbinContext.php b/tests/acceptance/features/bootstrap/TrashbinContext.php index ecc130326cf6..4dc4645fbb56 100644 --- a/tests/acceptance/features/bootstrap/TrashbinContext.php +++ b/tests/acceptance/features/bootstrap/TrashbinContext.php @@ -21,6 +21,7 @@ use Behat\Behat\Context\Context; use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use TestHelpers\WebDavHelper; require_once 'bootstrap.php'; @@ -44,21 +45,22 @@ class TrashbinContext implements Context { * @return void */ public function emptyTrashbin($user) { - $body = new \Behat\Gherkin\Node\TableNode( - [['allfiles', 'true'], ['dir', '/']] - ); - $this->featureContext->sendingToWithDirectUrl( - $user, 'POST', "/index.php/apps/files_trashbin/ajax/delete.php", $body + $response = WebDavHelper::makeDavRequest( + $this->featureContext->getBaseUrl(), + $user, + $this->featureContext->getPasswordForUser($user), + 'DELETE', + "/trash-bin/$user/", + [], + null, + null, + 2, + 'trash-bin' ); - $this->featureContext->theHTTPStatusCodeShouldBe('200'); - $decodedResponse = \json_decode( - $this->featureContext->getResponse()->getBody(), true + + PHPUnit\Framework\Assert::assertEquals( + 204, $response->getStatusCode() ); - if (isset($decodedResponse['status'])) { - PHPUnit\Framework\Assert::assertNotEquals( - 'error', $decodedResponse['status'] - ); - } } /** @@ -70,20 +72,66 @@ public function emptyTrashbin($user) { * @return array response */ public function listTrashbinFolder($user, $path) { - $params = '?dir=' . \rawurlencode('/' . \trim($path, '/')); - $this->featureContext->sendingToWithDirectUrl( + $path = $path ?? '/'; + $responseXml = WebDavHelper::listFolder( + $this->featureContext->getBaseUrl(), $user, - 'GET', - "/index.php/apps/files_trashbin/ajax/list.php$params", - null + $this->featureContext->getPasswordForUser($user), + "/trash-bin/$user/$path", + 1, + [ + 'oc:trashbin-original-filename', + 'oc:trashbin-original-location', + 'oc:trashbin-delete-timestamp', + 'd:getlastmodified' + ], + 'trash-bin' ); - $this->featureContext->theHTTPStatusCodeShouldBe('200'); - $decodedResponse = \json_decode( - $this->featureContext->getResponse()->getBody(), true + $xmlElements = $responseXml->xpath('//d:response'); + $files = \array_map( + static function (SimpleXMLElement $element) { + $href = $element->xpath('./d:href')[0]; + + $propStats = $element->xpath('./d:propstat'); + $successPropStat = \array_filter( + $propStats, static function (SimpleXMLElement $propStat) { + $status = $propStat->xpath('./d:status'); + return (string)$status[0] === 'HTTP/1.1 200 OK'; + } + ); + if (isset($successPropStat[0])) { + $successPropStat = $successPropStat[0]; + + $name = $successPropStat->xpath('./d:prop/oc:trashbin-original-filename'); + $mtime = $successPropStat->xpath('./d:prop/oc:trashbin-delete-timestamp'); + $originalLocation = $successPropStat->xpath('./d:prop/oc:trashbin-original-location'); + } else { + $name = []; + $mtime = []; + $originalLocation = []; + } + + return [ + 'href' => (string)$href, + 'name' => isset($name[0]) ? (string)$name[0] : null, + 'mtime' => isset($mtime[0]) ? (string)$mtime[0] : null, + 'original-location' => isset($originalLocation[0]) ? (string)$originalLocation[0] : null + ]; + }, $xmlElements ); - return $decodedResponse['data']['files']; + // filter root element + $files = \array_filter( + $files, static function ($element) use ($user, $path) { + $path = \ltrim($path, '/'); + if ($path !== '') { + $path .= '/'; + } + return ($element['href'] !== "/remote.php/dav/trash-bin/$user/$path"); + } + ); + return $files; } /** @@ -108,14 +156,8 @@ public function asFileOrFolderExistsInTrash($user, $path) { return; } - $subdir = \trim(\dirname($sections[1]), '/'); - if ($subdir !== '' && $subdir !== '.') { - $subdir = "$firstEntry/$subdir"; - } else { - $subdir = $firstEntry; - } - - $listing = $this->listTrashbinFolder($user, $subdir); + // TODO: handle deeper structures + $listing = $this->listTrashbinFolder($user, \basename(\rtrim($firstEntry['href'], '/'))); $checkedName = \basename($path); $found = false; @@ -141,41 +183,32 @@ private function isInTrash($user, $originalPath) { $listing = $this->listTrashbinFolder($user, null); $originalPath = \trim($originalPath, '/'); - $found = false; foreach ($listing as $entry) { - if (\substr($entry['extraData'], 0, 2) === "./") { - $entry['extraData'] = \substr($entry['extraData'], 2); - } - if ($entry['extraData'] === $originalPath) { - $found = true; - break; + if ($entry['original-location'] === $originalPath) { + return true; } } - return $found; + return false; } /** * @param string $user - * @param string $elementTrashID + * @param string $trashItemHRef + * @param string $originalLocation * * @return void */ - private function sendUndeleteRequest($user, $elementTrashID) { - $body = new \Behat\Gherkin\Node\TableNode( - [['files', "[\"$elementTrashID\"]"], ['dir', '/']] - ); - $this->featureContext->sendingToWithDirectUrl( - $user, 'POST', "/index.php/apps/files_trashbin/ajax/undelete.php", $body + private function sendUndeleteRequest($user, $trashItemHRef, $originalLocation) { + $destinationValue = $this->featureContext->getBaseUrl() . "/remote.php/dav/files/$user/$originalLocation"; + + $trashItemHRef = \substr($trashItemHRef, 15); + $headers['Destination'] = $destinationValue; + $response = $this->featureContext->makeDavRequest( + $user, 'MOVE', $trashItemHRef, $headers, null, 'trash-bin', null, 2 ); - $this->featureContext->theHTTPStatusCodeShouldBe('200'); - $decodedResponse = \json_decode( - $this->featureContext->getResponse()->getBody(), true + PHPUnit\Framework\Assert::assertEquals( + 201, $response->getStatusCode() ); - if (isset($decodedResponse['status'])) { - PHPUnit\Framework\Assert::assertNotEquals( - 'error', $decodedResponse['status'] - ); - } } /** @@ -189,13 +222,11 @@ private function restoreElement($user, $originalPath) { $originalPath = \trim($originalPath, '/'); foreach ($listing as $entry) { - if (\substr($entry['extraData'], 0, 2) === "./") { - $entry['extraData'] = \substr($entry['extraData'], 2); - } - if ($entry['extraData'] === $originalPath) { + if ($entry['original-location'] === $originalPath) { $this->sendUndeleteRequest( $user, - $entry['name'] . '.d' . \floor((integer)$entry['mtime'] / 1000) + $entry['href'], + $entry['original-location'] ); break; } @@ -266,7 +297,7 @@ private function findFirstTrashedEntry($user, $name) { foreach ($listing as $entry) { if ($entry['name'] === $name) { - return $entry['name'] . '.d' . ((int)$entry['mtime'] / 1000); + return $entry; } } From 60cf2ee314973118feb69f16eafa6dead247e916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Mon, 1 Jul 2019 11:09:39 +0200 Subject: [PATCH 3/4] Introduce delete datetime pproperty --- apps/dav/lib/TrashBin/TrashBinPlugin.php | 6 ++++++ apps/dav/tests/unit/TrashBin/TrashBinPluginTest.php | 11 ++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/dav/lib/TrashBin/TrashBinPlugin.php b/apps/dav/lib/TrashBin/TrashBinPlugin.php index 01b08765fc5d..aa39de03d67a 100644 --- a/apps/dav/lib/TrashBin/TrashBinPlugin.php +++ b/apps/dav/lib/TrashBin/TrashBinPlugin.php @@ -25,11 +25,13 @@ use Sabre\DAV\PropFind; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; +use Sabre\DAV\Xml\Property\GetLastModified; class TrashBinPlugin extends ServerPlugin { public const TRASHBIN_ORIGINAL_FILENAME = '{http://owncloud.org/ns}trashbin-original-filename'; public const TRASHBIN_ORIGINAL_LOCATION = '{http://owncloud.org/ns}trashbin-original-location'; public const TRASHBIN_DELETE_TIMESTAMP = '{http://owncloud.org/ns}trashbin-delete-timestamp'; + public const TRASHBIN_DELETE_DATETIME = '{http://owncloud.org/ns}trashbin-delete-datetime'; /** @var Server */ private $server; @@ -56,5 +58,9 @@ public function propFind(PropFind $propFind, INode $node) { $propFind->handle(self::TRASHBIN_DELETE_TIMESTAMP, static function () use ($node) { return $node->getDeleteTimestamp(); }); + + $propFind->handle(self::TRASHBIN_DELETE_DATETIME, static function () use ($node) { + return new GetLastModified($node->getDeleteTimestamp()); + }); } } diff --git a/apps/dav/tests/unit/TrashBin/TrashBinPluginTest.php b/apps/dav/tests/unit/TrashBin/TrashBinPluginTest.php index 2e9081955403..7d2460a9c51a 100644 --- a/apps/dav/tests/unit/TrashBin/TrashBinPluginTest.php +++ b/apps/dav/tests/unit/TrashBin/TrashBinPluginTest.php @@ -28,6 +28,7 @@ use phpDocumentor\Reflection\Types\This; use Sabre\DAV\PropFind; use Sabre\DAV\Server; +use Sabre\DAV\Xml\Property\GetLastModified; use Sabre\DAVACL\PrincipalBackend\BackendInterface; use Test\TestCase; @@ -43,9 +44,12 @@ public function testInit() { /** * @dataProvider providesMethods */ - public function testPropFind($expectedMethod, $expectedMethodReturn, $prop) { + public function testPropFind($expectedMethod, $expectedMethodReturn, $prop, $methodReturnValue = null) { + if ($methodReturnValue === null) { + $methodReturnValue = $expectedMethodReturn; + } $node = $this->createMock(ITrashBinNode::class); - $node->expects(self::once())->method($expectedMethod)->willReturn($expectedMethodReturn); + $node->expects(self::once())->method($expectedMethod)->willReturn($methodReturnValue); $propFind = new PropFind('', [$prop]); $plugin = new TrashBinPlugin(); $plugin->propFind($propFind, $node); @@ -57,7 +61,8 @@ public function providesMethods() { return [ ['getOriginalFileName', 'bar.txt', TrashBinPlugin::TRASHBIN_ORIGINAL_FILENAME], ['getOriginalLocation', 'foo/bar.txt', TrashBinPlugin::TRASHBIN_ORIGINAL_LOCATION], - ['getDeleteTimestamp', 123456, TrashBinPlugin::TRASHBIN_DELETE_TIMESTAMP] + ['getDeleteTimestamp', 123456, TrashBinPlugin::TRASHBIN_DELETE_TIMESTAMP], + ['getDeleteTimestamp', new GetLastModified(123456), TrashBinPlugin::TRASHBIN_DELETE_DATETIME, 123456] ]; } } From 7c8f3eb565c36fabfa8af02ab319b54cb88f2c20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Mon, 1 Jul 2019 11:17:15 +0200 Subject: [PATCH 4/4] Cleanup of trashbin acceptance tests --- .../features/apiTrashbin/trashbinRestore.feature | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/acceptance/features/apiTrashbin/trashbinRestore.feature b/tests/acceptance/features/apiTrashbin/trashbinRestore.feature index c301940073d4..63cb89db1e96 100644 --- a/tests/acceptance/features/apiTrashbin/trashbinRestore.feature +++ b/tests/acceptance/features/apiTrashbin/trashbinRestore.feature @@ -19,7 +19,6 @@ Feature: Restore deleted files/folders And user "user0" has shared folder "/shared" with user "user1" And user "user1" has moved file "/shared" to "/renamed_shared" And user "user1" has deleted file "/renamed_shared/shared_file.txt" - And user "user1" has logged in to a web-style session When user "user1" restores the file with original path "/renamed_shared/shared_file.txt" using the trashbin API Then as "user1" the file with original path "/renamed_shared/shared_file.txt" should not exist in trash And user "user1" should see the following elements @@ -36,7 +35,6 @@ Feature: Restore deleted files/folders And user "user0" has been created with default attributes and skeleton files And user "user0" has deleted file "/textfile0.txt" And as "user0" file "/textfile0.txt" should exist in trash - And user "user0" has logged in to a web-style session When user "user0" restores the folder with original path "/textfile0.txt" using the trashbin API Then as "user0" the folder with original path "/textfile0.txt" should not exist in trash And user "user0" should see the following elements @@ -59,7 +57,6 @@ Feature: Restore deleted files/folders And user "user0" has created folder "/new-folder" And user "user0" has moved file "/textfile0.txt" to "/new-folder/new-file.txt" And user "user0" has deleted file "/new-folder/new-file.txt" - And user "user0" has logged in to a web-style session When user "user0" restores the file with original path "/new-folder/new-file.txt" using the trashbin API Then as "user0" the file with original path "/new-folder/new-file.txt" should not exist in trash And as "user0" file "/new-folder/new-file.txt" should exist @@ -75,7 +72,6 @@ Feature: Restore deleted files/folders And user "user0" has moved file "/textfile0.txt" to "/new-folder/new-file.txt" And user "user0" has deleted file "/new-folder/new-file.txt" And user "user0" has deleted folder "/new-folder" - And user "user0" has logged in to a web-style session When user "user0" restores the folder with original path "/new-folder" using the trashbin API And user "user0" restores the file with original path "/new-folder/new-file.txt" using the trashbin API Then as "user0" the file with original path "/new-folder/new-file.txt" should not exist in trash @@ -92,7 +88,6 @@ Feature: Restore deleted files/folders And user "user0" has moved file "/textfile0.txt" to "/new-folder/new-file.txt" And user "user0" has deleted file "/new-folder/new-file.txt" And user "user0" has deleted folder "/new-folder" - And user "user0" has logged in to a web-style session When user "user0" creates folder "/new-folder" using the WebDAV API And user "user0" restores the file with original path "/new-folder/new-file.txt" using the trashbin API Then as "user0" the file with original path "/new-folder/new-file.txt" should not exist in trash @@ -113,7 +108,6 @@ Feature: Restore deleted files/folders And user "user0" has moved file "/textfile0.txt" to "/local_storage/tmp/textfile0.txt" And user "user0" has deleted file "/local_storage/tmp/textfile0.txt" And as "user0" the folder with original path "/local_storage/tmp/textfile0.txt" should exist in trash - And user "user0" has logged in to a web-style session When user "user0" restores the folder with original path "/local_storage/tmp/textfile0.txt" using the trashbin API Then as "user0" the folder with original path "/local_storage/tmp/textfile0.txt" should not exist in trash And user "user0" should see the following elements @@ -137,7 +131,6 @@ Feature: Restore deleted files/folders And user "user0" has uploaded chunk file "1" of "1" with "AA" to "/local_storage/tmp/textfile0.txt" And user "user0" has deleted file "/local_storage/tmp/textfile0.txt" And as "user0" the folder with original path "/local_storage/tmp/textfile0.txt" should exist in trash - And user "user0" has logged in to a web-style session When user "user0" restores the folder with original path "/local_storage/tmp/textfile0.txt" using the trashbin API Then as "user0" the folder with original path "/local_storage/tmp/textfile0.txt" should not exist in trash And the downloaded content when downloading file "/local_storage/tmp/textfile0.txt" for user "user0" with range "bytes=0-1" should be "AA" @@ -155,7 +148,6 @@ Feature: Restore deleted files/folders | 1 | AA | And user "user0" has deleted file "/local_storage/tmp/textfile0.txt" And as "user0" the folder with original path "/local_storage/tmp/textfile0.txt" should exist in trash - And user "user0" has logged in to a web-style session When user "user0" restores the folder with original path "/local_storage/tmp/textfile0.txt" using the trashbin API Then as "user0" the folder with original path "/local_storage/tmp/textfile0.txt" should not exist in trash And the downloaded content when downloading file "/local_storage/tmp/textfile0.txt" for user "user0" with range "bytes=0-1" should be "AA"