Merge "Set visibility on class properties of UnregisteredLocalFile"
[lhc/web/wiklou.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 protected $foreignRepos;
34
35 /** @var bool */
36 protected $reposInitialised = false;
37
38 protected $localInfo;
39 protected $foreignInfo;
40
41 /** @var array */
42 protected $cache;
43
44 /** @var RepoGroup */
45 protected static $instance;
46
47 /** Maximum number of cache items */
48 const MAX_CACHE_SIZE = 500;
49
50 /**
51 * Get a RepoGroup instance. At present only one instance of RepoGroup is
52 * needed in a MediaWiki invocation, this may change in the future.
53 * @return RepoGroup
54 */
55 static function singleton() {
56 if ( self::$instance ) {
57 return self::$instance;
58 }
59 global $wgLocalFileRepo, $wgForeignFileRepos;
60 self::$instance = new RepoGroup( $wgLocalFileRepo, $wgForeignFileRepos );
61
62 return self::$instance;
63 }
64
65 /**
66 * Destroy the singleton instance, so that a new one will be created next
67 * time singleton() is called.
68 */
69 static function destroySingleton() {
70 self::$instance = null;
71 }
72
73 /**
74 * Set the singleton instance to a given object
75 * Used by extensions which hook into the Repo chain.
76 * It's not enough to just create a superclass ... you have
77 * to get people to call into it even though all they know is RepoGroup::singleton()
78 *
79 * @param $instance RepoGroup
80 */
81 static function setSingleton( $instance ) {
82 self::$instance = $instance;
83 }
84
85 /**
86 * Construct a group of file repositories.
87 *
88 * @param array $localInfo Associative array for local repo's info
89 * @param array $foreignInfo of repository info arrays.
90 * Each info array is an associative array with the 'class' member
91 * giving the class name. The entire array is passed to the repository
92 * constructor as the first parameter.
93 */
94 function __construct( $localInfo, $foreignInfo ) {
95 $this->localInfo = $localInfo;
96 $this->foreignInfo = $foreignInfo;
97 $this->cache = array();
98 }
99
100 /**
101 * Search repositories for an image.
102 * You can also use wfFindFile() to do this.
103 *
104 * @param $title Title|string Title object or string
105 * @param array $options Associative array of options:
106 * time: requested time for an archived image, or false for the
107 * current version. An image object will be returned which was
108 * created at the specified time.
109 *
110 * ignoreRedirect: If true, do not follow file redirects
111 *
112 * private: If true, return restricted (deleted) files if the current
113 * user is allowed to view them. Otherwise, such files will not
114 * be found.
115 *
116 * bypassCache: If true, do not use the process-local cache of File objects
117 * @return File object or false if it is not found
118 */
119 function findFile( $title, $options = array() ) {
120 if ( !is_array( $options ) ) {
121 // MW 1.15 compat
122 $options = array( 'time' => $options );
123 }
124 if ( !$this->reposInitialised ) {
125 $this->initialiseRepos();
126 }
127 $title = File::normalizeTitle( $title );
128 if ( !$title ) {
129 return false;
130 }
131
132 # Check the cache
133 if ( empty( $options['ignoreRedirect'] )
134 && empty( $options['private'] )
135 && empty( $options['bypassCache'] )
136 ) {
137 $time = isset( $options['time'] ) ? $options['time'] : '';
138 $dbkey = $title->getDBkey();
139 if ( isset( $this->cache[$dbkey][$time] ) ) {
140 wfDebug( __METHOD__ . ": got File:$dbkey from process cache\n" );
141 # Move it to the end of the list so that we can delete the LRU entry later
142 $this->pingCache( $dbkey );
143
144 # Return the entry
145 return $this->cache[$dbkey][$time];
146 }
147 $useCache = true;
148 } else {
149 $useCache = false;
150 }
151
152 # Check the local repo
153 $image = $this->localRepo->findFile( $title, $options );
154
155 # Check the foreign repos
156 if ( !$image ) {
157 foreach ( $this->foreignRepos as $repo ) {
158 $image = $repo->findFile( $title, $options );
159 if ( $image ) {
160 break;
161 }
162 }
163 }
164
165 $image = $image ? $image : false; // type sanity
166 # Cache file existence or non-existence
167 if ( $useCache && ( !$image || $image->isCacheable() ) ) {
168 $this->trimCache();
169 $this->cache[$dbkey][$time] = $image;
170 }
171
172 return $image;
173 }
174
175 /**
176 * @param $inputItems array
177 * @return array
178 */
179 function findFiles( $inputItems ) {
180 if ( !$this->reposInitialised ) {
181 $this->initialiseRepos();
182 }
183
184 $items = array();
185 foreach ( $inputItems as $item ) {
186 if ( !is_array( $item ) ) {
187 $item = array( 'title' => $item );
188 }
189 $item['title'] = File::normalizeTitle( $item['title'] );
190 if ( $item['title'] ) {
191 $items[$item['title']->getDBkey()] = $item;
192 }
193 }
194
195 $images = $this->localRepo->findFiles( $items );
196
197 foreach ( $this->foreignRepos as $repo ) {
198 // Remove found files from $items
199 foreach ( $images as $name => $image ) {
200 unset( $items[$name] );
201 }
202
203 $images = array_merge( $images, $repo->findFiles( $items ) );
204 }
205
206 return $images;
207 }
208
209 /**
210 * Interface for FileRepo::checkRedirect()
211 * @param $title Title
212 * @return bool
213 */
214 function checkRedirect( Title $title ) {
215 if ( !$this->reposInitialised ) {
216 $this->initialiseRepos();
217 }
218
219 $redir = $this->localRepo->checkRedirect( $title );
220 if ( $redir ) {
221 return $redir;
222 }
223 foreach ( $this->foreignRepos as $repo ) {
224 $redir = $repo->checkRedirect( $title );
225 if ( $redir ) {
226 return $redir;
227 }
228 }
229
230 return false;
231 }
232
233 /**
234 * Find an instance of the file with this key, created at the specified time
235 * Returns false if the file does not exist.
236 *
237 * @param string $hash base 36 SHA-1 hash
238 * @param array $options Option array, same as findFile()
239 * @return File object or false if it is not found
240 */
241 function findFileFromKey( $hash, $options = array() ) {
242 if ( !$this->reposInitialised ) {
243 $this->initialiseRepos();
244 }
245
246 $file = $this->localRepo->findFileFromKey( $hash, $options );
247 if ( !$file ) {
248 foreach ( $this->foreignRepos as $repo ) {
249 $file = $repo->findFileFromKey( $hash, $options );
250 if ( $file ) {
251 break;
252 }
253 }
254 }
255
256 return $file;
257 }
258
259 /**
260 * Find all instances of files with this key
261 *
262 * @param string $hash base 36 SHA-1 hash
263 * @return Array of File objects
264 */
265 function findBySha1( $hash ) {
266 if ( !$this->reposInitialised ) {
267 $this->initialiseRepos();
268 }
269
270 $result = $this->localRepo->findBySha1( $hash );
271 foreach ( $this->foreignRepos as $repo ) {
272 $result = array_merge( $result, $repo->findBySha1( $hash ) );
273 }
274 usort( $result, 'File::compare' );
275
276 return $result;
277 }
278
279 /**
280 * Find all instances of files with this keys
281 *
282 * @param array $hashes base 36 SHA-1 hashes
283 * @return Array of array of File objects
284 */
285 function findBySha1s( array $hashes ) {
286 if ( !$this->reposInitialised ) {
287 $this->initialiseRepos();
288 }
289
290 $result = $this->localRepo->findBySha1s( $hashes );
291 foreach ( $this->foreignRepos as $repo ) {
292 $result = array_merge_recursive( $result, $repo->findBySha1s( $hashes ) );
293 }
294 //sort the merged (and presorted) sublist of each hash
295 foreach ( $result as $hash => $files ) {
296 usort( $result[$hash], 'File::compare' );
297 }
298
299 return $result;
300 }
301
302 /**
303 * Get the repo instance with a given key.
304 * @param $index string|int
305 * @return bool|LocalRepo
306 */
307 function getRepo( $index ) {
308 if ( !$this->reposInitialised ) {
309 $this->initialiseRepos();
310 }
311 if ( $index === 'local' ) {
312 return $this->localRepo;
313 } elseif ( isset( $this->foreignRepos[$index] ) ) {
314 return $this->foreignRepos[$index];
315 } else {
316 return false;
317 }
318 }
319
320 /**
321 * Get the repo instance by its name
322 * @param $name string
323 * @return bool
324 */
325 function getRepoByName( $name ) {
326 if ( !$this->reposInitialised ) {
327 $this->initialiseRepos();
328 }
329 foreach ( $this->foreignRepos as $repo ) {
330 if ( $repo->name == $name ) {
331 return $repo;
332 }
333 }
334
335 return false;
336 }
337
338 /**
339 * Get the local repository, i.e. the one corresponding to the local image
340 * table. Files are typically uploaded to the local repository.
341 *
342 * @return LocalRepo
343 */
344 function getLocalRepo() {
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 $callback Callback: the function to call
353 * @param array $params optional additional parameters to pass to the function
354 * @return bool
355 */
356 function forEachForeignRepo( $callback, $params = array() ) {
357 foreach ( $this->foreignRepos as $repo ) {
358 $args = array_merge( array( $repo ), $params );
359 if ( call_user_func_array( $callback, $args ) ) {
360 return true;
361 }
362 }
363
364 return false;
365 }
366
367 /**
368 * Does the installation have any foreign repos set up?
369 * @return Boolean
370 */
371 function hasForeignRepos() {
372 return (bool)$this->foreignRepos;
373 }
374
375 /**
376 * Initialise the $repos array
377 */
378 function initialiseRepos() {
379 if ( $this->reposInitialised ) {
380 return;
381 }
382 $this->reposInitialised = true;
383
384 $this->localRepo = $this->newRepo( $this->localInfo );
385 $this->foreignRepos = array();
386 foreach ( $this->foreignInfo as $key => $info ) {
387 $this->foreignRepos[$key] = $this->newRepo( $info );
388 }
389 }
390
391 /**
392 * Create a repo class based on an info structure
393 */
394 protected function newRepo( $info ) {
395 $class = $info['class'];
396
397 return new $class( $info );
398 }
399
400 /**
401 * Split a virtual URL into repo, zone and rel parts
402 * @param $url string
403 * @throws MWException
404 * @return array containing repo, zone and rel
405 */
406 function splitVirtualUrl( $url ) {
407 if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
408 throw new MWException( __METHOD__ . ': unknown protocol' );
409 }
410
411 $bits = explode( '/', substr( $url, 9 ), 3 );
412 if ( count( $bits ) != 3 ) {
413 throw new MWException( __METHOD__ . ": invalid mwrepo URL: $url" );
414 }
415
416 return $bits;
417 }
418
419 /**
420 * @param $fileName string
421 * @return array
422 */
423 function getFileProps( $fileName ) {
424 if ( FileRepo::isVirtualUrl( $fileName ) ) {
425 list( $repoName, /* $zone */, /* $rel */ ) = $this->splitVirtualUrl( $fileName );
426 if ( $repoName === '' ) {
427 $repoName = 'local';
428 }
429 $repo = $this->getRepo( $repoName );
430
431 return $repo->getFileProps( $fileName );
432 } else {
433 return FSFile::getPropsFromPath( $fileName );
434 }
435 }
436
437 /**
438 * Move a cache entry to the top (such as when accessed)
439 */
440 protected function pingCache( $key ) {
441 if ( isset( $this->cache[$key] ) ) {
442 $tmp = $this->cache[$key];
443 unset( $this->cache[$key] );
444 $this->cache[$key] = $tmp;
445 }
446 }
447
448 /**
449 * Limit cache memory
450 */
451 protected function trimCache() {
452 while ( count( $this->cache ) >= self::MAX_CACHE_SIZE ) {
453 reset( $this->cache );
454 $key = key( $this->cache );
455 wfDebug( __METHOD__ . ": evicting $key\n" );
456 unset( $this->cache[$key] );
457 }
458 }
459
460 /**
461 * Clear RepoGroup process cache used for finding a file
462 * @param $title Title|null Title of the file or null to clear all files
463 */
464 public function clearCache( Title $title = null ) {
465 if ( $title == null ) {
466 $this->cache = array();
467 } else {
468 $dbKey = $title->getDBkey();
469 if ( isset( $this->cache[$dbKey] ) ) {
470 unset( $this->cache[$dbKey] );
471 }
472 }
473 }
474 }