Add unified search API

Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
This commit is contained in:
Christoph Wurst 2020-05-11 10:35:54 +02:00
parent 50b1568d48
commit 4488e846a5
No known key found for this signature in database
GPG Key ID: CC42AC2A7F0E56D8
15 changed files with 820 additions and 0 deletions

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 OCA\Files\Search;
use OCP\IL10N;
use OCP\IUser;
use OCP\Search\IProvider;
use OCP\Search\ISearchQuery;
use OCP\Search\SearchResult;
class FilesSearchProvider implements IProvider {
/** @var IL10N */
private $l10n;
public function __construct(IL10N $l10n) {
$this->l10n = $l10n;
}
public function getId(): string {
return 'files';
}
public function search(IUser $user, ISearchQuery $query): SearchResult {
return SearchResult::complete(
$this->l10n->t('Files'),
[
new FilesSearchResultEntry(
"path/to/icon.png",
"cute cats.jpg",
"/Cats",
"/f/21156"
),
new FilesSearchResultEntry(
"path/to/icon.png",
"cat 1.jpg",
"/Cats",
"/f/21192"
),
new FilesSearchResultEntry(
"path/to/icon.png",
"cat 2.jpg",
"/Cats",
"/f/25942"
),
]
);
}
}

View File

@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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\Core\Controller;
use OC\Search\SearchComposer;
use OC\Search\SearchQuery;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
use OCP\IUserSession;
use OCP\Search\ISearchQuery;
class UnifiedSearchController extends Controller {
/** @var SearchComposer */
private $composer;
/** @var IUserSession */
private $userSession;
public function __construct(IRequest $request,
IUserSession $userSession,
SearchComposer $composer) {
parent::__construct('core', $request);
$this->composer = $composer;
$this->userSession = $userSession;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function getProviders(): JSONResponse {
return new JSONResponse(
$this->composer->getProviders()
);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @param string $providerId
* @param string $term
* @param int|null $sortOrder
* @param int|null $limit
* @param int|string|null $cursor
*
* @return JSONResponse
*/
public function search(string $providerId,
string $term = '',
?int $sortOrder = null,
?int $limit = null,
$cursor = null): JSONResponse {
if (empty($term)) {
return new JSONResponse(null, Http::STATUS_BAD_REQUEST);
}
return new JSONResponse(
$this->composer->search(
$this->userSession->getUser(),
$providerId,
new SearchQuery(
$term,
$sortOrder ?? ISearchQuery::SORT_DATE_DESC,
$limit ?? SearchQuery::LIMIT_DEFAULT,
$cursor
)
)
);
}
}

View File

@ -77,6 +77,8 @@ $application->registerRoutes($this, [
['name' => 'RecommendedApps#index', 'url' => '/core/apps/recommended', 'verb' => 'GET'], ['name' => 'RecommendedApps#index', 'url' => '/core/apps/recommended', 'verb' => 'GET'],
['name' => 'Svg#getSvgFromCore', 'url' => '/svg/core/{folder}/{fileName}', 'verb' => 'GET'], ['name' => 'Svg#getSvgFromCore', 'url' => '/svg/core/{folder}/{fileName}', 'verb' => 'GET'],
['name' => 'Svg#getSvgFromApp', 'url' => '/svg/{app}/{fileName}', 'verb' => 'GET'], ['name' => 'Svg#getSvgFromApp', 'url' => '/svg/{app}/{fileName}', 'verb' => 'GET'],
['name' => 'UnifiedSearch#getProviders', 'url' => '/search/providers', 'verb' => 'GET'],
['name' => 'UnifiedSearch#search', 'url' => '/search/providers/{providerId}/search', 'verb' => 'GET'],
['name' => 'Css#getCss', 'url' => '/css/{appName}/{fileName}', 'verb' => 'GET'], ['name' => 'Css#getCss', 'url' => '/css/{appName}/{fileName}', 'verb' => 'GET'],
['name' => 'Js#getJs', 'url' => '/js/{appName}/{fileName}', 'verb' => 'GET'], ['name' => 'Js#getJs', 'url' => '/js/{appName}/{fileName}', 'verb' => 'GET'],
['name' => 'contactsMenu#index', 'url' => '/contactsmenu/contacts', 'verb' => 'POST'], ['name' => 'contactsMenu#index', 'url' => '/contactsmenu/contacts', 'verb' => 'POST'],

View File

@ -432,9 +432,13 @@ return array(
'OCP\\Route\\IRouter' => $baseDir . '/lib/public/Route/IRouter.php', 'OCP\\Route\\IRouter' => $baseDir . '/lib/public/Route/IRouter.php',
'OCP\\SabrePluginEvent' => $baseDir . '/lib/public/SabrePluginEvent.php', 'OCP\\SabrePluginEvent' => $baseDir . '/lib/public/SabrePluginEvent.php',
'OCP\\SabrePluginException' => $baseDir . '/lib/public/SabrePluginException.php', 'OCP\\SabrePluginException' => $baseDir . '/lib/public/SabrePluginException.php',
'OCP\\Search\\ASearchResultEntry' => $baseDir . '/lib/public/Search/ASearchResultEntry.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', 'OCP\\Search\\PagedProvider' => $baseDir . '/lib/public/Search/PagedProvider.php',
'OCP\\Search\\Provider' => $baseDir . '/lib/public/Search/Provider.php', 'OCP\\Search\\Provider' => $baseDir . '/lib/public/Search/Provider.php',
'OCP\\Search\\Result' => $baseDir . '/lib/public/Search/Result.php', 'OCP\\Search\\Result' => $baseDir . '/lib/public/Search/Result.php',
'OCP\\Search\\SearchResult' => $baseDir . '/lib/public/Search/SearchResult.php',
'OCP\\Security\\CSP\\AddContentSecurityPolicyEvent' => $baseDir . '/lib/public/Security/CSP/AddContentSecurityPolicyEvent.php', 'OCP\\Security\\CSP\\AddContentSecurityPolicyEvent' => $baseDir . '/lib/public/Security/CSP/AddContentSecurityPolicyEvent.php',
'OCP\\Security\\Events\\GenerateSecurePasswordEvent' => $baseDir . '/lib/public/Security/Events/GenerateSecurePasswordEvent.php', 'OCP\\Security\\Events\\GenerateSecurePasswordEvent' => $baseDir . '/lib/public/Security/Events/GenerateSecurePasswordEvent.php',
'OCP\\Security\\Events\\ValidatePasswordPolicyEvent' => $baseDir . '/lib/public/Security/Events/ValidatePasswordPolicyEvent.php', 'OCP\\Security\\Events\\ValidatePasswordPolicyEvent' => $baseDir . '/lib/public/Security/Events/ValidatePasswordPolicyEvent.php',
@ -853,6 +857,7 @@ return array(
'OC\\Core\\Controller\\SetupController' => $baseDir . '/core/Controller/SetupController.php', 'OC\\Core\\Controller\\SetupController' => $baseDir . '/core/Controller/SetupController.php',
'OC\\Core\\Controller\\SvgController' => $baseDir . '/core/Controller/SvgController.php', 'OC\\Core\\Controller\\SvgController' => $baseDir . '/core/Controller/SvgController.php',
'OC\\Core\\Controller\\TwoFactorChallengeController' => $baseDir . '/core/Controller/TwoFactorChallengeController.php', 'OC\\Core\\Controller\\TwoFactorChallengeController' => $baseDir . '/core/Controller/TwoFactorChallengeController.php',
'OC\\Core\\Controller\\UnifiedSearchController' => $baseDir . '/core/Controller/UnifiedSearchController.php',
'OC\\Core\\Controller\\UserController' => $baseDir . '/core/Controller/UserController.php', 'OC\\Core\\Controller\\UserController' => $baseDir . '/core/Controller/UserController.php',
'OC\\Core\\Controller\\WalledGardenController' => $baseDir . '/core/Controller/WalledGardenController.php', 'OC\\Core\\Controller\\WalledGardenController' => $baseDir . '/core/Controller/WalledGardenController.php',
'OC\\Core\\Controller\\WebAuthnController' => $baseDir . '/core/Controller/WebAuthnController.php', 'OC\\Core\\Controller\\WebAuthnController' => $baseDir . '/core/Controller/WebAuthnController.php',
@ -1233,6 +1238,8 @@ return array(
'OC\\Search\\Result\\File' => $baseDir . '/lib/private/Search/Result/File.php', 'OC\\Search\\Result\\File' => $baseDir . '/lib/private/Search/Result/File.php',
'OC\\Search\\Result\\Folder' => $baseDir . '/lib/private/Search/Result/Folder.php', 'OC\\Search\\Result\\Folder' => $baseDir . '/lib/private/Search/Result/Folder.php',
'OC\\Search\\Result\\Image' => $baseDir . '/lib/private/Search/Result/Image.php', '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\\Security\\Bruteforce\\Capabilities' => $baseDir . '/lib/private/Security/Bruteforce/Capabilities.php', 'OC\\Security\\Bruteforce\\Capabilities' => $baseDir . '/lib/private/Security/Bruteforce/Capabilities.php',
'OC\\Security\\Bruteforce\\Throttler' => $baseDir . '/lib/private/Security/Bruteforce/Throttler.php', 'OC\\Security\\Bruteforce\\Throttler' => $baseDir . '/lib/private/Security/Bruteforce/Throttler.php',
'OC\\Security\\CSP\\ContentSecurityPolicy' => $baseDir . '/lib/private/Security/CSP/ContentSecurityPolicy.php', 'OC\\Security\\CSP\\ContentSecurityPolicy' => $baseDir . '/lib/private/Security/CSP/ContentSecurityPolicy.php',

View File

@ -461,9 +461,13 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OCP\\Route\\IRouter' => __DIR__ . '/../../..' . '/lib/public/Route/IRouter.php', 'OCP\\Route\\IRouter' => __DIR__ . '/../../..' . '/lib/public/Route/IRouter.php',
'OCP\\SabrePluginEvent' => __DIR__ . '/../../..' . '/lib/public/SabrePluginEvent.php', 'OCP\\SabrePluginEvent' => __DIR__ . '/../../..' . '/lib/public/SabrePluginEvent.php',
'OCP\\SabrePluginException' => __DIR__ . '/../../..' . '/lib/public/SabrePluginException.php', 'OCP\\SabrePluginException' => __DIR__ . '/../../..' . '/lib/public/SabrePluginException.php',
'OCP\\Search\\ASearchResultEntry' => __DIR__ . '/../../..' . '/lib/public/Search/ASearchResultEntry.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', 'OCP\\Search\\PagedProvider' => __DIR__ . '/../../..' . '/lib/public/Search/PagedProvider.php',
'OCP\\Search\\Provider' => __DIR__ . '/../../..' . '/lib/public/Search/Provider.php', 'OCP\\Search\\Provider' => __DIR__ . '/../../..' . '/lib/public/Search/Provider.php',
'OCP\\Search\\Result' => __DIR__ . '/../../..' . '/lib/public/Search/Result.php', 'OCP\\Search\\Result' => __DIR__ . '/../../..' . '/lib/public/Search/Result.php',
'OCP\\Search\\SearchResult' => __DIR__ . '/../../..' . '/lib/public/Search/SearchResult.php',
'OCP\\Security\\CSP\\AddContentSecurityPolicyEvent' => __DIR__ . '/../../..' . '/lib/public/Security/CSP/AddContentSecurityPolicyEvent.php', 'OCP\\Security\\CSP\\AddContentSecurityPolicyEvent' => __DIR__ . '/../../..' . '/lib/public/Security/CSP/AddContentSecurityPolicyEvent.php',
'OCP\\Security\\Events\\GenerateSecurePasswordEvent' => __DIR__ . '/../../..' . '/lib/public/Security/Events/GenerateSecurePasswordEvent.php', 'OCP\\Security\\Events\\GenerateSecurePasswordEvent' => __DIR__ . '/../../..' . '/lib/public/Security/Events/GenerateSecurePasswordEvent.php',
'OCP\\Security\\Events\\ValidatePasswordPolicyEvent' => __DIR__ . '/../../..' . '/lib/public/Security/Events/ValidatePasswordPolicyEvent.php', 'OCP\\Security\\Events\\ValidatePasswordPolicyEvent' => __DIR__ . '/../../..' . '/lib/public/Security/Events/ValidatePasswordPolicyEvent.php',
@ -882,6 +886,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Core\\Controller\\SetupController' => __DIR__ . '/../../..' . '/core/Controller/SetupController.php', 'OC\\Core\\Controller\\SetupController' => __DIR__ . '/../../..' . '/core/Controller/SetupController.php',
'OC\\Core\\Controller\\SvgController' => __DIR__ . '/../../..' . '/core/Controller/SvgController.php', 'OC\\Core\\Controller\\SvgController' => __DIR__ . '/../../..' . '/core/Controller/SvgController.php',
'OC\\Core\\Controller\\TwoFactorChallengeController' => __DIR__ . '/../../..' . '/core/Controller/TwoFactorChallengeController.php', 'OC\\Core\\Controller\\TwoFactorChallengeController' => __DIR__ . '/../../..' . '/core/Controller/TwoFactorChallengeController.php',
'OC\\Core\\Controller\\UnifiedSearchController' => __DIR__ . '/../../..' . '/core/Controller/UnifiedSearchController.php',
'OC\\Core\\Controller\\UserController' => __DIR__ . '/../../..' . '/core/Controller/UserController.php', 'OC\\Core\\Controller\\UserController' => __DIR__ . '/../../..' . '/core/Controller/UserController.php',
'OC\\Core\\Controller\\WalledGardenController' => __DIR__ . '/../../..' . '/core/Controller/WalledGardenController.php', 'OC\\Core\\Controller\\WalledGardenController' => __DIR__ . '/../../..' . '/core/Controller/WalledGardenController.php',
'OC\\Core\\Controller\\WebAuthnController' => __DIR__ . '/../../..' . '/core/Controller/WebAuthnController.php', 'OC\\Core\\Controller\\WebAuthnController' => __DIR__ . '/../../..' . '/core/Controller/WebAuthnController.php',
@ -1262,6 +1267,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Search\\Result\\File' => __DIR__ . '/../../..' . '/lib/private/Search/Result/File.php', 'OC\\Search\\Result\\File' => __DIR__ . '/../../..' . '/lib/private/Search/Result/File.php',
'OC\\Search\\Result\\Folder' => __DIR__ . '/../../..' . '/lib/private/Search/Result/Folder.php', 'OC\\Search\\Result\\Folder' => __DIR__ . '/../../..' . '/lib/private/Search/Result/Folder.php',
'OC\\Search\\Result\\Image' => __DIR__ . '/../../..' . '/lib/private/Search/Result/Image.php', '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\\Security\\Bruteforce\\Capabilities' => __DIR__ . '/../../..' . '/lib/private/Security/Bruteforce/Capabilities.php', 'OC\\Security\\Bruteforce\\Capabilities' => __DIR__ . '/../../..' . '/lib/private/Security/Bruteforce/Capabilities.php',
'OC\\Security\\Bruteforce\\Throttler' => __DIR__ . '/../../..' . '/lib/private/Security/Bruteforce/Throttler.php', 'OC\\Security\\Bruteforce\\Throttler' => __DIR__ . '/../../..' . '/lib/private/Security/Bruteforce/Throttler.php',
'OC\\Security\\CSP\\ContentSecurityPolicy' => __DIR__ . '/../../..' . '/lib/private/Security/CSP/ContentSecurityPolicy.php', 'OC\\Security\\CSP\\ContentSecurityPolicy' => __DIR__ . '/../../..' . '/lib/private/Security/CSP/ContentSecurityPolicy.php',

View File

@ -25,6 +25,7 @@ declare(strict_types=1);
namespace OC\AppFramework\Bootstrap; namespace OC\AppFramework\Bootstrap;
use OC\Search\SearchComposer;
use OC\Support\CrashReport\Registry; use OC\Support\CrashReport\Registry;
use OC_App; use OC_App;
use OCP\AppFramework\App; use OCP\AppFramework\App;
@ -49,16 +50,21 @@ class Coordinator {
/** @var IEventDispatcher */ /** @var IEventDispatcher */
private $eventDispatcher; private $eventDispatcher;
/** @var SearchComposer */
private $searchComposer;
/** @var ILogger */ /** @var ILogger */
private $logger; private $logger;
public function __construct(IServerContainer $container, public function __construct(IServerContainer $container,
Registry $registry, Registry $registry,
IEventDispatcher $eventListener, IEventDispatcher $eventListener,
SearchComposer $searchComposer,
ILogger $logger) { ILogger $logger) {
$this->serverContainer = $container; $this->serverContainer = $container;
$this->registry = $registry; $this->registry = $registry;
$this->eventDispatcher = $eventListener; $this->eventDispatcher = $eventListener;
$this->searchComposer = $searchComposer;
$this->logger = $logger; $this->logger = $logger;
} }
@ -112,6 +118,7 @@ class Coordinator {
$context->delegateEventListenerRegistrations($this->eventDispatcher); $context->delegateEventListenerRegistrations($this->eventDispatcher);
$context->delegateContainerRegistrations($apps); $context->delegateContainerRegistrations($apps);
$context->delegateMiddlewareRegistrations($apps); $context->delegateMiddlewareRegistrations($apps);
$context->delegateSearchProviderRegistration($apps, $this->searchComposer);
} }
public function bootApp(string $appId): void { public function bootApp(string $appId): void {

View File

@ -26,6 +26,7 @@ declare(strict_types=1);
namespace OC\AppFramework\Bootstrap; namespace OC\AppFramework\Bootstrap;
use Closure; use Closure;
use OC\Search\SearchComposer;
use OC\Support\CrashReport\Registry; use OC\Support\CrashReport\Registry;
use OCP\AppFramework\App; use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\AppFramework\Bootstrap\IRegistrationContext;
@ -56,6 +57,9 @@ class RegistrationContext {
/** @var array[] */ /** @var array[] */
private $middlewares = []; private $middlewares = [];
/** @var array[] */
private $searchProviders = [];
/** @var ILogger */ /** @var ILogger */
private $logger; private $logger;
@ -130,6 +134,13 @@ class RegistrationContext {
$class $class
); );
} }
public function registerSearchProvider(string $class): void {
$this->context->registerSearchProvider(
$this->appId,
$class
);
}
}; };
} }
@ -188,6 +199,13 @@ class RegistrationContext {
]; ];
} }
public function registerSearchProvider(string $appId, string $class) {
$this->searchProviders[] = [
'appId' => $appId,
'class' => $class,
];
}
/** /**
* @param App[] $apps * @param App[] $apps
*/ */
@ -327,4 +345,21 @@ class RegistrationContext {
} }
} }
} }
/**
* @param App[] $apps
*/
public function delegateSearchProviderRegistration(array $apps, SearchComposer $searchComposer): void {
foreach ($this->searchProviders as $registration) {
try {
$searchComposer->registerProvider($registration['class']);
} catch (Throwable $e) {
$appId = $registration['appId'];
$this->logger->logException($e, [
'message' => "Error during search provider registration of $appId: " . $e->getMessage(),
'level' => ILogger::ERROR,
]);
}
}
}
} }

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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\AppFramework\QueryException;
use OCP\ILogger;
use OCP\IServerContainer;
use OCP\IUser;
use OCP\Search\IProvider;
use OCP\Search\ISearchQuery;
use OCP\Search\SearchResult;
use function array_map;
/**
* Queries individual \OCP\Search\IProvider implementations and composes a
* unified search result for the user's search term
*/
class SearchComposer {
/** @var string[] */
private $lazyProviders = [];
/** @var IProvider[] */
private $providers = [];
/** @var IServerContainer */
private $container;
/** @var ILogger */
private $logger;
public function __construct(IServerContainer $container,
ILogger $logger) {
$this->container = $container;
$this->logger = $logger;
}
public function registerProvider(string $class): void {
$this->lazyProviders[] = $class;
}
/**
* Load all providers dynamically that were registered through `registerProvider`
*
* If a provider can't be loaded we log it but the operation continues nevertheless
*/
private function loadLazyProviders(): void {
$classes = $this->lazyProviders;
foreach ($classes as $class) {
try {
/** @var IProvider $provider */
$provider = $this->container->query($class);
$this->providers[$provider->getId()] = $provider;
} catch (QueryException $e) {
// Log an continue. We can be fault tolerant here.
$this->logger->logException($e, [
'message' => 'Could not load search provider dynamically: ' . $e->getMessage(),
'level' => ILogger::ERROR,
]);
}
}
$this->lazyProviders = [];
}
public function getProviders(): array {
$this->loadLazyProviders();
/**
* Return an array with the IDs, but strip the associative keys
*/
return array_values(
array_map(function (IProvider $provider) {
return $provider->getId();
}, $this->providers));
}
public function search(IUser $user,
string $providerId,
ISearchQuery $query): SearchResult {
$this->loadLazyProviders();
return $this->providers[$providerId]->search($user, $query);
}
}

