Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
18.43% covered (danger)
18.43%
113 / 613
10.71% covered (danger)
10.71%
9 / 84
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileRepo
18.43% covered (danger)
18.43%
113 / 613
10.71% covered (danger)
10.71%
9 / 84
31497.27
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
14
 getBackend
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getReadOnlyReason
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 initZones
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 isVirtualUrl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVirtualUrl
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getZoneUrl
40.00% covered (danger)
40.00%
6 / 15
0.00% covered (danger)
0.00%
0 / 1
37.14
 backendSupportsUnicodePaths
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 resolveVirtualUrl
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 getZoneLocation
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getZonePath
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 newFile
50.00% covered (danger)
50.00%
4 / 8
0.00% covered (danger)
0.00%
0 / 1
6.00
 findFile
37.50% covered (danger)
37.50%
15 / 40
0.00% covered (danger)
0.00%
0 / 1
128.67
 findFiles
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
56
 findFileFromKey
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
182
 findBySha1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 findBySha1s
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 findFilesByPrefix
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getThumbScriptUrl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getThumbProxyUrl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getThumbProxySecret
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 canTransformVia404
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 canTransformLocally
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNameFromTitle
57.14% covered (warning)
57.14%
4 / 7
0.00% covered (danger)
0.00%
0 / 1
3.71
 getRootDirectory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHashPath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTempHashPath
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getHashPathForLevel
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getHashLevels
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeUrl
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getDescriptionUrl
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 getDescriptionRenderUrl
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 getDescriptionStylesheetUrl
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 store
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 storeBatch
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
110
 cleanupBatch
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 quickImport
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 quickImportBatch
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
72
 quickPurge
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 quickCleanDir
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 quickPurgeBatch
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 storeTemp
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 freeTemp
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 concatenate
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 publish
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 publishBatch
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 1
182
 initDirectory
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 cleanDir
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 fileExists
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 fileExistsBatch
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 delete
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 deleteBatch
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
30
 cleanupDeletedBatch
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDeletedHashPath
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 resolveToStoragePathIfVirtual
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getLocalCopy
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getLocalReference
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getFileProps
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getFileTimestamp
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getFileSize
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getFileSha1
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 streamFileWithStatus
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 enumFiles
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 enumFilesInStorage
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 validateFilename
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getErrorCleanupFunction
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 paranoidClean
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 passThrough
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newFatal
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 newGood
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 checkRedirect
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 invalidateImageRedirect
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDisplayName
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 nameForThumb
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 isLocal
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSharedCacheKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLocalCacheKey
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getTempRepo
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
12
 getUploadStash
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 assertWritableRepo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getInfo
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
4
 hasSha1Storage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 supportsSha1URLs
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @defgroup FileRepo File Repository
4 *
5 * @brief This module handles how MediaWiki interacts with filesystems.
6 *
7 * @details
8 */
9
10use MediaWiki\Context\RequestContext;
11use MediaWiki\Linker\LinkTarget;
12use MediaWiki\MainConfigNames;
13use MediaWiki\MediaWikiServices;
14use MediaWiki\Page\PageIdentity;
15use MediaWiki\Permissions\Authority;
16use MediaWiki\Status\Status;
17use MediaWiki\Title\Title;
18use MediaWiki\User\UserIdentity;
19use MediaWiki\Utils\MWTimestamp;
20use Wikimedia\AtEase\AtEase;
21
22/**
23 * Base code for file repositories.
24 *
25 * This program is free software; you can redistribute it and/or modify
26 * it under the terms of the GNU General Public License as published by
27 * the Free Software Foundation; either version 2 of the License, or
28 * (at your option) any later version.
29 *
30 * This program is distributed in the hope that it will be useful,
31 * but WITHOUT ANY WARRANTY; without even the implied warranty of
32 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
33 * GNU General Public License for more details.
34 *
35 * You should have received a copy of the GNU General Public License along
36 * with this program; if not, write to the Free Software Foundation, Inc.,
37 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
38 * http://www.gnu.org/copyleft/gpl.html
39 *
40 * @file
41 * @ingroup FileRepo
42 */
43
44/**
45 * Base class for file repositories
46 *
47 * See [the architecture doc](@ref filerepoarch) for more information.
48 *
49 * @ingroup FileRepo
50 */
51class FileRepo {
52    public const DELETE_SOURCE = 1;
53    public const OVERWRITE = 2;
54    public const OVERWRITE_SAME = 4;
55    public const SKIP_LOCKING = 8;
56
57    public const NAME_AND_TIME_ONLY = 1;
58
59    /** @var bool Whether to fetch commons image description pages and display
60     *    them on the local wiki
61     */
62    public $fetchDescription;
63
64    /** @var int */
65    public $descriptionCacheExpiry;
66
67    /** @var bool */
68    protected $hasSha1Storage = false;
69
70    /** @var bool */
71    protected $supportsSha1URLs = false;
72
73    /** @var FileBackend */
74    protected $backend;
75
76    /** @var array Map of zones to config */
77    protected $zones = [];
78
79    /** @var string URL of thumb.php */
80    protected $thumbScriptUrl;
81
82    /** @var bool Whether to skip media file transformation on parse and rely
83     *    on a 404 handler instead.
84     */
85    protected $transformVia404;
86
87    /** @var string URL of image description pages, e.g.
88     *    https://en.wikipedia.org/wiki/File:
89     */
90    protected $descBaseUrl;
91
92    /** @var string URL of the MediaWiki installation, equivalent to
93     *    $wgScriptPath, e.g. https://en.wikipedia.org/w
94     */
95    protected $scriptDirUrl;
96
97    /** @var string Equivalent to $wgArticlePath, e.g. https://en.wikipedia.org/wiki/$1 */
98    protected $articleUrl;
99
100    /** @var bool Equivalent to $wgCapitalLinks (or $wgCapitalLinkOverrides[NS_FILE],
101     *    determines whether filenames implicitly start with a capital letter.
102     *    The current implementation may give incorrect description page links
103     *    when the local $wgCapitalLinks and initialCapital are mismatched.
104     */
105    protected $initialCapital;
106
107    /** @var string May be 'paranoid' to remove all parameters from error
108     *    messages, 'none' to leave the paths in unchanged, or 'simple' to
109     *    replace paths with placeholders. Default for LocalRepo is
110     *    'simple'.
111     */
112    protected $pathDisclosureProtection = 'simple';
113
114    /** @var string|false Public zone URL. */
115    protected $url;
116
117    /** @var string|false The base thumbnail URL. Defaults to "<url>/thumb". */
118    protected $thumbUrl;
119
120    /** @var int The number of directory levels for hash-based division of files */
121    protected $hashLevels;
122
123    /** @var int The number of directory levels for hash-based division of deleted files */
124    protected $deletedHashLevels;
125
126    /** @var int File names over this size will use the short form of thumbnail
127     *    names. Short thumbnail names only have the width, parameters, and the
128     *    extension.
129     */
130    protected $abbrvThreshold;
131
132    /** @var null|string The URL to a favicon (optional, may be a server-local path URL). */
133    protected $favicon = null;
134
135    /** @var bool Whether all zones should be private (e.g. private wiki repo) */
136    protected $isPrivate;
137
138    /** @var callable Override these in the base class */
139    protected $fileFactory = [ UnregisteredLocalFile::class, 'newFromTitle' ];
140    /** @var callable|false Override these in the base class */
141    protected $oldFileFactory = false;
142    /** @var callable|false Override these in the base class */
143    protected $fileFactoryKey = false;
144    /** @var callable|false Override these in the base class */
145    protected $oldFileFactoryKey = false;
146
147    /** @var string URL of where to proxy thumb.php requests to.
148     *    Example: http://127.0.0.1:8888/wiki/dev/thumb/
149     */
150    protected $thumbProxyUrl;
151    /** @var string Secret key to pass as an X-Swift-Secret header to the proxied thumb service */
152    protected $thumbProxySecret;
153
154    /** @var bool Disable local image scaling */
155    protected $disableLocalTransform = false;
156
157    /** @var WANObjectCache */
158    protected $wanCache;
159
160    /**
161     * @var string
162     * @note Use $this->getName(). Public for back-compat only
163     * @todo make protected
164     */
165    public $name;
166
167    /**
168     * @see Documentation of info options at $wgLocalFileRepo
169     * @param array|null $info
170     * @phan-assert array $info
171     */
172    public function __construct( array $info = null ) {
173        // Verify required settings presence
174        if (
175            $info === null
176            || !array_key_exists( 'name', $info )
177            || !array_key_exists( 'backend', $info )
178        ) {
179            throw new InvalidArgumentException( __CLASS__ .
180                " requires an array of options having both 'name' and 'backend' keys.\n" );
181        }
182
183        // Required settings
184        $this->name = $info['name'];
185        if ( $info['backend'] instanceof FileBackend ) {
186            $this->backend = $info['backend']; // useful for testing
187        } else {
188            $this->backend =
189                MediaWikiServices::getInstance()->getFileBackendGroup()->get( $info['backend'] );
190        }
191
192        // Optional settings that can have no value
193        $optionalSettings = [
194            'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
195            'thumbScriptUrl', 'pathDisclosureProtection', 'descriptionCacheExpiry',
196            'favicon', 'thumbProxyUrl', 'thumbProxySecret', 'disableLocalTransform'
197        ];
198        foreach ( $optionalSettings as $var ) {
199            if ( isset( $info[$var] ) ) {
200                $this->$var = $info[$var];
201            }
202        }
203
204        // Optional settings that have a default
205        $localCapitalLinks =
206            MediaWikiServices::getInstance()->getNamespaceInfo()->isCapitalized( NS_FILE );
207        $this->initialCapital = $info['initialCapital'] ?? $localCapitalLinks;
208        if ( $localCapitalLinks && !$this->initialCapital ) {
209            // If the local wiki's file namespace requires an initial capital, but a foreign file
210            // repo doesn't, complications will result. Linker code will want to auto-capitalize the
211            // first letter of links to files, but those links might actually point to files on
212            // foreign wikis with initial-lowercase names. This combination is not likely to be
213            // used by anyone anyway, so we just outlaw it to save ourselves the bugs. If you want
214            // to include a foreign file repo with initialCapital false, set your local file
215            // namespace to not be capitalized either.
216            throw new InvalidArgumentException(
217                'File repos with initial capital false are not allowed on wikis where the File ' .
218                'namespace has initial capital true' );
219        }
220
221        $this->url = $info['url'] ?? false; // a subclass may set the URL (e.g. ForeignAPIRepo)
222        $defaultThumbUrl = $this->url ? $this->url . '/thumb' : false;
223        $this->thumbUrl = $info['thumbUrl'] ?? $defaultThumbUrl;
224        $this->hashLevels = $info['hashLevels'] ?? 2;
225        $this->deletedHashLevels = $info['deletedHashLevels'] ?? $this->hashLevels;
226        $this->transformVia404 = !empty( $info['transformVia404'] );
227        $this->abbrvThreshold = $info['abbrvThreshold'] ?? 255;
228        $this->isPrivate = !empty( $info['isPrivate'] );
229        // Give defaults for the basic zones...
230        $this->zones = $info['zones'] ?? [];
231        foreach ( [ 'public', 'thumb', 'transcoded', 'temp', 'deleted' ] as $zone ) {
232            if ( !isset( $this->zones[$zone]['container'] ) ) {
233                $this->zones[$zone]['container'] = "{$this->name}-{$zone}";
234            }
235            if ( !isset( $this->zones[$zone]['directory'] ) ) {
236                $this->zones[$zone]['directory'] = '';
237            }
238            if ( !isset( $this->zones[$zone]['urlsByExt'] ) ) {
239                $this->zones[$zone]['urlsByExt'] = [];
240            }
241        }
242
243        $this->supportsSha1URLs = !empty( $info['supportsSha1URLs'] );
244
245        $this->wanCache = $info['wanCache'] ?? WANObjectCache::newEmpty();
246    }
247
248    /**
249     * Get the file backend instance. Use this function wisely.
250     *
251     * @return FileBackend
252     */
253    public function getBackend() {
254        return $this->backend;
255    }
256
257    /**
258     * Get an explanatory message if this repo is read-only.
259     * This checks if an administrator disabled writes to the backend.
260     *
261     * @return string|false Returns false if the repo is not read-only
262     */
263    public function getReadOnlyReason() {
264        return $this->backend->getReadOnlyReason();
265    }
266
267    /**
268     * Ensure that a single zone or list of zones is defined for usage
269     *
270     * @param string[]|string $doZones Only do a particular zones
271     */
272    protected function initZones( $doZones = [] ): void {
273        foreach ( (array)$doZones as $zone ) {
274            $root = $this->getZonePath( $zone );
275            if ( $root === null ) {
276                throw new RuntimeException( "No '$zone' zone defined in the {$this->name} repo." );
277            }
278        }
279    }
280
281    /**
282     * Determine if a string is an mwrepo:// URL
283     *
284     * @param string $url
285     * @return bool
286     */
287    public static function isVirtualUrl( $url ) {
288        return str_starts_with( $url, 'mwrepo://' );
289    }
290
291    /**
292     * Get a URL referring to this repository, with the private mwrepo protocol.
293     * The suffix, if supplied, is considered to be unencoded, and will be
294     * URL-encoded before being returned.
295     *
296     * @param string|false $suffix
297     * @return string
298     */
299    public function getVirtualUrl( $suffix = false ) {
300        $path = 'mwrepo://' . $this->name;
301        if ( $suffix !== false ) {
302            $path .= '/' . rawurlencode( $suffix );
303        }
304
305        return $path;
306    }
307
308    /**
309     * Get the URL corresponding to one of the four basic zones
310     *
311     * @param string $zone One of: public, deleted, temp, thumb
312     * @param string|null $ext Optional file extension
313     * @return string|false
314     */
315    public function getZoneUrl( $zone, $ext = null ) {
316        if ( in_array( $zone, [ 'public', 'thumb', 'transcoded' ] ) ) {
317            // standard public zones
318            if ( $ext !== null && isset( $this->zones[$zone]['urlsByExt'][$ext] ) ) {
319                // custom URL for extension/zone
320                // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
321                return $this->zones[$zone]['urlsByExt'][$ext];
322            } elseif ( isset( $this->zones[$zone]['url'] ) ) {
323                // custom URL for zone
324                return $this->zones[$zone]['url'];
325            }
326        }
327        switch ( $zone ) {
328            case 'public':
329                return $this->url;
330            case 'temp':
331            case 'deleted':
332                return false; // no public URL
333            case 'thumb':
334                return $this->thumbUrl;
335            case 'transcoded':
336                return "{$this->url}/transcoded";
337            default:
338                return false;
339        }
340    }
341
342    /**
343     * @return bool Whether non-ASCII path characters are allowed
344     */
345    public function backendSupportsUnicodePaths() {
346        return (bool)( $this->getBackend()->getFeatures() & FileBackend::ATTR_UNICODE_PATHS );
347    }
348
349    /**
350     * Get the backend storage path corresponding to a virtual URL. Callers are responsible of
351     * verifying that $url is a valid virtual URL.
352     * Use this function wisely.
353     *
354     * @param string $url
355     * @return string
356     */
357    public function resolveVirtualUrl( $url ) {
358        if ( !str_starts_with( $url, 'mwrepo://' ) ) {
359            throw new InvalidArgumentException( __METHOD__ . ': unknown protocol' );
360        }
361        $bits = explode( '/', substr( $url, 9 ), 3 );
362        if ( count( $bits ) != 3 ) {
363            throw new InvalidArgumentException( __METHOD__ . ": invalid mwrepo URL: $url" );
364        }
365        [ $repo, $zone, $rel ] = $bits;
366        if ( $repo !== $this->name ) {
367            throw new InvalidArgumentException( __METHOD__ . ": fetching from a foreign repo is not supported" );
368        }
369        $base = $this->getZonePath( $zone );
370        if ( !$base ) {
371            throw new InvalidArgumentException( __METHOD__ . ": invalid zone: $zone" );
372        }
373
374        return $base . '/' . rawurldecode( $rel );
375    }
376
377    /**
378     * The storage container and base path of a zone
379     *
380     * @param string $zone
381     * @return array (container, base path) or (null, null)
382     */
383    protected function getZoneLocation( $zone ) {
384        if ( !isset( $this->zones[$zone] ) ) {
385            return [ null, null ]; // bogus
386        }
387
388        return [ $this->zones[$zone]['container'], $this->zones[$zone]['directory'] ];
389    }
390
391    /**
392     * Get the storage path corresponding to one of the zones
393     *
394     * @param string $zone
395     * @return string|null Returns null if the zone is not defined
396     */
397    public function getZonePath( $zone ) {
398        [ $container, $base ] = $this->getZoneLocation( $zone );
399        if ( $container === null || $base === null ) {
400            return null;
401        }
402        $backendName = $this->backend->getName();
403        if ( $base != '' ) { // may not be set
404            $base = "/{$base}";
405        }
406
407        return "mwstore://$backendName/{$container}{$base}";
408    }
409
410    /**
411     * Create a new File object from the local repository
412     *
413     * @param PageIdentity|LinkTarget|string $title
414     * @param string|false $time Time at which the image was uploaded. If this
415     *   is specified, the returned object will be an instance of the
416     *   repository's old file class instead of a current file. Repositories
417     *   not supporting version control should return false if this parameter
418     *   is set.
419     * @return File|null A File, or null if passed an invalid Title
420     */
421    public function newFile( $title, $time = false ) {
422        $title = File::normalizeTitle( $title );
423        if ( !$title ) {
424            return null;
425        }
426        if ( $time ) {
427            if ( $this->oldFileFactory ) {
428                return call_user_func( $this->oldFileFactory, $title, $this, $time );
429            } else {
430                return null;
431            }
432        } else {
433            return call_user_func( $this->fileFactory, $title, $this );
434        }
435    }
436
437    /**
438     * Find an instance of the named file created at the specified time
439     * Returns false if the file does not exist. Repositories not supporting
440     * version control should return false if the time is specified.
441     *
442     * @param PageIdentity|LinkTarget|string $title
443     * @param array $options Associative array of options:
444     *   time:           requested time for a specific file version, or false for the
445     *                   current version. An image object will be returned which was
446     *                   created at the specified time (which may be archived or current).
447     *   ignoreRedirect: If true, do not follow file redirects
448     *   private:        If an Authority object, return restricted (deleted) files if the
449     *                   performer is allowed to view them. Otherwise, such files will not
450     *                   be found. If set and not an Authority object, throws an exception.
451     *                   Authority is only accepted since 1.37, User was required before.
452     *   latest:         If true, load from the latest available data into File objects
453     * @return File|false False on failure
454     * @throws InvalidArgumentException
455     */
456    public function findFile( $title, $options = [] ) {
457        if ( !empty( $options['private'] ) && !( $options['private'] instanceof Authority ) ) {
458            throw new InvalidArgumentException(
459                __METHOD__ . ' called with the `private` option set to something ' .
460                'other than an Authority object'
461            );
462        }
463
464        $title = File::normalizeTitle( $title );
465        if ( !$title ) {
466            return false;
467        }
468        if ( isset( $options['bypassCache'] ) ) {
469            $options['latest'] = $options['bypassCache']; // b/c
470        }
471        $time = $options['time'] ?? false;
472        $flags = !empty( $options['latest'] ) ? IDBAccessObject::READ_LATEST : 0;
473        # First try the current version of the file to see if it precedes the timestamp
474        $img = $this->newFile( $title );
475        if ( !$img ) {
476            return false;
477        }
478        $img->load( $flags );
479        if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) {
480            return $img;
481        }
482        # Now try an old version of the file
483        if ( $time !== false ) {
484            $img = $this->newFile( $title, $time );
485            if ( $img ) {
486                $img->load( $flags );
487                if ( $img->exists() ) {
488                    if ( !$img->isDeleted( File::DELETED_FILE ) ) {
489                        return $img; // always OK
490                    } elseif (
491                        // If its not empty, its an Authority object
492                        !empty( $options['private'] ) &&
493                        $img->userCan( File::DELETED_FILE, $options['private'] )
494                    ) {
495                        return $img;
496                    }
497                }
498            }
499        }
500
501        # Now try redirects
502        if ( !empty( $options['ignoreRedirect'] ) ) {
503            return false;
504        }
505        $redir = $this->checkRedirect( $title );
506        if ( $redir && $title->getNamespace() === NS_FILE ) {
507            $img = $this->newFile( $redir );
508            if ( !$img ) {
509                return false;
510            }
511            $img->load( $flags );
512            if ( $img->exists() ) {
513                $img->redirectedFrom( $title->getDBkey() );
514
515                return $img;
516            }
517        }
518
519        return false;
520    }
521
522    /**
523     * Find many files at once.
524     *
525     * @param array $items An array of titles, or an array of findFile() options with
526     *    the "title" option giving the title. Example:
527     *
528     *     $findItem = [ 'title' => $title, 'private' => true ];
529     *     $findBatch = [ $findItem ];
530     *     $repo->findFiles( $findBatch );
531     *
532     *    No title should appear in $items twice, as the result use titles as keys
533     * @param int $flags Supports:
534     *     - FileRepo::NAME_AND_TIME_ONLY : return a (search title => (title,timestamp)) map.
535     *       The search title uses the input titles; the other is the final post-redirect title.
536     *       All titles are returned as string DB keys and the inner array is associative.
537     * @return array Map of (file name => File objects) for matches or (search title => (title,timestamp))
538     */
539    public function findFiles( array $items, $flags = 0 ) {
540        $result = [];
541        foreach ( $items as $item ) {
542            if ( is_array( $item ) ) {
543                $title = $item['title'];
544                $options = $item;
545                unset( $options['title'] );
546
547                if (
548                    !empty( $options['private'] ) &&
549                    !( $options['private'] instanceof Authority )
550                ) {
551                    $options['private'] = RequestContext::getMain()->getAuthority();
552                }
553            } else {
554                $title = $item;
555                $options = [];
556            }
557            $file = $this->findFile( $title, $options );
558            if ( $file ) {
559                $searchName = File::normalizeTitle( $title )->getDBkey(); // must be valid
560                if ( $flags & self::NAME_AND_TIME_ONLY ) {
561                    $result[$searchName] = [
562                        'title' => $file->getTitle()->getDBkey(),
563                        'timestamp' => $file->getTimestamp()
564                    ];
565                } else {
566                    $result[$searchName] = $file;
567                }
568            }
569        }
570
571        return $result;
572    }
573
574    /**
575     * Find an instance of the file with this key, created at the specified time
576     * Returns false if the file does not exist. Repositories not supporting
577     * version control should return false if the time is specified.
578     *
579     * @param string $sha1 Base 36 SHA-1 hash
580     * @param array $options Option array, same as findFile().
581     * @return File|false False on failure
582     * @throws InvalidArgumentException if the `private` option is set and not an Authority object
583     */
584    public function findFileFromKey( $sha1, $options = [] ) {
585        if ( !empty( $options['private'] ) && !( $options['private'] instanceof Authority ) ) {
586            throw new InvalidArgumentException(
587                __METHOD__ . ' called with the `private` option set to something ' .
588                'other than an Authority object'
589            );
590        }
591
592        $time = $options['time'] ?? false;
593        # First try to find a matching current version of a file...
594        if ( !$this->fileFactoryKey ) {
595            return false; // find-by-sha1 not supported
596        }
597        $img = call_user_func( $this->fileFactoryKey, $sha1, $this, $time );
598        if ( $img && $img->exists() ) {
599            return $img;
600        }
601        # Now try to find a matching old version of a file...
602        if ( $time !== false && $this->oldFileFactoryKey ) { // find-by-sha1 supported?
603            $img = call_user_func( $this->oldFileFactoryKey, $sha1, $this, $time );
604            if ( $img && $img->exists() ) {
605                if ( !$img->isDeleted( File::DELETED_FILE ) ) {
606                    return $img; // always OK
607                } elseif (
608                    // If its not empty, its an Authority object
609                    !empty( $options['private'] ) &&
610                    $img->userCan( File::DELETED_FILE, $options['private'] )
611                ) {
612                    return $img;
613                }
614            }
615        }
616
617        return false;
618    }
619
620    /**
621     * Get an array or iterator of file objects for files that have a given
622     * SHA-1 content hash.
623     *
624     * STUB
625     * @param string $hash SHA-1 hash
626     * @return File[]
627     */
628    public function findBySha1( $hash ) {
629        return [];
630    }
631
632    /**
633     * Get an array of arrays or iterators of file objects for files that
634     * have the given SHA-1 content hashes.
635     *
636     * @param string[] $hashes An array of hashes
637     * @return File[][] An Array of arrays or iterators of file objects and the hash as key
638     */
639    public function findBySha1s( array $hashes ) {
640        $result = [];
641        foreach ( $hashes as $hash ) {
642            $files = $this->findBySha1( $hash );
643            if ( count( $files ) ) {
644                $result[$hash] = $files;
645            }
646        }
647
648        return $result;
649    }
650
651    /**
652     * Return an array of files where the name starts with $prefix.
653     *
654     * STUB
655     * @param string $prefix The prefix to search for
656     * @param int $limit The maximum amount of files to return
657     * @return LocalFile[]
658     */
659    public function findFilesByPrefix( $prefix, $limit ) {
660        return [];
661    }
662
663    /**
664     * Get the URL of thumb.php
665     *
666     * @return string
667     */
668    public function getThumbScriptUrl() {
669        return $this->thumbScriptUrl;
670    }
671
672    /**
673     * Get the URL thumb.php requests are being proxied to
674     *
675     * @return string
676     */
677    public function getThumbProxyUrl() {
678        return $this->thumbProxyUrl;
679    }
680
681    /**
682     * Get the secret key for the proxied thumb service
683     *
684     * @return string
685     */
686    public function getThumbProxySecret() {
687        return $this->thumbProxySecret;
688    }
689
690    /**
691     * Returns true if the repository can transform files via a 404 handler
692     *
693     * @return bool
694     */
695    public function canTransformVia404() {
696        return $this->transformVia404;
697    }
698
699    /**
700     * Returns true if the repository can transform files locally.
701     *
702     * @since 1.36
703     * @return bool
704     */
705    public function canTransformLocally() {
706        return !$this->disableLocalTransform;
707    }
708
709    /**
710     * Get the name of a file from its title
711     *
712     * @param PageIdentity|LinkTarget $title
713     * @return string
714     */
715    public function getNameFromTitle( $title ) {
716        if (
717            $this->initialCapital !=
718            MediaWikiServices::getInstance()->getNamespaceInfo()->isCapitalized( NS_FILE )
719        ) {
720            $name = $title->getDBkey();
721            if ( $this->initialCapital ) {
722                $name = MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $name );
723            }
724        } else {
725            $name = $title->getDBkey();
726        }
727
728        return $name;
729    }
730
731    /**
732     * Get the public zone root storage directory of the repository
733     *
734     * @return string
735     */
736    public function getRootDirectory() {
737        return $this->getZonePath( 'public' );
738    }
739
740    /**
741     * Get a relative path including trailing slash, e.g. f/fa/
742     * If the repo is not hashed, returns an empty string
743     *
744     * @param string $name Name of file
745     * @return string
746     */
747    public function getHashPath( $name ) {
748        return self::getHashPathForLevel( $name, $this->hashLevels );
749    }
750
751    /**
752     * Get a relative path including trailing slash, e.g. f/fa/
753     * If the repo is not hashed, returns an empty string
754     *
755     * @param string $suffix Basename of file from FileRepo::storeTemp()
756     * @return string
757     */
758    public function getTempHashPath( $suffix ) {
759        $parts = explode( '!', $suffix, 2 ); // format is <timestamp>!<name> or just <name>
760        $name = $parts[1] ?? $suffix; // hash path is not based on timestamp
761        return self::getHashPathForLevel( $name, $this->hashLevels );
762    }
763
764    /**
765     * @param string $name
766     * @param int $levels
767     * @return string
768     */
769    protected static function getHashPathForLevel( $name, $levels ) {
770        if ( $levels == 0 ) {
771            return '';
772        } else {
773            $hash = md5( $name );
774            $path = '';
775            for ( $i = 1; $i <= $levels; $i++ ) {
776                $path .= substr( $hash, 0, $i ) . '/';
777            }
778
779            return $path;
780        }
781    }
782
783    /**
784     * Get the number of hash directory levels
785     *
786     * @return int
787     */
788    public function getHashLevels() {
789        return $this->hashLevels;
790    }
791
792    /**
793     * Get the name of this repository, as specified by $info['name]' to the constructor
794     *
795     * @return string
796     */
797    public function getName() {
798        return $this->name;
799    }
800
801    /**
802     * Make an url to this repo
803     *
804     * @param string|array $query Query string to append
805     * @param string $entry Entry point; defaults to index
806     * @return string|false False on failure
807     */
808    public function makeUrl( $query = '', $entry = 'index' ) {
809        if ( isset( $this->scriptDirUrl ) ) {
810            return wfAppendQuery( "{$this->scriptDirUrl}/{$entry}.php", $query );
811        }
812
813        return false;
814    }
815
816    /**
817     * Get the URL of an image description page. May return false if it is
818     * unknown or not applicable. In general this should only be called by the
819     * File class, since it may return invalid results for certain kinds of
820     * repositories. Use File::getDescriptionUrl() in user code.
821     *
822     * In particular, it uses the article paths as specified to the repository
823     * constructor, whereas local repositories use the local Title functions.
824     *
825     * @param string $name
826     * @return string|false
827     */
828    public function getDescriptionUrl( $name ) {
829        $encName = wfUrlencode( $name );
830        if ( $this->descBaseUrl !== null ) {
831            # "http://example.com/wiki/File:"
832            return $this->descBaseUrl . $encName;
833        }
834        if ( $this->articleUrl !== null ) {
835            # "http://example.com/wiki/$1"
836            # We use "Image:" as the canonical namespace for
837            # compatibility across all MediaWiki versions.
838            return str_replace( '$1',
839                "Image:$encName", $this->articleUrl );
840        }
841        if ( $this->scriptDirUrl !== null ) {
842            # "http://example.com/w"
843            # We use "Image:" as the canonical namespace for
844            # compatibility across all MediaWiki versions,
845            # and just sort of hope index.php is right. ;)
846            return $this->makeUrl( "title=Image:$encName" );
847        }
848
849        return false;
850    }
851
852    /**
853     * Get the URL of the content-only fragment of the description page. For
854     * MediaWiki this means action=render. This should only be called by the
855     * repository's file class, since it may return invalid results. User code
856     * should use File::getDescriptionText().
857     *
858     * @param string $name Name of image to fetch
859     * @param string|null $lang Language to fetch it in, if any.
860     * @return string|false
861     */
862    public function getDescriptionRenderUrl( $name, $lang = null ) {
863        $query = 'action=render';
864        if ( $lang !== null ) {
865            $query .= '&uselang=' . urlencode( $lang );
866        }
867        if ( isset( $this->scriptDirUrl ) ) {
868            return $this->makeUrl(
869                'title=' .
870                wfUrlencode( 'Image:' . $name ) .
871                "&$query" );
872        } else {
873            $descUrl = $this->getDescriptionUrl( $name );
874            if ( $descUrl ) {
875                return wfAppendQuery( $descUrl, $query );
876            } else {
877                return false;
878            }
879        }
880    }
881
882    /**
883     * Get the URL of the stylesheet to apply to description pages
884     *
885     * @return string|false False on failure
886     */
887    public function getDescriptionStylesheetUrl() {
888        if ( isset( $this->scriptDirUrl ) ) {
889            // Must match canonical query parameter order for optimum caching
890            // See HTMLCacheUpdater::getUrls
891            return $this->makeUrl( 'title=MediaWiki:Filepage.css&action=raw&ctype=text/css' );
892        }
893
894        return false;
895    }
896
897    /**
898     * Store a file to a given destination.
899     *
900     * Using FSFile/TempFSFile can improve performance via caching.
901     * Using TempFSFile can further improve performance by signalling that it is safe
902     * to touch the source file or write extended attribute metadata to it directly.
903     *
904     * @param string|FSFile $srcPath Source file system path, storage path, or virtual URL
905     * @param string $dstZone Destination zone
906     * @param string $dstRel Destination relative path
907     * @param int $flags Bitwise combination of the following flags:
908     *   self::OVERWRITE         Overwrite an existing destination file instead of failing
909     *   self::OVERWRITE_SAME    Overwrite the file if the destination exists and has the
910     *                           same contents as the source
911     *   self::SKIP_LOCKING      Skip any file locking when doing the store
912     * @return Status
913     */
914    public function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
915        $this->assertWritableRepo(); // fail out if read-only
916
917        $status = $this->storeBatch( [ [ $srcPath, $dstZone, $dstRel ] ], $flags );
918        if ( $status->successCount == 0 ) {
919            $status->setOK( false );
920        }
921
922        return $status;
923    }
924
925    /**
926     * Store a batch of files
927     *
928     * @see FileRepo::store()
929     *
930     * @param array $triplets (src, dest zone, dest rel) triplets as per store()
931     * @param int $flags Bitwise combination of the following flags:
932     *   self::OVERWRITE         Overwrite an existing destination file instead of failing
933     *   self::OVERWRITE_SAME    Overwrite the file if the destination exists and has the
934     *                           same contents as the source
935     *   self::SKIP_LOCKING      Skip any file locking when doing the store
936     * @return Status
937     */
938    public function storeBatch( array $triplets, $flags = 0 ) {
939        $this->assertWritableRepo(); // fail out if read-only
940
941        if ( $flags & self::DELETE_SOURCE ) {
942            throw new InvalidArgumentException( "DELETE_SOURCE not supported in " . __METHOD__ );
943        }
944
945        $status = $this->newGood();
946        $backend = $this->backend; // convenience
947
948        $operations = [];
949        // Validate each triplet and get the store operation...
950        foreach ( $triplets as [ $src, $dstZone, $dstRel ] ) {
951            $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
952            wfDebug( __METHOD__
953                . "( \$src='$srcPath', \$dstZone='$dstZone', \$dstRel='$dstRel' )"
954            );
955            // Resolve source path
956            if ( $src instanceof FSFile ) {
957                $op = 'store';
958            } else {
959                $src = $this->resolveToStoragePathIfVirtual( $src );
960                $op = FileBackend::isStoragePath( $src ) ? 'copy' : 'store';
961            }
962            // Resolve destination path
963            $root = $this->getZonePath( $dstZone );
964            if ( !$root ) {
965                throw new RuntimeException( "Invalid zone: $dstZone" );
966            }
967            if ( !$this->validateFilename( $dstRel ) ) {
968                throw new RuntimeException( 'Validation error in $dstRel' );
969            }
970            $dstPath = "$root/$dstRel";
971            $dstDir = dirname( $dstPath );
972            // Create destination directories for this triplet
973            if ( !$this->initDirectory( $dstDir )->isOK() ) {
974                return $this->newFatal( 'directorycreateerror', $dstDir );
975            }
976
977            // Copy the source file to the destination
978            $operations[] = [
979                'op' => $op,
980                'src' => $src, // storage path (copy) or local file path (store)
981                'dst' => $dstPath,
982                'overwrite' => (bool)( $flags & self::OVERWRITE ),
983                'overwriteSame' => (bool)( $flags & self::OVERWRITE_SAME ),
984            ];
985        }
986
987        // Execute the store operation for each triplet
988        $opts = [ 'force' => true ];
989        if ( $flags & self::SKIP_LOCKING ) {
990            $opts['nonLocking'] = true;
991        }
992
993        return $status->merge( $backend->doOperations( $operations, $opts ) );
994    }
995
996    /**
997     * Deletes a batch of files.
998     * Each file can be a (zone, rel) pair, virtual url, storage path.
999     * It will try to delete each file, but ignores any errors that may occur.
1000     *
1001     * @param string[] $files List of files to delete
1002     * @param int $flags Bitwise combination of the following flags:
1003     *   self::SKIP_LOCKING      Skip any file locking when doing the deletions
1004     * @return Status
1005     */
1006    public function cleanupBatch( array $files, $flags = 0 ) {
1007        $this->assertWritableRepo(); // fail out if read-only
1008
1009        $status = $this->newGood();
1010
1011        $operations = [];
1012        foreach ( $files as $path ) {
1013            if ( is_array( $path ) ) {
1014                // This is a pair, extract it
1015                [ $zone, $rel ] = $path;
1016                $path = $this->getZonePath( $zone ) . "/$rel";
1017            } else {
1018                // Resolve source to a storage path if virtual
1019                $path = $this->resolveToStoragePathIfVirtual( $path );
1020            }
1021            $operations[] = [ 'op' => 'delete', 'src' => $path ];
1022        }
1023        // Actually delete files from storage...
1024        $opts = [ 'force' => true ];
1025        if ( $flags & self::SKIP_LOCKING ) {
1026            $opts['nonLocking'] = true;
1027        }
1028
1029        return $status->merge( $this->backend->doOperations( $operations, $opts ) );
1030    }
1031
1032    /**
1033     * Import a file from the local file system into the repo.
1034     * This does no locking and overrides existing files.
1035     * This function can be used to write to otherwise read-only foreign repos.
1036     * This is intended for copying generated thumbnails into the repo.
1037     *
1038     * Using FSFile/TempFSFile can improve performance via caching.
1039     * Using TempFSFile can further improve performance by signalling that it is safe
1040     * to touch the source file or write extended attribute metadata to it directly.
1041     *
1042     * @param string|FSFile $src Source file system path, storage path, or virtual URL
1043     * @param string $dst Virtual URL or storage path
1044     * @param array|string|null $options An array consisting of a key named headers
1045     *   listing extra headers. If a string, taken as content-disposition header.
1046     *   (Support for array of options new in 1.23)
1047     * @return Status
1048     */
1049    final public function quickImport( $src, $dst, $options = null ) {
1050        return $this->quickImportBatch( [ [ $src, $dst, $options ] ] );
1051    }
1052
1053    /**
1054     * Import a batch of files from the local file system into the repo.
1055     * This does no locking and overrides existing files.
1056     * This function can be used to write to otherwise read-only foreign repos.
1057     * This is intended for copying generated thumbnails into the repo.
1058     *
1059     * @see FileRepo::quickImport()
1060     *
1061     * All path parameters may be a file system path, storage path, or virtual URL.
1062     * When "headers" are given they are used as HTTP headers if supported.
1063     *
1064     * @param array $triples List of (source path or FSFile, destination path, disposition)
1065     * @return Status
1066     */
1067    public function quickImportBatch( array $triples ) {
1068        $status = $this->newGood();
1069        $operations = [];
1070        foreach ( $triples as $triple ) {
1071            [ $src, $dst ] = $triple;
1072            if ( $src instanceof FSFile ) {
1073                $op = 'store';
1074            } else {
1075                $src = $this->resolveToStoragePathIfVirtual( $src );
1076                $op = FileBackend::isStoragePath( $src ) ? 'copy' : 'store';
1077            }
1078            $dst = $this->resolveToStoragePathIfVirtual( $dst );
1079
1080            if ( !isset( $triple[2] ) ) {
1081                $headers = [];
1082            } elseif ( is_string( $triple[2] ) ) {
1083                // back-compat
1084                $headers = [ 'Content-Disposition' => $triple[2] ];
1085            } elseif ( is_array( $triple[2] ) && isset( $triple[2]['headers'] ) ) {
1086                $headers = $triple[2]['headers'];
1087            } else {
1088                $headers = [];
1089            }
1090
1091            $operations[] = [
1092                'op' => $op,
1093                'src' => $src, // storage path (copy) or local path/FSFile (store)
1094                'dst' => $dst,
1095                'headers' => $headers
1096            ];
1097            $status->merge( $this->initDirectory( dirname( $dst ) ) );
1098        }
1099
1100        return $status->merge( $this->backend->doQuickOperations( $operations ) );
1101    }
1102
1103    /**
1104     * Purge a file from the repo. This does no locking.
1105     * This function can be used to write to otherwise read-only foreign repos.
1106     * This is intended for purging thumbnails.
1107     *
1108     * @param string $path Virtual URL or storage path
1109     * @return Status
1110     */
1111    final public function quickPurge( $path ) {
1112        return $this->quickPurgeBatch( [ $path ] );
1113    }
1114
1115    /**
1116     * Deletes a directory if empty.
1117     * This function can be used to write to otherwise read-only foreign repos.
1118     *
1119     * @param string $dir Virtual URL (or storage path) of directory to clean
1120     * @return Status
1121     */
1122    public function quickCleanDir( $dir ) {
1123        return $this->newGood()->merge(
1124            $this->backend->clean(
1125                [ 'dir' => $this->resolveToStoragePathIfVirtual( $dir ) ]
1126            )
1127        );
1128    }
1129
1130    /**
1131     * Purge a batch of files from the repo.
1132     * This function can be used to write to otherwise read-only foreign repos.
1133     * This does no locking and is intended for purging thumbnails.
1134     *
1135     * @param string[] $paths List of virtual URLs or storage paths
1136     * @return Status
1137     */
1138    public function quickPurgeBatch( array $paths ) {
1139        $status = $this->newGood();
1140        $operations = [];
1141        foreach ( $paths as $path ) {
1142            $operations[] = [
1143                'op' => 'delete',
1144                'src' => $this->resolveToStoragePathIfVirtual( $path ),
1145                'ignoreMissingSource' => true
1146            ];
1147        }
1148        $status->merge( $this->backend->doQuickOperations( $operations ) );
1149
1150        return $status;
1151    }
1152
1153    /**
1154     * Pick a random name in the temp zone and store a file to it.
1155     * Returns a Status object with the file Virtual URL in the value,
1156     * file can later be disposed using FileRepo::freeTemp().
1157     *
1158     * @param string $originalName The base name of the file as specified
1159     *   by the user. The file extension will be maintained.
1160     * @param string $srcPath The current location of the file.
1161     * @return Status Object with the URL in the value.
1162     */
1163    public function storeTemp( $originalName, $srcPath ) {
1164        $this->assertWritableRepo(); // fail out if read-only
1165
1166        $date = MWTimestamp::getInstance()->format( 'YmdHis' );
1167        $hashPath = $this->getHashPath( $originalName );
1168        $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName );
1169        $virtualUrl = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
1170
1171        $result = $this->quickImport( $srcPath, $virtualUrl );
1172        $result->value = $virtualUrl;
1173
1174        return $result;
1175    }
1176
1177    /**
1178     * Remove a temporary file or mark it for garbage collection
1179     *
1180     * @param string $virtualUrl The virtual URL returned by FileRepo::storeTemp()
1181     * @return bool True on success, false on failure
1182     */
1183    public function freeTemp( $virtualUrl ) {
1184        $this->assertWritableRepo(); // fail out if read-only
1185
1186        $temp = $this->getVirtualUrl( 'temp' );
1187        if ( !str_starts_with( $virtualUrl, $temp ) ) {
1188            wfDebug( __METHOD__ . ": Invalid temp virtual URL" );
1189
1190            return false;
1191        }
1192
1193        return $this->quickPurge( $virtualUrl )->isOK();
1194    }
1195
1196    /**
1197     * Concatenate a list of temporary files into a target file location.
1198     *
1199     * @param string[] $srcPaths Ordered list of source virtual URLs/storage paths
1200     * @param string $dstPath Target file system path
1201     * @param int $flags Bitwise combination of the following flags:
1202     *   self::DELETE_SOURCE     Delete the source files on success
1203     * @return Status
1204     */
1205    public function concatenate( array $srcPaths, $dstPath, $flags = 0 ) {
1206        $this->assertWritableRepo(); // fail out if read-only
1207
1208        $status = $this->newGood();
1209
1210        $sources = [];
1211        foreach ( $srcPaths as $srcPath ) {
1212            // Resolve source to a storage path if virtual
1213            $source = $this->resolveToStoragePathIfVirtual( $srcPath );
1214            $sources[] = $source; // chunk to merge
1215        }
1216
1217        // Concatenate the chunks into one FS file
1218        $params = [ 'srcs' => $sources, 'dst' => $dstPath ];
1219        $status->merge( $this->backend->concatenate( $params ) );
1220        if ( !$status->isOK() ) {
1221            return $status;
1222        }
1223
1224        // Delete the sources if required
1225        if ( $flags & self::DELETE_SOURCE ) {
1226            $status->merge( $this->quickPurgeBatch( $srcPaths ) );
1227        }
1228
1229        // Make sure status is OK, despite any quickPurgeBatch() fatals
1230        $status->setResult( true );
1231
1232        return $status;
1233    }
1234
1235    /**
1236     * Copy or move a file either from a storage path, virtual URL,
1237     * or file system path, into this repository at the specified destination location.
1238     *
1239     * Returns a Status object. On success, the value contains "new" or
1240     * "archived", to indicate whether the file was new with that name.
1241     *
1242     * Using FSFile/TempFSFile can improve performance via caching.
1243     * Using TempFSFile can further improve performance by signalling that it is safe
1244     * to touch the source file or write extended attribute metadata to it directly.
1245     *
1246     * Options to $options include:
1247     *   - headers : name/value map of HTTP headers to use in response to GET/HEAD requests
1248     *
1249     * @param string|FSFile $src The source file system path, storage path, or URL
1250     * @param string $dstRel The destination relative path
1251     * @param string $archiveRel The relative path where the existing file is to
1252     *   be archived, if there is one. Relative to the public zone root.
1253     * @param int $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
1254     *   that the source file should be deleted if possible
1255     * @param array $options Optional additional parameters
1256     * @return Status
1257     */
1258    public function publish(
1259        $src, $dstRel, $archiveRel, $flags = 0, array $options = []
1260    ) {
1261        $this->assertWritableRepo(); // fail out if read-only
1262
1263        $status = $this->publishBatch(
1264            [ [ $src, $dstRel, $archiveRel, $options ] ], $flags );
1265        if ( $status->successCount == 0 ) {
1266            $status->setOK( false );
1267        }
1268        $status->value = $status->value[0] ?? false;
1269
1270        return $status;
1271    }
1272
1273    /**
1274     * Publish a batch of files
1275     *
1276     * @see FileRepo::publish()
1277     *
1278     * @param array $ntuples (source, dest, archive) triplets or
1279     *   (source, dest, archive, options) 4-tuples as per publish().
1280     * @param int $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
1281     *   that the source files should be deleted if possible
1282     * @return Status
1283     */
1284    public function publishBatch( array $ntuples, $flags = 0 ) {
1285        $this->assertWritableRepo(); // fail out if read-only
1286
1287        $backend = $this->backend; // convenience
1288        // Try creating directories
1289        $this->initZones( 'public' );
1290
1291        $status = $this->newGood( [] );
1292
1293        $operations = [];
1294        $sourceFSFilesToDelete = []; // cleanup for disk source files
1295        // Validate each triplet and get the store operation...
1296        foreach ( $ntuples as $ntuple ) {
1297            [ $src, $dstRel, $archiveRel ] = $ntuple;
1298            $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1299
1300            $options = $ntuple[3] ?? [];
1301            // Resolve source to a storage path if virtual
1302            $srcPath = $this->resolveToStoragePathIfVirtual( $srcPath );
1303            if ( !$this->validateFilename( $dstRel ) ) {
1304                throw new RuntimeException( 'Validation error in $dstRel' );
1305            }
1306            if ( !$this->validateFilename( $archiveRel ) ) {
1307                throw new RuntimeException( 'Validation error in $archiveRel' );
1308            }
1309
1310            $publicRoot = $this->getZonePath( 'public' );
1311            $dstPath = "$publicRoot/$dstRel";
1312            $archivePath = "$publicRoot/$archiveRel";
1313
1314            $dstDir = dirname( $dstPath );
1315            $archiveDir = dirname( $archivePath );
1316            // Abort immediately on directory creation errors since they're likely to be repetitive
1317            if ( !$this->initDirectory( $dstDir )->isOK() ) {
1318                return $this->newFatal( 'directorycreateerror', $dstDir );
1319            }
1320            if ( !$this->initDirectory( $archiveDir )->isOK() ) {
1321                return $this->newFatal( 'directorycreateerror', $archiveDir );
1322            }
1323
1324            // Set any desired headers to be use in GET/HEAD responses
1325            $headers = $options['headers'] ?? [];
1326
1327            // Archive destination file if it exists.
1328            // This will check if the archive file also exists and fail if does.
1329            // This is a check to avoid data loss. On Windows and Linux,
1330            // copy() will overwrite, so the existence check is vulnerable to
1331            // race conditions unless a functioning LockManager is used.
1332            // LocalFile also uses SELECT FOR UPDATE for synchronization.
1333            $operations[] = [
1334                'op' => 'copy',
1335                'src' => $dstPath,
1336                'dst' => $archivePath,
1337                'ignoreMissingSource' => true
1338            ];
1339
1340            // Copy (or move) the source file to the destination
1341            if ( FileBackend::isStoragePath( $srcPath ) ) {
1342                $operations[] = [
1343                    'op' => ( $flags & self::DELETE_SOURCE ) ? 'move' : 'copy',
1344                    'src' => $srcPath,
1345                    'dst' => $dstPath,
1346                    'overwrite' => true, // replace current
1347                    'headers' => $headers
1348                ];
1349            } else {
1350                $operations[] = [
1351                    'op' => 'store',
1352                    'src' => $src, // storage path (copy) or local path/FSFile (store)
1353                    'dst' => $dstPath,
1354                    'overwrite' => true, // replace current
1355                    'headers' => $headers
1356                ];
1357                if ( $flags & self::DELETE_SOURCE ) {
1358                    $sourceFSFilesToDelete[] = $srcPath;
1359                }
1360            }
1361        }
1362
1363        // Execute the operations for each triplet
1364        $status->merge( $backend->doOperations( $operations ) );
1365        // Find out which files were archived...
1366        foreach ( $ntuples as $i => $ntuple ) {
1367            [ , , $archiveRel ] = $ntuple;
1368            $archivePath = $this->getZonePath( 'public' ) . "/$archiveRel";
1369            if ( $this->fileExists( $archivePath ) ) {
1370                $status->value[$i] = 'archived';
1371            } else {
1372                $status->value[$i] = 'new';
1373            }
1374        }
1375        // Cleanup for disk source files...
1376        foreach ( $sourceFSFilesToDelete as $file ) {
1377            AtEase::suppressWarnings();
1378            unlink( $file ); // FS cleanup
1379            AtEase::restoreWarnings();
1380        }
1381
1382        return $status;
1383    }
1384
1385    /**
1386     * Creates a directory with the appropriate zone permissions.
1387     * Callers are responsible for doing read-only and "writable repo" checks.
1388     *
1389     * @param string $dir Virtual URL (or storage path) of directory to clean
1390     * @return Status Good status without value for success, fatal otherwise.
1391     */
1392    protected function initDirectory( $dir ) {
1393        $path = $this->resolveToStoragePathIfVirtual( $dir );
1394        [ , $container, ] = FileBackend::splitStoragePath( $path );
1395
1396        $params = [ 'dir' => $path ];
1397        if ( $this->isPrivate
1398            || $container === $this->zones['deleted']['container']
1399            || $container === $this->zones['temp']['container']
1400        ) {
1401            # Take all available measures to prevent web accessibility of new deleted
1402            # directories, in case the user has not configured offline storage
1403            $params = [ 'noAccess' => true, 'noListing' => true ] + $params;
1404        }
1405
1406        return $this->newGood()->merge( $this->backend->prepare( $params ) );
1407    }
1408
1409    /**
1410     * Deletes a directory if empty.
1411     *
1412     * @param string $dir Virtual URL (or storage path) of directory to clean
1413     * @return Status
1414     */
1415    public function cleanDir( $dir ) {
1416        $this->assertWritableRepo(); // fail out if read-only
1417
1418        return $this->newGood()->merge(
1419            $this->backend->clean(
1420                [ 'dir' => $this->resolveToStoragePathIfVirtual( $dir ) ]
1421            )
1422        );
1423    }
1424
1425    /**
1426     * Checks existence of a file
1427     *
1428     * @param string $file Virtual URL (or storage path) of file to check
1429     * @return bool|null Whether the file exists, or null in case of I/O errors
1430     */
1431    public function fileExists( $file ) {
1432        $result = $this->fileExistsBatch( [ $file ] );
1433
1434        return $result[0];
1435    }
1436
1437    /**
1438     * Checks existence of an array of files.
1439     *
1440     * @param string[] $files Virtual URLs (or storage paths) of files to check
1441     * @return array<string|int,bool|null> Map of files and either bool indicating whether the files exist,
1442     *   or null in case of I/O errors
1443     */
1444    public function fileExistsBatch( array $files ) {
1445        $paths = array_map( [ $this, 'resolveToStoragePathIfVirtual' ], $files );
1446        $this->backend->preloadFileStat( [ 'srcs' => $paths ] );
1447
1448        $result = [];
1449        foreach ( $paths as $key => $path ) {
1450            $result[$key] = $this->backend->fileExists( [ 'src' => $path ] );
1451        }
1452
1453        return $result;
1454    }
1455
1456    /**
1457     * Move a file to the deletion archive.
1458     * If no valid deletion archive exists, this may either delete the file
1459     * or throw an exception, depending on the preference of the repository
1460     *
1461     * @param mixed $srcRel Relative path for the file to be deleted
1462     * @param mixed $archiveRel Relative path for the archive location.
1463     *   Relative to a private archive directory.
1464     * @return Status
1465     */
1466    public function delete( $srcRel, $archiveRel ) {
1467        $this->assertWritableRepo(); // fail out if read-only
1468
1469        return $this->deleteBatch( [ [ $srcRel, $archiveRel ] ] );
1470    }
1471
1472    /**
1473     * Move a group of files to the deletion archive.
1474     *
1475     * If no valid deletion archive is configured, this may either delete the
1476     * file or throw an exception, depending on the preference of the repository.
1477     *
1478     * The overwrite policy is determined by the repository -- currently LocalRepo
1479     * assumes a naming scheme in the deleted zone based on content hash, as
1480     * opposed to the public zone which is assumed to be unique.
1481     *
1482     * @param array $sourceDestPairs Array of source/destination pairs. Each element
1483     *   is a two-element array containing the source file path relative to the
1484     *   public root in the first element, and the archive file path relative
1485     *   to the deleted zone root in the second element.
1486     * @return Status
1487     */
1488    public function deleteBatch( array $sourceDestPairs ) {
1489        $this->assertWritableRepo(); // fail out if read-only
1490
1491        // Try creating directories
1492        $this->initZones( [ 'public', 'deleted' ] );
1493
1494        $status = $this->newGood();
1495
1496        $backend = $this->backend; // convenience
1497        $operations = [];
1498        // Validate filenames and create archive directories
1499        foreach ( $sourceDestPairs as [ $srcRel, $archiveRel ] ) {
1500            if ( !$this->validateFilename( $srcRel ) ) {
1501                throw new RuntimeException( __METHOD__ . ':Validation error in $srcRel' );
1502            } elseif ( !$this->validateFilename( $archiveRel ) ) {
1503                throw new RuntimeException( __METHOD__ . ':Validation error in $archiveRel' );
1504            }
1505
1506            $publicRoot = $this->getZonePath( 'public' );
1507            $srcPath = "{$publicRoot}/$srcRel";
1508
1509            $deletedRoot = $this->getZonePath( 'deleted' );
1510            $archivePath = "{$deletedRoot}/{$archiveRel}";
1511            $archiveDir = dirname( $archivePath ); // does not touch FS
1512
1513            // Create destination directories
1514            if ( !$this->initDirectory( $archiveDir )->isGood() ) {
1515                return $this->newFatal( 'directorycreateerror', $archiveDir );
1516            }
1517
1518            $operations[] = [
1519                'op' => 'move',
1520                'src' => $srcPath,
1521                'dst' => $archivePath,
1522                // We may have 2+ identical files being deleted,
1523                // all of which will map to the same destination file
1524                'overwriteSame' => true // also see T33792
1525            ];
1526        }
1527
1528        // Move the files by execute the operations for each pair.
1529        // We're now committed to returning an OK result, which will
1530        // lead to the files being moved in the DB also.
1531        $opts = [ 'force' => true ];
1532        return $status->merge( $backend->doOperations( $operations, $opts ) );
1533    }
1534
1535    /**
1536     * Delete files in the deleted directory if they are not referenced in the filearchive table
1537     *
1538     * STUB
1539     * @param string[] $storageKeys
1540     */
1541    public function cleanupDeletedBatch( array $storageKeys ) {
1542        $this->assertWritableRepo();
1543    }
1544
1545    /**
1546     * Get a relative path for a deletion archive key,
1547     * e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg
1548     *
1549     * @param string $key
1550     * @return string
1551     */
1552    public function getDeletedHashPath( $key ) {
1553        if ( strlen( $key ) < 31 ) {
1554            throw new InvalidArgumentException( "Invalid storage key '$key'." );
1555        }
1556        $path = '';
1557        for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) {
1558            $path .= $key[$i] . '/';
1559        }
1560
1561        return $path;
1562    }
1563
1564    /**
1565     * If a path is a virtual URL, resolve it to a storage path.
1566     * Otherwise, just return the path as it is.
1567     *
1568     * @param string $path
1569     * @return string
1570     */
1571    protected function resolveToStoragePathIfVirtual( $path ) {
1572        if ( self::isVirtualUrl( $path ) ) {
1573            return $this->resolveVirtualUrl( $path );
1574        }
1575
1576        return $path;
1577    }
1578
1579    /**
1580     * Get a local FS copy of a file with a given virtual URL/storage path.
1581     * Temporary files may be purged when the file object falls out of scope.
1582     *
1583     * @param string $virtualUrl
1584     * @return TempFSFile|null|false Returns false for missing file, null on failure
1585     */
1586    public function getLocalCopy( $virtualUrl ) {
1587        $path = $this->resolveToStoragePathIfVirtual( $virtualUrl );
1588
1589        return $this->backend->getLocalCopy( [ 'src' => $path ] );
1590    }
1591
1592    /**
1593     * Get a local FS file with a given virtual URL/storage path.
1594     * The file is either an original or a copy. It should not be changed.
1595     * Temporary files may be purged when the file object falls out of scope.
1596     *
1597     * @param string $virtualUrl
1598     * @return FSFile|null|false Returns false for missing file, null on failure.
1599     */
1600    public function getLocalReference( $virtualUrl ) {
1601        $path = $this->resolveToStoragePathIfVirtual( $virtualUrl );
1602
1603        return $this->backend->getLocalReference( [ 'src' => $path ] );
1604    }
1605
1606    /**
1607     * Get properties of a file with a given virtual URL/storage path.
1608     * Properties should ultimately be obtained via FSFile::getProps().
1609     *
1610     * @param string $virtualUrl
1611     * @return array
1612     */
1613    public function getFileProps( $virtualUrl ) {
1614        $fsFile = $this->getLocalReference( $virtualUrl );
1615        $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
1616        if ( $fsFile ) {
1617            $props = $mwProps->getPropsFromPath( $fsFile->getPath(), true );
1618        } else {
1619            $props = $mwProps->newPlaceholderProps();
1620        }
1621
1622        return $props;
1623    }
1624
1625    /**
1626     * Get the timestamp of a file with a given virtual URL/storage path
1627     *
1628     * @param string $virtualUrl
1629     * @return string|false False on failure
1630     */
1631    public function getFileTimestamp( $virtualUrl ) {
1632        $path = $this->resolveToStoragePathIfVirtual( $virtualUrl );
1633
1634        return $this->backend->getFileTimestamp( [ 'src' => $path ] );
1635    }
1636
1637    /**
1638     * Get the size of a file with a given virtual URL/storage path
1639     *
1640     * @param string $virtualUrl
1641     * @return int|false
1642     */
1643    public function getFileSize( $virtualUrl ) {
1644        $path = $this->resolveToStoragePathIfVirtual( $virtualUrl );
1645
1646        return $this->backend->getFileSize( [ 'src' => $path ] );
1647    }
1648
1649    /**
1650     * Get the sha1 (base 36) of a file with a given virtual URL/storage path
1651     *
1652     * @param string $virtualUrl
1653     * @return string|false
1654     */
1655    public function getFileSha1( $virtualUrl ) {
1656        $path = $this->resolveToStoragePathIfVirtual( $virtualUrl );
1657
1658        return $this->backend->getFileSha1Base36( [ 'src' => $path ] );
1659    }
1660
1661    /**
1662     * Attempt to stream a file with the given virtual URL/storage path
1663     *
1664     * @param string $virtualUrl
1665     * @param array $headers Additional HTTP headers to send on success
1666     * @param array $optHeaders HTTP request headers (if-modified-since, range, ...)
1667     * @return Status
1668     * @since 1.27
1669     */
1670    public function streamFileWithStatus( $virtualUrl, $headers = [], $optHeaders = [] ) {
1671        $path = $this->resolveToStoragePathIfVirtual( $virtualUrl );
1672        $params = [ 'src' => $path, 'headers' => $headers, 'options' => $optHeaders ];
1673
1674        // T172851: HHVM does not flush the output properly, causing OOM
1675        ob_start( null, 1_048_576 );
1676        ob_implicit_flush( true );
1677
1678        $status = $this->newGood()->merge( $this->backend->streamFile( $params ) );
1679
1680        // T186565: Close the buffer, unless it has already been closed
1681        // in HTTPFileStreamer::resetOutputBuffers().
1682        if ( ob_get_status() ) {
1683            ob_end_flush();
1684        }
1685
1686        return $status;
1687    }
1688
1689    /**
1690     * Call a callback function for every public regular file in the repository.
1691     * This only acts on the current version of files, not any old versions.
1692     * May use either the database or the filesystem.
1693     *
1694     * @param callable $callback
1695     * @return void
1696     */
1697    public function enumFiles( $callback ) {
1698        $this->enumFilesInStorage( $callback );
1699    }
1700
1701    /**
1702     * Call a callback function for every public file in the repository.
1703     * May use either the database or the filesystem.
1704     *
1705     * @param callable $callback
1706     * @return void
1707     */
1708    protected function enumFilesInStorage( $callback ) {
1709        $publicRoot = $this->getZonePath( 'public' );
1710        $numDirs = 1 << ( $this->hashLevels * 4 );
1711        // Use a priori assumptions about directory structure
1712        // to reduce the tree height of the scanning process.
1713        for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) {
1714            $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex );
1715            $path = $publicRoot;
1716            for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) {
1717                $path .= '/' . substr( $hexString, 0, $hexPos + 1 );
1718            }
1719            $iterator = $this->backend->getFileList( [ 'dir' => $path ] );
1720            if ( $iterator === null ) {
1721                throw new RuntimeException( __METHOD__ . ': could not get file listing for ' . $path );
1722            }
1723            foreach ( $iterator as $name ) {
1724                // Each item returned is a public file
1725                call_user_func( $callback, "{$path}/{$name}" );
1726            }
1727        }
1728    }
1729
1730    /**
1731     * Determine if a relative path is valid, i.e. not blank or involving directory traversal
1732     *
1733     * @param string $filename
1734     * @return bool
1735     */
1736    public function validateFilename( $filename ) {
1737        if ( strval( $filename ) == '' ) {
1738            return false;
1739        }
1740
1741        return FileBackend::isPathTraversalFree( $filename );
1742    }
1743
1744    /**
1745     * Get a callback function to use for cleaning error message parameters
1746     *
1747     * @return callable
1748     */
1749    private function getErrorCleanupFunction() {
1750        switch ( $this->pathDisclosureProtection ) {
1751            case 'none':
1752            case 'simple': // b/c
1753                $callback = [ $this, 'passThrough' ];
1754                break;
1755            default: // 'paranoid'
1756                $callback = [ $this, 'paranoidClean' ];
1757        }
1758        return $callback;
1759    }
1760
1761    /**
1762     * Path disclosure protection function
1763     *
1764     * @param string $param
1765     * @return string
1766     */
1767    public function paranoidClean( $param ) {
1768        return '[hidden]';
1769    }
1770
1771    /**
1772     * Path disclosure protection function
1773     *
1774     * @param string $param
1775     * @return string
1776     */
1777    public function passThrough( $param ) {
1778        return $param;
1779    }
1780
1781    /**
1782     * Create a new fatal error
1783     *
1784     * @param string $message
1785     * @param mixed ...$parameters
1786     * @return Status
1787     */
1788    public function newFatal( $message, ...$parameters ) {
1789        $status = Status::newFatal( $message, ...$parameters );
1790        $status->cleanCallback = $this->getErrorCleanupFunction();
1791
1792        return $status;
1793    }
1794
1795    /**
1796     * Create a new good result
1797     *
1798     * @param null|mixed $value
1799     * @return Status
1800     */
1801    public function newGood( $value = null ) {
1802        $status = Status::newGood( $value );
1803        $status->cleanCallback = $this->getErrorCleanupFunction();
1804
1805        return $status;
1806    }
1807
1808    /**
1809     * Checks if there is a redirect named as $title. If there is, return the
1810     * title object. If not, return false.
1811     * STUB
1812     *
1813     * @param PageIdentity|LinkTarget $title Title of image
1814     * @return Title|false
1815     */
1816    public function checkRedirect( $title ) {
1817        return false;
1818    }
1819
1820    /**
1821     * Invalidates image redirect cache related to that image
1822     * Doesn't do anything for repositories that don't support image redirects.
1823     *
1824     * STUB
1825     * @param PageIdentity|LinkTarget $title Title of image
1826     */
1827    public function invalidateImageRedirect( $title ) {
1828    }
1829
1830    /**
1831     * Get the human-readable name of the repo
1832     *
1833     * @return string
1834     */
1835    public function getDisplayName() {
1836        $sitename = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::Sitename );
1837
1838        if ( $this->isLocal() ) {
1839            return $sitename;
1840        }
1841
1842        // 'shared-repo-name-wikimediacommons' is used when $wgUseInstantCommons = true
1843        return wfMessageFallback( 'shared-repo-name-' . $this->name, 'shared-repo' )->text();
1844    }
1845
1846    /**
1847     * Get the portion of the file that contains the origin file name.
1848     * If that name is too long, then the name "thumbnail.<ext>" will be given.
1849     *
1850     * @param string $name
1851     * @return string
1852     */
1853    public function nameForThumb( $name ) {
1854        if ( strlen( $name ) > $this->abbrvThreshold ) {
1855            $ext = FileBackend::extensionFromPath( $name );
1856            $name = ( $ext == '' ) ? 'thumbnail' : "thumbnail.$ext";
1857        }
1858
1859        return $name;
1860    }
1861
1862    /**
1863     * Returns true if this the local file repository.
1864     *
1865     * @return bool
1866     */
1867    public function isLocal() {
1868        return $this->getName() == 'local';
1869    }
1870
1871    /**
1872     * Get a global, repository-qualified, WAN cache key
1873     *
1874     * This might be called from either the site context of the wiki that owns the repo or
1875     * the site context of another wiki that simply has access to the repo. This returns
1876     * false if the repository's cache is not accessible from the current site context.
1877     *
1878     * @param string $kClassSuffix Key collection name suffix (added to this repo class)
1879     * @param mixed ...$components Additional key components
1880     * @return string|false
1881     */
1882    public function getSharedCacheKey( $kClassSuffix, ...$components ) {
1883        return false;
1884    }
1885
1886    /**
1887     * Get a site-local, repository-qualified, WAN cache key
1888     *
1889     * These cache keys are not shared among different site context and thus cannot be
1890     * directly invalidated when repo objects are modified. These are useful when there
1891     * is no accessible global cache or the values depend on the current site context.
1892     *
1893     * @param string $kClassSuffix Key collection name suffix (added to this repo class)
1894     * @param mixed ...$components Additional key components
1895     * @return string
1896     */
1897    public function getLocalCacheKey( $kClassSuffix, ...$components ) {
1898        return $this->wanCache->makeKey(
1899            'filerepo-' . $kClassSuffix,
1900            $this->getName(),
1901            ...$components
1902        );
1903    }
1904
1905    /**
1906     * Get a temporary private FileRepo associated with this repo.
1907     *
1908     * Files will be created in the temp zone of this repo.
1909     * It will have the same backend as this repo.
1910     *
1911     * @return TempFileRepo
1912     */
1913    public function getTempRepo() {
1914        return new TempFileRepo( [
1915            'name' => "{$this->name}-temp",
1916            'backend' => $this->backend,
1917            'zones' => [
1918                'public' => [
1919                    // Same place storeTemp() uses in the base repo, though
1920                    // the path hashing is mismatched, which is annoying.
1921                    'container' => $this->zones['temp']['container'],
1922                    'directory' => $this->zones['temp']['directory']
1923                ],
1924                'thumb' => [
1925                    'container' => $this->zones['temp']['container'],
1926                    'directory' => $this->zones['temp']['directory'] == ''
1927                        ? 'thumb'
1928                        : $this->zones['temp']['directory'] . '/thumb'
1929                ],
1930                'transcoded' => [
1931                    'container' => $this->zones['temp']['container'],
1932                    'directory' => $this->zones['temp']['directory'] == ''
1933                        ? 'transcoded'
1934                        : $this->zones['temp']['directory'] . '/transcoded'
1935                ]
1936            ],
1937            'hashLevels' => $this->hashLevels, // performance
1938            'isPrivate' => true // all in temp zone
1939        ] );
1940    }
1941
1942    /**
1943     * Get an UploadStash associated with this repo.
1944     *
1945     * @param UserIdentity|null $user
1946     * @return UploadStash
1947     */
1948    public function getUploadStash( UserIdentity $user = null ) {
1949        return new UploadStash( $this, $user );
1950    }
1951
1952    /**
1953     * Throw an exception if this repo is read-only by design.
1954     * This does not and should not check getReadOnlyReason().
1955     *
1956     * @throws LogicException
1957     */
1958    protected function assertWritableRepo() {
1959    }
1960
1961    /**
1962     * Return information about the repository.
1963     *
1964     * @return array
1965     * @since 1.22
1966     */
1967    public function getInfo() {
1968        $ret = [
1969            'name' => $this->getName(),
1970            'displayname' => $this->getDisplayName(),
1971            'rootUrl' => $this->getZoneUrl( 'public' ),
1972            'local' => $this->isLocal(),
1973        ];
1974
1975        $optionalSettings = [
1976            'url',
1977            'thumbUrl',
1978            'initialCapital',
1979            'descBaseUrl',
1980            'scriptDirUrl',
1981            'articleUrl',
1982            'fetchDescription',
1983            'descriptionCacheExpiry',
1984        ];
1985        foreach ( $optionalSettings as $k ) {
1986            if ( isset( $this->$k ) ) {
1987                $ret[$k] = $this->$k;
1988            }
1989        }
1990        if ( isset( $this->favicon ) ) {
1991            // Expand any local path to full URL to improve API usability (T77093).
1992            $ret['favicon'] = MediaWikiServices::getInstance()->getUrlUtils()
1993                ->expand( $this->favicon );
1994        }
1995
1996        return $ret;
1997    }
1998
1999    /**
2000     * Returns whether or not storage is SHA-1 based
2001     * @return bool
2002     */
2003    public function hasSha1Storage() {
2004        return $this->hasSha1Storage;
2005    }
2006
2007    /**
2008     * Returns whether or not repo supports having originals SHA-1s in the thumb URLs
2009     * @return bool
2010     */
2011    public function supportsSha1URLs() {
2012        return $this->supportsSha1URLs;
2013    }
2014}