Completed
Branch master (e2eefa)
by
unknown
25:58
created

FileRepo::storeBatch()   B

Complexity

Conditions 8
Paths 10

Size

Total Lines 60
Code Lines 36

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 8
eloc 36
nc 10
nop 2
dl 0
loc 60
rs 7.0677

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @defgroup FileRepo File Repository
4
 *
5
 * @brief This module handles how MediaWiki interacts with filesystems.
6
 *
7
 * @details
8
 */
9
10
/**
11
 * Base code for file repositories.
12
 *
13
 * This program is free software; you can redistribute it and/or modify
14
 * it under the terms of the GNU General Public License as published by
15
 * the Free Software Foundation; either version 2 of the License, or
16
 * (at your option) any later version.
17
 *
18
 * This program is distributed in the hope that it will be useful,
19
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
 * GNU General Public License for more details.
22
 *
23
 * You should have received a copy of the GNU General Public License along
24
 * with this program; if not, write to the Free Software Foundation, Inc.,
25
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
26
 * http://www.gnu.org/copyleft/gpl.html
27
 *
28
 * @file
29
 * @ingroup FileRepo
30
 */
31
32
/**
33
 * Base class for file repositories
34
 *
35
 * @ingroup FileRepo
36
 */
37
class FileRepo {
38
	const DELETE_SOURCE = 1;
39
	const OVERWRITE = 2;
40
	const OVERWRITE_SAME = 4;
41
	const SKIP_LOCKING = 8;
42
43
	const NAME_AND_TIME_ONLY = 1;
44
45
	/** @var bool Whether to fetch commons image description pages and display
46
	 *    them on the local wiki */
47
	public $fetchDescription;
48
49
	/** @var int */
50
	public $descriptionCacheExpiry;
51
52
	/** @var bool */
53
	protected $hasSha1Storage = false;
54
55
	/** @var bool */
56
	protected $supportsSha1URLs = false;
57
58
	/** @var FileBackend */
59
	protected $backend;
60
61
	/** @var array Map of zones to config */
62
	protected $zones = [];
63
64
	/** @var string URL of thumb.php */
65
	protected $thumbScriptUrl;
66
67
	/** @var bool Whether to skip media file transformation on parse and rely
68
	 *    on a 404 handler instead. */
69
	protected $transformVia404;
70
71
	/** @var string URL of image description pages, e.g.
72
	 *    https://en.wikipedia.org/wiki/File:
73
	 */
74
	protected $descBaseUrl;
75
76
	/** @var string URL of the MediaWiki installation, equivalent to
77
	 *    $wgScriptPath, e.g. https://en.wikipedia.org/w
78
	 */
79
	protected $scriptDirUrl;
80
81
	/** @var string Script extension of the MediaWiki installation, equivalent
82
	 *    to the old $wgScriptExtension, e.g. .php5 defaults to .php */
83
	protected $scriptExtension;
84
85
	/** @var string Equivalent to $wgArticlePath, e.g. https://en.wikipedia.org/wiki/$1 */
86
	protected $articleUrl;
87
88
	/** @var bool Equivalent to $wgCapitalLinks (or $wgCapitalLinkOverrides[NS_FILE],
89
	 *    determines whether filenames implicitly start with a capital letter.
90
	 *    The current implementation may give incorrect description page links
91
	 *    when the local $wgCapitalLinks and initialCapital are mismatched.
92
	 */
93
	protected $initialCapital;
94
95
	/** @var string May be 'paranoid' to remove all parameters from error
96
	 *    messages, 'none' to leave the paths in unchanged, or 'simple' to
97
	 *    replace paths with placeholders. Default for LocalRepo is
98
	 *    'simple'.
99
	 */
100
	protected $pathDisclosureProtection = 'simple';
101
102
	/** @var bool Public zone URL. */
103
	protected $url;
104
105
	/** @var string The base thumbnail URL. Defaults to "<url>/thumb". */
106
	protected $thumbUrl;
107
108
	/** @var int The number of directory levels for hash-based division of files */
109
	protected $hashLevels;
110
111
	/** @var int The number of directory levels for hash-based division of deleted files */
112
	protected $deletedHashLevels;
113
114
	/** @var int File names over this size will use the short form of thumbnail
115
	 *    names. Short thumbnail names only have the width, parameters, and the
116
	 *    extension.
117
	 */
118
	protected $abbrvThreshold;
119
120
	/** @var string The URL of the repo's favicon, if any */
121
	protected $favicon;
122
123
	/** @var bool Whether all zones should be private (e.g. private wiki repo) */
124
	protected $isPrivate;
125
126
	/** @var array callable Override these in the base class */
127
	protected $fileFactory = [ 'UnregisteredLocalFile', 'newFromTitle' ];
128
	/** @var array callable|bool Override these in the base class */
129
	protected $oldFileFactory = false;
130
	/** @var array callable|bool Override these in the base class */
131
	protected $fileFactoryKey = false;
132
	/** @var array callable|bool Override these in the base class */
133
	protected $oldFileFactoryKey = false;
134
135
	/**
136
	 * @param array|null $info
137
	 * @throws MWException
138
	 */
139
	public function __construct( array $info = null ) {
140
		// Verify required settings presence
141
		if (
142
			$info === null
143
			|| !array_key_exists( 'name', $info )
144
			|| !array_key_exists( 'backend', $info )
145
		) {
146
			throw new MWException( __CLASS__ .
147
				" requires an array of options having both 'name' and 'backend' keys.\n" );
148
		}
149
150
		// Required settings
151
		$this->name = $info['name'];
0 ignored issues
show
Bug introduced by
The property name does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
152
		if ( $info['backend'] instanceof FileBackend ) {
153
			$this->backend = $info['backend']; // useful for testing
154
		} else {
155
			$this->backend = FileBackendGroup::singleton()->get( $info['backend'] );
156
		}
157
158
		// Optional settings that can have no value
159
		$optionalSettings = [
160
			'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
161
			'thumbScriptUrl', 'pathDisclosureProtection', 'descriptionCacheExpiry',
162
			'scriptExtension', 'favicon'
163
		];
164
		foreach ( $optionalSettings as $var ) {
165
			if ( isset( $info[$var] ) ) {
166
				$this->$var = $info[$var];
167
			}
168
		}
169
170
		// Optional settings that have a default
171
		$this->initialCapital = isset( $info['initialCapital'] )
172
			? $info['initialCapital']
173
			: MWNamespace::isCapitalized( NS_FILE );
174
		$this->url = isset( $info['url'] )
175
			? $info['url']
176
			: false; // a subclass may set the URL (e.g. ForeignAPIRepo)
177
		if ( isset( $info['thumbUrl'] ) ) {
178
			$this->thumbUrl = $info['thumbUrl'];
179
		} else {
180
			$this->thumbUrl = $this->url ? "{$this->url}/thumb" : false;
181
		}
182
		$this->hashLevels = isset( $info['hashLevels'] )
183
			? $info['hashLevels']
184
			: 2;
185
		$this->deletedHashLevels = isset( $info['deletedHashLevels'] )
186
			? $info['deletedHashLevels']
187
			: $this->hashLevels;
188
		$this->transformVia404 = !empty( $info['transformVia404'] );
189
		$this->abbrvThreshold = isset( $info['abbrvThreshold'] )
190
			? $info['abbrvThreshold']
191
			: 255;
192
		$this->isPrivate = !empty( $info['isPrivate'] );
193
		// Give defaults for the basic zones...
194
		$this->zones = isset( $info['zones'] ) ? $info['zones'] : [];
195
		foreach ( [ 'public', 'thumb', 'transcoded', 'temp', 'deleted' ] as $zone ) {
196
			if ( !isset( $this->zones[$zone]['container'] ) ) {
197
				$this->zones[$zone]['container'] = "{$this->name}-{$zone}";
198
			}
199
			if ( !isset( $this->zones[$zone]['directory'] ) ) {
200
				$this->zones[$zone]['directory'] = '';
201
			}
202
			if ( !isset( $this->zones[$zone]['urlsByExt'] ) ) {
203
				$this->zones[$zone]['urlsByExt'] = [];
204
			}
205
		}
206
207
		$this->supportsSha1URLs = !empty( $info['supportsSha1URLs'] );
208
	}
209
210
	/**
211
	 * Get the file backend instance. Use this function wisely.
212
	 *
213
	 * @return FileBackend
214
	 */
215
	public function getBackend() {
216
		return $this->backend;
217
	}
218
219
	/**
220
	 * Get an explanatory message if this repo is read-only.
221
	 * This checks if an administrator disabled writes to the backend.
222
	 *
223
	 * @return string|bool Returns false if the repo is not read-only
224
	 */
225
	public function getReadOnlyReason() {
226
		return $this->backend->getReadOnlyReason();
227
	}
228
229
	/**
230
	 * Check if a single zone or list of zones is defined for usage
231
	 *
232
	 * @param array $doZones Only do a particular zones
233
	 * @throws MWException
234
	 * @return Status
235
	 */
236
	protected function initZones( $doZones = [] ) {
237
		$status = $this->newGood();
238
		foreach ( (array)$doZones as $zone ) {
239
			$root = $this->getZonePath( $zone );
240
			if ( $root === null ) {
241
				throw new MWException( "No '$zone' zone defined in the {$this->name} repo." );
242
			}
243
		}
244
245
		return $status;
246
	}
247
248
	/**
249
	 * Determine if a string is an mwrepo:// URL
250
	 *
251
	 * @param string $url
252
	 * @return bool
253
	 */
254
	public static function isVirtualUrl( $url ) {
255
		return substr( $url, 0, 9 ) == 'mwrepo://';
256
	}
257
258
	/**
259
	 * Get a URL referring to this repository, with the private mwrepo protocol.
260
	 * The suffix, if supplied, is considered to be unencoded, and will be
261
	 * URL-encoded before being returned.
262
	 *
263
	 * @param string|bool $suffix
264
	 * @return string
265
	 */
266
	public function getVirtualUrl( $suffix = false ) {
267
		$path = 'mwrepo://' . $this->name;
268
		if ( $suffix !== false ) {
269
			$path .= '/' . rawurlencode( $suffix );
270
		}
271
272
		return $path;
273
	}
274
275
	/**
276
	 * Get the URL corresponding to one of the four basic zones
277
	 *
278
	 * @param string $zone One of: public, deleted, temp, thumb
279
	 * @param string|null $ext Optional file extension
280
	 * @return string|bool
281
	 */
282
	public function getZoneUrl( $zone, $ext = null ) {
283
		if ( in_array( $zone, [ 'public', 'thumb', 'transcoded' ] ) ) {
284
			// standard public zones
285
			if ( $ext !== null && isset( $this->zones[$zone]['urlsByExt'][$ext] ) ) {
286
				// custom URL for extension/zone
287
				return $this->zones[$zone]['urlsByExt'][$ext];
288
			} elseif ( isset( $this->zones[$zone]['url'] ) ) {
289
				// custom URL for zone
290
				return $this->zones[$zone]['url'];
291
			}
292
		}
293
		switch ( $zone ) {
294
			case 'public':
295
				return $this->url;
296
			case 'temp':
297
			case 'deleted':
298
				return false; // no public URL
299
			case 'thumb':
300
				return $this->thumbUrl;
301
			case 'transcoded':
302
				return "{$this->url}/transcoded";
303
			default:
304
				return false;
305
		}
306
	}
307
308
	/**
309
	 * @return bool Whether non-ASCII path characters are allowed
310
	 */
311
	public function backendSupportsUnicodePaths() {
312
		return ( $this->getBackend()->getFeatures() & FileBackend::ATTR_UNICODE_PATHS );
313
	}
314
315
	/**
316
	 * Get the backend storage path corresponding to a virtual URL.
317
	 * Use this function wisely.
318
	 *
319
	 * @param string $url
320
	 * @throws MWException
321
	 * @return string
322
	 */
323
	public function resolveVirtualUrl( $url ) {
324
		if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
325
			throw new MWException( __METHOD__ . ': unknown protocol' );
326
		}
327
		$bits = explode( '/', substr( $url, 9 ), 3 );
328
		if ( count( $bits ) != 3 ) {
329
			throw new MWException( __METHOD__ . ": invalid mwrepo URL: $url" );
330
		}
331
		list( $repo, $zone, $rel ) = $bits;
332
		if ( $repo !== $this->name ) {
333
			throw new MWException( __METHOD__ . ": fetching from a foreign repo is not supported" );
334
		}
335
		$base = $this->getZonePath( $zone );
336
		if ( !$base ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $base of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
337
			throw new MWException( __METHOD__ . ": invalid zone: $zone" );
338
		}
339
340
		return $base . '/' . rawurldecode( $rel );
341
	}
