QuerySearchHelper.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-License-Identifier: AGPL-3.0-or-later
  5. */
  6. namespace OC\Files\Cache;
  7. use OC\Files\Cache\Wrapper\CacheJail;
  8. use OC\Files\Search\QueryOptimizer\QueryOptimizer;
  9. use OC\Files\Search\SearchBinaryOperator;
  10. use OC\SystemConfig;
  11. use OCP\DB\QueryBuilder\IQueryBuilder;
  12. use OCP\Files\Cache\ICache;
  13. use OCP\Files\Cache\ICacheEntry;
  14. use OCP\Files\IMimeTypeLoader;
  15. use OCP\Files\IRootFolder;
  16. use OCP\Files\Mount\IMountPoint;
  17. use OCP\Files\Search\ISearchBinaryOperator;
  18. use OCP\Files\Search\ISearchQuery;
  19. use OCP\FilesMetadata\IFilesMetadataManager;
  20. use OCP\FilesMetadata\IMetadataQuery;
  21. use OCP\IDBConnection;
  22. use OCP\IGroupManager;
  23. use OCP\IUser;
  24. use Psr\Log\LoggerInterface;
  25. class QuerySearchHelper {
  26. public function __construct(
  27. private IMimeTypeLoader $mimetypeLoader,
  28. private IDBConnection $connection,
  29. private SystemConfig $systemConfig,
  30. private LoggerInterface $logger,
  31. private SearchBuilder $searchBuilder,
  32. private QueryOptimizer $queryOptimizer,
  33. private IGroupManager $groupManager,
  34. private IFilesMetadataManager $filesMetadataManager,
  35. ) {
  36. }
  37. protected function getQueryBuilder() {
  38. return new CacheQueryBuilder(
  39. $this->connection->getQueryBuilder(),
  40. $this->filesMetadataManager,
  41. );
  42. }
  43. /**
  44. * @param CacheQueryBuilder $query
  45. * @param ISearchQuery $searchQuery
  46. * @param array $caches
  47. * @param IMetadataQuery|null $metadataQuery
  48. */
  49. protected function applySearchConstraints(
  50. CacheQueryBuilder $query,
  51. ISearchQuery $searchQuery,
  52. array $caches,
  53. ?IMetadataQuery $metadataQuery = null
  54. ): void {
  55. $storageFilters = array_values(array_map(function (ICache $cache) {
  56. return $cache->getQueryFilterForStorage();
  57. }, $caches));
  58. $storageFilter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $storageFilters);
  59. $filter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$searchQuery->getSearchOperation(), $storageFilter]);
  60. $this->queryOptimizer->processOperator($filter);
  61. $searchExpr = $this->searchBuilder->searchOperatorToDBExpr($query, $filter, $metadataQuery);
  62. if ($searchExpr) {
  63. $query->andWhere($searchExpr);
  64. }
  65. $this->searchBuilder->addSearchOrdersToQuery($query, $searchQuery->getOrder(), $metadataQuery);
  66. if ($searchQuery->getLimit()) {
  67. $query->setMaxResults($searchQuery->getLimit());
  68. }
  69. if ($searchQuery->getOffset()) {
  70. $query->setFirstResult($searchQuery->getOffset());
  71. }
  72. }
  73. /**
  74. * @return array<array-key, array{id: int, name: string, visibility: int, editable: int, ref_file_id: int, number_files: int}>
  75. */
  76. public function findUsedTagsInCaches(ISearchQuery $searchQuery, array $caches): array {
  77. $query = $this->getQueryBuilder();
  78. $query->selectTagUsage();
  79. $this->applySearchConstraints($query, $searchQuery, $caches);
  80. $result = $query->execute();
  81. $tags = $result->fetchAll();
  82. $result->closeCursor();
  83. return $tags;
  84. }
  85. protected function equipQueryForSystemTags(CacheQueryBuilder $query, IUser $user): void {
  86. $query->leftJoin('file', 'systemtag_object_mapping', 'systemtagmap', $query->expr()->andX(
  87. $query->expr()->eq('file.fileid', $query->expr()->castColumn('systemtagmap.objectid', IQueryBuilder::PARAM_INT)),
  88. $query->expr()->eq('systemtagmap.objecttype', $query->createNamedParameter('files'))
  89. ));
  90. $on = $query->expr()->andX($query->expr()->eq('systemtag.id', 'systemtagmap.systemtagid'));
  91. if (!$this->groupManager->isAdmin($user->getUID())) {
  92. $on->add($query->expr()->eq('systemtag.visibility', $query->createNamedParameter(true)));
  93. }
  94. $query->leftJoin('systemtagmap', 'systemtag', 'systemtag', $on);
  95. }
  96. protected function equipQueryForDavTags(CacheQueryBuilder $query, IUser $user): void {
  97. $query
  98. ->leftJoin('file', 'vcategory_to_object', 'tagmap', $query->expr()->eq('file.fileid', 'tagmap.objid'))
  99. ->leftJoin('tagmap', 'vcategory', 'tag', $query->expr()->andX(
  100. $query->expr()->eq('tagmap.categoryid', 'tag.id'),
  101. $query->expr()->eq('tag.type', $query->createNamedParameter('files')),
  102. $query->expr()->eq('tag.uid', $query->createNamedParameter($user->getUID()))
  103. ));
  104. }
  105. protected function equipQueryForShares(CacheQueryBuilder $query): void {
  106. $query->join('file', 'share', 's', $query->expr()->eq('file.fileid', 's.file_source'));
  107. }
  108. /**
  109. * Perform a file system search in multiple caches
  110. *
  111. * the results will be grouped by the same array keys as the $caches argument to allow
  112. * post-processing based on which cache the result came from
  113. *
  114. * @template T of array-key
  115. * @param ISearchQuery $searchQuery
  116. * @param array<T, ICache> $caches
  117. * @return array<T, ICacheEntry[]>
  118. */
  119. public function searchInCaches(ISearchQuery $searchQuery, array $caches): array {
  120. // search in multiple caches at once by creating one query in the following format
  121. // SELECT ... FROM oc_filecache WHERE
  122. // <filter expressions from the search query>
  123. // AND (
  124. // <filter expression for storage1> OR
  125. // <filter expression for storage2> OR
  126. // ...
  127. // );
  128. //
  129. // This gives us all the files matching the search query from all caches
  130. //
  131. // while the resulting rows don't have a way to tell what storage they came from (multiple storages/caches can share storage_id)
  132. // we can just ask every cache if the row belongs to them and give them the cache to do any post processing on the result.
  133. $builder = $this->getQueryBuilder();
  134. $query = $builder->selectFileCache('file', false);
  135. $requestedFields = $this->searchBuilder->extractRequestedFields($searchQuery->getSearchOperation());
  136. if (in_array('systemtag', $requestedFields)) {
  137. $this->equipQueryForSystemTags($query, $this->requireUser($searchQuery));
  138. }
  139. if (in_array('tagname', $requestedFields) || in_array('favorite', $requestedFields)) {
  140. $this->equipQueryForDavTags($query, $this->requireUser($searchQuery));
  141. }
  142. if (in_array('owner', $requestedFields) || in_array('share_with', $requestedFields) || in_array('share_type', $requestedFields)) {
  143. $this->equipQueryForShares($query);
  144. }
  145. $metadataQuery = $query->selectMetadata();
  146. $this->applySearchConstraints($query, $searchQuery, $caches, $metadataQuery);
  147. $result = $query->execute();
  148. $files = $result->fetchAll();
  149. $rawEntries = array_map(function (array $data) use ($metadataQuery) {
  150. $data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
  151. return Cache::cacheEntryFromData($data, $this->mimetypeLoader);
  152. }, $files);
  153. $result->closeCursor();
  154. // loop through all caches for each result to see if the result matches that storage
  155. // results are grouped by the same array keys as the caches argument to allow the caller to distinguish the source of the results
  156. $results = array_fill_keys(array_keys($caches), []);
  157. foreach ($rawEntries as $rawEntry) {
  158. foreach ($caches as $cacheKey => $cache) {
  159. $entry = $cache->getCacheEntryFromSearchResult($rawEntry);
  160. if ($entry) {
  161. $results[$cacheKey][] = $entry;
  162. }
  163. }
  164. }
  165. return $results;
  166. }
  167. protected function requireUser(ISearchQuery $searchQuery): IUser {
  168. $user = $searchQuery->getUser();
  169. if ($user === null) {
  170. throw new \InvalidArgumentException("This search operation requires the user to be set in the query");
  171. }
  172. return $user;
  173. }
  174. /**
  175. * @return list{0?: array<array-key, ICache>, 1?: array<array-key, IMountPoint>}
  176. */
  177. public function getCachesAndMountPointsForSearch(IRootFolder $root, string $path, bool $limitToHome = false): array {
  178. $rootLength = strlen($path);
  179. $mount = $root->getMount($path);
  180. $storage = $mount->getStorage();
  181. if ($storage === null) {
  182. return [];
  183. }
  184. $internalPath = $mount->getInternalPath($path);
  185. if ($internalPath !== '') {
  186. // a temporary CacheJail is used to handle filtering down the results to within this folder
  187. /** @var ICache[] $caches */
  188. $caches = ['' => new CacheJail($storage->getCache(''), $internalPath)];
  189. } else {
  190. /** @var ICache[] $caches */
  191. $caches = ['' => $storage->getCache('')];
  192. }
  193. /** @var IMountPoint[] $mountByMountPoint */
  194. $mountByMountPoint = ['' => $mount];
  195. if (!$limitToHome) {
  196. $mounts = $root->getMountsIn($path);
  197. foreach ($mounts as $mount) {
  198. $storage = $mount->getStorage();
  199. if ($storage) {
  200. $relativeMountPoint = ltrim(substr($mount->getMountPoint(), $rootLength), '/');
  201. $caches[$relativeMountPoint] = $storage->getCache('');
  202. $mountByMountPoint[$relativeMountPoint] = $mount;
  203. }
  204. }
  205. }
  206. return [$caches, $mountByMountPoint];
  207. }
  208. }