View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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\ISearchQuery;
class SearchQuery implements ISearchQuery {
public const LIMIT_DEFAULT = 20;
/** @var string */
private $term;
/** @var int */
private $sortOrder;
/** @var int */
private $limit;
/** @var int|string|null */
private $cursor;
/**
* @param string $term
* @param int $sortOrder
* @param int $limit
* @param int|string|null $cursor
*/
public function __construct(string $term,
int $sortOrder = ISearchQuery::SORT_DATE_DESC,
int $limit = self::LIMIT_DEFAULT,
$cursor = null) {
$this->term = $term;
$this->sortOrder = $sortOrder;
$this->limit = $limit;
$this->cursor = $cursor;
}
/**
* @inheritDoc
*/
public function getTerm(): string {
return $this->term;
}
/**
* @inheritDoc
*/
public function getSortOrder(): int {
return $this->sortOrder;
}
/**
* @inheritDoc
*/
public function getLimit(): int {
return $this->limit;
}
/**
* @inheritDoc
*/
public function getCursor() {
return $this->cursor;
}
}

View File

@ -115,4 +115,19 @@ interface IRegistrationContext {
* @since 20.0.0 * @since 20.0.0
*/ */
public function registerMiddleware(string $class): void; public function registerMiddleware(string $class): void;
/**
* Register a search provider for the unified search
*
* It is allowed to register more than one provider per app as the search
* results can go into distinct sections, e.g. "Files" and "Files shared
* with you" in the Files app.
*
* @param string $class
*
* @return void
*
* @since 20.0.0
*/
public function registerSearchProvider(string $class): void;
} }

