mirror of
https://github.com/nextcloud/server.git
synced 2026-05-28 04:32:30 -04:00
Merge pull request #40618 from nextcloud/feat/39162/advanced_search
Advanced search: backend allows multiples terms to search
This commit is contained in:
commit
fa761b51cc
37 changed files with 1423 additions and 300 deletions
|
|
@ -208,36 +208,22 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
|||
*/
|
||||
protected array $userDisplayNames;
|
||||
|
||||
private IDBConnection $db;
|
||||
private Backend $calendarSharingBackend;
|
||||
private Principal $principalBackend;
|
||||
private IUserManager $userManager;
|
||||
private ISecureRandom $random;
|
||||
private LoggerInterface $logger;
|
||||
private IEventDispatcher $dispatcher;
|
||||
private IConfig $config;
|
||||
private bool $legacyEndpoint;
|
||||
private string $dbObjectPropertiesTable = 'calendarobjects_props';
|
||||
private array $cachedObjects = [];
|
||||
|
||||
public function __construct(IDBConnection $db,
|
||||
Principal $principalBackend,
|
||||
IUserManager $userManager,
|
||||
IGroupManager $groupManager,
|
||||
ISecureRandom $random,
|
||||
LoggerInterface $logger,
|
||||
IEventDispatcher $dispatcher,
|
||||
IConfig $config,
|
||||
bool $legacyEndpoint = false) {
|
||||
$this->db = $db;
|
||||
$this->principalBackend = $principalBackend;
|
||||
$this->userManager = $userManager;
|
||||
public function __construct(
|
||||
private IDBConnection $db,
|
||||
private Principal $principalBackend,
|
||||
private IUserManager $userManager,
|
||||
IGroupManager $groupManager,
|
||||
private ISecureRandom $random,
|
||||
private LoggerInterface $logger,
|
||||
private IEventDispatcher $dispatcher,
|
||||
private IConfig $config,
|
||||
private bool $legacyEndpoint = false,
|
||||
) {
|
||||
$this->calendarSharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar');
|
||||
$this->random = $random;
|
||||
$this->logger = $logger;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->config = $config;
|
||||
$this->legacyEndpoint = $legacyEndpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1855,8 +1841,14 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
|||
*
|
||||
* @return array
|
||||
*/
|
||||
public function search(array $calendarInfo, $pattern, array $searchProperties,
|
||||
array $options, $limit, $offset) {
|
||||
public function search(
|
||||
array $calendarInfo,
|
||||
$pattern,
|
||||
array $searchProperties,
|
||||
array $options,
|
||||
$limit,
|
||||
$offset
|
||||
) {
|
||||
$outerQuery = $this->db->getQueryBuilder();
|
||||
$innerQuery = $this->db->getQueryBuilder();
|
||||
|
||||
|
|
@ -2074,11 +2066,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
|||
* @return array
|
||||
*/
|
||||
public function searchPrincipalUri(string $principalUri,
|
||||
string $pattern,
|
||||
array $componentTypes,
|
||||
array $searchProperties,
|
||||
array $searchParameters,
|
||||
array $options = []): array {
|
||||
string $pattern,
|
||||
array $componentTypes,
|
||||
array $searchProperties,
|
||||
array $searchParameters,
|
||||
array $options = []
|
||||
): array {
|
||||
return $this->atomic(function () use ($principalUri, $pattern, $componentTypes, $searchProperties, $searchParameters, $options) {
|
||||
$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
|
||||
|
||||
|
|
@ -2160,6 +2153,20 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
|||
if (isset($options['offset'])) {
|
||||
$calendarObjectIdQuery->setFirstResult($options['offset']);
|
||||
}
|
||||
if (isset($options['timerange'])) {
|
||||
if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
|
||||
$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->gt(
|
||||
'lastoccurence',
|
||||
$calendarObjectIdQuery->createNamedParameter($options['timerange']['start']->getTimeStamp()),
|
||||
));
|
||||
}
|
||||
if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
|
||||
$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->lt(
|
||||
'firstoccurence',
|
||||
$calendarObjectIdQuery->createNamedParameter($options['timerange']['end']->getTimeStamp()),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$result = $calendarObjectIdQuery->executeQuery();
|
||||
$matches = [];
|
||||
|
|
@ -3187,7 +3194,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
|||
$maxId = (int) $result->fetchOne();
|
||||
$result->closeCursor();
|
||||
if (!$maxId || $maxId < $keep) {
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
$query = $this->db->getQueryBuilder();
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ use OCP\EventDispatcher\IEventDispatcher;
|
|||
use OCP\IDBConnection;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IUserManager;
|
||||
use OC\Search\Filter\DateTimeFilter;
|
||||
use PDO;
|
||||
use Sabre\CardDAV\Backend\BackendInterface;
|
||||
use Sabre\CardDAV\Backend\SyncSupport;
|
||||
|
|
@ -1109,7 +1110,15 @@ class CardDavBackend implements BackendInterface, SyncSupport {
|
|||
* @param string $pattern
|
||||
* @param array $searchProperties
|
||||
* @param array $options
|
||||
* @psalm-param array{types?: bool, escape_like_param?: bool, limit?: int, offset?: int, wildcard?: bool} $options
|
||||
* @psalm-param array{
|
||||
* types?: bool,
|
||||
* escape_like_param?: bool,
|
||||
* limit?: int,
|
||||
* offset?: int,
|
||||
* wildcard?: bool,
|
||||
* since?: DateTimeFilter|null,
|
||||
* until?: DateTimeFilter|null,
|
||||
* } $options
|
||||
* @return array
|
||||
*/
|
||||
private function searchByAddressBookIds(array $addressBookIds,
|
||||
|
|
@ -1130,32 +1139,31 @@ class CardDavBackend implements BackendInterface, SyncSupport {
|
|||
return [];
|
||||
}
|
||||
|
||||
$propertyOr = $query2->expr()->orX();
|
||||
foreach ($searchProperties as $property) {
|
||||
if ($escapePattern) {
|
||||
if ($escapePattern) {
|
||||
$searchProperties = array_filter($searchProperties, function ($property) use ($pattern) {
|
||||
if ($property === 'EMAIL' && str_contains($pattern, ' ')) {
|
||||
// There can be no spaces in emails
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($property === 'CLOUD' && preg_match('/[^a-zA-Z0-9 :_.@\/\-\']/', $pattern) === 1) {
|
||||
// There can be no chars in cloud ids which are not valid for user ids plus :/
|
||||
// worst case: CA61590A-BBBC-423E-84AF-E6DF01455A53@https://my.nxt/srv/
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$propertyOr->add($query2->expr()->eq('cp.name', $query2->createNamedParameter($property)));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
if ($propertyOr->count() === 0) {
|
||||
if (empty($searchProperties)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query2->selectDistinct('cp.cardid')
|
||||
->from($this->dbCardsPropertiesTable, 'cp')
|
||||
->andWhere($addressBookOr)
|
||||
->andWhere($propertyOr);
|
||||
->andWhere($query2->expr()->in('cp.name', $query2->createNamedParameter($searchProperties, IQueryBuilder::PARAM_STR_ARRAY)));
|
||||
|
||||
// No need for like when the pattern is empty
|
||||
if ('' !== $pattern) {
|
||||
|
|
@ -1167,7 +1175,6 @@ class CardDavBackend implements BackendInterface, SyncSupport {
|
|||
$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($options['limit'])) {
|
||||
$query2->setMaxResults($options['limit']);
|
||||
}
|
||||
|
|
@ -1175,6 +1182,29 @@ class CardDavBackend implements BackendInterface, SyncSupport {
|
|||
$query2->setFirstResult($options['offset']);
|
||||
}
|
||||
|
||||
if (isset($options['since']) || isset($options['until'])) {
|
||||
$query2->join('cp', $this->dbCardsPropertiesTable, 'cp_bday', 'cp.cardid = cp_bday.cardid');
|
||||
$query2->andWhere($query2->expr()->eq('cp_bday.name', $query2->createNamedParameter('BDAY')));
|
||||
/**
|
||||
* FIXME Find a way to match only 4 last digits
|
||||
* BDAY can be --1018 without year or 20001019 with it
|
||||
* $bDayOr = $query2->expr()->orX();
|
||||
* if ($options['since'] instanceof DateTimeFilter) {
|
||||
* $bDayOr->add(
|
||||
* $query2->expr()->gte('SUBSTR(cp_bday.value, -4)',
|
||||
* $query2->createNamedParameter($options['since']->get()->format('md')))
|
||||
* );
|
||||
* }
|
||||
* if ($options['until'] instanceof DateTimeFilter) {
|
||||
* $bDayOr->add(
|
||||
* $query2->expr()->lte('SUBSTR(cp_bday.value, -4)',
|
||||
* $query2->createNamedParameter($options['until']->get()->format('md')))
|
||||
* );
|
||||
* }
|
||||
* $query2->andWhere($bDayOr);
|
||||
*/
|
||||
}
|
||||
|
||||
$result = $query2->execute();
|
||||
$matches = $result->fetchAll();
|
||||
$result->closeCursor();
|
||||
|
|
@ -1410,7 +1440,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
|
|||
$maxId = (int) $result->fetchOne();
|
||||
$result->closeCursor();
|
||||
if (!$maxId || $maxId < $keep) {
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
$query = $this->db->getQueryBuilder();
|
||||
|
|
|
|||
|
|
@ -32,31 +32,23 @@ use OCP\App\IAppManager;
|
|||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUser;
|
||||
use OCP\Search\IProvider;
|
||||
use OCP\Search\FilterDefinition;
|
||||
use OCP\Search\IFilteringProvider;
|
||||
use OCP\Search\ISearchQuery;
|
||||
use OCP\Search\SearchResult;
|
||||
use OCP\Search\SearchResultEntry;
|
||||
use Sabre\VObject\Component\VCard;
|
||||
use Sabre\VObject\Reader;
|
||||
|
||||
class ContactsSearchProvider implements IProvider {
|
||||
class ContactsSearchProvider implements IFilteringProvider {
|
||||
private static array $searchPropertiesRestricted = [
|
||||
'N',
|
||||
'FN',
|
||||
'NICKNAME',
|
||||
'EMAIL',
|
||||
];
|
||||
|
||||
/** @var IAppManager */
|
||||
private $appManager;
|
||||
|
||||
/** @var IL10N */
|
||||
private $l10n;
|
||||
|
||||
/** @var IURLGenerator */
|
||||
private $urlGenerator;
|
||||
|
||||
/** @var CardDavBackend */
|
||||
private $backend;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private static $searchProperties = [
|
||||
private static array $searchProperties = [
|
||||
'N',
|
||||
'FN',
|
||||
'NICKNAME',
|
||||
|
|
@ -68,22 +60,12 @@ class ContactsSearchProvider implements IProvider {
|
|||
'NOTE',
|
||||
];
|
||||
|
||||
/**
|
||||
* ContactsSearchProvider constructor.
|
||||
*
|
||||
* @param IAppManager $appManager
|
||||
* @param IL10N $l10n
|
||||
* @param IURLGenerator $urlGenerator
|
||||
* @param CardDavBackend $backend
|
||||
*/
|
||||
public function __construct(IAppManager $appManager,
|
||||
IL10N $l10n,
|
||||
IURLGenerator $urlGenerator,
|
||||
CardDavBackend $backend) {
|
||||
$this->appManager = $appManager;
|
||||
$this->l10n = $l10n;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->backend = $backend;
|
||||
public function __construct(
|
||||
private IAppManager $appManager,
|
||||
private IL10N $l10n,
|
||||
private IURLGenerator $urlGenerator,
|
||||
private CardDavBackend $backend,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -100,9 +82,6 @@ class ContactsSearchProvider implements IProvider {
|
|||
return $this->l10n->t('Contacts');
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getOrder(string $route, array $routeParameters): int {
|
||||
if ($route === 'contacts.Page.index') {
|
||||
return -1;
|
||||
|
|
@ -110,9 +89,6 @@ class ContactsSearchProvider implements IProvider {
|
|||
return 25;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function search(IUser $user, ISearchQuery $query): SearchResult {
|
||||
if (!$this->appManager->isEnabledForUser('contacts', $user)) {
|
||||
return SearchResult::complete($this->getName(), []);
|
||||
|
|
@ -127,12 +103,16 @@ class ContactsSearchProvider implements IProvider {
|
|||
|
||||
$searchResults = $this->backend->searchPrincipalUri(
|
||||
$principalUri,
|
||||
$query->getTerm(),
|
||||
self::$searchProperties,
|
||||
$query->getFilter('term')?->get() ?? '',
|
||||
$query->getFilter('title-only')?->get() ? self::$searchPropertiesRestricted : self::$searchProperties,
|
||||
[
|
||||
'limit' => $query->getLimit(),
|
||||
'offset' => $query->getCursor(),
|
||||
]
|
||||
'since' => $query->getFilter('since'),
|
||||
'until' => $query->getFilter('until'),
|
||||
'person' => $query->getFilter('person'),
|
||||
'company' => $query->getFilter('company'),
|
||||
],
|
||||
);
|
||||
$formattedResults = \array_map(function (array $contactRow) use ($addressBooksById):SearchResultEntry {
|
||||
$addressBook = $addressBooksById[$contactRow['addressbookid']];
|
||||
|
|
@ -158,15 +138,11 @@ class ContactsSearchProvider implements IProvider {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $principalUri
|
||||
* @param string $addressBookUri
|
||||
* @param string $contactsUri
|
||||
* @return string
|
||||
*/
|
||||
protected function getDavUrlForContact(string $principalUri,
|
||||
string $addressBookUri,
|
||||
string $contactsUri): string {
|
||||
protected function getDavUrlForContact(
|
||||
string $principalUri,
|
||||
string $addressBookUri,
|
||||
string $contactsUri,
|
||||
): string {
|
||||
[, $principalType, $principalId] = explode('/', $principalUri, 3);
|
||||
|
||||
return $this->urlGenerator->getAbsoluteURL(
|
||||
|
|
@ -178,13 +154,10 @@ class ContactsSearchProvider implements IProvider {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $addressBookUri
|
||||
* @param string $contactUid
|
||||
* @return string
|
||||
*/
|
||||
protected function getDeepLinkToContactsApp(string $addressBookUri,
|
||||
string $contactUid): string {
|
||||
protected function getDeepLinkToContactsApp(
|
||||
string $addressBookUri,
|
||||
string $contactUid,
|
||||
): string {
|
||||
return $this->urlGenerator->getAbsoluteURL(
|
||||
$this->urlGenerator->linkToRoute('contacts.contacts.direct', [
|
||||
'contact' => $contactUid . '~' . $addressBookUri
|
||||
|
|
@ -192,10 +165,6 @@ class ContactsSearchProvider implements IProvider {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param VCard $vCard
|
||||
* @return string
|
||||
*/
|
||||
protected function generateSubline(VCard $vCard): string {
|
||||
$emailAddresses = $vCard->select('EMAIL');
|
||||
if (!is_array($emailAddresses) || empty($emailAddresses)) {
|
||||
|
|
@ -204,4 +173,24 @@ class ContactsSearchProvider implements IProvider {
|
|||
|
||||
return (string)$emailAddresses[0];
|
||||
}
|
||||
|
||||
public function getSupportedFilters(): array {
|
||||
return [
|
||||
'term',
|
||||
'since',
|
||||
'until',
|
||||
'person',
|
||||
'title-only',
|
||||
];
|
||||
}
|
||||
|
||||
public function getAlternateIds(): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getCustomFilters(): array {
|
||||
return [
|
||||
new FilterDefinition('company'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ use Sabre\VObject\Property;
|
|||
* @package OCA\DAV\Search
|
||||
*/
|
||||
class EventsSearchProvider extends ACalendarSearchProvider {
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
|
|
@ -95,8 +94,10 @@ class EventsSearchProvider extends ACalendarSearchProvider {
|
|||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function search(IUser $user,
|
||||
ISearchQuery $query): SearchResult {
|
||||
public function search(
|
||||
IUser $user,
|
||||
ISearchQuery $query,
|
||||
): SearchResult {
|
||||
if (!$this->appManager->isEnabledForUser('calendar', $user)) {
|
||||
return SearchResult::complete($this->getName(), []);
|
||||
}
|
||||
|
|
@ -107,13 +108,17 @@ class EventsSearchProvider extends ACalendarSearchProvider {
|
|||
|
||||
$searchResults = $this->backend->searchPrincipalUri(
|
||||
$principalUri,
|
||||
$query->getTerm(),
|
||||
$query->getFilter('term')?->get() ?? '',
|
||||
[self::$componentType],
|
||||
self::$searchProperties,
|
||||
self::$searchParameters,
|
||||
[
|
||||
'limit' => $query->getLimit(),
|
||||
'offset' => $query->getCursor(),
|
||||
'timerange' => [
|
||||
'start' => $query->getFilter('since')?->get(),
|
||||
'end' => $query->getFilter('until')?->get(),
|
||||
],
|
||||
]
|
||||
);
|
||||
$formattedResults = \array_map(function (array $eventRow) use ($calendarsById, $subscriptionsById):SearchResultEntry {
|
||||
|
|
@ -138,15 +143,11 @@ class EventsSearchProvider extends ACalendarSearchProvider {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $principalUri
|
||||
* @param string $calendarUri
|
||||
* @param string $calendarObjectUri
|
||||
* @return string
|
||||
*/
|
||||
protected function getDeepLinkToCalendarApp(string $principalUri,
|
||||
string $calendarUri,
|
||||
string $calendarObjectUri): string {
|
||||
protected function getDeepLinkToCalendarApp(
|
||||
string $principalUri,
|
||||
string $calendarUri,
|
||||
string $calendarObjectUri,
|
||||
): string {
|
||||
$davUrl = $this->getDavUrlForCalendarObject($principalUri, $calendarUri, $calendarObjectUri);
|
||||
// This route will automatically figure out what recurrence-id to open
|
||||
return $this->urlGenerator->getAbsoluteURL(
|
||||
|
|
@ -156,15 +157,11 @@ class EventsSearchProvider extends ACalendarSearchProvider {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $principalUri
|
||||
* @param string $calendarUri
|
||||
* @param string $calendarObjectUri
|
||||
* @return string
|
||||
*/
|
||||
protected function getDavUrlForCalendarObject(string $principalUri,
|
||||
string $calendarUri,
|
||||
string $calendarObjectUri): string {
|
||||
protected function getDavUrlForCalendarObject(
|
||||
string $principalUri,
|
||||
string $calendarUri,
|
||||
string $calendarObjectUri
|
||||
): string {
|
||||
[,, $principalId] = explode('/', $principalUri, 3);
|
||||
|
||||
return $this->urlGenerator->linkTo('', 'remote.php') . '/dav/calendars/'
|
||||
|
|
@ -173,10 +170,6 @@ class EventsSearchProvider extends ACalendarSearchProvider {
|
|||
. $calendarObjectUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Component $eventComponent
|
||||
* @return string
|
||||
*/
|
||||
protected function generateSubline(Component $eventComponent): string {
|
||||
$dtStart = $eventComponent->DTSTART;
|
||||
$dtEnd = $this->getDTEndForEvent($eventComponent);
|
||||
|
|
@ -207,10 +200,6 @@ class EventsSearchProvider extends ACalendarSearchProvider {
|
|||
return "$formattedStartDate $formattedStartTime - $formattedEndDate $formattedEndTime";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Component $eventComponent
|
||||
* @return Property
|
||||
*/
|
||||
protected function getDTEndForEvent(Component $eventComponent):Property {
|
||||
if (isset($eventComponent->DTEND)) {
|
||||
$end = $eventComponent->DTEND;
|
||||
|
|
@ -233,13 +222,10 @@ class EventsSearchProvider extends ACalendarSearchProvider {
|
|||
return $end;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $dtStart
|
||||
* @param \DateTime $dtEnd
|
||||
* @return bool
|
||||
*/
|
||||
protected function isDayEqual(\DateTime $dtStart,
|
||||
\DateTime $dtEnd) {
|
||||
protected function isDayEqual(
|
||||
\DateTime $dtStart,
|
||||
\DateTime $dtEnd,
|
||||
): bool {
|
||||
return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ use Sabre\VObject\Component;
|
|||
* @package OCA\DAV\Search
|
||||
*/
|
||||
class TasksSearchProvider extends ACalendarSearchProvider {
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
|
|
@ -88,8 +87,10 @@ class TasksSearchProvider extends ACalendarSearchProvider {
|
|||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function search(IUser $user,
|
||||
ISearchQuery $query): SearchResult {
|
||||
public function search(
|
||||
IUser $user,
|
||||
ISearchQuery $query,
|
||||
): SearchResult {
|
||||
if (!$this->appManager->isEnabledForUser('tasks', $user)) {
|
||||
return SearchResult::complete($this->getName(), []);
|
||||
}
|
||||
|
|
@ -100,13 +101,15 @@ class TasksSearchProvider extends ACalendarSearchProvider {
|
|||
|
||||
$searchResults = $this->backend->searchPrincipalUri(
|
||||
$principalUri,
|
||||
$query->getTerm(),
|
||||
$query->getFilter('term')?->get() ?? '',
|
||||
[self::$componentType],
|
||||
self::$searchProperties,
|
||||
self::$searchParameters,
|
||||
[
|
||||
'limit' => $query->getLimit(),
|
||||
'offset' => $query->getCursor(),
|
||||
'since' => $query->getFilter('since'),
|
||||
'until' => $query->getFilter('until'),
|
||||
]
|
||||
);
|
||||
$formattedResults = \array_map(function (array $taskRow) use ($calendarsById, $subscriptionsById):SearchResultEntry {
|
||||
|
|
@ -131,13 +134,10 @@ class TasksSearchProvider extends ACalendarSearchProvider {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $calendarUri
|
||||
* @param string $taskUri
|
||||
* @return string
|
||||
*/
|
||||
protected function getDeepLinkToTasksApp(string $calendarUri,
|
||||
string $taskUri): string {
|
||||
protected function getDeepLinkToTasksApp(
|
||||
string $calendarUri,
|
||||
string $taskUri,
|
||||
): string {
|
||||
return $this->urlGenerator->getAbsoluteURL(
|
||||
$this->urlGenerator->linkToRoute('tasks.page.index')
|
||||
. '#/calendars/'
|
||||
|
|
@ -147,10 +147,6 @@ class TasksSearchProvider extends ACalendarSearchProvider {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Component $taskComponent
|
||||
* @return string
|
||||
*/
|
||||
protected function generateSubline(Component $taskComponent): string {
|
||||
if ($taskComponent->COMPLETED) {
|
||||
$completedDateTime = new \DateTime($taskComponent->COMPLETED->getDateTime()->format(\DateTimeInterface::ATOM));
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ class ContactsSearchProviderTest extends TestCase {
|
|||
]);
|
||||
$this->backend->expects($this->once())
|
||||
->method('searchPrincipalUri')
|
||||
->with('principals/users/john.doe', 'search term',
|
||||
->with('principals/users/john.doe', '',
|
||||
[
|
||||
'N',
|
||||
'FN',
|
||||
|
|
@ -171,7 +171,7 @@ class ContactsSearchProviderTest extends TestCase {
|
|||
'ORG',
|
||||
'NOTE',
|
||||
],
|
||||
['limit' => 5, 'offset' => 20])
|
||||
['limit' => 5, 'offset' => 20, 'since' => null, 'until' => null, 'person' => null, 'company' => null])
|
||||
->willReturn([
|
||||
[
|
||||
'addressbookid' => 99,
|
||||
|
|
|
|||
|
|
@ -328,10 +328,10 @@ class EventsSearchProviderTest extends TestCase {
|
|||
]);
|
||||
$this->backend->expects($this->once())
|
||||
->method('searchPrincipalUri')
|
||||
->with('principals/users/john.doe', 'search term', ['VEVENT'],
|
||||
->with('principals/users/john.doe', '', ['VEVENT'],
|
||||
['SUMMARY', 'LOCATION', 'DESCRIPTION', 'ATTENDEE', 'ORGANIZER', 'CATEGORIES'],
|
||||
['ATTENDEE' => ['CN'], 'ORGANIZER' => ['CN']],
|
||||
['limit' => 5, 'offset' => 20])
|
||||
['limit' => 5, 'offset' => 20, 'timerange' => ['start' => null, 'end' => null]])
|
||||
->willReturn([
|
||||
[
|
||||
'calendarid' => 99,
|
||||
|
|
|
|||
|
|
@ -213,10 +213,10 @@ class TasksSearchProviderTest extends TestCase {
|
|||
]);
|
||||
$this->backend->expects($this->once())
|
||||
->method('searchPrincipalUri')
|
||||
->with('principals/users/john.doe', 'search term', ['VTODO'],
|
||||
->with('principals/users/john.doe', '', ['VTODO'],
|
||||
['SUMMARY', 'DESCRIPTION', 'CATEGORIES'],
|
||||
[],
|
||||
['limit' => 5, 'offset' => 20])
|
||||
['limit' => 5, 'offset' => 20, 'since' => null, 'until' => null])
|
||||
->willReturn([
|
||||
[
|
||||
'calendarid' => 99,
|
||||
|
|
|
|||
|
|
@ -29,25 +29,32 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace OCA\Files\Search;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OCP\Files\Search\ISearchOperator;
|
||||
use OCP\Search\FilterDefinition;
|
||||
use OCP\Search\IFilter;
|
||||
use OCP\Search\IFilteringProvider;
|
||||
use OCP\Share\IShare;
|
||||
use OC\Files\Search\SearchBinaryOperator;
|
||||
use OC\Files\Search\SearchComparison;
|
||||
use OC\Files\Search\SearchOrder;
|
||||
use OC\Files\Search\SearchQuery;
|
||||
use OCP\Files\FileInfo;
|
||||
use OCP\Files\IMimeTypeDetector;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\Search\ISearchComparison;
|
||||
use OCP\Files\Node;
|
||||
use OCP\Files\Search\ISearchComparison;
|
||||
use OCP\Files\Search\ISearchOrder;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUser;
|
||||
use OCP\Search\IProvider;
|
||||
use OCP\Search\ISearchQuery;
|
||||
use OCP\Search\SearchResult;
|
||||
use OCP\Search\SearchResultEntry;
|
||||
use OC\Search\Filter\GroupFilter;
|
||||
use OC\Search\Filter\UserFilter;
|
||||
|
||||
class FilesSearchProvider implements IProvider {
|
||||
|
||||
class FilesSearchProvider implements IFilteringProvider {
|
||||
/** @var IL10N */
|
||||
private $l10n;
|
||||
|
||||
|
|
@ -97,21 +104,38 @@ class FilesSearchProvider implements IProvider {
|
|||
return 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getSupportedFilters(): array {
|
||||
return [
|
||||
'term',
|
||||
'since',
|
||||
'until',
|
||||
'person',
|
||||
'min-size',
|
||||
'max-size',
|
||||
'mime',
|
||||
'type',
|
||||
'is-favorite',
|
||||
'title-only',
|
||||
];
|
||||
}
|
||||
|
||||
public function getAlternateIds(): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getCustomFilters(): array {
|
||||
return [
|
||||
new FilterDefinition('min-size', FilterDefinition::TYPE_INT),
|
||||
new FilterDefinition('max-size', FilterDefinition::TYPE_INT),
|
||||
new FilterDefinition('mime', FilterDefinition::TYPE_STRING),
|
||||
new FilterDefinition('type', FilterDefinition::TYPE_STRING),
|
||||
new FilterDefinition('is-favorite', FilterDefinition::TYPE_BOOL),
|
||||
];
|
||||
}
|
||||
|
||||
public function search(IUser $user, ISearchQuery $query): SearchResult {
|
||||
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
|
||||
$fileQuery = new SearchQuery(
|
||||
new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $query->getTerm() . '%'),
|
||||
$query->getLimit(),
|
||||
(int)$query->getCursor(),
|
||||
$query->getSortOrder() === ISearchQuery::SORT_DATE_DESC ? [
|
||||
new SearchOrder(ISearchOrder::DIRECTION_DESCENDING, 'mtime'),
|
||||
] : [],
|
||||
$user
|
||||
);
|
||||
|
||||
$fileQuery = $this->buildSearchQuery($query, $user);
|
||||
return SearchResult::paginated(
|
||||
$this->l10n->t('Files'),
|
||||
array_map(function (Node $result) use ($userFolder) {
|
||||
|
|
@ -141,6 +165,53 @@ class FilesSearchProvider implements IProvider {
|
|||
);
|
||||
}
|
||||
|
||||
private function buildSearchQuery(ISearchQuery $query, IUser $user): SearchQuery {
|
||||
$comparisons = [];
|
||||
foreach ($query->getFilters() as $name => $filter) {
|
||||
$comparisons[] = match ($name) {
|
||||
'term' => new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $filter->get() . '%'),
|
||||
'since' => new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN_EQUAL, 'mtime', $filter->get()->getTimestamp()),
|
||||
'until' => new SearchComparison(ISearchComparison::COMPARE_LESS_THAN_EQUAL, 'mtime', $filter->get()->getTimestamp()),
|
||||
'min-size' => new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN_EQUAL, 'size', $filter->get()),
|
||||
'max-size' => new SearchComparison(ISearchComparison::COMPARE_LESS_THAN_EQUAL, 'size', $filter->get()),
|
||||
'mime' => new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $filter->get()),
|
||||
'type' => new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $filter->get() . '/%'),
|
||||
'person' => $this->buildPersonSearchQuery($filter),
|
||||
default => throw new InvalidArgumentException('Unsupported comparison'),
|
||||
};
|
||||
}
|
||||
|
||||
return new SearchQuery(
|
||||
new SearchBinaryOperator(SearchBinaryOperator::OPERATOR_AND, $comparisons),
|
||||
$query->getLimit(),
|
||||
(int) $query->getCursor(),
|
||||
$query->getSortOrder() === ISearchQuery::SORT_DATE_DESC
|
||||
? [new SearchOrder(ISearchOrder::DIRECTION_DESCENDING, 'mtime')]
|
||||
: [],
|
||||
$user
|
||||
);
|
||||
}
|
||||
|
||||
private function buildPersonSearchQuery(IFilter $person): ISearchOperator {
|
||||
if ($person instanceof UserFilter) {
|
||||
return new SearchBinaryOperator(SearchBinaryOperator::OPERATOR_OR, [
|
||||
new SearchBinaryOperator(SearchBinaryOperator::OPERATOR_AND, [
|
||||
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'share_with', $person->get()->getUID()),
|
||||
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'share_type', IShare::TYPE_USER),
|
||||
]),
|
||||
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'owner', $person->get()->getUID()),
|
||||
]);
|
||||
}
|
||||
if ($person instanceof GroupFilter) {
|
||||
return new SearchBinaryOperator(SearchBinaryOperator::OPERATOR_AND, [
|
||||
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'share_with', $person->get()->getGID()),
|
||||
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'share_type', IShare::TYPE_GROUP),
|
||||
]);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unsupported filter type');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format subline for files
|
||||
*
|
||||
|
|
|
|||
|
|
@ -31,14 +31,15 @@ namespace OC\Core\Controller;
|
|||
use OC\Search\SearchComposer;
|
||||
use OC\Search\SearchQuery;
|
||||
use OCA\Core\ResponseDefinitions;
|
||||
use OCP\AppFramework\OCSController;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\OCSController;
|
||||
use OCP\IRequest;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Route\IRouter;
|
||||
use OCP\Search\ISearchQuery;
|
||||
use OC\Search\UnsupportedFilter;
|
||||
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
|
||||
|
||||
/**
|
||||
|
|
@ -80,7 +81,10 @@ class UnifiedSearchController extends OCSController {
|
|||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
*
|
||||
* Search
|
||||
* Launch a search for a specific search provider.
|
||||
*
|
||||
* Additional filters are available for each provider.
|
||||
* Send a request to /providers endpoint to list providers with their available filters.
|
||||
*
|
||||
* @param string $providerId ID of the provider
|
||||
* @param string $term Term to search
|
||||
|
|
@ -89,28 +93,33 @@ class UnifiedSearchController extends OCSController {
|
|||
* @param int|string|null $cursor Offset for searching
|
||||
* @param string $from The current user URL
|
||||
*
|
||||
* @return DataResponse<Http::STATUS_OK, CoreUnifiedSearchResult, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, null, array{}>
|
||||
* @return DataResponse<Http::STATUS_OK, CoreUnifiedSearchResult, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, string, array{}>
|
||||
*
|
||||
* 200: Search entries returned
|
||||
* 400: Searching is not possible
|
||||
*/
|
||||
public function search(string $providerId,
|
||||
string $term = '',
|
||||
?int $sortOrder = null,
|
||||
?int $limit = null,
|
||||
$cursor = null,
|
||||
string $from = ''): DataResponse {
|
||||
if (trim($term) === "") {
|
||||
return new DataResponse(null, Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
public function search(
|
||||
string $providerId,
|
||||
// Unused parameter for OpenAPI spec generator
|
||||
string $term = '',
|
||||
?int $sortOrder = null,
|
||||
?int $limit = null,
|
||||
$cursor = null,
|
||||
string $from = '',
|
||||
): DataResponse {
|
||||
[$route, $routeParameters] = $this->getRouteInformation($from);
|
||||
|
||||
try {
|
||||
$filters = $this->composer->buildFilterList($providerId, $this->request->getParams());
|
||||
} catch (UnsupportedFilter $e) {
|
||||
return new DataResponse($e->getMessage(), Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
return new DataResponse(
|
||||
$this->composer->search(
|
||||
$this->userSession->getUser(),
|
||||
$providerId,
|
||||
new SearchQuery(
|
||||
$term,
|
||||
$filters,
|
||||
$sortOrder ?? ISearchQuery::SORT_DATE_DESC,
|
||||
$limit ?? SearchQuery::LIMIT_DEFAULT,
|
||||
$cursor,
|
||||
|
|
|
|||
|
|
@ -98,8 +98,12 @@ namespace OCA\Core;
|
|||
*
|
||||
* @psalm-type CoreUnifiedSearchProvider = array{
|
||||
* id: string,
|
||||
* appId: string,
|
||||
* name: string,
|
||||
* icon: string,
|
||||
* order: int,
|
||||
* triggers: string[],
|
||||
* filters: array<string, string>,
|
||||
* }
|
||||
*
|
||||
* @psalm-type CoreUnifiedSearchResultEntry = array{
|
||||
|
|
|
|||
|
|
@ -501,19 +501,41 @@
|
|||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"appId",
|
||||
"name",
|
||||
"order"
|
||||
"icon",
|
||||
"order",
|
||||
"triggers",
|
||||
"filters"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"appId": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"order": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"triggers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -4000,7 +4022,8 @@
|
|||
"/ocs/v2.php/search/providers/{providerId}/search": {
|
||||
"get": {
|
||||
"operationId": "unified_search-search",
|
||||
"summary": "Search",
|
||||
"summary": "Launch a search for a specific search provider.",
|
||||
"description": "Additional filters are available for each provider. Send a request to /providers endpoint to list providers with their available filters.",
|
||||
"tags": [
|
||||
"unified_search"
|
||||
],
|
||||
|
|
@ -4132,7 +4155,7 @@
|
|||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {
|
||||
"nullable": true
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ class InstalledVersions
|
|||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (isset($installed['versions'][$packageName])) {
|
||||
return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
|
||||
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -119,7 +119,7 @@ class InstalledVersions
|
|||
*/
|
||||
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
||||
{
|
||||
$constraint = $parser->parseConstraints($constraint);
|
||||
$constraint = $parser->parseConstraints((string) $constraint);
|
||||
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
||||
|
||||
return $provided->matches($constraint);
|
||||
|
|
@ -328,7 +328,9 @@ class InstalledVersions
|
|||
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir];
|
||||
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require $vendorDir.'/composer/installed.php';
|
||||
$installed[] = self::$installedByVendor[$vendorDir] = $required;
|
||||
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
|
||||
self::$installed = $installed[count($installed) - 1];
|
||||
}
|
||||
|
|
@ -340,12 +342,17 @@ class InstalledVersions
|
|||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
self::$installed = require __DIR__ . '/installed.php';
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require __DIR__ . '/installed.php';
|
||||
self::$installed = $required;
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
$installed[] = self::$installed;
|
||||
|
||||
if (self::$installed !== array()) {
|
||||
$installed[] = self::$installed;
|
||||
}
|
||||
|
||||
return $installed;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -589,6 +589,10 @@ return array(
|
|||
'OCP\\Route\\IRouter' => $baseDir . '/lib/public/Route/IRouter.php',
|
||||
'OCP\\SabrePluginEvent' => $baseDir . '/lib/public/SabrePluginEvent.php',
|
||||
'OCP\\SabrePluginException' => $baseDir . '/lib/public/SabrePluginException.php',
|
||||
'OCP\\Search\\FilterDefinition' => $baseDir . '/lib/public/Search/FilterDefinition.php',
|
||||
'OCP\\Search\\IFilter' => $baseDir . '/lib/public/Search/IFilter.php',
|
||||
'OCP\\Search\\IFilterCollection' => $baseDir . '/lib/public/Search/IFilterCollection.php',
|
||||
'OCP\\Search\\IFilteringProvider' => $baseDir . '/lib/public/Search/IFilteringProvider.php',
|
||||
'OCP\\Search\\IProvider' => $baseDir . '/lib/public/Search/IProvider.php',
|
||||
'OCP\\Search\\ISearchQuery' => $baseDir . '/lib/public/Search/ISearchQuery.php',
|
||||
'OCP\\Search\\PagedProvider' => $baseDir . '/lib/public/Search/PagedProvider.php',
|
||||
|
|
@ -1641,6 +1645,16 @@ return array(
|
|||
'OC\\Route\\Route' => $baseDir . '/lib/private/Route/Route.php',
|
||||
'OC\\Route\\Router' => $baseDir . '/lib/private/Route/Router.php',
|
||||
'OC\\Search' => $baseDir . '/lib/private/Search.php',
|
||||
'OC\\Search\\FilterCollection' => $baseDir . '/lib/private/Search/FilterCollection.php',
|
||||
'OC\\Search\\FilterFactory' => $baseDir . '/lib/private/Search/FilterFactory.php',
|
||||
'OC\\Search\\Filter\\BooleanFilter' => $baseDir . '/lib/private/Search/Filter/BooleanFilter.php',
|
||||
'OC\\Search\\Filter\\DateTimeFilter' => $baseDir . '/lib/private/Search/Filter/DateTimeFilter.php',
|
||||
'OC\\Search\\Filter\\FloatFilter' => $baseDir . '/lib/private/Search/Filter/FloatFilter.php',
|
||||
'OC\\Search\\Filter\\GroupFilter' => $baseDir . '/lib/private/Search/Filter/GroupFilter.php',
|
||||
'OC\\Search\\Filter\\IntegerFilter' => $baseDir . '/lib/private/Search/Filter/IntegerFilter.php',
|
||||
'OC\\Search\\Filter\\StringFilter' => $baseDir . '/lib/private/Search/Filter/StringFilter.php',
|
||||
'OC\\Search\\Filter\\StringsFilter' => $baseDir . '/lib/private/Search/Filter/StringsFilter.php',
|
||||
'OC\\Search\\Filter\\UserFilter' => $baseDir . '/lib/private/Search/Filter/UserFilter.php',
|
||||
'OC\\Search\\Provider\\File' => $baseDir . '/lib/private/Search/Provider/File.php',
|
||||
'OC\\Search\\Result\\Audio' => $baseDir . '/lib/private/Search/Result/Audio.php',
|
||||
'OC\\Search\\Result\\File' => $baseDir . '/lib/private/Search/Result/File.php',
|
||||
|
|
@ -1648,6 +1662,7 @@ return array(
|
|||
'OC\\Search\\Result\\Image' => $baseDir . '/lib/private/Search/Result/Image.php',
|
||||
'OC\\Search\\SearchComposer' => $baseDir . '/lib/private/Search/SearchComposer.php',
|
||||
'OC\\Search\\SearchQuery' => $baseDir . '/lib/private/Search/SearchQuery.php',
|
||||
'OC\\Search\\UnsupportedFilter' => $baseDir . '/lib/private/Search/UnsupportedFilter.php',
|
||||
'OC\\Security\\Bruteforce\\Backend\\DatabaseBackend' => $baseDir . '/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php',
|
||||
'OC\\Security\\Bruteforce\\Backend\\IBackend' => $baseDir . '/lib/private/Security/Bruteforce/Backend/IBackend.php',
|
||||
'OC\\Security\\Bruteforce\\Backend\\MemoryCacheBackend' => $baseDir . '/lib/private/Security/Bruteforce/Backend/MemoryCacheBackend.php',
|
||||
|
|
|
|||
|
|
@ -622,6 +622,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
|
|||
'OCP\\Route\\IRouter' => __DIR__ . '/../../..' . '/lib/public/Route/IRouter.php',
|
||||
'OCP\\SabrePluginEvent' => __DIR__ . '/../../..' . '/lib/public/SabrePluginEvent.php',
|
||||
'OCP\\SabrePluginException' => __DIR__ . '/../../..' . '/lib/public/SabrePluginException.php',
|
||||
'OCP\\Search\\FilterDefinition' => __DIR__ . '/../../..' . '/lib/public/Search/FilterDefinition.php',
|
||||
'OCP\\Search\\IFilter' => __DIR__ . '/../../..' . '/lib/public/Search/IFilter.php',
|
||||
'OCP\\Search\\IFilterCollection' => __DIR__ . '/../../..' . '/lib/public/Search/IFilterCollection.php',
|
||||
'OCP\\Search\\IFilteringProvider' => __DIR__ . '/../../..' . '/lib/public/Search/IFilteringProvider.php',
|
||||
'OCP\\Search\\IProvider' => __DIR__ . '/../../..' . '/lib/public/Search/IProvider.php',
|
||||
'OCP\\Search\\ISearchQuery' => __DIR__ . '/../../..' . '/lib/public/Search/ISearchQuery.php',
|
||||
'OCP\\Search\\PagedProvider' => __DIR__ . '/../../..' . '/lib/public/Search/PagedProvider.php',
|
||||
|
|
@ -1674,6 +1678,16 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
|
|||
'OC\\Route\\Route' => __DIR__ . '/../../..' . '/lib/private/Route/Route.php',
|
||||
'OC\\Route\\Router' => __DIR__ . '/../../..' . '/lib/private/Route/Router.php',
|
||||
'OC\\Search' => __DIR__ . '/../../..' . '/lib/private/Search.php',
|
||||
'OC\\Search\\FilterCollection' => __DIR__ . '/../../..' . '/lib/private/Search/FilterCollection.php',
|
||||
'OC\\Search\\FilterFactory' => __DIR__ . '/../../..' . '/lib/private/Search/FilterFactory.php',
|
||||
'OC\\Search\\Filter\\BooleanFilter' => __DIR__ . '/../../..' . '/lib/private/Search/Filter/BooleanFilter.php',
|
||||
'OC\\Search\\Filter\\DateTimeFilter' => __DIR__ . '/../../..' . '/lib/private/Search/Filter/DateTimeFilter.php',
|
||||
'OC\\Search\\Filter\\FloatFilter' => __DIR__ . '/../../..' . '/lib/private/Search/Filter/FloatFilter.php',
|
||||
'OC\\Search\\Filter\\GroupFilter' => __DIR__ . '/../../..' . '/lib/private/Search/Filter/GroupFilter.php',
|
||||
'OC\\Search\\Filter\\IntegerFilter' => __DIR__ . '/../../..' . '/lib/private/Search/Filter/IntegerFilter.php',
|
||||
'OC\\Search\\Filter\\StringFilter' => __DIR__ . '/../../..' . '/lib/private/Search/Filter/StringFilter.php',
|
||||
'OC\\Search\\Filter\\StringsFilter' => __DIR__ . '/../../..' . '/lib/private/Search/Filter/StringsFilter.php',
|
||||
'OC\\Search\\Filter\\UserFilter' => __DIR__ . '/../../..' . '/lib/private/Search/Filter/UserFilter.php',
|
||||
'OC\\Search\\Provider\\File' => __DIR__ . '/../../..' . '/lib/private/Search/Provider/File.php',
|
||||
'OC\\Search\\Result\\Audio' => __DIR__ . '/../../..' . '/lib/private/Search/Result/Audio.php',
|
||||
'OC\\Search\\Result\\File' => __DIR__ . '/../../..' . '/lib/private/Search/Result/File.php',
|
||||
|
|
@ -1681,6 +1695,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
|
|||
'OC\\Search\\Result\\Image' => __DIR__ . '/../../..' . '/lib/private/Search/Result/Image.php',
|
||||
'OC\\Search\\SearchComposer' => __DIR__ . '/../../..' . '/lib/private/Search/SearchComposer.php',
|
||||
'OC\\Search\\SearchQuery' => __DIR__ . '/../../..' . '/lib/private/Search/SearchQuery.php',
|
||||
'OC\\Search\\UnsupportedFilter' => __DIR__ . '/../../..' . '/lib/private/Search/UnsupportedFilter.php',
|
||||
'OC\\Security\\Bruteforce\\Backend\\DatabaseBackend' => __DIR__ . '/../../..' . '/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php',
|
||||
'OC\\Security\\Bruteforce\\Backend\\IBackend' => __DIR__ . '/../../..' . '/lib/private/Security/Bruteforce/Backend/IBackend.php',
|
||||
'OC\\Security\\Bruteforce\\Backend\\MemoryCacheBackend' => __DIR__ . '/../../..' . '/lib/private/Security/Bruteforce/Backend/MemoryCacheBackend.php',
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
<?php return array(
|
||||
'root' => array(
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'name' => '__root__',
|
||||
'pretty_version' => 'dev-master',
|
||||
'version' => 'dev-master',
|
||||
'reference' => '41d274cd58f168047eb6a7673a7e43fff69ac07f',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../../',
|
||||
'aliases' => array(),
|
||||
'reference' => NULL,
|
||||
'name' => '__root__',
|
||||
'dev' => false,
|
||||
),
|
||||
'versions' => array(
|
||||
'__root__' => array(
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'pretty_version' => 'dev-master',
|
||||
'version' => 'dev-master',
|
||||
'reference' => '41d274cd58f168047eb6a7673a7e43fff69ac07f',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../../',
|
||||
'aliases' => array(),
|
||||
'reference' => NULL,
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ class CacheQueryBuilder extends QueryBuilder {
|
|||
public function selectFileCache(string $alias = null, bool $joinExtendedCache = true) {
|
||||
$name = $alias ?: 'filecache';
|
||||
$this->select("$name.fileid", 'storage', 'path', 'path_hash', "$name.parent", "$name.name", 'mimetype', 'mimepart', 'size', 'mtime',
|
||||
'storage_mtime', 'encrypted', 'etag', 'permissions', 'checksum', 'unencrypted_size')
|
||||
'storage_mtime', 'encrypted', 'etag', "$name.permissions", 'checksum', 'unencrypted_size')
|
||||
->from('filecache', $name);
|
||||
|
||||
if ($joinExtendedCache) {
|
||||
|
|
|
|||
|
|
@ -134,6 +134,11 @@ class QuerySearchHelper {
|
|||
));
|
||||
}
|
||||
|
||||
|
||||
protected function equipQueryForShares(CacheQueryBuilder $query): void {
|
||||
$query->join('file', 'share', 's', $query->expr()->eq('file.fileid', 's.file_source'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a file system search in multiple caches
|
||||
*
|
||||
|
|
@ -172,6 +177,9 @@ class QuerySearchHelper {
|
|||
if (in_array('tagname', $requestedFields) || in_array('favorite', $requestedFields)) {
|
||||
$this->equipQueryForDavTags($query, $this->requireUser($searchQuery));
|
||||
}
|
||||
if (in_array('owner', $requestedFields) || in_array('share_with', $requestedFields) || in_array('share_type', $requestedFields)) {
|
||||
$this->equipQueryForShares($query);
|
||||
}
|
||||
|
||||
$metadataQuery = $query->selectMetadata();
|
||||
|
||||
|
|
|
|||
|
|
@ -192,6 +192,8 @@ class SearchBuilder {
|
|||
} elseif ($field === 'path' && $type === ISearchComparison::COMPARE_EQUAL && $operator->getQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, true)) {
|
||||
$field = 'path_hash';
|
||||
$value = md5((string)$value);
|
||||
} elseif ($field === 'owner') {
|
||||
$field = 'uid_owner';
|
||||
}
|
||||
return [$field, $value, $type];
|
||||
}
|
||||
|
|
@ -208,6 +210,9 @@ class SearchBuilder {
|
|||
'favorite' => 'boolean',
|
||||
'fileid' => 'integer',
|
||||
'storage' => 'integer',
|
||||
'share_with' => 'string',
|
||||
'share_type' => 'integer',
|
||||
'owner' => 'string',
|
||||
];
|
||||
$comparisons = [
|
||||
'mimetype' => ['eq', 'like'],
|
||||
|
|
@ -220,6 +225,9 @@ class SearchBuilder {
|
|||
'favorite' => ['eq'],
|
||||
'fileid' => ['eq'],
|
||||
'storage' => ['eq'],
|
||||
'share_with' => ['eq'],
|
||||
'share_type' => ['eq'],
|
||||
'owner' => ['eq'],
|
||||
];
|
||||
|
||||
if (!isset($types[$operator->getField()])) {
|
||||
|
|
|
|||
46
lib/private/Search/Filter/BooleanFilter.php
Normal file
46
lib/private/Search/Filter/BooleanFilter.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OC\Search\Filter;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OCP\Search\IFilter;
|
||||
|
||||
class BooleanFilter implements IFilter {
|
||||
private bool $value;
|
||||
|
||||
public function __construct(string $value) {
|
||||
$this->value = match ($value) {
|
||||
'true', 'yes', 'y', '1' => true,
|
||||
'false', 'no', 'n', '0', '' => false,
|
||||
default => throw new InvalidArgumentException('Invalid boolean value '. $value),
|
||||
};
|
||||
}
|
||||
|
||||
public function get(): bool {
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
46
lib/private/Search/Filter/DateTimeFilter.php
Normal file
46
lib/private/Search/Filter/DateTimeFilter.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OC\Search\Filter;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use OCP\Search\IFilter;
|
||||
|
||||
class DateTimeFilter implements IFilter {
|
||||
private DateTimeImmutable $value;
|
||||
|
||||
public function __construct(string $value) {
|
||||
if (filter_var($value, FILTER_VALIDATE_INT)) {
|
||||
$value = '@'.$value;
|
||||
}
|
||||
|
||||
$this->value = new DateTimeImmutable($value);
|
||||
}
|
||||
|
||||
public function get(): DateTimeImmutable {
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
45
lib/private/Search/Filter/FloatFilter.php
Normal file
45
lib/private/Search/Filter/FloatFilter.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OC\Search\Filter;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OCP\Search\IFilter;
|
||||
|
||||
class FloatFilter implements IFilter {
|
||||
private float $value;
|
||||
|
||||
public function __construct(string $value) {
|
||||
$this->value = filter_var($value, FILTER_VALIDATE_FLOAT);
|
||||
if ($this->value === false) {
|
||||
throw new InvalidArgumentException('Invalid float value '. $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function get(): float {
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
50
lib/private/Search/Filter/GroupFilter.php
Normal file
50
lib/private/Search/Filter/GroupFilter.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OC\Search\Filter;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OCP\IGroup;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\Search\IFilter;
|
||||
|
||||
class GroupFilter implements IFilter {
|
||||
private IGroup $group;
|
||||
|
||||
public function __construct(
|
||||
string $value,
|
||||
IGroupManager $groupManager,
|
||||
) {
|
||||
$this->group = $groupManager->get($value);
|
||||
if ($this->group === null) {
|
||||
throw new InvalidArgumentException('Group '.$value.' not found');
|
||||
}
|
||||
}
|
||||
|
||||
public function get(): IGroup {
|
||||
return $this->group;
|
||||
}
|
||||
}
|
||||
45
lib/private/Search/Filter/IntegerFilter.php
Normal file
45
lib/private/Search/Filter/IntegerFilter.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OC\Search\Filter;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OCP\Search\IFilter;
|
||||
|
||||
class IntegerFilter implements IFilter {
|
||||
private int $value;
|
||||
|
||||
public function __construct(string $value) {
|
||||
$this->value = filter_var($value, FILTER_VALIDATE_INT);
|
||||
if ($this->value === false) {
|
||||
throw new InvalidArgumentException('Invalid integer value '. $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function get(): int {
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
44
lib/private/Search/Filter/StringFilter.php
Normal file
44
lib/private/Search/Filter/StringFilter.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OC\Search\Filter;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OCP\Search\IFilter;
|
||||
|
||||
class StringFilter implements IFilter {
|
||||
public function __construct(
|
||||
private string $value,
|
||||
) {
|
||||
if ($value === '') {
|
||||
throw new InvalidArgumentException('String filter can’t be empty');
|
||||
}
|
||||
}
|
||||
|
||||
public function get(): string {
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
51
lib/private/Search/Filter/StringsFilter.php
Normal file
51
lib/private/Search/Filter/StringsFilter.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OC\Search\Filter;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OCP\Search\IFilter;
|
||||
|
||||
class StringsFilter implements IFilter {
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $values;
|
||||
|
||||
public function __construct(string ...$values) {
|
||||
$this->values = array_unique(array_filter($values));
|
||||
if (empty($this->values)) {
|
||||
throw new InvalidArgumentException('Strings filter can’t be empty');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function get(): array {
|
||||
return $this->values;
|
||||
}
|
||||
}
|
||||
50
lib/private/Search/Filter/UserFilter.php
Normal file
50
lib/private/Search/Filter/UserFilter.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OC\Search\Filter;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use OCP\Search\IFilter;
|
||||
|
||||
class UserFilter implements IFilter {
|
||||
private IUser $user;
|
||||
|
||||
public function __construct(
|
||||
string $value,
|
||||
IUserManager $userManager,
|
||||
) {
|
||||
$this->user = $userManager->get($value);
|
||||
if ($this->user === null) {
|
||||
throw new InvalidArgumentException('User '.$value.' not found');
|
||||
}
|
||||
}
|
||||
|
||||
public function get(): IUser {
|
||||
return $this->user;
|
||||
}
|
||||
}
|
||||
60
lib/private/Search/FilterCollection.php
Normal file
60
lib/private/Search/FilterCollection.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
namespace OC\Search;
|
||||
|
||||
use Generator;
|
||||
use OCP\Search\IFilterCollection;
|
||||
use OCP\Search\IFilter;
|
||||
|
||||
/**
|
||||
* Interface for search filters
|
||||
*
|
||||
* @since 28.0.0
|
||||
*/
|
||||
class FilterCollection implements IFilterCollection {
|
||||
/**
|
||||
* @var IFilter[]
|
||||
*/
|
||||
private array $filters;
|
||||
|
||||
public function __construct(IFilter ...$filters) {
|
||||
$this->filters = $filters;
|
||||
}
|
||||
|
||||
public function has(string $name): bool {
|
||||
return isset($this->filters[$name]);
|
||||
}
|
||||
|
||||
public function get(string $name): ?IFilter {
|
||||
return $this->filters[$name] ?? null;
|
||||
}
|
||||
|
||||
public function getIterator(): Generator {
|
||||
foreach ($this->filters as $k => $v) {
|
||||
yield $k => $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
lib/private/Search/FilterFactory.php
Normal file
58
lib/private/Search/FilterFactory.php
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
namespace OC\Search;
|
||||
|
||||
use OCP\Search\FilterDefinition;
|
||||
use OCP\Search\IFilter;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IUserManager;
|
||||
use RuntimeException;
|
||||
|
||||
final class FilterFactory {
|
||||
public static function get(string $type, string|array $filter): IFilter {
|
||||
return match ($type) {
|
||||
FilterDefinition::TYPE_BOOL => new Filter\BooleanFilter($filter),
|
||||
FilterDefinition::TYPE_DATETIME => new Filter\DateTimeFilter($filter),
|
||||
FilterDefinition::TYPE_FLOAT => new Filter\FloatFilter($filter),
|
||||
FilterDefinition::TYPE_INT => new Filter\IntegerFilter($filter),
|
||||
FilterDefinition::TYPE_NC_GROUP => new Filter\GroupFilter($filter, \OC::$server->get(IGroupManager::class)),
|
||||
FilterDefinition::TYPE_NC_USER => new Filter\UserFilter($filter, \OC::$server->get(IUserManager::class)),
|
||||
FilterDefinition::TYPE_PERSON => self::getPerson($filter),
|
||||
FilterDefinition::TYPE_STRING => new Filter\StringFilter($filter),
|
||||
FilterDefinition::TYPE_STRINGS => new Filter\StringsFilter(... (array) $filter),
|
||||
default => throw new RuntimeException('Invalid filter type '. $type),
|
||||
};
|
||||
}
|
||||
|
||||
private static function getPerson(string $person): IFilter {
|
||||
$parts = explode('_', $person, 2);
|
||||
|
||||
return match (count($parts)) {
|
||||
1 => self::get(FilterDefinition::TYPE_NC_USER, $person),
|
||||
2 => self::get(... $parts),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -28,14 +28,19 @@ declare(strict_types=1);
|
|||
namespace OC\Search;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OCP\AppFramework\QueryException;
|
||||
use OCP\IServerContainer;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Search\FilterDefinition;
|
||||
use OCP\Search\IFilteringProvider;
|
||||
use OC\AppFramework\Bootstrap\Coordinator;
|
||||
use OCP\IUser;
|
||||
use OCP\Search\IFilter;
|
||||
use OCP\Search\IProvider;
|
||||
use OCP\Search\ISearchQuery;
|
||||
use OCP\Search\SearchResult;
|
||||
use OC\AppFramework\Bootstrap\Coordinator;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use function array_map;
|
||||
|
||||
/**
|
||||
|
|
@ -58,31 +63,40 @@ use function array_map;
|
|||
* @see IProvider::search() for the arguments of the individual search requests
|
||||
*/
|
||||
class SearchComposer {
|
||||
/** @var IProvider[] */
|
||||
private $providers = [];
|
||||
/**
|
||||
* @var array<string, array{appId: string, provider: IProvider}>
|
||||
*/
|
||||
private array $providers = [];
|
||||
|
||||
/** @var Coordinator */
|
||||
private $bootstrapCoordinator;
|
||||
private array $commonFilters;
|
||||
private array $customFilters = [];
|
||||
|
||||
/** @var IServerContainer */
|
||||
private $container;
|
||||
private array $handlers = [];
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(Coordinator $bootstrapCoordinator,
|
||||
IServerContainer $container,
|
||||
LoggerInterface $logger) {
|
||||
$this->container = $container;
|
||||
$this->logger = $logger;
|
||||
$this->bootstrapCoordinator = $bootstrapCoordinator;
|
||||
public function __construct(
|
||||
private Coordinator $bootstrapCoordinator,
|
||||
private ContainerInterface $container,
|
||||
private IURLGenerator $urlGenerator,
|
||||
private LoggerInterface $logger
|
||||
) {
|
||||
$this->commonFilters = [
|
||||
'term' => new FilterDefinition('term', FilterDefinition::TYPE_STRING),
|
||||
'since' => new FilterDefinition('since', FilterDefinition::TYPE_DATETIME),
|
||||
'until' => new FilterDefinition('until', FilterDefinition::TYPE_DATETIME),
|
||||
'title-only' => new FilterDefinition('title-only', FilterDefinition::TYPE_BOOL, false),
|
||||
'person' => new FilterDefinition('person', FilterDefinition::TYPE_PERSON),
|
||||
'places' => new FilterDefinition('places', FilterDefinition::TYPE_STRINGS, false),
|
||||
'provider' => new FilterDefinition('provider', FilterDefinition::TYPE_STRING, false),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all providers dynamically that were registered through `registerProvider`
|
||||
*
|
||||
* If $targetProviderId is provided, only this provider is loaded
|
||||
* If a provider can't be loaded we log it but the operation continues nevertheless
|
||||
*/
|
||||
private function loadLazyProviders(): void {
|
||||
private function loadLazyProviders(?string $targetProviderId = null): void {
|
||||
$context = $this->bootstrapCoordinator->getRegistrationContext();
|
||||
if ($context === null) {
|
||||
// Too early, nothing registered yet
|
||||
|
|
@ -93,9 +107,20 @@ class SearchComposer {
|
|||
foreach ($registrations as $registration) {
|
||||
try {
|
||||
/** @var IProvider $provider */
|
||||
$provider = $this->container->query($registration->getService());
|
||||
$this->providers[$provider->getId()] = $provider;
|
||||
} catch (QueryException $e) {
|
||||
$provider = $this->container->get($registration->getService());
|
||||
$providerId = $provider->getId();
|
||||
if ($targetProviderId !== null && $targetProviderId !== $providerId) {
|
||||
continue;
|
||||
}
|
||||
$this->providers[$providerId] = [
|
||||
'appId' => $registration->getAppId(),
|
||||
'provider' => $provider,
|
||||
];
|
||||
$this->handlers[$providerId] = [$providerId];
|
||||
if ($targetProviderId !== null) {
|
||||
break;
|
||||
}
|
||||
} catch (ContainerExceptionInterface $e) {
|
||||
// Log an continue. We can be fault tolerant here.
|
||||
$this->logger->error('Could not load search provider dynamically: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
|
|
@ -103,6 +128,43 @@ class SearchComposer {
|
|||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->loadFilters();
|
||||
}
|
||||
|
||||
private function loadFilters(): void {
|
||||
foreach ($this->providers as $providerId => $providerData) {
|
||||
$appId = $providerData['appId'];
|
||||
$provider = $providerData['provider'];
|
||||
if (!$provider instanceof IFilteringProvider) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($provider->getCustomFilters() as $filter) {
|
||||
$this->registerCustomFilter($filter, $providerId);
|
||||
}
|
||||
foreach ($provider->getAlternateIds() as $alternateId) {
|
||||
$this->handlers[$alternateId][] = $providerId;
|
||||
}
|
||||
foreach ($provider->getSupportedFilters() as $filterName) {
|
||||
if ($this->getFilterDefinition($filterName, $providerId) === null) {
|
||||
throw new InvalidArgumentException('Invalid filter '. $filterName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function registerCustomFilter(FilterDefinition $filter, string $providerId): void {
|
||||
$name = $filter->name();
|
||||
if (isset($this->commonFilters[$name])) {
|
||||
throw new InvalidArgumentException('Filter name is already used');
|
||||
}
|
||||
|
||||
if (isset($this->customFilters[$providerId])) {
|
||||
$this->customFilters[$providerId][$name] = $filter;
|
||||
} else {
|
||||
$this->customFilters[$providerId] = [$name => $filter];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -117,26 +179,134 @@ class SearchComposer {
|
|||
public function getProviders(string $route, array $routeParameters): array {
|
||||
$this->loadLazyProviders();
|
||||
|
||||
$providers = array_values(
|
||||
array_map(function (IProvider $provider) use ($route, $routeParameters) {
|
||||
$providers = array_map(
|
||||
function (array $providerData) use ($route, $routeParameters) {
|
||||
$appId = $providerData['appId'];
|
||||
$provider = $providerData['provider'];
|
||||
$triggers = [$provider->getId()];
|
||||
if ($provider instanceof IFilteringProvider) {
|
||||
$triggers += $provider->getAlternateIds();
|
||||
$filters = $provider->getSupportedFilters();
|
||||
} else {
|
||||
$filters = ['term'];
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $provider->getId(),
|
||||
'appId' => $appId,
|
||||
'name' => $provider->getName(),
|
||||
'icon' => $this->fetchIcon($appId, $provider->getId()),
|
||||
'order' => $provider->getOrder($route, $routeParameters),
|
||||
'triggers' => $triggers,
|
||||
'filters' => $this->getFiltersType($filters, $provider->getId()),
|
||||
];
|
||||
}, $this->providers)
|
||||
},
|
||||
$this->providers,
|
||||
);
|
||||
|
||||
// Sort providers by order and strip associative keys
|
||||
usort($providers, function ($provider1, $provider2) {
|
||||
return $provider1['order'] <=> $provider2['order'];
|
||||
});
|
||||
|
||||
/**
|
||||
* Return an array with the IDs, but strip the associative keys
|
||||
*/
|
||||
return $providers;
|
||||
}
|
||||
|
||||
private function fetchIcon(string $appId, string $providerId): string {
|
||||
$icons = [
|
||||
[$providerId, $providerId.'.svg'],
|
||||
[$providerId, 'app.svg'],
|
||||
[$appId, $providerId.'.svg'],
|
||||
[$appId, $appId.'.svg'],
|
||||
[$appId, 'app.svg'],
|
||||
['core', 'places/default-app-icon.svg'],
|
||||
];
|
||||
foreach ($icons as $i => $icon) {
|
||||
try {
|
||||
return $this->urlGenerator->imagePath(... $icon);
|
||||
} catch (RuntimeException $e) {
|
||||
// Ignore error
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $filters string[]
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function getFiltersType(array $filters, string $providerId): array {
|
||||
$filterList = [];
|
||||
foreach ($filters as $filter) {
|
||||
$filterList[$filter] = $this->getFilterDefinition($filter, $providerId)->type();
|
||||
}
|
||||
|
||||
return $filterList;
|
||||
}
|
||||
|
||||
private function getFilterDefinition(string $name, string $providerId): ?FilterDefinition {
|
||||
if (isset($this->commonFilters[$name])) {
|
||||
return $this->commonFilters[$name];
|
||||
}
|
||||
if (isset($this->customFilters[$providerId][$name])) {
|
||||
return $this->customFilters[$providerId][$name];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $parameters
|
||||
*/
|
||||
public function buildFilterList(string $providerId, array $parameters): FilterCollection {
|
||||
$this->loadLazyProviders($providerId);
|
||||
|
||||
$list = [];
|
||||
foreach ($parameters as $name => $value) {
|
||||
$filter = $this->buildFilter($name, $value, $providerId);
|
||||
if ($filter === null) {
|
||||
continue;
|
||||
}
|
||||
$list[$name] = $filter;
|
||||
}
|
||||
|
||||
return new FilterCollection(... $list);
|
||||
}
|
||||
|
||||
private function buildFilter(string $name, string $value, string $providerId): ?IFilter {
|
||||
$filterDefinition = $this->getFilterDefinition($name, $providerId);
|
||||
if ($filterDefinition === null) {
|
||||
$this->logger->debug('Unable to find {name} definition', [
|
||||
'name' => $name,
|
||||
'value' => $value,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$this->filterSupportedByProvider($filterDefinition, $providerId)) {
|
||||
// FIXME Use dedicated exception and handle it
|
||||
throw new UnsupportedFilter($name, $providerId);
|
||||
}
|
||||
|
||||
return FilterFactory::get($filterDefinition->type(), $value);
|
||||
}
|
||||
|
||||
private function filterSupportedByProvider(FilterDefinition $filterDefinition, string $providerId): bool {
|
||||
// Non exclusive filters can be ommited by apps
|
||||
if (!$filterDefinition->exclusive()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$provider = $this->providers[$providerId]['provider'];
|
||||
$supportedFilters = $provider instanceof IFilteringProvider
|
||||
? $provider->getSupportedFilters()
|
||||
: ['term'];
|
||||
|
||||
return in_array($filterDefinition->name(), $supportedFilters, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query an individual search provider for results
|
||||
*
|
||||
|
|
@ -147,15 +317,18 @@ class SearchComposer {
|
|||
* @return SearchResult
|
||||
* @throws InvalidArgumentException when the $providerId does not correspond to a registered provider
|
||||
*/
|
||||
public function search(IUser $user,
|
||||
string $providerId,
|
||||
ISearchQuery $query): SearchResult {
|
||||
$this->loadLazyProviders();
|
||||
public function search(
|
||||
IUser $user,
|
||||
string $providerId,
|
||||
ISearchQuery $query,
|
||||
): SearchResult {
|
||||
$this->loadLazyProviders($providerId);
|
||||
|
||||
$provider = $this->providers[$providerId] ?? null;
|
||||
$provider = $this->providers[$providerId]['provider'] ?? null;
|
||||
if ($provider === null) {
|
||||
throw new InvalidArgumentException("Provider $providerId is unknown");
|
||||
}
|
||||
|
||||
return $provider->search($user, $query);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,89 +27,57 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace OC\Search;
|
||||
|
||||
use OCP\Search\IFilterCollection;
|
||||
use OCP\Search\IFilter;
|
||||
use OCP\Search\ISearchQuery;
|
||||
|
||||
class SearchQuery implements ISearchQuery {
|
||||
public const LIMIT_DEFAULT = 5;
|
||||
|
||||
/** @var string */
|
||||
private $term;
|
||||
|
||||
/** @var int */
|
||||
private $sortOrder;
|
||||
|
||||
/** @var int */
|
||||
private $limit;
|
||||
|
||||
/** @var int|string|null */
|
||||
private $cursor;
|
||||
|
||||
/** @var string */
|
||||
private $route;
|
||||
|
||||
/** @var array */
|
||||
private $routeParameters;
|
||||
|
||||
/**
|
||||
* @param string $term
|
||||
* @param int $sortOrder
|
||||
* @param int $limit
|
||||
* @param int|string|null $cursor
|
||||
* @param string $route
|
||||
* @param array $routeParameters
|
||||
* @param string[] $params Request query
|
||||
* @param string[] $routeParameters
|
||||
*/
|
||||
public function __construct(string $term,
|
||||
int $sortOrder = ISearchQuery::SORT_DATE_DESC,
|
||||
int $limit = self::LIMIT_DEFAULT,
|
||||
$cursor = null,
|
||||
string $route = '',
|
||||
array $routeParameters = []) {
|
||||
$this->term = $term;
|
||||
$this->sortOrder = $sortOrder;
|
||||
$this->limit = $limit;
|
||||
$this->cursor = $cursor;
|
||||
$this->route = $route;
|
||||
$this->routeParameters = $routeParameters;
|
||||
public function __construct(
|
||||
private IFilterCollection $filters,
|
||||
private int $sortOrder = ISearchQuery::SORT_DATE_DESC,
|
||||
private int $limit = self::LIMIT_DEFAULT,
|
||||
private int|string|null $cursor = null,
|
||||
private string $route = '',
|
||||
private array $routeParameters = [],
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getTerm(): string {
|
||||
return $this->term;
|
||||
return $this->getFilter('term')?->get() ?? '';
|
||||
}
|
||||
|
||||
public function getFilter(string $name): ?IFilter {
|
||||
return $this->filters->has($name)
|
||||
? $this->filters->get($name)
|
||||
: null;
|
||||
}
|
||||
|
||||
public function getFilters(): IFilterCollection {
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getSortOrder(): int {
|
||||
return $this->sortOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getLimit(): int {
|
||||
return $this->limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getCursor() {
|
||||
public function getCursor(): int|string|null {
|
||||
return $this->cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getRoute(): string {
|
||||
return $this->route;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getRouteParameters(): array {
|
||||
return $this->routeParameters;
|
||||
}
|
||||
|
|
|
|||
34
lib/private/Search/UnsupportedFilter.php
Normal file
34
lib/private/Search/UnsupportedFilter.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
namespace OC\Search;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class UnsupportedFilter extends Exception {
|
||||
public function __construct(string $filerName, $providerId) {
|
||||
parent::__construct('Provider '.$providerId.' doesn’t support filter '.$filerName.'.');
|
||||
}
|
||||
}
|
||||
101
lib/public/Search/FilterDefinition.php
Normal file
101
lib/public/Search/FilterDefinition.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
namespace OCP\Search;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Filter definition
|
||||
*
|
||||
* Describe filter attributes
|
||||
*
|
||||
* @since 28.0.0
|
||||
*/
|
||||
class FilterDefinition {
|
||||
public const TYPE_BOOL = 'bool';
|
||||
public const TYPE_INT = 'int';
|
||||
public const TYPE_FLOAT = 'float';
|
||||
public const TYPE_STRING = 'string';
|
||||
public const TYPE_STRINGS = 'strings';
|
||||
public const TYPE_DATETIME = 'datetime';
|
||||
public const TYPE_PERSON = 'person';
|
||||
public const TYPE_NC_USER = 'nc-user';
|
||||
public const TYPE_NC_GROUP = 'nc-group';
|
||||
|
||||
/**
|
||||
* Build filter definition
|
||||
*
|
||||
* @param self::TYPE_* $type
|
||||
* @param bool $exclusive If true, all providers not supporting this filter will be ignored when this filter is provided
|
||||
* @throw InvalidArgumentException in case of invalid name. Allowed characters are -, 0-9, a-z.
|
||||
* @since 28.0.0
|
||||
*/
|
||||
public function __construct(
|
||||
private string $name,
|
||||
private string $type = self::TYPE_STRING,
|
||||
private bool $exclusive = true,
|
||||
) {
|
||||
if (!preg_match('/[-0-9a-z]+/Au', $name)) {
|
||||
throw new InvalidArgumentException('Invalid filter name. Allowed characters are [-0-9a-z]');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter name
|
||||
*
|
||||
* Name is used in query string and for advanced syntax `name: <value>`
|
||||
*
|
||||
* @since 28.0.0
|
||||
*/
|
||||
public function name(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter type
|
||||
*
|
||||
* Expected type of value for the filter
|
||||
*
|
||||
* @return self::TYPE_*
|
||||
* @since 28.0.0
|
||||
*/
|
||||
public function type(): string {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is filter exclusive?
|
||||
*
|
||||
* If exclusive, only provider with support for this filter will receive the query.
|
||||
* Example: if an exclusive filter `mimetype` is declared, a search with this term will not
|
||||
* be send to providers like `settings` that doesn't support it.
|
||||
*
|
||||
* @since 28.0.0
|
||||
*/
|
||||
public function exclusive(): bool {
|
||||
return $this->exclusive;
|
||||
}
|
||||
}
|
||||
40
lib/public/Search/IFilter.php
Normal file
40
lib/public/Search/IFilter.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
namespace OCP\Search;
|
||||
|
||||
/**
|
||||
* Interface for search filters
|
||||
*
|
||||
* @since 28.0.0
|
||||
*/
|
||||
interface IFilter {
|
||||
/**
|
||||
* Get filter value
|
||||
*
|
||||
* @since 28.0.0
|
||||
*/
|
||||
public function get(): mixed;
|
||||
}
|
||||
57
lib/public/Search/IFilterCollection.php
Normal file
57
lib/public/Search/IFilterCollection.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
namespace OCP\Search;
|
||||
|
||||
use IteratorAggregate;
|
||||
|
||||
/**
|
||||
* Interface for search filters
|
||||
*
|
||||
* @since 28.0.0
|
||||
* @extends IteratorAggregate<string, \OCP\Search\IFilter>
|
||||
*/
|
||||
interface IFilterCollection extends IteratorAggregate {
|
||||
/**
|
||||
* Check if a filter exits
|
||||
*
|
||||
* @since 28.0.0
|
||||
*/
|
||||
public function has(string $name): bool;
|
||||
|
||||
/**
|
||||
* Get a filter by name
|
||||
*
|
||||
* @since 28.0.0
|
||||
*/
|
||||
public function get(string $name): ?IFilter;
|
||||
|
||||
/**
|
||||
* Return Iterator of filters
|
||||
*
|
||||
* @since 28.0.0
|
||||
*/
|
||||
public function getIterator(): \Traversable;
|
||||
}
|
||||
72
lib/public/Search/IFilteringProvider.php
Normal file
72
lib/public/Search/IFilteringProvider.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2023 Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @author Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
namespace OCP\Search;
|
||||
|
||||
/**
|
||||
* Interface for advanced search providers
|
||||
*
|
||||
* These providers will be implemented in apps, so they can participate in the
|
||||
* global search results of Nextcloud. If an app provides more than one type of
|
||||
* resource, e.g. contacts and address books in Nextcloud Contacts, it should
|
||||
* register one provider per group.
|
||||
*
|
||||
* @since 28.0.0
|
||||
*/
|
||||
interface IFilteringProvider extends IProvider {
|
||||
/**
|
||||
* Return the names of filters supported by the application
|
||||
*
|
||||
* If a filter sent by client is not in this list,
|
||||
* the current provider will be ignored.
|
||||
* Example:
|
||||
* array('term', 'since', 'custom-filter');
|
||||
*
|
||||
* @since 28.0.0
|
||||
* @return string[] Name of supported filters (default or defined by application)
|
||||
*/
|
||||
public function getSupportedFilters(): array;
|
||||
|
||||
/**
|
||||
* Get alternate IDs handled by this provider
|
||||
*
|
||||
* A search provider can complete results from other search providers.
|
||||
* For example, files and full-text-search can search in files.
|
||||
* If you use `in:files` in a search, provider files will be invoked,
|
||||
* with all other providers declaring `files` in this method
|
||||
*
|
||||
* @since 28.0.0
|
||||
* @return string[] IDs
|
||||
*/
|
||||
public function getAlternateIds(): array;
|
||||
|
||||
/**
|
||||
* Allows application to declare custom filters
|
||||
*
|
||||
* @since 28.0.0
|
||||
* @return list<FilterDefinition>
|
||||
*/
|
||||
public function getCustomFilters(): array;
|
||||
}
|
||||
|
|
@ -48,9 +48,24 @@ interface ISearchQuery {
|
|||
*
|
||||
* @return string the search term
|
||||
* @since 20.0.0
|
||||
* @deprecated 28.0.0
|
||||
*/
|
||||
public function getTerm(): string;
|
||||
|
||||
/**
|
||||
* Get a single request filter
|
||||
*
|
||||
* @since 28.0.0
|
||||
*/
|
||||
public function getFilter(string $name): ?IFilter;
|
||||
|
||||
/**
|
||||
* Get request filters
|
||||
*
|
||||
* @since 28.0.0
|
||||
*/
|
||||
public function getFilters(): IFilterCollection;
|
||||
|
||||
/**
|
||||
* Get the sort order of results as defined as SORT_* constants on this interface
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in a new issue