342
343
	/**
344
	 * The the storage container and base path of a zone
345
	 *
346
	 * @param string $zone
347
	 * @return array (container, base path) or (null, null)
348
	 */
349
	protected function getZoneLocation( $zone ) {
350
		if ( !isset( $this->zones[$zone] ) ) {
351
			return [ null, null ]; // bogus
352
		}
353
354
		return [ $this->zones[$zone]['container'], $this->zones[$zone]['directory'] ];
355
	}
356
357
	/**
358
	 * Get the storage path corresponding to one of the zones
359
	 *
360
	 * @param string $zone
361
	 * @return string|null Returns null if the zone is not defined
362
	 */
363
	public function getZonePath( $zone ) {
364
		list( $container, $base ) = $this->getZoneLocation( $zone );
365
		if ( $container === null || $base === null ) {
366
			return null;
367
		}
368
		$backendName = $this->backend->getName();
369
		if ( $base != '' ) { // may not be set
370
			$base = "/{$base}";
371
		}
372
373
		return "mwstore://$backendName/{$container}{$base}";
374
	}
375
376
	/**
377
	 * Create a new File object from the local repository
378
	 *
379
	 * @param Title|string $title Title object or string
380
	 * @param bool|string $time Time at which the image was uploaded. If this
381
	 *   is specified, the returned object will be an instance of the
382
	 *   repository's old file class instead of a current file. Repositories
383
	 *   not supporting version control should return false if this parameter
384
	 *   is set.
385
	 * @return File|null A File, or null if passed an invalid Title
386
	 */
387
	public function newFile( $title, $time = false ) {
388
		$title = File::normalizeTitle( $title );
389
		if ( !$title ) {
390
			return null;
391
		}
392
		if ( $time ) {
393
			if ( $this->oldFileFactory ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->oldFileFactory of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
394
				return call_user_func( $this->oldFileFactory, $title, $this, $time );
395
			} else {
396
				return false;
397
			}
398
		} else {
399
			return call_user_func( $this->fileFactory, $title, $this );
400
		}
401
	}
402
403
	/**
404
	 * Find an instance of the named file created at the specified time
405
	 * Returns false if the file does not exist. Repositories not supporting
406
	 * version control should return false if the time is specified.
407
	 *
408
	 * @param Title|string $title Title object or string
409
	 * @param array $options Associative array of options:
410
	 *   time:           requested time for a specific file version, or false for the
411
	 *                   current version. An image object will be returned which was
412
	 *                   created at the specified time (which may be archived or current).
413
	 *   ignoreRedirect: If true, do not follow file redirects
414
	 *   private:        If true, return restricted (deleted) files if the current
415
	 *                   user is allowed to view them. Otherwise, such files will not
416
	 *                   be found. If a User object, use that user instead of the current.
417
	 *   latest:         If true, load from the latest available data into File objects
418
	 * @return File|bool False on failure
419
	 */
420
	public function findFile( $title, $options = [] ) {
421
		$title = File::normalizeTitle( $title );
422
		if ( !$title ) {
423
			return false;
424
		}
425
		if ( isset( $options['bypassCache'] ) ) {
426
			$options['latest'] = $options['bypassCache']; // b/c
427
		}
428
		$time = isset( $options['time'] ) ? $options['time'] : false;
429
		$flags = !empty( $options['latest'] ) ? File::READ_LATEST : 0;
430
		# First try the current version of the file to see if it precedes the timestamp
431
		$img = $this->newFile( $title );
432
		if ( !$img ) {
433
			return false;
434
		}
435
		$img->load( $flags );
436
		if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) {
437
			return $img;
438
		}
439
		# Now try an old version of the file
440
		if ( $time !== false ) {
441
			$img = $this->newFile( $title, $time );
442 View Code Duplication
			if ( $img ) {
443
				$img->load( $flags );
444
				if ( $img->exists() ) {
445
					if ( !$img->isDeleted( File::DELETED_FILE ) ) {
446
						return $img; // always OK
447
					} elseif ( !empty( $options['private'] ) &&
448
						$img->userCan( File::DELETED_FILE,
449
							$options['private'] instanceof User ? $options['private'] : null
450
						)
451
					) {
452
						return $img;
453
					}
454
				}
455
			}
456
		}
457
458
		# Now try redirects
459
		if ( !empty( $options['ignoreRedirect'] ) ) {
460
			return false;
461
		}
462
		$redir = $this->checkRedirect( $title );
463
		if ( $redir && $title->getNamespace() == NS_FILE ) {
464
			$img = $this->newFile( $redir );
465
			if ( !$img ) {
466
				return false;
467
			}
468
			$img->load( $flags );
469
			if ( $img->exists() ) {
470
				$img->redirectedFrom( $title->getDBkey() );
471
472
				return $img;
473
			}
474
		}
475
476
		return false;
477
	}