View File

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 JsonSerializable;
/**
* Represents an entry in a list of results an app returns for a unified search
* query.
*
* The app providing the results has to extend this class for customization. In
* most cases apps do not have to add any additional code.
*
* @example ``class MailResultEntry extends ASearchResultEntry {}`
*
* This approach was chosen over a final class as it allows Nextcloud to later
* add new optional properties of an entry without having to break the usage of
* this class in apps.
*
* @since 20.0.0
*/
abstract class ASearchResultEntry implements JsonSerializable {
/**
* @var string
* @since 20.0.0
*/
protected $thumbnailUrl;
/**
* @var string
* @since 20.0.0
*/
protected $title;
/**
* @var string
* @since 20.0.0
*/
protected $subline;
/**
* @var string
* @since 20.0.0
*/
protected $resourceUrl;
/**
* @param string $thumbnailUrl a relative or absolute URL to the thumbnail or icon of the entry
* @param string $title a main title of the entry
* @param string $subline the secondary line of the entry
* @param string $resourceUrl the URL where the user can find the detail, like a deep link inside the app
*
* @since 20.0.0
*/
public function __construct(string $thumbnailUrl,
string $title,
string $subline,
string $resourceUrl) {
$this->thumbnailUrl = $thumbnailUrl;
$this->title = $title;
$this->subline = $subline;
$this->resourceUrl = $resourceUrl;
}
/**
* @return array
*
* @since 20.0.0
*/
public function jsonSerialize(): array {
return [
'thumbnailUrl' => $this->thumbnailUrl,
'title' => $this->title,
'subline' => $this->subline,
'resourceUrl' => $this->resourceUrl,
];
}
}

