diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index 9f57b7d9c2c9..bd28e5c3c0a6 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -31,6 +31,8 @@ namespace OCA\DAV\Connector\Sabre; use OC\AppFramework\Http\Request; +use OCA\DAV\Files\IProvidesAdditionalHeaders; +use OCA\DAV\Meta\MetaFile; use OCP\Files\ForbiddenException; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; @@ -231,6 +233,9 @@ function httpGet(RequestInterface $request, ResponseInterface $response) { // adds a 'Content-Disposition: attachment' header if ($this->downloadAttachment) { $filename = $node->getName(); + if ($node instanceof IProvidesAdditionalHeaders) { + $filename = $node->getContentDispositionFileName(); + } if ($this->request->isUserAgent( [ Request::USER_AGENT_IE, @@ -255,6 +260,13 @@ function httpGet(RequestInterface $request, ResponseInterface $response) { // cause memory problems in the nginx process. $response->addHeader('X-Accel-Buffering', 'no'); } + + if ($node instanceof IProvidesAdditionalHeaders) { + $headers = $node->getHeaders(); + if (is_array($headers)) { + $response->addHeaders($headers); + } + } } /** diff --git a/apps/dav/lib/Connector/Sabre/Node.php b/apps/dav/lib/Connector/Sabre/Node.php index 79045907aef9..3d6d31e6ec53 100644 --- a/apps/dav/lib/Connector/Sabre/Node.php +++ b/apps/dav/lib/Connector/Sabre/Node.php @@ -384,6 +384,9 @@ public function changeLock($type) { $this->fileView->changeLock($this->path, $type); } + /** + * @return \OCP\Files\FileInfo + */ public function getFileInfo() { return $this->info; } diff --git a/apps/dav/lib/Connector/Sabre/Server.php b/apps/dav/lib/Connector/Sabre/Server.php index c01afff71c6b..a172ee6e9708 100644 --- a/apps/dav/lib/Connector/Sabre/Server.php +++ b/apps/dav/lib/Connector/Sabre/Server.php @@ -24,6 +24,8 @@ namespace OCA\DAV\Connector\Sabre; +use OCA\DAV\DAV\CopyPlugin; + /** * Class \OCA\DAV\Connector\Sabre\Server * @@ -40,5 +42,6 @@ public function __construct($treeOrNode = null) { parent::__construct($treeOrNode); self::$exposeVersion = false; $this->enablePropfindDepthInfinity = true; + $this->addPlugin(new CopyPlugin()); } } diff --git a/apps/dav/lib/DAV/CopyPlugin.php b/apps/dav/lib/DAV/CopyPlugin.php new file mode 100644 index 000000000000..6d412fb2ac3f --- /dev/null +++ b/apps/dav/lib/DAV/CopyPlugin.php @@ -0,0 +1,103 @@ + + * + * @copyright Copyright (c) 2017, 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\DAV; + +use OCA\DAV\Connector\Sabre\Exception\Forbidden; +use OCA\DAV\Connector\Sabre\File; +use OCA\DAV\Files\ICopySource; +use OCP\Files\ForbiddenException; +use Sabre\DAV\IFile; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +/** + * Class CopyPlugin - adds own implementation of the COPY method. + * This is necessary because we don't want the target to be deleted before the move. + * + * Deleting the target will kill the versions which is the wrong behavior. + * + * @package OCA\DAV\DAV + */ +class CopyPlugin extends ServerPlugin { + + /** @var Server */ + private $server; + + /** + * @param Server $server + */ + function initialize(Server $server) { + $this->server = $server; + $server->on('method:COPY', [$this, 'httpCopy'], 90); + } + + /** + * WebDAV HTTP COPY method + * + * This method copies one uri to a different uri, and works much like the MOVE request + * A lot of the actual request processing is done in getCopyMoveInfo + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + * @throws Forbidden + */ + function httpCopy(RequestInterface $request, ResponseInterface $response) { + + try { + + $path = $request->getPath(); + + $copyInfo = $this->server->getCopyAndMoveInfo($request); + $sourceNode = $this->server->tree->getNodeForPath($path); + $destinationNode = $copyInfo['destinationNode']; + if (!$copyInfo['destinationExists'] || !$destinationNode instanceof File || !$sourceNode instanceof IFile) { + return true; + } + + if (!$this->server->emit('beforeBind', [$copyInfo['destination']])) return false; + + $copySuccess = false; + if ($sourceNode instanceof ICopySource) { + $copySuccess = $sourceNode->copy($destinationNode->getFileInfo()->getPath()); + } + if (!$copySuccess) { + $destinationNode->put($sourceNode->get()); + } + + $this->server->emit('afterBind', [$copyInfo['destination']]); + + $response->setHeader('Content-Length', '0'); + $response->setStatus(204); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } catch (ForbiddenException $ex) { + throw new Forbidden($ex->getMessage(), $ex->getRetry()); + } + } + +} diff --git a/apps/dav/lib/Files/ICopySource.php b/apps/dav/lib/Files/ICopySource.php new file mode 100644 index 000000000000..44e4ceff64ec --- /dev/null +++ b/apps/dav/lib/Files/ICopySource.php @@ -0,0 +1,43 @@ + + * + * @copyright Copyright (c) 2017, 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\Files; + + +/** + * Interface ICopySource + * This interface allows special handling of copy operations based on the copy source. + * This gives the developer the freedom to implement a more efficient copy operation. + * + * @package OCA\DAV\Files + */ +interface ICopySource { + + /** + * Copies the source to the given destination. + * If the operation was not successful false is returned. + * + * @param string $destinationPath + * @return boolean + */ + public function copy($destinationPath); +} diff --git a/apps/dav/lib/Files/IProvidesAdditionalHeaders.php b/apps/dav/lib/Files/IProvidesAdditionalHeaders.php new file mode 100644 index 000000000000..88e9c9020770 --- /dev/null +++ b/apps/dav/lib/Files/IProvidesAdditionalHeaders.php @@ -0,0 +1,44 @@ + + * + * @copyright Copyright (c) 2017, 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\Files; + + +/** + * Interface IProvidesAdditionalHeaders + * This interface allows to add additional headers to the response + * + * @package OCA\DAV\Files + */ +interface IProvidesAdditionalHeaders { + + /** + * @return array + */ + public function getHeaders(); + + /** + * @return string + */ + public function getContentDispositionFileName(); + +} diff --git a/apps/dav/lib/Meta/MetaFile.php b/apps/dav/lib/Meta/MetaFile.php index 3053f75817ee..b3e277b6361d 100644 --- a/apps/dav/lib/Meta/MetaFile.php +++ b/apps/dav/lib/Meta/MetaFile.php @@ -23,9 +23,18 @@ namespace OCA\DAV\Meta; +use OC\Files\Meta\MetaFileVersionNode; +use OCA\DAV\Files\ICopySource; +use OCA\DAV\Files\IProvidesAdditionalHeaders; use Sabre\DAV\File; -class MetaFile extends File { +/** + * Class MetaFile + * This is a Sabre based implementation of a file living in the /meta resource. + * + * @package OCA\DAV\Meta + */ +class MetaFile extends File implements ICopySource, IProvidesAdditionalHeaders { /** @var \OCP\Files\File */ private $file; @@ -74,7 +83,40 @@ public function getLastModified() { return $this->file->getMTime(); } + /** + * @inheritdoc + */ public function getETag() { return $this->file->getEtag(); } + + /** + * @inheritdoc + */ + public function copy($path) { + if ($this->file instanceof MetaFileVersionNode) { + return $this->file->copy($path); + } + return false; + } + + /** + * @return array + */ + public function getHeaders() { + if ($this->file instanceof \OCP\Files\IProvidesAdditionalHeaders) { + return $this->file->getHeaders(); + } + return []; + } + + /** + * @return string + */ + public function getContentDispositionFileName() { + if ($this->file instanceof \OCP\Files\IProvidesAdditionalHeaders) { + return $this->file->getContentDispositionFileName(); + } + return $this->getName(); + } } diff --git a/apps/dav/lib/Meta/MetaFolder.php b/apps/dav/lib/Meta/MetaFolder.php index 46082d69a777..c09973818e5b 100644 --- a/apps/dav/lib/Meta/MetaFolder.php +++ b/apps/dav/lib/Meta/MetaFolder.php @@ -28,6 +28,12 @@ use OCP\Files\Node; use Sabre\DAV\Collection; +/** + * Class MetaFolder + * This is a Sabre based implementation of a folder living in the /meta resource. + * + * @package OCA\DAV\Meta + */ class MetaFolder extends Collection { /** @var Folder */ @@ -48,7 +54,7 @@ public function __construct(Folder $folder) { function getChildren() { $nodes = $this->folder->getDirectoryListing(); return array_map(function($node) { - return static::nodeFactory($node); + return $this->nodeFactory($node); }, $nodes); } @@ -59,7 +65,7 @@ function getName() { return $this->folder->getName(); } - public static function nodeFactory(Node $node) { + private function nodeFactory(Node $node) { if ($node instanceof Folder) { return new MetaFolder($node); } diff --git a/apps/dav/lib/Meta/RootCollection.php b/apps/dav/lib/Meta/RootCollection.php index cf3b55a93bcc..59620b79895d 100644 --- a/apps/dav/lib/Meta/RootCollection.php +++ b/apps/dav/lib/Meta/RootCollection.php @@ -23,10 +23,8 @@ namespace OCA\DAV\Meta; -use OCP\Files\File; -use OCP\Files\Folder; +use OC\Files\Meta\MetaFileIdNode; use OCP\Files\IRootFolder; -use OCP\Files\Node; use OCP\Files\NotFoundException; use Sabre\DAV\Collection; use Sabre\DAV\Exception\MethodNotAllowed; @@ -52,8 +50,11 @@ public function __construct(IRootFolder $rootFolder) { public function getChild($name) { try { $child = $this->rootFolder->get("meta/$name"); - return MetaFolder::nodeFactory($child); - } catch (NotFoundException $ex) { + if (!$child instanceof MetaFileIdNode) { + throw new NotFound(); + } + return new MetaFolder($child); + } catch (NotFoundException $exception) { throw new NotFound(); } } diff --git a/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php index df2327d7b9b5..b8e445a15cdd 100644 --- a/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php @@ -25,10 +25,19 @@ */ namespace OCA\DAV\Tests\unit\Connector\Sabre; +use OC\User\User; +use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\File; use OCA\DAV\Connector\Sabre\FilesPlugin; +use OCA\DAV\Connector\Sabre\Node; +use OCA\DAV\Meta\MetaFile; use OCP\Files\StorageNotAvailableException; +use OCP\IConfig; +use OCP\IRequest; use Sabre\DAV\PropFind; use Sabre\DAV\PropPatch; +use Sabre\DAV\Server; +use Sabre\DAV\Tree; use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; use Test\TestCase; @@ -53,12 +62,12 @@ class FilesPluginTest extends TestCase { const DATA_FINGERPRINT_PROPERTYNAME = FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME; /** - * @var \Sabre\DAV\Server | \PHPUnit_Framework_MockObject_MockObject + * @var Server | \PHPUnit_Framework_MockObject_MockObject */ private $server; /** - * @var \Sabre\DAV\Tree | \PHPUnit_Framework_MockObject_MockObject + * @var Tree | \PHPUnit_Framework_MockObject_MockObject */ private $tree; @@ -68,28 +77,28 @@ class FilesPluginTest extends TestCase { private $plugin; /** - * @var \OCP\IConfig | \PHPUnit_Framework_MockObject_MockObject + * @var IConfig | \PHPUnit_Framework_MockObject_MockObject */ private $config; /** - * @var \OCP\IRequest | \PHPUnit_Framework_MockObject_MockObject + * @var IRequest | \PHPUnit_Framework_MockObject_MockObject */ private $request; public function setUp() { parent::setUp(); - $this->server = $this->getMockBuilder('\Sabre\DAV\Server') + $this->server = $this->getMockBuilder(Server::class) ->disableOriginalConstructor() ->getMock(); - $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree') + $this->tree = $this->getMockBuilder(Tree::class) ->disableOriginalConstructor() ->getMock(); - $this->config = $this->createMock('\OCP\IConfig'); + $this->config = $this->createMock(IConfig::class); $this->config->expects($this->any())->method('getSystemValue') ->with($this->equalTo('data-fingerprint'), $this->equalTo('')) ->willReturn('my_fingerprint'); - $this->request = $this->createMock('\OCP\IRequest'); + $this->request = $this->createMock(IRequest::class); $this->plugin = new FilesPlugin( $this->tree, @@ -107,6 +116,7 @@ public function setUp() { /** * @param string $class + * @param string $path * @return \PHPUnit_Framework_MockObject_MockObject */ private function createTestNode($class, $path = '/dummypath') { @@ -149,8 +159,8 @@ private function createTestNode($class, $path = '/dummypath') { } public function testGetPropertiesForFile() { - /** @var \OCA\DAV\Connector\Sabre\File | \PHPUnit_Framework_MockObject_MockObject $node */ - $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File'); + /** @var File | \PHPUnit_Framework_MockObject_MockObject $node */ + $node = $this->createTestNode(File::class); $propFind = new PropFind( '/dummyPath', @@ -168,7 +178,7 @@ public function testGetPropertiesForFile() { 0 ); - $user = $this->getMockBuilder('\OC\User\User') + $user = $this->getMockBuilder(User::class) ->disableOriginalConstructor()->getMock(); $user ->expects($this->once()) @@ -204,8 +214,8 @@ public function testGetPropertiesForFile() { } public function testGetPropertiesStorageNotAvailable() { - /** @var \OCA\DAV\Connector\Sabre\File | \PHPUnit_Framework_MockObject_MockObject $node */ - $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File'); + /** @var File | \PHPUnit_Framework_MockObject_MockObject $node */ + $node = $this->createTestNode(File::class); $propFind = new PropFind( '/dummyPath', @@ -231,7 +241,7 @@ public function testGetPublicPermissions() { $this->plugin = new FilesPlugin( $this->tree, $this->config, - $this->createMock('\OCP\IRequest'), + $this->request, true); $this->plugin->initialize($this->server); @@ -243,8 +253,8 @@ public function testGetPublicPermissions() { 0 ); - /** @var \OCA\DAV\Connector\Sabre\File | \PHPUnit_Framework_MockObject_MockObject $node */ - $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File'); + /** @var File | \PHPUnit_Framework_MockObject_MockObject $node */ + $node = $this->createTestNode(File::class); $node->expects($this->any()) ->method('getDavPermissions') ->will($this->returnValue('DWCKMSR')); @@ -258,8 +268,8 @@ public function testGetPublicPermissions() { } public function testGetPropertiesForDirectory() { - /** @var \OCA\DAV\Connector\Sabre\Directory | \PHPUnit_Framework_MockObject_MockObject $node */ - $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\Directory'); + /** @var Directory | \PHPUnit_Framework_MockObject_MockObject $node */ + $node = $this->createTestNode(Directory::class); $propFind = new PropFind( '/dummyPath', @@ -293,8 +303,8 @@ public function testGetPropertiesForDirectory() { } public function testGetPropertiesForRootDirectory() { - /** @var \OCA\DAV\Connector\Sabre\Directory | \PHPUnit_Framework_MockObject_MockObject $node */ - $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory') + /** @var Directory | \PHPUnit_Framework_MockObject_MockObject $node */ + $node = $this->getMockBuilder(Directory::class) ->disableOriginalConstructor() ->getMock(); $node->expects($this->any())->method('getPath')->willReturn('/'); @@ -328,8 +338,8 @@ public function testGetPropertiesForRootDirectory() { * @expectedException \Sabre\DAV\Exception\NotFound */ public function testGetPropertiesWhenNoPermission() { - /** @var \OCA\DAV\Connector\Sabre\Directory | \PHPUnit_Framework_MockObject_MockObject $node */ - $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory') + /** @var Directory | \PHPUnit_Framework_MockObject_MockObject $node */ + $node = $this->getMockBuilder(Directory::class) ->disableOriginalConstructor() ->getMock(); $node->expects($this->any())->method('getPath')->willReturn('/'); @@ -358,7 +368,7 @@ public function testGetPropertiesWhenNoPermission() { } public function testUpdateProps() { - $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File'); + $node = $this->createTestNode(File::class); $testDate = 'Fri, 13 Feb 2015 00:01:02 GMT'; @@ -433,14 +443,14 @@ public function testUpdatePropsForbidden() { * @expectedExceptionMessage FolderA/test.txt cannot be deleted */ public function testMoveSrcNotDeletable() { - $fileInfoFolderATestTXT = $this->getMockBuilder('\OCP\Files\FileInfo') + $fileInfoFolderATestTXT = $this->getMockBuilder(FileInfo::class) ->disableOriginalConstructor() ->getMock(); $fileInfoFolderATestTXT->expects($this->once()) ->method('isDeletable') ->willReturn(false); - $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Node') + $node = $this->getMockBuilder(Node::class) ->disableOriginalConstructor() ->getMock(); $node->expects($this->once()) @@ -454,14 +464,14 @@ public function testMoveSrcNotDeletable() { } public function testMoveSrcDeletable() { - $fileInfoFolderATestTXT = $this->getMockBuilder('\OCP\Files\FileInfo') + $fileInfoFolderATestTXT = $this->getMockBuilder(FileInfo::class) ->disableOriginalConstructor() ->getMock(); $fileInfoFolderATestTXT->expects($this->once()) ->method('isDeletable') ->willReturn(true); - $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Node') + $node = $this->getMockBuilder(Node::class) ->disableOriginalConstructor() ->getMock(); $node->expects($this->once()) @@ -479,7 +489,7 @@ public function testMoveSrcDeletable() { * @expectedExceptionMessage FolderA/test.txt does not exist */ public function testMoveSrcNotExist() { - $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Node') + $node = $this->getMockBuilder(Node::class) ->disableOriginalConstructor() ->getMock(); $node->expects($this->once()) @@ -507,21 +517,25 @@ public function downloadHeadersProvider() { /** * @dataProvider downloadHeadersProvider + * @param boolean $isClumsyAgent + * @param string $contentDispositionHeader */ public function testDownloadHeaders($isClumsyAgent, $contentDispositionHeader) { + /** @var RequestInterface | \PHPUnit_Framework_MockObject_MockObject $request */ $request = $this->getMockBuilder(RequestInterface::class) ->disableOriginalConstructor() ->getMock(); + /** @var ResponseInterface | \PHPUnit_Framework_MockObject_MockObject $response */ $response = $this->getMockBuilder(ResponseInterface::class) - ->disableOriginalConstructor() - ->getMock(); + ->disableOriginalConstructor() + ->getMock(); $request ->expects($this->once()) ->method('getPath') ->will($this->returnValue('test/somefile.xml')); - $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File') + $node = $this->getMockBuilder(File::class) ->disableOriginalConstructor() ->getMock(); $node @@ -529,6 +543,11 @@ public function testDownloadHeaders($isClumsyAgent, $contentDispositionHeader) { ->method('getName') ->will($this->returnValue('somefile.xml')); + $node->expects($this->once()) + ->method('getChecksum') + ->with('sha1') + ->willReturn('abcdefghijkl'); + $this->tree ->expects($this->once()) ->method('getNodeForPath') @@ -541,13 +560,60 @@ public function testDownloadHeaders($isClumsyAgent, $contentDispositionHeader) { ->will($this->returnValue($isClumsyAgent)); $response - ->expects($this->exactly(2)) + ->expects($this->exactly(3)) ->method('addHeader') ->withConsecutive( ['Content-Disposition', $contentDispositionHeader], + ['OC-Checksum', 'abcdefghijkl'], ['X-Accel-Buffering', 'no'] ); $this->plugin->httpGet($request, $response); } + + public function testAdditionalHeaders() { + /** @var RequestInterface | \PHPUnit_Framework_MockObject_MockObject $request */ + $request = $this->getMockBuilder(RequestInterface::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var ResponseInterface | \PHPUnit_Framework_MockObject_MockObject $response */ + $response = $this->getMockBuilder(ResponseInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $request + ->expects($this->once()) + ->method('getPath') + ->willReturn('test/somefile.xml'); + + $node = $this->getMockBuilder(MetaFile::class) + ->disableOriginalConstructor() + ->getMock(); + $node + ->expects($this->once()) + ->method('getName') + ->willReturn('somefile.xml'); + + $this->tree + ->expects($this->once()) + ->method('getNodeForPath') + ->with('test/somefile.xml') + ->willReturn($node); + + $node + ->expects($this->once()) + ->method('getHeaders') + ->willReturn([ + 'a' => 'b' + ]); + + $response + ->expects($this->once()) + ->method('addHeaders') + ->with( + ['a' => 'b'] + ); + + $this->plugin->httpGet($request, $response); + } } diff --git a/apps/dav/tests/unit/DAV/CopyPluginTest.php b/apps/dav/tests/unit/DAV/CopyPluginTest.php new file mode 100644 index 000000000000..1bf14c8c9aa7 --- /dev/null +++ b/apps/dav/tests/unit/DAV/CopyPluginTest.php @@ -0,0 +1,118 @@ + + * + * @copyright Copyright (c) 2017, 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\DAV; + + +use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\File; +use OCA\DAV\DAV\CopyPlugin; +use Sabre\DAV\ICollection; +use Sabre\DAV\IFile; +use Sabre\DAV\Server; +use Sabre\DAV\Tree; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Test\TestCase; + +class CopyPluginTest extends TestCase { + + /** @var Server | \PHPUnit_Framework_MockObject_MockObject */ + private $server; + /** @var CopyPlugin */ + private $plugin; + /** @var Tree | \PHPUnit_Framework_MockObject_MockObject */ + private $tree; + /** @var RequestInterface | \PHPUnit_Framework_MockObject_MockObject */ + private $request; + /** @var ResponseInterface | \PHPUnit_Framework_MockObject_MockObject */ + private $response; + + public function setUp() { + parent::setUp(); + $this->plugin = new CopyPlugin(); + + $this->server = $this->createMock(Server::class); + $this->tree = $this->createMock(Tree::class); + $this->server->tree = $this->tree; + /** @var RequestInterface | \PHPUnit_Framework_MockObject_MockObject $request */ + $this->request = $this->createMock(RequestInterface::class); + /** @var ResponseInterface | \PHPUnit_Framework_MockObject_MockObject $response */ + $this->response = $this->createMock( ResponseInterface::class); + + $this->plugin->initialize($this->server); + } + + /** + * @dataProvider providesSourceAndDestinations + * @param bool $destinationExists + * @param $destinationNode + * @param $sourceNode + */ + public function testCopyPluginReturnTrue($destinationExists, $destinationNode, $sourceNode) { + + $this->tree->expects($this->once())->method('getNodeForPath')->willReturn($sourceNode); + $this->server->expects($this->once())->method('getCopyAndMoveInfo')->willReturn([ + 'destinationExists' => $destinationExists, + 'destinationNode' => $destinationNode + ]); + + $returnValue = $this->plugin->httpCopy($this->request, $this->response); + $this->assertTrue($returnValue); + } + + public function providesSourceAndDestinations() { + return [ + 'destination does not exist' => [false, null, null], + 'destination is not a File' => [true, $this->createMock(Directory::class), $this->createMock(IFile::class)], + 'source is not a IFile' => [true, $this->createMock(File::class), $this->createMock(ICollection::class)], + ]; + } + + public function testCopyPluginReturnFalse() { + + $destinationNode = $this->createMock(File::class); + $sourceNode = $this->createMock(IFile::class); + + $this->tree->expects($this->once())->method('getNodeForPath')->willReturn($sourceNode); + $this->server->expects($this->once())->method('getCopyAndMoveInfo')->willReturn([ + 'destinationExists' => true, + 'destinationNode' => $destinationNode, + 'destination' => 'destination.txt' + ]); + + // make sure the plugin properly emits beforeBind and afterBind + $this->server->expects($this->exactly(2))->method('emit')->withConsecutive( + ['beforeBind', ['destination.txt']], ['afterBind', ['destination.txt']])->willReturn(true); + + // make sure the file content is actually copied over + $sourceNode->expects($this->once())->method('get')->willReturn('123456'); + $destinationNode->expects($this->once())->method('put')->with('123456'); + + // make sure http status and content length are properly set + $this->response->expects($this->once())->method('setHeader')->with('Content-Length', '0'); + $this->response->expects($this->once())->method('setStatus')->with(204); + + $returnValue = $this->plugin->httpCopy($this->request, $this->response); + $this->assertFalse($returnValue); + } +} diff --git a/apps/files_versions/ajax/getVersions.php b/apps/files_versions/ajax/getVersions.php deleted file mode 100644 index e7d3a0b011d3..000000000000 --- a/apps/files_versions/ajax/getVersions.php +++ /dev/null @@ -1,56 +0,0 @@ - - * @author Björn Schießle - * @author Frank Karlitschek - * @author Lukas Reschke - * @author Sam Tuke - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2017, 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 - * - */ -OCP\JSON::checkLoggedIn(); -OCP\JSON::callCheck(); -OCP\JSON::checkAppEnabled('files_versions'); - -$source = (string)$_GET['source']; -$start = (int)$_GET['start']; -list ($uid, $filename) = OCA\Files_Versions\Storage::getUidAndFilename($source); -$count = 5; //show the newest revisions -$versions = OCA\Files_Versions\Storage::getVersions($uid, $filename, $source); -if( $versions ) { - - $endReached = false; - if (count($versions) <= $start+$count) { - $endReached = true; - } - - $versions = array_slice($versions, $start, $count); - - // remove owner path from request to not disclose it to the recipient - foreach ($versions as $version) { - unset($version['path']); - } - - \OCP\JSON::success(['data' => ['versions' => $versions, 'endReached' => $endReached]]); - -} else { - - \OCP\JSON::success(['data' => ['versions' => [], 'endReached' => true]]); - -} diff --git a/apps/files_versions/ajax/rollbackVersion.php b/apps/files_versions/ajax/rollbackVersion.php deleted file mode 100644 index 1d5f533eb297..000000000000 --- a/apps/files_versions/ajax/rollbackVersion.php +++ /dev/null @@ -1,40 +0,0 @@ - - * @author Björn Schießle - * @author Frank Karlitschek - * @author Lukas Reschke - * @author Robin Appelman - * @author Sam Tuke - * @author Thomas Müller - * @author Thomas Tanghus - * - * @copyright Copyright (c) 2017, 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 - * - */ -OCP\JSON::checkLoggedIn(); -OCP\JSON::checkAppEnabled('files_versions'); -OCP\JSON::callCheck(); - -$file = (string)$_GET['file']; -$revision=(int)$_GET['revision']; - -if(OCA\Files_Versions\Storage::rollback( $file, $revision )) { - OCP\JSON::success(["data" => ["revision" => $revision, "file" => $file]]); -}else{ - $l = \OC::$server->getL10N('files_versions'); - OCP\JSON::error(["data" => ["message" => $l->t("Could not revert: %s", [$file])]]); -} diff --git a/apps/files_versions/appinfo/routes.php b/apps/files_versions/appinfo/routes.php index 25bc2d5cf68b..5e28348619c8 100644 --- a/apps/files_versions/appinfo/routes.php +++ b/apps/files_versions/appinfo/routes.php @@ -35,10 +35,3 @@ function() { require_once __DIR__ . '/../ajax/preview.php'; }); -$this->create('files_versions_download', 'download.php') - ->actionInclude('files_versions/download.php'); -$this->create('files_versions_ajax_getVersions', 'ajax/getVersions.php') - ->actionInclude('files_versions/ajax/getVersions.php'); -$this->create('files_versions_ajax_rollbackVersion', 'ajax/rollbackVersion.php') - ->actionInclude('files_versions/ajax/rollbackVersion.php'); - diff --git a/apps/files_versions/download.php b/apps/files_versions/download.php deleted file mode 100644 index 28ba05535255..000000000000 --- a/apps/files_versions/download.php +++ /dev/null @@ -1,48 +0,0 @@ - - * @author Björn Schießle - * @author Lukas Reschke - * @author Morris Jobke - * @author Roeland Jago Douma - * @author Vincent Petry - * - * @copyright Copyright (c) 2017, 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 - * - */ - -OCP\JSON::checkAppEnabled('files_versions'); -OCP\JSON::checkLoggedIn(); - -$file = $_GET['file']; -$revision=(int)$_GET['revision']; - -list($uid, $filename) = OCA\Files_Versions\Storage::getUidAndFilename($file); - -$versionName = '/'.$uid.'/files_versions/'.$filename.'.v'.$revision; - -$view = new OC\Files\View('/'); - -$ftype = \OC::$server->getMimeTypeDetector()->getSecureMimeType($view->getMimeType('/'.$uid.'/files/'.$filename)); - -header('Content-Type:'.$ftype); -OCP\Response::setContentDispositionHeader(basename($filename), 'attachment'); -OCP\Response::disableCaching(); -OCP\Response::setContentLengthHeader($view->filesize($versionName)); - -OC_Util::obEnd(); - -$view->readfile($versionName); diff --git a/apps/files_versions/js/versioncollection.js b/apps/files_versions/js/versioncollection.js index fdb12bae0a95..5ec4e21586ea 100644 --- a/apps/files_versions/js/versioncollection.js +++ b/apps/files_versions/js/versioncollection.js @@ -13,6 +13,8 @@ * @memberof OCA.Versions */ var VersionCollection = OC.Backbone.Collection.extend({ + sync: OC.Backbone.davSync, + model: OCA.Versions.VersionModel, /** @@ -20,72 +22,34 @@ */ _fileInfo: null, - _endReached: false, - _currentIndex: 0, - url: function() { - var url = OC.generateUrl('/apps/files_versions/ajax/getVersions.php'); - var query = { - source: this._fileInfo.getFullPath(), - start: this._currentIndex - }; - return url + '?' + OC.buildQueryString(query); + return OC.linkToRemoteBase('dav') + '/meta/' + + encodeURIComponent(this._fileInfo.get('id')) + '/v'; }, setFileInfo: function(fileInfo) { this._fileInfo = fileInfo; - // reset - this._endReached = false; - this._currentIndex = 0; }, getFileInfo: function() { return this._fileInfo; }, - hasMoreResults: function() { - return !this._endReached; - }, - - fetch: function(options) { - if (!options || options.remove) { - this._currentIndex = 0; - } - return OC.Backbone.Collection.prototype.fetch.apply(this, arguments); - }, - - /** - * Fetch the next set of results - */ - fetchNext: function() { - if (!this.hasMoreResults()) { - return null; - } - if (this._currentIndex === 0) { - return this.fetch(); - } - return this.fetch({remove: false}); - }, - - reset: function() { - this._currentIndex = 0; - OC.Backbone.Collection.prototype.reset.apply(this, arguments); - }, - parse: function(result) { var fullPath = this._fileInfo.getFullPath(); - var results = _.map(result.data.versions, function(version) { - var revision = parseInt(version.version, 10); + var fileId = this._fileInfo.get('id'); + var results = _.map(result, function(version) { + var revision = parseInt(version.id, 10); return { id: revision, - name: version.name, + name: revision, fullPath: fullPath, timestamp: revision, - size: version.size + versionId: revision, + size: version['{DAV:}getcontentlength'], + fileId: fileId }; }); - this._endReached = result.data.endReached; - this._currentIndex += results.length; return results; } }); diff --git a/apps/files_versions/js/versionmodel.js b/apps/files_versions/js/versionmodel.js index dc610fc21449..5551aa2430d2 100644 --- a/apps/files_versions/js/versionmodel.js +++ b/apps/files_versions/js/versionmodel.js @@ -20,31 +20,25 @@ revert: function(options) { options = options ? _.clone(options) : {}; var model = this; - var file = this.getFullPath(); - var revision = this.get('timestamp'); + var client = new OC.Files.Client({ + host: OC.getHost(), + root: OC.linkToRemoteBase('dav') + '/files/' + OC.getCurrentUser().uid, + useHTTPS: OC.getProtocol() === 'https' + }); - $.ajax({ - type: 'GET', - url: OC.generateUrl('/apps/files_versions/ajax/rollbackVersion.php'), - dataType: 'json', - data: { - file: file, - revision: revision - }, - success: function(response) { - if (response.status === 'error') { - if (options.error) { - options.error.call(options.context, model, response, options); - } - model.trigger('error', model, response, options); - } else { - if (options.success) { - options.success.call(options.context, model, response, options); - } - model.trigger('revert', model, response, options); + client.copy(this.getDownloadUrl(), this.getFullPath(), true, {}, {pathIsUrl: true}) + .done(function() { + if (options.success) { + options.success.call(options.context, model, {}, options); } - } - }); + model.trigger('revert', model, options); + }) + .fail(function () { + if (options.error) { + options.error.call(options.context, model, {}, options); + } + model.trigger('error', model, {}, options); + }); }, getFullPath: function() { @@ -61,12 +55,9 @@ }, getDownloadUrl: function() { - var url = OC.generateUrl('/apps/files_versions/download.php'); - var params = { - file: this.get('fullPath'), - revision: this.get('timestamp') - }; - return url + '?' + OC.buildQueryString(params); + return OC.linkToRemoteBase('dav') + '/meta/' + + encodeURIComponent(this.get('fileId')) + '/v/' + + encodeURIComponent(this.get('versionId')); } }); diff --git a/apps/files_versions/js/versionstabview.js b/apps/files_versions/js/versionstabview.js index abbfd4502db7..1faaf2d89884 100644 --- a/apps/files_versions/js/versionstabview.js +++ b/apps/files_versions/js/versionstabview.js @@ -37,8 +37,6 @@ '
    ' + '
    ' + '' + - '' + ''; /** @@ -54,8 +52,7 @@ $versionsContainer: null, events: { - 'click .revertVersion': '_onClickRevertVersion', - 'click .showMoreVersions': '_onClickShowMoreVersions' + 'click .revertVersion': '_onClickRevertVersion' }, initialize: function() { @@ -73,19 +70,14 @@ }, nextPage: function() { - if (this._loading || !this.collection.hasMoreResults()) { + if (this._loading) { return; } if (this.collection.getFileInfo() && this.collection.getFileInfo().isDirectory()) { return; } - this.collection.fetchNext(); - }, - - _onClickShowMoreVersions: function(ev) { - ev.preventDefault(); - this.nextPage(); + this.collection.fetch(); }, _onClickRevertVersion: function(ev) { @@ -100,8 +92,6 @@ ev.preventDefault(); revision = $target.attr('data-revision'); - this.$el.find('.versions, .showMoreVersions').addClass('hidden'); - var versionModel = this.collection.get(revision); versionModel.revert({ success: function() { @@ -109,7 +99,7 @@ self.$versionsContainer.empty(); self.collection.setFileInfo(fileInfoModel); self.collection.reset([], {silent: true}); - self.collection.fetchNext(); + self.collection.fetch(); self.$el.find('.versions').removeClass('hidden'); @@ -151,13 +141,11 @@ _onRequest: function() { this._toggleLoading(true); - this.$el.find('.showMoreVersions').addClass('hidden'); }, _onEndRequest: function() { this._toggleLoading(false); this.$el.find('.empty').toggleClass('hidden', !!this.collection.length); - this.$el.find('.showMoreVersions').toggleClass('hidden', !this.collection.hasMoreResults()); }, _onAddModel: function(model) { @@ -218,7 +206,6 @@ render: function() { this.$el.html(this.template({ emptyResultLabel: t('files_versions', 'No other versions available'), - moreVersionsLabel: t('files_versions', 'More versions...') })); this.$el.find('.has-tooltip').tooltip(); this.$versionsContainer = this.$el.find('ul.versions'); diff --git a/apps/files_versions/lib/Storage.php b/apps/files_versions/lib/Storage.php index 95866d2824a7..130be95b22e3 100644 --- a/apps/files_versions/lib/Storage.php +++ b/apps/files_versions/lib/Storage.php @@ -309,68 +309,53 @@ public static function renameOrCopy($sourcePath, $targetPath, $operation) { } - /** - * Rollback to an old version of a file. - * - * @param string $file file name - * @param int $revision revision timestamp - */ - public static function rollback($file, $revision) { - - if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') { - // add expected leading slash - $file = '/' . ltrim($file, '/'); - list($uid, $filename) = self::getUidAndFilename($file); - if ($uid === null || trim($filename, '/') === '') { - return false; - } - $users_view = new View('/'.$uid); - $files_view = new View('/'. User::getUser().'/files'); - if (!$files_view->isUpdatable($filename)) { - return false; - } - - $versionCreated = false; + public static function restoreVersion($uid, $filename, $fileToRestore, $revision) { + if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED) !== true) { + return false; + } + $users_view = new View('/'.$uid); + if (!$users_view->isUpdatable("/files$filename")) { + return false; + } - //first create a new version - $version = 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename); - if (!$users_view->file_exists($version)) { - $users_view->copy('files'.$filename, 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename)); - $versionCreated = true; - } + $versionCreated = false; - $fileToRestore = 'files_versions' . $filename . '.v' . $revision; - - // Restore encrypted version of the old file for the newly restored file - // This has to happen manually here since the file is manually copied below - $oldVersion = $users_view->getFileInfo($fileToRestore)->getEncryptedVersion(); - $oldFileInfo = $users_view->getFileInfo($fileToRestore); - $newFileInfo = $files_view->getFileInfo($filename); - $cache = $newFileInfo->getStorage()->getCache(); - $cache->update( - $newFileInfo->getId(), [ - 'encrypted' => $oldVersion, - 'encryptedVersion' => $oldVersion, - 'size' => $oldFileInfo->getSize() - ] - ); - - // rollback - if (self::copyFileContents($users_view, $fileToRestore, 'files' . $filename)) { - $files_view->touch($file, $revision); - Storage::scheduleExpire($uid, $file); - \OC_Hook::emit('\OCP\Versions', 'rollback', [ - 'path' => $filename, - 'revision' => $revision, - ]); - return true; - } else if ($versionCreated) { - self::deleteVersion($users_view, $version); - } + //first create a new version + $version = 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename); + if (!$users_view->file_exists($version)) { + $users_view->copy('files'.$filename, 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename)); + $versionCreated = true; } - return false; + // Restore encrypted version of the old file for the newly restored file + // This has to happen manually here since the file is manually copied below + $oldVersion = $users_view->getFileInfo($fileToRestore)->getEncryptedVersion(); + $oldFileInfo = $users_view->getFileInfo($fileToRestore); + $newFileInfo = $users_view->getFileInfo("/files$filename"); + $cache = $newFileInfo->getStorage()->getCache(); + $cache->update( + $newFileInfo->getId(), [ + 'encrypted' => $oldVersion, + 'encryptedVersion' => $oldVersion, + 'size' => $oldFileInfo->getSize() + ] + ); + + // rollback + if (self::copyFileContents($users_view, $fileToRestore, 'files' . $filename)) { + $users_view->touch("/files$filename", $revision); + Storage::scheduleExpire($uid, $filename); + \OC_Hook::emit('\OCP\Versions', 'rollback', [ + 'path' => $filename, + 'user' => $uid, + 'revision' => $revision, + ]); + return true; + } else if ($versionCreated) { + self::deleteVersion($users_view, $version); + } + return true; } /** diff --git a/apps/files_versions/tests/VersioningTest.php b/apps/files_versions/tests/VersioningTest.php index e9b22e31455e..13c1e8f5a492 100644 --- a/apps/files_versions/tests/VersioningTest.php +++ b/apps/files_versions/tests/VersioningTest.php @@ -118,57 +118,6 @@ protected function tearDown() { parent::tearDown(); } - - public function testMoveFileIntoSharedFolderAsRecipient() { - - \OC\Files\Filesystem::mkdir('folder1'); - $fileInfo = \OC\Files\Filesystem::getFileInfo('folder1'); - - $node = \OC::$server->getUserFolder(self::TEST_VERSIONS_USER)->get('folder1'); - $share = \OC::$server->getShareManager()->newShare(); - $share->setNode($node) - ->setShareType(\OCP\Share::SHARE_TYPE_USER) - ->setSharedBy(self::TEST_VERSIONS_USER) - ->setSharedWith(self::TEST_VERSIONS_USER2) - ->setPermissions(\OCP\Constants::PERMISSION_ALL); - $share = \OC::$server->getShareManager()->createShare($share); - - self::loginHelper(self::TEST_VERSIONS_USER2); - $versionsFolder2 = '/' . self::TEST_VERSIONS_USER2 . '/files_versions'; - \OC\Files\Filesystem::file_put_contents('test.txt', 'test file'); - - $t1 = time(); - // second version is two weeks older, this way we make sure that no - // version will be expired - $t2 = $t1 - 60 * 60 * 24 * 14; - - $this->rootView->mkdir($versionsFolder2); - // create some versions - $v1 = $versionsFolder2 . '/test.txt.v' . $t1; - $v2 = $versionsFolder2 . '/test.txt.v' . $t2; - - $this->rootView->file_put_contents($v1, 'version1'); - $this->rootView->file_put_contents($v2, 'version2'); - - // move file into the shared folder as recipient - \OC\Files\Filesystem::rename('/test.txt', '/folder1/test.txt'); - - $this->assertFalse($this->rootView->file_exists($v1)); - $this->assertFalse($this->rootView->file_exists($v2)); - - self::loginHelper(self::TEST_VERSIONS_USER); - - $versionsFolder1 = '/' . self::TEST_VERSIONS_USER . '/files_versions'; - - $v1Renamed = $versionsFolder1 . '/folder1/test.txt.v' . $t1; - $v2Renamed = $versionsFolder1 . '/folder1/test.txt.v' . $t2; - - $this->assertTrue($this->rootView->file_exists($v1Renamed)); - $this->assertTrue($this->rootView->file_exists($v2Renamed)); - - \OC::$server->getShareManager()->deleteShare($share); - } - /** * @medium * test expire logic @@ -430,106 +379,6 @@ public function testMoveFolder() { } - public function testMoveFolderIntoSharedFolderAsRecipient() { - - \OC\Files\Filesystem::mkdir('folder1'); - - $node = \OC::$server->getUserFolder(self::TEST_VERSIONS_USER)->get('folder1'); - $share = \OC::$server->getShareManager()->newShare(); - $share->setNode($node) - ->setShareType(\OCP\Share::SHARE_TYPE_USER) - ->setSharedBy(self::TEST_VERSIONS_USER) - ->setSharedWith(self::TEST_VERSIONS_USER2) - ->setPermissions(\OCP\Constants::PERMISSION_ALL); - $share = \OC::$server->getShareManager()->createShare($share); - - self::loginHelper(self::TEST_VERSIONS_USER2); - $versionsFolder2 = '/' . self::TEST_VERSIONS_USER2 . '/files_versions'; - \OC\Files\Filesystem::mkdir('folder2'); - \OC\Files\Filesystem::file_put_contents('folder2/test.txt', 'test file'); - - $t1 = time(); - // second version is two weeks older, this way we make sure that no - // version will be expired - $t2 = $t1 - 60 * 60 * 24 * 14; - - $this->rootView->mkdir($versionsFolder2); - $this->rootView->mkdir($versionsFolder2 . '/folder2'); - // create some versions - $v1 = $versionsFolder2 . '/folder2/test.txt.v' . $t1; - $v2 = $versionsFolder2 . '/folder2/test.txt.v' . $t2; - - $this->rootView->file_put_contents($v1, 'version1'); - $this->rootView->file_put_contents($v2, 'version2'); - - // move file into the shared folder as recipient - \OC\Files\Filesystem::rename('/folder2', '/folder1/folder2'); - - $this->assertFalse($this->rootView->file_exists($v1)); - $this->assertFalse($this->rootView->file_exists($v2)); - - self::loginHelper(self::TEST_VERSIONS_USER); - - $versionsFolder1 = '/' . self::TEST_VERSIONS_USER . '/files_versions'; - - $v1Renamed = $versionsFolder1 . '/folder1/folder2/test.txt.v' . $t1; - $v2Renamed = $versionsFolder1 . '/folder1/folder2/test.txt.v' . $t2; - - $this->assertTrue($this->rootView->file_exists($v1Renamed)); - $this->assertTrue($this->rootView->file_exists($v2Renamed)); - - \OC::$server->getShareManager()->deleteShare($share); - } - - public function testRenameSharedFile() { - - \OC\Files\Filesystem::file_put_contents("test.txt", "test file"); - - $t1 = time(); - // second version is two weeks older, this way we make sure that no - // version will be expired - $t2 = $t1 - 60 * 60 * 24 * 14; - - $this->rootView->mkdir(self::USERS_VERSIONS_ROOT); - // create some versions - $v1 = self::USERS_VERSIONS_ROOT . '/test.txt.v' . $t1; - $v2 = self::USERS_VERSIONS_ROOT . '/test.txt.v' . $t2; - // the renamed versions should not exist! Because we only moved the mount point! - $v1Renamed = self::USERS_VERSIONS_ROOT . '/test2.txt.v' . $t1; - $v2Renamed = self::USERS_VERSIONS_ROOT . '/test2.txt.v' . $t2; - - $this->rootView->file_put_contents($v1, 'version1'); - $this->rootView->file_put_contents($v2, 'version2'); - - $node = \OC::$server->getUserFolder(self::TEST_VERSIONS_USER)->get('test.txt'); - $share = \OC::$server->getShareManager()->newShare(); - $share->setNode($node) - ->setShareType(\OCP\Share::SHARE_TYPE_USER) - ->setSharedBy(self::TEST_VERSIONS_USER) - ->setSharedWith(self::TEST_VERSIONS_USER2) - ->setPermissions(\OCP\Constants::PERMISSION_READ | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_SHARE); - $share = \OC::$server->getShareManager()->createShare($share); - - self::loginHelper(self::TEST_VERSIONS_USER2); - - $this->assertTrue(\OC\Files\Filesystem::file_exists('test.txt')); - - // execute rename hook of versions app - \OC\Files\Filesystem::rename('test.txt', 'test2.txt'); - - self::loginHelper(self::TEST_VERSIONS_USER); - - $this->runCommands(); - - $this->assertTrue($this->rootView->file_exists($v1)); - $this->assertTrue($this->rootView->file_exists($v2)); - - $this->assertFalse($this->rootView->file_exists($v1Renamed)); - $this->assertFalse($this->rootView->file_exists($v2Renamed)); - - \OC::$server->getShareManager()->deleteShare($share); - } - public function testCopy() { \OC\Files\Filesystem::file_put_contents("test.txt", "test file"); @@ -639,43 +488,9 @@ public function testRestoreCrossStorage() { $this->doTestRestore(); } - public function testRestoreNoPermission() { - $this->loginAsUser(self::TEST_VERSIONS_USER); - - $userHome = \OC::$server->getUserFolder(self::TEST_VERSIONS_USER); - $node = $userHome->newFolder('folder'); - $file = $node->newFile('test.txt'); - - $share = \OC::$server->getShareManager()->newShare(); - $share->setNode($node) - ->setShareType(\OCP\Share::SHARE_TYPE_USER) - ->setSharedBy(self::TEST_VERSIONS_USER) - ->setSharedWith(self::TEST_VERSIONS_USER2) - ->setPermissions(\OCP\Constants::PERMISSION_READ); - $share = \OC::$server->getShareManager()->createShare($share); - - $versions = $this->createAndCheckVersions( - \OC\Files\Filesystem::getView(), - 'folder/test.txt' - ); - - $file->putContent('test file'); - - $this->loginAsUser(self::TEST_VERSIONS_USER2); - - $firstVersion = current($versions); - - $this->assertFalse(\OCA\Files_Versions\Storage::rollback('folder/test.txt', $firstVersion['version']), 'Revert did not happen'); - - $this->loginAsUser(self::TEST_VERSIONS_USER); - - \OC::$server->getShareManager()->deleteShare($share); - $this->assertEquals('test file', $file->getContent(), 'File content has not changed'); - } - /** * @param string $hookName name of hook called - * @param string $params variable to receive parameters provided by hook + * @param array $params variable to receive parameters provided by hook */ private function connectMockHooks($hookName, &$params) { if ($hookName === null) { @@ -733,14 +548,15 @@ private function doTestRestore() { $params = []; $this->connectMockHooks('rollback', $params); - $this->assertTrue(\OCA\Files_Versions\Storage::rollback('sub/test.txt', $t2)); + $v = $oldVersions["$t2#test.txt"]; + $this->assertTrue(\OCA\Files_Versions\Storage::restoreVersion(self::TEST_VERSIONS_USER, $v['path'], $v['storage_location'], $t2)); $expectedParams = [ 'path' => '/sub/test.txt', + 'user' => self::TEST_VERSIONS_USER, + 'revision' => $t2 ]; - $this->assertEquals($expectedParams['path'], $params['path']); - $this->assertTrue(array_key_exists('revision', $params)); - $this->assertTrue($params['revision'] > 0); + $this->assertEquals($expectedParams, $params); $this->assertEquals('version2', $this->rootView->file_get_contents($filePath)); $info2 = $this->rootView->getFileInfo($filePath); diff --git a/apps/files_versions/tests/js/versioncollectionSpec.js b/apps/files_versions/tests/js/versioncollectionSpec.js deleted file mode 100644 index 87065fa1d361..000000000000 --- a/apps/files_versions/tests/js/versioncollectionSpec.js +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (c) 2015 - * - * This file is licensed under the Affero General Public License version 3 - * or later. - * - * See the COPYING-README file. - * - */ -describe('OCA.Versions.VersionCollection', function() { - var VersionCollection = OCA.Versions.VersionCollection; - var collection, fileInfoModel; - - beforeEach(function() { - fileInfoModel = new OCA.Files.FileInfoModel({ - path: '/subdir', - name: 'some file.txt' - }); - collection = new VersionCollection(); - collection.setFileInfo(fileInfoModel); - }); - it('fetches the next page', function() { - collection.fetchNext(); - - expect(fakeServer.requests.length).toEqual(1); - expect(fakeServer.requests[0].url).toEqual( - OC.generateUrl('apps/files_versions/ajax/getVersions.php') + - '?source=%2Fsubdir%2Fsome%20file.txt&start=0' - ); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - status: 'success', - data: { - endReached: false, - versions: [{ - version: 10000000, - size: 123, - name: 'some file.txt', - fullPath: '/subdir/some file.txt' - },{ - version: 15000000, - size: 150, - name: 'some file.txt', - path: '/subdir/some file.txt' - }] - } - }) - ); - - expect(collection.length).toEqual(2); - expect(collection.hasMoreResults()).toEqual(true); - - collection.fetchNext(); - - expect(fakeServer.requests.length).toEqual(2); - expect(fakeServer.requests[1].url).toEqual( - OC.generateUrl('apps/files_versions/ajax/getVersions.php') + - '?source=%2Fsubdir%2Fsome%20file.txt&start=2' - ); - fakeServer.requests[1].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - status: 'success', - data: { - endReached: true, - versions: [{ - version: 18000000, - size: 123, - name: 'some file.txt', - path: '/subdir/some file.txt' - }] - } - }) - ); - - expect(collection.length).toEqual(3); - expect(collection.hasMoreResults()).toEqual(false); - - collection.fetchNext(); - - // no further requests - expect(fakeServer.requests.length).toEqual(2); - }); - it('properly parses the results', function() { - collection.fetchNext(); - - expect(fakeServer.requests.length).toEqual(1); - expect(fakeServer.requests[0].url).toEqual( - OC.generateUrl('apps/files_versions/ajax/getVersions.php') + - '?source=%2Fsubdir%2Fsome%20file.txt&start=0' - ); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - status: 'success', - data: { - endReached: false, - versions: [{ - version: 10000000, - size: 123, - name: 'some file.txt', - path: '/subdir/some file.txt' - },{ - version: 15000000, - size: 150, - name: 'some file.txt', - path: '/subdir/some file.txt' - }] - } - }) - ); - - expect(collection.length).toEqual(2); - - var model = collection.at(0); - expect(model.get('id')).toEqual(10000000); - expect(model.get('timestamp')).toEqual(10000000); - expect(model.get('name')).toEqual('some file.txt'); - expect(model.get('fullPath')).toEqual('/subdir/some file.txt'); - expect(model.get('size')).toEqual(123); - - model = collection.at(1); - expect(model.get('id')).toEqual(15000000); - expect(model.get('timestamp')).toEqual(15000000); - expect(model.get('name')).toEqual('some file.txt'); - expect(model.get('fullPath')).toEqual('/subdir/some file.txt'); - expect(model.get('size')).toEqual(150); - }); - it('resets page counted when setting a new file info model', function() { - collection.fetchNext(); - - expect(fakeServer.requests.length).toEqual(1); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - status: 'success', - data: { - endReached: true, - versions: [{ - version: 18000000, - size: 123, - name: 'some file.txt', - path: '/subdir/some file.txt' - }] - } - }) - ); - - expect(collection.hasMoreResults()).toEqual(false); - - collection.setFileInfo(fileInfoModel); - - expect(collection.hasMoreResults()).toEqual(true); - }); -}); - diff --git a/apps/files_versions/tests/js/versionmodelSpec.js b/apps/files_versions/tests/js/versionmodelSpec.js index 0f1c06581d54..3aeaecab7cec 100644 --- a/apps/files_versions/tests/js/versionmodelSpec.js +++ b/apps/files_versions/tests/js/versionmodelSpec.js @@ -7,18 +7,32 @@ * See the COPYING-README file. * */ + +/* global dav */ describe('OCA.Versions.VersionModel', function() { var VersionModel = OCA.Versions.VersionModel; var model; + var requestStub; + var requestDeferred; + beforeEach(function() { model = new VersionModel({ id: 10000000, + fileId: 10000000, timestamp: 10000000, fullPath: '/subdir/some file.txt', name: 'some file.txt', - size: 150 + size: 150, + versionId: 123456789 }); + OC.currentUser = 'user0'; + + requestDeferred = new $.Deferred(); + requestStub = sinon.stub(dav.Client.prototype, 'request').returns(requestDeferred.promise()); + }); + afterEach(function() { + requestStub.restore(); }); it('returns the full path', function() { @@ -32,8 +46,8 @@ describe('OCA.Versions.VersionModel', function() { }); it('returns the download url', function() { expect(model.getDownloadUrl()) - .toEqual(OC.generateUrl('/apps/files_versions/download.php') + - '?file=%2Fsubdir%2Fsome%20file.txt&revision=10000000' + .toEqual( + OC.linkToRemoteBase('dav') + '/meta/10000000/v/123456789' ); }); describe('reverting', function() { @@ -49,27 +63,27 @@ describe('OCA.Versions.VersionModel', function() { model.on('revert', revertEventStub); model.on('error', errorStub); }); + it('tells the server to revert when calling the revert method', function() { model.revert({ success: successStub }); - expect(fakeServer.requests.length).toEqual(1); - expect(fakeServer.requests[0].url) - .toEqual( - OC.generateUrl('/apps/files_versions/ajax/rollbackVersion.php') + - '?file=%2Fsubdir%2Fsome+file.txt&revision=10000000' - ); - - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - status: 'success', - }) + expect(requestStub.calledOnce).toEqual(true); + expect(requestStub.getCall(0).args[0]).toEqual('COPY'); + expect(requestStub.getCall(0).args[1]).toEqual( + '/owncloud/remote.php/dav/meta/10000000/v/123456789' + ); + expect(requestStub.getCall(0).args[2]['Destination']).toEqual( + OC.TestUtil.buildAbsoluteUrl('/owncloud/remote.php/dav/files/user0/subdir/some%20file.txt') ); - expect(revertEventStub.calledOnce).toEqual(true); + requestDeferred.resolve({ + status: 204, + body: '' + }); + + expect(revertEventStub.called).toEqual(true); expect(successStub.calledOnce).toEqual(true); expect(errorStub.notCalled).toEqual(true); }); @@ -78,14 +92,12 @@ describe('OCA.Versions.VersionModel', function() { success: successStub }); - expect(fakeServer.requests.length).toEqual(1); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - status: 'error', - }) - ); + expect(requestStub.calledOnce).toEqual(true); + + requestDeferred.resolve({ + status: 400, + body: '' + }); expect(revertEventStub.notCalled).toEqual(true); expect(successStub.notCalled).toEqual(true); diff --git a/apps/files_versions/tests/js/versionstabviewSpec.js b/apps/files_versions/tests/js/versionstabviewSpec.js index 1c9c02de67d7..2e4bbb693c73 100644 --- a/apps/files_versions/tests/js/versionstabviewSpec.js +++ b/apps/files_versions/tests/js/versionstabviewSpec.js @@ -113,64 +113,6 @@ describe('OCA.Versions.VersionsTabView', function() { expect($item.find('.preview').attr('src')).toEqual(version2.getPreviewUrl()); }); }); - - describe('More versions', function() { - var hasMoreResultsStub; - - beforeEach(function() { - tabView.setFileInfo(fileInfoModel); - fetchStub.reset(); - tabView.collection.set(testVersions); - hasMoreResultsStub = sinon.stub(VersionCollection.prototype, 'hasMoreResults'); - }); - afterEach(function() { - hasMoreResultsStub.restore(); - }); - - it('shows "More versions" button when more versions are available', function() { - hasMoreResultsStub.returns(true); - tabView.collection.trigger('sync'); - - expect(tabView.$el.find('.showMoreVersions').hasClass('hidden')).toEqual(false); - }); - it('does not show "More versions" button when more versions are available', function() { - hasMoreResultsStub.returns(false); - tabView.collection.trigger('sync'); - - expect(tabView.$el.find('.showMoreVersions').hasClass('hidden')).toEqual(true); - }); - it('fetches and appends the next page when clicking the "More" button', function() { - hasMoreResultsStub.returns(true); - - expect(fetchStub.notCalled).toEqual(true); - - tabView.$el.find('.showMoreVersions').click(); - - expect(fetchStub.calledOnce).toEqual(true); - }); - it('appends version to the list when added to collection', function() { - var time3 = Date.UTC(2015, 6, 10, 1, 0, 0, 0) / 1000; - - var version3 = new VersionModel({ - id: time3, - timestamp: time3, - name: 'some file.txt', - size: 54, - fullPath: '/subdir/some file.txt' - }); - - tabView.collection.add(version3); - - expect(tabView.$el.find('.versions>li').length).toEqual(3); - - var $item = tabView.$el.find('.versions>li').eq(2); - expect($item.find('.downloadVersion').attr('href')).toEqual(version3.getDownloadUrl()); - expect($item.find('.versiondate').text()).toEqual('7 days ago'); - expect($item.find('.revertVersion').length).toEqual(1); - expect($item.find('.preview').attr('src')).toEqual(version3.getPreviewUrl()); - }); - }); - describe('Reverting', function() { var revertStub; @@ -179,7 +121,7 @@ describe('OCA.Versions.VersionsTabView', function() { tabView.setFileInfo(fileInfoModel); tabView.collection.set(testVersions); }); - + afterEach(function() { revertStub.restore(); }); diff --git a/core/js/files/client.js b/core/js/files/client.js index 81c58863ede8..222f5cadf55a 100644 --- a/core/js/files/client.js +++ b/core/js/files/client.js @@ -410,6 +410,9 @@ */ _getSabreException: function(response) { var result = {}; + if (!response.body) { + return result; + } var xml = response.xhr.responseXML; var messages = xml.getElementsByTagNameNS('http://sabredav.org/ns', 'message'); var exceptions = xml.getElementsByTagNameNS('http://sabredav.org/ns', 'exception'); @@ -704,6 +707,49 @@ return promise; }, + _moveOrCopy: function(operation, path, destinationPath, allowOverwrite, headers, options) { + if (!path) { + throw 'Missing argument "path"'; + } + if (!destinationPath) { + throw 'Missing argument "destinationPath"'; + } + if (operation !== 'MOVE' && operation !== 'COPY') { + throw 'Invalid operation'; + } + + var self = this; + var deferred = $.Deferred(); + var promise = deferred.promise(); + options = _.extend({ + 'pathIsUrl' : false, + 'destinationPathIsUrl' : false + }, options); + headers = _.extend({}, headers, { + 'Destination' : options.destinationPathIsUrl ? destinationPath : this._buildUrl(destinationPath) + }); + + if (!allowOverwrite) { + headers['Overwrite'] = 'F'; + } + + this._client.request( + operation, + options.pathIsUrl ? path : this._buildUrl(path), + headers + ).then( + function(result) { + if (self._isSuccessStatus(result.status)) { + deferred.resolve(result.status); + } else { + result = _.extend(result, self._getSabreException(result)); + deferred.reject(result.status, result); + } + } + ); + return promise; + }, + /** * Creates a directory * @@ -737,40 +783,24 @@ * * @return {Promise} promise */ - move: function(path, destinationPath, allowOverwrite, headers) { - if (!path) { - throw 'Missing argument "path"'; - } - if (!destinationPath) { - throw 'Missing argument "destinationPath"'; - } - - var self = this; - var deferred = $.Deferred(); - var promise = deferred.promise(); - headers = _.extend({}, headers, { - 'Destination' : this._buildUrl(destinationPath) - }); - - if (!allowOverwrite) { - headers['Overwrite'] = 'F'; - } + move: function(path, destinationPath, allowOverwrite, headers, options) { + return this._moveOrCopy('MOVE', path, destinationPath, allowOverwrite, headers, options); + }, - this._client.request( - 'MOVE', - this._buildUrl(path), - headers - ).then( - function(result) { - if (self._isSuccessStatus(result.status)) { - deferred.resolve(result.status); - } else { - result = _.extend(result, self._getSabreException(result)); - deferred.reject(result.status, result); - } - } - ); - return promise; + /** + * Copies path to another path + * + * @param {String} path path to copy + * @param {String} destinationPath destination path + * @param {boolean} [allowOverwrite=false] true to allow overwriting, + * false otherwise + * @param {Object} [headers=null] additional headers + * + * @return {Promise} promise + * @since 10.1.0 + */ + copy: function(path, destinationPath, allowOverwrite, headers, options) { + return this._moveOrCopy('COPY', path, destinationPath, allowOverwrite, headers, options); }, /** diff --git a/lib/private/Files/Meta/MetaFileVersionNode.php b/lib/private/Files/Meta/MetaFileVersionNode.php index 56cd033838a4..8aa245baefff 100644 --- a/lib/private/Files/Meta/MetaFileVersionNode.php +++ b/lib/private/Files/Meta/MetaFileVersionNode.php @@ -25,9 +25,10 @@ use OC\Files\Node\AbstractFile; use OC\Files\Node\File; +use OCP\Files\ForbiddenException; +use OCP\Files\IProvidesAdditionalHeaders; use OCP\Files\IRootFolder; use OCP\Files\Storage\IVersionedStorage; -use OCP\Files\NotPermittedException; use OCP\Files\Storage; /** @@ -36,7 +37,7 @@ * * @package OC\Files\Meta */ -class MetaFileVersionNode extends AbstractFile { +class MetaFileVersionNode extends AbstractFile implements IProvidesAdditionalHeaders { /** @var string */ private $versionId; @@ -102,12 +103,15 @@ public function getContent() { public function copy($targetPath) { $target = $this->root->get($targetPath); if ($target instanceof File && $target->getId() === $this->parent->getId()) { + if (!$target->isUpdateable()) { + throw new ForbiddenException("Cannot write to $targetPath", false); + } $this->storage->restoreVersion($this->internalPath, $this->versionId); - return; + return true; } // for now we only allow restoring of a version - throw new NotPermittedException(); + return false; } public function getMTime() { @@ -125,4 +129,19 @@ public function getEtag() { public function fopen($mode) { return $this->storage->getContentOfVersion($this->internalPath, $this->versionId); } + + /** + * @return array + */ + public function getHeaders() { + return []; + } + + /** + * @inheritdoc + */ + public function getContentDispositionFileName() { + return basename($this->internalPath); + } + } diff --git a/lib/private/Files/Meta/MetaVersionCollection.php b/lib/private/Files/Meta/MetaVersionCollection.php index 8e81b1dfbdef..6b042643db84 100644 --- a/lib/private/Files/Meta/MetaVersionCollection.php +++ b/lib/private/Files/Meta/MetaVersionCollection.php @@ -79,11 +79,9 @@ public function getDirectoryListing() { if (!$storage->instanceOfStorage(IVersionedStorage::class)) { return []; } - $mimeType = $view->getMimeType($path); /** @var IVersionedStorage | Storage $storage */ $versions = $storage->getVersions($internalPath); - return array_map(function($version) use ($storage, $internalPath, $mimeType) { - $version['mime-type'] = isset($version['mime-type']) ? $version['mime-type'] : $mimeType; + return array_map(function($version) use ($storage, $internalPath) { return new MetaFileVersionNode($this, $this->root, $version, $storage, $internalPath); }, $versions); } diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php index c14fa71f6858..ebca69b0c969 100644 --- a/lib/private/Files/Storage/Common.php +++ b/lib/private/Files/Storage/Common.php @@ -694,8 +694,11 @@ public function getVersions($internalPath) { } list ($uid, $filename) = $this->convertInternalPathToGlobalPath($internalPath); - return array_values( - \OCA\Files_Versions\Storage::getVersions($uid, $filename)); + return array_map(function ($version) use ($internalPath) { + $version['mime-type'] = $this->getMimeType($internalPath); + return $version; + }, array_values( + \OCA\Files_Versions\Storage::getVersions($uid, $filename))); } /** @@ -730,9 +733,9 @@ public function getContentOfVersion($internalPath, $versionId) { public function restoreVersion($internalPath, $versionId) { // KISS implementation if (!\OC_App::isEnabled('files_versions')) { - return; + return false; } - list ($uid, $filename) = $this->convertInternalPathToGlobalPath($internalPath); - \OCA\Files_Versions\Storage::rollback($filename, $versionId); + $v = $this->getVersion($internalPath, $versionId); + return \OCA\Files_Versions\Storage::restoreVersion($v['owner'], $v['path'], $v['storage_location'], $versionId); } } diff --git a/lib/private/Preview.php b/lib/private/Preview.php index ae9f547a5fd9..feeb7cd5b970 100644 --- a/lib/private/Preview.php +++ b/lib/private/Preview.php @@ -1334,8 +1334,9 @@ public static function post_delete_versions($args) { */ public static function post_delete($args, $prefix = '') { $path = Files\Filesystem::normalizePath($args['path']); + $user = isset($args['user']) ? $args['user'] : \OC_User::getUser(); - $preview = new Preview(\OC_User::getUser(), $prefix, $path); + $preview = new Preview($user, $prefix, $path); $preview->deleteAllPreviews(); } diff --git a/lib/public/Files/IProvidesAdditionalHeaders.php b/lib/public/Files/IProvidesAdditionalHeaders.php new file mode 100644 index 000000000000..8253a9fa3f14 --- /dev/null +++ b/lib/public/Files/IProvidesAdditionalHeaders.php @@ -0,0 +1,49 @@ + + * + * @copyright Copyright (c) 2017, 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 OCP\Files; + + +/** + * Interface IProvidesAdditionalHeaders + * This interface allows to add additional headers to the response + * + * @package OCP\Files + * @since 10.0.5 + */ +interface IProvidesAdditionalHeaders { + + /** + * Returns an array of headers. + * + * @return array + * @since 10.0.5 + */ + public function getHeaders(); + + /** + * Returns the file name which is to be used for the content disposition + * @return string + * @since 10.0.5 + */ + public function getContentDispositionFileName(); +} diff --git a/tests/lib/Files/MetaFilesTest.php b/tests/lib/Files/MetaFilesTest.php index 2f02c78642d8..651736b4e427 100644 --- a/tests/lib/Files/MetaFilesTest.php +++ b/tests/lib/Files/MetaFilesTest.php @@ -27,6 +27,7 @@ use OC\Files\Meta\MetaFileVersionNode; use OC\Files\Meta\MetaRootNode; use OC\Files\Meta\MetaVersionCollection; +use OC\Files\Node\File; use OC\Files\View; use OCA\Files_Versions\Hooks; use OCP\Files\Folder; @@ -57,7 +58,8 @@ public function testMetaInNodeAPI() { $this->loginAsUser($userId); // create file - $fileName = "$userId/files/" . $this->getUniqueID('file') . '.txt'; + $file = $this->getUniqueID('file') . '.txt'; + $fileName = "$userId/files/$file"; $view = new View(); $view->file_put_contents($fileName, '1234'); $info = $view->getFileInfo($fileName); @@ -86,13 +88,23 @@ public function testMetaInNodeAPI() { $this->assertInstanceOf(MetaFileVersionNode::class, $children[0]); $versionId = $children[0]->getName(); + /** @var MetaFileVersionNode $metaNodeOfFile */ $metaNodeOfFile = \OC::$server->getRootFolder()->get("meta/{$info->getId()}/v/$versionId"); $this->assertInstanceOf(MetaFileVersionNode::class, $metaNodeOfFile); $this->assertEquals($versionId, $metaNodeOfFile->getName()); + $this->assertEquals(4, $metaNodeOfFile->getSize()); + $this->assertEquals([], $metaNodeOfFile->getHeaders()); + $this->assertEquals($file, $metaNodeOfFile->getContentDispositionFileName()); + $this->assertEquals('text/plain', $metaNodeOfFile->getMimetype()); + $this->assertEquals($info->getMTime(), $metaNodeOfFile->getMTime()); + $this->assertTrue(is_string($metaNodeOfFile->getMTime())); + $this->assertTrue(strlen($metaNodeOfFile->getMTime()) > 0); + /** @var MetaFileVersionNode $metaNodeOfFile */ $this->assertEquals('1234', $metaNodeOfFile->getContent()); // restore a version using move + /** @var File $target */ $target = \OC::$server->getRootFolder()->get($fileName); $this->assertEquals('1234567890', $target->getContent()); $metaNodeOfFile->copy($fileName);