478
479
	/**
480
	 * Find many files at once.
481
	 *
482
	 * @param array $items An array of titles, or an array of findFile() options with
483
	 *    the "title" option giving the title. Example:
484
	 *
485
	 *     $findItem = array( 'title' => $title, 'private' => true );
486
	 *     $findBatch = array( $findItem );
487
	 *     $repo->findFiles( $findBatch );
488
	 *
489
	 *    No title should appear in $items twice, as the result use titles as keys
490
	 * @param int $flags Supports:
491
	 *     - FileRepo::NAME_AND_TIME_ONLY : return a (search title => (title,timestamp)) map.
492
	 *       The search title uses the input titles; the other is the final post-redirect title.
493
	 *       All titles are returned as string DB keys and the inner array is associative.
494
	 * @return array Map of (file name => File objects) for matches
495
	 */
496
	public function findFiles( array $items, $flags = 0 ) {
497
		$result = [];
498
		foreach ( $items as $item ) {
499
			if ( is_array( $item ) ) {
500
				$title = $item['title'];
501
				$options = $item;
502
				unset( $options['title'] );
503
			} else {
504
				$title = $item;
505
				$options = [];
506
			}
507
			$file = $this->findFile( $title, $options );
508
			if ( $file ) {
509
				$searchName = File::normalizeTitle( $title )->getDBkey(); // must be valid
510 View Code Duplication
				if ( $flags & self::NAME_AND_TIME_ONLY ) {
511
					$result[$searchName] = [
512
						'title' => $file->getTitle()->getDBkey(),
513
						'timestamp' => $file->getTimestamp()
514
					];
515
				} else {
516
					$result[$searchName] = $file;
517
				}
518
			}
519
		}
520
521
		return $result;
522
	}
523
524
	/**
525
	 * Find an instance of the file with this key, created at the specified time
526
	 * Returns false if the file does not exist. Repositories not supporting
527
	 * version control should return false if the time is specified.
528
	 *
529
	 * @param string $sha1 Base 36 SHA-1 hash
530
	 * @param array $options Option array, same as findFile().
531
	 * @return File|bool False on failure
532
	 */
533
	public function findFileFromKey( $sha1, $options = [] ) {
534
		$time = isset( $options['time'] ) ? $options['time'] : false;
535
		# First try to find a matching current version of a file...
536
		if ( $this->fileFactoryKey ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->fileFactoryKey of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
537
			$img = call_user_func( $this->fileFactoryKey, $sha1, $this, $time );
538
		} else {
539
			return false; // find-by-sha1 not supported
540
		}
541
		if ( $img && $img->exists() ) {
542
			return $img;
543
		}
544
		# Now try to find a matching old version of a file...
545
		if ( $time !== false && $this->oldFileFactoryKey ) { // find-by-sha1 supported?
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->oldFileFactoryKey of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
546
			$img = call_user_func( $this->oldFileFactoryKey, $sha1, $this, $time );
547 View Code Duplication
			if ( $img && $img->exists() ) {
548
				if ( !$img->isDeleted( File::DELETED_FILE ) ) {
549
					return $img; // always OK
550
				} elseif ( !empty( $options['private'] ) &&
551
					$img->userCan( File::DELETED_FILE,
552
						$options['private'] instanceof User ? $options['private'] : null
553
					)
554
				) {
555
					return $img;
556
				}
557
			}
558
		}
559
560
		return false;
561
	}
562
563
	/**
564
	 * Get an array or iterator of file objects for files that have a given
565
	 * SHA-1 content hash.
566
	 *
567
	 * STUB
568
	 * @param string $hash SHA-1 hash
569
	 * @return File[]
570
	 */
571
	public function findBySha1( $hash ) {
572
		return [];
573
	}
574
575
	/**
576
	 * Get an array of arrays or iterators of file objects for files that
577
	 * have the given SHA-1 content hashes.
578
	 *
579
	 * @param array $hashes An array of hashes
580
	 * @return array An Array of arrays or iterators of file objects and the hash as key
581
	 */
582
	public function findBySha1s( array $hashes ) {
583
		$result = [];
584
		foreach ( $hashes as $hash ) {
585
			$files = $this->findBySha1( $hash );
586
			if ( count( $files ) ) {
587
				$result[$hash] = $files;
588
			}
589
		}
590
591
		return $result;
592
	}
593
594
	/**
595
	 * Return an array of files where the name starts with $prefix.
596
	 *
597
	 * STUB
598
	 * @param string $prefix The prefix to search for
599
	 * @param int $limit The maximum amount of files to return
600
	 * @return array
601
	 */
602
	public function findFilesByPrefix( $prefix, $limit ) {
603
		return [];
604
	}
605
606
	/**
607
	 * Get the URL of thumb.php
608
	 *
609
	 * @return string
610
	 */
611
	public function getThumbScriptUrl() {
612
		return $this->thumbScriptUrl;
613
	}
614
615
	/**
616
	 * Returns true if the repository can transform files via a 404 handler
617
	 *
618
	 * @return bool
619
	 */
620
	public function canTransformVia404() {
621
		return $this->transformVia404;
622
	}
623
624
	/**
625
	 * Get the name of a file from its title object
626
	 *
627
	 * @param Title $title
628
	 * @return string
629
	 */
630
	public function getNameFromTitle( Title $title ) {
631
		global $wgContLang;
632
		if ( $this->initialCapital != MWNamespace::isCapitalized( NS_FILE ) ) {
633
			$name = $title->getUserCaseDBKey();
634
			if ( $this->initialCapital ) {
635
				$name = $wgContLang->ucfirst( $name );
636
			}
637
		} else {
638
			$name = $title->getDBkey();
639
		}
640
641
		return $name;
642
	}
643
644
	/**
645
	 * Get the public zone root storage directory of the repository
646
	 *
647
	 * @return string
648
	 */
649
	public function getRootDirectory() {
650
		return $this->getZonePath( 'public' );
651
	}
652
653
	/**
654
	 * Get a relative path including trailing slash, e.g. f/fa/
655
	 * If the repo is not hashed, returns an empty string
656
	 *
657
	 * @param string $name Name of file
658
	 * @return string
659
	 */
660
	public function getHashPath( $name ) {
661
		return self::getHashPathForLevel( $name, $this->hashLevels );
662
	}
663
664
	/**
665
	 * Get a relative path including trailing slash, e.g. f/fa/
666
	 * If the repo is not hashed, returns an empty string
667
	 *
668
	 * @param string $suffix Basename of file from FileRepo::storeTemp()
669
	 * @return string
670
	 */
671
	public function getTempHashPath( $suffix ) {
672
		$parts = explode( '!', $suffix, 2 ); // format is <timestamp>!<name> or just <name>
673
		$name = isset( $parts[1] ) ? $parts[1] : $suffix; // hash path is not based on timestamp
674
		return self::getHashPathForLevel( $name, $this->hashLevels );
675
	}
676
677
	/**
678
	 * @param string $name
679
	 * @param int $levels
680
	 * @return string
681
	 */
682 View Code Duplication
	protected static function getHashPathForLevel( $name, $levels ) {
683
		if ( $levels == 0 ) {
684
			return '';
685
		} else {
686
			$hash = md5( $name );
687
			$path = '';
688
			for ( $i = 1; $i <= $levels; $i++ ) {
689
				$path .= substr( $hash, 0, $i ) . '/';
690
			}
691
692
			return $path;
693
		}
694
	}
695
696
	/**
697
	 * Get the number of hash directory levels
698
	 *
699
	 * @return int
700
	 */
701
	public function getHashLevels() {
702
		return $this->hashLevels;
703
	}
704
705
	/**
706
	 * Get the name of this repository, as specified by $info['name]' to the constructor
707
	 *
708
	 * @return string
709
	 */
710
	public function getName() {
711
		return $this->name;
712
	}
713
714
	/**
715
	 * Make an url to this repo
716
	 *
717
	 * @param string $query Query string to append
718
	 * @param string $entry Entry point; defaults to index
719
	 * @return string|bool False on failure
720
	 */
721
	public function makeUrl( $query = '', $entry = 'index' ) {
722
		if ( isset( $this->scriptDirUrl ) ) {
723
			$ext = isset( $this->scriptExtension ) ? $this->scriptExtension : '.php';
724
725
			return wfAppendQuery( "{$this->scriptDirUrl}/{$entry}{$ext}", $query );
726
		}
727
728
		return false;
729
	}
730
731
	/**
732
	 * Get the URL of an image description page. May return false if it is
733
	 * unknown or not applicable. In general this should only be called by the
734
	 * File class, since it may return invalid results for certain kinds of
735
	 * repositories. Use File::getDescriptionUrl() in user code.
736
	 *
737
	 * In particular, it uses the article paths as specified to the repository
738
	 * constructor, whereas local repositories use the local Title functions.
739
	 *
740
	 * @param string $name
741
	 * @return string
742
	 */
