Skip to content

Commit c7a8c3b

Browse files
authored
Merge pull request #39232 from nextcloud/backport/39202/stable26
2 parents ce036dc + fc9fd0d commit c7a8c3b

9 files changed

Lines changed: 526 additions & 243 deletions

File tree

apps/dav/lib/Connector/Sabre/FilesReportPlugin.php

Lines changed: 80 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use OC\Files\View;
3131
use OCP\App\IAppManager;
3232
use OCP\Files\Folder;
33+
use OCP\Files\Node as INode;
3334
use OCP\IGroupManager;
3435
use OCP\ITagManager;
3536
use OCP\IUserSession;
@@ -45,9 +46,9 @@
4546
use Sabre\DAV\Xml\Response\MultiStatus;
4647

4748
class FilesReportPlugin extends ServerPlugin {
48-
4949
// namespace
5050
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
51+
public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
5152
public const REPORT_NAME = '{http://owncloud.org/ns}filter-files';
5253
public const SYSTEMTAG_PROPERTYNAME = '{http://owncloud.org/ns}systemtag';
5354
public const CIRCLE_PROPERTYNAME = '{http://owncloud.org/ns}circle';
@@ -186,6 +187,7 @@ public function onReport($reportName, $report, $uri) {
186187
}
187188

188189
$ns = '{' . $this::NS_OWNCLOUD . '}';
190+
$ncns = '{' . $this::NS_NEXTCLOUD . '}';
189191
$requestedProps = [];
190192
$filterRules = [];
191193

@@ -199,6 +201,14 @@ public function onReport($reportName, $report, $uri) {
199201
foreach ($reportProps['value'] as $propVal) {
200202
$requestedProps[] = $propVal['name'];
201203
}
204+
} elseif ($name === '{DAV:}limit') {
205+
foreach ($reportProps['value'] as $propVal) {
206+
if ($propVal['name'] === '{DAV:}nresults') {
207+
$limit = (int)$propVal['value'];
208+
} elseif ($propVal['name'] === $ncns . 'firstresult') {
209+
$offset = (int)$propVal['value'];
210+
}
211+
}
202212
}
203213
}
204214

