Skip to content

Commit

Permalink
Adjust REPORT search query, perf and unit tests
Browse files Browse the repository at this point in the history
- disable REPORT without filter

Because we need to use Node::search($pattern) to find all matching nodes
in all the subfolder recursively, but the result nodes contain
incomplete information like owner.

- remove unneeded trailing slash when buildling response href

This got obsoleted when switching to generateMultiStatus()

- add integration pagination test for favorites REPORT

- throw exception for unknown node types in FilesReportPlugin

Signed-off-by: Vincent Petry <pvince81@owncloud.com>
  • Loading branch information
Vincent Petry authored and icewind1991 committed Apr 26, 2017
1 parent 2f2cc99 commit 26f40db
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 53 deletions.
50 changes: 33 additions & 17 deletions apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,27 +179,31 @@ 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);
} catch (TagNotFoundException $e) {
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);

Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -358,17 +366,25 @@ 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;
}
}
}

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
*/
Expand Down
22 changes: 14 additions & 8 deletions apps/dav/lib/Files/Xml/FilterRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class FilterRequest implements XmlDeserializable {
/**
* @var array
*/
public $limit;
public $search;

/**
* The deserialize method is called during xml parsing.
Expand All @@ -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 = [
Expand All @@ -60,7 +60,7 @@ static function xmlDeserialize(Reader $reader) {
'favorite' => null
],
'properties' => [],
'limit' => null,
'search' => null,
];

if (!is_array($elems)) {
Expand All @@ -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();
Expand Down
154 changes: 135 additions & 19 deletions apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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');
Expand All @@ -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')
Expand All @@ -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() {
Expand Down
Loading

0 comments on commit 26f40db

Please sign in to comment.