743
	public function getDescriptionUrl( $name ) {
744
		$encName = wfUrlencode( $name );
745
		if ( !is_null( $this->descBaseUrl ) ) {
746
			# "http://example.com/wiki/File:"
747
			return $this->descBaseUrl . $encName;
748
		}
749
		if ( !is_null( $this->articleUrl ) ) {
750
			# "http://example.com/wiki/$1"
751
			# We use "Image:" as the canonical namespace for
752
			# compatibility across all MediaWiki versions.
753
			return str_replace( '$1',
754
				"Image:$encName", $this->articleUrl );
755
		}
756
		if ( !is_null( $this->scriptDirUrl ) ) {
757
			# "http://example.com/w"
758
			# We use "Image:" as the canonical namespace for
759
			# compatibility across all MediaWiki versions,
760
			# and just sort of hope index.php is right. ;)
761
			return $this->makeUrl( "title=Image:$encName" );
762
		}
763
764
		return false;
765
	}
766
767
	/**
768
	 * Get the URL of the content-only fragment of the description page. For
769
	 * MediaWiki this means action=render. This should only be called by the
770
	 * repository's file class, since it may return invalid results. User code
771
	 * should use File::getDescriptionText().
772
	 *
773
	 * @param string $name Name of image to fetch
774
	 * @param string $lang Language to fetch it in, if any.
775
	 * @return string
776
	 */
777
	public function getDescriptionRenderUrl( $name, $lang = null ) {
778
		$query = 'action=render';
779
		if ( !is_null( $lang ) ) {
780
			$query .= '&uselang=' . $lang;
781
		}
782
		if ( isset( $this->scriptDirUrl ) ) {
783
			return $this->makeUrl(
784
				'title=' .
785
				wfUrlencode( 'Image:' . $name ) .
786
				"&$query" );
787
		} else {
788
			$descUrl = $this->getDescriptionUrl( $name );
789
			if ( $descUrl ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $descUrl of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
790
				return wfAppendQuery( $descUrl, $query );
791
			} else {
792
				return false;
793
			}
794
		}
795
	}
796
797
	/**
798
	 * Get the URL of the stylesheet to apply to description pages
799
	 *
800
	 * @return string|bool False on failure
801
	 */
802
	public function getDescriptionStylesheetUrl() {
803
		if ( isset( $this->scriptDirUrl ) ) {
804
			return $this->makeUrl( 'title=MediaWiki:Filepage.css&' .
805
				wfArrayToCgi( Skin::getDynamicStylesheetQuery() ) );
806
		}
807
808
		return false;
809
	}
810
811
	/**
812
	 * Store a file to a given destination.
813
	 *
814
	 * @param string $srcPath Source file system path, storage path, or virtual URL
815
	 * @param string $dstZone Destination zone
816
	 * @param string $dstRel Destination relative path
817
	 * @param int $flags Bitwise combination of the following flags:
818
	 *   self::OVERWRITE         Overwrite an existing destination file instead of failing
819
	 *   self::OVERWRITE_SAME    Overwrite the file if the destination exists and has the
820
	 *                           same contents as the source
821
	 *   self::SKIP_LOCKING      Skip any file locking when doing the store
822
	 * @return FileRepoStatus
823
	 */
824
	public function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
825
		$this->assertWritableRepo(); // fail out if read-only
826
827
		$status = $this->storeBatch( [ [ $srcPath, $dstZone, $dstRel ] ], $flags );
828
		if ( $status->successCount == 0 ) {
829
			$status->ok = false;
0 ignored issues
show
Documentation introduced by
The property ok does not exist on object<FileRepoStatus>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
830
		}
831
832
		return $status;
833
	}
834
835
	/**
836
	 * Store a batch of files
837
	 *
838
	 * @param array $triplets (src, dest zone, dest rel) triplets as per store()
839
	 * @param int $flags Bitwise combination of the following flags:
840
	 *   self::OVERWRITE         Overwrite an existing destination file instead of failing
841
	 *   self::OVERWRITE_SAME    Overwrite the file if the destination exists and has the
842
	 *                           same contents as the source
843
	 *   self::SKIP_LOCKING      Skip any file locking when doing the store
844
	 * @throws MWException
845
	 * @return FileRepoStatus
846
	 */
847
	public function storeBatch( array $triplets, $flags = 0 ) {
848
		$this->assertWritableRepo(); // fail out if read-only
849
850
		if ( $flags & self::DELETE_SOURCE ) {
851
			throw new InvalidArgumentException( "DELETE_SOURCE not supported in " . __METHOD__ );
852
		}
853
854
		$status = $this->newGood();
855
		$backend = $this->backend; // convenience
856
857
		$operations = [];
858
		// Validate each triplet and get the store operation...
859
		foreach ( $triplets as $triplet ) {
860
			list( $srcPath, $dstZone, $dstRel ) = $triplet;
861
			wfDebug( __METHOD__
862
				. "( \$src='$srcPath', \$dstZone='$dstZone', \$dstRel='$dstRel' )\n"
863
			);
864
865
			// Resolve destination path
866
			$root = $this->getZonePath( $dstZone );
867
			if ( !$root ) {
868
				throw new MWException( "Invalid zone: $dstZone" );
869
			}
870
			if ( !$this->validateFilename( $dstRel ) ) {
871
				throw new MWException( 'Validation error in $dstRel' );
872
			}
873
			$dstPath = "$root/$dstRel";
874
			$dstDir = dirname( $dstPath );
875
			// Create destination directories for this triplet
876
			if ( !$this->initDirectory( $dstDir )->isOK() ) {
877
				return $this->newFatal( 'directorycreateerror', $dstDir );
878
			}
879
880
			// Resolve source to a storage path if virtual
881
			$srcPath = $this->resolveToStoragePath( $srcPath );
882
883
			// Get the appropriate file operation
884
			if ( FileBackend::isStoragePath( $srcPath ) ) {
885
				$opName = 'copy';
886
			} else {
887
				$opName = 'store';
888
			}
889
			$operations[] = [
890
				'op' => $opName,
891
				'src' => $srcPath,
892
				'dst' => $dstPath,
893
				'overwrite' => $flags & self::OVERWRITE,
894
				'overwriteSame' => $flags & self::OVERWRITE_SAME,
895
			];
896
		}
897
898
		// Execute the store operation for each triplet
899
		$opts = [ 'force' => true ];
900
		if ( $flags & self::SKIP_LOCKING ) {
901
			$opts['nonLocking'] = true;
902
		}
903
		$status->merge( $backend->doOperations( $operations, $opts ) );
904
905
		return $status;
906
	}
907
908
	/**
909
	 * Deletes a batch of files.
910
	 * Each file can be a (zone, rel) pair, virtual url, storage path.
911
	 * It will try to delete each file, but ignores any errors that may occur.
912
	 *
913
	 * @param array $files List of files to delete
914
	 * @param int $flags Bitwise combination of the following flags:
915
	 *   self::SKIP_LOCKING      Skip any file locking when doing the deletions
916
	 * @return FileRepoStatus
917
	 */
918
	public function cleanupBatch( array $files, $flags = 0 ) {
919
		$this->assertWritableRepo(); // fail out if read-only
920
921
		$status = $this->newGood();
922
923
		$operations = [];
924
		foreach ( $files as $path ) {
925
			if ( is_array( $path ) ) {
926
				// This is a pair, extract it
927
				list( $zone, $rel ) = $path;
928
				$path = $this->getZonePath( $zone ) . "/$rel";
929
			} else {
930
				// Resolve source to a storage path if virtual
931
				$path = $this->resolveToStoragePath( $path );
932
			}
933
			$operations[] = [ 'op' => 'delete', 'src' => $path ];
934
		}
935
		// Actually delete files from storage...
936
		$opts = [ 'force' => true ];
937
		if ( $flags & self::SKIP_LOCKING ) {
938
			$opts['nonLocking'] = true;
939
		}
940
		$status->merge( $this->backend->doOperations( $operations, $opts ) );
941
942
		return $status;
943
	}
944
945
	/**
946
	 * Import a file from the local file system into the repo.
947
	 * This does no locking nor journaling and overrides existing files.
948
	 * This function can be used to write to otherwise read-only foreign repos.
949
	 * This is intended for copying generated thumbnails into the repo.
950
	 *
951
	 * @param string|FSFile $src Source file system path, storage path, or virtual URL
952
	 * @param string $dst Virtual URL or storage path
953
	 * @param array|string|null $options An array consisting of a key named headers
954
	 *   listing extra headers. If a string, taken as content-disposition header.
955
	 *   (Support for array of options new in 1.23)
956
	 * @return FileRepoStatus
957
	 */
958
	final public function quickImport( $src, $dst, $options = null ) {
959
		return $this->quickImportBatch( [ [ $src, $dst, $options ] ] );
960
	}
961
962
	/**
963
	 * Purge a file from the repo. This does no locking nor journaling.
964
	 * This function can be used to write to otherwise read-only foreign repos.
965
	 * This is intended for purging thumbnails.
966
	 *
967
	 * @param string $path Virtual URL or storage path
968
	 * @return FileRepoStatus
969
	 */
970
	final public function quickPurge( $path ) {
971
		return $this->quickPurgeBatch( [ $path ] );
972
	}
