diff options
Diffstat (limited to 'lib/private/Search')
-rw-r--r-- | lib/private/Search/Filter/BooleanFilter.php | 29 | ||||
-rw-r--r-- | lib/private/Search/Filter/DateTimeFilter.php | 29 | ||||
-rw-r--r-- | lib/private/Search/Filter/FloatFilter.php | 28 | ||||
-rw-r--r-- | lib/private/Search/Filter/GroupFilter.php | 34 | ||||
-rw-r--r-- | lib/private/Search/Filter/IntegerFilter.php | 28 | ||||
-rw-r--r-- | lib/private/Search/Filter/StringFilter.php | 27 | ||||
-rw-r--r-- | lib/private/Search/Filter/StringsFilter.php | 34 | ||||
-rw-r--r-- | lib/private/Search/Filter/UserFilter.php | 34 | ||||
-rw-r--r-- | lib/private/Search/FilterCollection.php | 43 | ||||
-rw-r--r-- | lib/private/Search/FilterFactory.php | 43 | ||||
-rw-r--r-- | lib/private/Search/Provider/File.php | 108 | ||||
-rw-r--r-- | lib/private/Search/Result/Audio.php | 42 | ||||
-rw-r--r-- | lib/private/Search/Result/File.php | 152 | ||||
-rw-r--r-- | lib/private/Search/Result/Folder.php | 38 | ||||
-rw-r--r-- | lib/private/Search/Result/Image.php | 42 | ||||
-rw-r--r-- | lib/private/Search/SearchComposer.php | 309 | ||||
-rw-r--r-- | lib/private/Search/SearchQuery.php | 103 | ||||
-rw-r--r-- | lib/private/Search/UnsupportedFilter.php | 17 |
18 files changed, 625 insertions, 515 deletions
diff --git a/lib/private/Search/Filter/BooleanFilter.php b/lib/private/Search/Filter/BooleanFilter.php new file mode 100644 index 00000000000..894dc13b657 --- /dev/null +++ b/lib/private/Search/Filter/BooleanFilter.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Search\Filter; + +use InvalidArgumentException; +use OCP\Search\IFilter; + +class BooleanFilter implements IFilter { + private bool $value; + + public function __construct(string $value) { + $this->value = match ($value) { + 'true', 'yes', 'y', '1' => true, + 'false', 'no', 'n', '0', '' => false, + default => throw new InvalidArgumentException('Invalid boolean value ' . $value), + }; + } + + public function get(): bool { + return $this->value; + } +} diff --git a/lib/private/Search/Filter/DateTimeFilter.php b/lib/private/Search/Filter/DateTimeFilter.php new file mode 100644 index 00000000000..48c1725a5e1 --- /dev/null +++ b/lib/private/Search/Filter/DateTimeFilter.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Search\Filter; + +use DateTimeImmutable; +use OCP\Search\IFilter; + +class DateTimeFilter implements IFilter { + private DateTimeImmutable $value; + + public function __construct(string $value) { + if (filter_var($value, FILTER_VALIDATE_INT)) { + $value = '@' . $value; + } + + $this->value = new DateTimeImmutable($value); + } + + public function get(): DateTimeImmutable { + return $this->value; + } +} diff --git a/lib/private/Search/Filter/FloatFilter.php b/lib/private/Search/Filter/FloatFilter.php new file mode 100644 index 00000000000..f2384552943 --- /dev/null +++ b/lib/private/Search/Filter/FloatFilter.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Search\Filter; + +use InvalidArgumentException; +use OCP\Search\IFilter; + +class FloatFilter implements IFilter { + private float $value; + + public function __construct(string $value) { + $this->value = filter_var($value, FILTER_VALIDATE_FLOAT); + if ($this->value === false) { + throw new InvalidArgumentException('Invalid float value ' . $value); + } + } + + public function get(): float { + return $this->value; + } +} diff --git a/lib/private/Search/Filter/GroupFilter.php b/lib/private/Search/Filter/GroupFilter.php new file mode 100644 index 00000000000..fe0b2ce42d8 --- /dev/null +++ b/lib/private/Search/Filter/GroupFilter.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Search\Filter; + +use InvalidArgumentException; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\Search\IFilter; + +class GroupFilter implements IFilter { + private IGroup $group; + + public function __construct( + string $value, + IGroupManager $groupManager, + ) { + $group = $groupManager->get($value); + if ($group === null) { + throw new InvalidArgumentException('Group ' . $value . ' not found'); + } + $this->group = $group; + } + + public function get(): IGroup { + return $this->group; + } +} diff --git a/lib/private/Search/Filter/IntegerFilter.php b/lib/private/Search/Filter/IntegerFilter.php new file mode 100644 index 00000000000..028b7d0678f --- /dev/null +++ b/lib/private/Search/Filter/IntegerFilter.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Search\Filter; + +use InvalidArgumentException; +use OCP\Search\IFilter; + +class IntegerFilter implements IFilter { + private int $value; + + public function __construct(string $value) { + $this->value = filter_var($value, FILTER_VALIDATE_INT); + if ($this->value === false) { + throw new InvalidArgumentException('Invalid integer value ' . $value); + } + } + + public function get(): int { + return $this->value; + } +} diff --git a/lib/private/Search/Filter/StringFilter.php b/lib/private/Search/Filter/StringFilter.php new file mode 100644 index 00000000000..6944a7803f3 --- /dev/null +++ b/lib/private/Search/Filter/StringFilter.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Search\Filter; + +use InvalidArgumentException; +use OCP\Search\IFilter; + +class StringFilter implements IFilter { + public function __construct( + private string $value, + ) { + if ($value === '') { + throw new InvalidArgumentException('String filter can’t be empty'); + } + } + + public function get(): string { + return $this->value; + } +} diff --git a/lib/private/Search/Filter/StringsFilter.php b/lib/private/Search/Filter/StringsFilter.php new file mode 100644 index 00000000000..8b8fabb5347 --- /dev/null +++ b/lib/private/Search/Filter/StringsFilter.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Search\Filter; + +use InvalidArgumentException; +use OCP\Search\IFilter; + +class StringsFilter implements IFilter { + /** + * @var string[] + */ + private array $values; + + public function __construct(string ...$values) { + $this->values = array_unique(array_filter($values)); + if (empty($this->values)) { + throw new InvalidArgumentException('Strings filter can’t be empty'); + } + } + + /** + * @return string[] + */ + public function get(): array { + return $this->values; + } +} diff --git a/lib/private/Search/Filter/UserFilter.php b/lib/private/Search/Filter/UserFilter.php new file mode 100644 index 00000000000..4f2061a4ba6 --- /dev/null +++ b/lib/private/Search/Filter/UserFilter.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Search\Filter; + +use InvalidArgumentException; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Search\IFilter; + +class UserFilter implements IFilter { + private IUser $user; + + public function __construct( + string $value, + IUserManager $userManager, + ) { + $user = $userManager->get($value); + if ($user === null) { + throw new InvalidArgumentException('User ' . $value . ' not found'); + } + $this->user = $user; + } + + public function get(): IUser { + return $this->user; + } +} diff --git a/lib/private/Search/FilterCollection.php b/lib/private/Search/FilterCollection.php new file mode 100644 index 00000000000..173c967245a --- /dev/null +++ b/lib/private/Search/FilterCollection.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Search; + +use Generator; +use OCP\Search\IFilter; +use OCP\Search\IFilterCollection; + +/** + * Interface for search filters + * + * @since 28.0.0 + */ +class FilterCollection implements IFilterCollection { + /** + * @var IFilter[] + */ + private array $filters; + + public function __construct(IFilter ...$filters) { + $this->filters = $filters; + } + + public function has(string $name): bool { + return isset($this->filters[$name]); + } + + public function get(string $name): ?IFilter { + return $this->filters[$name] ?? null; + } + + public function getIterator(): Generator { + foreach ($this->filters as $k => $v) { + yield $k => $v; + } + } +} diff --git a/lib/private/Search/FilterFactory.php b/lib/private/Search/FilterFactory.php new file mode 100644 index 00000000000..07063c604f4 --- /dev/null +++ b/lib/private/Search/FilterFactory.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Search; + +use OCP\IGroupManager; +use OCP\IUserManager; +use OCP\Search\FilterDefinition; +use OCP\Search\IFilter; +use RuntimeException; + +final class FilterFactory { + private const PERSON_TYPE_SEPARATOR = '/'; + + public static function get(string $type, string|array $filter): IFilter { + return match ($type) { + FilterDefinition::TYPE_BOOL => new Filter\BooleanFilter($filter), + FilterDefinition::TYPE_DATETIME => new Filter\DateTimeFilter($filter), + FilterDefinition::TYPE_FLOAT => new Filter\FloatFilter($filter), + FilterDefinition::TYPE_INT => new Filter\IntegerFilter($filter), + FilterDefinition::TYPE_NC_GROUP => new Filter\GroupFilter($filter, \OC::$server->get(IGroupManager::class)), + FilterDefinition::TYPE_NC_USER => new Filter\UserFilter($filter, \OC::$server->get(IUserManager::class)), + FilterDefinition::TYPE_PERSON => self::getPerson($filter), + FilterDefinition::TYPE_STRING => new Filter\StringFilter($filter), + FilterDefinition::TYPE_STRINGS => new Filter\StringsFilter(... (array)$filter), + default => throw new RuntimeException('Invalid filter type ' . $type), + }; + } + + private static function getPerson(string $person): IFilter { + $parts = explode(self::PERSON_TYPE_SEPARATOR, $person, 2); + + return match (count($parts)) { + 1 => self::get(FilterDefinition::TYPE_NC_USER, $person), + 2 => self::get(... $parts), + }; + } +} diff --git a/lib/private/Search/Provider/File.php b/lib/private/Search/Provider/File.php deleted file mode 100644 index fba8e6db05f..00000000000 --- a/lib/private/Search/Provider/File.php +++ /dev/null @@ -1,108 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Andrew Brown <andrew@casabrown.com> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Jakob Sack <mail@jakobsack.de> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OC\Search\Provider; - -use OC\Files\Search\SearchComparison; -use OC\Files\Search\SearchOrder; -use OC\Files\Search\SearchQuery; -use OCP\Files\FileInfo; -use OCP\Files\IRootFolder; -use OCP\Files\Search\ISearchComparison; -use OCP\Files\Search\ISearchOrder; -use OCP\IUserSession; -use OCP\Search\PagedProvider; - -/** - * Provide search results from the 'files' app - * @deprecated 20.0.0 - */ -class File extends PagedProvider { - - /** - * Search for files and folders matching the given query - * - * @param string $query - * @param int|null $limit - * @param int|null $offset - * @return \OCP\Search\Result[] - * @deprecated 20.0.0 - */ - public function search($query, int $limit = null, int $offset = null) { - /** @var IRootFolder $rootFolder */ - $rootFolder = \OC::$server->query(IRootFolder::class); - /** @var IUserSession $userSession */ - $userSession = \OC::$server->query(IUserSession::class); - $user = $userSession->getUser(); - if (!$user) { - return []; - } - $userFolder = $rootFolder->getUserFolder($user->getUID()); - $fileQuery = new SearchQuery( - new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $query . '%'), - (int)$limit, - (int)$offset, - [ - new SearchOrder(ISearchOrder::DIRECTION_DESCENDING, 'mtime'), - ], - $user - ); - $files = $userFolder->search($fileQuery); - $results = []; - // edit results - foreach ($files as $fileData) { - // create audio result - if ($fileData->getMimePart() === 'audio') { - $result = new \OC\Search\Result\Audio($fileData); - } - // create image result - elseif ($fileData->getMimePart() === 'image') { - $result = new \OC\Search\Result\Image($fileData); - } - // create folder result - elseif ($fileData->getMimetype() === FileInfo::MIMETYPE_FOLDER) { - $result = new \OC\Search\Result\Folder($fileData); - } - // or create file result - else { - $result = new \OC\Search\Result\File($fileData); - } - // add to results - $results[] = $result; - } - // return - return $results; - } - - public function searchPaged($query, $page, $size) { - if ($size === 0) { - return $this->search($query); - } else { - return $this->search($query, $size, ($page - 1) * $size); - } - } -} diff --git a/lib/private/Search/Result/Audio.php b/lib/private/Search/Result/Audio.php deleted file mode 100644 index 0a734767df0..00000000000 --- a/lib/private/Search/Result/Audio.php +++ /dev/null @@ -1,42 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Andrew Brown <andrew@casabrown.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OC\Search\Result; - -/** - * A found audio file - * @deprecated 20.0.0 - */ -class Audio extends File { - - /** - * Type name; translated in templates - * @var string - * @deprecated 20.0.0 - */ - public $type = 'audio'; - - /** - * @TODO add ID3 information - */ -} diff --git a/lib/private/Search/Result/File.php b/lib/private/Search/Result/File.php deleted file mode 100644 index dc10cab09e9..00000000000 --- a/lib/private/Search/Result/File.php +++ /dev/null @@ -1,152 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Andrew Brown <andrew@casabrown.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OC\Search\Result; - -use OCP\Files\FileInfo; -use OCP\Files\Folder; -use OCP\IPreview; -use OCP\IUserSession; - -/** - * A found file - * @deprecated 20.0.0 - */ -class File extends \OCP\Search\Result { - - /** - * Type name; translated in templates - * @var string - * @deprecated 20.0.0 - */ - public $type = 'file'; - - /** - * Path to file - * @var string - * @deprecated 20.0.0 - */ - public $path; - - /** - * Size, in bytes - * @var int - * @deprecated 20.0.0 - */ - public $size; - - /** - * Date modified, in human readable form - * @var string - * @deprecated 20.0.0 - */ - public $modified; - - /** - * File mime type - * @var string - * @deprecated 20.0.0 - */ - public $mime_type; - - /** - * File permissions: - * - * @var string - * @deprecated 20.0.0 - */ - public $permissions; - - /** - * Has a preview - * - * @var string - * @deprecated 20.0.0 - */ - public $has_preview; - - /** - * Create a new file search result - * @param FileInfo $data file data given by provider - * @deprecated 20.0.0 - */ - public function __construct(FileInfo $data) { - $path = $this->getRelativePath($data->getPath()); - - $this->id = $data->getId(); - $this->name = $data->getName(); - $this->link = \OC::$server->getURLGenerator()->linkToRoute( - 'files.view.index', - [ - 'dir' => dirname($path), - 'scrollto' => $data->getName(), - ] - ); - $this->permissions = $data->getPermissions(); - $this->path = $path; - $this->size = $data->getSize(); - $this->modified = $data->getMtime(); - $this->mime_type = $data->getMimetype(); - $this->has_preview = $this->hasPreview($data); - } - - /** - * @var Folder $userFolderCache - * @deprecated 20.0.0 - */ - protected static $userFolderCache = null; - - /** - * converts a path relative to the users files folder - * eg /user/files/foo.txt -> /foo.txt - * @param string $path - * @return string relative path - * @deprecated 20.0.0 - */ - protected function getRelativePath($path) { - if (!isset(self::$userFolderCache)) { - $userSession = \OC::$server->get(IUserSession::class); - $userID = $userSession->getUser()->getUID(); - self::$userFolderCache = \OC::$server->getUserFolder($userID); - } - $relativePath = self::$userFolderCache->getRelativePath($path); - if ($relativePath === null) { - throw new \Exception("Search result not in user folder"); - } - return $relativePath; - } - - /** - * Is the preview available - * @param FileInfo $data - * @return bool - * @deprecated 20.0.0 - */ - protected function hasPreview($data) { - $previewManager = \OC::$server->get(IPreview::class); - return $previewManager->isAvailable($data); - } -} diff --git a/lib/private/Search/Result/Folder.php b/lib/private/Search/Result/Folder.php deleted file mode 100644 index 590943fb941..00000000000 --- a/lib/private/Search/Result/Folder.php +++ /dev/null @@ -1,38 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Andrew Brown <andrew@casabrown.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OC\Search\Result; - -/** - * A found folder - * @deprecated 20.0.0 - */ -class Folder extends File { - - /** - * Type name; translated in templates - * @var string - * @deprecated 20.0.0 - */ - public $type = 'folder'; -} diff --git a/lib/private/Search/Result/Image.php b/lib/private/Search/Result/Image.php deleted file mode 100644 index 9870e316a79..00000000000 --- a/lib/private/Search/Result/Image.php +++ /dev/null @@ -1,42 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Andrew Brown <andrew@casabrown.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OC\Search\Result; - -/** - * A found image file - * @deprecated 20.0.0 - */ -class Image extends File { - - /** - * Type name; translated in templates - * @var string - * @deprecated 20.0.0 - */ - public $type = 'image'; - - /** - * @TODO add EXIF information - */ -} diff --git a/lib/private/Search/SearchComposer.php b/lib/private/Search/SearchComposer.php index 3c228261ec2..be366e8ba6c 100644 --- a/lib/private/Search/SearchComposer.php +++ b/lib/private/Search/SearchComposer.php @@ -3,40 +3,33 @@ declare(strict_types=1); /** - * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @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: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Search; use InvalidArgumentException; -use OCP\AppFramework\QueryException; -use OCP\IServerContainer; +use OC\AppFramework\Bootstrap\Coordinator; +use OC\Core\ResponseDefinitions; +use OCP\IAppConfig; +use OCP\IURLGenerator; use OCP\IUser; +use OCP\Search\FilterDefinition; +use OCP\Search\IExternalProvider; +use OCP\Search\IFilter; +use OCP\Search\IFilteringProvider; +use OCP\Search\IInAppSearch; use OCP\Search\IProvider; use OCP\Search\ISearchQuery; use OCP\Search\SearchResult; -use OC\AppFramework\Bootstrap\Coordinator; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; +use RuntimeException; +use function array_filter; use function array_map; +use function array_values; +use function in_array; /** * Queries individual \OCP\Search\IProvider implementations and composes a @@ -56,34 +49,44 @@ use function array_map; * results are awaited or shown as they come in. * * @see IProvider::search() for the arguments of the individual search requests + * @psalm-import-type CoreUnifiedSearchProvider from ResponseDefinitions */ class SearchComposer { + /** + * @var array<string, array{appId: string, provider: IProvider}> + */ + private array $providers = []; - /** @var IProvider[] */ - private $providers = []; - - /** @var Coordinator */ - private $bootstrapCoordinator; - - /** @var IServerContainer */ - private $container; + private array $commonFilters; + private array $customFilters = []; - private LoggerInterface $logger; + private array $handlers = []; - public function __construct(Coordinator $bootstrapCoordinator, - IServerContainer $container, - LoggerInterface $logger) { - $this->container = $container; - $this->logger = $logger; - $this->bootstrapCoordinator = $bootstrapCoordinator; + public function __construct( + private Coordinator $bootstrapCoordinator, + private ContainerInterface $container, + private IURLGenerator $urlGenerator, + private LoggerInterface $logger, + private IAppConfig $appConfig, + ) { + $this->commonFilters = [ + IFilter::BUILTIN_TERM => new FilterDefinition(IFilter::BUILTIN_TERM, FilterDefinition::TYPE_STRING), + IFilter::BUILTIN_SINCE => new FilterDefinition(IFilter::BUILTIN_SINCE, FilterDefinition::TYPE_DATETIME), + IFilter::BUILTIN_UNTIL => new FilterDefinition(IFilter::BUILTIN_UNTIL, FilterDefinition::TYPE_DATETIME), + IFilter::BUILTIN_TITLE_ONLY => new FilterDefinition(IFilter::BUILTIN_TITLE_ONLY, FilterDefinition::TYPE_BOOL, false), + IFilter::BUILTIN_PERSON => new FilterDefinition(IFilter::BUILTIN_PERSON, FilterDefinition::TYPE_PERSON), + IFilter::BUILTIN_PLACES => new FilterDefinition(IFilter::BUILTIN_PLACES, FilterDefinition::TYPE_STRINGS, false), + IFilter::BUILTIN_PROVIDER => new FilterDefinition(IFilter::BUILTIN_PROVIDER, FilterDefinition::TYPE_STRING, false), + ]; } /** * Load all providers dynamically that were registered through `registerProvider` * + * If $targetProviderId is provided, only this provider is loaded * If a provider can't be loaded we log it but the operation continues nevertheless */ - private function loadLazyProviders(): void { + private function loadLazyProviders(?string $targetProviderId = null): void { $context = $this->bootstrapCoordinator->getRegistrationContext(); if ($context === null) { // Too early, nothing registered yet @@ -94,9 +97,20 @@ class SearchComposer { foreach ($registrations as $registration) { try { /** @var IProvider $provider */ - $provider = $this->container->query($registration->getService()); - $this->providers[$provider->getId()] = $provider; - } catch (QueryException $e) { + $provider = $this->container->get($registration->getService()); + $providerId = $provider->getId(); + if ($targetProviderId !== null && $targetProviderId !== $providerId) { + continue; + } + $this->providers[$providerId] = [ + 'appId' => $registration->getAppId(), + 'provider' => $provider, + ]; + $this->handlers[$providerId] = [$providerId]; + if ($targetProviderId !== null) { + break; + } + } catch (ContainerExceptionInterface $e) { // Log an continue. We can be fault tolerant here. $this->logger->error('Could not load search provider dynamically: ' . $e->getMessage(), [ 'exception' => $e, @@ -104,6 +118,45 @@ class SearchComposer { ]); } } + + $this->filterProviders(); + + $this->loadFilters(); + } + + private function loadFilters(): void { + foreach ($this->providers as $providerId => $providerData) { + $appId = $providerData['appId']; + $provider = $providerData['provider']; + if (!$provider instanceof IFilteringProvider) { + continue; + } + + foreach ($provider->getCustomFilters() as $filter) { + $this->registerCustomFilter($filter, $providerId); + } + foreach ($provider->getAlternateIds() as $alternateId) { + $this->handlers[$alternateId][] = $providerId; + } + foreach ($provider->getSupportedFilters() as $filterName) { + if ($this->getFilterDefinition($filterName, $providerId) === null) { + throw new InvalidArgumentException('Invalid filter ' . $filterName); + } + } + } + } + + private function registerCustomFilter(FilterDefinition $filter, string $providerId): void { + $name = $filter->name(); + if (isset($this->commonFilters[$name])) { + throw new InvalidArgumentException('Filter name is already used'); + } + + if (isset($this->customFilters[$providerId])) { + $this->customFilters[$providerId][$name] = $filter; + } else { + $this->customFilters[$providerId] = [$name => $filter]; + } } /** @@ -113,32 +166,173 @@ class SearchComposer { * @param string $route the route the user is currently at * @param array $routeParameters the parameters of the route the user is currently at * - * @return array + * @return list<CoreUnifiedSearchProvider> */ public function getProviders(string $route, array $routeParameters): array { $this->loadLazyProviders(); - $providers = array_values( - array_map(function (IProvider $provider) use ($route, $routeParameters) { + $providers = array_map( + function (array $providerData) use ($route, $routeParameters) { + $appId = $providerData['appId']; + $provider = $providerData['provider']; + $order = $provider->getOrder($route, $routeParameters); + if ($order === null) { + return; + } + $isExternalProvider = $provider instanceof IExternalProvider ? $provider->isExternalProvider() : false; + $triggers = [$provider->getId()]; + if ($provider instanceof IFilteringProvider) { + $triggers += $provider->getAlternateIds(); + $filters = $provider->getSupportedFilters(); + } else { + $filters = [IFilter::BUILTIN_TERM]; + } + return [ 'id' => $provider->getId(), + 'appId' => $appId, 'name' => $provider->getName(), - 'order' => $provider->getOrder($route, $routeParameters), + 'icon' => $this->fetchIcon($appId, $provider->getId()), + 'order' => $order, + 'isExternalProvider' => $isExternalProvider, + 'triggers' => array_values($triggers), + 'filters' => $this->getFiltersType($filters, $provider->getId()), + 'inAppSearch' => $provider instanceof IInAppSearch, ]; - }, $this->providers) + }, + $this->providers, ); + $providers = array_filter($providers); + // Sort providers by order and strip associative keys usort($providers, function ($provider1, $provider2) { return $provider1['order'] <=> $provider2['order']; }); - /** - * Return an array with the IDs, but strip the associative keys - */ return $providers; } /** + * Filter providers based on 'unified_search.providers_allowed' core app config array + * Will remove providers that are not in the allowed list + */ + private function filterProviders(): void { + $allowedProviders = $this->appConfig->getValueArray('core', 'unified_search.providers_allowed'); + + if (empty($allowedProviders)) { + return; + } + + foreach (array_keys($this->providers) as $providerId) { + if (!in_array($providerId, $allowedProviders, true)) { + unset($this->providers[$providerId]); + unset($this->handlers[$providerId]); + } + } + } + + private function fetchIcon(string $appId, string $providerId): string { + $icons = [ + [$providerId, $providerId . '.svg'], + [$providerId, 'app.svg'], + [$appId, $providerId . '.svg'], + [$appId, $appId . '.svg'], + [$appId, 'app.svg'], + ['core', 'places/default-app-icon.svg'], + ]; + if ($appId === 'settings' && $providerId === 'users') { + // Conflict: + // the file /apps/settings/users.svg is already used in black version by top right user menu + // Override icon name here + $icons = [['settings', 'users-white.svg']]; + } + foreach ($icons as $i => $icon) { + try { + return $this->urlGenerator->imagePath(... $icon); + } catch (RuntimeException $e) { + // Ignore error + } + } + + return ''; + } + + /** + * @param $filters string[] + * @return array<string, string> + */ + private function getFiltersType(array $filters, string $providerId): array { + $filterList = []; + foreach ($filters as $filter) { + $filterList[$filter] = $this->getFilterDefinition($filter, $providerId)->type(); + } + + return $filterList; + } + + private function getFilterDefinition(string $name, string $providerId): ?FilterDefinition { + if (isset($this->commonFilters[$name])) { + return $this->commonFilters[$name]; + } + if (isset($this->customFilters[$providerId][$name])) { + return $this->customFilters[$providerId][$name]; + } + + return null; + } + + /** + * @param array<string, string> $parameters + */ + public function buildFilterList(string $providerId, array $parameters): FilterCollection { + $this->loadLazyProviders($providerId); + + $list = []; + foreach ($parameters as $name => $value) { + $filter = $this->buildFilter($name, $value, $providerId); + if ($filter === null) { + continue; + } + $list[$name] = $filter; + } + + return new FilterCollection(... $list); + } + + private function buildFilter(string $name, string $value, string $providerId): ?IFilter { + $filterDefinition = $this->getFilterDefinition($name, $providerId); + if ($filterDefinition === null) { + $this->logger->debug('Unable to find {name} definition', [ + 'name' => $name, + 'value' => $value, + ]); + + return null; + } + + if (!$this->filterSupportedByProvider($filterDefinition, $providerId)) { + // FIXME Use dedicated exception and handle it + throw new UnsupportedFilter($name, $providerId); + } + + return FilterFactory::get($filterDefinition->type(), $value); + } + + private function filterSupportedByProvider(FilterDefinition $filterDefinition, string $providerId): bool { + // Non exclusive filters can be ommited by apps + if (!$filterDefinition->exclusive()) { + return true; + } + + $provider = $this->providers[$providerId]['provider']; + $supportedFilters = $provider instanceof IFilteringProvider + ? $provider->getSupportedFilters() + : [IFilter::BUILTIN_TERM]; + + return in_array($filterDefinition->name(), $supportedFilters, true); + } + + /** * Query an individual search provider for results * * @param IUser $user @@ -148,15 +342,18 @@ class SearchComposer { * @return SearchResult * @throws InvalidArgumentException when the $providerId does not correspond to a registered provider */ - public function search(IUser $user, - string $providerId, - ISearchQuery $query): SearchResult { - $this->loadLazyProviders(); + public function search( + IUser $user, + string $providerId, + ISearchQuery $query, + ): SearchResult { + $this->loadLazyProviders($providerId); - $provider = $this->providers[$providerId] ?? null; + $provider = $this->providers[$providerId]['provider'] ?? null; if ($provider === null) { throw new InvalidArgumentException("Provider $providerId is unknown"); } + return $provider->search($user, $query); } } diff --git a/lib/private/Search/SearchQuery.php b/lib/private/Search/SearchQuery.php index c89446d5970..791edb7a0f7 100644 --- a/lib/private/Search/SearchQuery.php +++ b/lib/private/Search/SearchQuery.php @@ -3,113 +3,62 @@ declare(strict_types=1); /** - * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @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: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Search; +use OCP\Search\IFilter; +use OCP\Search\IFilterCollection; use OCP\Search\ISearchQuery; class SearchQuery implements ISearchQuery { public const LIMIT_DEFAULT = 5; - /** @var string */ - private $term; - - /** @var int */ - private $sortOrder; - - /** @var int */ - private $limit; - - /** @var int|string|null */ - private $cursor; - - /** @var string */ - private $route; - - /** @var array */ - private $routeParameters; - /** - * @param string $term - * @param int $sortOrder - * @param int $limit - * @param int|string|null $cursor - * @param string $route - * @param array $routeParameters + * @param string[] $params Request query + * @param string[] $routeParameters */ - public function __construct(string $term, - int $sortOrder = ISearchQuery::SORT_DATE_DESC, - int $limit = self::LIMIT_DEFAULT, - $cursor = null, - string $route = '', - array $routeParameters = []) { - $this->term = $term; - $this->sortOrder = $sortOrder; - $this->limit = $limit; - $this->cursor = $cursor; - $this->route = $route; - $this->routeParameters = $routeParameters; + public function __construct( + private IFilterCollection $filters, + private int $sortOrder = ISearchQuery::SORT_DATE_DESC, + private int $limit = self::LIMIT_DEFAULT, + private int|string|null $cursor = null, + private string $route = '', + private array $routeParameters = [], + ) { } - /** - * @inheritDoc - */ public function getTerm(): string { - return $this->term; + return $this->getFilter('term')?->get() ?? ''; + } + + public function getFilter(string $name): ?IFilter { + return $this->filters->has($name) + ? $this->filters->get($name) + : null; + } + + public function getFilters(): IFilterCollection { + return $this->filters; } - /** - * @inheritDoc - */ public function getSortOrder(): int { return $this->sortOrder; } - /** - * @inheritDoc - */ public function getLimit(): int { return $this->limit; } - /** - * @inheritDoc - */ - public function getCursor() { + public function getCursor(): int|string|null { return $this->cursor; } - /** - * @inheritDoc - */ public function getRoute(): string { return $this->route; } - /** - * @inheritDoc - */ public function getRouteParameters(): array { return $this->routeParameters; } diff --git a/lib/private/Search/UnsupportedFilter.php b/lib/private/Search/UnsupportedFilter.php new file mode 100644 index 00000000000..ea520e6b872 --- /dev/null +++ b/lib/private/Search/UnsupportedFilter.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Search; + +use Exception; + +final class UnsupportedFilter extends Exception { + public function __construct(string $filerName, $providerId) { + parent::__construct('Provider ' . $providerId . ' doesn’t support filter ' . $filerName . '.'); + } +} |