]> source.dussan.org Git - nextcloud-server.git/commitdiff
feat(search): Allow multiple search terms in UnifiedController
authorBenjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
Thu, 21 Sep 2023 14:10:48 +0000 (16:10 +0200)
committerBenjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
Fri, 10 Nov 2023 08:21:16 +0000 (09:21 +0100)
Signed-off-by: Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
26 files changed:
core/Controller/UnifiedSearchController.php
core/openapi.json
lib/composer/composer/InstalledVersions.php
lib/composer/composer/autoload_classmap.php
lib/composer/composer/autoload_psr4.php
lib/composer/composer/autoload_static.php
lib/composer/composer/installed.json
lib/composer/composer/installed.php
lib/private/Search/Filter/BooleanFilter.php [new file with mode: 0644]
lib/private/Search/Filter/DateTimeFilter.php [new file with mode: 0644]
lib/private/Search/Filter/FloatFilter.php [new file with mode: 0644]
lib/private/Search/Filter/GroupFilter.php [new file with mode: 0644]
lib/private/Search/Filter/IntegerFilter.php [new file with mode: 0644]
lib/private/Search/Filter/StringFilter.php [new file with mode: 0644]
lib/private/Search/Filter/StringsFilter.php [new file with mode: 0644]
lib/private/Search/Filter/UserFilter.php [new file with mode: 0644]
lib/private/Search/FilterCollection.php [new file with mode: 0644]
lib/private/Search/FilterFactory.php [new file with mode: 0644]
lib/private/Search/SearchComposer.php
lib/private/Search/SearchQuery.php
lib/private/Search/UnsupportedFilter.php [new file with mode: 0644]
lib/public/Search/FilterDefinition.php [new file with mode: 0644]
lib/public/Search/IFilter.php [new file with mode: 0644]
lib/public/Search/IFilterCollection.php [new file with mode: 0644]
lib/public/Search/IFilteringProvider.php [new file with mode: 0644]
lib/public/Search/ISearchQuery.php

index 9704850bb1f27e7da9ad2388651828ea880c3ace..87aa84e1d91b67994cd84a2522bdc1da04c21a77 100644 (file)
@@ -31,14 +31,15 @@ namespace OC\Core\Controller;
 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;
 use OCP\Route\IRouter;
 use OCP\Search\ISearchQuery;
+use OC\Search\UnsupportedFilter;
 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
 
 /**
@@ -80,7 +81,10 @@ class UnifiedSearchController extends OCSController {
         * @NoAdminRequired
         * @NoCSRFRequired
         *
-        * Search
+        * Launch a search for a specific search provider.
+        *
+        * Additional filters are available for each provider.
+        * Send a request to /providers endpoint to list providers with their available filters.
         *
         * @param string $providerId ID of the provider
         * @param string $term Term to search
@@ -89,28 +93,33 @@ class UnifiedSearchController extends OCSController {
         * @param int|string|null $cursor Offset for searching
         * @param string $from The current user URL
         *
-        * @return DataResponse<Http::STATUS_OK, CoreUnifiedSearchResult, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, null, array{}>
+        * @return DataResponse<Http::STATUS_OK, CoreUnifiedSearchResult, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, string, array{}>
         *
         * 200: Search entries returned
         * 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);
-               }
+       public function search(
+               string $providerId,
+               // Unused parameter for OpenAPI spec generator
+               string $term = '',
+               ?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 (UnsupportedFilter $e) {
+                       return new DataResponse($e->getMessage(), Http::STATUS_BAD_REQUEST);
+               }
                return new DataResponse(
                        $this->composer->search(
                                $this->userSession->getUser(),
                                $providerId,
                                new SearchQuery(
-                                       $term,
+                                       $filters,
                                        $sortOrder ?? ISearchQuery::SORT_DATE_DESC,
                                        $limit ?? SearchQuery::LIMIT_DEFAULT,
                                        $cursor,
index 7cb48b58f0a4cbaf66dbbddc07121331618bf9e3..872a2f80a51ca133596e1664c55e6f34c98a155f 100644 (file)
             "description": "Controller about the endpoint /ocm-provider/"
         }
     ]
-}
\ No newline at end of file
+}
index c6b54af7ba2e1e3e960134233321efe47aa4528c..51e734a774b3ed9ca110a921cb40a74f8c7905c2 100644 (file)
@@ -98,7 +98,7 @@ class InstalledVersions
     {
         foreach (self::getInstalled() as $installed) {
             if (isset($installed['versions'][$packageName])) {
-                return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
+                return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
             }
         }
 
@@ -119,7 +119,7 @@ class InstalledVersions
      */
     public static function satisfies(VersionParser $parser, $packageName, $constraint)
     {
-        $constraint = $parser->parseConstraints($constraint);
+        $constraint = $parser->parseConstraints((string) $constraint);
         $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
 
         return $provided->matches($constraint);
@@ -328,7 +328,9 @@ class InstalledVersions
                 if (isset(self::$installedByVendor[$vendorDir])) {
                     $installed[] = self::$installedByVendor[$vendorDir];
                 } elseif (is_file($vendorDir.'/composer/installed.php')) {
-                    $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
+                    /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                    $required = require $vendorDir.'/composer/installed.php';
+                    $installed[] = self::$installedByVendor[$vendorDir] = $required;
                     if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
                         self::$installed = $installed[count($installed) - 1];
                     }
@@ -340,12 +342,17 @@ class InstalledVersions
             // only require the installed.php file if this file is loaded from its dumped location,
             // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
             if (substr(__DIR__, -8, 1) !== 'C') {
-                self::$installed = require __DIR__ . '/installed.php';
+                /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                $required = require __DIR__ . '/installed.php';
+                self::$installed = $required;
             } else {
                 self::$installed = array();
             }
         }
