UnifiedSearchController.php 4.6 KB

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