diff options
Diffstat (limited to 'apps/dav/lib/Files/FileSearchBackend.php')
-rw-r--r-- | apps/dav/lib/Files/FileSearchBackend.php | 271 |
1 files changed, 163 insertions, 108 deletions
diff --git a/apps/dav/lib/Files/FileSearchBackend.php b/apps/dav/lib/Files/FileSearchBackend.php index 524f90e6623..eb548bbd55c 100644 --- a/apps/dav/lib/Files/FileSearchBackend.php +++ b/apps/dav/lib/Files/FileSearchBackend.php @@ -1,27 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> - * - * @author Christian <16852529+cviereck@users.noreply.github.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Files; @@ -29,19 +10,26 @@ use OC\Files\Search\SearchBinaryOperator; use OC\Files\Search\SearchComparison; use OC\Files\Search\SearchOrder; use OC\Files\Search\SearchQuery; +use OC\Files\Storage\Wrapper\Jail; use OC\Files\View; -use OC\Metadata\IMetadataManager; use OCA\DAV\Connector\Sabre\CachingTree; use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\File; use OCA\DAV\Connector\Sabre\FilesPlugin; +use OCA\DAV\Connector\Sabre\Server; use OCA\DAV\Connector\Sabre\TagsPlugin; use OCP\Files\Cache\ICacheEntry; use OCP\Files\Folder; use OCP\Files\IRootFolder; use OCP\Files\Node; +use OCP\Files\Search\ISearchBinaryOperator; +use OCP\Files\Search\ISearchComparison; use OCP\Files\Search\ISearchOperator; use OCP\Files\Search\ISearchOrder; use OCP\Files\Search\ISearchQuery; +use OCP\FilesMetadata\IFilesMetadataManager; +use OCP\FilesMetadata\IMetadataQuery; +use OCP\FilesMetadata\Model\IMetadataValueWrapper; use OCP\IUser; use OCP\Share\IManager; use Sabre\DAV\Exception\NotFound; @@ -57,37 +45,15 @@ use SearchDAV\Query\Query; class FileSearchBackend implements ISearchBackend { public const OPERATOR_LIMIT = 100; - /** @var CachingTree */ - private $tree; - - /** @var IUser */ - private $user; - - /** @var IRootFolder */ - private $rootFolder; - - /** @var IManager */ - private $shareManager; - - /** @var View */ - private $view; - - /** - * FileSearchBackend constructor. - * - * @param CachingTree $tree - * @param IUser $user - * @param IRootFolder $rootFolder - * @param IManager $shareManager - * @param View $view - * @internal param IRootFolder $rootFolder - */ - public function __construct(CachingTree $tree, IUser $user, IRootFolder $rootFolder, IManager $shareManager, View $view) { - $this->tree = $tree; - $this->user = $user; - $this->rootFolder = $rootFolder; - $this->shareManager = $shareManager; - $this->view = $view; + public function __construct( + private Server $server, + private CachingTree $tree, + private IUser $user, + private IRootFolder $rootFolder, + private IManager $shareManager, + private View $view, + private IFilesMetadataManager $filesMetadataManager, + ) { } /** @@ -115,7 +81,7 @@ class FileSearchBackend implements ISearchBackend { // all valid scopes support the same schema //todo dynamically load all propfind properties that are supported - return [ + $props = [ // queryable properties new SearchPropertyDefinition('{DAV:}displayname', true, true, true), new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), @@ -134,9 +100,35 @@ class FileSearchBackend implements ISearchBackend { new SearchPropertyDefinition(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, true, false, false), new SearchPropertyDefinition(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, true, false, false), new SearchPropertyDefinition(FilesPlugin::HAS_PREVIEW_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_BOOLEAN), - new SearchPropertyDefinition(FilesPlugin::FILE_METADATA_SIZE, true, false, false, SearchPropertyDefinition::DATATYPE_STRING), new SearchPropertyDefinition(FilesPlugin::FILEID_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), ]; + + return array_merge($props, $this->getPropertyDefinitionsForMetadata()); + } + + + private function getPropertyDefinitionsForMetadata(): array { + $metadataProps = []; + $metadata = $this->filesMetadataManager->getKnownMetadata(); + $indexes = $metadata->getIndexes(); + foreach ($metadata->getKeys() as $key) { + $isIndex = in_array($key, $indexes); + $type = match ($metadata->getType($key)) { + IMetadataValueWrapper::TYPE_INT => SearchPropertyDefinition::DATATYPE_INTEGER, + IMetadataValueWrapper::TYPE_FLOAT => SearchPropertyDefinition::DATATYPE_DECIMAL, + IMetadataValueWrapper::TYPE_BOOL => SearchPropertyDefinition::DATATYPE_BOOLEAN, + default => SearchPropertyDefinition::DATATYPE_STRING + }; + $metadataProps[] = new SearchPropertyDefinition( + FilesPlugin::FILE_METADATA_PREFIX . $key, + true, + $isIndex, + $isIndex, + $type + ); + } + + return $metadataProps; } /** @@ -144,58 +136,84 @@ class FileSearchBackend implements ISearchBackend { * @param string[] $requestProperties */ public function preloadPropertyFor(array $nodes, array $requestProperties): void { - if (in_array(FilesPlugin::FILE_METADATA_SIZE, $requestProperties, true)) { - // Preloading of the metadata - $fileIds = []; - foreach ($nodes as $node) { - /** @var \OCP\Files\Node|\OCA\DAV\Connector\Sabre\Node $node */ - if (str_starts_with($node->getFileInfo()->getMimeType(), 'image/')) { - /** @var \OCA\DAV\Connector\Sabre\File $node */ - $fileIds[] = $node->getFileInfo()->getId(); - } - } - /** @var IMetaDataManager $metadataManager */ - $metadataManager = \OC::$server->get(IMetadataManager::class); - $preloadedMetadata = $metadataManager->fetchMetadataFor('size', $fileIds); - foreach ($nodes as $node) { - /** @var \OCP\Files\Node|\OCA\DAV\Connector\Sabre\Node $node */ - if (str_starts_with($node->getFileInfo()->getMimeType(), 'image/')) { - /** @var \OCA\DAV\Connector\Sabre\File $node */ - $node->setMetadata('size', $preloadedMetadata[$node->getFileInfo()->getId()]); - } - } - } + $this->server->emit('preloadProperties', [$nodes, $requestProperties]); } - /** - * @param Query $search - * @return SearchResult[] - */ - public function search(Query $search): array { - if (count($search->from) !== 1) { - throw new \InvalidArgumentException('Searching more than one folder is not supported'); - } - $query = $this->transformQuery($search); - $scope = $search->from[0]; - if ($scope->path === null) { + private function getFolderForPath(?string $path = null): Folder { + if ($path === null) { throw new \InvalidArgumentException('Using uri\'s as scope is not supported, please use a path relative to the search arbiter instead'); } - $node = $this->tree->getNodeForPath($scope->path); + + $node = $this->tree->getNodeForPath($path); + if (!$node instanceof Directory) { throw new \InvalidArgumentException('Search is only supported on directories'); } $fileInfo = $node->getFileInfo(); - $folder = $this->rootFolder->get($fileInfo->getPath()); - /** @var Folder $folder $results */ - $results = $folder->search($query); + + /** @var Folder */ + return $this->rootFolder->get($fileInfo->getPath()); + } + + /** + * @param Query $search + * @return SearchResult[] + */ + public function search(Query $search): array { + switch (count($search->from)) { + case 0: + throw new \InvalidArgumentException('You need to specify a scope for the search.'); + break; + case 1: + $scope = $search->from[0]; + $folder = $this->getFolderForPath($scope->path); + $query = $this->transformQuery($search); + $results = $folder->search($query); + break; + default: + $scopes = []; + foreach ($search->from as $scope) { + $folder = $this->getFolderForPath($scope->path); + $folderStorage = $folder->getStorage(); + if ($folderStorage->instanceOfStorage(Jail::class)) { + /** @var Jail $folderStorage */ + $internalPath = $folderStorage->getUnjailedPath($folder->getInternalPath()); + } else { + $internalPath = $folder->getInternalPath(); + } + + $scopes[] = new SearchBinaryOperator( + ISearchBinaryOperator::OPERATOR_AND, + [ + new SearchComparison( + ISearchComparison::COMPARE_EQUAL, + 'storage', + $folderStorage->getCache()->getNumericStorageId(), + '' + ), + new SearchComparison( + ISearchComparison::COMPARE_LIKE, + 'path', + $internalPath . '/%', + '' + ), + ] + ); + } + + $scopeOperators = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $scopes); + $query = $this->transformQuery($search, $scopeOperators); + $userFolder = $this->rootFolder->getUserFolder($this->user->getUID()); + $results = $userFolder->search($query); + } /** @var SearchResult[] $nodes */ $nodes = array_map(function (Node $node) { if ($node instanceof Folder) { - $davNode = new \OCA\DAV\Connector\Sabre\Directory($this->view, $node, $this->tree, $this->shareManager); + $davNode = new Directory($this->view, $node, $this->tree, $this->shareManager); } else { - $davNode = new \OCA\DAV\Connector\Sabre\File($this->view, $node, $this->shareManager); + $davNode = new File($this->view, $node, $this->shareManager); } $path = $this->getHrefForNode($node); $this->tree->cacheNode($davNode, $path); @@ -300,11 +318,20 @@ class FileSearchBackend implements ISearchBackend { /** * @param Query $query + * * @return ISearchQuery */ - private function transformQuery(Query $query): ISearchQuery { + private function transformQuery(Query $query, ?SearchBinaryOperator $scopeOperators = null): ISearchQuery { + $orders = array_map(function (Order $order): ISearchOrder { + $direction = $order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING; + if (str_starts_with($order->property->name, FilesPlugin::FILE_METADATA_PREFIX)) { + return new SearchOrder($direction, substr($order->property->name, strlen(FilesPlugin::FILE_METADATA_PREFIX)), IMetadataQuery::EXTRA); + } else { + return new SearchOrder($direction, $this->mapPropertyNameToColumn($order->property)); + } + }, $query->orderBy); + $limit = $query->limit; - $orders = array_map([$this, 'mapSearchOrder'], $query->orderBy); $offset = $limit->firstResult; $limitHome = false; @@ -322,8 +349,16 @@ class FileSearchBackend implements ISearchBackend { throw new \InvalidArgumentException('Invalid search query, maximum operator limit of ' . self::OPERATOR_LIMIT . ' exceeded, got ' . $operatorCount . ' operators'); } + /** @var SearchBinaryOperator|SearchComparison */ + $queryOperators = $this->transformSearchOperation($query->where); + if ($scopeOperators === null) { + $operators = $queryOperators; + } else { + $operators = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$queryOperators, $scopeOperators]); + } + return new SearchQuery( - $this->transformSearchOperation($query->where), + $operators, (int)$limit->maxResults, $offset, $orders, @@ -353,14 +388,6 @@ class FileSearchBackend implements ISearchBackend { } /** - * @param Order $order - * @return ISearchOrder - */ - private function mapSearchOrder(Order $order) { - return new SearchOrder($order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING, $this->mapPropertyNameToColumn($order->property)); - } - - /** * @param Operator $operator * @return ISearchOperator */ @@ -381,13 +408,37 @@ class FileSearchBackend implements ISearchBackend { if (count($operator->arguments) !== 2) { throw new \InvalidArgumentException('Invalid number of arguments for ' . $trimmedType . ' operation'); } + if (!($operator->arguments[1] instanceof Literal)) { + throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal'); + } + $value = $operator->arguments[1]->value; + // no break + case Operator::OPERATION_IS_DEFINED: if (!($operator->arguments[0] instanceof SearchPropertyDefinition)) { throw new \InvalidArgumentException('Invalid argument 1 for ' . $trimmedType . ' operation, expected property'); } - if (!($operator->arguments[1] instanceof Literal)) { - throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal'); + $property = $operator->arguments[0]; + + if (str_starts_with($property->name, FilesPlugin::FILE_METADATA_PREFIX)) { + $field = substr($property->name, strlen(FilesPlugin::FILE_METADATA_PREFIX)); + $extra = IMetadataQuery::EXTRA; + } else { + $field = $this->mapPropertyNameToColumn($property); + } + + try { + $castedValue = $this->castValue($property, $value ?? ''); + } catch (\Error $e) { + throw new \InvalidArgumentException('Invalid property value for ' . $property->name, previous: $e); } - return new SearchComparison($trimmedType, $this->mapPropertyNameToColumn($operator->arguments[0]), $this->castValue($operator->arguments[0], $operator->arguments[1]->value)); + + return new SearchComparison( + $trimmedType, + $field, + $castedValue, + $extra ?? '' + ); + case Operator::OPERATION_IS_COLLECTION: return new SearchComparison('eq', 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE); default: @@ -421,6 +472,10 @@ class FileSearchBackend implements ISearchBackend { } private function castValue(SearchPropertyDefinition $property, $value) { + if ($value === '') { + return ''; + } + switch ($property->dataType) { case SearchPropertyDefinition::DATATYPE_BOOLEAN: return $value === 'yes'; |