View File

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 OCP\IUser;
/**
* Interface for an app 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 20.0.0
*/
interface IProvider {
/**
* Get the unique ID of this search provider
*
* Ideally this should be the app name or an identifier identified with the
* app name, especially if the app registers more than one provider.
*
* Example: 'mail', 'mail_recipients', 'files_sharing'
*
* @return string
*
* @since 20.0.0
*/
public function getId(): string;
/**
* Find matching search entries in an app
*
* Search results can either be a complete list of all the matches the app can
* find, or ideally a paginated result set where more data can be fetched on
* demand. To be able to tell where the next offset starts the search uses
* "cursors" which are a property of the last result entry. E.g. search results
* that show most recent entries first can look for entries older than the last
* one of the first result set. This approach was chosen over a numeric limit/
* offset approach as the offset moves as new data comes in. The cursor is
* resistant to these changes and will still show results without overlaps or
* gaps.
*
* See https://dev.to/jackmarchant/offset-and-cursor-pagination-explained-b89
* for the concept of cursors.
*
* Implementations that return result pages have to adhere to the limit
* property of a search query.
*
* @param IUser $user
* @param ISearchQuery $query
*
* @return SearchResult
*
* @since 20.0.0
*/
public function search(IUser $user, ISearchQuery $query): SearchResult;
}

