xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; $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) { return [self::REPORT_NAME]; } /** * REPORT operations to look for files * * @param string $reportName * @param $report * @param string $uri * @return bool * @throws BadRequest * @throws PreconditionFailed * @internal param $ [] $report */ public function onReport($reportName, $report, $uri) { $reportTargetNode = $this->server->tree->getNodeForPath($uri); if (!$reportTargetNode instanceof Directory || $reportName !== self::REPORT_NAME) { return; } $ns = '{' . $this::NS_OWNCLOUD . '}'; $ncns = '{' . $this::NS_NEXTCLOUD . '}'; $requestedProps = []; $filterRules = []; // parse report properties and gather filter info foreach ($report as $reportProps) { $name = $reportProps['name']; if ($name === $ns . 'filter-rules') { $filterRules = $reportProps['value']; } elseif ($name === '{DAV:}prop') { // propfind properties foreach ($reportProps['value'] as $propVal) { $requestedProps[] = $propVal['name']; } } elseif ($name === '{DAV:}limit') { foreach ($reportProps['value'] as $propVal) { if ($propVal['name'] === '{DAV:}nresults') { $limit = (int)$propVal['value']; } elseif ($propVal['name'] === $ncns . 'firstresult') { $offset = (int)$propVal['value']; } } } } if (empty($filterRules)) { // an empty filter would return all existing files which would be slow throw new BadRequest('Missing filter-rule block in request'); } // gather all file ids matching filter try { $resultFileIds = $this->processFilterRulesForFileIDs($filterRules); // no logic in circles and favorites for paging, we always have all results, and slice later on $resultFileIds = array_slice($resultFileIds, $offset ?? 0, $limit ?? null); // fetching nodes has paging on DB level – therefore we cannot mix and slice the results, similar // to user backends. I.e. the final result may return more results than requested. $resultNodes = $this->processFilterRulesForFileNodes($filterRules, $limit ?? null, $offset ?? null); } catch (TagNotFoundException $e) { throw new PreconditionFailed('Cannot filter by non-existing tag', 0, $e); } $results = []; foreach ($resultNodes as $entry) { if ($entry) { $results[] = $this->wrapNode($entry); } } // find sabre nodes by file id, restricted to the root node path $additionalNodes = $this->findNodesByFileIds($reportTargetNode, $resultFileIds); if ($additionalNodes && $results) { $results = array_uintersect($results, $additionalNodes, function (Node $a, Node $b): int { return $a->getId() - $b->getId(); }); } elseif (!$results && $additionalNodes) { $results = $additionalNodes; } $filesUri = $this->getFilesBaseUri($uri, $reportTargetNode->getPath()); $responses = $this->prepareResponses($filesUri, $requestedProps, $results); $xml = $this->server->xml->write( '{DAV:}multistatus', new MultiStatus($responses) ); $this->server->httpResponse->setStatus(207); $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); $this->server->httpResponse->setBody($xml); return false; } /** * 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(string $uri, string $subPath): string { $uri = trim($uri, '/'); $subPath = trim($subPath, '/'); if (empty($subPath)) { $filesUri = $uri; } else { $filesUri = substr($uri, 0, strlen($uri) - strlen($subPath)); } $filesUri = trim($filesUri, '/'); if (empty($filesUri)) { return ''; } return '/' . $filesUri; } /** * Find file ids matching the given filter rules * * @param array $filterRules * @return array array of unique file id results */ protected function processFilterRulesForFileIDs(array $filterRules): array { $ns = '{' . $this::NS_OWNCLOUD . '}'; $resultFileIds = []; $circlesIds = []; $favoriteFilter = null; foreach ($filterRules as $filterRule) { if ($filterRule['name'] === self::CIRCLE_PROPERTYNAME) { $circlesIds[] = $filterRule['value']; } if ($filterRule['name'] === $ns . 'favorite') { $favoriteFilter = true; } } if ($favoriteFilter !== null) { $resultFileIds = $this->fileTagger->load('files')->getFavorites(); if (empty($resultFileIds)) { return []; } } if (!empty($circlesIds)) { $fileIds = $this->getCirclesFileIds($circlesIds); if (empty($resultFileIds)) { $resultFileIds = $fileIds; } else { $resultFileIds = array_intersect($fileIds, $resultFileIds); } } return $resultFileIds; } protected function processFilterRulesForFileNodes(array $filterRules, ?int $limit, ?int $offset): array { $systemTagIds = []; foreach ($filterRules as $filterRule) { if ($filterRule['name'] === self::SYSTEMTAG_PROPERTYNAME) { $systemTagIds[] = $filterRule['value']; } } $nodes = []; if (!empty($systemTagIds)) { $tags = $this->tagManager->getTagsByIds($systemTagIds, $this->userSession->getUser()); // For we run DB queries per tag and require intersection, we cannot apply limit and offset for DB queries on multi tag search. $oneTagSearch = count($tags) === 1; $dbLimit = $oneTagSearch ? $limit ?? 0 : 0; $dbOffset = $oneTagSearch ? $offset ?? 0 : 0; foreach ($tags as $tag) { $tagName = $tag->getName(); $tmpNodes = $this->userFolder->searchBySystemTag($tagName, $this->userSession->getUser()->getUID(), $dbLimit, $dbOffset); if (count($nodes) === 0) { $nodes = $tmpNodes; } else { $nodes = array_uintersect($nodes, $tmpNodes, function (INode $a, INode $b): int { return $a->getId() - $b->getId(); }); } if ($nodes === []) { // there cannot be a common match when nodes are empty early. return $nodes; } } if (!$oneTagSearch && ($limit !== null || $offset !== null)) { $nodes = array_slice($nodes, $offset, $limit); } } return $nodes; } /** * @suppress PhanUndeclaredClassMethod * @param array $circlesIds * @return array */ private function getCirclesFileIds(array $circlesIds) { if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) { return []; } return Circles::getFilesForCircles($circlesIds); } /** * Prepare propfind response for the given nodes * * @param string $filesUri $filesUri URI leading to root of the files URI, * with a leading slash but no trailing slash * @param string[] $requestedProps requested properties * @param Node[] nodes nodes for which to fetch and prepare responses * @return Response[] */ public function prepareResponses($filesUri, $requestedProps, $nodes) { $responses = []; foreach ($nodes as $node) { $propFind = new PropFind($filesUri . $node->getPath(), $requestedProps); $this->server->getPropertiesByNode($propFind, $node); // copied from Sabre Server's getPropertiesForPath $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'] .= '/'; } $responses[] = new Response( rtrim($this->server->getBaseUri(), '/') . $filesUri . $node->getPath(), $result, ); } return $responses; } /** * Find Sabre nodes by file ids * * @param Node $rootNode root node for search * @param array $fileIds file ids * @return Node[] array of Sabre nodes */ public function findNodesByFileIds(Node $rootNode, array $fileIds): array { if (empty($fileIds)) { return []; } $folder = $this->userFolder; if (trim($rootNode->getPath(), '/') !== '') { /** @var Folder $folder */ $folder = $folder->get($rootNode->getPath()); } $results = []; foreach ($fileIds as $fileId) { $entry = $folder->getFirstNodeById((int)$fileId); if ($entry) { $results[] = $this->wrapNode($entry); } } return $results; } protected function wrapNode(INode $node): File|Directory { if ($node instanceof \OCP\Files\File) { return new File($this->fileView, $node); } else { return new Directory($this->fileView, $node); } } /** * Returns whether the currently logged in user is an administrator */ private function isAdmin() { $user = $this->userSession->getUser(); if ($user !== null) { return $this->groupManager->isAdmin($user->getUID()); } return false; } }