-        $installed[] = self::$installed;
+
+        if (self::$installed !== array()) {
+            $installed[] = self::$installed;
+        }
 
         return $installed;
     }
index 8eaf0970494c15122146b461536fde51b8cb0043..7d5eee2f4b9f6c4d418d3953e44282dc76d7bdf2 100644 (file)
@@ -6,6 +6,19 @@ $vendorDir = dirname(__DIR__);
 $baseDir = dirname(dirname($vendorDir));
 
 return array(
+    'Bamarni\\Composer\\Bin\\ApplicationFactory\\FreshInstanceApplicationFactory' => $vendorDir . '/bamarni/composer-bin-plugin/src/ApplicationFactory/FreshInstanceApplicationFactory.php',
+    'Bamarni\\Composer\\Bin\\ApplicationFactory\\NamespaceApplicationFactory' => $vendorDir . '/bamarni/composer-bin-plugin/src/ApplicationFactory/NamespaceApplicationFactory.php',
+    'Bamarni\\Composer\\Bin\\BamarniBinPlugin' => $vendorDir . '/bamarni/composer-bin-plugin/src/BamarniBinPlugin.php',
+    'Bamarni\\Composer\\Bin\\CommandProvider' => $vendorDir . '/bamarni/composer-bin-plugin/src/CommandProvider.php',
+    'Bamarni\\Composer\\Bin\\Command\\BinCommand' => $vendorDir . '/bamarni/composer-bin-plugin/src/Command/BinCommand.php',
+    'Bamarni\\Composer\\Bin\\Command\\CouldNotCreateNamespaceDir' => $vendorDir . '/bamarni/composer-bin-plugin/src/Command/CouldNotCreateNamespaceDir.php',
+    'Bamarni\\Composer\\Bin\\Config\\Config' => $vendorDir . '/bamarni/composer-bin-plugin/src/Config/Config.php',
+    'Bamarni\\Composer\\Bin\\Config\\ConfigFactory' => $vendorDir . '/bamarni/composer-bin-plugin/src/Config/ConfigFactory.php',
+    'Bamarni\\Composer\\Bin\\Config\\InvalidBamarniComposerExtraConfig' => $vendorDir . '/bamarni/composer-bin-plugin/src/Config/InvalidBamarniComposerExtraConfig.php',
+    'Bamarni\\Composer\\Bin\\Input\\BinInputFactory' => $vendorDir . '/bamarni/composer-bin-plugin/src/Input/BinInputFactory.php',
+    'Bamarni\\Composer\\Bin\\Input\\InvalidBinInput' => $vendorDir . '/bamarni/composer-bin-plugin/src/Input/InvalidBinInput.php',
+    'Bamarni\\Composer\\Bin\\Logger' => $vendorDir . '/bamarni/composer-bin-plugin/src/Logger.php',
+    'Bamarni\\Composer\\Bin\\PublicIO' => $vendorDir . '/bamarni/composer-bin-plugin/src/PublicIO.php',
     'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
     'OCP\\Accounts\\IAccount' => $baseDir . '/lib/public/Accounts/IAccount.php',
     'OCP\\Accounts\\IAccountManager' => $baseDir . '/lib/public/Accounts/IAccountManager.php',
@@ -589,6 +602,10 @@ return array(
     'OCP\\Route\\IRouter' => $baseDir . '/lib/public/Route/IRouter.php',
     'OCP\\SabrePluginEvent' => $baseDir . '/lib/public/SabrePluginEvent.php',
     'OCP\\SabrePluginException' => $baseDir . '/lib/public/SabrePluginException.php',
+    'OCP\\Search\\FilterDefinition' => $baseDir . '/lib/public/Search/FilterDefinition.php',
+    'OCP\\Search\\IFilter' => $baseDir . '/lib/public/Search/IFilter.php',
+    'OCP\\Search\\IFilterCollection' => $baseDir . '/lib/public/Search/IFilterCollection.php',
+    'OCP\\Search\\IFilteringProvider' => $baseDir . '/lib/public/Search/IFilteringProvider.php',
     'OCP\\Search\\IProvider' => $baseDir . '/lib/public/Search/IProvider.php',
     'OCP\\Search\\ISearchQuery' => $baseDir . '/lib/public/Search/ISearchQuery.php',
     'OCP\\Search\\PagedProvider' => $baseDir . '/lib/public/Search/PagedProvider.php',
@@ -1641,6 +1658,16 @@ return array(
     'OC\\Route\\Route' => $baseDir . '/lib/private/Route/Route.php',
     'OC\\Route\\Router' => $baseDir . '/lib/private/Route/Router.php',
     'OC\\Search' => $baseDir . '/lib/private/Search.php',
+    'OC\\Search\\FilterCollection' => $baseDir . '/lib/private/Search/FilterCollection.php',
+    'OC\\Search\\FilterFactory' => $baseDir . '/lib/private/Search/FilterFactory.php',
+    'OC\\Search\\Filter\\BooleanFilter' => $baseDir . '/lib/private/Search/Filter/BooleanFilter.php',
+    'OC\\Search\\Filter\\DateTimeFilter' => $baseDir . '/lib/private/Search/Filter/DateTimeFilter.php',
+    'OC\\Search\\Filter\\FloatFilter' => $baseDir . '/lib/private/Search/Filter/FloatFilter.php',
+    'OC\\Search\\Filter\\GroupFilter' => $baseDir . '/lib/private/Search/Filter/GroupFilter.php',
+    'OC\\Search\\Filter\\IntegerFilter' => $baseDir . '/lib/private/Search/Filter/IntegerFilter.php',
+    'OC\\Search\\Filter\\StringFilter' => $baseDir . '/lib/private/Search/Filter/StringFilter.php',
+    'OC\\Search\\Filter\\StringsFilter' => $baseDir . '/lib/private/Search/Filter/StringsFilter.php',
+    'OC\\Search\\Filter\\UserFilter' => $baseDir . '/lib/private/Search/Filter/UserFilter.php',
     'OC\\Search\\Provider\\File' => $baseDir . '/lib/private/Search/Provider/File.php',
     'OC\\Search\\Result\\Audio' => $baseDir . '/lib/private/Search/Result/Audio.php',
     'OC\\Search\\Result\\File' => $baseDir . '/lib/private/Search/Result/File.php',
@@ -1648,6 +1675,7 @@ return array(
     'OC\\Search\\Result\\Image' => $baseDir . '/lib/private/Search/Result/Image.php',
     'OC\\Search\\SearchComposer' => $baseDir . '/lib/private/Search/SearchComposer.php',
     'OC\\Search\\SearchQuery' => $baseDir . '/lib/private/Search/SearchQuery.php',
+    'OC\\Search\\UnsupportedFilter' => $baseDir . '/lib/private/Search/UnsupportedFilter.php',
     'OC\\Security\\Bruteforce\\Backend\\DatabaseBackend' => $baseDir . '/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php',
     'OC\\Security\\Bruteforce\\Backend\\IBackend' => $baseDir . '/lib/private/Security/Bruteforce/Backend/IBackend.php',
     'OC\\Security\\Bruteforce\\Backend\\MemoryCacheBackend' => $baseDir . '/lib/private/Security/Bruteforce/Backend/MemoryCacheBackend.php',
index 74e48cf69ae282da037509240c583d4634fbe094..7bf40f7a6b581a41c354ac1dbe3d181c3782f158 100644 (file)
@@ -9,5 +9,6 @@ return array(
     'OC\\Core\\' => array($baseDir . '/core'),
     'OC\\' => array($baseDir . '/lib/private'),
     'OCP\\' => array($baseDir . '/lib/public'),
+    'Bamarni\\Composer\\Bin\\' => array($vendorDir . '/bamarni/composer-bin-plugin/src'),
     '' => array($baseDir . '/lib/private/legacy'),
 );
index bd674e317a29a93973f331a0f72d14027c27f08b..d8159851d760c2de8e24c2c963e90ddef8750bc1 100644 (file)
@@ -17,6 +17,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
             'OC\\' => 3,
             'OCP\\' => 4,
         ),
+        'B' => 
+        array (
+            'Bamarni\\Composer\\Bin\\' => 21,
+        ),
     );
 
     public static $prefixDirsPsr4 = array (
@@ -32,6 +36,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         array (
             0 => __DIR__ . '/../../..' . '/lib/public',
         ),
+        'Bamarni\\Composer\\Bin\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src',
+        ),
     );
 
     public static $fallbackDirsPsr4 = array (
@@ -39,6 +47,19 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
     );
 
     public static $classMap = array (
+        'Bamarni\\Composer\\Bin\\ApplicationFactory\\FreshInstanceApplicationFactory' => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src/ApplicationFactory/FreshInstanceApplicationFactory.php',
+        'Bamarni\\Composer\\Bin\\ApplicationFactory\\NamespaceApplicationFactory' => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src/ApplicationFactory/NamespaceApplicationFactory.php',
+        'Bamarni\\Composer\\Bin\\BamarniBinPlugin' => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src/BamarniBinPlugin.php',
+        'Bamarni\\Composer\\Bin\\CommandProvider' => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src/CommandProvider.php',
+        'Bamarni\\Composer\\Bin\\Command\\BinCommand' => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src/Command/BinCommand.php',
+        'Bamarni\\Composer\\Bin\\Command\\CouldNotCreateNamespaceDir' => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src/Command/CouldNotCreateNamespaceDir.php',
+        'Bamarni\\Composer\\Bin\\Config\\Config' => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src/Config/Config.php',
+        'Bamarni\\Composer\\Bin\\Config\\ConfigFactory' => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src/Config/ConfigFactory.php',
+        'Bamarni\\Composer\\Bin\\Config\\InvalidBamarniComposerExtraConfig' => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src/Config/InvalidBamarniComposerExtraConfig.php',
+        'Bamarni\\Composer\\Bin\\Input\\BinInputFactory' => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src/Input/BinInputFactory.php',
+        'Bamarni\\Composer\\Bin\\Input\\InvalidBinInput' => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src/Input/InvalidBinInput.php',
+        'Bamarni\\Composer\\Bin\\Logger' => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src/Logger.php',
+        'Bamarni\\Composer\\Bin\\PublicIO' => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src/PublicIO.php',
         'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
         'OCP\\Accounts\\IAccount' => __DIR__ . '/../../..' . '/lib/public/Accounts/IAccount.php',
         'OCP\\Accounts\\IAccountManager' => __DIR__ . '/../../..' . '/lib/public/Accounts/IAccountManager.php',
@@ -622,6 +643,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OCP\\Route\\IRouter' => __DIR__ . '/../../..' . '/lib/public/Route/IRouter.php',
         'OCP\\SabrePluginEvent' => __DIR__ . '/../../..' . '/lib/public/SabrePluginEvent.php',
         'OCP\\SabrePluginException' => __DIR__ . '/../../..' . '/lib/public/SabrePluginException.php',
+        'OCP\\Search\\FilterDefinition' => __DIR__ . '/../../..' . '/lib/public/Search/FilterDefinition.php',
+        'OCP\\Search\\IFilter' => __DIR__ . '/../../..' . '/lib/public/Search/IFilter.php',
+        'OCP\\Search\\IFilterCollection' => __DIR__ . '/../../..' . '/lib/public/Search/IFilterCollection.php',
+        'OCP\\Search\\IFilteringProvider' => __DIR__ . '/../../..' . '/lib/public/Search/IFilteringProvider.php',
         'OCP\\Search\\IProvider' => __DIR__ . '/../../..' . '/lib/public/Search/IProvider.php',
         'OCP\\Search\\ISearchQuery' => __DIR__ . '/../../..' . '/lib/public/Search/ISearchQuery.php',
         'OCP\\Search\\PagedProvider' => __DIR__ . '/../../..' . '/lib/public/Search/PagedProvider.php',
@@ -1674,6 +1699,16 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OC\\Route\\Route' => __DIR__ . '/../../..' . '/lib/private/Route/Route.php',
         'OC\\Route\\Router' => __DIR__ . '/../../..' . '/lib/private/Route/Router.php',
         'OC\\Search' => __DIR__ . '/../../..' . '/lib/private/Search.php',
+        'OC\\Search\\FilterCollection' => __DIR__ . '/../../..' . '/lib/private/Search/FilterCollection.php',
+        'OC\\Search\\FilterFactory' => __DIR__ . '/../../..' . '/lib/private/Search/FilterFactory.php',
+        'OC\\Search\\Filter\\BooleanFilter' => __DIR__ . '/../../..' . '/lib/private/Search/Filter/BooleanFilter.php',
+        'OC\\Search\\Filter\\DateTimeFilter' => __DIR__ . '/../../..' . '/lib/private/Search/Filter/DateTimeFilter.php',
+        'OC\\Search\\Filter\\FloatFilter' => __DIR__ . '/../../..' . '/lib/private/Search/Filter/FloatFilter.php',
+        'OC\\Search\\Filter\\GroupFilter' => __DIR__ . '/../../..' . '/lib/private/Search/Filter/GroupFilter.php',
+        'OC\\Search\\Filter\\IntegerFilter' => __DIR__ . '/../../..' . '/lib/private/Search/Filter/IntegerFilter.php',
+        'OC\\Search\\Filter\\StringFilter' => __DIR__ . '/../../..' . '/lib/private/Search/Filter/StringFilter.php',
+        'OC\\Search\\Filter\\StringsFilter' => __DIR__ . '/../../..' . '/lib/private/Search/Filter/StringsFilter.php',
+        'OC\\Search\\Filter\\UserFilter' => __DIR__ . '/../../..' . '/lib/private/Search/Filter/UserFilter.php',
         'OC\\Search\\Provider\\File' => __DIR__ . '/../../..' . '/lib/private/Search/Provider/File.php',
         'OC\\Search\\Result\\Audio' => __DIR__ . '/../../..' . '/lib/private/Search/Result/Audio.php',
         'OC\\Search\\Result\\File' => __DIR__ . '/../../..' . '/lib/private/Search/Result/File.php',
@@ -1681,6 +1716,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OC\\Search\\Result\\Image' => __DIR__ . '/../../..' . '/lib/private/Search/Result/Image.php',
         'OC\\Search\\SearchComposer' => __DIR__ . '/../../..' . '/lib/private/Search/SearchComposer.php',
         'OC\\Search\\SearchQuery' => __DIR__ . '/../../..' . '/lib/private/Search/SearchQuery.php',
+        'OC\\Search\\UnsupportedFilter' => __DIR__ . '/../../..' . '/lib/private/Search/UnsupportedFilter.php',
         'OC\\Security\\Bruteforce\\Backend\\DatabaseBackend' => __DIR__ . '/../../..' . '/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php',
         'OC\\Security\\Bruteforce\\Backend\\IBackend' => __DIR__ . '/../../..' . '/lib/private/Security/Bruteforce/Backend/IBackend.php',
         'OC\\Security\\Bruteforce\\Backend\\MemoryCacheBackend' => __DIR__ . '/../../..' . '/lib/private/Security/Bruteforce/Backend/MemoryCacheBackend.php',
index f20a6c47c6d4ffe31ac1c071722ca6e2f330e699..13ea12dca2a279e7d83b3e6778c0f4b8fc9c1d70 100644 (file)
@@ -1,5 +1,68 @@
 {
-    "packages": [],
-    "dev": false,
-    "dev-package-names": []
+    "packages": [
+        {
+            "name": "bamarni/composer-bin-plugin",
+            "version": "1.8.2",
+            "version_normalized": "1.8.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/bamarni/composer-bin-plugin.git",
+                "reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/bamarni/composer-bin-plugin/zipball/92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880",
+                "reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880",
+                "shasum": ""
+            },
+            "require": {
+                "composer-plugin-api": "^2.0",
+                "php": "^7.2.5 || ^8.0"
+            },
+            "require-dev": {
+                "composer/composer": "^2.0",
+                "ext-json": "*",
+                "phpstan/extension-installer": "^1.1",
+                "phpstan/phpstan": "^1.8",
+                "phpstan/phpstan-phpunit": "^1.1",
+                "phpunit/phpunit": "^8.5 || ^9.5",
+                "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0",
+                "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0",
+                "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0"
+            },
+            "time": "2022-10-31T08:38:03+00:00",
+            "type": "composer-plugin",
+            "extra": {
+                "class": "Bamarni\\Composer\\Bin\\BamarniBinPlugin"
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Bamarni\\Composer\\Bin\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "No conflicts for your bin dependencies",
+            "keywords": [
+                "composer",
+                "conflict",
+                "dependency",
+                "executable",
+                "isolation",
+                "tool"
+            ],
+            "support": {
+                "issues": "https://github.com/bamarni/composer-bin-plugin/issues",
+                "source": "https://github.com/bamarni/composer-bin-plugin/tree/1.8.2"
+            },
+            "install-path": "../bamarni/composer-bin-plugin"
+        }
+    ],
+    "dev": true,
+    "dev-package-names": [
+        "bamarni/composer-bin-plugin"
+    ]
 }
index 1f382499aeb2189a33d1b71c2e731b7848d0672c..1b40f31ef6dd2968e1b17425469ef8e91fb6fb66 100644 (file)
@@ -1,23 +1,32 @@
 <?php return array(
     'root' => array(
-        'pretty_version' => '1.0.0+no-version-set',
-        'version' => '1.0.0.0',
+        'name' => '__root__',
+        'pretty_version' => 'dev-master',
+        'version' => 'dev-master',
+        'reference' => '1ad489970cd05a57df6649d0e601bfa46eba8564',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../../',
         'aliases' => array(),
-        'reference' => NULL,
-        'name' => '__root__',
-        'dev' => false,
+        'dev' => true,
     ),
     'versions' => array(
         '__root__' => array(
-            'pretty_version' => '1.0.0+no-version-set',
-            'version' => '1.0.0.0',
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'reference' => '1ad489970cd05a57df6649d0e601bfa46eba8564',
             'type' => 'library',
             'install_path' => __DIR__ . '/../../../',
             'aliases' => array(),
-            'reference' => NULL,
             'dev_requirement' => false,
         ),
+        'bamarni/composer-bin-plugin' => array(
+            'pretty_version' => '1.8.2',
+            'version' => '1.8.2.0',
+            'reference' => '92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880',
+            'type' => 'composer-plugin',
+            'install_path' => __DIR__ . '/../bamarni/composer-bin-plugin',
+            'aliases' => array(),
+            'dev_requirement' => true,
+        ),
     ),
 );
diff --git a/lib/private/Search/Filter/BooleanFilter.php b/lib/private/Search/Filter/BooleanFilter.php
new file mode 100644 (file)
index 0000000..a64bf17
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
+ *
+ * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.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/>.
+ *
+ */
+
+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 (file)
index 0000000..79abf9a
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
+ *
+ * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.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/>.
+ *
+ */
+
+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 (file)
index 0000000..3db19de
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
+ *
+ * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.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/>.
+ *
+ */
+
+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 (file)
index 0000000..6b42492
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
+ *
+ * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.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/>.
+ *
+ */
+
+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,
+       ) {
+               $this->group = $groupManager->get($value);
+               if ($this->group === null) {
+                       throw new InvalidArgumentException('Group '.$value.' not found');
+               }
+       }
+
+       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 (file)
index 0000000..b5b907b
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
+ *
+ * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.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/>.
+ *
+ */
+
+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 (file)
index 0000000..8f754d1
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
+ *
+ * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.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/>.
+ *
+ */
+
+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 (file)
index 0000000..7a8d887
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
+ *
+ * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.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/>.
+ *
+ */
+
+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 (file)
index 0000000..1624b60
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
+ *
+ * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.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/>.
+ *
+ */
+
+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,
+       ) {
+               $this->user = $userManager->get($value);
+               if ($this->user === null) {
+                       throw new InvalidArgumentException('User '.$value.' not found');
+               }
+       }
+
+       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 (file)
index 0000000..8c23cc7
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
+ *
+ * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.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/>.
+ *
+ */
+namespace OC\Search;
+
+use Generator;
+use OCP\Search\IFilterCollection;
+use OCP\Search\IFilter;
+
+/**
+ * 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 (file)
index 0000000..19afb2c
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
+ *
+ * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.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/>.
+ *
+ */
+namespace OC\Search;
+
+use OCP\Search\FilterDefinition;
+use OCP\Search\IFilter;
+use OCP\IGroupManager;
+use OCP\IUserManager;
+use RuntimeException;
+
+final class FilterFactory {
+       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('_', $person, 2);
+
+               return match (count($parts)) {
+                       1 => self::get(FilterDefinition::TYPE_NC_USER, $person),
+                       2 => self::get(... $parts),
+               };
+       }
+}
index 4ec73ec54e97645a4558204bfd51fa16a42e3d4a..41d969ca90bc5f4570a48d74b37b95b7f493bfa6 100644 (file)
@@ -28,14 +28,19 @@ declare(strict_types=1);
 namespace OC\Search;
 
 use InvalidArgumentException;