973
974
	/**
975
	 * Deletes a directory if empty.
976
	 * This function can be used to write to otherwise read-only foreign repos.
977
	 *
978
	 * @param string $dir Virtual URL (or storage path) of directory to clean
979
	 * @return Status
980
	 */
981 View Code Duplication
	public function quickCleanDir( $dir ) {
982
		$status = $this->newGood();
983
		$status->merge( $this->backend->clean(
984
			[ 'dir' => $this->resolveToStoragePath( $dir ) ] ) );
985
986
		return $status;
987
	}
988
989
	/**
990
	 * Import a batch of files from the local file system into the repo.
991
	 * This does no locking nor journaling and overrides existing files.
992
	 * This function can be used to write to otherwise read-only foreign repos.
993
	 * This is intended for copying generated thumbnails into the repo.
994
	 *
995
	 * All path parameters may be a file system path, storage path, or virtual URL.
996
	 * When "headers" are given they are used as HTTP headers if supported.
997
	 *
998
	 * @param array $triples List of (source path or FSFile, destination path, disposition)
999
	 * @return FileRepoStatus
1000
	 */
1001
	public function quickImportBatch( array $triples ) {
1002
		$status = $this->newGood();
1003
		$operations = [];
1004
		foreach ( $triples as $triple ) {
1005
			list( $src, $dst ) = $triple;
1006
			if ( $src instanceof FSFile ) {
1007
				$op = 'store';
1008
			} else {
1009
				$src = $this->resolveToStoragePath( $src );
1010
				$op = FileBackend::isStoragePath( $src ) ? 'copy' : 'store';
1011
			}
1012
			$dst = $this->resolveToStoragePath( $dst );
1013
1014
			if ( !isset( $triple[2] ) ) {
1015
				$headers = [];
1016
			} elseif ( is_string( $triple[2] ) ) {
1017
				// back-compat
1018
				$headers = [ 'Content-Disposition' => $triple[2] ];
1019
			} elseif ( is_array( $triple[2] ) && isset( $triple[2]['headers'] ) ) {
1020
				$headers = $triple[2]['headers'];
1021
			} else {
1022
				$headers = [];
1023
			}
1024
1025
			$operations[] = [
1026
				'op' => $op,
1027
				'src' => $src,
1028
				'dst' => $dst,
1029
				'headers' => $headers
1030
			];
1031
			$status->merge( $this->initDirectory( dirname( $dst ) ) );
1032
		}
1033
		$status->merge( $this->backend->doQuickOperations( $operations ) );
1034
1035
		return $status;
1036
	}
1037
1038
	/**
1039
	 * Purge a batch of files from the repo.
1040
	 * This function can be used to write to otherwise read-only foreign repos.
1041
	 * This does no locking nor journaling and is intended for purging thumbnails.
1042
	 *
1043
	 * @param array $paths List of virtual URLs or storage paths
1044
	 * @return FileRepoStatus
1045
	 */
1046
	public function quickPurgeBatch( array $paths ) {
1047
		$status = $this->newGood();
1048
		$operations = [];
1049
		foreach ( $paths as $path ) {
1050
			$operations[] = [
1051
				'op' => 'delete',
1052
				'src' => $this->resolveToStoragePath( $path ),
1053
				'ignoreMissingSource' => true
1054
			];
1055
		}
1056
		$status->merge( $this->backend->doQuickOperations( $operations ) );
1057
1058
		return $status;
1059
	}
1060
1061
	/**
1062
	 * Pick a random name in the temp zone and store a file to it.
1063
	 * Returns a FileRepoStatus object with the file Virtual URL in the value,
1064
	 * file can later be disposed using FileRepo::freeTemp().
1065
	 *
1066
	 * @param string $originalName The base name of the file as specified
1067
	 *   by the user. The file extension will be maintained.
1068
	 * @param string $srcPath The current location of the file.
1069
	 * @return FileRepoStatus Object with the URL in the value.
1070
	 */
1071
	public function storeTemp( $originalName, $srcPath ) {
1072
		$this->assertWritableRepo(); // fail out if read-only
1073
1074
		$date = MWTimestamp::getInstance()->format( 'YmdHis' );
1075
		$hashPath = $this->getHashPath( $originalName );
1076
		$dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName );
1077
		$virtualUrl = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
1078
1079
		$result = $this->quickImport( $srcPath, $virtualUrl );
1080
		$result->value = $virtualUrl;
1081
1082
		return $result;
1083
	}
1084
1085
	/**
1086
	 * Remove a temporary file or mark it for garbage collection
1087
	 *
1088
	 * @param string $virtualUrl The virtual URL returned by FileRepo::storeTemp()
1089
	 * @return bool True on success, false on failure
1090
	 */
1091
	public function freeTemp( $virtualUrl ) {
1092
		$this->assertWritableRepo(); // fail out if read-only
1093
1094
		$temp = $this->getVirtualUrl( 'temp' );
1095
		if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) {
1096
			wfDebug( __METHOD__ . ": Invalid temp virtual URL\n" );
1097
1098
			return false;
1099
		}
1100
1101
		return $this->quickPurge( $virtualUrl )->isOK();
1102
	}
1103
1104
	/**
1105
	 * Concatenate a list of temporary files into a target file location.
1106
	 *
1107
	 * @param array $srcPaths Ordered list of source virtual URLs/storage paths
1108
	 * @param string $dstPath Target file system path
1109
	 * @param int $flags Bitwise combination of the following flags:
1110
	 *   self::DELETE_SOURCE     Delete the source files on success
1111
	 * @return FileRepoStatus
1112
	 */
1113
	public function concatenate( array $srcPaths, $dstPath, $flags = 0 ) {
1114
		$this->assertWritableRepo(); // fail out if read-only
1115
1116
		$status = $this->newGood();
1117
1118
		$sources = [];
1119
		foreach ( $srcPaths as $srcPath ) {
1120
			// Resolve source to a storage path if virtual
1121
			$source = $this->resolveToStoragePath( $srcPath );
1122
			$sources[] = $source; // chunk to merge
1123
		}
1124
1125
		// Concatenate the chunks into one FS file
1126
		$params = [ 'srcs' => $sources, 'dst' => $dstPath ];
1127
		$status->merge( $this->backend->concatenate( $params ) );
1128
		if ( !$status->isOK() ) {
1129
			return $status;
1130
		}
1131
1132
		// Delete the sources if required
1133
		if ( $flags & self::DELETE_SOURCE ) {
1134
			$status->merge( $this->quickPurgeBatch( $srcPaths ) );
1135
		}
1136
1137
		// Make sure status is OK, despite any quickPurgeBatch() fatals
1138
		$status->setResult( true );
1139
1140
		return $status;
1141
	}
1142
1143
	/**
1144
	 * Copy or move a file either from a storage path, virtual URL,
1145
	 * or file system path, into this repository at the specified destination location.
1146
	 *
1147
	 * Returns a FileRepoStatus object. On success, the value contains "new" or
1148
	 * "archived", to indicate whether the file was new with that name.
1149
	 *
1150
	 * Options to $options include:
1151
	 *   - headers : name/value map of HTTP headers to use in response to GET/HEAD requests
1152
	 *
1153
	 * @param string|FSFile $src The source file system path, storage path, or URL
1154
	 * @param string $dstRel The destination relative path
1155
	 * @param string $archiveRel The relative path where the existing file is to
1156
	 *   be archived, if there is one. Relative to the public zone root.
1157
	 * @param int $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
1158
	 *   that the source file should be deleted if possible
1159
	 * @param array $options Optional additional parameters
1160
	 * @return FileRepoStatus
1161
	 */
1162
	public function publish(
1163
		$src, $dstRel, $archiveRel, $flags = 0, array $options = []
1164
	) {
1165
		$this->assertWritableRepo(); // fail out if read-only
1166
1167
		$status = $this->publishBatch(
1168
			[ [ $src, $dstRel, $archiveRel, $options ] ], $flags );
1169
		if ( $status->successCount == 0 ) {
1170
			$status->ok = false;
0 ignored issues
show
Documentation introduced by
The property ok does not exist on object<FileRepoStatus>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1171
		}
1172
		if ( isset( $status->value[0] ) ) {
1173
			$status->value = $status->value[0];
1174
		} else {
1175
			$status->value = false;
1176
		}
1177
1178
		return $status;
1179
	}
1180
1181
	/**
1182
	 * Publish a batch of files
1183
	 *
1184
	 * @param array $ntuples (source, dest, archive) triplets or
1185
	 *   (source, dest, archive, options) 4-tuples as per publish().
1186
	 * @param int $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
1187
	 *   that the source files should be deleted if possible
1188
	 * @throws MWException
1189
	 * @return FileRepoStatus
1190
	 */
