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