diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 2b2a4aead..4e1e21098 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -72,7 +72,7 @@ use OCA\Circles\Notification\Notifier; use OCA\Circles\Service\ConfigService; use OCA\Circles\Service\DavService; -use OCA\Circles\UnifiedSearch\UnifiedSearchProvider; +use OCA\Circles\Search\UnifiedSearchProvider; use OCP\App\ManagerEvent; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; @@ -90,8 +90,6 @@ use Symfony\Component\EventDispatcher\GenericEvent; use Throwable; -//use OCA\Files\App as FilesApp; - /** * Class Application * diff --git a/lib/Db/CoreQueryBuilder.php b/lib/Db/CoreQueryBuilder.php index 532d6e023..9bf406f18 100644 --- a/lib/Db/CoreQueryBuilder.php +++ b/lib/Db/CoreQueryBuilder.php @@ -391,9 +391,36 @@ public function filterCircle(Circle $circle): void { return; } + $expr = $this->expr(); + $orX = $expr->orX(); if ($circle->getDisplayName() !== '') { - $this->searchInDBField('display_name', '%' . $circle->getDisplayName() . '%'); + $andX = $expr->andX(); + foreach (explode(' ', $circle->getDisplayName()) as $word) { + $andX->add( + $expr->iLike( + $this->getDefaultSelectAlias() . '.' . 'display_name', + $this->createNamedParameter('%' . $word . '%') + ) + ); + } + $orX->add($andX); } + if ($circle->getDescription() !== '') { + $orDescription = $expr->orX(); + foreach (explode(' ', $circle->getDescription()) as $word) { + $orDescription->add( + $expr->iLike( + $this->getDefaultSelectAlias() . '.' . 'description', + $this->createNamedParameter('%' . $word . '%') + ) + ); + } + $orX->add($orDescription); + } + if ($orX->count() > 0) { + $this->andWhere($orX); + } + if ($circle->getSource() > 0) { $this->limitInt('source', $circle->getSource()); } diff --git a/lib/Search/UnifiedSearchProvider.php b/lib/Search/UnifiedSearchProvider.php new file mode 100644 index 000000000..d9aca0ed2 --- /dev/null +++ b/lib/Search/UnifiedSearchProvider.php @@ -0,0 +1,153 @@ + + * @copyright 2021 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Search; + +use Exception; +use OCA\Circles\Exceptions\ParseMemberLevelException; +use OCA\Circles\Model\Member; +use OCA\Circles\Service\FederatedUserService; +use OCA\Circles\Service\SearchService; +use OCP\IL10N; +use OCP\IUser; +use OCP\Search\IProvider; +use OCP\Search\ISearchQuery; +use OCP\Search\SearchResult; + +class UnifiedSearchProvider implements IProvider { + public const PROVIDER_ID = 'circles'; + public const ORDER = 9; + + + /** @var IL10N */ + private $l10n; + + /** @var FederatedUserService */ + private $federatedUserService; + + /** @var SearchService */ + private $searchService; + + + /** + * @param IL10N $l10n + * @param FederatedUserService $federatedUserService + * @param SearchService $searchService + */ + public function __construct( + IL10N $l10n, + FederatedUserService $federatedUserService, + SearchService $searchService + ) { + $this->l10n = $l10n; + $this->federatedUserService = $federatedUserService; + $this->searchService = $searchService; + } + + + /** + * return unique id of the provider + */ + public function getId(): string { + return self::PROVIDER_ID; + } + + + /** + * @return string + */ + public function getName(): string { + return $this->l10n->t('Circles'); + } + + + /** + * @param string $route + * @param array $routeParameters + * + * @return int + */ + public function getOrder(string $route, array $routeParameters): int { + return self::ORDER; + } + + + /** + * @param IUser $user + * @param ISearchQuery $query + * + * @return SearchResult + */ + public function search(IUser $user, ISearchQuery $query): SearchResult { + $term = $query->getTerm(); + $options = $this->extractOptions($term); + + $result = []; + try { + $this->federatedUserService->setLocalCurrentUser($user); + $result = $this->searchService->unifiedSearch($term, $options); + } catch (Exception $e) { + } + + return SearchResult::paginated( + $this->l10n->t('Circles'), + $result, + ($query->getCursor() ?? 0) + $query->getLimit() + ); + } + + + /** + * This is temporary, should be handled by core to extract Options from Term + * + * @param string $term + * + * @return array + */ + private function extractOptions(string &$term): array { + $new = $options = []; + foreach (explode(' ', $term) as $word) { + $word = trim($word); + if (strtolower(substr($word, 0, 3)) === 'is:') { + try { + $options['level'] = Member::parseLevelString(substr($word, 3)); + } catch (ParseMemberLevelException $e) { + } + } else { + $new[] = $word; + } + } + + $term = trim(implode(' ', $new)); + + return $options; + } +} diff --git a/lib/Search/UnifiedSearchResult.php b/lib/Search/UnifiedSearchResult.php new file mode 100644 index 000000000..eeec748d7 --- /dev/null +++ b/lib/Search/UnifiedSearchResult.php @@ -0,0 +1,173 @@ + + * @copyright 2021 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Search; + +use OCP\Search\SearchResultEntry; + +class UnifiedSearchResult extends SearchResultEntry { + + + /** + * UnifiedSearchResult constructor. + * + * @param string $thumbnailUrl + * @param string $title + * @param string $subline + * @param string $resourceUrl + * @param string $icon + * @param bool $rounded + */ + public function __construct( + string $thumbnailUrl = '', + string $title = '', + string $subline = '', + string $resourceUrl = '', + string $icon = '', + bool $rounded = false + ) { + parent::__construct($thumbnailUrl, $title, $subline, $resourceUrl, $icon, $rounded); + } + + + /** + * @return string + */ + public function getThumbnailUrl(): string { + return $this->thumbnailUrl; + } + + /** + * @param string $thumbnailUrl + * + * @return UnifiedSearchResult + */ + public function setThumbnailUrl(string $thumbnailUrl): self { + $this->thumbnailUrl = $thumbnailUrl; + + return $this; + } + + + /** + * @return string + */ + public function getTitle(): string { + return $this->title; + } + + /** + * @param string $title + * + * @return UnifiedSearchResult + */ + public function setTitle(string $title): self { + $this->title = $title; + + return $this; + } + + + /** + * @return string + */ + public function getSubline(): string { + return $this->subline; + } + + /** + * @param string $subline + * + * @return UnifiedSearchResult + */ + public function setSubline(string $subline): self { + $this->subline = $subline; + + return $this; + } + + + /** + * @return string + */ + public function getResourceUrl(): string { + return $this->resourceUrl; + } + + /** + * @param string $resourceUrl + * + * @return UnifiedSearchResult + */ + public function setResourceUrl(string $resourceUrl): self { + $this->resourceUrl = $resourceUrl; + + return $this; + } + + + /** + * @return string + */ + public function getIcon(): string { + return $this->icon; + } + + /** + * @param string $icon + * + * @return UnifiedSearchResult + */ + public function setIcon(string $icon): self { + $this->icon = $icon; + + return $this; + } + + + /** + * @return bool + */ + public function isRounded(): bool { + return $this->rounded; + } + + /** + * @param bool $rounded + * + * @return UnifiedSearchResult + */ + public function setRounded(bool $rounded): self { + $this->rounded = $rounded; + + return $this; + } +} diff --git a/lib/Service/SearchService.php b/lib/Service/SearchService.php index aaddf5992..0688f207b 100644 --- a/lib/Service/SearchService.php +++ b/lib/Service/SearchService.php @@ -32,26 +32,45 @@ namespace OCA\Circles\Service; use OC; +use OCA\Circles\AppInfo\Application; +use OCA\Circles\Exceptions\InitiatorNotFoundException; +use OCA\Circles\Exceptions\RequestBuilderException; use OCA\Circles\ISearch; +use OCA\Circles\Model\Circle; use OCA\Circles\Model\FederatedUser; +use OCA\Circles\Model\Member; +use OCA\Circles\Model\Probes\CircleProbe; use OCA\Circles\Search\FederatedUsers; +use OCA\Circles\Search\UnifiedSearchResult; +use OCA\Circles\Tools\Traits\TArrayTools; +use OCP\IURLGenerator; -/** - * Class SearchService - * - * @package OCA\Circles\Service - */ class SearchService { + use TArrayTools; + + public static $SERVICES = [ FederatedUsers::class ]; + /** @var IURLGenerator */ + private $urlGenerator; + + /** @var CircleService */ + private $circleService; + + /** - * SearchService constructor. - * + * @param IURLGenerator $urlGenerator + * @param CircleService $circleService */ - public function __construct() { + public function __construct( + IURLGenerator $urlGenerator, + CircleService $circleService + ) { + $this->urlGenerator = $urlGenerator; + $this->circleService = $circleService; } @@ -63,13 +82,82 @@ public function __construct() { public function search(string $needle): array { $result = []; - foreach (self::$SERVICES as $service) { + foreach (self::$SERVICES as $entry) { /** @var ISearch $service */ - $service = OC::$server->get($service); + $service = OC::$server->get($entry); $result = array_merge($result, $service->search($needle)); } return $result; } + + + /** + * @param string $term + * @param array $options + * + * @return UnifiedSearchResult[] + * @throws RequestBuilderException + */ + public function unifiedSearch(string $term, array $options): array { + $result = []; + $probe = $this->generateSearchProbe($term, $options); + + try { + $circles = $this->circleService->getCircles($probe); + } catch (InitiatorNotFoundException $e) { + return []; + } + + $iconPath = $this->urlGenerator->imagePath(Application::APP_ID, 'circles.svg'); + $icon = $this->urlGenerator->getAbsoluteURL($iconPath); + foreach ($circles as $circle) { + $result[] = new UnifiedSearchResult( + '', + $circle->getDisplayName(), + $circle->getDescription(), + $circle->getUrl(), + $icon + ); + } + + return $result; + } + + + /** + * @param string $term + * @param array $options + * + * @return CircleProbe + */ + private function generateSearchProbe(string $term, array $options): CircleProbe { + $probe = new CircleProbe(); + switch ($this->getInt('level', $options)) { + case Member::LEVEL_MEMBER: + $probe->mustBeMember(); + break; + case Member::LEVEL_MODERATOR: + $probe->mustBeModerator(); + break; + case Member::LEVEL_ADMIN: + $probe->mustBeAdmin(); + break; + case Member::LEVEL_OWNER: + $probe->mustBeOwner(); + break; + } + + $probe->filterHiddenCircles() + ->filterBackendCircles(); + + $circle = new Circle(); + $circle->setDisplayName($term) + ->setDescription($term); + + $probe->setFilterCircle($circle); + + return $probe; + } }