UnifiedSearchController.php 4.7 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\Core\ResponseDefinitions;
  10. use OC\Search\SearchComposer;
  11. use OC\Search\SearchQuery;
  12. use OC\Search\UnsupportedFilter;
  13. use OCP\AppFramework\Http;
  14. use OCP\AppFramework\Http\Attribute\ApiRoute;
  15. use OCP\AppFramework\Http\Attribute\NoAdminRequired;
  16. use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
  17. use OCP\AppFramework\Http\DataResponse;
  18. use OCP\AppFramework\OCSController;
  19. use OCP\IRequest;
  20. use OCP\IURLGenerator;
  21. use OCP\IUserSession;
  22. use OCP\Route\IRouter;
  23. use OCP\Search\ISearchQuery;
  24. use Symfony\Component\Routing\Exception\ResourceNotFoundException;
  25. /**
  26. * @psalm-import-type CoreUnifiedSearchProvider from ResponseDefinitions
  27. * @psalm-import-type CoreUnifiedSearchResult from ResponseDefinitions
  28. */
  29. class UnifiedSearchController extends OCSController {
  30. public function __construct(
  31. IRequest $request,
  32. private IUserSession $userSession,
  33. private SearchComposer $composer,
  34. private IRouter $router,
  35. private IURLGenerator $urlGenerator,
  36. ) {
  37. parent::__construct('core', $request);
  38. }
  39. /**
  40. * Get the providers for unified search
  41. *
  42. * @param string $from the url the user is currently at
  43. * @return DataResponse<Http::STATUS_OK, CoreUnifiedSearchProvider[], array{}>
  44. *
  45. * 200: Providers returned
  46. */
  47. #[NoAdminRequired]
  48. #[NoCSRFRequired]
  49. #[ApiRoute(verb: 'GET', url: '/providers', root: '/search')]
  50. public function getProviders(string $from = ''): DataResponse {
  51. [$route, $parameters] = $this->getRouteInformation($from);
  52. $result = $this->composer->getProviders($route, $parameters);
  53. $response = new DataResponse($result);
  54. $response->setETag(md5(json_encode($result)));
  55. return $response;
  56. }
  57. /**
  58. * Launch a search for a specific search provider.
  59. *
  60. * Additional filters are available for each provider.
  61. * Send a request to /providers endpoint to list providers with their available filters.
  62. *
  63. * @param string $providerId ID of the provider
  64. * @param string $term Term to search
  65. * @param int|null $sortOrder Order of entries
  66. * @param int|null $limit Maximum amount of entries, limited to 25
  67. * @param int|string|null $cursor Offset for searching
  68. * @param string $from The current user URL
  69. *
  70. * @return DataResponse<Http::STATUS_OK, CoreUnifiedSearchResult, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, string, array{}>
  71. *
  72. * 200: Search entries returned
  73. * 400: Searching is not possible
  74. */
  75. #[NoAdminRequired]
  76. #[NoCSRFRequired]
  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. }