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