Passed
Push — master ( e17684...b6c034 )
by Blizzz
35:05 queued 17:14
created

QuerySearchHelper::applySearchConstraints()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 14
nc 8
nop 3
dl 0
loc 20
rs 9.7998
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2017 Robin Appelman <[email protected]>
4
 *
5
 * @author Christoph Wurst <[email protected]>
6
 * @author Robin Appelman <[email protected]>
7
 * @author Roeland Jago Douma <[email protected]>
8
 * @author Tobias Kaminsky <[email protected]>
9
 *
10
 * @license GNU AGPL version 3 or any later version
11
 *
12
 * This program is free software: you can redistribute it and/or modify
13
 * it under the terms of the GNU Affero General Public License as
14
 * published by the Free Software Foundation, either version 3 of the
15
 * License, or (at your option) any later version.
16
 *
17
 * This program is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
 * GNU Affero General Public License for more details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License
23
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
 *
25
 */
26
namespace OC\Files\Cache;
27
28
use OC\Files\Cache\Wrapper\CacheJail;
29
use OC\Files\Node\Root;
30
use OC\Files\Search\QueryOptimizer\QueryOptimizer;
31
use OC\Files\Search\SearchBinaryOperator;
32
use OC\SystemConfig;
33
use OCP\DB\QueryBuilder\IQueryBuilder;
34
use OCP\Files\Cache\ICache;
35
use OCP\Files\Cache\ICacheEntry;
36
use OCP\Files\Folder;
37
use OCP\Files\IMimeTypeLoader;
38
use OCP\Files\Mount\IMountPoint;
39
use OCP\Files\Search\ISearchBinaryOperator;
40
use OCP\Files\Search\ISearchQuery;
41
use OCP\IDBConnection;
42
use Psr\Log\LoggerInterface;
43
44
class QuerySearchHelper {
45
	/** @var IMimeTypeLoader */
46
	private $mimetypeLoader;
47
	/** @var IDBConnection */
48
	private $connection;
49
	/** @var SystemConfig */
50
	private $systemConfig;
51
	private LoggerInterface $logger;
52
	/** @var SearchBuilder */
53
	private $searchBuilder;
54
	/** @var QueryOptimizer */
55
	private $queryOptimizer;
56
57
	public function __construct(
58
		IMimeTypeLoader $mimetypeLoader,
59
		IDBConnection $connection,
60
		SystemConfig $systemConfig,
61
		LoggerInterface $logger,
62
		SearchBuilder $searchBuilder,
63
		QueryOptimizer $queryOptimizer
64
	) {
65
		$this->mimetypeLoader = $mimetypeLoader;
66
		$this->connection = $connection;
67
		$this->systemConfig = $systemConfig;
68
		$this->logger = $logger;
69
		$this->searchBuilder = $searchBuilder;
70
		$this->queryOptimizer = $queryOptimizer;
71
	}
72
73
	protected function getQueryBuilder() {
74
		return new CacheQueryBuilder(
75
			$this->connection,
76
			$this->systemConfig,
77
			$this->logger
78
		);
79
	}
80
81
	protected function applySearchConstraints(CacheQueryBuilder $query, ISearchQuery $searchQuery, array $caches): void {
82
		$storageFilters = array_values(array_map(function (ICache $cache) {
83
			return $cache->getQueryFilterForStorage();
84
		}, $caches));
85
		$storageFilter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $storageFilters);
86
		$filter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$searchQuery->getSearchOperation(), $storageFilter]);
87
		$this->queryOptimizer->processOperator($filter);
88
89
		$searchExpr = $this->searchBuilder->searchOperatorToDBExpr($query, $filter);
90
		if ($searchExpr) {
91
			$query->andWhere($searchExpr);
92
		}
93
94
		$this->searchBuilder->addSearchOrdersToQuery($query, $searchQuery->getOrder());
95
96
		if ($searchQuery->getLimit()) {
97
			$query->setMaxResults($searchQuery->getLimit());
98
		}
99
		if ($searchQuery->getOffset()) {
100
			$query->setFirstResult($searchQuery->getOffset());
101
		}
102
	}
103
104
105
	/**
106
	 * @return array<array-key, array{id: int, name: string, visibility: int, editable: int, ref_file_id: int, number_files: int}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key, array{i...nt, number_files: int}> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key, array{id: int, name: string, visibility: int, editable: int, ref_file_id: int, number_files: int}>.
Loading history...
107
	 */
108
	public function findUsedTagsInCaches(ISearchQuery $searchQuery, array $caches): array {
109
		$query = $this->getQueryBuilder();
110
		$query->selectTagUsage();
111
112
		$this->applySearchConstraints($query, $searchQuery, $caches);
113
114
		$result = $query->execute();
115
		$tags = $result->fetchAll();
116
		$result->closeCursor();
117
		return $tags;
118
	}
