diff --git a/apps/dav/lib/Capabilities.php b/apps/dav/lib/Capabilities.php
index ec447d193fb5..a916cce07eba 100644
--- a/apps/dav/lib/Capabilities.php
+++ b/apps/dav/lib/Capabilities.php
@@ -30,7 +30,12 @@ public function getCapabilities() {
return [
'dav' => [
'chunking' => '1.0',
+ 'zsync' => '1.0',
+ 'reports' => [
+ 'search-files',
+ ],
]
];
}
}
+
diff --git a/apps/dav/lib/Connector/Sabre/FilesSearchReportPlugin.php b/apps/dav/lib/Connector/Sabre/FilesSearchReportPlugin.php
new file mode 100644
index 000000000000..2953bf660b47
--- /dev/null
+++ b/apps/dav/lib/Connector/Sabre/FilesSearchReportPlugin.php
@@ -0,0 +1,201 @@
+
+ *
+ * @copyright Copyright (c) 2018, 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\Connector\Sabre;
+
+use Sabre\DAV\Server as DavServer;
+use Sabre\DAV\ServerPlugin;
+use Sabre\DAV\PropFind;
+use Sabre\DAV\Exception\BadRequest;
+use Sabre\DAV\Exception\NotImplemented;
+use OCA\DAV\Files\Xml\SearchRequest;
+use OCP\ISearch;
+use OC\Search\Result\File as FileResult;
+
+class FilesSearchReportPlugin extends ServerPlugin {
+ // namespace
+ const NS_OWNCLOUD = 'http://owncloud.org/ns';
+ const REPORT_NAME = '{http://owncloud.org/ns}search-files';
+
+ /**
+ * Reference to main server object
+ *
+ * @var DavServer
+ */
+ private $server;
+
+ /** @var ISearch */
+ private $searchService;
+
+ public function __construct(ISearch $searchService) {
+ $this->searchService = $searchService;
+ }
+
+ /**
+ * This initializes the plugin.
+ *
+ * This function is called by \Sabre\DAV\Server, after
+ * addPlugin is called.
+ *
+ * This method should set up the required event subscriptions.
+ *
+ * @param DavServer $server
+ * @return void
+ */
+ public function initialize(DavServer $server) {
+ $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
+
+ $server->xml->elementMap[self::REPORT_NAME] = SearchRequest::class;
+
+ $this->server = $server;
+ $this->server->on('report', [$this, 'onReport']);
+ }
+
+ /**
+ * Returns a list of reports this plugin supports.
+ *
+ * This will be used in the {DAV:}supported-report-set property.
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getSupportedReportSet($uri) {
+ $reportTargetNode = $this->server->tree->getNodeForPath($uri);
+ if ($reportTargetNode instanceof Directory && $reportTargetNode->getPath() === '/') {
+ return [self::REPORT_NAME];
+ } else {
+ return [];
+ }
+ }
+
+ /**
+ * REPORT operations to look for files
+ *
+ * @param string $reportName
+ * @param mixed $report
+ * @param string $uri
+ * @return bool
+ * @throws BadRequest
+ * @throws NotImplemented
+ * @internal param $ [] $report
+ */
+ public function onReport($reportName, $report, $uri) {
+ $reportTargetNode = $this->server->tree->getNodeForPath($uri);
+ if (!$reportTargetNode instanceof Directory ||
+ $reportName !== self::REPORT_NAME) {
+ return;
+ }
+
+ if ($reportTargetNode->getPath() !== '/') {
+ throw new NotImplemented('Search report only available in the root folder of the user');
+ }
+
+ $requestedProps = $report->properties;
+ $searchInfo = $report->searchInfo;
+
+ if (!isset($searchInfo['pattern'])) {
+ throw new BadRequest('Search pattern cannot be empty');
+ }
+
+ $limit = 30;
+ if (isset($searchInfo['limit'])) {
+ $limit = $searchInfo['limit'];
+ }
+
+ $searchResults = $this->searchService->searchPaged(
+ $searchInfo['pattern'],
+ ['files'],
+ 1,
+ $limit
+ );
+
+ $filesUri = $this->getFilesBaseUri($uri, $reportTargetNode->getPath());
+
+ $xml = $this->server->generateMultiStatus(
+ $this->getSearchResultIterator($filesUri, $searchResults, $requestedProps)
+ );
+ $this->server->httpResponse->setStatus(207);
+ $this->server->httpResponse->setHeader(
+ 'Content-Type',
+ 'application/xml; charset=utf-8'
+ );
+ $this->server->httpResponse->setBody($xml);
+
+ return false;
+ }
+
+ /**
+ * @param string $filesUri the base uri for this user's files directory,
+ * usually /files/username
+ * @param File[] $searchResults the results coming from the search service,
+ * within the files app
+ * @param array $requestedProps the list of requested webDAV properties
+ * @return \Generator a generator to traverse over the properties of the
+ * search result, suitable for server's multistatus response
+ */
+ private function getSearchResultIterator($filesUri, $searchResults, $requestedProps) {
+ $paths = \array_map(function ($searchResult) use ($filesUri) {
+ return $filesUri . $searchResult->path;
+ }, $searchResults);
+
+ $nodes = $this->server->tree->getMultipleNodes($paths);
+
+ $propFindType = $requestedProps ? PropFind::NORMAL : PropFind::ALLPROPS;
+
+ foreach ($nodes as $path => $node) {
+ $propFind = new PropFind(
+ $path,
+ $requestedProps,
+ 0,
+ $propFindType
+ );
+ $this->server->getPropertiesByNode($propFind, $node);
+
+ $result = $propFind->getResultForMultiStatus();
+ $result['href'] = $propFind->getPath();
+ yield $result;
+ }
+ }
+
+ /**
+ * Returns the base uri of the files root by removing
+ * the subpath from the URI
+ *
+ * @param string $uri URI from this request
+ * @param string $subPath subpath to remove from the URI
+ *
+ * @return string files base uri
+ */
+ private function getFilesBaseUri($uri, $subPath) {
+ $uri = \trim($uri, '/');
+ $subPath = \trim($subPath, '/');
+ if ($subPath === '') {
+ $filesUri = $uri;
+ } else {
+ $filesUri = \substr($uri, 0, \strlen($uri) - \strlen($subPath));
+ }
+ $filesUri = \trim($filesUri, '/');
+ if ($filesUri === '') {
+ return '';
+ }
+ return '/' . $filesUri;
+ }
+}
diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php
index 1273ab11975b..e78d6c716a2f 100644
--- a/apps/dav/lib/Connector/Sabre/ServerFactory.php
+++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php
@@ -180,6 +180,11 @@ public function createServer($baseUri,
\OC::$server->getGroupManager(),
$userFolder
));
+ $server->addPlugin(
+ new \OCA\DAV\Connector\Sabre\FilesSearchReportPlugin(
+ \OC::$server->getSearch()
+ )
+ );
// custom properties plugin must be the last one
$server->addPlugin(
diff --git a/apps/dav/lib/Files/Xml/SearchRequest.php b/apps/dav/lib/Files/Xml/SearchRequest.php
new file mode 100644
index 000000000000..d50541176edd
--- /dev/null
+++ b/apps/dav/lib/Files/Xml/SearchRequest.php
@@ -0,0 +1,81 @@
+
+ *
+ * @copyright Copyright (c) 2018, 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\Xml;
+
+use Sabre\Xml\Element\Base;
+use Sabre\Xml\Element\KeyValue;
+use Sabre\Xml\Reader;
+use Sabre\Xml\XmlDeserializable;
+
+class SearchRequest implements XmlDeserializable {
+ /**
+ * An array with requested properties.
+ *
+ * @var array
+ */
+ public $properties;
+
+ /**
+ * @var array
+ */
+ public $searchInfo;
+
+ public static function xmlDeserialize(Reader $reader) {
+ $newProps = [
+ 'properties' => [],
+ 'searchInfo' => null,
+ ];
+
+ $elems = (array)$reader->parseInnerTree([
+ '{DAV:}prop' => KeyValue::class,
+ '{http://owncloud.org/ns}search' => KeyValue::class,
+ ]);
+
+ if (!\is_array($elems)) {
+ $elems = [];
+ }
+
+ foreach ($elems as $elem) {
+ switch ($elem['name']) {
+ case '{DAV:}prop':
+ $newProps['properties'] = \array_keys($elem['value']);
+ break;
+ case '{http://owncloud.org/ns}search':
+ $value = $elem['value'];
+ if (isset($value['{http://owncloud.org/ns}pattern'])) {
+ $newProps['searchInfo']['pattern'] = $value['{http://owncloud.org/ns}pattern'];
+ }
+ if (isset($value['{http://owncloud.org/ns}limit'])) {
+ $newProps['searchInfo']['limit'] = (int)$value['{http://owncloud.org/ns}limit'];
+ }
+ break;
+ }
+ }
+
+ $obj = new self();
+ foreach ($newProps as $key => $value) {
+ $obj->$key = $value;
+ }
+
+ return $obj;
+ }
+}
diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php
index 7f9689fff50f..1dfd73d1ed83 100644
--- a/apps/dav/lib/Server.php
+++ b/apps/dav/lib/Server.php
@@ -250,7 +250,8 @@ public function __construct(IRequest $request, $baseUri) {
\OC::$server->getCommentsManager(),
$userSession
));
- if (!is_null($view)) {
+
+ if ($view !== null) {
$this->server->addPlugin(new FilesReportPlugin(
$this->server->tree,
$view,
@@ -262,6 +263,11 @@ public function __construct(IRequest $request, $baseUri) {
$userFolder
));
}
+ $this->server->addPlugin(
+ new \OCA\DAV\Connector\Sabre\FilesSearchReportPlugin(
+ \OC::$server->getSearch()
+ )
+ );
}
// register plugins from apps
diff --git a/apps/dav/tests/unit/Connector/Sabre/FilesSearchReportPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/FilesSearchReportPluginTest.php
new file mode 100644
index 000000000000..d9579210c01c
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/FilesSearchReportPluginTest.php
@@ -0,0 +1,321 @@
+
+ *
+ * @copyright Copyright (c) 2018, 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\Connector\Sabre;
+
+use OCP\ISearch;
+use OCA\DAV\Files\Xml\SearchRequest;
+use OCA\DAV\Connector\Sabre\FilesSearchReportPlugin;
+use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Connector\Sabre\File;
+use OCA\DAV\Connector\Sabre\Node;
+use OC\Search\Result\File as ResultFile;
+use Sabre\DAV\Tree;
+use Sabre\DAV\Server;
+use Sabre\DAV\PropFind;
+
+class FilesSearchReportPluginTest extends \Test\TestCase {
+ /** @var Server|\PHPUnit_Framework_MockObject_MockObject */
+ private $server;
+
+ /** @var Tree|\PHPUnit_Framework_MockObject_MockObject */
+ private $tree;
+
+ /** @var ISearch|\PHPUnit_Framework_MockObject_MockObject */
+ private $searchService;
+
+ /** @var FilesSearchReportPlugin|\PHPUnit_Framework_MockObject_MockObject */
+ private $plugin;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->tree = $this->getMockBuilder(Tree::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->server = $this->getMockBuilder(Server::class)
+ ->setConstructorArgs([$this->tree])
+ ->setMethods(['getRequestUri', 'getBaseUri', 'generateMultiStatus'])
+ ->getMock();
+
+ $this->server->expects($this->any())
+ ->method('getBaseUri')
+ ->will($this->returnValue('http://example.com/owncloud/remote.php/dav'));
+
+ $this->searchService = $this->getMockBuilder(ISearch::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->plugin = new FilesSearchReportPlugin($this->searchService);
+ }
+
+ private function setupBaseTreeNode($path, Node $node) {
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with($path)
+ ->will($this->returnValue($node));
+
+ $this->server->expects($this->any())
+ ->method('getRequestUri')
+ ->will($this->returnValue($path));
+ }
+
+ public function testOnReportFileNode() {
+ $base = '/remote.php/dav/files/user';
+ $nodePath = '/totally/unrelated/13';
+ $path = "{$base}{$nodePath}";
+
+ $node = $this->createMock(File::class);
+ $node->method('getPath')->willReturn($nodePath);
+
+ $this->setupBaseTreeNode($path, $node);
+ $this->plugin->initialize($this->server);
+
+ $this->assertNull($this->plugin->onReport(FilesSearchReportPlugin::REPORT_NAME, [], $path));
+ }
+
+ public function testOnReportWrongReportName() {
+ $base = '/remote.php/dav/files/user';
+ $nodePath = '/totally/unrelated/13';
+ $path = "{$base}{$nodePath}";
+
+ $node = $this->createMock(Directory::class);
+ $node->method('getPath')->willReturn($nodePath);
+
+ $this->setupBaseTreeNode($path, $node);
+ $this->plugin->initialize($this->server);
+
+ $this->assertNull($this->plugin->onReport('this name should not exist', [], $path));
+ }
+
+ /**
+ * @expectedException \Sabre\DAV\Exception\NotImplemented
+ */
+ public function testOnReportNotRoot() {
+ $base = '/remote.php/dav/files/user';
+ $nodePath = '/totally/unrelated/13';
+ $path = "{$base}{$nodePath}";
+
+ $node = $this->createMock(Directory::class);
+ $node->method('getPath')->willReturn($nodePath);
+
+ $this->setupBaseTreeNode($path, $node);
+ $this->plugin->initialize($this->server);
+
+ $this->plugin->onReport(FilesSearchReportPlugin::REPORT_NAME, [], $path);
+ }
+
+ public function onReportNoSearchPatternProvider() {
+ return [
+ [[], null],
+ [['{http://owncloud.org/ns}fileid','{DAV:}getcontentlength'], null],
+ [[], []],
+ [['{http://owncloud.org/ns}fileid','{DAV:}getcontentlength'], []],
+ [[], ['limit' => 50]],
+ [['{http://owncloud.org/ns}fileid','{DAV:}getcontentlength'], ['limit' => 50]],
+ ];
+ }
+
+ /**
+ * @dataProvider onReportNoSearchPatternProvider
+ * @expectedException \Sabre\DAV\Exception\BadRequest
+ */
+ public function testOnReportNoSearchPattern($properties, $searchInfo) {
+ $base = '/remote.php/dav/files/user';
+ $nodePath = '/';
+ $path = "{$base}{$nodePath}";
+
+ $parameters = new SearchRequest();
+ $parameters->properties = $properties;
+ $parameters->searchInfo = $searchInfo;
+
+ $node = $this->createMock(Directory::class);
+ $node->method('getPath')->willReturn($nodePath);
+
+ $this->setupBaseTreeNode($path, $node);
+ $this->plugin->initialize($this->server);
+
+ $this->plugin->onReport(FilesSearchReportPlugin::REPORT_NAME, $parameters, $path);
+ }
+
+ public function onReportProvider() {
+ return [
+ [[], ['pattern' => 'go']],
+ [['{http://owncloud.org/ns}fileid','{DAV:}getcontentlength'], ['pattern' => 'go']],
+ [[], ['pattern' => 'se', 'limit' => 5]],
+ [['{http://owncloud.org/ns}fileid','{DAV:}getcontentlength'], ['pattern' => 'se', 'limit' => 5]],
+ ];
+ }
+
+ /**
+ * @dataProvider onReportProvider
+ */
+ public function testOnReport($properties, $searchInfo) {
+ $base = '/remote.php/dav/files/user';
+ $nodePath = '/';
+ $path = "{$base}{$nodePath}";
+
+ $parameters = new SearchRequest();
+ $parameters->properties = $properties;
+ $parameters->searchInfo = $searchInfo;
+
+ $node = $this->createMock(Directory::class);
+ $node->method('getPath')->willReturn($nodePath);
+
+ $expectedLimit = (isset($searchInfo['limit'])) ? $searchInfo['limit'] : 30;
+ $realLimit = \min($expectedLimit, 8);
+
+ $searchList = $this->getSearchList($searchInfo['pattern'], $realLimit);
+ $searchListNodePaths = \array_map(function ($path) use ($base) {
+ return "{$base}{$path}";
+ }, $searchList);
+
+ $this->searchService->method('searchPaged')
+ ->with($searchInfo['pattern'], ['files'], 1, $expectedLimit)
+ ->will($this->returnCallback(function ($pattern, $apps, $page, $limit) use ($searchList) {
+ return \array_map(function ($value) {
+ $mock = $this->createMock(ResultFile::class);
+ $mock->path = $value;
+ return $mock;
+ }, $searchList);
+ }));
+
+ $this->tree->method('getMultipleNodes')
+ ->with($searchListNodePaths)
+ ->will($this->returnCallback(function ($paths) use ($searchList) {
+ $nodes = [];
+ foreach ($paths as $key => $path) {
+ $mock = $this->createMock(Node::class);
+ $mock->method('getName')->willReturn($path);
+ $mock->method('getId')->willReturn($key);
+ $mock->method('getSize')->willReturn($key * 1024);
+ $mock->method('getEtag')->willReturn(\str_repeat($key, 8));
+ $nodes[$path] = $mock;
+ }
+ return $nodes;
+ }));
+
+ $responses = [];
+ $this->server->expects($this->once())
+ ->method('generateMultiStatus')
+ ->will($this->returnCallback(function ($responsesArg) use (&$responses) {
+ foreach ($responsesArg as $responseArg) {
+ $responses[] = $responseArg;
+ }
+ })
+ );
+
+ $this->setupBaseTreeNode($path, $node);
+ $this->plugin->initialize($this->server);
+
+ // setup a propfind handler to fill some data
+ $this->server->on('propFind', function (Propfind $propfind, $node) {
+ $propfind->handle('{http://owncloud.org/ns}fileid', $node->getId());
+ $propfind->handle('{DAV:}getcontentlength', $node->getSize());
+ $propfind->handle('{DAV:}getetag', $node->getEtag());
+ });
+
+ $this->plugin->onReport(FilesSearchReportPlugin::REPORT_NAME, $parameters, $path);
+
+ $this->assertEquals($realLimit, \count($responses));
+ if ($properties === []) {
+ foreach ($responses as $key => $response) {
+ // dav properties should be shown while non-dav properties won't appear
+ $this->assertEquals($key * 1024, $response[200]['{DAV:}getcontentlength']);
+ $this->assertEquals(\str_repeat($key, 8), $response[200]['{DAV:}getetag']);
+ $this->assertFalse(isset($response[200]['{http://owncloud.org/ns}fileid']));
+ }
+ } else {
+ $propfindProperties = [
+ '{http://owncloud.org/ns}fileid',
+ '{DAV:}getcontentlength',
+ '{DAV:}getetag',
+ ];
+ $foundProperties = \array_intersect($properties, $propfindProperties);
+ $notFoundProperties = \array_diff($properties, $propfindProperties);
+ $notRequestedProperties = \array_diff($propfindProperties, $properties);
+ foreach ($responses as $key => $response) {
+ // only requested properties should appear
+ foreach ($foundProperties as $foundProperty) {
+ switch ($foundProperty) {
+ case '{DAV:}getcontentlength':
+ $this->assertEquals($key * 1024, $response[200]['{DAV:}getcontentlength']);
+ break;
+ case '{DAV:}getetag':
+ $this->assertEquals(\str_repeat($key, 8), $response[200]['{DAV:}getetag']);
+ break;
+ case '{http://owncloud.org/ns}fileid':
+ $this->assertEquals($key, $response[200]['{http://owncloud.org/ns}fileid']);
+ break;
+ }
+ }
+ foreach ($notFoundProperties as $notFoundProperty) {
+ $this->assertTrue(isset($response[400][$notFoundProperty]));
+ }
+ foreach ($notRequestedProperties as $notRequestedProperty) {
+ $this->assertFalse(isset($response[200][$notRequestedProperty]));
+ $this->assertFalse(isset($response[400][$notRequestedProperty]));
+ }
+ }
+ }
+ }
+
+ private function getSearchList($search, $numberOfItems) {
+ $results = [];
+ $pathParts = [];
+ for ($i = 0 ; $i < $numberOfItems ; $i++) {
+ $pathParts[] = $search . \strval($i);
+ $results[] = '/' . \implode('/', $pathParts);
+ }
+ return $results;
+ }
+
+ public function getSupportedReportSetProvider() {
+ return [
+ ['/remote.php/dav/files/user', '/totally/unrelated/13'],
+ ['/remote.php/dav/files/user', '/'],
+ ['/remote.php/webdav', '/totally/unrelated/13'],
+ ['/remote.php/webdav', '/'],
+ ];
+ }
+
+ /**
+ * @dataProvider getSupportedReportSetProvider
+ */
+ public function testGetSupportedReportSet($base, $nodePath) {
+ $path = "{$base}{$nodePath}";
+
+ $node = $this->createMock(Directory::class);
+ $node->method('getPath')->willReturn($nodePath);
+
+ $this->setupBaseTreeNode($path, $node);
+ $this->plugin->initialize($this->server);
+
+ if ($nodePath === '/') {
+ $this->assertEquals(
+ ['{http://owncloud.org/ns}search-files'],
+ $this->plugin->getSupportedReportSet($path)
+ );
+ } else {
+ $this->assertEquals([], $this->plugin->getSupportedReportSet($path));
+ }
+ }
+}