View File

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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;
/**
* The query objected passed into \OCP\Search\IProvider::search
*
* This mainly wraps the search term, but will ensure that Nextcloud can add new
* optional properties to a search request without having break the interface of
* \OCP\Search\IProvider::search.
*
* @see \OCP\Search\IProvider::search
*
* @since 20.0.0
*/
interface ISearchQuery {
/**
* @since 20.0.0
*/
public const SORT_DATE_DESC = 1;
/**
* Get the user-entered search term to find matches for
*
* @return string the search term
* @since 20.0.0
*/
public function getTerm(): string;
/**
* Get the sort order of results as defined as SORT_* constants on this interface
*
* @return int
* @since 20.0.0
*/
public function getSortOrder(): int;
/**
* Get the number of items to return for a paginated result
*
* @return int
* @see \OCP\Search\IProvider for details
* @since 20.0.0
*/
public function getLimit(): int;
/**
* Get the app-specific cursor of the tail of the previous result entries
*
* @return int|string|null
* @see \OCP\Search\IProvider for details
* @since 20.0.0
*/
public function getCursor();
}

View File

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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 JsonSerializable;
/**
* @since 20.0.0
*/
final class SearchResult implements JsonSerializable {
/** @var string */
private $name;
/** @var bool */
private $isPaginated;
/** @var ASearchResultEntry[] */
private $entries;
/** @var int|string|null */
private $cursor;
/**
* @param string $name the translated name of the result section or group, e.g. "Mail"
* @param bool $isPaginated
* @param ASearchResultEntry[] $entries
* @param null $cursor
*
* @since 20.0.0
*/
private function __construct(string $name,
bool $isPaginated,
array $entries,
$cursor = null) {
$this->name = $name;
$this->isPaginated = $isPaginated;
$this->entries = $entries;
$this->cursor = $cursor;
}
/**
* @param ASearchResultEntry[] $entries
*
* @return static
*
* @since 20.0.0
*/
public static function complete(string $name, array $entries): self {
return new self(
$name,
false,
$entries
);
}
/**
* @param ASearchResultEntry[] $entries
* @param int|string $cursor
*
* @return static
*
* @since 20.0.0
*/
public static function paginated(string $name,
array $entries,
$cursor): self {
return new self(
$name,
true,
$entries,
$cursor
);
}
/**
* @return array
*
* @since 20.0.0
*/
public function jsonSerialize(): array {
return [
'name' => $this->name,
'isPaginated' => $this->isPaginated,
'entries' => $this->entries,
'cursor' => $this->cursor,
];
}
}

