You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

173 lines
5.2 KiB

  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OC\Core\Controller;
  8. use InvalidArgumentException;
  9. use OC\Core\AppInfo\Application;
  10. use OC\Core\AppInfo\ConfigLexicon;
  11. use OC\Core\ResponseDefinitions;
  12. use OC\Search\SearchComposer;
  13. use OC\Search\SearchQuery;
  14. use OC\Search\UnsupportedFilter;
  15. use OCP\AppFramework\Http;
  16. use OCP\AppFramework\Http\Attribute\ApiRoute;
  17. use OCP\AppFramework\Http\Attribute\NoAdminRequired;
  18. use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
  19. use OCP\AppFramework\Http\DataResponse;
  20. use OCP\AppFramework\OCSController;
  21. use OCP\IAppConfig;
  22. use OCP\IL10N;
  23. use OCP\IRequest;
  24. use OCP\IURLGenerator;
  25. use OCP\IUserSession;
  26. use OCP\Route\IRouter;
  27. use OCP\Search\ISearchQuery;
  28. use Symfony\Component\Routing\Exception\ResourceNotFoundException;
  29. /**
  30. * @psalm-import-type CoreUnifiedSearchProvider from ResponseDefinitions
  31. * @psalm-import-type CoreUnifiedSearchResult from ResponseDefinitions
  32. */
  33. class UnifiedSearchController extends OCSController {
  34. public function __construct(
  35. IRequest $request,
  36. private IUserSession $userSession,
  37. private SearchComposer $composer,
  38. private IRouter $router,
  39. private IURLGenerator $urlGenerator,
  40. private IL10N $l10n,
  41. private IAppConfig $appConfig,
  42. ) {
  43. parent::__construct('core', $request);
  44. }
  45. /**
  46. * Get the providers for unified search
  47. *
  48. * @param string $from the url the user is currently at
  49. * @return DataResponse<Http::STATUS_OK, list<CoreUnifiedSearchProvider>, array{}>
  50. *
  51. * 200: Providers returned
  52. */
  53. #[NoAdminRequired]
  54. #[NoCSRFRequired]
  55. #[ApiRoute(verb: 'GET', url: '/providers', root: '/search')]
  56. public function getProviders(string $from = ''): DataResponse {
  57. [$route, $parameters] = $this->getRouteInformation($from);
  58. $result = $this->composer->getProviders($route, $parameters);
  59. $response = new DataResponse($result);
  60. $response->setETag(md5(json_encode($result)));
  61. return $response;
  62. }
  63. /**
  64. * Launch a search for a specific search provider.
  65. *
  66. * Additional filters are available for each provider.
  67. * Send a request to /providers endpoint to list providers with their available filters.
  68. *
  69. * @param string $providerId ID of the provider
  70. * @param string $term Term to search
  71. * @param int|null $sortOrder Order of entries
  72. * @param int|null $limit Maximum amount of entries (capped by configurable unified-search.max-results-per-request, default: 25)
  73. * @param int|string|null $cursor Offset for searching
  74. * @param string $from The current user URL
  75. *
  76. * @return DataResponse<Http::STATUS_OK, CoreUnifiedSearchResult, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, string, array{}>
  77. *
  78. * 200: Search entries returned
  79. * 400: Searching is not possible
  80. */
  81. #[NoAdminRequired]
  82. #[NoCSRFRequired]
  83. #[ApiRoute(verb: 'GET', url: '/providers/{providerId}/search', root: '/search')]
  84. public function search(
  85. string $providerId,
  86. // Unused parameter for OpenAPI spec generator
  87. string $term = '',
  88. ?int $sortOrder = null,
  89. ?int $limit = null,
  90. $cursor = null,
  91. string $from = '',
  92. ): DataResponse {
  93. [$route, $routeParameters] = $this->getRouteInformation($from);
  94. $limit ??= SearchQuery::LIMIT_DEFAULT;
  95. $maxLimit = $this->appConfig->getValueInt(Application::APP_ID, ConfigLexicon::UNIFIED_SEARCH_MAX_RESULTS_PER_REQUEST);
  96. $limit = max(1, min($limit, $maxLimit));
  97. try {
  98. $filters = $this->composer->buildFilterList($providerId, $this->request->getParams());
  99. } catch (UnsupportedFilter|InvalidArgumentException $e) {
  100. return new DataResponse($e->getMessage(), Http::STATUS_BAD_REQUEST);
  101. }
  102. if ($filters->count() === 0) {
  103. return new DataResponse($this->l10n->t('No valid filters provided'), Http::STATUS_BAD_REQUEST);
  104. }
  105. return new DataResponse(
  106. $this->composer->search(
  107. $this->userSession->getUser(),
  108. $providerId,
  109. new SearchQuery(
  110. $filters,
  111. $sortOrder ?? ISearchQuery::SORT_DATE_DESC,
  112. $limit,
  113. $cursor,
  114. $route,
  115. $routeParameters
  116. )
  117. )->jsonSerialize()
  118. );
  119. }
  120. protected function getRouteInformation(string $url): array {
  121. $routeStr = '';
  122. $parameters = [];
  123. if ($url !== '') {
  124. $urlParts = parse_url($url);
  125. $urlPath = $urlParts['path'];
  126. // Optionally strip webroot from URL. Required for route matching on setups
  127. // with Nextcloud in a webserver subfolder (webroot).
  128. $webroot = $this->urlGenerator->getWebroot();
  129. if ($webroot !== '' && substr($urlPath, 0, strlen($webroot)) === $webroot) {
  130. $urlPath = substr($urlPath, strlen($webroot));
  131. }
  132. try {
  133. $parameters = $this->router->findMatchingRoute($urlPath);
  134. // contacts.PageController.index => contacts.Page.index
  135. $route = $parameters['caller'];
  136. if (substr($route[1], -10) === 'Controller') {
  137. $route[1] = substr($route[1], 0, -10);
  138. }
  139. $routeStr = implode('.', $route);
  140. // cleanup
  141. unset($parameters['_route'], $parameters['action'], $parameters['caller']);
  142. } catch (ResourceNotFoundException $exception) {
  143. }
  144. if (isset($urlParts['query'])) {
  145. parse_str($urlParts['query'], $queryParameters);
  146. $parameters = array_merge($parameters, $queryParameters);
  147. }
  148. }
  149. return [
  150. $routeStr,
  151. $parameters,
  152. ];
  153. }
  154. }