1191
	public function publishBatch( array $ntuples, $flags = 0 ) {
1192
		$this->assertWritableRepo(); // fail out if read-only
1193
1194
		$backend = $this->backend; // convenience
1195
		// Try creating directories
1196
		$status = $this->initZones( 'public' );
1197
		if ( !$status->isOK() ) {
1198
			return $status;
1199
		}
1200
1201
		$status = $this->newGood( [] );
1202
1203
		$operations = [];
1204
		$sourceFSFilesToDelete = []; // cleanup for disk source files
1205
		// Validate each triplet and get the store operation...
1206
		foreach ( $ntuples as $ntuple ) {
1207
			list( $src, $dstRel, $archiveRel ) = $ntuple;
1208
			$srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1209
1210
			$options = isset( $ntuple[3] ) ? $ntuple[3] : [];
1211
			// Resolve source to a storage path if virtual
1212
			$srcPath = $this->resolveToStoragePath( $srcPath );
1213
			if ( !$this->validateFilename( $dstRel ) ) {
1214
				throw new MWException( 'Validation error in $dstRel' );
1215
			}
1216
			if ( !$this->validateFilename( $archiveRel ) ) {
1217
				throw new MWException( 'Validation error in $archiveRel' );
1218
			}
1219
1220
			$publicRoot = $this->getZonePath( 'public' );
1221
			$dstPath = "$publicRoot/$dstRel";
1222
			$archivePath = "$publicRoot/$archiveRel";
1223
1224
			$dstDir = dirname( $dstPath );
1225
			$archiveDir = dirname( $archivePath );
1226
			// Abort immediately on directory creation errors since they're likely to be repetitive
1227
			if ( !$this->initDirectory( $dstDir )->isOK() ) {
1228
				return $this->newFatal( 'directorycreateerror', $dstDir );
1229
			}
1230
			if ( !$this->initDirectory( $archiveDir )->isOK() ) {
1231
				return $this->newFatal( 'directorycreateerror', $archiveDir );
1232
			}
1233
1234
			// Set any desired headers to be use in GET/HEAD responses
1235
			$headers = isset( $options['headers'] ) ? $options['headers'] : [];
1236
1237
			// Archive destination file if it exists.
1238
			// This will check if the archive file also exists and fail if does.
1239
			// This is a sanity check to avoid data loss. On Windows and Linux,
1240
			// copy() will overwrite, so the existence check is vulnerable to
1241
			// race conditions unless a functioning LockManager is used.
1242
			// LocalFile also uses SELECT FOR UPDATE for synchronization.
1243
			$operations[] = [
1244
				'op' => 'copy',
1245
				'src' => $dstPath,
1246
				'dst' => $archivePath,
1247
				'ignoreMissingSource' => true
1248
			];
1249
1250
			// Copy (or move) the source file to the destination
1251
			if ( FileBackend::isStoragePath( $srcPath ) ) {
1252
				if ( $flags & self::DELETE_SOURCE ) {
1253
					$operations[] = [
1254
						'op' => 'move',
1255
						'src' => $srcPath,
1256
						'dst' => $dstPath,
1257
						'overwrite' => true, // replace current
1258
						'headers' => $headers
1259
					];
1260
				} else {
1261
					$operations[] = [
1262
						'op' => 'copy',
1263
						'src' => $srcPath,
1264
						'dst' => $dstPath,
1265
						'overwrite' => true, // replace current
1266
						'headers' => $headers
1267
					];
1268
				}
1269
			} else { // FS source path
1270
				$operations[] = [
1271
					'op' => 'store',
1272
					'src' => $src, // prefer FSFile objects
1273
					'dst' => $dstPath,
1274
					'overwrite' => true, // replace current
1275
					'headers' => $headers
1276
				];
1277
				if ( $flags & self::DELETE_SOURCE ) {
1278
					$sourceFSFilesToDelete[] = $srcPath;
1279
				}
1280
			}
1281
		}
1282
1283
		// Execute the operations for each triplet
1284
		$status->merge( $backend->doOperations( $operations ) );
1285
		// Find out which files were archived...
1286
		foreach ( $ntuples as $i => $ntuple ) {
1287
			list( , , $archiveRel ) = $ntuple;
1288
			$archivePath = $this->getZonePath( 'public' ) . "/$archiveRel";
1289
			if ( $this->fileExists( $archivePath ) ) {
1290
				$status->value[$i] = 'archived';
1291
			} else {
1292
				$status->value[$i] = 'new';
1293
			}
1294
		}
1295
		// Cleanup for disk source files...
1296
		foreach ( $sourceFSFilesToDelete as $file ) {
1297
			MediaWiki\suppressWarnings();
1298
			unlink( $file ); // FS cleanup
1299
			MediaWiki\restoreWarnings();
1300
		}
1301
1302
		return $status;
1303
	}
1304
1305
	/**
1306
	 * Creates a directory with the appropriate zone permissions.
1307
	 * Callers are responsible for doing read-only and "writable repo" checks.
1308
	 *
1309
	 * @param string $dir Virtual URL (or storage path) of directory to clean
1310
	 * @return Status
1311
	 */
1312
	protected function initDirectory( $dir ) {
1313
		$path = $this->resolveToStoragePath( $dir );
1314
		list( , $container, ) = FileBackend::splitStoragePath( $path );
1315
1316
		$params = [ 'dir' => $path ];
1317
		if ( $this->isPrivate
1318
			|| $container === $this->zones['deleted']['container']
1319
			|| $container === $this->zones['temp']['container']
1320
		) {
1321
			# Take all available measures to prevent web accessibility of new deleted
1322
			# directories, in case the user has not configured offline storage
1323
			$params = [ 'noAccess' => true, 'noListing' => true ] + $params;
1324
		}
1325
1326
		return $this->backend->prepare( $params );
1327
	}
1328
1329
	/**
1330
	 * Deletes a directory if empty.
1331
	 *
1332
	 * @param string $dir Virtual URL (or storage path) of directory to clean
1333
	 * @return Status
1334
	 */
1335 View Code Duplication
	public function cleanDir( $dir ) {
1336
		$this->assertWritableRepo(); // fail out if read-only
1337
1338
		$status = $this->newGood();
1339
		$status->merge( $this->backend->clean(
1340
			[ 'dir' => $this->resolveToStoragePath( $dir ) ] ) );
1341
1342
		return $status;
1343
	}
1344
1345
	/**
1346
	 * Checks existence of a a file
1347
	 *
1348
	 * @param string $file Virtual URL (or storage path) of file to check
1349
	 * @return bool
1350
	 */
1351
	public function fileExists( $file ) {
1352
		$result = $this->fileExistsBatch( [ $file ] );
1353
1354
		return $result[0];
1355
	}
1356
1357
	/**
1358
	 * Checks existence of an array of files.
1359
	 *
1360
	 * @param array $files Virtual URLs (or storage paths) of files to check
1361
	 * @return array Map of files and existence flags, or false
1362
	 */
1363
	public function fileExistsBatch( array $files ) {
1364
		$paths = array_map( [ $this, 'resolveToStoragePath' ], $files );
1365
		$this->backend->preloadFileStat( [ 'srcs' => $paths ] );
1366
1367
		$result = [];
1368
		foreach ( $files as $key => $file ) {
1369
			$path = $this->resolveToStoragePath( $file );
1370
			$result[$key] = $this->backend->fileExists( [ 'src' => $path ] );
1371
		}
1372
1373
		return $result;
1374
	}
1375
1376
	/**
1377
	 * Move a file to the deletion archive.
1378
	 * If no valid deletion archive exists, this may either delete the file
1379
	 * or throw an exception, depending on the preference of the repository
1380
	 *
1381
	 * @param mixed $srcRel Relative path for the file to be deleted
1382
	 * @param mixed $archiveRel Relative path for the archive location.
1383
	 *   Relative to a private archive directory.
1384
	 * @return FileRepoStatus
1385
	 */
1386
	public function delete( $srcRel, $archiveRel ) {
1387
		$this->assertWritableRepo(); // fail out if read-only
1388
1389
		return $this->deleteBatch( [ [ $srcRel, $archiveRel ] ] );
1390
	}
1391
1392
	/**
1393
	 * Move a group of files to the deletion archive.
1394
	 *
1395
	 * If no valid deletion archive is configured, this may either delete the
1396
	 * file or throw an exception, depending on the preference of the repository.
1397
	 *
1398
	 * The overwrite policy is determined by the repository -- currently LocalRepo
1399
	 * assumes a naming scheme in the deleted zone based on content hash, as
1400
	 * opposed to the public zone which is assumed to be unique.
1401
	 *
1402
	 * @param array $sourceDestPairs Array of source/destination pairs. Each element
1403
	 *   is a two-element array containing the source file path relative to the
1404
	 *   public root in the first element, and the archive file path relative
1405
	 *   to the deleted zone root in the second element.
1406
	 * @throws MWException
1407
	 * @return FileRepoStatus
1408
	 */