View File

@ -26,6 +26,7 @@ declare(strict_types=1);
namespace lib\AppFramework\Bootstrap; namespace lib\AppFramework\Bootstrap;
use OC\AppFramework\Bootstrap\Coordinator; use OC\AppFramework\Bootstrap\Coordinator;
use OC\Search\SearchComposer;
use OC\Support\CrashReport\Registry; use OC\Support\CrashReport\Registry;
use OCP\App\IAppManager; use OCP\App\IAppManager;
use OCP\AppFramework\App; use OCP\AppFramework\App;
@ -53,6 +54,9 @@ class CoordinatorTest extends TestCase {
/** @var IEventDispatcher|MockObject */ /** @var IEventDispatcher|MockObject */
private $eventDispatcher; private $eventDispatcher;
/** @var SearchComposer|MockObject */
private $searchComposer;
/** @var ILogger|MockObject */ /** @var ILogger|MockObject */
private $logger; private $logger;
@ -66,12 +70,14 @@ class CoordinatorTest extends TestCase {
$this->serverContainer = $this->createMock(IServerContainer::class); $this->serverContainer = $this->createMock(IServerContainer::class);
$this->crashReporterRegistry = $this->createMock(Registry::class); $this->crashReporterRegistry = $this->createMock(Registry::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class); $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->searchComposer = $this->createMock(SearchComposer::class);
$this->logger = $this->createMock(ILogger::class); $this->logger = $this->createMock(ILogger::class);
$this->coordinator = new Coordinator( $this->coordinator = new Coordinator(
$this->serverContainer, $this->serverContainer,
$this->crashReporterRegistry, $this->crashReporterRegistry,
$this->eventDispatcher, $this->eventDispatcher,
$this->searchComposer,
$this->logger $this->logger
); );
} }