Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
16.06% |
22 / 137 |
|
16.67% |
3 / 18 |
CRAP | |
0.00% |
0 / 1 |
RepoGroup | |
16.18% |
22 / 136 |
|
16.67% |
3 / 18 |
3040.03 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
findFile | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
380 | |||
findFiles | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
checkRedirect | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
findFileFromKey | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
findBySha1 | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
findBySha1s | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
getRepo | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getRepoByName | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
getLocalRepo | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
forEachForeignRepo | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
4.07 | |||
hasForeignRepos | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
initialiseRepos | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
newCustomLocalRepo | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
newRepo | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
splitVirtualUrl | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getFileProps | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
clearCache | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\FileRepo; |
22 | |
23 | use InvalidArgumentException; |
24 | use MediaWiki\FileRepo\File\File; |
25 | use MediaWiki\Linker\LinkTarget; |
26 | use MediaWiki\Page\PageIdentity; |
27 | use MediaWiki\Title\Title; |
28 | use MWFileProps; |
29 | use Wikimedia\MapCacheLRU\MapCacheLRU; |
30 | use Wikimedia\Mime\MimeAnalyzer; |
31 | use Wikimedia\ObjectCache\WANObjectCache; |
32 | |
33 | /** |
34 | * Prioritized list of file repositories. |
35 | * |
36 | * @ingroup FileRepo |
37 | */ |
38 | class RepoGroup { |
39 | /** @var LocalRepo */ |
40 | protected $localRepo; |
41 | |
42 | /** @var FileRepo[] */ |
43 | protected $foreignRepos; |
44 | |
45 | /** @var WANObjectCache */ |
46 | protected $wanCache; |
47 | |
48 | /** @var bool */ |
49 | protected $reposInitialised = false; |
50 | |
51 | /** @var array */ |
52 | protected $localInfo; |
53 | |
54 | /** @var array */ |
55 | protected $foreignInfo; |
56 | |
57 | /** @var MapCacheLRU */ |
58 | protected $cache; |
59 | |
60 | /** Maximum number of cache items */ |
61 | private const MAX_CACHE_SIZE = 500; |
62 | |
63 | /** @var MimeAnalyzer */ |
64 | private $mimeAnalyzer; |
65 | |
66 | /** |
67 | * Construct a group of file repositories. Do not call this -- use |
68 | * MediaWikiServices::getRepoGroup. |
69 | * |
70 | * @param array $localInfo Associative array for local repo's info |
71 | * @param array $foreignInfo Array of repository info arrays. |
72 | * Each info array is an associative array with the 'class' member |
73 | * giving the class name. The entire array is passed to the repository |
74 | * constructor as the first parameter. |
75 | * @param WANObjectCache $wanCache |
76 | * @param MimeAnalyzer $mimeAnalyzer |
77 | */ |
78 | public function __construct( |
79 | $localInfo, |
80 | $foreignInfo, |
81 | WANObjectCache $wanCache, |
82 | MimeAnalyzer $mimeAnalyzer |
83 | ) { |
84 | $this->localInfo = $localInfo; |
85 | $this->foreignInfo = $foreignInfo; |
86 | $this->cache = new MapCacheLRU( self::MAX_CACHE_SIZE ); |
87 | $this->wanCache = $wanCache; |
88 | $this->mimeAnalyzer = $mimeAnalyzer; |
89 | } |
90 | |
91 | /** |
92 | * Search repositories for an image. |
93 | * |
94 | * @param PageIdentity|LinkTarget|string $title The file to find |
95 | * @param array $options Associative array of options: |
96 | * time: requested time for an archived image, or false for the |
97 | * current version. An image object will be returned which was |
98 | * created at the specified time. |
99 | * ignoreRedirect: If true, do not follow file redirects |
100 | * private: If Authority object, return restricted (deleted) files if the |
101 | * performer is allowed to view them. Otherwise, such files will not |
102 | * be found. Authority is only accepted since 1.37, User was required |
103 | * before. |
104 | * latest: If true, load from the latest available data into File objects |
105 | * @phpcs:ignore Generic.Files.LineLength |
106 | * @phan-param array{time?:mixed,ignoreRedirect?:bool,private?:bool|\MediaWiki\Permissions\Authority,latest?:bool} $options |
107 | * @return File|false False if title is not found |
108 | */ |
109 | public function findFile( $title, $options = [] ) { |
110 | if ( !is_array( $options ) ) { |
111 | // MW 1.15 compat |
112 | $options = [ 'time' => $options ]; |
113 | } |
114 | if ( isset( $options['bypassCache'] ) ) { |
115 | $options['latest'] = $options['bypassCache']; // b/c |
116 | } |
117 | if ( isset( $options['time'] ) && $options['time'] !== false ) { |
118 | $options['time'] = wfTimestamp( TS_MW, $options['time'] ); |
119 | } else { |
120 | $options['time'] = false; |
121 | } |
122 | |
123 | if ( !$this->reposInitialised ) { |
124 | $this->initialiseRepos(); |
125 | } |
126 | |
127 | $title = File::normalizeTitle( $title ); |
128 | if ( !$title ) { |
129 | return false; |
130 | } |
131 | |
132 | # Check the cache |
133 | $dbkey = $title->getDBkey(); |
134 | $timeKey = is_string( $options['time'] ) ? $options['time'] : ''; |
135 | if ( empty( $options['ignoreRedirect'] ) |
136 | && empty( $options['private'] ) |
137 | && empty( $options['latest'] ) |
138 | ) { |
139 | if ( $this->cache->hasField( $dbkey, $timeKey, 60 ) ) { |
140 | return $this->cache->getField( $dbkey, $timeKey ); |
141 | } |
142 | $useCache = true; |
143 | } else { |
144 | $useCache = false; |
145 | } |
146 | |
147 | # Check the local repo |
148 | $image = $this->localRepo->findFile( $title, $options ); |
149 | |
150 | # Check the foreign repos |
151 | if ( !$image ) { |
152 | foreach ( $this->foreignRepos as $repo ) { |
153 | $image = $repo->findFile( $title, $options ); |
154 | if ( $image ) { |
155 | break; |
156 | } |
157 | } |
158 | } |
159 | |
160 | $image = $image instanceof File ? $image : false; // type check |
161 | # Cache file existence or non-existence |
162 | if ( $useCache && ( !$image || $image->isCacheable() ) ) { |
163 | $this->cache->setField( $dbkey, $timeKey, $image ); |
164 | } |
165 | |
166 | return $image; |
167 | } |
168 | |
169 | /** |
170 | * Search repositories for many files at once. |
171 | * |
172 | * @param array $inputItems An array of titles, or an array of findFile() options with |
173 | * the "title" option giving the title. Example: |
174 | * |
175 | * $findItem = [ 'title' => $title, 'private' => true ]; |
176 | * $findBatch = [ $findItem ]; |
177 | * $repo->findFiles( $findBatch ); |
178 | * |
179 | * No title should appear in $items twice, as the result use titles as keys |
180 | * @param int $flags Supports: |
181 | * - FileRepo::NAME_AND_TIME_ONLY : return a (search title => (title,timestamp)) map. |
182 | * The search title uses the input titles; the other is the final post-redirect title. |
183 | * All titles are returned as string DB keys and the inner array is associative. |
184 | * @return array Map of (file name => File objects) for matches or (search title => (title,timestamp)) |
185 | */ |
186 | public function findFiles( array $inputItems, $flags = 0 ) { |
187 | if ( !$this->reposInitialised ) { |
188 | $this->initialiseRepos(); |
189 | } |
190 | |
191 | $items = []; |
192 | foreach ( $inputItems as $item ) { |
193 | if ( !is_array( $item ) ) { |
194 | $item = [ 'title' => $item ]; |
195 | } |
196 | $item['title'] = File::normalizeTitle( $item['title'] ); |
197 | if ( $item['title'] ) { |
198 | $items[$item['title']->getDBkey()] = $item; |
199 | } |
200 | } |
201 | |
202 | $images = $this->localRepo->findFiles( $items, $flags ); |
203 | |
204 | foreach ( $this->foreignRepos as $repo ) { |
205 | // Remove found files from $items |
206 | $items = array_diff_key( $items, $images ); |
207 | $images = array_merge( $images, $repo->findFiles( $items, $flags ) ); |
208 | } |
209 | |
210 | return $images; |
211 | } |
212 | |
213 | /** |
214 | * Interface for FileRepo::checkRedirect() |
215 | * @param PageIdentity|LinkTarget|string $title |
216 | * @return Title|false |
217 | */ |
218 | public function checkRedirect( $title ) { |
219 | if ( !$this->reposInitialised ) { |
220 | $this->initialiseRepos(); |
221 | } |
222 | |
223 | $title = File::normalizeTitle( $title ); |
224 | |
225 | $redir = $this->localRepo->checkRedirect( $title ); |
226 | if ( $redir ) { |
227 | return $redir; |
228 | } |
229 | |
230 | foreach ( $this->foreignRepos as $repo ) { |
231 | $redir = $repo->checkRedirect( $title ); |
232 | if ( $redir ) { |
233 | return $redir; |
234 | } |
235 | } |
236 | |
237 | return false; |
238 | } |
239 | |
240 | /** |
241 | * Find an instance of the file with this key, created at the specified time |
242 | * Returns false if the file does not exist. |
243 | * |
244 | * @param string $hash Base 36 SHA-1 hash |
245 | * @param array $options Option array, same as findFile() |
246 | * @return File|false File object or false if it is not found |
247 | */ |
248 | public function findFileFromKey( $hash, $options = [] ) { |
249 | if ( !$this->reposInitialised ) { |
250 | $this->initialiseRepos(); |
251 | } |
252 | |
253 | $file = $this->localRepo->findFileFromKey( $hash, $options ); |
254 | if ( !$file ) { |
255 | foreach ( $this->foreignRepos as $repo ) { |
256 | $file = $repo->findFileFromKey( $hash, $options ); |
257 | if ( $file ) { |
258 | break; |
259 | } |
260 | } |
261 | } |
262 | |
263 | return $file; |
264 | } |
265 | |
266 | /** |
267 | * Find all instances of files with this key |
268 | * |
269 | * @param string $hash Base 36 SHA-1 hash |
270 | * @return File[] |
271 | */ |
272 | public function findBySha1( $hash ) { |
273 | if ( !$this->reposInitialised ) { |
274 | $this->initialiseRepos(); |
275 | } |
276 | |
277 | $result = $this->localRepo->findBySha1( $hash ); |
278 | foreach ( $this->foreignRepos as $repo ) { |
279 | $result = array_merge( $result, $repo->findBySha1( $hash ) ); |
280 | } |
281 | usort( $result, [ File::class, 'compare' ] ); |
282 | |
283 | return $result; |
284 | } |
285 | |
286 | /** |
287 | * Find all instances of files with this keys |
288 | * |
289 | * @param string[] $hashes Base 36 SHA-1 hashes |
290 | * @return File[][] |
291 | */ |
292 | public function findBySha1s( array $hashes ) { |
293 | if ( !$this->reposInitialised ) { |
294 | $this->initialiseRepos(); |
295 | } |
296 | |
297 | $result = $this->localRepo->findBySha1s( $hashes ); |
298 | foreach ( $this->foreignRepos as $repo ) { |
299 | $result = array_merge_recursive( $result, $repo->findBySha1s( $hashes ) ); |
300 | } |
301 | // sort the merged (and presorted) sublist of each hash |
302 | foreach ( $result as $hash => $files ) { |
303 | usort( $result[$hash], [ File::class, 'compare' ] ); |
304 | } |
305 | |
306 | return $result; |
307 | } |
308 | |
309 | /** |
310 | * Get the repo instance with a given key. |
311 | * @param string|int $index |
312 | * @return FileRepo|false |
313 | */ |
314 | public function getRepo( $index ) { |
315 | if ( !$this->reposInitialised ) { |
316 | $this->initialiseRepos(); |
317 | } |
318 | if ( $index === 'local' ) { |
319 | return $this->localRepo; |
320 | } |
321 | return $this->foreignRepos[$index] ?? false; |
322 | } |
323 | |
324 | /** |
325 | * Get the repo instance by its name |
326 | * @param string $name |
327 | * @return FileRepo|false |
328 | */ |
329 | public function getRepoByName( $name ) { |
330 | if ( !$this->reposInitialised ) { |
331 | $this->initialiseRepos(); |
332 | } |
333 | foreach ( $this->foreignRepos as $repo ) { |
334 | if ( $repo->name == $name ) { |
335 | return $repo; |
336 | } |
337 | } |
338 | |
339 | return false; |
340 | } |
341 | |
342 | /** |
343 | * Get the local repository, i.e. the one corresponding to the local image |
344 | * table. Files are typically uploaded to the local repository. |
345 | * |
346 | * @return LocalRepo |
347 | */ |
348 | public function getLocalRepo() { |
349 | // @phan-suppress-next-line PhanTypeMismatchReturnSuperType |
350 | return $this->getRepo( 'local' ); |
351 | } |
352 | |
353 | /** |
354 | * Call a function for each foreign repo, with the repo object as the |
355 | * first parameter. |
356 | * |
357 | * @param callable $callback The function to call |
358 | * @param array $params Optional additional parameters to pass to the function |
359 | * @return bool |
360 | */ |
361 | public function forEachForeignRepo( $callback, $params = [] ) { |
362 | if ( !$this->reposInitialised ) { |
363 | $this->initialiseRepos(); |
364 | } |
365 | foreach ( $this->foreignRepos as $repo ) { |
366 | if ( $callback( $repo, ...$params ) ) { |
367 | return true; |
368 | } |
369 | } |
370 | |
371 | return false; |
372 | } |
373 | |
374 | /** |
375 | * Does the installation have any foreign repos set up? |
376 | * @return bool |
377 | */ |
378 | public function hasForeignRepos() { |
379 | if ( !$this->reposInitialised ) { |
380 | $this->initialiseRepos(); |
381 | } |
382 | return (bool)$this->foreignRepos; |
383 | } |
384 | |
385 | /** |
386 | * Initialise the $repos array |
387 | */ |
388 | public function initialiseRepos() { |
389 | if ( $this->reposInitialised ) { |
390 | return; |
391 | } |
392 | $this->reposInitialised = true; |
393 | |
394 | $this->localRepo = $this->newRepo( $this->localInfo ); |
395 | $this->foreignRepos = []; |
396 | foreach ( $this->foreignInfo as $key => $info ) { |
397 | $this->foreignRepos[$key] = $this->newRepo( $info ); |
398 | } |
399 | } |
400 | |
401 | /** |
402 | * Create a local repo with the specified option overrides. |
403 | * |
404 | * @param array $info |
405 | * @return LocalRepo |
406 | */ |
407 | public function newCustomLocalRepo( $info = [] ) { |
408 | // @phan-suppress-next-line PhanTypeMismatchReturnSuperType |
409 | return $this->newRepo( $info + $this->localInfo ); |
410 | } |
411 | |
412 | /** |
413 | * Create a repo class based on an info structure |
414 | * @param array $info |
415 | * @return FileRepo |
416 | */ |
417 | protected function newRepo( $info ) { |
418 | $class = $info['class']; |
419 | |
420 | $info['wanCache'] = $this->wanCache; |
421 | |
422 | return new $class( $info ); |
423 | } |
424 | |
425 | /** |
426 | * Split a virtual URL into repo, zone and rel parts |
427 | * @param string $url |
428 | * @return string[] Containing repo, zone and rel |
429 | */ |
430 | private function splitVirtualUrl( $url ) { |
431 | if ( !str_starts_with( $url, 'mwrepo://' ) ) { |
432 | throw new InvalidArgumentException( __METHOD__ . ': unknown protocol' ); |
433 | } |
434 | |
435 | $bits = explode( '/', substr( $url, 9 ), 3 ); |
436 | if ( count( $bits ) != 3 ) { |
437 | throw new InvalidArgumentException( __METHOD__ . ": invalid mwrepo URL: $url" ); |
438 | } |
439 | |
440 | return $bits; |
441 | } |
442 | |
443 | /** |
444 | * @param string $fileName |
445 | * @return array |
446 | */ |
447 | public function getFileProps( $fileName ) { |
448 | if ( FileRepo::isVirtualUrl( $fileName ) ) { |
449 | [ $repoName, /* $zone */, /* $rel */ ] = $this->splitVirtualUrl( $fileName ); |
450 | if ( $repoName === '' ) { |
451 | $repoName = 'local'; |
452 | } |
453 | $repo = $this->getRepo( $repoName ); |
454 | |
455 | return $repo->getFileProps( $fileName ); |
456 | } else { |
457 | $mwProps = new MWFileProps( $this->mimeAnalyzer ); |
458 | |
459 | return $mwProps->getPropsFromPath( $fileName, true ); |
460 | } |
461 | } |
462 | |
463 | /** |
464 | * Clear RepoGroup process cache used for finding a file |
465 | * @param PageIdentity|string|null $title File page or file name, or null to clear all files |
466 | */ |
467 | public function clearCache( $title = null ) { |
468 | if ( $title == null ) { |
469 | $this->cache->clear(); |
470 | } elseif ( is_string( $title ) ) { |
471 | $this->cache->clear( $title ); |
472 | } else { |
473 | $this->cache->clear( $title->getDBkey() ); |
474 | } |
475 | } |
476 | } |
477 | |
478 | /** @deprecated class alias since 1.44 */ |
479 | class_alias( RepoGroup::class, 'RepoGroup' ); |