123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389 |
- <?php
- /**
- * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
- * SPDX-License-Identifier: AGPL-3.0-only
- */
- namespace OCA\DAV\Connector\Sabre;
- use OC\Files\View;
- use OCA\Circles\Api\v1\Circles;
- use OCP\App\IAppManager;
- use OCP\Files\Folder;
- use OCP\Files\Node as INode;
- use OCP\IGroupManager;
- use OCP\ITagManager;
- use OCP\IUserSession;
- use OCP\SystemTag\ISystemTagManager;
- use OCP\SystemTag\ISystemTagObjectMapper;
- use OCP\SystemTag\TagNotFoundException;
- use Sabre\DAV\Exception\BadRequest;
- use Sabre\DAV\Exception\PreconditionFailed;
- use Sabre\DAV\PropFind;
- use Sabre\DAV\ServerPlugin;
- use Sabre\DAV\Tree;
- use Sabre\DAV\Xml\Element\Response;
- use Sabre\DAV\Xml\Response\MultiStatus;
- class FilesReportPlugin extends ServerPlugin {
- // namespace
- public const NS_OWNCLOUD = 'http://owncloud.org/ns';
- public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
- public const REPORT_NAME = '{http://owncloud.org/ns}filter-files';
- public const SYSTEMTAG_PROPERTYNAME = '{http://owncloud.org/ns}systemtag';
- public const CIRCLE_PROPERTYNAME = '{http://owncloud.org/ns}circle';
- /**
- * Reference to main server object
- *
- * @var \Sabre\DAV\Server
- */
- private $server;
- /**
- * @param Tree $tree
- * @param View $fileView
- * @param ISystemTagManager $tagManager
- * @param ISystemTagObjectMapper $tagMapper
- * @param ITagManager $fileTagger manager for private tags
- * @param IUserSession $userSession
- * @param IGroupManager $groupManager
- * @param Folder $userFolder
- * @param IAppManager $appManager
- */
- public function __construct(
- private Tree $tree,
- private View $fileView,
- private ISystemTagManager $tagManager,
- private ISystemTagObjectMapper $tagMapper,
- /**
- * Manager for private tags
- */
- private ITagManager $fileTagger,
- private IUserSession $userSession,
- private IGroupManager $groupManager,
- private Folder $userFolder,
- private IAppManager $appManager,
- ) {
- }
- /**
- * This initializes the plugin.
- *
- * This function is called by \Sabre\DAV\Server, after
- * addPlugin is called.
- *
- * This method should set up the required event subscriptions.
- *
- * @param \Sabre\DAV\Server $server
- * @return void
- */
- public function initialize(\Sabre\DAV\Server $server) {
- $server->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;
- }
- }
|