1409
	public function deleteBatch( array $sourceDestPairs ) {
1410
		$this->assertWritableRepo(); // fail out if read-only
1411
1412
		// Try creating directories
1413
		$status = $this->initZones( [ 'public', 'deleted' ] );
1414
		if ( !$status->isOK() ) {
1415
			return $status;
1416
		}
1417
1418
		$status = $this->newGood();
1419
1420
		$backend = $this->backend; // convenience
1421
		$operations = [];
1422
		// Validate filenames and create archive directories
1423
		foreach ( $sourceDestPairs as $pair ) {
1424
			list( $srcRel, $archiveRel ) = $pair;
1425
			if ( !$this->validateFilename( $srcRel ) ) {
1426
				throw new MWException( __METHOD__ . ':Validation error in $srcRel' );
1427
			} elseif ( !$this->validateFilename( $archiveRel ) ) {
1428
				throw new MWException( __METHOD__ . ':Validation error in $archiveRel' );
1429
			}
1430
1431
			$publicRoot = $this->getZonePath( 'public' );
1432
			$srcPath = "{$publicRoot}/$srcRel";
1433
1434
			$deletedRoot = $this->getZonePath( 'deleted' );
1435
			$archivePath = "{$deletedRoot}/{$archiveRel}";
1436
			$archiveDir = dirname( $archivePath ); // does not touch FS
1437
1438
			// Create destination directories
1439
			if ( !$this->initDirectory( $archiveDir )->isOK() ) {
1440
				return $this->newFatal( 'directorycreateerror', $archiveDir );
1441
			}
1442
1443
			$operations[] = [
1444
				'op' => 'move',
1445
				'src' => $srcPath,
1446
				'dst' => $archivePath,
1447
				// We may have 2+ identical files being deleted,
1448
				// all of which will map to the same destination file
1449
				'overwriteSame' => true // also see bug 31792
1450
			];
1451
		}
1452
1453
		// Move the files by execute the operations for each pair.
1454
		// We're now committed to returning an OK result, which will
1455
		// lead to the files being moved in the DB also.
1456
		$opts = [ 'force' => true ];
1457
		$status->merge( $backend->doOperations( $operations, $opts ) );
1458
1459
		return $status;
1460
	}
1461
1462
	/**
1463
	 * Delete files in the deleted directory if they are not referenced in the filearchive table
1464
	 *
1465
	 * STUB
1466
	 * @param array $storageKeys
1467
	 */
1468
	public function cleanupDeletedBatch( array $storageKeys ) {
1469
		$this->assertWritableRepo();
1470
	}
1471
1472
	/**
1473
	 * Get a relative path for a deletion archive key,
1474
	 * e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg
1475
	 *
1476
	 * @param string $key
1477
	 * @throws MWException
1478
	 * @return string
1479
	 */
1480
	public function getDeletedHashPath( $key ) {
1481
		if ( strlen( $key ) < 31 ) {
1482
			throw new MWException( "Invalid storage key '$key'." );
1483
		}
1484
		$path = '';
1485
		for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) {
1486
			$path .= $key[$i] . '/';
1487
		}
1488
1489
		return $path;
1490
	}
1491
1492
	/**
1493
	 * If a path is a virtual URL, resolve it to a storage path.
1494
	 * Otherwise, just return the path as it is.
1495
	 *
1496
	 * @param string $path
1497
	 * @return string
1498
	 * @throws MWException
1499
	 */
1500
	protected function resolveToStoragePath( $path ) {
1501
		if ( $this->isVirtualUrl( $path ) ) {
1502
			return $this->resolveVirtualUrl( $path );
1503
		}
1504
1505
		return $path;
1506
	}
1507
1508
	/**
1509
	 * Get a local FS copy of a file with a given virtual URL/storage path.
1510
	 * Temporary files may be purged when the file object falls out of scope.
1511
	 *
1512
	 * @param string $virtualUrl
1513
	 * @return TempFSFile|null Returns null on failure
1514
	 */
1515
	public function getLocalCopy( $virtualUrl ) {
1516
		$path = $this->resolveToStoragePath( $virtualUrl );
1517
1518
		return $this->backend->getLocalCopy( [ 'src' => $path ] );
1519
	}
1520
1521
	/**
1522
	 * Get a local FS file with a given virtual URL/storage path.
1523
	 * The file is either an original or a copy. It should not be changed.
1524
	 * Temporary files may be purged when the file object falls out of scope.
1525
	 *
1526
	 * @param string $virtualUrl
1527
	 * @return FSFile|null Returns null on failure.
1528
	 */
1529
	public function getLocalReference( $virtualUrl ) {
1530
		$path = $this->resolveToStoragePath( $virtualUrl );
1531
1532
		return $this->backend->getLocalReference( [ 'src' => $path ] );
1533
	}
1534
1535
	/**
1536
	 * Get properties of a file with a given virtual URL/storage path.
1537
	 * Properties should ultimately be obtained via FSFile::getProps().
1538
	 *
1539
	 * @param string $virtualUrl
1540
	 * @return array
1541
	 */
1542
	public function getFileProps( $virtualUrl ) {
1543
		$path = $this->resolveToStoragePath( $virtualUrl );
1544
1545
		return $this->backend->getFileProps( [ 'src' => $path ] );
1546
	}
1547
1548
	/**
1549
	 * Get the timestamp of a file with a given virtual URL/storage path
1550
	 *
1551
	 * @param string $virtualUrl
1552
	 * @return string|bool False on failure
1553
	 */
1554
	public function getFileTimestamp( $virtualUrl ) {
1555
		$path = $this->resolveToStoragePath( $virtualUrl );
1556
1557
		return $this->backend->getFileTimestamp( [ 'src' => $path ] );
1558
	}
1559
1560
	/**
1561
	 * Get the size of a file with a given virtual URL/storage path
1562
	 *
1563
	 * @param string $virtualUrl
1564
	 * @return int|bool False on failure
1565
	 */
1566
	public function getFileSize( $virtualUrl ) {
1567
		$path = $this->resolveToStoragePath( $virtualUrl );
1568
1569
		return $this->backend->getFileSize( [ 'src' => $path ] );
1570
	}
1571
1572
	/**
1573
	 * Get the sha1 (base 36) of a file with a given virtual URL/storage path
1574
	 *
1575
	 * @param string $virtualUrl
1576
	 * @return string|bool
1577
	 */
1578
	public function getFileSha1( $virtualUrl ) {
1579
		$path = $this->resolveToStoragePath( $virtualUrl );
1580
1581
		return $this->backend->getFileSha1Base36( [ 'src' => $path ] );
1582
	}
1583
1584
	/**
1585
	 * Attempt to stream a file with the given virtual URL/storage path
1586
	 *
1587
	 * @param string $virtualUrl
1588
	 * @param array $headers Additional HTTP headers to send on success
1589
	 * @return Status
1590
	 * @since 1.27
1591
	 */
1592
	public function streamFileWithStatus( $virtualUrl, $headers = [] ) {
1593
		$path = $this->resolveToStoragePath( $virtualUrl );
1594
		$params = [ 'src' => $path, 'headers' => $headers ];
1595
1596
		return $this->backend->streamFile( $params );
1597
	}
1598
1599
	/**
1600
	 * Attempt to stream a file with the given virtual URL/storage path
1601
	 *
1602
	 * @deprecated since 1.26, use streamFileWithStatus
1603
	 * @param string $virtualUrl
1604
	 * @param array $headers Additional HTTP headers to send on success
1605
	 * @return bool Success
1606
	 */
1607
	public function streamFile( $virtualUrl, $headers = [] ) {
1608
		return $this->streamFileWithStatus( $virtualUrl, $headers )->isOK();
1609
	}
1610
1611
	/**
1612
	 * Call a callback function for every public regular file in the repository.
1613
	 * This only acts on the current version of files, not any old versions.
1614
	 * May use either the database or the filesystem.
1615
	 *
1616
	 * @param callable $callback
1617
	 * @return void
1618
	 */
1619
	public function enumFiles( $callback ) {
1620
		$this->enumFilesInStorage( $callback );
1621
	}
1622
1623
	/**
1624
	 * Call a callback function for every public file in the repository.
1625
	 * May use either the database or the filesystem.
1626
	 *
1627
	 * @param callable $callback
1628
	 * @return void
1629
	 */
1630
	protected function enumFilesInStorage( $callback ) {
1631
		$publicRoot = $this->getZonePath( 'public' );
1632
		$numDirs = 1 << ( $this->hashLevels * 4 );
1633
		// Use a priori assumptions about directory structure
1634
		// to reduce the tree height of the scanning process.
1635
		for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) {
1636
			$hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex );
1637
			$path = $publicRoot;
1638
			for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) {
1639
				$path .= '/' . substr( $hexString, 0, $hexPos + 1 );
1640
			}
1641
			$iterator = $this->backend->getFileList( [ 'dir' => $path ] );
1642
			foreach ( $iterator as $name ) {
0 ignored issues
show
Bug introduced by
The expression $iterator of type object<Traversable>|array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1643
				// Each item returned is a public file
1644
				call_user_func( $callback, "{$path}/{$name}" );
1645
			}
1646
		}
1647
	}