119
120
	/**
121
	 * Perform a file system search in multiple caches
122
	 *
123
	 * the results will be grouped by the same array keys as the $caches argument to allow
124
	 * post-processing based on which cache the result came from
125
	 *
126
	 * @template T of array-key
127
	 * @param ISearchQuery $searchQuery
128
	 * @param array<T, ICache> $caches
129
	 * @return array<T, ICacheEntry[]>
130
	 */
131
	public function searchInCaches(ISearchQuery $searchQuery, array $caches): array {
132
		// search in multiple caches at once by creating one query in the following format
133
		// SELECT ... FROM oc_filecache WHERE
134
		//     <filter expressions from the search query>
135
		// AND (
136
		//     <filter expression for storage1> OR
137
		//     <filter expression for storage2> OR
138
		//     ...
139
		// );
140
		//
141
		// This gives us all the files matching the search query from all caches
142
		//
143
		// while the resulting rows don't have a way to tell what storage they came from (multiple storages/caches can share storage_id)
144
		// 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.
145
146
		$builder = $this->getQueryBuilder();
147
148
		$query = $builder->selectFileCache('file', false);
149
150
		if ($this->searchBuilder->shouldJoinTags($searchQuery->getSearchOperation())) {
151
			$user = $searchQuery->getUser();
152
			if ($user === null) {
153
				throw new \InvalidArgumentException("Searching by tag requires the user to be set in the query");
154
			}
155
			$query
156
				->leftJoin('file', 'vcategory_to_object', 'tagmap', $builder->expr()->eq('file.fileid', 'tagmap.objid'))
157
				->leftJoin('tagmap', 'vcategory', 'tag', $builder->expr()->andX(
158
					$builder->expr()->eq('tagmap.type', 'tag.type'),
159
					$builder->expr()->eq('tagmap.categoryid', 'tag.id'),
160
					$builder->expr()->eq('tag.type', $builder->createNamedParameter('files')),
161
					$builder->expr()->eq('tag.uid', $builder->createNamedParameter($user->getUID()))
162
				))
163
				->leftJoin('file', 'systemtag_object_mapping', 'systemtagmap', $builder->expr()->andX(
164
					$builder->expr()->eq('file.fileid', $builder->expr()->castColumn('systemtagmap.objectid', IQueryBuilder::PARAM_INT)),
165
					$builder->expr()->eq('systemtagmap.objecttype', $builder->createNamedParameter('files'))
166
				))
167
				->leftJoin('systemtagmap', 'systemtag', 'systemtag', $builder->expr()->andX(
168
					$builder->expr()->eq('systemtag.id', 'systemtagmap.systemtagid'),
169
					$builder->expr()->eq('systemtag.visibility', $builder->createNamedParameter(true))
170
				));
171
		}
172
173
		$this->applySearchConstraints($query, $searchQuery, $caches);
174
175
		$result = $query->execute();
176
		$files = $result->fetchAll();
177
178
		$rawEntries = array_map(function (array $data) {
179
			return Cache::cacheEntryFromData($data, $this->mimetypeLoader);
180
		}, $files);
181
182
		$result->closeCursor();
183
184
		// loop through all caches for each result to see if the result matches that storage
185
		// results are grouped by the same array keys as the caches argument to allow the caller to distinguish the source of the results
186
		$results = array_fill_keys(array_keys($caches), []);
187
		foreach ($rawEntries as $rawEntry) {
188
			foreach ($caches as $cacheKey => $cache) {
189
				$entry = $cache->getCacheEntryFromSearchResult($rawEntry);
190
				if ($entry) {
191
					$results[$cacheKey][] = $entry;
192
				}
193
			}
194
		}
195
		return $results;
196
	}
197
198
	/**
199
	 * @return array{array<string, ICache>, array<string, IMountPoint>}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{array<string, ICac...y<string, IMountPoint>} at position 2 could not be parsed: Expected ':' at position 2, but found 'array'.
Loading history...
200
	 */
201
	public function getCachesAndMountPointsForSearch(Root $root, string $path, bool $limitToHome = false): array {
202
		$rootLength = strlen($path);
203
		$mount = $root->getMount($path);
204
		$storage = $mount->getStorage();
205
		$internalPath = $mount->getInternalPath($path);
206
207
		if ($internalPath !== '') {
208
			// a temporary CacheJail is used to handle filtering down the results to within this folder
209
			$caches = ['' => new CacheJail($storage->getCache(''), $internalPath)];
210
		} else {
211
			$caches = ['' => $storage->getCache('')];
212
		}
213
		$mountByMountPoint = ['' => $mount];
214
215
		if (!$limitToHome) {
216
			/** @var IMountPoint[] $mounts */
217
			$mounts = $root->getMountsIn($path);
218
			foreach ($mounts as $mount) {
219
				$storage = $mount->getStorage();
220
				if ($storage) {
221
					$relativeMountPoint = ltrim(substr($mount->getMountPoint(), $rootLength), '/');
222
					$caches[$relativeMountPoint] = $storage->getCache('');
223
					$mountByMountPoint[$relativeMountPoint] = $mount;
224
				}
225
			}
226
		}
227
228
		return [$caches, $mountByMountPoint];
229
	}
230
}
231