-use OCP\AppFramework\QueryException;
-use OCP\IServerContainer;
+use OCP\IURLGenerator;
+use OCP\Search\FilterDefinition;
+use OCP\Search\IFilteringProvider;
+use OC\AppFramework\Bootstrap\Coordinator;
 use OCP\IUser;
+use OCP\Search\IFilter;
 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_map;
 
 /**
@@ -58,31 +63,40 @@ use function array_map;
  * @see IProvider::search() for the arguments of the individual search requests
  */
 class SearchComposer {
-       /** @var IProvider[] */
-       private $providers = [];
-
-       /** @var Coordinator */
-       private $bootstrapCoordinator;
+       /**
+        * @var array<string, array{appId: string, provider: IProvider}>
+        */
+       private array $providers = [];
 
-       /** @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
+       ) {
+               $this->commonFilters = [
+                       'term' => new FilterDefinition('term', FilterDefinition::TYPE_STRING),
+                       'since' => new FilterDefinition('since', FilterDefinition::TYPE_DATETIME),
+                       'until' => new FilterDefinition('until', FilterDefinition::TYPE_DATETIME),
+                       'title-only' => new FilterDefinition('title-only', FilterDefinition::TYPE_BOOL, false),
+                       'person' => new FilterDefinition('person', FilterDefinition::TYPE_PERSON),
+                       'places' => new FilterDefinition('places', FilterDefinition::TYPE_STRINGS, false),
+                       'provider' => new FilterDefinition('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
@@ -93,9 +107,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,
@@ -103,6 +128,43 @@ class SearchComposer {
                                ]);
                        }
                }
+
+               $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];
+               }
        }
 
        /**
@@ -117,26 +179,134 @@ class SearchComposer {
        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'];
+                               $triggers = [$provider->getId()];
+                               if ($provider instanceof IFilteringProvider) {
+                                       $triggers += $provider->getAlternateIds();
+                                       $filters = $provider->getSupportedFilters();
+                               } else {
+                                       $filters = ['term'];
+                               }
+
                                return [
                                        'id' => $provider->getId(),
+                                       'appId' => $appId,
                                        'name' => $provider->getName(),
+                                       'icon' => $this->fetchIcon($appId, $provider->getId()),
                                        'order' => $provider->getOrder($route, $routeParameters),
+                                       'triggers' => $triggers,
+                                       'filters' => $this->getFiltersType($filters, $provider->getId()),
                                ];
-                       }, $this->providers)
+                       },
+                       $this->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;
        }
 
+       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'],
+               ];
+               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()
+                       : ['term'];
+
+               return in_array($filterDefinition->name(), $supportedFilters, true);
+       }
+
        /**
         * Query an individual search provider for results
         *
@@ -147,15 +317,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);
        }
 }
index c89446d59703b0eaee4490240b863fc7a6c8305d..aae2044bcd65ae7801133b7797faac5f02de42f3 100644 (file)
@@ -27,89 +27,57 @@ declare(strict_types=1);
  */
 namespace OC\Search;
 
+use OCP\Search\IFilterCollection;
+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 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 (file)
index 0000000..84b6163
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
+ *
+ * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.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/>.
+ *
+ */
+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.'.');
+       }
+}
diff --git a/lib/public/Search/FilterDefinition.php b/lib/public/Search/FilterDefinition.php
new file mode 100644 (file)
index 0000000..7e1538a
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
+ *
+ * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.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/>.
+ *
+ */
+namespace OCP\Search;
+
+use InvalidArgumentException;
+
+/**
+ * Filter definition
+ *
+ * Describe filter attributes
+ *
+ * @since 28.0.0
+ */
+class FilterDefinition {
+       public const TYPE_BOOL = 'bool';
+       public const TYPE_INT = 'int';
+       public const TYPE_FLOAT = 'float';
+       public const TYPE_STRING = 'string';
+       public const TYPE_STRINGS = 'strings';
+       public const TYPE_DATETIME = 'datetime';
+       public const TYPE_PERSON = 'person';
+       public const TYPE_NC_USER = 'nc-user';
+       public const TYPE_NC_GROUP = 'nc-group';
+
+       /**
+        * Build filter definition
+        *
+        * @param self::TYPE_* $type
+        * @param bool $exclusive If true, all providers not supporting this filter will be ignored when this filter is provided
+        * @throw InvalidArgumentException in case of invalid name. Allowed characters are -, 0-9, a-z.
+        * @since 28.0.0
+        */
+       public function __construct(
+               private string $name,
+               private string $type = self::TYPE_STRING,
+               private bool $exclusive = true,
+       ) {
+               if (!preg_match('/[-0-9a-z]+/Au', $name)) {
+                       throw new InvalidArgumentException('Invalid filter name. Allowed characters are [-0-9a-z]');
+               }
+       }
+
+       /**
+        * Filter name
+        *
+        * Name is used in query string and for advanced syntax `name: <value>`
+        *
+        * @since 28.0.0
+        */
+       public function name(): string {
+               return $this->name;
+       }
+
+       /**
+        * Filter type
+        *
+        * Expected type of value for the filter
+        *
+        * @return self::TYPE_*
+        * @since 28.0.0
+        */
+       public function type(): string {
+               return $this->type;
+       }
+
+       /**
+        * Is filter exclusive?
+        *
+        * If exclusive, only provider with support for this filter will receive the query.
+        * Example: if an exclusive filter `mimetype` is declared, a search with this term will not
+        * be send to providers like `settings` that doesn't support it.
+        *
+        * @since 28.0.0
+        */
+       public function exclusive(): bool {
+               return $this->exclusive;
+       }
+}
diff --git a/lib/public/Search/IFilter.php b/lib/public/Search/IFilter.php
new file mode 100644 (file)
index 0000000..c4e444e
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
+ *
+ * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.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/>.
+ *
+ */
+namespace OCP\Search;
+
+/**
+ * Interface for search filters
+ *
+ * @since 28.0.0
+ */
+interface IFilter {
+       /**
+        * Get filter value
+        *
+        * @since 28.0.0
+        */
+       public function get(): mixed;
+}
diff --git a/lib/public/Search/IFilterCollection.php b/lib/public/Search/IFilterCollection.php
new file mode 100644 (file)
index 0000000..6ca53a1
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
+ *
+ * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.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/>.
+ *
+ */
+namespace OCP\Search;
+
+use IteratorAggregate;
+
+/**
+ * Interface for search filters
+ *
+ * @since 28.0.0
+ * @extends IteratorAggregate<string, \OCP\Search\IFilter>
+ */
+interface IFilterCollection extends IteratorAggregate {
+       /**
+        * Check if a filter exits
+        *
+        * @since 28.0.0
+        */
+       public function has(string $name): bool;
+
+       /**
+        * Get a filter by name
+        *
+        * @since 28.0.0
+        */
+       public function get(string $name): ?IFilter;
+
+       /**
+        * Return Iterator of filters
+        *
+        * @since 28.0.0
+        */
+       public function getIterator(): \Traversable;
+}
diff --git a/lib/public/Search/IFilteringProvider.php b/lib/public/Search/IFilteringProvider.php
new file mode 100644 (file)
index 0000000..dbe1044
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
+ *
+ * @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.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/>.
+ *
+ */
+namespace OCP\Search;
+
+/**
+ * Interface for advanced 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 IFilteringProvider extends IProvider {
+       /**
+        * Return the names of filters supported by the application
+        *
+        * If a filter sent by client is not in this list,
+        * the current provider will be ignored.
+        * Example:
+        *   array('term', 'since', 'custom-filter');
+        *
+        * @since 28.0.0
+        * @return string[] Name of supported filters (default or defined by application)
+        */
+       public function getSupportedFilters(): array;
+
+       /**
+        * Get alternate IDs 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 string[] IDs
+        */
+       public function getAlternateIds(): array;
+
+       /**
+        * Allows application to declare custom filters
+        *
+        * @since 28.0.0
+        * @return list<FilterDefinition>
+        */
+       public function getCustomFilters(): array;
+}
index a545d1dbccbacddbb47be5cb18b995bfd3749672..56f1f1f0faaf4464f11dc04ff920c6d9ef413658 100644 (file)
@@ -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(): IFilterCollection;
+
        /**
         * Get the sort order of results as defined as SORT_* constants on this interface
         *