1648
1649
	/**
1650
	 * Determine if a relative path is valid, i.e. not blank or involving directory traveral
1651
	 *
1652
	 * @param string $filename
1653
	 * @return bool
1654
	 */
1655
	public function validateFilename( $filename ) {
1656
		if ( strval( $filename ) == '' ) {
1657
			return false;
1658
		}
1659
1660
		return FileBackend::isPathTraversalFree( $filename );
1661
	}
1662
1663
	/**
1664
	 * Get a callback function to use for cleaning error message parameters
1665
	 *
1666
	 * @return array
1667
	 */
1668
	function getErrorCleanupFunction() {
1669
		switch ( $this->pathDisclosureProtection ) {
1670
			case 'none':
1671
			case 'simple': // b/c
1672
				$callback = [ $this, 'passThrough' ];
1673
				break;
1674
			default: // 'paranoid'
1675
				$callback = [ $this, 'paranoidClean' ];
1676
		}
1677
		return $callback;
1678
	}
1679
1680
	/**
1681
	 * Path disclosure protection function
1682
	 *
1683
	 * @param string $param
1684
	 * @return string
1685
	 */
1686
	function paranoidClean( $param ) {
1687
		return '[hidden]';
1688
	}
1689
1690
	/**
1691
	 * Path disclosure protection function
1692
	 *
1693
	 * @param string $param
1694
	 * @return string
1695
	 */
1696
	function passThrough( $param ) {
1697
		return $param;
1698
	}
1699
1700
	/**
1701
	 * Create a new fatal error
1702
	 *
1703
	 * @param string $message
1704
	 * @return Status
1705
	 */
1706
	public function newFatal( $message /*, parameters...*/ ) {
1707
		$status = call_user_func_array( [ 'Status', 'newFatal' ], func_get_args() );
1708
		$status->cleanCallback = $this->getErrorCleanupFunction();
1709
1710
		return $status;
1711
	}
1712
1713
	/**
1714
	 * Create a new good result
1715
	 *
1716
	 * @param null|string $value
1717
	 * @return Status
1718
	 */
1719
	public function newGood( $value = null ) {
1720
		$status = Status::newGood( $value );
1721
		$status->cleanCallback = $this->getErrorCleanupFunction();
1722
1723
		return $status;
1724
	}
1725
1726
	/**
1727
	 * Checks if there is a redirect named as $title. If there is, return the
1728
	 * title object. If not, return false.
1729
	 * STUB
1730
	 *
1731
	 * @param Title $title Title of image
1732
	 * @return bool
1733
	 */
1734
	public function checkRedirect( Title $title ) {
1735
		return false;
1736
	}
1737
1738
	/**
1739
	 * Invalidates image redirect cache related to that image
1740
	 * Doesn't do anything for repositories that don't support image redirects.
1741
	 *
1742
	 * STUB
1743
	 * @param Title $title Title of image
1744
	 */
1745
	public function invalidateImageRedirect( Title $title ) {
1746
	}
1747
1748
	/**
1749
	 * Get the human-readable name of the repo
1750
	 *
1751
	 * @return string
1752
	 */
1753
	public function getDisplayName() {
1754
		global $wgSitename;
1755
1756
		if ( $this->isLocal() ) {
1757
			return $wgSitename;
1758
		}
1759
1760
		// 'shared-repo-name-wikimediacommons' is used when $wgUseInstantCommons = true
1761
		return wfMessageFallback( 'shared-repo-name-' . $this->name, 'shared-repo' )->text();
1762
	}
1763
1764
	/**
1765
	 * Get the portion of the file that contains the origin file name.
1766
	 * If that name is too long, then the name "thumbnail.<ext>" will be given.
1767
	 *
1768
	 * @param string $name
1769
	 * @return string
1770
	 */
1771
	public function nameForThumb( $name ) {
1772
		if ( strlen( $name ) > $this->abbrvThreshold ) {
1773
			$ext = FileBackend::extensionFromPath( $name );
1774
			$name = ( $ext == '' ) ? 'thumbnail' : "thumbnail.$ext";
1775
		}
1776
1777
		return $name;
1778
	}
1779
1780
	/**
1781
	 * Returns true if this the local file repository.
1782
	 *
1783
	 * @return bool
1784
	 */
1785
	public function isLocal() {
1786
		return $this->getName() == 'local';
1787
	}
1788
1789
	/**
1790
	 * Get a key on the primary cache for this repository.
1791
	 * Returns false if the repository's cache is not accessible at this site.
1792
	 * The parameters are the parts of the key, as for wfMemcKey().
1793
	 *
1794
	 * STUB
1795
	 * @return bool
1796
	 */
1797
	public function getSharedCacheKey( /*...*/ ) {
1798
		return false;
1799
	}
1800
1801
	/**
1802
	 * Get a key for this repo in the local cache domain. These cache keys are
1803
	 * not shared with remote instances of the repo.
1804
	 * The parameters are the parts of the key, as for wfMemcKey().
1805
	 *
1806
	 * @return string
1807
	 */
1808
	public function getLocalCacheKey( /*...*/ ) {
1809
		$args = func_get_args();
1810
		array_unshift( $args, 'filerepo', $this->getName() );
1811
1812
		return call_user_func_array( 'wfMemcKey', $args );
1813
	}
1814
1815
	/**
1816
	 * Get a temporary private FileRepo associated with this repo.
1817
	 *
1818
	 * Files will be created in the temp zone of this repo.
1819
	 * It will have the same backend as this repo.
1820
	 *
1821
	 * @return TempFileRepo
1822
	 */
1823
	public function getTempRepo() {
1824
		return new TempFileRepo( [
1825
			'name' => "{$this->name}-temp",
1826
			'backend' => $this->backend,
1827
			'zones' => [
1828
				'public' => [
1829
					// Same place storeTemp() uses in the base repo, though
1830
					// the path hashing is mismatched, which is annoying.
1831
					'container' => $this->zones['temp']['container'],
1832
					'directory' => $this->zones['temp']['directory']
1833
				],
1834
				'thumb' => [
1835
					'container' => $this->zones['temp']['container'],
1836
					'directory' => $this->zones['temp']['directory'] == ''
1837
						? 'thumb'
1838
						: $this->zones['temp']['directory'] . '/thumb'
1839
				],
1840
				'transcoded' => [
1841
					'container' => $this->zones['temp']['container'],
1842
					'directory' => $this->zones['temp']['directory'] == ''
1843
						? 'transcoded'
1844
						: $this->zones['temp']['directory'] . '/transcoded'
1845
				]
1846
			],
1847
			'hashLevels' => $this->hashLevels, // performance
1848
			'isPrivate' => true // all in temp zone
1849
		] );
1850
	}
1851
1852
	/**
1853
	 * Get an UploadStash associated with this repo.
1854
	 *
1855
	 * @param User $user
1856
	 * @return UploadStash
1857
	 */
1858
	public function getUploadStash( User $user = null ) {
1859
		return new UploadStash( $this, $user );
1860
	}
1861
1862
	/**
1863
	 * Throw an exception if this repo is read-only by design.
1864
	 * This does not and should not check getReadOnlyReason().
1865
	 *
1866
	 * @return void
1867
	 * @throws MWException
1868
	 */
1869
	protected function assertWritableRepo() {
1870
	}
1871
1872
	/**
1873
	 * Return information about the repository.
1874
	 *
1875
	 * @return array
1876
	 * @since 1.22
1877
	 */
1878
	public function getInfo() {
1879
		$ret = [
1880
			'name' => $this->getName(),
1881
			'displayname' => $this->getDisplayName(),
1882
			'rootUrl' => $this->getZoneUrl( 'public' ),
1883
			'local' => $this->isLocal(),
1884
		];
1885
1886
		$optionalSettings = [
1887
			'url', 'thumbUrl', 'initialCapital', 'descBaseUrl', 'scriptDirUrl', 'articleUrl',
1888
			'fetchDescription', 'descriptionCacheExpiry', 'scriptExtension', 'favicon'
1889
		];
1890
		foreach ( $optionalSettings as $k ) {
1891
			if ( isset( $this->$k ) ) {
1892
				$ret[$k] = $this->$k;
1893
			}
1894
		}
1895
1896
		return $ret;
1897
	}
1898
1899
	/**
1900
	 * Returns whether or not storage is SHA-1 based
1901
	 * @return bool
1902
	 */
1903
	public function hasSha1Storage() {
1904
		return $this->hasSha1Storage;
1905
	}
1906
1907
	/**
1908
	 * Returns whether or not repo supports having originals SHA-1s in the thumb URLs
1909
	 * @return bool
1910
	 */
1911
	public function supportsSha1URLs() {
1912
		return $this->supportsSha1URLs;
1913
	}
1914
}
1915
1916
/**
1917
 * FileRepo for temporary files created via FileRepo::getTempRepo()
1918
 */
1919
class TempFileRepo extends FileRepo {
1920
	public function getTempRepo() {
1921
		throw new MWException( "Cannot get a temp repo from a temp repo." );
1922
	}
1923
}
1924