summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/comments/composer/composer/autoload_classmap.php2
-rw-r--r--apps/comments/composer/composer/autoload_static.php2
-rw-r--r--apps/comments/lib/AppInfo/Application.php4
-rw-r--r--apps/comments/lib/Search/CommentsSearchResultEntry.php31
-rw-r--r--apps/comments/lib/Search/LegacyProvider.php113
-rw-r--r--apps/comments/lib/Search/Provider.php125
-rw-r--r--apps/comments/lib/Search/Result.php22
-rw-r--r--apps/files/composer/composer/autoload_classmap.php2
-rw-r--r--apps/files/composer/composer/autoload_static.php2
-rw-r--r--apps/files/lib/AppInfo/Application.php3
-rw-r--r--apps/files/lib/Search/FilesSearchProvider.php73
-rw-r--r--apps/files/lib/Search/FilesSearchResultEntry.php37
-rw-r--r--core/Controller/UnifiedSearchController.php98
-rw-r--r--core/routes.php2
-rw-r--r--lib/composer/composer/autoload_classmap.php7
-rw-r--r--lib/composer/composer/autoload_static.php7
-rw-r--r--lib/private/AppFramework/Bootstrap/Coordinator.php7
-rw-r--r--lib/private/AppFramework/Bootstrap/RegistrationContext.php35
-rw-r--r--lib/private/Search/Provider/File.php2
-rw-r--r--lib/private/Search/Result/Audio.php4
-rw-r--r--lib/private/Search/Result/File.php10
-rw-r--r--lib/private/Search/Result/Folder.php2
-rw-r--r--lib/private/Search/Result/Image.php4
-rw-r--r--lib/private/Search/SearchComposer.php154
-rw-r--r--lib/private/Search/SearchQuery.php88
-rw-r--r--lib/public/AppFramework/Bootstrap/IRegistrationContext.php15
-rw-r--r--lib/public/ISearch.php5
-rw-r--r--lib/public/IServerContainer.php1
-rw-r--r--lib/public/Search/ASearchResultEntry.php102
-rw-r--r--lib/public/Search/IProvider.php83
-rw-r--r--lib/public/Search/ISearchQuery.php79
-rw-r--r--lib/public/Search/PagedProvider.php5
-rw-r--r--lib/public/Search/Provider.php7
-rw-r--r--lib/public/Search/Result.php6
-rw-r--r--lib/public/Search/SearchResult.php112
-rw-r--r--tests/lib/AppFramework/Bootstrap/CoordinatorTest.php6
36 files changed, 1178 insertions, 79 deletions
diff --git a/apps/comments/composer/composer/autoload_classmap.php b/apps/comments/composer/composer/autoload_classmap.php
index d5d51c7a12c..5682d66468d 100644
--- a/apps/comments/composer/composer/autoload_classmap.php
+++ b/apps/comments/composer/composer/autoload_classmap.php
@@ -21,6 +21,8 @@ return array(
'OCA\\Comments\\Listener\\LoadSidebarScripts' => $baseDir . '/../lib/Listener/LoadSidebarScripts.php',
'OCA\\Comments\\Notification\\Listener' => $baseDir . '/../lib/Notification/Listener.php',
'OCA\\Comments\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',
+ 'OCA\\Comments\\Search\\CommentsSearchResultEntry' => $baseDir . '/../lib/Search/CommentsSearchResultEntry.php',
+ 'OCA\\Comments\\Search\\LegacyProvider' => $baseDir . '/../lib/Search/LegacyProvider.php',
'OCA\\Comments\\Search\\Provider' => $baseDir . '/../lib/Search/Provider.php',
'OCA\\Comments\\Search\\Result' => $baseDir . '/../lib/Search/Result.php',
);
diff --git a/apps/comments/composer/composer/autoload_static.php b/apps/comments/composer/composer/autoload_static.php
index 1292415290b..87a2127c144 100644
--- a/apps/comments/composer/composer/autoload_static.php
+++ b/apps/comments/composer/composer/autoload_static.php
@@ -36,6 +36,8 @@ class ComposerStaticInitComments
'OCA\\Comments\\Listener\\LoadSidebarScripts' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarScripts.php',
'OCA\\Comments\\Notification\\Listener' => __DIR__ . '/..' . '/../lib/Notification/Listener.php',
'OCA\\Comments\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',
+ 'OCA\\Comments\\Search\\CommentsSearchResultEntry' => __DIR__ . '/..' . '/../lib/Search/CommentsSearchResultEntry.php',
+ 'OCA\\Comments\\Search\\LegacyProvider' => __DIR__ . '/..' . '/../lib/Search/LegacyProvider.php',
'OCA\\Comments\\Search\\Provider' => __DIR__ . '/..' . '/../lib/Search/Provider.php',
'OCA\\Comments\\Search\\Result' => __DIR__ . '/..' . '/../lib/Search/Result.php',
);
diff --git a/apps/comments/lib/AppInfo/Application.php b/apps/comments/lib/AppInfo/Application.php
index 8bcf17b2afe..fafac0c6040 100644
--- a/apps/comments/lib/AppInfo/Application.php
+++ b/apps/comments/lib/AppInfo/Application.php
@@ -35,6 +35,7 @@ use OCA\Comments\Listener\CommentsEntityEventListener;
use OCA\Comments\Listener\LoadAdditionalScripts;
use OCA\Comments\Listener\LoadSidebarScripts;
use OCA\Comments\Notification\Notifier;
+use OCA\Comments\Search\LegacyProvider;
use OCA\Comments\Search\Provider;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\Files\Event\LoadSidebar;
@@ -70,6 +71,7 @@ class Application extends App implements IBootstrap {
CommentsEntityEvent::EVENT_ENTITY,
CommentsEntityEventListener::class
);
+ $context->registerSearchProvider(Provider::class);
}
public function boot(IBootContext $context): void {
@@ -79,7 +81,7 @@ class Application extends App implements IBootstrap {
$jsSettingsHelper = new JSSettingsHelper($context->getServerContainer());
Util::connectHook('\OCP\Config', 'js', $jsSettingsHelper, 'extend');
- $context->getServerContainer()->getSearch()->registerProvider(Provider::class, ['apps' => ['files']]);
+ $context->getServerContainer()->getSearch()->registerProvider(LegacyProvider::class, ['apps' => ['files']]);
}
protected function registerNotifier(IServerContainer $container) {
diff --git a/apps/comments/lib/Search/CommentsSearchResultEntry.php b/apps/comments/lib/Search/CommentsSearchResultEntry.php
new file mode 100644
index 00000000000..de24cf9cc52
--- /dev/null
+++ b/apps/comments/lib/Search/CommentsSearchResultEntry.php
@@ -0,0 +1,31 @@
+<?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\Comments\Search;
+
+use OCP\Search\ASearchResultEntry;
+
+class CommentsSearchResultEntry extends ASearchResultEntry {
+}
diff --git a/apps/comments/lib/Search/LegacyProvider.php b/apps/comments/lib/Search/LegacyProvider.php
new file mode 100644
index 00000000000..abcb8535f5c
--- /dev/null
+++ b/apps/comments/lib/Search/LegacyProvider.php
@@ -0,0 +1,113 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2018 Joas Schilling <coding@schilljs.com>
+ *
+ * @author Joas Schilling <coding@schilljs.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 OCA\Comments\Search;
+
+use OCP\Comments\IComment;
+use OCP\Files\Folder;
+use OCP\Files\Node;
+use OCP\Files\NotFoundException;
+use OCP\IUser;
+use OCP\Search\Provider;
+use function count;
+
+class LegacyProvider extends Provider {
+
+ /**
+ * Search for $query
+ *
+ * @param string $query
+ * @return array An array of OCP\Search\Result's
+ * @since 7.0.0
+ */
+ public function search($query): array {
+ $cm = \OC::$server->getCommentsManager();
+ $us = \OC::$server->getUserSession();
+
+ $user = $us->getUser();
+ if (!$user instanceof IUser) {
+ return [];
+ }
+ $uf = \OC::$server->getUserFolder($user->getUID());
+
+ if ($uf === null) {
+ return [];
+ }
+
+ $result = [];
+ $numComments = 50;
+ $offset = 0;
+
+ while (count($result) < $numComments) {
+ /** @var IComment[] $comments */
+ $comments = $cm->search($query, 'files', '', 'comment', $offset, $numComments);
+
+ foreach ($comments as $comment) {
+ if ($comment->getActorType() !== 'users') {
+ continue;
+ }
+
+ $displayName = $cm->resolveDisplayName('user', $comment->getActorId());
+
+ try {
+ $file = $this->getFileForComment($uf, $comment);
+ $result[] = new Result($query,
+ $comment,
+ $displayName,
+ $file->getPath()
+ );
+ } catch (NotFoundException $e) {
+ continue;
+ }
+ }
+
+ if (count($comments) < $numComments) {
+ // Didn't find more comments when we tried to get, so there are no more comments.
+ return $result;
+ }
+
+ $offset += $numComments;
+ $numComments = 50 - count($result);
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param Folder $userFolder
+ * @param IComment $comment
+ * @return Node
+ * @throws NotFoundException
+ */
+ protected function getFileForComment(Folder $userFolder, IComment $comment): Node {
+ $nodes = $userFolder->getById((int) $comment->getObjectId());
+ if (empty($nodes)) {
+ throw new NotFoundException('File not found');
+ }
+
+ return array_shift($nodes);
+ }
+}
diff --git a/apps/comments/lib/Search/Provider.php b/apps/comments/lib/Search/Provider.php
index ad3b63d46b5..0c0561c988c 100644
--- a/apps/comments/lib/Search/Provider.php
+++ b/apps/comments/lib/Search/Provider.php
@@ -1,8 +1,11 @@
<?php
+
+declare(strict_types=1);
+
/**
- * @copyright Copyright (c) 2018 Joas Schilling <coding@schilljs.com>
+ * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
- * @author Joas Schilling <coding@schilljs.com>
+ * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
@@ -17,92 +20,62 @@
* 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/>.
- *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\Comments\Search;
-use OCP\Comments\IComment;
-use OCP\Files\Folder;
-use OCP\Files\Node;
-use OCP\Files\NotFoundException;
+use OCP\IL10N;
+use OCP\IURLGenerator;
use OCP\IUser;
+use OCP\Search\IProvider;
+use OCP\Search\ISearchQuery;
+use OCP\Search\SearchResult;
+use function array_map;
+use function pathinfo;
-class Provider extends \OCP\Search\Provider {
-
- /**
- * Search for $query
- *
- * @param string $query
- * @return array An array of OCP\Search\Result's
- * @since 7.0.0
- */
- public function search($query): array {
- $cm = \OC::$server->getCommentsManager();
- $us = \OC::$server->getUserSession();
-
- $user = $us->getUser();
- if (!$user instanceof IUser) {
- return [];
- }
- $uf = \OC::$server->getUserFolder($user->getUID());
-
- if ($uf === null) {
- return [];
- }
+class Provider implements IProvider {
- $result = [];
- $numComments = 50;
- $offset = 0;
+ /** @var IL10N */
+ private $l10n;
- while (\count($result) < $numComments) {
- /** @var IComment[] $comments */
- $comments = $cm->search($query, 'files', '', 'comment', $offset, $numComments);
+ /** @var IURLGenerator */
+ private $urlGenerator;
- foreach ($comments as $comment) {
- if ($comment->getActorType() !== 'users') {
- continue;
- }
+ /** @var LegacyProvider */
+ private $legacyProvider;
- $displayName = $cm->resolveDisplayName('user', $comment->getActorId());
-
- try {
- $file = $this->getFileForComment($uf, $comment);
- $result[] = new Result($query,
- $comment,
- $displayName,
- $file->getPath()
- );
- } catch (NotFoundException $e) {
- continue;
- }
- }
-
- if (\count($comments) < $numComments) {
- // Didn't find more comments when we tried to get, so there are no more comments.
- return $result;
- }
-
- $offset += $numComments;
- $numComments = 50 - \count($result);
- }
-
- return $result;
+ public function __construct(IL10N $l10n,
+ IURLGenerator $urlGenerator,
+ LegacyProvider $legacyProvider) {
+ $this->l10n = $l10n;
+ $this->urlGenerator = $urlGenerator;
+ $this->legacyProvider = $legacyProvider;
}
- /**
- * @param Folder $userFolder
- * @param IComment $comment
- * @return Node
- * @throws NotFoundException
- */
- protected function getFileForComment(Folder $userFolder, IComment $comment): Node {
- $nodes = $userFolder->getById((int) $comment->getObjectId());
- if (empty($nodes)) {
- throw new NotFoundException('File not found');
- }
+ public function getId(): string {
+ return 'comments';
+ }
- return array_shift($nodes);
+ public function search(IUser $user, ISearchQuery $query): SearchResult {
+ return SearchResult::complete(
+ $this->l10n->t('Comments'),
+ array_map(function (Result $result) {
+ $path = $result->path;
+ $pathInfo = pathinfo($path);
+ return new CommentsSearchResultEntry(
+ $this->urlGenerator->linkToRoute('core.Preview.getPreviewByFileId', ['x' => 32, 'y' => 32, 'fileId' => $result->id]),
+ $result->name,
+ $path,
+ $this->urlGenerator->linkToRoute(
+ 'files.view.index',
+ [
+ 'dir' => $pathInfo['dirname'],
+ 'scrollto' => $pathInfo['basename'],
+ ]
+ )
+ );
+ }, $this->legacyProvider->search($query->getTerm()))
+ );
}
}
diff --git a/apps/comments/lib/Search/Result.php b/apps/comments/lib/Search/Result.php
index 9f0e6936320..d0e9902c1fb 100644
--- a/apps/comments/lib/Search/Result.php
+++ b/apps/comments/lib/Search/Result.php
@@ -28,12 +28,33 @@ use OCP\Comments\IComment;
use OCP\Files\NotFoundException;
use OCP\Search\Result as BaseResult;
+/**
+ * @deprecated 20.0.0
+ */
class Result extends BaseResult {
+ /**
+ * @deprecated 20.0.0
+ */
public $type = 'comment';
+ /**
+ * @deprecated 20.0.0
+ */
public $comment;
+ /**
+ * @deprecated 20.0.0
+ */
public $authorId;
+ /**
+ * @deprecated 20.0.0
+ */
public $authorName;
+ /**
+ * @deprecated 20.0.0
+ */
public $path;
+ /**
+ * @deprecated 20.0.0
+ */
public $fileName;
/**
@@ -42,6 +63,7 @@ class Result extends BaseResult {
* @param string $authorName
* @param string $path
* @throws NotFoundException
+ * @deprecated 20.0.0
*/
public function __construct(string $search,
IComment $comment,
diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php
index a4a72d59c13..04c24ee52d9 100644
--- a/apps/files/composer/composer/autoload_classmap.php
+++ b/apps/files/composer/composer/autoload_classmap.php
@@ -47,6 +47,8 @@ return array(
'OCA\\Files\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php',
'OCA\\Files\\Migration\\Version11301Date20191205150729' => $baseDir . '/../lib/Migration/Version11301Date20191205150729.php',
'OCA\\Files\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',
+ 'OCA\\Files\\Search\\FilesSearchProvider' => $baseDir . '/../lib/Search/FilesSearchProvider.php',
+ 'OCA\\Files\\Search\\FilesSearchResultEntry' => $baseDir . '/../lib/Search/FilesSearchResultEntry.php',
'OCA\\Files\\Service\\DirectEditingService' => $baseDir . '/../lib/Service/DirectEditingService.php',
'OCA\\Files\\Service\\OwnershipTransferService' => $baseDir . '/../lib/Service/OwnershipTransferService.php',
'OCA\\Files\\Service\\TagService' => $baseDir . '/../lib/Service/TagService.php',
diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php
index 91e29fac487..f9050eca862 100644
--- a/apps/files/composer/composer/autoload_static.php
+++ b/apps/files/composer/composer/autoload_static.php
@@ -62,6 +62,8 @@ class ComposerStaticInitFiles
'OCA\\Files\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php',
'OCA\\Files\\Migration\\Version11301Date20191205150729' => __DIR__ . '/..' . '/../lib/Migration/Version11301Date20191205150729.php',
'OCA\\Files\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',
+ 'OCA\\Files\\Search\\FilesSearchProvider' => __DIR__ . '/..' . '/../lib/Search/FilesSearchProvider.php',
+ 'OCA\\Files\\Search\\FilesSearchResultEntry' => __DIR__ . '/..' . '/../lib/Search/FilesSearchResultEntry.php',
'OCA\\Files\\Service\\DirectEditingService' => __DIR__ . '/..' . '/../lib/Service/DirectEditingService.php',
'OCA\\Files\\Service\\OwnershipTransferService' => __DIR__ . '/..' . '/../lib/Service/OwnershipTransferService.php',
'OCA\\Files\\Service\\TagService' => __DIR__ . '/..' . '/../lib/Service/TagService.php',
diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php
index 82562ffe9f0..5e473c411ee 100644
--- a/apps/files/lib/AppInfo/Application.php
+++ b/apps/files/lib/AppInfo/Application.php
@@ -44,6 +44,7 @@ use OCA\Files\Event\LoadSidebar;
use OCA\Files\Listener\LegacyLoadAdditionalScriptsAdapter;
use OCA\Files\Listener\LoadSidebarListener;
use OCA\Files\Notification\Notifier;
+use OCA\Files\Search\FilesSearchProvider;
use OCA\Files\Service\TagService;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
@@ -106,6 +107,8 @@ class Application extends App implements IBootstrap {
$context->registerEventListener(LoadAdditionalScriptsEvent::class, LegacyLoadAdditionalScriptsAdapter::class);
$context->registerEventListener(LoadSidebar::class, LoadSidebarListener::class);
+
+ $context->registerSearchProvider(FilesSearchProvider::class);
}
public function boot(IBootContext $context): void {
diff --git a/apps/files/lib/Search/FilesSearchProvider.php b/apps/files/lib/Search/FilesSearchProvider.php
new file mode 100644
index 00000000000..3f1c4de0aa1
--- /dev/null
+++ b/apps/files/lib/Search/FilesSearchProvider.php
@@ -0,0 +1,73 @@
+<?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 OC\Search\Provider\File;
+use OC\Search\Result\File as FileResult;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\Search\IProvider;
+use OCP\Search\ISearchQuery;
+use OCP\Search\SearchResult;
+
+class FilesSearchProvider implements IProvider {
+
+ /** @var File */
+ private $fileSearch;
+
+ /** @var IL10N */
+ private $l10n;
+
+ /** @var IURLGenerator */
+ private $urlGenerator;
+
+ public function __construct(File $fileSearch,
+ IL10N $l10n,
+ IURLGenerator $urlGenerator) {
+ $this->l10n = $l10n;
+ $this->fileSearch = $fileSearch;
+ $this->urlGenerator = $urlGenerator;
+ }
+
+ public function getId(): string {
+ return 'files';
+ }
+
+ public function search(IUser $user, ISearchQuery $query): SearchResult {
+ return SearchResult::complete(
+ $this->l10n->t('Files'),
+ array_map(function (FileResult $result) {
+ return new FilesSearchResultEntry(
+ $this->urlGenerator->linkToRoute('core.Preview.getPreviewByFileId', ['x' => 32, 'y' => 32, 'fileId' => $result->id]),
+ $result->name,
+ $result->path,
+ $result->link
+ );
+ }, $this->fileSearch->search($query->getTerm()))
+ );
+ }
+}
diff --git a/apps/files/lib/Search/FilesSearchResultEntry.php b/apps/files/lib/Search/FilesSearchResultEntry.php
new file mode 100644
index 00000000000..c4f6e491d6f
--- /dev/null
+++ b/apps/files/lib/Search/FilesSearchResultEntry.php
@@ -0,0 +1,37 @@
+<?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\Search\ASearchResultEntry;
+
+class FilesSearchResultEntry extends ASearchResultEntry {
+ public function __construct(string $thumbnailUrl,
+ string $filename,
+ string $path,
+ string $url) {
+ parent::__construct($thumbnailUrl, $filename, $path, $url);
+ }
+}
diff --git a/core/Controller/UnifiedSearchController.php b/core/Controller/UnifiedSearchController.php
new file mode 100644
index 00000000000..ddb1745dea5
--- /dev/null
+++ b/core/Controller/UnifiedSearchController.php
@@ -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
+ )
+ )
+ );
+ }
+}
diff --git a/core/routes.php b/core/routes.php
index 3d30b12e392..5bb58f63e45 100644
--- a/core/routes.php
+++ b/core/routes.php
@@ -77,6 +77,8 @@ $application->registerRoutes($this, [
['name' => 'RecommendedApps#index', 'url' => '/core/apps/recommended', 'verb' => 'GET'],
['name' => 'Svg#getSvgFromCore', 'url' => '/svg/core/{folder}/{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' => 'Js#getJs', 'url' => '/js/{appName}/{fileName}', 'verb' => 'GET'],
['name' => 'contactsMenu#index', 'url' => '/contactsmenu/contacts', 'verb' => 'POST'],
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index e405d25ee66..b51c9876e5d 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -432,9 +432,13 @@ 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\\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\\Provider' => $baseDir . '/lib/public/Search/Provider.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\\Events\\GenerateSecurePasswordEvent' => $baseDir . '/lib/public/Security/Events/GenerateSecurePasswordEvent.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\\SvgController' => $baseDir . '/core/Controller/SvgController.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\\WalledGardenController' => $baseDir . '/core/Controller/WalledGardenController.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\\Folder' => $baseDir . '/lib/private/Search/Result/Folder.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\\Throttler' => $baseDir . '/lib/private/Security/Bruteforce/Throttler.php',
'OC\\Security\\CSP\\ContentSecurityPolicy' => $baseDir . '/lib/private/Security/CSP/ContentSecurityPolicy.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index 456fa81087d..2f640e014f3 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -461,9 +461,13 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'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\\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\\Provider' => __DIR__ . '/../../..' . '/lib/public/Search/Provider.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\\Events\\GenerateSecurePasswordEvent' => __DIR__ . '/../../..' . '/lib/public/Security/Events/GenerateSecurePasswordEvent.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\\SvgController' => __DIR__ . '/../../..' . '/core/Controller/SvgController.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\\WalledGardenController' => __DIR__ . '/../../..' . '/core/Controller/WalledGardenController.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\\Folder' => __DIR__ . '/../../..' . '/lib/private/Search/Result/Folder.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\\Throttler' => __DIR__ . '/../../..' . '/lib/private/Security/Bruteforce/Throttler.php',
'OC\\Security\\CSP\\ContentSecurityPolicy' => __DIR__ . '/../../..' . '/lib/private/Security/CSP/ContentSecurityPolicy.php',
diff --git a/lib/private/AppFramework/Bootstrap/Coordinator.php b/lib/private/AppFramework/Bootstrap/Coordinator.php
index 9b0f6a9188c..085e7460da6 100644
--- a/lib/private/AppFramework/Bootstrap/Coordinator.php
+++ b/lib/private/AppFramework/Bootstrap/Coordinator.php
@@ -25,6 +25,7 @@ declare(strict_types=1);
namespace OC\AppFramework\Bootstrap;
+use OC\Search\SearchComposer;
use OC\Support\CrashReport\Registry;
use OC_App;
use OCP\AppFramework\App;
@@ -49,16 +50,21 @@ class Coordinator {
/** @var IEventDispatcher */
private $eventDispatcher;
+ /** @var SearchComposer */
+ private $searchComposer;
+
/** @var ILogger */
private $logger;
public function __construct(IServerContainer $container,
Registry $registry,
IEventDispatcher $eventListener,
+ SearchComposer $searchComposer,
ILogger $logger) {
$this->serverContainer = $container;
$this->registry = $registry;
$this->eventDispatcher = $eventListener;
+ $this->searchComposer = $searchComposer;
$this->logger = $logger;
}
@@ -112,6 +118,7 @@ class Coordinator {
$context->delegateEventListenerRegistrations($this->eventDispatcher);
$context->delegateContainerRegistrations($apps);
$context->delegateMiddlewareRegistrations($apps);
+ $context->delegateSearchProviderRegistration($apps, $this->searchComposer);
}
public function bootApp(string $appId): void {
diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php
index 340012c8b1b..23eee9c6e33 100644
--- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php
+++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php
@@ -26,6 +26,7 @@ declare(strict_types=1);
namespace OC\AppFramework\Bootstrap;
use Closure;
+use OC\Search\SearchComposer;
use OC\Support\CrashReport\Registry;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
@@ -56,6 +57,9 @@ class RegistrationContext {
/** @var array[] */
private $middlewares = [];
+ /** @var array[] */
+ private $searchProviders = [];
+
/** @var ILogger */
private $logger;
@@ -130,6 +134,13 @@ class RegistrationContext {
$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
*/
@@ -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,
+ ]);
+ }
+ }
+ }
}
diff --git a/lib/private/Search/Provider/File.php b/lib/private/Search/Provider/File.php
index 02521460d8c..9a41a46bd35 100644
--- a/lib/private/Search/Provider/File.php
+++ b/lib/private/Search/Provider/File.php
@@ -32,6 +32,7 @@ use OC\Files\Filesystem;
/**
* Provide search results from the 'files' app
+ * @deprecated 20.0.0
*/
class File extends \OCP\Search\Provider {
@@ -39,6 +40,7 @@ class File extends \OCP\Search\Provider {
* Search for files and folders matching the given query
* @param string $query
* @return \OCP\Search\Result
+ * @deprecated 20.0.0
*/
public function search($query) {
$files = Filesystem::search($query);
diff --git a/lib/private/Search/Result/Audio.php b/lib/private/Search/Result/Audio.php
index ef0d3bf9d20..e3917b7e4b3 100644
--- a/lib/private/Search/Result/Audio.php
+++ b/lib/private/Search/Result/Audio.php
@@ -27,15 +27,17 @@ namespace OC\Search\Result;
/**
* A found audio file
+ * @deprecated 20.0.0
*/
class Audio extends File {
/**
* Type name; translated in templates
* @var string
+ * @deprecated 20.0.0
*/
public $type = 'audio';
-
+
/**
* @TODO add ID3 information
*/
diff --git a/lib/private/Search/Result/File.php b/lib/private/Search/Result/File.php
index cfff54e0692..f93b033c07f 100644
--- a/lib/private/Search/Result/File.php
+++ b/lib/private/Search/Result/File.php
@@ -31,36 +31,42 @@ use OCP\Files\Folder;
/**
* A found file
+ * @deprecated 20.0.0
*/
class File extends \OCP\Search\Result {
/**
* Type name; translated in templates
* @var string
+ * @deprecated 20.0.0
*/
public $type = 'file';
/**
* Path to file
* @var string
+ * @deprecated 20.0.0
*/
public $path;
/**
* Size, in bytes
* @var int
+ * @deprecated 20.0.0
*/
public $size;
/**
* Date modified, in human readable form
* @var string
+ * @deprecated 20.0.0
*/
public $modified;
/**
* File mime type
* @var string
+ * @deprecated 20.0.0
*/
public $mime_type;
@@ -68,12 +74,14 @@ class File extends \OCP\Search\Result {
* File permissions:
*
* @var string
+ * @deprecated 20.0.0
*/
public $permissions;
/**
* Create a new file search result
* @param FileInfo $data file data given by provider
+ * @deprecated 20.0.0
*/
public function __construct(FileInfo $data) {
$path = $this->getRelativePath($data->getPath());
@@ -97,6 +105,7 @@ class File extends \OCP\Search\Result {
/**
* @var Folder $userFolderCache
+ * @deprecated 20.0.0
*/
protected static $userFolderCache = null;
@@ -105,6 +114,7 @@ class File extends \OCP\Search\Result {
* eg /user/files/foo.txt -> /foo.txt
* @param string $path
* @return string relative path
+ * @deprecated 20.0.0
*/
protected function getRelativePath($path) {
if (!isset(self::$userFolderCache)) {
diff --git a/lib/private/Search/Result/Folder.php b/lib/private/Search/Result/Folder.php
index 8110d61bead..1268b1379b2 100644
--- a/lib/private/Search/Result/Folder.php
+++ b/lib/private/Search/Result/Folder.php
@@ -27,12 +27,14 @@ namespace OC\Search\Result;
/**
* A found folder
+ * @deprecated 20.0.0
*/
class Folder extends File {
/**
* Type name; translated in templates
* @var string
+ * @deprecated 20.0.0
*/
public $type = 'folder';
}
diff --git a/lib/private/Search/Result/Image.php b/lib/private/Search/Result/Image.php
index e569c91ea02..5a46138f594 100644
--- a/lib/private/Search/Result/Image.php
+++ b/lib/private/Search/Result/Image.php
@@ -27,15 +27,17 @@ namespace OC\Search\Result;
/**
* A found image file
+ * @deprecated 20.0.0
*/
class Image extends File {
/**
* Type name; translated in templates
* @var string
+ * @deprecated 20.0.0
*/
public $type = 'image';
-
+
/**
* @TODO add EXIF information
*/
diff --git a/lib/private/Search/SearchComposer.php b/lib/private/Search/SearchComposer.php
new file mode 100644
index 00000000000..ae4350ca5cc
--- /dev/null
+++ b/lib/private/Search/SearchComposer.php
@@ -0,0 +1,154 @@
+<?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 InvalidArgumentException;
+use OCP\AppFramework\Bootstrap\IRegistrationContext;
+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
+ *
+ * The search process is generally split into two steps
+ *
+ * 1. Get a list of provider (`getProviders`)
+ * 2. Get search results of each provider (`search`)
+ *
+ * The reasoning behind this is that the runtime complexity of a combined search
+ * result would be O(n) and linearly grow with each provider added. This comes
+ * from the nature of php where we can't concurrently fetch the search results.
+ * So we offload the concurrency the client application (e.g. JavaScript in the
+ * browser) and let it first get the list of providers to then fetch all results
+ * concurrently. The client is free to decide whether all concurrent search
+ * results are awaited or shown as they come in.
+ *
+ * @see IProvider::search() for the arguments of the individual search requests
+ */
+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;
+ }
+
+ /**
+ * Register a search provider lazily
+ *
+ * Registers the fully-qualified class name of an implementation of an
+ * IProvider. The service will only be queried on demand. Apps will register
+ * the providers through the registration context object.
+ *
+ * @see IRegistrationContext::registerSearchProvider()
+ *
+ * @param string $class
+ */
+ 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 = [];
+ }
+
+ /**
+ * Get a list of all provider IDs for the consecutive calls to `search`
+ *
+ * @return string[]
+ */
+ 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));
+ }
+
+ /**
+ * Query an individual search provider for results
+ *
+ * @param IUser $user
+ * @param string $providerId one of the IDs received by `getProviders`
+ * @param ISearchQuery $query
+ *
+ * @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();
+
+ $provider = $this->providers[$providerId] ?? null;
+ if ($provider === null) {
+ throw new InvalidArgumentException("Provider $providerId is unknown");
+ }
+ return $provider->search($user, $query);
+ }
+}
diff --git a/lib/private/Search/SearchQuery.php b/lib/private/Search/SearchQuery.php
new file mode 100644
index 00000000000..2ed31fed441
--- /dev/null
+++ b/lib/private/Search/SearchQuery.php
@@ -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;
+ }
+}
diff --git a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php
index 5d3ffc8d479..12367e5ed05 100644
--- a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php
+++ b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php
@@ -115,4 +115,19 @@ interface IRegistrationContext {
* @since 20.0.0
*/
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;
}
diff --git a/lib/public/ISearch.php b/lib/public/ISearch.php
index 747b598e669..94338b3d20d 100644
--- a/lib/public/ISearch.php
+++ b/lib/public/ISearch.php
@@ -29,6 +29,7 @@ namespace OCP;
/**
* Small Interface for Search
* @since 7.0.0
+ * @deprecated 20.0.0
*/
interface ISearch {
@@ -40,6 +41,7 @@ interface ISearch {
* @param int $size
* @return array An array of OCP\Search\Result's
* @since 8.0.0
+ * @deprecated 20.0.0
*/
public function searchPaged($query, array $inApps = [], $page = 1, $size = 30);
@@ -48,6 +50,7 @@ interface ISearch {
* @param string $class class name of a OCP\Search\Provider
* @param array $options optional
* @since 7.0.0
+ * @deprecated 20.0.0
*/
public function registerProvider($class, array $options = []);
@@ -55,12 +58,14 @@ interface ISearch {
* Remove one existing search provider
* @param string $provider class name of a OCP\Search\Provider
* @since 7.0.0
+ * @deprecated 20.0.0
*/
public function removeProvider($provider);
/**
* Remove all registered search providers
* @since 7.0.0
+ * @deprecated 20.0.0
*/
public function clearProviders();
}
diff --git a/lib/public/IServerContainer.php b/lib/public/IServerContainer.php
index 45f2a1caf4e..f5a644bee04 100644
--- a/lib/public/IServerContainer.php
+++ b/lib/public/IServerContainer.php
@@ -359,6 +359,7 @@ interface IServerContainer extends IContainer {
*
* @return \OCP\ISearch
* @since 7.0.0
+ * @deprecated 20.0.0
*/
public function getSearch();
diff --git a/lib/public/Search/ASearchResultEntry.php b/lib/public/Search/ASearchResultEntry.php
new file mode 100644
index 00000000000..45d62525abd
--- /dev/null
+++ b/lib/public/Search/ASearchResultEntry.php
@@ -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,
+ ];
+ }
+}
diff --git a/lib/public/Search/IProvider.php b/lib/public/Search/IProvider.php
new file mode 100644
index 00000000000..080f5089f1f
--- /dev/null
+++ b/lib/public/Search/IProvider.php
@@ -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 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;
+}
diff --git a/lib/public/Search/ISearchQuery.php b/lib/public/Search/ISearchQuery.php
new file mode 100644
index 00000000000..00d538050d4
--- /dev/null
+++ b/lib/public/Search/ISearchQuery.php
@@ -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();
+}
diff --git a/lib/public/Search/PagedProvider.php b/lib/public/Search/PagedProvider.php
index cbccc1abc0f..479214ad405 100644
--- a/lib/public/Search/PagedProvider.php
+++ b/lib/public/Search/PagedProvider.php
@@ -30,12 +30,14 @@ namespace OCP\Search;
/**
* Provides a template for search functionality throughout ownCloud;
* @since 8.0.0
+ * @deprecated 20.0.0
*/
abstract class PagedProvider extends Provider {
/**
* show all results
* @since 8.0.0
+ * @deprecated 20.0.0
*/
public const SIZE_ALL = 0;
@@ -43,6 +45,7 @@ abstract class PagedProvider extends Provider {
* Constructor
* @param array $options
* @since 8.0.0
+ * @deprecated 20.0.0
*/
public function __construct($options) {
parent::__construct($options);
@@ -53,6 +56,7 @@ abstract class PagedProvider extends Provider {
* @param string $query
* @return array An array of OCP\Search\Result's
* @since 8.0.0
+ * @deprecated 20.0.0
*/
public function search($query) {
// old apps might assume they get all results, so we use SIZE_ALL
@@ -66,6 +70,7 @@ abstract class PagedProvider extends Provider {
* @param int $size 0 = SIZE_ALL
* @return array An array of OCP\Search\Result's
* @since 8.0.0
+ * @deprecated 20.0.0
*/
abstract public function searchPaged($query, $page, $size);
}
diff --git a/lib/public/Search/Provider.php b/lib/public/Search/Provider.php
index 18594eefb8f..275a63c0056 100644
--- a/lib/public/Search/Provider.php
+++ b/lib/public/Search/Provider.php
@@ -30,11 +30,13 @@ namespace OCP\Search;
/**
* Provides a template for search functionality throughout ownCloud;
* @since 7.0.0
+ * @deprecated 20.0.0
*/
abstract class Provider {
/**
* @since 8.0.0
+ * @deprecated 20.0.0
*/
public const OPTION_APPS = 'apps';
@@ -42,6 +44,7 @@ abstract class Provider {
* List of options
* @var array
* @since 7.0.0
+ * @deprecated 20.0.0
*/
protected $options;
@@ -49,6 +52,7 @@ abstract class Provider {
* Constructor
* @param array $options as key => value
* @since 7.0.0 - default value for $options was added in 8.0.0
+ * @deprecated 20.0.0
*/
public function __construct($options = []) {
$this->options = $options;
@@ -59,6 +63,7 @@ abstract class Provider {
* @param string $key
* @return mixed
* @since 8.0.0
+ * @deprecated 20.0.0
*/
public function getOption($key) {
if (is_array($this->options) && isset($this->options[$key])) {
@@ -76,6 +81,7 @@ abstract class Provider {
* @param string[] $apps
* @return bool
* @since 8.0.0
+ * @deprecated 20.0.0
*/
public function providesResultsFor(array $apps = []) {
$forApps = $this->getOption(self::OPTION_APPS);
@@ -87,6 +93,7 @@ abstract class Provider {
* @param string $query
* @return array An array of OCP\Search\Result's
* @since 7.0.0
+ * @deprecated 20.0.0
*/
abstract public function search($query);
}
diff --git a/lib/public/Search/Result.php b/lib/public/Search/Result.php
index 33748cff375..a3a58a38cde 100644
--- a/lib/public/Search/Result.php
+++ b/lib/public/Search/Result.php
@@ -29,6 +29,7 @@ namespace OCP\Search;
/**
* The generic result of a search
* @since 7.0.0
+ * @deprecated 20.0.0
*/
class Result {
@@ -37,6 +38,7 @@ class Result {
* corresponding application.
* @var string
* @since 7.0.0
+ * @deprecated 20.0.0
*/
public $id;
@@ -45,6 +47,7 @@ class Result {
* results.
* @var string
* @since 7.0.0
+ * @deprecated 20.0.0
*/
public $name;
@@ -52,6 +55,7 @@ class Result {
* URL to the application item.
* @var string
* @since 7.0.0
+ * @deprecated 20.0.0
*/
public $link;
@@ -60,6 +64,7 @@ class Result {
* as the class name (e.g. \OC\Search\File -> 'file') in lowercase.
* @var string
* @since 7.0.0
+ * @deprecated 20.0.0
*/
public $type = 'generic';
@@ -69,6 +74,7 @@ class Result {
* @param string $name displayed text of result
* @param string $link URL to the result within its app
* @since 7.0.0
+ * @deprecated 20.0.0
*/
public function __construct($id = null, $name = null, $link = null) {
$this->id = $id;
diff --git a/lib/public/Search/SearchResult.php b/lib/public/Search/SearchResult.php
new file mode 100644
index 00000000000..7abb5b9f188
--- /dev/null
+++ b/lib/public/Search/SearchResult.php
@@ -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,
+ ];
+ }
+}
diff --git a/tests/lib/AppFramework/Bootstrap/CoordinatorTest.php b/tests/lib/AppFramework/Bootstrap/CoordinatorTest.php
index c12e5eeb150..6909ad94e7f 100644
--- a/tests/lib/AppFramework/Bootstrap/CoordinatorTest.php
+++ b/tests/lib/AppFramework/Bootstrap/CoordinatorTest.php
@@ -26,6 +26,7 @@ declare(strict_types=1);
namespace lib\AppFramework\Bootstrap;
use OC\AppFramework\Bootstrap\Coordinator;
+use OC\Search\SearchComposer;
use OC\Support\CrashReport\Registry;
use OCP\App\IAppManager;
use OCP\AppFramework\App;
@@ -53,6 +54,9 @@ class CoordinatorTest extends TestCase {
/** @var IEventDispatcher|MockObject */
private $eventDispatcher;
+ /** @var SearchComposer|MockObject */
+ private $searchComposer;
+
/** @var ILogger|MockObject */
private $logger;
@@ -66,12 +70,14 @@ class CoordinatorTest extends TestCase {
$this->serverContainer = $this->createMock(IServerContainer::class);
$this->crashReporterRegistry = $this->createMock(Registry::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
+ $this->searchComposer = $this->createMock(SearchComposer::class);
$this->logger = $this->createMock(ILogger::class);
$this->coordinator = new Coordinator(
$this->serverContainer,
$this->crashReporterRegistry,
$this->eventDispatcher,
+ $this->searchComposer,
$this->logger
);
}