diff --git a/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
index adbb6ee658e88..fb07925b6bc30 100644
--- a/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
@@ -179,10 +179,16 @@ public function onReport($reportName, $report, $uri) {
$requestedProps = $report->properties;
$filterRules = $report->filters;
+ // "systemtag" is always an array of tags, favorite a string/int/null
if (empty($filterRules['systemtag']) && is_null($filterRules['favorite'])) {
- // load all
- $results = $reportTargetNode->getChildren();
+ // FIXME: search currently not possible because results are missing properties!
+ throw new BadRequest('No filter criteria specified');
} else {
+ if (isset($report->search['pattern'])) {
+ // TODO: implement this at some point...
+ throw new BadRequest('Search pattern cannot be combined with filter');
+ }
+
// gather all file ids matching filter
try {
$resultFileIds = $this->processFilterRules($filterRules);
@@ -190,16 +196,14 @@ public function onReport($reportName, $report, $uri) {
throw new PreconditionFailed('Cannot filter by non-existing tag', 0, $e);
}
+ // pre-slice the results if needed for pagination to not waste
+ // time resolving nodes that will not be returned anyway
+ $resultFileIds = $this->slice($resultFileIds, $report);
+
// find sabre nodes by file id, restricted to the root node path
$results = $this->findNodesByFileIds($reportTargetNode, $resultFileIds);
}
- if (!is_null($report->limit)) {
- $length = $report->limit['size'];
- $offset = $report->limit['page'] * $length;
- $results = array_slice($results, $offset, $length);
- }
-
$filesUri = $this->getFilesBaseUri($uri, $reportTargetNode->getPath());
$results = $this->prepareResponses($filesUri, $requestedProps, $results);
@@ -212,6 +216,15 @@ public function onReport($reportName, $report, $uri) {
return false;
}
+ private function slice($results, $report) {
+ if (!is_null($report->search)) {
+ $length = $report->search['limit'];
+ $offset = $report->search['offset'];
+ $results = array_slice($results, $offset, $length);
+ }
+ return $results;
+ }
+
/**
* Returns the base uri of the files root by removing
* the subpath from the URI
@@ -330,11 +343,6 @@ public function prepareResponses($filesUri, $requestedProps, $nodes) {
$result = $propFind->getResultForMultiStatus();
$result['href'] = $propFind->getPath();
- $resourceType = $this->server->getResourceTypeForNode($node);
- if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) {
- $result['href'] .= '/';
- }
-
$results[] = $result;
}
return $results;
@@ -358,10 +366,9 @@ public function findNodesByFileIds($rootNode, $fileIds) {
$entry = $folder->getById($fileId);
if ($entry) {
$entry = current($entry);
- if ($entry instanceof \OCP\Files\File) {
- $results[] = new File($this->fileView, $entry);
- } else if ($entry instanceof \OCP\Files\Folder) {
- $results[] = new Directory($this->fileView, $entry);
+ $node = $this->makeSabreNode($entry);
+ if ($node) {
+ $results[] = $node;
}
}
}
@@ -369,6 +376,15 @@ public function findNodesByFileIds($rootNode, $fileIds) {
return $results;
}
+ private function makeSabreNode(\OCP\Files\Node $filesNode) {
+ if ($filesNode instanceof \OCP\Files\File) {
+ return new File($this->fileView, $filesNode);
+ } else if ($filesNode instanceof \OCP\Files\Folder) {
+ return new Directory($this->fileView, $filesNode);
+ }
+ throw new \Exception('Unrecognized Files API node returned, aborting');
+ }
+
/**
* Returns whether the currently logged in user is an administrator
*/
diff --git a/apps/dav/lib/Files/Xml/FilterRequest.php b/apps/dav/lib/Files/Xml/FilterRequest.php
index aeda47abfbc5b..8cbc6f73ebbd9 100644
--- a/apps/dav/lib/Files/Xml/FilterRequest.php
+++ b/apps/dav/lib/Files/Xml/FilterRequest.php
@@ -24,7 +24,7 @@ class FilterRequest implements XmlDeserializable {
/**
* @var array
*/
- public $limit;
+ public $search;
/**
* The deserialize method is called during xml parsing.
@@ -51,7 +51,7 @@ static function xmlDeserialize(Reader $reader) {
$elems = (array)$reader->parseInnerTree([
'{DAV:}prop' => KeyValue::class,
'{http://owncloud.org/ns}filter-rules' => Base::class,
- '{http://owncloud.org/ns}limit' => Base::class
+ '{http://owncloud.org/ns}search' => KeyValue::class,
]);
$newProps = [
@@ -60,7 +60,7 @@ static function xmlDeserialize(Reader $reader) {
'favorite' => null
],
'properties' => [],
- 'limit' => null,
+ 'search' => null,
];
if (!is_array($elems)) {
@@ -85,13 +85,19 @@ static function xmlDeserialize(Reader $reader) {
}
}
break;
- case '{http://owncloud.org/ns}limit' :
- // TODO verify page and size
- $newProps['limit'] = $elem['attributes'];
+ case '{http://owncloud.org/ns}search' :
+ $value = $elem['value'];
+ if (isset($value['{http://owncloud.org/ns}pattern'])) {
+ $newProps['search']['pattern'] = $value['{http://owncloud.org/ns}pattern'];
+ }
+ if (isset($value['{http://owncloud.org/ns}limit'])) {
+ $newProps['search']['limit'] = (int)$value['{http://owncloud.org/ns}limit'];
+ }
+ if (isset($value['{http://owncloud.org/ns}offset'])) {
+ $newProps['search']['offset'] = (int)$value['{http://owncloud.org/ns}offset'];
+ }
break;
-
}
-
}
$obj = new self();
diff --git a/apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php
index 8e7f92a87991f..491d048d8c2f7 100644
--- a/apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php
@@ -35,6 +35,9 @@
use OCP\IGroupManager;
use OCP\SystemTag\ISystemTagManager;
use OCP\ITags;
+use OCP\Files\FileInfo;
+use OCP\IRequest;
+use OCP\IConfig;
class FilesReportPluginTest extends \Test\TestCase {
/** @var \Sabre\DAV\Server|\PHPUnit_Framework_MockObject_MockObject */
@@ -76,13 +79,11 @@ public function setUp() {
->disableOriginalConstructor()
->getMock();
- $this->view = $this->getMockBuilder('\OC\Files\View')
- ->disableOriginalConstructor()
- ->getMock();
+ $this->view = new View();
$this->server = $this->getMockBuilder('\Sabre\DAV\Server')
->setConstructorArgs([$this->tree])
- ->setMethods(['getRequestUri', 'getBaseUri'])
+ ->setMethods(['getRequestUri', 'getBaseUri', 'generateMultiStatus'])
->getMock();
$this->server->expects($this->any())
@@ -121,6 +122,15 @@ public function setUp() {
->method('getUser')
->will($this->returnValue($user));
+ // add FilesPlugin to test more properties
+ $this->server->addPlugin(
+ new \OCA\DAV\Connector\Sabre\FilesPlugin(
+ $this->tree,
+ $this->createMock(IConfig::class),
+ $this->createMock(IRequest::class)
+ )
+ );
+
$this->plugin = new FilesReportPluginImplementation(
$this->tree,
$this->view,
@@ -177,7 +187,12 @@ public function testOnReport() {
$path = 'test';
$parameters = new FilterRequest();
- $parameters->properties = ['{DAV:}getcontentlength', '{http://owncloud.org/ns}size'];
+ $parameters->properties = [
+ '{DAV:}getcontentlength',
+ '{http://owncloud.org/ns}size',
+ '{http://owncloud.org/ns}fileid',
+ '{DAV:}resourcetype',
+ ];
$parameters->filters = [
'systemtag' => [123, 456],
'favorite' => null
@@ -196,14 +211,8 @@ public function testOnReport() {
->with('456', 'files')
->will($this->returnValue(['111', '222', '333']));
- $reportTargetNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory')
- ->disableOriginalConstructor()
- ->getMock();
-
- $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
- ->disableOriginalConstructor()
- ->getMock();
-
+ $reportTargetNode = $this->createMock(\OCA\DAV\Connector\Sabre\Directory::class);
+ $response = $this->createMock(\Sabre\HTTP\ResponseInterface::class);
$response->expects($this->once())
->method('setHeader')
->with('Content-Type', 'application/xml; charset=utf-8');
@@ -220,12 +229,16 @@ public function testOnReport() {
->with('/' . $path)
->will($this->returnValue($reportTargetNode));
- $filesNode1 = $this->getMockBuilder('\OCP\Files\Folder')
- ->disableOriginalConstructor()
- ->getMock();
- $filesNode2 = $this->getMockBuilder('\OCP\Files\File')
- ->disableOriginalConstructor()
- ->getMock();
+ $filesNode1 = $this->createMock(\OCP\Files\Folder::class);
+ $filesNode1->method('getId')->willReturn(111);
+ $filesNode1->method('getPath')->willReturn('/node1');
+ $filesNode1->method('isReadable')->willReturn(true);
+ $filesNode1->method('getSize')->willReturn(2048);
+ $filesNode2 = $this->createMock(\OCP\Files\File::class);
+ $filesNode2->method('getId')->willReturn(222);
+ $filesNode2->method('getPath')->willReturn('/sub/node2');
+ $filesNode2->method('getSize')->willReturn(1024);
+ $filesNode2->method('isReadable')->willReturn(true);
$this->userFolder->expects($this->at(0))
->method('getById')
@@ -242,7 +255,110 @@ public function testOnReport() {
$this->server->httpResponse = $response;
$this->plugin->initialize($this->server);
+ $responses = null;
+ $this->server->expects($this->once())
+ ->method('generateMultiStatus')
+ ->will($this->returnCallback(function($responsesArg) use (&$responses) {
+ $responses = $responsesArg;
+ })
+ );
+
+ $this->assertFalse($this->plugin->onReport(FilesReportPluginImplementation::REPORT_NAME, $parameters, '/' . $path));
+
+ $this->assertCount(2, $responses);
+
+ $this->assertTrue(isset($responses[0][200]));
+ $this->assertTrue(isset($responses[1][200]));
+
+ $this->assertEquals('/test/node1', $responses[0]['href']);
+ $this->assertEquals('/test/sub/node2', $responses[1]['href']);
+
+ $props1 = $responses[0];
+ $this->assertEquals('111', $props1[200]['{http://owncloud.org/ns}fileid']);
+ $this->assertNull($props1[404]['{DAV:}getcontentlength']);
+ $this->assertInstanceOf('\Sabre\DAV\Xml\Property\ResourceType', $props1[200]['{DAV:}resourcetype']);
+ $resourceType1 = $props1[200]['{DAV:}resourcetype']->getValue();
+ $this->assertEquals('{DAV:}collection', $resourceType1[0]);
+
+ $props2 = $responses[1];
+ $this->assertEquals('1024', $props2[200]['{DAV:}getcontentlength']);
+ $this->assertEquals('222', $props2[200]['{http://owncloud.org/ns}fileid']);
+ $this->assertInstanceOf('\Sabre\DAV\Xml\Property\ResourceType', $props2[200]['{DAV:}resourcetype']);
+ $this->assertCount(0, $props2[200]['{DAV:}resourcetype']->getValue());
+ }
+
+ public function testOnReportPaginationFiltered() {
+ $path = 'test';
+
+ $parameters = new FilterRequest();
+ $parameters->properties = [
+ '{DAV:}getcontentlength',
+ ];
+ $parameters->filters = [
+ 'systemtag' => [],
+ 'favorite' => true
+ ];
+ $parameters->search = [
+ 'offset' => 2,
+ 'limit' => 3,
+ ];
+
+ $filesNodes = [];
+ for ($i = 0; $i < 20; $i++) {
+ $filesNode = $this->createMock(\OCP\Files\File::class);
+ $filesNode->method('getId')->willReturn(1000 + $i);
+ $filesNode->method('getPath')->willReturn('/nodes/node' . $i);
+ $filesNode->method('isReadable')->willReturn(true);
+ $filesNodes[$filesNode->getId()] = $filesNode;
+ }
+
+ // return all above nodes as favorites
+ $this->privateTags->expects($this->once())
+ ->method('getFavorites')
+ ->will($this->returnValue(array_keys($filesNodes)));
+
+ $reportTargetNode = $this->createMock(\OCA\DAV\Connector\Sabre\Directory::class);
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('/' . $path)
+ ->will($this->returnValue($reportTargetNode));
+
+ // getById must only be called for the required nodes
+ $this->userFolder->expects($this->at(0))
+ ->method('getById')
+ ->with(1002)
+ ->willReturn([$filesNodes[1002]]);
+ $this->userFolder->expects($this->at(1))
+ ->method('getById')
+ ->with(1003)
+ ->willReturn([$filesNodes[1003]]);
+ $this->userFolder->expects($this->at(2))
+ ->method('getById')
+ ->with(1004)
+ ->willReturn([$filesNodes[1004]]);
+
+ $this->server->expects($this->any())
+ ->method('getRequestUri')
+ ->will($this->returnValue($path));
+
+ $this->plugin->initialize($this->server);
+
+ $responses = null;
+ $this->server->expects($this->once())
+ ->method('generateMultiStatus')
+ ->will($this->returnCallback(function($responsesArg) use (&$responses) {
+ $responses = $responsesArg;
+ })
+ );
+
$this->assertFalse($this->plugin->onReport(FilesReportPluginImplementation::REPORT_NAME, $parameters, '/' . $path));
+
+ $this->assertCount(3, $responses);
+
+ $this->assertEquals('/test/nodes/node2', $responses[0]['href']);
+ $this->assertEquals('/test/nodes/node3', $responses[1]['href']);
+ $this->assertEquals('/test/nodes/node4', $responses[2]['href']);
}
public function testFindNodesByFileIdsRoot() {
diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php
index 3a018a2d0faca..ca9a4f3489262 100644
--- a/build/integration/features/bootstrap/WebDav.php
+++ b/build/integration/features/bootstrap/WebDav.php
@@ -410,18 +410,33 @@ public function listFolder($user, $path, $folderDepth, $properties = null){
* @param string $properties properties which needs to be included in the report
* @param string $filterRules filter-rules to choose what needs to appear in the report
*/
- public function reportFolder($user, $path, $properties, $filterRules){
+ public function reportFolder($user, $path, $properties, $filterRules, $offset = null, $limit = null){
$client = $this->getSabreClient($user);
$body = '
-
-
- ' . $properties . '
-
-
- ' . $filterRules . '
-
- ';
+
+
+ ' . $properties . '
+
+
+ ' . $filterRules . '
+ ';
+ if (is_int($offset) || is_int($limit)) {
+ $body .= '
+ ';
+ if (is_int($offset)) {
+ $body .= "
+ ${offset}";
+ }
+ if (is_int($limit)) {
+ $body .= "
+ ${limit}";
+ }
+ $body .= '
+ ';
+ }
+ $body .= '
+ ';
$response = $client->request('REPORT', $this->makeSabrePath($user, $path), $body);
$parsedResponse = $client->parseMultistatus($response['body']);
@@ -714,6 +729,18 @@ public function thereAreNoDuplicateHeaders() {
* @param \Behat\Gherkin\Node\TableNode|null $expectedElements
*/
public function checkFavoritedElements($user, $folder, $expectedElements){
+ $this->checkFavoritedElementsPaginated($user, $folder, $expectedElements, null, null);
+ }
+
+ /**
+ * @Then /^user "([^"]*)" in folder "([^"]*)" should have favorited the following elements from offset ([\d*]) and limit ([\d*])$/
+ * @param string $user
+ * @param string $folder
+ * @param \Behat\Gherkin\Node\TableNode|null $expectedElements
+ * @param int $offset
+ * @param int $limit
+ */
+ public function checkFavoritedElementsPaginated($user, $folder, $expectedElements, $offset, $limit){
$elementList = $this->reportFolder($user,
$folder,
'',
diff --git a/build/integration/features/favorites.feature b/build/integration/features/favorites.feature
index a6c5c9fda950f..9c15616fc3f9f 100644
--- a/build/integration/features/favorites.feature
+++ b/build/integration/features/favorites.feature
@@ -146,3 +146,25 @@ Feature: favorite
When User "user1" moved file "/shared/shared_file.txt" to "/taken_out.txt"
Then user "user1" in folder "/" should have favorited the following elements
| /taken_out.txt |
+
+ Scenario: Get favorited elements paginated
+ Given using old dav path
+ And As an "admin"
+ And user "user0" exists
+ And user "user0" created a folder "/subfolder"
+ And User "user0" copies file "/textfile0.txt" to "/subfolder/textfile0.txt"
+ And User "user0" copies file "/textfile0.txt" to "/subfolder/textfile1.txt"
+ And User "user0" copies file "/textfile0.txt" to "/subfolder/textfile2.txt"
+ And User "user0" copies file "/textfile0.txt" to "/subfolder/textfile3.txt"
+ And User "user0" copies file "/textfile0.txt" to "/subfolder/textfile4.txt"
+ And User "user0" copies file "/textfile0.txt" to "/subfolder/textfile5.txt"
+ When user "user0" favorites element "/subfolder/textfile0.txt"
+ And user "user0" favorites element "/subfolder/textfile1.txt"
+ And user "user0" favorites element "/subfolder/textfile2.txt"
+ And user "user0" favorites element "/subfolder/textfile3.txt"
+ And user "user0" favorites element "/subfolder/textfile4.txt"
+ And user "user0" favorites element "/subfolder/textfile5.txt"
+ Then user "user0" in folder "/subfolder" should have favorited the following elements from offset 3 and limit 2
+ | /subfolder/textfile2.txt |
+ | /subfolder/textfile3.txt |
+