3030use OC \Files \View ;
3131use OCP \App \IAppManager ;
3232use OCP \Files \Folder ;
33+ use OCP \Files \Node as INode ;
3334use OCP \IGroupManager ;
3435use OCP \ITagManager ;
3536use OCP \IUserSession ;
4546use Sabre \DAV \Xml \Response \MultiStatus ;
4647
4748class 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