@@ -209,13 +219,32 @@ public function onReport($reportName, $report, $uri) {
209219

210220
// gather all file ids matching filter
211221
try {
212-
$resultFileIds = $this->processFilterRules($filterRules);
222+
$resultFileIds = $this->processFilterRulesForFileIDs($filterRules);
223+
// no logic in circles and favorites for paging, we always have all results, and slice later on
224+
$resultFileIds = array_slice($resultFileIds, $offset ?? 0, $limit ?? null);
225+
// fetching nodes has paging on DB level – therefore we cannot mix and slice the results, similar
226+
// to user backends. I.e. the final result may return more results than requested.
227+
$resultNodes = $this->processFilterRulesForFileNodes($filterRules, $limit ?? null, $offset ?? null);
213228
} catch (TagNotFoundException $e) {
214229
throw new PreconditionFailed('Cannot filter by non-existing tag', 0, $e);
215230
}
216231

232+
$results = [];
233+
foreach ($resultNodes as $entry) {
234+
if ($entry) {
235+
$results[] = $this->wrapNode($entry);
236+
}
237+
}
238+
217239
// find sabre nodes by file id, restricted to the root node path
218-
$results = $this->findNodesByFileIds($reportTargetNode, $resultFileIds);
240+
$additionalNodes = $this->findNodesByFileIds($reportTargetNode, $resultFileIds);
241+
if ($additionalNodes && $results) {
242+
$results = array_uintersect($results, $additionalNodes, function (Node $a, Node $b): int {
243+
return $a->getId() - $b->getId();
244+
});
245+
} elseif (!$results && $additionalNodes) {
246+
$results = $additionalNodes;
247+
}
219248

220249
$filesUri = $this->getFilesBaseUri($uri, $reportTargetNode->getPath());
221250
$responses = $this->prepareResponses($filesUri, $requestedProps, $results);
@@ -261,19 +290,13 @@ private function getFilesBaseUri(string $uri, string $subPath): string {
261290
*
262291
* @param array $filterRules
263292
* @return array array of unique file id results
264-
*
265-
* @throws TagNotFoundException whenever a tag was not found
266293
*/
267-
protected function processFilterRules($filterRules) {
294+
protected function processFilterRulesForFileIDs(array $filterRules): array {
268295
$ns = '{' . $this::NS_OWNCLOUD . '}';
269-
$resultFileIds = null;
270-
$systemTagIds = [];
296+
$resultFileIds = [];
271297
$circlesIds = [];
272298
$favoriteFilter = null;
273299
foreach ($filterRules as $filterRule) {
274-
if ($filterRule['name'] === $ns . 'systemtag') {
275-
$systemTagIds[] = $filterRule['value'];
276-
}
277300
if ($filterRule['name'] === self::CIRCLE_PROPERTYNAME) {
278301
$circlesIds[] = $filterRule['value'];
279302
}
@@ -289,15 +312,6 @@ protected function processFilterRules($filterRules) {
289312
}
290313
}
291314

292-
if (!empty($systemTagIds)) {
293-
$fileIds = $this->getSystemTagFileIds($systemTagIds);
294-
if (empty($resultFileIds)) {
295-
$resultFileIds = $fileIds;
296-
} else {
297-
$resultFileIds = array_intersect($fileIds, $resultFileIds);
298-
}
299-
}
300-
301315
if (!empty($circlesIds)) {
302316
$fileIds = $this->getCirclesFileIds($circlesIds);
303317
if (empty($resultFileIds)) {
@@ -310,47 +324,48 @@ protected function processFilterRules($filterRules) {
310324
return $resultFileIds;
311325
}
312326

313-
private function getSystemTagFileIds($systemTagIds) {
314-
$resultFileIds = null;
315-
316-
// check user permissions, if applicable
317-
if (!$this->isAdmin()) {
318-
// check visibility/permission
319-
$tags = $this->tagManager->getTagsByIds($systemTagIds);
320-
$unknownTagIds = [];
321-
foreach ($tags as $tag) {
322-
if (!$tag->isUserVisible()) {
323-
$unknownTagIds[] = $tag->getId();
324-
}
325-
}
326-
327-
if (!empty($unknownTagIds)) {
328-
throw new TagNotFoundException('Tag with ids ' . implode(', ', $unknownTagIds) . ' not found');
327+
protected function processFilterRulesForFileNodes(array $filterRules, ?int $limit, ?int $offset): array {
328+
$systemTagIds = [];
329+
foreach ($filterRules as $filterRule) {
330+
if ($filterRule['name'] === self::SYSTEMTAG_PROPERTYNAME) {
331+
$systemTagIds[] = $filterRule['value'];
329332
}
330333
}
331334

332-
// fetch all file ids and intersect them
333-
foreach ($systemTagIds as $systemTagId) {
334-
$fileIds = $this->tagMapper->getObjectIdsForTags($systemTagId, 'files');
335+
$nodes = [];
335336

336-
if (empty($fileIds)) {
337-
// This tag has no files, nothing can ever show up
338-
return [];
339-
}
337+
// type check to ensure searchBySystemTag is available, it is not
338+
// exposed in API yet
339+
if (!empty($systemTagIds) && method_exists($this->userFolder, 'searchBySystemTag')) {
340+
$tags = $this->tagManager->getTagsByIds($systemTagIds, $this->userSession->getUser());
340341

341-
// first run ?
342-
if ($resultFileIds === null) {
343-
$resultFileIds = $fileIds;
344-
} else {
345-
$resultFileIds = array_intersect($resultFileIds, $fileIds);
342+
// For we run DB queries per tag and require intersection, we cannot apply limit and offset for DB queries on multi tag search.
343+
$oneTagSearch = count($tags) === 1;
344+
$dbLimit = $oneTagSearch ? $limit ?? 0 : 0;
345+
$dbOffset = $oneTagSearch ? $offset ?? 0 : 0;
346+
347+
foreach ($tags as $tag) {
348+
$tagName = $tag->getName();
349+
$tmpNodes = $this->userFolder->searchBySystemTag($tagName, $this->userSession->getUser()->getUID(), $dbLimit, $dbOffset);
350+
if (count($nodes) === 0) {
351+
$nodes = $tmpNodes;
352+
} else {
353+
$nodes = array_uintersect($nodes, $tmpNodes, function (INode $a, INode $b): int {
354+
return $a->getId() - $b->getId();
355+
});
356+
}
357+
if ($nodes === []) {
358+
// there cannot be a common match when nodes are empty early.
359+
return $nodes;
360+
}
346361
}
347362

348-
if (empty($resultFileIds)) {
349-
// Empty intersection, nothing can show up anymore
350-
return [];
363+
if (!$oneTagSearch && ($limit !== null || $offset !== null)) {
364+
$nodes = array_slice($nodes, $offset, $limit);
351365
}
352366
}
353-
return $resultFileIds;
367+
368+
return $nodes;
354369
}
355370

356371
/**
@@ -406,7 +421,10 @@ public function prepareResponses($filesUri, $requestedProps, $nodes) {
406421
* @param array $fileIds file ids
407422
* @return Node[] array of Sabre nodes
408423
*/
409-
public function findNodesByFileIds($rootNode, $fileIds) {
424+
public function findNodesByFileIds(Node $rootNode, array $fileIds): array {
425+
if (empty($fileIds)) {
426+
return [];
427+
}
410428
$folder = $this->userFolder;
411429
if (trim($rootNode->getPath(), '/') !== '') {
412430
$folder = $folder->get($rootNode->getPath());
@@ -417,17 +435,21 @@ public function findNodesByFileIds($rootNode, $fileIds) {
417435
$entry = $folder->getById($fileId);
418436
if ($entry) {
419437
$entry = current($entry);
420-
if ($entry instanceof \OCP\Files\File) {
421-
$results[] = new File($this->fileView, $entry);
422-
} elseif ($entry instanceof \OCP\Files\Folder) {
423-
$results[] = new Directory($this->fileView, $entry);
424-
}
438+
$results[] = $this->wrapNode($entry);
425439
}
426440
}
427441

428442
return $results;
429443
}
430444

445+
protected function wrapNode(\OCP\Files\File|\OCP\Files\Folder $node): File|Directory {
446+
if ($node instanceof \OCP\Files\File) {
447+
return new File($this->fileView, $node);
448+
} else {
449+
return new Directory($this->fileView, $node);
450+
}
451+
}
452+
431453
/**
432454
* Returns whether the currently logged in user is an administrator
433455
*/

0 commit comments

Comments
 (0)