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