From 26d4498c08af7325cc82953124a74c082f29a1c7 Mon Sep 17 00:00:00 2001 From: Benjamin Gaussorgues Date: Thu, 21 Sep 2023 16:10:48 +0200 Subject: [PATCH] feat(search): Allow multiple search terms in UnifiedController Signed-off-by: Benjamin Gaussorgues --- core/Controller/UnifiedSearchController.php | 25 ++-- core/openapi.json | 11 +- lib/private/Search/Filter/BoolFilter.php | 48 ++++++ lib/private/Search/Filter/DateTimeFilter.php | 50 +++++++ lib/private/Search/Filter/FloatFilter.php | 49 ++++++ lib/private/Search/Filter/IntFilter.php | 49 ++++++ .../Search/Filter/MultiStringFilter.php | 52 +++++++ lib/private/Search/Filter/StringFilter.php | 49 ++++++ lib/private/Search/Filter/UserFilter.php | 58 +++++++ lib/private/Search/InvalidFilter.php | 44 ++++++ lib/private/Search/MultiFilter.php | 51 +++++++ lib/private/Search/SearchComposer.php | 141 +++++++++++++++--- lib/private/Search/SearchQuery.php | 80 +++------- lib/private/Search/SingleFilter.php | 48 ++++++ lib/public/Search/FilterCollection.php | 78 ++++++++++ lib/public/Search/IFilter.php | 56 +++++++ lib/public/Search/IProvider.php | 1 + lib/public/Search/IProviderV2.php | 61 ++++++++ lib/public/Search/ISearchQuery.php | 15 ++ 19 files changed, 865 insertions(+), 101 deletions(-) create mode 100644 lib/private/Search/Filter/BoolFilter.php create mode 100644 lib/private/Search/Filter/DateTimeFilter.php create mode 100644 lib/private/Search/Filter/FloatFilter.php create mode 100644 lib/private/Search/Filter/IntFilter.php create mode 100644 lib/private/Search/Filter/MultiStringFilter.php create mode 100644 lib/private/Search/Filter/StringFilter.php create mode 100644 lib/private/Search/Filter/UserFilter.php create mode 100644 lib/private/Search/InvalidFilter.php create mode 100644 lib/private/Search/MultiFilter.php create mode 100644 lib/private/Search/SingleFilter.php create mode 100644 lib/public/Search/FilterCollection.php create mode 100644 lib/public/Search/IFilter.php create mode 100644 lib/public/Search/IProviderV2.php diff --git a/core/Controller/UnifiedSearchController.php b/core/Controller/UnifiedSearchController.php index 9704850bb1f27..44f02021de540 100644 --- a/core/Controller/UnifiedSearchController.php +++ b/core/Controller/UnifiedSearchController.php @@ -28,12 +28,13 @@ */ namespace OC\Core\Controller; +use OC\Search\InvalidFilter; use OC\Search\SearchComposer; use OC\Search\SearchQuery; use OCA\Core\ResponseDefinitions; -use OCP\AppFramework\OCSController; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; use OCP\IRequest; use OCP\IURLGenerator; use OCP\IUserSession; @@ -83,7 +84,6 @@ public function getProviders(string $from = ''): DataResponse { * Search * * @param string $providerId ID of the provider - * @param string $term Term to search * @param int|null $sortOrder Order of entries * @param int|null $limit Maximum amount of entries * @param int|string|null $cursor Offset for searching @@ -95,22 +95,25 @@ public function getProviders(string $from = ''): DataResponse { * 400: Searching is not possible */ public function search(string $providerId, - string $term = '', - ?int $sortOrder = null, - ?int $limit = null, - $cursor = null, - string $from = ''): DataResponse { - if (trim($term) === "") { - return new DataResponse(null, Http::STATUS_BAD_REQUEST); - } + ?int $sortOrder = null, + ?int $limit = null, + $cursor = null, + string $from = ''): DataResponse { [$route, $routeParameters] = $this->getRouteInformation($from); + try { + $filters = $this->composer->buildFilterList($providerId, $this->request->getParams()); + } catch (InvalidFilter $e) { + // Unsupported filter, send empty answer + return new DataResponse([], Http::STATUS_OK); + } + return new DataResponse( $this->composer->search( $this->userSession->getUser(), $providerId, new SearchQuery( - $term, + $filters, $sortOrder ?? ISearchQuery::SORT_DATE_DESC, $limit ?? SearchQuery::LIMIT_DEFAULT, $cursor, diff --git a/core/openapi.json b/core/openapi.json index a9c810a790ae0..0f419ff5a3963 100644 --- a/core/openapi.json +++ b/core/openapi.json @@ -3966,15 +3966,6 @@ } ], "parameters": [ - { - "name": "term", - "in": "query", - "description": "Term to search", - "schema": { - "type": "string", - "default": "" - } - }, { "name": "sortOrder", "in": "query", @@ -5162,4 +5153,4 @@ "description": "Controller about the endpoint /ocm-provider/" } ] -} \ No newline at end of file +} diff --git a/lib/private/Search/Filter/BoolFilter.php b/lib/private/Search/Filter/BoolFilter.php new file mode 100644 index 0000000000000..d9b3978dee774 --- /dev/null +++ b/lib/private/Search/Filter/BoolFilter.php @@ -0,0 +1,48 @@ + + * + * @author Benjamin Gaussorgues + * + * @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 . + * + */ + +namespace OC\Search\Filter; + +use OC\Search\SingleFilter; + +class BoolFilter extends SingleFilter { + private bool $value; + + protected function set(string $value): void { + $this->value = match ($value) { + 'true', 'yes', 'y', '1' => true, + 'false', 'no', 'n', '0', '' => false, + }; + } + + public function get(): bool { + return $this->value; + } + + public static function type(): string { + return 'bool'; + } +} diff --git a/lib/private/Search/Filter/DateTimeFilter.php b/lib/private/Search/Filter/DateTimeFilter.php new file mode 100644 index 0000000000000..2ebfdec71ba5f --- /dev/null +++ b/lib/private/Search/Filter/DateTimeFilter.php @@ -0,0 +1,50 @@ + + * + * @author Benjamin Gaussorgues + * + * @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 . + * + */ + +namespace OC\Search\Filter; + +use DateTimeImmutable; +use OC\Search\SingleFilter; + +class DateTimeFilter extends SingleFilter { + private DateTimeImmutable $value; + + protected function set(string $value): void { + if (filter_var($value, FILTER_VALIDATE_INT)) { + $value = '@'.$value; + } + + $this->value = new DateTimeImmutable($value); + } + + public function get(): DateTimeImmutable { + return $this->value; + } + + public static function type(): string { + return 'datetime'; + } +} diff --git a/lib/private/Search/Filter/FloatFilter.php b/lib/private/Search/Filter/FloatFilter.php new file mode 100644 index 0000000000000..0b8b9428a5978 --- /dev/null +++ b/lib/private/Search/Filter/FloatFilter.php @@ -0,0 +1,49 @@ + + * + * @author Benjamin Gaussorgues + * + * @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 . + * + */ + +namespace OC\Search\Filter; + +use OC\Search\InvalidFilter; +use OC\Search\SingleFilter; + +class FloatFilter extends SingleFilter { + private float $value; + + protected function set(string $value): void { + $this->value = filter_var($value, FILTER_VALIDATE_FLOAT); + if ($this->value === false) { + throw new InvalidFilter($value); + } + } + + public function get(): float { + return $this->value; + } + + public static function type(): string { + return 'float'; + } +} diff --git a/lib/private/Search/Filter/IntFilter.php b/lib/private/Search/Filter/IntFilter.php new file mode 100644 index 0000000000000..6fc7216b4cb84 --- /dev/null +++ b/lib/private/Search/Filter/IntFilter.php @@ -0,0 +1,49 @@ + + * + * @author Benjamin Gaussorgues + * + * @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 . + * + */ + +namespace OC\Search\Filter; + +use OC\Search\InvalidFilter; +use OC\Search\SingleFilter; + +class IntFilter extends SingleFilter { + private int $value; + + protected function set(string $value): void { + $this->value = filter_var($value, FILTER_VALIDATE_INT); + if ($this->value === false) { + throw new InvalidFilter($value); + } + } + + public function get(): int { + return $this->value; + } + + public static function type(): string { + return 'int'; + } +} diff --git a/lib/private/Search/Filter/MultiStringFilter.php b/lib/private/Search/Filter/MultiStringFilter.php new file mode 100644 index 0000000000000..bd82fb29ee037 --- /dev/null +++ b/lib/private/Search/Filter/MultiStringFilter.php @@ -0,0 +1,52 @@ + + * + * @author Benjamin Gaussorgues + * + * @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 . + * + */ + +namespace OC\Search\Filter; + +use OC\Search\InvalidFilter; +use OC\Search\MultiFilter; + +class MultiStringFilter extends MultiFilter { + /** + * @var string[] + */ + private array $values; + + protected function set(string ...$values): void { + $this->values = array_unique(array_filter($values)); + if (empty($this->values)) { + throw new InvalidFilter($values); + } + } + + public function get(): array { + return $this->values; + } + + public static function type(): string { + return 'string'; + } +} diff --git a/lib/private/Search/Filter/StringFilter.php b/lib/private/Search/Filter/StringFilter.php new file mode 100644 index 0000000000000..15ea96bf3cc98 --- /dev/null +++ b/lib/private/Search/Filter/StringFilter.php @@ -0,0 +1,49 @@ + + * + * @author Benjamin Gaussorgues + * + * @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 . + * + */ + +namespace OC\Search\Filter; + +use OC\Search\InvalidFilter; +use OC\Search\SingleFilter; + +class StringFilter extends SingleFilter { + private string $value; + + protected function set(string $value): void { + if ($value === '') { + throw new InvalidFilter($value); + } + $this->value = $value; + } + + public function get(): string { + return $this->value; + } + + public static function type(): string { + return 'string'; + } +} diff --git a/lib/private/Search/Filter/UserFilter.php b/lib/private/Search/Filter/UserFilter.php new file mode 100644 index 0000000000000..9ed980f4daf17 --- /dev/null +++ b/lib/private/Search/Filter/UserFilter.php @@ -0,0 +1,58 @@ + + * + * @author Benjamin Gaussorgues + * + * @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 . + * + */ + +namespace OC\Search\Filter; + +use OC\Search\InvalidFilter; +use OC\Search\SingleFilter; +use OCP\IUser; +use OCP\IUserManager; + +class UserFilter extends SingleFilter { + private IUser $user; + + public function __construct( + string $value, + private IUserManager $userManager, + ) { + parent::__construct($value); + } + + protected function set(string $value): void { + $this->user = $this->userManager->get($value); + if ($this->user === null) { + throw new InvalidFilter($value); + } + } + + public function get(): IUser { + return $this->user; + } + + public static function type(): string { + return 'user'; + } +} diff --git a/lib/private/Search/InvalidFilter.php b/lib/private/Search/InvalidFilter.php new file mode 100644 index 0000000000000..d0ef7a63d7bdc --- /dev/null +++ b/lib/private/Search/InvalidFilter.php @@ -0,0 +1,44 @@ + + * + * @author Benjamin Gaussorgues + * + * @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 . + * + */ +namespace OC\Search; + +use InvalidArgumentException; +use Throwable; + +final class InvalidFilter extends InvalidArgumentException { + public function __construct( + string|array $value, + Throwable $previous = null, + ) { + if (is_array($value)) { + $value = 'Array('.implode(',', $value).')'; + } + parent::__construct( + message: sprintf('Invalid value "%s" for filter', $value), + previous: $previous + ); + } +} diff --git a/lib/private/Search/MultiFilter.php b/lib/private/Search/MultiFilter.php new file mode 100644 index 0000000000000..46bbc9e8bfa27 --- /dev/null +++ b/lib/private/Search/MultiFilter.php @@ -0,0 +1,51 @@ + + * + * @author Benjamin Gaussorgues + * + * @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 . + * + */ + +namespace OC\Search; + +use OCP\Search\IFilter; +use Throwable; + +abstract class MultiFilter implements IFilter { + final public function __construct(string|array $value) { + if (is_string($value)) { + $value = [$value]; + } + try { + $this->set(... $value); + } catch (InvalidFilter $e) { + throw $e; + } catch (Throwable $e) { + throw new InvalidFilter($value, $e); + } + } + + abstract protected function set(string ...$values): void; + + public static function multiple(): bool { + return true; + } +} diff --git a/lib/private/Search/SearchComposer.php b/lib/private/Search/SearchComposer.php index 4ec73ec54e976..caddb69752117 100644 --- a/lib/private/Search/SearchComposer.php +++ b/lib/private/Search/SearchComposer.php @@ -28,14 +28,18 @@ namespace OC\Search; use InvalidArgumentException; -use OCP\AppFramework\QueryException; -use OCP\IServerContainer; +use OC\AppFramework\Bootstrap\Coordinator; +use OC\Search\Filter\StringFilter; use OCP\IUser; +use OCP\Search\FilterCollection; use OCP\Search\IProvider; +use OCP\Search\IProviderV2; 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_map; /** @@ -58,23 +62,19 @@ * @see IProvider::search() for the arguments of the individual search requests */ class SearchComposer { - /** @var IProvider[] */ - private $providers = []; - - /** @var Coordinator */ - private $bootstrapCoordinator; - - /** @var IServerContainer */ - private $container; + /** + * @var IProvider[] + */ + private array $providers = []; - private LoggerInterface $logger; + private array $filters = []; + 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 LoggerInterface $logger + ) { } /** @@ -93,9 +93,11 @@ private function loadLazyProviders(): void { 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(); + $this->providers[$providerId] = $provider; + $this->handlers[$providerId] = [$providerId]; + } 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, @@ -103,6 +105,48 @@ private function loadLazyProviders(): void { ]); } } + + $this->loadSupportedFilters(); + $this->loadAlternateIds(); + } + + private function loadSupportedFilters(): void { + foreach ($this->providers as $providerId => $provider) { + if (!$provider instanceof IProviderV2) { + // TODO Send deprecated warning? + $this->registerFilter('term', StringFilter::class, $provider->getId()); + continue; + } + + foreach ($provider->getSupportedFilters() as $name => $filter) { + $this->registerFilter($name, $filter, $provider->getId()); + } + } + } + + private function registerFilter(string $name, string $filter, string $providerId): void { + if (!class_exists($filter)) { + throw new RuntimeException('Invalid filter class provided'); + } + if (!isset($this->filters[$name])) { + $this->filters[$name] = [$providerId => $filter]; + return; + } + + $this->filters[$name][$providerId] = $filter; + } + + private function loadAlternateIds(): void { + foreach ($this->providers as $providerId => $provider) { + if (!$provider instanceof IProviderV2) { + // TODO Send deprecated warning? + continue; + } + + foreach ($provider->getAlternateIds() as $alternateId) { + $this->handlers[$alternateId][] = $providerId; + } + } } /** @@ -119,10 +163,19 @@ public function getProviders(string $route, array $routeParameters): array { $providers = array_values( array_map(function (IProvider $provider) use ($route, $routeParameters) { + $triggers = [$provider->getId()]; + if ($provider instanceof IProviderV2) { + $triggers = array_merge($triggers, $provider->getAlternateIds()); + $filters = $provider->getSupportedFilters(); + } else { + $filters = ['term' => StringFilter::class]; + } return [ 'id' => $provider->getId(), 'name' => $provider->getName(), 'order' => $provider->getOrder($route, $routeParameters), + 'triggers' => $triggers, + 'filters' => $this->getFiltersAsArray($filters), ]; }, $this->providers) ); @@ -137,6 +190,42 @@ public function getProviders(string $route, array $routeParameters): array { return $providers; } + /** + * @param $filters array{string, IFilter} + * @return array{string, array{type: string, multiple: bool}} + */ + private function getFiltersAsArray(array $filters): array { + $filterList = []; + foreach ($filters as $name => $filter) { + $filterList[$name] = [ + 'type' => $filter::type(), + 'multiple' => $filter::multiple(), + ]; + } + + return $filterList; + } + + public function buildFilterList(string $providerId, array $filters): FilterCollection { + $this->loadLazyProviders(); + + $list = []; + foreach ($filters as $name => $value) { + if (!isset($this->filters[$name])) { + // Non existing filter + continue; + } + if (!isset($this->filters[$name][$providerId])) { + // Current filter isn't supported by app + throw new InvalidFilter($value); + } + $class = $this->filters[$name][$providerId]; + $list[$name] = new $class($value); + } + + return new FilterCollection(... $list); + } + /** * Query an individual search provider for results * @@ -147,15 +236,19 @@ public function getProviders(string $route, array $routeParameters): array { * @return SearchResult * @throws InvalidArgumentException when the $providerId does not correspond to a registered provider */ - public function search(IUser $user, - string $providerId, - ISearchQuery $query): SearchResult { + public function search( + IUser $user, + string $providerId, + ISearchQuery $query, + ): SearchResult { + // TODO Only load specified provider? $this->loadLazyProviders(); $provider = $this->providers[$providerId] ?? 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 c89446d59703b..8c0f72322846c 100644 --- a/lib/private/Search/SearchQuery.php +++ b/lib/private/Search/SearchQuery.php @@ -27,89 +27,57 @@ */ namespace OC\Search; +use OCP\Search\FilterCollection; +use OCP\Search\IFilter; 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 FilterCollection $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(): FilterCollection { + 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/SingleFilter.php b/lib/private/Search/SingleFilter.php new file mode 100644 index 0000000000000..d3c43fbcb5426 --- /dev/null +++ b/lib/private/Search/SingleFilter.php @@ -0,0 +1,48 @@ + + * + * @author Benjamin Gaussorgues + * + * @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 . + * + */ + +namespace OC\Search; + +use OCP\Search\IFilter; +use Throwable; + +abstract class SingleFilter implements IFilter { + public function __construct(string $value) { + try { + $this->set($value); + } catch (InvalidFilter $e) { + throw $e; + } catch (Throwable $e) { + throw new InvalidFilter($value, $e); + } + } + + abstract protected function set(string $value): void; + + public static function multiple(): bool { + return false; + } +} diff --git a/lib/public/Search/FilterCollection.php b/lib/public/Search/FilterCollection.php new file mode 100644 index 0000000000000..cad00d0d50c87 --- /dev/null +++ b/lib/public/Search/FilterCollection.php @@ -0,0 +1,78 @@ + + * + * @author Benjamin Gaussorgues + * + * @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 . + * + */ +namespace OCP\Search; + +use ArrayIterator; +use IteratorAggregate; + +/** + * Interface for search filters + * + * @since 28.0.0 + * @implements IteratorAggregate + */ +class FilterCollection implements IteratorAggregate { + /** + * @var IFilter[] + */ + private array $filters; + + /** + * Constructor + * + * @since 28.0.0 + */ + public function __construct(IFilter ...$filters) { + $this->filters = $filters; + } + + /** + * Check if a filter exits + * + * @since 28.0.0 + */ + public function has(string $name): bool { + return isset($this->filters[$name]); + } + + /** + * Get a filter by name + * + * @since 28.0.0 + */ + public function get(string $name): ?IFilter { + return $this->filters[$name] ?? null; + } + + /** + * Return Iterator of filters + * + * @since 28.0.0 + */ + public function getIterator(): \Traversable { + return new ArrayIterator($this->filters); + } +} diff --git a/lib/public/Search/IFilter.php b/lib/public/Search/IFilter.php new file mode 100644 index 0000000000000..937b5ecb97534 --- /dev/null +++ b/lib/public/Search/IFilter.php @@ -0,0 +1,56 @@ + + * + * @author Benjamin Gaussorgues + * + * @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 . + * + */ +namespace OCP\Search; + +/** + * Interface for search filters + * + * @since 28.0.0 + */ +interface IFilter { + /** + * Get filter value + * + * @since 28.0.0 + */ + public function get(): mixed; + + /** + * Indicates if this filter use multiple values + * + * @since 28.0.0 + */ + public static function multiple(): bool; + + /** + * Filter type as string + * + * Examples: string, int, user… + * + * @since 28.0.0 + */ + public static function type(): string; +} diff --git a/lib/public/Search/IProvider.php b/lib/public/Search/IProvider.php index 61655c47367f1..25fe8c0dff019 100644 --- a/lib/public/Search/IProvider.php +++ b/lib/public/Search/IProvider.php @@ -38,6 +38,7 @@ * register one provider per group. * * @since 20.0.0 + * @deprecated 28.0.0 Uses IProviderV2 instead to support advanced search */ interface IProvider { /** diff --git a/lib/public/Search/IProviderV2.php b/lib/public/Search/IProviderV2.php new file mode 100644 index 0000000000000..c76cac0e9fe22 --- /dev/null +++ b/lib/public/Search/IProviderV2.php @@ -0,0 +1,61 @@ + + * + * @author Benjamin Gaussorgues + * + * @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 . + * + */ +namespace OCP\Search; + +/** + * Interface for search providers + * + * These providers will be implemented in apps, so they can participate in the + * global search results of Nextcloud. If an app provides more than one type of + * resource, e.g. contacts and address books in Nextcloud Contacts, it should + * register one provider per group. + * + * @since 28.0.0 + */ +interface IProviderV2 extends IProvider { + /** + * Get the ID of other providers handled by this provider + * + * A search provider can complete results from other search providers. + * For example, files and full-text-search can search in files. + * If you use `in:files` in a search, provider files will be invoked, + * with all other providers declaring `files` in this method + * + * @since 28.0.0 + * @return array{array-key, literal-string} IDs + */ + public function getAlternateIds(): array; + + /** + * Return the list of filters handled by the search provider + * + * If a filter outside of this list is sent by client, the provider will be ignored + * + * @since 28.0.0 + * @return array{string, class-string} + */ + public function getSupportedFilters(): array; +} diff --git a/lib/public/Search/ISearchQuery.php b/lib/public/Search/ISearchQuery.php index a545d1dbccbac..3f2c18c3c5126 100644 --- a/lib/public/Search/ISearchQuery.php +++ b/lib/public/Search/ISearchQuery.php @@ -48,9 +48,24 @@ interface ISearchQuery { * * @return string the search term * @since 20.0.0 + * @deprecated 28.0.0 */ public function getTerm(): string; + /** + * Get a single request filter + * + * @since 28.0.0 + */ + public function getFilter(string $name): ?IFilter; + + /** + * Get request filters + * + * @since 28.0.0 + */ + public function getFilters(): FilterCollection; + /** * Get the sort order of results as defined as SORT_* constants on this interface *