Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
26.59% covered (danger)
26.59%
180 / 677
15.44% covered (danger)
15.44%
21 / 136
CRAP
0.00% covered (danger)
0.00%
0 / 1
File
26.63% covered (danger)
26.63%
180 / 676
15.44% covered (danger)
15.44%
21 / 136
40515.32
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 normalizeTitle
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
9
 __get
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 normalizeExtension
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 checkExtensionCompatibility
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 upgradeRow
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 splitMime
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 compare
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getName
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getExtension
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOriginalTitle
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getUrl
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getDescriptionShortUrl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFullUrl
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getCanonicalUrl
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getViewURL
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getPath
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getLocalRefPath
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 addToShellboxCommand
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getWidth
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHeight
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getThumbnailBucket
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
7.01
 getDisplayWidthHeight
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
8.02
 getLength
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 isVectorized
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getAvailableLanguages
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getMatchedLanguage
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getDefaultRenderLanguage
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 canAnimateThumbIfAppropriate
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
4.02
 getMetadata
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHandlerState
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setHandlerState
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMetadataArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMetadataItem
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getMetadataItems
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getCommonMetaArray
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 convertMetadataVersion
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getBitDepth
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMimeType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMediaType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 canRender
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
20
 getCanRender
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 mustRender
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 allowInlineDisplay
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isSafeFile
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getIsSafeFile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIsSafeFileUncached
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
72
 isTrustedFile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 load
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 exists
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 isVisible
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTransformScript
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 getUnscaledThumb
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 thumbName
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 generateThumbName
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
4.25
 adjustThumbWidthForSteps
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
182
 createThumb
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 transformErrorOutput
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 transform
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
420
 generateAndSaveThumb
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
182
 generateBucketsIfNeeded
96.30% covered (success)
96.30%
26 / 27
0.00% covered (danger)
0.00%
0 / 1
10
 getThumbnailSource
77.50% covered (warning)
77.50%
31 / 40
0.00% covered (danger)
0.00%
0 / 1
12.38
 getBucketThumbPath
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getBucketThumbName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeTransformTmpFile
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getThumbDisposition
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getHandler
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 iconThumb
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getLastError
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getThumbnails
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 purgeCache
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 purgeDescription
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 purgeEverything
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 getHistory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 nextHistoryLine
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 resetHistory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHashPath
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getRel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getArchiveRel
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getThumbRel
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getUrlRel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getArchiveThumbRel
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getArchivePath
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getArchiveThumbPath
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getThumbPath
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getTranscodedPath
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getArchiveUrl
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getArchiveThumbUrl
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getZoneUrl
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getThumbUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFilePageThumbUrl
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getTranscodedUrl
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 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getArchiveVirtualUrl
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getThumbVirtualUrl
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 isHashed
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 readOnlyError
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 publish
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 formatMetadata
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 isLocal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getRepoName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getRepo
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isOld
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isDeleted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVisibility
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 wasDeleted
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 move
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 deleteFile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 restore
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isMultipage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 pageCount
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 scaleHeight
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 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getDescriptionText
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
42
 getUploader
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDescription
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTimestamp
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getDescriptionTouched
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSha1
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getStorageKey
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 userCan
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getContentHeaders
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getLongDesc
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getShortDesc
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getDimensionsString
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getRedirected
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRedirectedTitle
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 redirectedFrom
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isMissing
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isCacheable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 assertRepoDefined
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 assertTitleDefined
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 isExpensiveToThumbnail
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isTransformedLocally
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @defgroup FileAbstraction File abstraction
4 * @ingroup FileRepo
5 *
6 * Represents files in a repository.
7 */
8
9namespace MediaWiki\FileRepo\File;
10
11use LogicException;
12use MediaHandler;
13use MediaHandlerState;
14use MediaTransformError;
15use MediaTransformOutput;
16use MediaWiki\Config\ConfigException;
17use MediaWiki\Context\IContextSource;
18use MediaWiki\FileRepo\FileRepo;
19use MediaWiki\FileRepo\ForeignAPIRepo;
20use MediaWiki\FileRepo\LocalRepo;
21use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
22use MediaWiki\JobQueue\Jobs\HTMLCacheUpdateJob;
23use MediaWiki\Language\Language;
24use MediaWiki\Linker\LinkTarget;
25use MediaWiki\Logger\LoggerFactory;
26use MediaWiki\MainConfigNames;
27use MediaWiki\MediaWikiServices;
28use MediaWiki\Page\PageIdentity;
29use MediaWiki\Permissions\Authority;
30use MediaWiki\PoolCounter\PoolCounterWorkViaCallback;
31use MediaWiki\Status\Status;
32use MediaWiki\Title\Title;
33use MediaWiki\User\UserIdentity;
34use RuntimeException;
35use Shellbox\Command\BoxedCommand;
36use StatusValue;
37use ThumbnailImage;
38use Wikimedia\FileBackend\FileBackend;
39use Wikimedia\FileBackend\FSFile\FSFile;
40use Wikimedia\FileBackend\FSFile\TempFSFile;
41use Wikimedia\ObjectCache\WANObjectCache;
42
43/**
44 * Base code for files.
45 *
46 * This program is free software; you can redistribute it and/or modify
47 * it under the terms of the GNU General Public License as published by
48 * the Free Software Foundation; either version 2 of the License, or
49 * (at your option) any later version.
50 *
51 * This program is distributed in the hope that it will be useful,
52 * but WITHOUT ANY WARRANTY; without even the implied warranty of
53 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
54 * GNU General Public License for more details.
55 *
56 * You should have received a copy of the GNU General Public License along
57 * with this program; if not, write to the Free Software Foundation, Inc.,
58 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
59 * http://www.gnu.org/copyleft/gpl.html
60 *
61 * @file
62 * @ingroup FileAbstraction
63 */
64
65/**
66 * Implements some public methods and some protected utility functions which
67 * are required by multiple child classes. Contains stub functionality for
68 * unimplemented public methods.
69 *
70 * Stub functions which should be overridden are marked with STUB. Some more
71 * concrete functions are also typically overridden by child classes.
72 *
73 * Note that only the repo object knows what its file class is called. You should
74 * never name a file class explicitly outside of the repo class. Instead use the
75 * repo's factory functions to generate file objects, for example:
76 *
77 * RepoGroup::singleton()->getLocalRepo()->newFile( $title );
78 *
79 * Consider the services container below;
80 *
81 * $services = MediaWikiServices::getInstance();
82 *
83 * The convenience services $services->getRepoGroup()->getLocalRepo()->newFile()
84 * and $services->getRepoGroup()->findFile() should be sufficient in most cases.
85 *
86 * @TODO: DI - Instead of using MediaWikiServices::getInstance(), a service should
87 * ideally accept a RepoGroup in its constructor and then, use $this->repoGroup->findFile()
88 * and $this->repoGroup->getLocalRepo()->newFile().
89 *
90 * @stable to extend
91 * @ingroup FileAbstraction
92 */
93abstract class File implements MediaHandlerState {
94    use ProtectedHookAccessorTrait;
95
96    // Bitfield values akin to the revision deletion constants
97    public const DELETED_FILE = 1;
98    public const DELETED_COMMENT = 2;
99    public const DELETED_USER = 4;
100    public const DELETED_RESTRICTED = 8;
101
102    /** Force rendering in the current process */
103    public const RENDER_NOW = 1;
104    /**
105     * Force rendering even if thumbnail already exist and using RENDER_NOW
106     * I.e. you have to pass both flags: File::RENDER_NOW | File::RENDER_FORCE
107     */
108    public const RENDER_FORCE = 2;
109
110    public const DELETE_SOURCE = 1;
111
112    // Audience options for File::getDescription()
113    public const FOR_PUBLIC = 1;
114    public const FOR_THIS_USER = 2;
115    public const RAW = 3;
116
117    // Options for File::thumbName()
118    public const THUMB_FULL_NAME = 1;
119
120    /**
121     * Some member variables can be lazy-initialised using __get(). The
122     * initialisation function for these variables is always a function named
123     * like getVar(), where Var is the variable name with upper-case first
124     * letter.
125     *
126     * The following variables are initialised in this way in this base class:
127     *    name, extension, handler, path, canRender, isSafeFile,
128     *    transformScript, hashPath, pageCount, url
129     *
130     * Code within this class should generally use the accessor function
131     * directly, since __get() isn't re-entrant and therefore causes bugs that
132     * depend on initialisation order.
133     */
134
135    /**
136     * The following member variables are not lazy-initialised
137     */
138
139    /** @var FileRepo|LocalRepo|ForeignAPIRepo|false */
140    public $repo;
141
142    /** @var Title|string|false */
143    protected $title;
144
145    /** @var string Text of last error */
146    protected $lastError;
147
148    /** @var ?string The name that was used to access the file, before
149     *       resolving redirects. Main part of the title, with underscores
150     *       per Title::getDBkey().
151     */
152    protected $redirected;
153
154    /** @var Title */
155    protected $redirectedTitle;
156
157    /** @var FSFile|false|null False if undefined */
158    protected $fsFile;
159
160    /** @var MediaHandler|null */
161    protected $handler;
162
163    /** @var string|null The URL corresponding to one of the four basic zones */
164    protected $url;
165
166    /** @var string|null File extension */
167    protected $extension;
168
169    /** @var string|null The name of a file from its title object */
170    protected $name;
171
172    /** @var string|null The storage path corresponding to one of the zones */
173    protected $path;
174
175    /** @var string|null Relative path including trailing slash */
176    protected $hashPath;
177
178    /** @var int|false|null Number of pages of a multipage document, or false for
179     *    documents which aren't multipage documents
180     */
181    protected $pageCount;
182
183    /** @var string|false|null URL of transformscript (for example thumb.php) */
184    protected $transformScript;
185
186    /** @var Title */
187    protected $redirectTitle;
188
189    /** @var bool|null Whether the output of transform() for this file is likely to be valid. */
190    protected $canRender;
191
192    /** @var bool|null Whether this media file is in a format that is unlikely to
193     *    contain viruses or malicious content
194     */
195    protected $isSafeFile;
196
197    /** @var string Required Repository class type */
198    protected $repoClass = FileRepo::class;
199
200    /** @var array Cache of tmp filepaths pointing to generated bucket thumbnails, keyed by width */
201    protected $tmpBucketedThumbCache = [];
202
203    /** @var array */
204    private $handlerState = [];
205
206    /**
207     * Call this constructor from child classes.
208     *
209     * Both $title and $repo are optional, though some functions
210     * may return false or throw exceptions if they are not set.
211     * Most subclasses will want to call assertRepoDefined() here.
212     *
213     * @stable to call
214     * @param Title|string|false $title
215     * @param FileRepo|false $repo
216     */
217    public function __construct( $title, $repo ) {
218        // Some subclasses do not use $title, but set name/title some other way
219        if ( $title !== false ) {
220            $title = self::normalizeTitle( $title, 'exception' );
221        }
222        $this->title = $title;
223        $this->repo = $repo;
224    }
225
226    /**
227     * Given a string or Title object return either a
228     * valid Title object with namespace NS_FILE or null
229     *
230     * @param PageIdentity|LinkTarget|string $title
231     * @param string|false $exception Use 'exception' to throw an error on bad titles
232     * @return Title|null
233     */
234    public static function normalizeTitle( $title, $exception = false ) {
235        $ret = $title;
236
237        if ( !$ret instanceof Title ) {
238            if ( $ret instanceof PageIdentity ) {
239                $ret = Title::castFromPageIdentity( $ret );
240            } elseif ( $ret instanceof LinkTarget ) {
241                $ret = Title::castFromLinkTarget( $ret );
242            }
243        }
244
245        if ( $ret instanceof Title ) {
246            # Normalize NS_MEDIA -> NS_FILE
247            if ( $ret->getNamespace() === NS_MEDIA ) {
248                $ret = Title::makeTitleSafe( NS_FILE, $ret->getDBkey() );
249            # Double check the titles namespace
250            } elseif ( $ret->getNamespace() !== NS_FILE ) {
251                $ret = null;
252            }
253        } else {
254            # Convert strings to Title objects
255            $ret = Title::makeTitleSafe( NS_FILE, (string)$ret );
256        }
257        if ( !$ret && $exception !== false ) {
258            throw new RuntimeException( "`$title` is not a valid file title." );
259        }
260
261        return $ret;
262    }
263
264    public function __get( $name ) {
265        $function = [ $this, 'get' . ucfirst( $name ) ];
266        if ( !is_callable( $function ) ) {
267            return null;
268        } else {
269            $this->$name = $function();
270
271            return $this->$name;
272        }
273    }
274
275    /**
276     * Normalize a file extension to the common form, making it lowercase and checking some synonyms,
277     * and ensure it's clean. Extensions with non-alphanumeric characters will be discarded.
278     * Keep in sync with mw.Title.normalizeExtension() in JS.
279     *
280     * @param string $extension File extension (without the leading dot)
281     * @return string File extension in canonical form
282     */
283    public static function normalizeExtension( $extension ) {
284        $lower = strtolower( $extension );
285        $squish = [
286            'htm' => 'html',
287            'jpeg' => 'jpg',
288            'mpeg' => 'mpg',
289            'tiff' => 'tif',
290            'ogv' => 'ogg' ];
291        if ( isset( $squish[$lower] ) ) {
292            return $squish[$lower];
293        } elseif ( preg_match( '/^[0-9a-z]+$/', $lower ) ) {
294            return $lower;
295        } else {
296            return '';
297        }
298    }
299
300    /**
301     * Checks if file extensions are compatible
302     *
303     * @param File $old Old file
304     * @param string $new New name
305     *
306     * @return bool|null
307     */
308    public static function checkExtensionCompatibility( File $old, $new ) {
309        $oldMime = $old->getMimeType();
310        $n = strrpos( $new, '.' );
311        $newExt = self::normalizeExtension( $n ? substr( $new, $n + 1 ) : '' );
312        $mimeMagic = MediaWikiServices::getInstance()->getMimeAnalyzer();
313
314        return $mimeMagic->isMatchingExtension( $newExt, $oldMime );
315    }
316
317    /**
318     * Upgrade the database row if there is one
319     * Called by ImagePage
320     * STUB
321     *
322     * @stable to override
323     */
324    public function upgradeRow() {
325    }
326
327    /**
328     * Split an internet media type into its two components; if not
329     * a two-part name, set the minor type to 'unknown'.
330     *
331     * @param ?string $mime "text/html" etc
332     * @return string[] ("text", "html") etc
333     */
334    public static function splitMime( ?string $mime ) {
335        if ( $mime === null ) {
336            return [ 'unknown', 'unknown' ];
337        } elseif ( str_contains( $mime, '/' ) ) {
338            return explode( '/', $mime, 2 );
339        } else {
340            return [ $mime, 'unknown' ];
341        }
342    }
343
344    /**
345     * Callback for usort() to do file sorts by name
346     *
347     * @param File $a
348     * @param File $b
349     * @return int Result of name comparison
350     */
351    public static function compare( File $a, File $b ) {
352        return strcmp( $a->getName(), $b->getName() );
353    }
354
355    /**
356     * Return the name of this file
357     *
358     * @stable to override
359     * @return string
360     */
361    public function getName() {
362        if ( $this->name === null ) {
363            $this->assertRepoDefined();
364            $this->name = $this->repo->getNameFromTitle( $this->title );
365        }
366
367        return $this->name;
368    }
369
370    /**
371     * Get the file extension, e.g. "svg"
372     *
373     * @stable to override
374     * @return string
375     */
376    public function getExtension() {
377        if ( $this->extension === null ) {
378            $n = strrpos( $this->getName(), '.' );
379            $this->extension = self::normalizeExtension(
380                $n ? substr( $this->getName(), $n + 1 ) : '' );
381        }
382
383        return $this->extension;
384    }
385
386    /**
387     * Return the associated title object
388     *
389     * @return Title
390     */
391    public function getTitle() {
392        return $this->title;
393    }
394
395    /**
396     * Return the title used to find this file
397     *
398     * @return Title
399     */
400    public function getOriginalTitle() {
401        if ( $this->redirected !== null ) {
402            return $this->getRedirectedTitle();
403        }
404
405        return $this->title;
406    }
407
408    /**
409     * Return the URL of the file
410     * @stable to override
411     *
412     * @return string
413     */
414    public function getUrl() {
415        if ( $this->url === null ) {
416            $this->assertRepoDefined();
417            $ext = $this->getExtension();
418            $this->url = $this->repo->getZoneUrl( 'public', $ext ) . '/' . $this->getUrlRel();
419        }
420
421        return $this->url;
422    }
423
424    /**
425     * Get short description URL for a files based on the page ID
426     * @stable to override
427     *
428     * @return string|null
429     * @since 1.27
430     */
431    public function getDescriptionShortUrl() {
432        return null;
433    }
434
435    /**
436     * Return a fully-qualified URL to the file.
437     * Upload URL paths _may or may not_ be fully qualified, so
438     * we check. Local paths are assumed to belong on $wgServer.
439     * @stable to override
440     *
441     * @return string
442     */
443    public function getFullUrl() {
444        return (string)MediaWikiServices::getInstance()->getUrlUtils()
445            ->expand( $this->getUrl(), PROTO_RELATIVE );
446    }
447
448    /**
449     * @stable to override
450     * @return string
451     */
452    public function getCanonicalUrl() {
453        return (string)MediaWikiServices::getInstance()->getUrlUtils()
454            ->expand( $this->getUrl(), PROTO_CANONICAL );
455    }
456
457    /**
458     * @return string
459     */
460    public function getViewURL() {
461        if ( $this->mustRender() ) {
462            if ( $this->canRender() ) {
463                return $this->createThumb( $this->getWidth() );
464            } else {
465                wfDebug( __METHOD__ . ': supposed to render ' . $this->getName() .
466                    ' (' . $this->getMimeType() . "), but can't!" );
467
468                return $this->getUrl(); # hm... return NULL?
469            }
470        } else {
471            return $this->getUrl();
472        }
473    }
474
475    /**
476     * Return the storage path to the file. Note that this does
477     * not mean that a file actually exists under that location.
478     *
479     * This path depends on whether directory hashing is active or not,
480     * i.e. whether the files are all found in the same directory,
481     * or in hashed paths like /images/3/3c.
482     *
483     * Most callers don't check the return value, but ForeignAPIFile::getPath
484     * returns false.
485     *
486     * @stable to override
487     * @return string|false ForeignAPIFile::getPath can return false
488     */
489    public function getPath() {
490        if ( $this->path === null ) {
491            $this->assertRepoDefined();
492            $this->path = $this->repo->getZonePath( 'public' ) . '/' . $this->getRel();
493        }
494
495        return $this->path;
496    }
497
498    /**
499     * Get an FS copy or original of this file and return the path.
500     * Returns false on failure. Callers must not alter the file.
501     * Temporary files are cleared automatically.
502     *
503     * @return string|false False on failure
504     */
505    public function getLocalRefPath() {
506        $this->assertRepoDefined();
507        if ( !$this->fsFile ) {
508            $timer = MediaWikiServices::getInstance()->getStatsFactory()
509                ->getTiming( 'media_thumbnail_generate_fetchoriginal_seconds' )
510                ->copyToStatsdAt( 'media.thumbnail.generate.fetchoriginal' )
511                ->start();
512
513            $this->fsFile = $this->repo->getLocalReference( $this->getPath() );
514
515            $timer->stop();
516
517            if ( !$this->fsFile ) {
518                $this->fsFile = false; // null => false; cache negative hits
519            }
520        }
521
522        return ( $this->fsFile )
523            ? $this->fsFile->getPath()
524            : false;
525    }
526
527    /**
528     * Add the file to a Shellbox command as an input file
529     *
530     * @since 1.43
531     * @param BoxedCommand $command
532     * @param string $boxedName
533     * @return StatusValue
534     */
535    public function addToShellboxCommand( BoxedCommand $command, string $boxedName ) {
536        return $this->repo->addShellboxInputFile( $command, $boxedName, $this->getVirtualUrl() );
537    }
538
539    /**
540     * Return the width of the image. Returns false if the width is unknown
541     * or undefined.
542     *
543     * STUB
544     * Overridden by LocalFile, UnregisteredLocalFile
545     *
546     * @stable to override
547     * @param int $page
548     * @return int|false
549     */
550    public function getWidth( $page = 1 ) {
551        return false;
552    }
553
554    /**
555     * Return the height of the image. Returns false if the height is unknown
556     * or undefined
557     *
558     * STUB
559     * Overridden by LocalFile, UnregisteredLocalFile
560     *
561     * @stable to override
562     * @param int $page
563     * @return int|false False on failure
564     */
565    public function getHeight( $page = 1 ) {
566        return false;
567    }
568
569    /**
570     * Return the smallest bucket from $wgThumbnailBuckets which is at least
571     * $wgThumbnailMinimumBucketDistance larger than $desiredWidth. The returned bucket, if any,
572     * will always be bigger than $desiredWidth.
573     *
574     * @param int $desiredWidth
575     * @param int $page
576     * @return int|false
577     */
578    public function getThumbnailBucket( $desiredWidth, $page = 1 ) {
579        $thumbnailBuckets = MediaWikiServices::getInstance()
580            ->getMainConfig()->get( MainConfigNames::ThumbnailBuckets );
581        $thumbnailMinimumBucketDistance = MediaWikiServices::getInstance()
582            ->getMainConfig()->get( MainConfigNames::ThumbnailMinimumBucketDistance );
583        $imageWidth = $this->getWidth( $page );
584
585        if ( $imageWidth === false ) {
586            return false;
587        }
588
589        if ( $desiredWidth > $imageWidth ) {
590            return false;
591        }
592
593        if ( !$thumbnailBuckets ) {
594            return false;
595        }
596
597        $sortedBuckets = $thumbnailBuckets;
598
599        sort( $sortedBuckets );
600
601        foreach ( $sortedBuckets as $bucket ) {
602            if ( $bucket >= $imageWidth ) {
603                return false;
604            }
605
606            if ( $bucket - $thumbnailMinimumBucketDistance > $desiredWidth ) {
607                return $bucket;
608            }
609        }
610
611        // Image is bigger than any available bucket
612        return false;
613    }
614
615    /**
616     * Get the width and height to display image at.
617     *
618     * @param int $maxWidth Max width to display at
619     * @param int $maxHeight Max height to display at
620     * @param int $page
621     * @return array Array (width, height)
622     * @since 1.35
623     */
624    public function getDisplayWidthHeight( $maxWidth, $maxHeight, $page = 1 ) {
625        if ( !$maxWidth || !$maxHeight ) {
626            // should never happen
627            throw new ConfigException( 'Using a choice from $wgImageLimits that is 0x0' );
628        }
629
630        $width = $this->getWidth( $page );
631        $height = $this->getHeight( $page );
632        if ( !$width || !$height ) {
633            return [ 0, 0 ];
634        }
635
636        // Calculate the thumbnail size.
637        if ( $width <= $maxWidth && $height <= $maxHeight ) {
638            // Vectorized image, do nothing.
639        } elseif ( $width / $height >= $maxWidth / $maxHeight ) {
640            # The limiting factor is the width, not the height.
641            $height = round( $height * $maxWidth / $width );
642            $width = $maxWidth;
643            // Note that $height <= $maxHeight now.
644        } else {
645            $newwidth = floor( $width * $maxHeight / $height );
646            $height = round( $height * $newwidth / $width );
647            $width = $newwidth;
648            // Note that $height <= $maxHeight now, but might not be identical
649            // because of rounding.
650        }
651        return [ $width, $height ];
652    }
653
654    /**
655     * Get the duration of a media file in seconds
656     *
657     * @stable to override
658     * @return float|int
659     */
660    public function getLength() {
661        $handler = $this->getHandler();
662        if ( $handler ) {
663            return $handler->getLength( $this );
664        } else {
665            return 0;
666        }
667    }
668
669    /**
670     * Return true if the file is vectorized
671     *
672     * @return bool
673     */
674    public function isVectorized() {
675        $handler = $this->getHandler();
676        if ( $handler ) {
677            return $handler->isVectorized( $this );
678        } else {
679            return false;
680        }
681    }
682
683    /**
684     * Gives a (possibly empty) list of IETF languages to render
685     * the file in.
686     *
687     * If the file doesn't have translations, or if the file
688     * format does not support that sort of thing, returns
689     * an empty array.
690     *
691     * @return string[]
692     * @since 1.23
693     */
694    public function getAvailableLanguages() {
695        $handler = $this->getHandler();
696        if ( $handler ) {
697            return $handler->getAvailableLanguages( $this );
698        } else {
699            return [];
700        }
701    }
702
703    /**
704     * Get the IETF language code from the available languages for this file that matches the language
705     * requested by the user
706     *
707     * @param string $userPreferredLanguage
708     * @return string|null
709     */
710    public function getMatchedLanguage( $userPreferredLanguage ) {
711        $handler = $this->getHandler();
712        if ( $handler ) {
713            return $handler->getMatchedLanguage(
714                $userPreferredLanguage,
715                $handler->getAvailableLanguages( $this )
716            );
717        }
718
719        return null;
720    }
721
722    /**
723     * In files that support multiple language, what is the default language
724     * to use if none specified.
725     *
726     * @return string|null IETF Lang code, or null if filetype doesn't support multiple languages.
727     * @since 1.23
728     */
729    public function getDefaultRenderLanguage() {
730        $handler = $this->getHandler();
731        if ( $handler ) {
732            return $handler->getDefaultRenderLanguage( $this );
733        } else {
734            return null;
735        }
736    }
737
738    /**
739     * Will the thumbnail be animated if one would expect it to be.
740     *
741     * Currently used to add a warning to the image description page
742     *
743     * @return bool False if the main image is both animated
744     *   and the thumbnail is not. In all other cases must return
745     *   true. If image is not renderable whatsoever, should
746     *   return true.
747     */
748    public function canAnimateThumbIfAppropriate() {
749        $handler = $this->getHandler();
750        if ( !$handler ) {
751            // We cannot handle image whatsoever, thus
752            // one would not expect it to be animated
753            // so true.
754            return true;
755        }
756
757        return !$this->allowInlineDisplay()
758            // Image is not animated, so one would
759            // not expect thumb to be
760            || !$handler->isAnimatedImage( $this )
761            // Image is animated, but thumbnail isn't.
762            // This is unexpected to the user.
763            || $handler->canAnimateThumbnail( $this );
764    }
765
766    /**
767     * Get handler-specific metadata
768     * Overridden by LocalFile, UnregisteredLocalFile
769     * STUB
770     * @deprecated since 1.37 use getMetadataArray() or getMetadataItem()
771     * @return string|false
772     */
773    public function getMetadata() {
774        return false;
775    }
776
777    public function getHandlerState( string $key ) {
778        return $this->handlerState[$key] ?? null;
779    }
780
781    public function setHandlerState( string $key, $value ) {
782        $this->handlerState[$key] = $value;
783    }
784
785    /**
786     * Get the unserialized handler-specific metadata
787     * STUB
788     * @since 1.37
789     * @return array
790     */
791    public function getMetadataArray(): array {
792        return [];
793    }
794
795    /**
796     * Get a specific element of the unserialized handler-specific metadata.
797     *
798     * @since 1.37
799     * @param string $itemName
800     * @return mixed
801     */
802    public function getMetadataItem( string $itemName ) {
803        $items = $this->getMetadataItems( [ $itemName ] );
804        return $items[$itemName] ?? null;
805    }
806
807    /**
808     * Get multiple elements of the unserialized handler-specific metadata.
809     *
810     * @since 1.37
811     * @param string[] $itemNames
812     * @return array
813     */
814    public function getMetadataItems( array $itemNames ): array {
815        return array_intersect_key(
816            $this->getMetadataArray(),
817            array_fill_keys( $itemNames, true ) );
818    }
819
820    /**
821     * Like getMetadata but returns a handler independent array of common values.
822     * @see MediaHandler::getCommonMetaArray()
823     * @return array|false Array or false if not supported
824     * @since 1.23
825     */
826    public function getCommonMetaArray() {
827        $handler = $this->getHandler();
828        return $handler ? $handler->getCommonMetaArray( $this ) : false;
829    }
830
831    /**
832     * get versioned metadata
833     *
834     * @param array $metadata Array of unserialized metadata
835     * @param int|string $version Version number.
836     * @return array Array containing metadata, or what was passed to it on fail
837     */
838    public function convertMetadataVersion( $metadata, $version ) {
839        $handler = $this->getHandler();
840        if ( $handler ) {
841            return $handler->convertMetadataVersion( $metadata, $version );
842        } else {
843            return $metadata;
844        }
845    }
846
847    /**
848     * Return the bit depth of the file
849     * Overridden by LocalFile
850     * STUB
851     * @stable to override
852     * @return int
853     */
854    public function getBitDepth() {
855        return 0;
856    }
857
858    /**
859     * Return the size of the image file, in bytes
860     * Overridden by LocalFile, UnregisteredLocalFile
861     * STUB
862     * @stable to override
863     * @return int|false
864     */
865    public function getSize() {
866        return false;
867    }
868
869    /**
870     * Returns the MIME type of the file.
871     * Overridden by LocalFile, UnregisteredLocalFile
872     * STUB
873     *
874     * @stable to override
875     * @return string
876     */
877    public function getMimeType() {
878        return 'unknown/unknown';
879    }
880
881    /**
882     * Return the type of the media in the file.
883     * Use the value returned by this function with the MEDIATYPE_xxx constants.
884     * Overridden by LocalFile,
885     * STUB
886     * @stable to override
887     * @return string
888     */
889    public function getMediaType() {
890        return MEDIATYPE_UNKNOWN;
891    }
892
893    /**
894     * Checks if the output of transform() for this file is likely to be valid.
895     *
896     * In other words, this will return true if a thumbnail can be provided for this
897     * image (e.g. if [[File:...|thumb]] produces a result on a wikitext page).
898     *
899     * If this is false, various user elements will display a placeholder instead.
900     *
901     * @return bool
902     */
903    public function canRender() {
904        if ( $this->canRender === null ) {
905            $this->canRender = $this->getHandler() && $this->handler->canRender( $this ) && $this->exists();
906        }
907
908        return $this->canRender;
909    }
910
911    /**
912     * Accessor for __get()
913     * @return bool
914     */
915    protected function getCanRender() {
916        return $this->canRender();
917    }
918
919    /**
920     * Return true if the file is of a type that can't be directly
921     * rendered by typical browsers and needs to be re-rasterized.
922     *
923     * This returns true for everything but the bitmap types
924     * supported by all browsers, i.e. JPEG; GIF and PNG. It will
925     * also return true for any non-image formats.
926     *
927     * @stable to override
928     * @return bool
929     */
930    public function mustRender() {
931        return $this->getHandler() && $this->handler->mustRender( $this );
932    }
933
934    /**
935     * Alias for canRender()
936     *
937     * @return bool
938     */
939    public function allowInlineDisplay() {
940        return $this->canRender();
941    }
942
943    /**
944     * Determines if this media file is in a format that is unlikely to
945     * contain viruses or malicious content. It uses the global
946     * $wgTrustedMediaFormats list to determine if the file is safe.
947     *
948     * This is used to show a warning on the description page of non-safe files.
949     * It may also be used to disallow direct [[media:...]] links to such files.
950     *
951     * Note that this function will always return true if allowInlineDisplay()
952     * or isTrustedFile() is true for this file.
953     *
954     * @return bool
955     */
956    public function isSafeFile() {
957        if ( $this->isSafeFile === null ) {
958            $this->isSafeFile = $this->getIsSafeFileUncached();
959        }
960
961        return $this->isSafeFile;
962    }
963
964    /**
965     * Accessor for __get()
966     *
967     * @return bool
968     */
969    protected function getIsSafeFile() {
970        return $this->isSafeFile();
971    }
972
973    /**
974     * Uncached accessor
975     *
976     * @return bool
977     */
978    protected function getIsSafeFileUncached() {
979        $trustedMediaFormats = MediaWikiServices::getInstance()->getMainConfig()
980            ->get( MainConfigNames::TrustedMediaFormats );
981
982        if ( $this->allowInlineDisplay() ) {
983            return true;
984        }
985        if ( $this->isTrustedFile() ) {
986            return true;
987        }
988
989        $type = $this->getMediaType();
990        $mime = $this->getMimeType();
991
992        if ( !$type || $type === MEDIATYPE_UNKNOWN ) {
993            return false; # unknown type, not trusted
994        }
995        if ( in_array( $type, $trustedMediaFormats ) ) {
996            return true;
997        }
998
999        if ( $mime === "unknown/unknown" ) {
1000            return false; # unknown type, not trusted
1001        }
1002        if ( in_array( $mime, $trustedMediaFormats ) ) {
1003            return true;
1004        }
1005
1006        return false;
1007    }
1008
1009    /**
1010     * Returns true if the file is flagged as trusted. Files flagged that way
1011     * can be linked to directly, even if that is not allowed for this type of
1012     * file normally.
1013     *
1014     * This is a dummy function right now and always returns false. It could be
1015     * implemented to extract a flag from the database. The trusted flag could be
1016     * set on upload, if the user has sufficient privileges, to bypass script-
1017     * and html-filters. It may even be coupled with cryptographic signatures
1018     * or such.
1019     *
1020     * @return bool
1021     */
1022    protected function isTrustedFile() {
1023        # this could be implemented to check a flag in the database,
1024        # look for signatures, etc
1025        return false;
1026    }
1027
1028    /**
1029     * Load any lazy-loaded file object fields from source
1030     *
1031     * This is only useful when setting $flags
1032     *
1033     * Overridden by LocalFile to actually query the DB
1034     *
1035     * @stable to override
1036     * @param int $flags Bitfield of IDBAccessObject::READ_* constants
1037     */
1038    public function load( $flags = 0 ) {
1039    }
1040
1041    /**
1042     * Returns true if file exists in the repository.
1043     *
1044     * Overridden by LocalFile to avoid unnecessary stat calls.
1045     *
1046     * @stable to override
1047     * @return bool Whether file exists in the repository.
1048     */
1049    public function exists() {
1050        return $this->getPath() && $this->repo->fileExists( $this->path );
1051    }
1052
1053    /**
1054     * Returns true if file exists in the repository and can be included in a page.
1055     * It would be unsafe to include private images, making public thumbnails inadvertently
1056     *
1057     * @stable to override
1058     * @return bool Whether file exists in the repository and is includable.
1059     */
1060    public function isVisible() {
1061        return $this->exists();
1062    }
1063
1064    /**
1065     * @return string|false
1066     */
1067    private function getTransformScript() {
1068        if ( $this->transformScript === null ) {
1069            $this->transformScript = false;
1070            if ( $this->repo ) {
1071                $script = $this->repo->getThumbScriptUrl();
1072                if ( $script ) {
1073                    $this->transformScript = wfAppendQuery( $script, [ 'f' => $this->getName() ] );
1074                }
1075            }
1076        }
1077
1078        return $this->transformScript;
1079    }
1080
1081    /**
1082     * Get a ThumbnailImage which is the same size as the source
1083     *
1084     * @param array $handlerParams
1085     *
1086     * @return ThumbnailImage|MediaTransformOutput|false False on failure
1087     */
1088    public function getUnscaledThumb( $handlerParams = [] ) {
1089        $hp =& $handlerParams;
1090        $page = $hp['page'] ?? false;
1091        $width = $this->getWidth( $page );
1092        if ( !$width ) {
1093            return $this->iconThumb();
1094        }
1095        $hp['width'] = $width;
1096        // be sure to ignore any height specification as well (T64258)
1097        unset( $hp['height'] );
1098
1099        return $this->transform( $hp );
1100    }
1101
1102    /**
1103     * Return the file name of a thumbnail with the specified parameters.
1104     * Use File::THUMB_FULL_NAME to always get a name like "<params>-<source>".
1105     * Otherwise, the format may be "<params>-<source>" or "<params>-thumbnail.<ext>".
1106     * @stable to override
1107     *
1108     * @param array $params Handler-specific parameters
1109     * @param int $flags Bitfield that supports THUMB_* constants
1110     * @return string|null
1111     */
1112    public function thumbName( $params, $flags = 0 ) {
1113        $name = ( $this->repo && !( $flags & self::THUMB_FULL_NAME ) )
1114            ? $this->repo->nameForThumb( $this->getName() )
1115            : $this->getName();
1116
1117        return $this->generateThumbName( $name, $params );
1118    }
1119
1120    /**
1121     * Generate a thumbnail file name from a name and specified parameters
1122     * @stable to override
1123     *
1124     * @param string $name
1125     * @param array $params Parameters which will be passed to MediaHandler::makeParamString
1126     * @return string|null
1127     */
1128    public function generateThumbName( $name, $params ) {
1129        if ( !$this->getHandler() ) {
1130            return null;
1131        }
1132        $extension = $this->getExtension();
1133        [ $thumbExt, ] = $this->getHandler()->getThumbType(
1134            $extension, $this->getMimeType(), $params );
1135        $thumbName = $this->getHandler()->makeParamString( $this->adjustThumbWidthForSteps( $params ) );
1136
1137        if ( $this->repo->supportsSha1URLs() ) {
1138            $thumbName .= '-' . $this->getSha1() . '.' . $thumbExt;
1139        } else {
1140            $thumbName .= '-' . $name;
1141
1142            if ( $thumbExt != $extension ) {
1143                $thumbName .= ".$thumbExt";
1144            }
1145        }
1146
1147        return $thumbName;
1148    }
1149
1150    private function adjustThumbWidthForSteps( array $params ): array {
1151        $thumbnailSteps = MediaWikiServices::getInstance()
1152            ->getMainConfig()->get( MainConfigNames::ThumbnailSteps );
1153        $thumbnailStepsRatio = MediaWikiServices::getInstance()
1154            ->getMainConfig()->get( MainConfigNames::ThumbnailStepsRatio );
1155
1156        if ( !$thumbnailSteps || !$thumbnailStepsRatio ) {
1157            return $params;
1158        }
1159        if ( !isset( $params['physicalWidth'] ) || !$params['physicalWidth'] ) {
1160            return $params;
1161        }
1162
1163        if ( $thumbnailStepsRatio < 1 ) {
1164            // If thumbnail ratio is below 100%, build a random number
1165            // out of the file name and decide whether to apply adjustments
1166            // based on that. This way, we get a good uniformity while not going
1167            // back and forth between old and new in different requests.
1168            // Also this way, ramping up (e.g. from 0.1 to 0.2) would also
1169            // cover the previous values too which would reduce the scale of changes.
1170            $hash = hexdec( substr( md5( $this->name ), 0, 8 ) ) & 0x7fffffff;
1171            if ( ( $hash % 1000 ) > ( $thumbnailStepsRatio * 1000 ) ) {
1172                return $params;
1173            }
1174        }
1175
1176        $newThumbSize = null;
1177        foreach ( $thumbnailSteps as $widthStep ) {
1178            if ( $widthStep > $this->getWidth() ) {
1179                return $params;
1180            }
1181            if ( $widthStep == $params['physicalWidth'] ) {
1182                return $params;
1183            }
1184            if ( $widthStep > $params['physicalWidth'] ) {
1185                $newThumbSize = $widthStep;
1186                break;
1187            }
1188        }
1189        if ( !$newThumbSize ) {
1190            return $params;
1191        }
1192
1193        if ( isset( $params['physicalHeight'] ) ) {
1194            $params['physicalHeight'] = intval(
1195                $params['physicalHeight'] *
1196                ( $newThumbSize / $params['physicalWidth'] )
1197            );
1198        }
1199        $params['physicalWidth'] = $newThumbSize;
1200        return $params;
1201    }
1202
1203    /**
1204     * Create a thumbnail of the image having the specified width/height.
1205     * The thumbnail will not be created if the width is larger than the
1206     * image's width. Let the browser do the scaling in this case.
1207     * The thumbnail is stored on disk and is only computed if the thumbnail
1208     * file does not exist OR if it is older than the image.
1209     * Returns the URL.
1210     *
1211     * Keeps aspect ratio of original image. If both width and height are
1212     * specified, the generated image will be no bigger than width x height,
1213     * and will also have correct aspect ratio.
1214     *
1215     * @param int $width Maximum width of the generated thumbnail
1216     * @param int $height Maximum height of the image (optional)
1217     *
1218     * @return string
1219     */
1220    public function createThumb( $width, $height = -1 ) {
1221        $params = [ 'width' => $width ];
1222        if ( $height != -1 ) {
1223            $params['height'] = $height;
1224        }
1225        $thumb = $this->transform( $params );
1226        if ( !$thumb || $thumb->isError() ) {
1227            return '';
1228        }
1229
1230        return $thumb->getUrl();
1231    }
1232
1233    /**
1234     * Return either a MediaTransformError or placeholder thumbnail (if $wgIgnoreImageErrors)
1235     *
1236     * @param string $thumbPath Thumbnail storage path
1237     * @param string $thumbUrl Thumbnail URL
1238     * @param array $params
1239     * @param int $flags
1240     * @return MediaTransformOutput
1241     */
1242    protected function transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags ) {
1243        $ignoreImageErrors = MediaWikiServices::getInstance()->getMainConfig()
1244            ->get( MainConfigNames::IgnoreImageErrors );
1245
1246        $handler = $this->getHandler();
1247        if ( $handler && $ignoreImageErrors && !( $flags & self::RENDER_NOW ) ) {
1248            return $handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
1249        } else {
1250            return new MediaTransformError( 'thumbnail_error',
1251                $params['width'], 0, wfMessage( 'thumbnail-dest-create' ) );
1252        }
1253    }
1254
1255    /**
1256     * Transform a media file
1257     * @stable to override
1258     *
1259     * @param array $params An associative array of handler-specific parameters.
1260     *   Typical keys are width, height and page.
1261     * @param int $flags A bitfield, may contain self::RENDER_NOW to force rendering
1262     * @return ThumbnailImage|MediaTransformOutput|false False on failure
1263     */
1264    public function transform( $params, $flags = 0 ) {
1265        $thumbnailEpoch = MediaWikiServices::getInstance()->getMainConfig()
1266            ->get( MainConfigNames::ThumbnailEpoch );
1267
1268        do {
1269            if ( !$this->canRender() ) {
1270                $thumb = $this->iconThumb();
1271                break; // not a bitmap or renderable image, don't try
1272            }
1273
1274            // Get the descriptionUrl to embed it as comment into the thumbnail. T21791.
1275            $descriptionUrl = $this->getDescriptionUrl();
1276            if ( $descriptionUrl ) {
1277                $params['descriptionUrl'] = MediaWikiServices::getInstance()->getUrlUtils()
1278                    ->expand( $descriptionUrl, PROTO_CANONICAL );
1279            }
1280
1281            $handler = $this->getHandler();
1282            $script = $this->getTransformScript();
1283            if ( $script && !( $flags & self::RENDER_NOW ) ) {
1284                // Use a script to transform on client request, if possible
1285                $thumb = $handler->getScriptedTransform( $this, $script, $params );
1286                if ( $thumb ) {
1287                    break;
1288                }
1289            }
1290
1291            $normalisedParams = $params;
1292            $handler->normaliseParams( $this, $normalisedParams );
1293
1294            $thumbName = $this->thumbName( $normalisedParams );
1295            $thumbUrl = $this->getThumbUrl( $thumbName );
1296            $thumbPath = $this->getThumbPath( $thumbName ); // final thumb path
1297            if ( isset( $normalisedParams['isFilePageThumb'] ) && $normalisedParams['isFilePageThumb'] ) {
1298                // Use a versioned URL on file description pages
1299                $thumbUrl = $this->getFilePageThumbUrl( $thumbUrl );
1300            }
1301
1302            if ( $this->repo ) {
1303                // Defer rendering if a 404 handler is set up...
1304                if ( $this->repo->canTransformVia404() && !( $flags & self::RENDER_NOW ) ) {
1305                    // XXX: Pass in the storage path even though we are not rendering anything
1306                    // and the path is supposed to be an FS path. This is due to getScalerType()
1307                    // getting called on the path and clobbering $thumb->getUrl() if it's false.
1308                    $thumb = $handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
1309                    break;
1310                }
1311                // Check if an up-to-date thumbnail already exists...
1312                wfDebug( __METHOD__ . ": Doing stat for $thumbPath" );
1313                if ( !( $flags & self::RENDER_FORCE ) && $this->repo->fileExists( $thumbPath ) ) {
1314                    $timestamp = $this->repo->getFileTimestamp( $thumbPath );
1315                    if ( $timestamp !== false && $timestamp >= $thumbnailEpoch ) {
1316                        // XXX: Pass in the storage path even though we are not rendering anything
1317                        // and the path is supposed to be an FS path. This is due to getScalerType()
1318                        // getting called on the path and clobbering $thumb->getUrl() if it's false.
1319                        $thumb = $handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
1320                        $thumb->setStoragePath( $thumbPath );
1321                        break;
1322                    }
1323                } elseif ( $flags & self::RENDER_FORCE ) {
1324                    wfDebug( __METHOD__ . ": forcing rendering per flag File::RENDER_FORCE" );
1325                }
1326
1327                // If the backend is ready-only, don't keep generating thumbnails
1328                // only to return transformation errors, just return the error now.
1329                if ( $this->repo->getReadOnlyReason() !== false ) {
1330                    $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags );
1331                    break;
1332                }
1333
1334                // Check to see if local transformation is disabled.
1335                if ( !$this->repo->canTransformLocally() ) {
1336                    LoggerFactory::getInstance( 'thumbnail' )
1337                        ->error( 'Local transform denied by configuration' );
1338                    $thumb = new MediaTransformError(
1339                        wfMessage(
1340                            'thumbnail_error',
1341                            'MediaWiki is configured to disallow local image scaling'
1342                        ),
1343                        $params['width'],
1344                        0
1345                    );
1346                    break;
1347                }
1348            }
1349
1350            $tmpFile = $this->makeTransformTmpFile( $thumbPath );
1351
1352            if ( !$tmpFile ) {
1353                $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags );
1354            } else {
1355                $thumb = $this->generateAndSaveThumb( $tmpFile, $params, $flags );
1356            }
1357        } while ( false );
1358
1359        return $thumb ?: false;
1360    }
1361
1362    /**
1363     * Generates a thumbnail according to the given parameters and saves it to storage
1364     * @param TempFSFile $tmpFile Temporary file where the rendered thumbnail will be saved
1365     * @param array $transformParams
1366     * @param int $flags
1367     * @return MediaTransformOutput|false
1368     */
1369    public function generateAndSaveThumb( $tmpFile, $transformParams, $flags ) {
1370        $ignoreImageErrors = MediaWikiServices::getInstance()->getMainConfig()
1371            ->get( MainConfigNames::IgnoreImageErrors );
1372
1373        if ( !$this->repo->canTransformLocally() ) {
1374            LoggerFactory::getInstance( 'thumbnail' )
1375                ->error( 'Local transform denied by configuration' );
1376            return new MediaTransformError(
1377                wfMessage(
1378                    'thumbnail_error',
1379                    'MediaWiki is configured to disallow local image scaling'
1380                ),
1381                $transformParams['width'],
1382                0
1383            );
1384        }
1385
1386        $statsFactory = MediaWikiServices::getInstance()->getStatsFactory();
1387
1388        $handler = $this->getHandler();
1389
1390        $normalisedParams = $transformParams;
1391        $handler->normaliseParams( $this, $normalisedParams );
1392
1393        $thumbName = $this->thumbName( $normalisedParams );
1394        $thumbUrl = $this->getThumbUrl( $thumbName );
1395        $thumbPath = $this->getThumbPath( $thumbName ); // final thumb path
1396        if ( isset( $normalisedParams['isFilePageThumb'] ) && $normalisedParams['isFilePageThumb'] ) {
1397            // Use a versioned URL on file description pages
1398            $thumbUrl = $this->getFilePageThumbUrl( $thumbUrl );
1399        }
1400
1401        $tmpThumbPath = $tmpFile->getPath();
1402
1403        if ( $handler->supportsBucketing() ) {
1404            $this->generateBucketsIfNeeded( $normalisedParams, $flags );
1405        }
1406
1407        # T367110
1408        # Calls to doTransform() can recur back on $this->transform()
1409        # depending on implementation. One such example is PagedTiffHandler.
1410        # TimingMetric->start() and stop() cannot be used in this situation
1411        # so we will track the time manually.
1412        $starttime = microtime( true );
1413
1414        // Actually render the thumbnail...
1415        $thumb = $handler->doTransform( $this, $tmpThumbPath, $thumbUrl, $transformParams );
1416        $tmpFile->bind( $thumb ); // keep alive with $thumb
1417
1418        $statsFactory->getTiming( 'media_thumbnail_generate_transform_seconds' )
1419            ->copyToStatsdAt( 'media.thumbnail.generate.transform' )
1420            ->observe( ( microtime( true ) - $starttime ) * 1000 );
1421
1422        if ( !$thumb ) { // bad params?
1423            $thumb = false;
1424        } elseif ( $thumb->isError() ) { // transform error
1425            /** @var MediaTransformError $thumb */
1426            '@phan-var MediaTransformError $thumb';
1427            $this->lastError = $thumb->toText();
1428            // Ignore errors if requested
1429            if ( $ignoreImageErrors && !( $flags & self::RENDER_NOW ) ) {
1430                $thumb = $handler->getTransform( $this, $tmpThumbPath, $thumbUrl, $transformParams );
1431            }
1432        } elseif ( $this->repo && $thumb->hasFile() && !$thumb->fileIsSource() ) {
1433            // Copy the thumbnail from the file system into storage...
1434
1435            $timer = $statsFactory->getTiming( 'media_thumbnail_generate_store_seconds' )
1436                ->copyToStatsdAt( 'media.thumbnail.generate.store' )
1437                ->start();
1438
1439            wfDebug( __METHOD__ . ": copying $tmpThumbPath to $thumbPath" );
1440            $disposition = $this->getThumbDisposition( $thumbName );
1441            $status = $this->repo->quickImport( $tmpThumbPath, $thumbPath, $disposition );
1442            if ( $status->isOK() ) {
1443                $thumb->setStoragePath( $thumbPath );
1444            } else {
1445                $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $transformParams, $flags );
1446            }
1447
1448            $timer->stop();
1449
1450            // Give extensions a chance to do something with this thumbnail...
1451            $this->getHookRunner()->onFileTransformed( $this, $thumb, $tmpThumbPath, $thumbPath );
1452        }
1453
1454        return $thumb;
1455    }
1456
1457    /**
1458     * Generates chained bucketed thumbnails if needed
1459     * @param array $params
1460     * @param int $flags
1461     * @return bool Whether at least one bucket was generated
1462     */
1463    protected function generateBucketsIfNeeded( $params, $flags = 0 ) {
1464        if ( !$this->repo
1465            || !isset( $params['physicalWidth'] )
1466            || !isset( $params['physicalHeight'] )
1467        ) {
1468            return false;
1469        }
1470
1471        $bucket = $this->getThumbnailBucket( $params['physicalWidth'] );
1472
1473        if ( !$bucket || $bucket == $params['physicalWidth'] ) {
1474            return false;
1475        }
1476
1477        $bucketPath = $this->getBucketThumbPath( $bucket );
1478
1479        if ( $this->repo->fileExists( $bucketPath ) ) {
1480            return false;
1481        }
1482
1483        $timer = MediaWikiServices::getInstance()->getStatsFactory()
1484            ->getTiming( 'media_thumbnail_generate_bucket_seconds' )
1485            ->copyToStatsdAt( 'media.thumbnail.generate.bucket' );
1486        $timer->start();
1487
1488        $params['physicalWidth'] = $bucket;
1489        $params['width'] = $bucket;
1490
1491        $params = $this->getHandler()->sanitizeParamsForBucketing( $params );
1492
1493        $tmpFile = $this->makeTransformTmpFile( $bucketPath );
1494
1495        if ( !$tmpFile ) {
1496            return false;
1497        }
1498
1499        $thumb = $this->generateAndSaveThumb( $tmpFile, $params, $flags );
1500
1501        if ( !$thumb || $thumb->isError() ) {
1502            return false;
1503        }
1504
1505        $timer->stop();
1506
1507        $this->tmpBucketedThumbCache[$bucket] = $tmpFile->getPath();
1508        // For the caching to work, we need to make the tmp file survive as long as
1509        // this object exists
1510        $tmpFile->bind( $this );
1511
1512        return true;
1513    }
1514
1515    /**
1516     * Returns the most appropriate source image for the thumbnail, given a target thumbnail size
1517     * @param array $params
1518     * @return array Source path and width/height of the source
1519     */
1520    public function getThumbnailSource( $params ) {
1521        if ( $this->repo
1522            && $this->getHandler()->supportsBucketing()
1523            && isset( $params['physicalWidth'] )
1524        ) {
1525            $bucket = $this->getThumbnailBucket( $params['physicalWidth'] );
1526            if ( $bucket ) {
1527                if ( $this->getWidth() != 0 ) {
1528                    $bucketHeight = round( $this->getHeight() * ( $bucket / $this->getWidth() ) );
1529                } else {
1530                    $bucketHeight = 0;
1531                }
1532
1533                // Try to avoid reading from storage if the file was generated by this script
1534                if ( isset( $this->tmpBucketedThumbCache[$bucket] ) ) {
1535                    $tmpPath = $this->tmpBucketedThumbCache[$bucket];
1536
1537                    if ( file_exists( $tmpPath ) ) {
1538                        return [
1539                            'path' => $tmpPath,
1540                            'width' => $bucket,
1541                            'height' => $bucketHeight
1542                        ];
1543                    }
1544                }
1545
1546                $bucketPath = $this->getBucketThumbPath( $bucket );
1547
1548                if ( $this->repo->fileExists( $bucketPath ) ) {
1549                    $fsFile = $this->repo->getLocalReference( $bucketPath );
1550
1551                    if ( $fsFile ) {
1552                        return [
1553                            'path' => $fsFile->getPath(),
1554                            'width' => $bucket,
1555                            'height' => $bucketHeight
1556                        ];
1557                    }
1558                }
1559            }
1560        }
1561
1562        // Thumbnailing a very large file could result in network saturation if
1563        // everyone does it at once.
1564        if ( $this->getSize() >= 1e7 ) { // 10 MB
1565            $work = new PoolCounterWorkViaCallback( 'GetLocalFileCopy', sha1( $this->getName() ),
1566                [
1567                    'doWork' => function () {
1568                        return $this->getLocalRefPath();
1569                    }
1570                ]
1571            );
1572            $srcPath = $work->execute();
1573        } else {
1574            $srcPath = $this->getLocalRefPath();
1575        }
1576
1577        // Original file
1578        return [
1579            'path' => $srcPath,
1580            'width' => $this->getWidth(),
1581            'height' => $this->getHeight()
1582        ];
1583    }
1584
1585    /**
1586     * Returns the repo path of the thumb for a given bucket
1587     * @param int $bucket
1588     * @return string
1589     */
1590    protected function getBucketThumbPath( $bucket ) {
1591        $thumbName = $this->getBucketThumbName( $bucket );
1592        return $this->getThumbPath( $thumbName );
1593    }
1594
1595    /**
1596     * Returns the name of the thumb for a given bucket
1597     * @param int $bucket
1598     * @return string
1599     */
1600    protected function getBucketThumbName( $bucket ) {
1601        return $this->thumbName( [ 'physicalWidth' => $bucket ] );
1602    }
1603
1604    /**
1605     * Creates a temp FS file with the same extension and the thumbnail
1606     * @param string $thumbPath Thumbnail path
1607     * @return TempFSFile|null
1608     */
1609    protected function makeTransformTmpFile( $thumbPath ) {
1610        $thumbExt = FileBackend::extensionFromPath( $thumbPath );
1611        return MediaWikiServices::getInstance()->getTempFSFileFactory()
1612            ->newTempFSFile( 'transform_', $thumbExt );
1613    }
1614
1615    /**
1616     * @param string $thumbName Thumbnail name
1617     * @param string $dispositionType Type of disposition (either "attachment" or "inline")
1618     * @return string Content-Disposition header value
1619     */
1620    public function getThumbDisposition( $thumbName, $dispositionType = 'inline' ) {
1621        $fileName = $this->getName(); // file name to suggest
1622        $thumbExt = FileBackend::extensionFromPath( $thumbName );
1623        if ( $thumbExt != '' && $thumbExt !== $this->getExtension() ) {
1624            $fileName .= ".$thumbExt";
1625        }
1626
1627        return FileBackend::makeContentDisposition( $dispositionType, $fileName );
1628    }
1629
1630    /**
1631     * Get a MediaHandler instance for this file
1632     *
1633     * @return MediaHandler|false Registered MediaHandler for file's MIME type
1634     *   or false if none found
1635     */
1636    public function getHandler() {
1637        if ( !$this->handler ) {
1638            $this->handler = MediaHandler::getHandler( $this->getMimeType() );
1639        }
1640
1641        return $this->handler;
1642    }
1643
1644    /**
1645     * Get a ThumbnailImage representing a file type icon
1646     *
1647     * @return ThumbnailImage|null
1648     */
1649    public function iconThumb() {
1650        global $IP;
1651        $resourceBasePath = MediaWikiServices::getInstance()->getMainConfig()
1652            ->get( MainConfigNames::ResourceBasePath );
1653        $assetsPath = "{$resourceBasePath}/resources/assets/file-type-icons/";
1654        $assetsDirectory = "$IP/resources/assets/file-type-icons/";
1655
1656        $try = [ 'fileicon-' . $this->getExtension() . '.png', 'fileicon.png' ];
1657        foreach ( $try as $icon ) {
1658            if ( file_exists( $assetsDirectory . $icon ) ) { // always FS
1659                $params = [ 'width' => 120, 'height' => 120 ];
1660
1661                return new ThumbnailImage( $this, $assetsPath . $icon, false, $params );
1662            }
1663        }
1664
1665        return null;
1666    }
1667
1668    /**
1669     * Get last thumbnailing error.
1670     * Largely obsolete.
1671     * @return string
1672     */
1673    public function getLastError() {
1674        return $this->lastError;
1675    }
1676
1677    /**
1678     * Get all thumbnail names previously generated for this file
1679     * STUB
1680     * Overridden by LocalFile
1681     * @stable to override
1682     * @return string[]
1683     */
1684    protected function getThumbnails() {
1685        return [];
1686    }
1687
1688    /**
1689     * Purge shared caches such as thumbnails and DB data caching
1690     * STUB
1691     * Overridden by LocalFile
1692     * @stable to override
1693     * @param array $options Options, which include:
1694     *   'forThumbRefresh' : The purging is only to refresh thumbnails
1695     */
1696    public function purgeCache( $options = [] ) {
1697    }
1698
1699    /**
1700     * Purge the file description page, but don't go after
1701     * pages using the file. Use when modifying file history
1702     * but not the current data.
1703     */
1704    public function purgeDescription() {
1705        $title = $this->getTitle();
1706        if ( $title ) {
1707            $title->invalidateCache();
1708            $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1709            $hcu->purgeTitleUrls( $title, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
1710        }
1711    }
1712
1713    /**
1714     * Purge metadata and all affected pages when the file is created,
1715     * deleted, or majorly updated.
1716     */
1717    public function purgeEverything() {
1718        // Delete thumbnails and refresh file metadata cache
1719        $this->purgeCache();
1720        $this->purgeDescription();
1721        // Purge cache of all pages using this file
1722        $title = $this->getTitle();
1723        if ( $title ) {
1724            $job = HTMLCacheUpdateJob::newForBacklinks(
1725                $title,
1726                'imagelinks',
1727                [ 'causeAction' => 'file-purge' ]
1728            );
1729            MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $job );
1730        }
1731    }
1732
1733    /**
1734     * Return a fragment of the history of file.
1735     *
1736     * STUB
1737     * @stable to override
1738     * @param int|null $limit Limit of rows to return
1739     * @param string|int|null $start Only revisions older than $start will be returned
1740     * @param string|int|null $end Only revisions newer than $end will be returned
1741     * @param bool $inc Include the endpoints of the time range
1742     *
1743     * @return File[] Guaranteed to be in descending order
1744     */
1745    public function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
1746        return [];
1747    }
1748
1749    /**
1750     * Return the history of this file, line by line. Starts with current version,
1751     * then old versions. Should return an object similar to an image/oldimage
1752     * database row.
1753     *
1754     * STUB
1755     * @stable to override
1756     * Overridden in LocalFile
1757     * @return bool
1758     */
1759    public function nextHistoryLine() {
1760        return false;
1761    }
1762
1763    /**
1764     * Reset the history pointer to the first element of the history.
1765     * Always call this function after using nextHistoryLine() to free db resources
1766     * STUB
1767     * Overridden in LocalFile.
1768     * @stable to override
1769     */
1770    public function resetHistory() {
1771    }
1772
1773    /**
1774     * Get the filename hash component of the directory including trailing slash,
1775     * e.g. f/fa/
1776     * If the repository is not hashed, returns an empty string.
1777     *
1778     * @return string
1779     */
1780    public function getHashPath() {
1781        if ( $this->hashPath === null ) {
1782            $this->assertRepoDefined();
1783            $this->hashPath = $this->repo->getHashPath( $this->getName() );
1784        }
1785
1786        return $this->hashPath;
1787    }
1788
1789    /**
1790     * Get the path of the file relative to the public zone root.
1791     * This function is overridden in OldLocalFile to be like getArchiveRel().
1792     *
1793     * @stable to override
1794     * @return string
1795     */
1796    public function getRel() {
1797        return $this->getHashPath() . $this->getName();
1798    }
1799
1800    /**
1801     * Get the path of an archived file relative to the public zone root
1802     * @stable to override
1803     *
1804     * @param string|false $suffix If not false, the name of an archived thumbnail file
1805     *
1806     * @return string
1807     */
1808    public function getArchiveRel( $suffix = false ) {
1809        $path = 'archive/' . $this->getHashPath();
1810        if ( $suffix === false ) {
1811            $path = rtrim( $path, '/' );
1812        } else {
1813            $path .= $suffix;
1814        }
1815
1816        return $path;
1817    }
1818
1819    /**
1820     * Get the path, relative to the thumbnail zone root, of the
1821     * thumbnail directory or a particular file if $suffix is specified
1822     * @stable to override
1823     *
1824     * @param string|false $suffix If not false, the name of a thumbnail file
1825     * @return string
1826     */
1827    public function getThumbRel( $suffix = false ) {
1828        $path = $this->getRel();
1829        if ( $suffix !== false ) {
1830            $path .= '/' . $suffix;
1831        }
1832
1833        return $path;
1834    }
1835
1836    /**
1837     * Get urlencoded path of the file relative to the public zone root.
1838     * This function is overridden in OldLocalFile to be like getArchiveUrl().
1839     * @stable to override
1840     *
1841     * @return string
1842     */
1843    public function getUrlRel() {
1844        return $this->getHashPath() . rawurlencode( $this->getName() );
1845    }
1846
1847    /**
1848     * Get the path, relative to the thumbnail zone root, for an archived file's thumbs directory
1849     * or a specific thumb if the $suffix is given.
1850     *
1851     * @param string $archiveName The timestamped name of an archived image
1852     * @param string|false $suffix If not false, the name of a thumbnail file
1853     * @return string
1854     */
1855    private function getArchiveThumbRel( $archiveName, $suffix = false ) {
1856        $path = $this->getArchiveRel( $archiveName );
1857        if ( $suffix !== false ) {
1858            $path .= '/' . $suffix;
1859        }
1860
1861        return $path;
1862    }
1863
1864    /**
1865     * Get the path of the archived file.
1866     *
1867     * @param string|false $suffix If not false, the name of an archived file.
1868     * @return string
1869     */
1870    public function getArchivePath( $suffix = false ) {
1871        $this->assertRepoDefined();
1872
1873        return $this->repo->getZonePath( 'public' ) . '/' . $this->getArchiveRel( $suffix );
1874    }
1875
1876    /**
1877     * Get the path of an archived file's thumbs, or a particular thumb if $suffix is specified
1878     *
1879     * @param string $archiveName The timestamped name of an archived image
1880     * @param string|false $suffix If not false, the name of a thumbnail file
1881     * @return string
1882     */
1883    public function getArchiveThumbPath( $archiveName, $suffix = false ) {
1884        $this->assertRepoDefined();
1885
1886        return $this->repo->getZonePath( 'thumb' ) . '/' .
1887        $this->getArchiveThumbRel( $archiveName, $suffix );
1888    }
1889
1890    /**
1891     * Get the path of the thumbnail directory, or a particular file if $suffix is specified
1892     * @stable to override
1893     *
1894     * @param string|false $suffix If not false, the name of a thumbnail file
1895     * @return string
1896     */
1897    public function getThumbPath( $suffix = false ) {
1898        $this->assertRepoDefined();
1899
1900        return $this->repo->getZonePath( 'thumb' ) . '/' . $this->getThumbRel( $suffix );
1901    }
1902
1903    /**
1904     * Get the path of the transcoded directory, or a particular file if $suffix is specified
1905     *
1906     * @param string|false $suffix If not false, the name of a media file
1907     * @return string
1908     */
1909    public function getTranscodedPath( $suffix = false ) {
1910        $this->assertRepoDefined();
1911
1912        return $this->repo->getZonePath( 'transcoded' ) . '/' . $this->getThumbRel( $suffix );
1913    }
1914
1915    /**
1916     * Get the URL of the archive directory, or a particular file if $suffix is specified
1917     * @stable to override
1918     *
1919     * @param string|false $suffix If not false, the name of an archived file
1920     * @return string
1921     */
1922    public function getArchiveUrl( $suffix = false ) {
1923        $this->assertRepoDefined();
1924        $ext = $this->getExtension();
1925        $path = $this->repo->getZoneUrl( 'public', $ext ) . '/archive/' . $this->getHashPath();
1926        if ( $suffix === false ) {
1927            $path = rtrim( $path, '/' );
1928        } else {
1929            $path .= rawurlencode( $suffix );
1930        }
1931
1932        return $path;
1933    }
1934
1935    /**
1936     * Get the URL of the archived file's thumbs, or a particular thumb if $suffix is specified
1937     * @stable to override
1938     *
1939     * @param string $archiveName The timestamped name of an archived image
1940     * @param string|false $suffix If not false, the name of a thumbnail file
1941     * @return string
1942     */
1943    public function getArchiveThumbUrl( $archiveName, $suffix = false ) {
1944        $this->assertRepoDefined();
1945        $ext = $this->getExtension();
1946        $path = $this->repo->getZoneUrl( 'thumb', $ext ) . '/archive/' .
1947            $this->getHashPath() . rawurlencode( $archiveName );
1948        if ( $suffix !== false ) {
1949            $path .= '/' . rawurlencode( $suffix );
1950        }
1951
1952        return $path;
1953    }
1954
1955    /**
1956     * Get the URL of the zone directory, or a particular file if $suffix is specified
1957     *
1958     * @param string $zone Name of requested zone
1959     * @param string|false $suffix If not false, the name of a file in zone
1960     * @return string Path
1961     */
1962    private function getZoneUrl( $zone, $suffix = false ) {
1963        $this->assertRepoDefined();
1964        $ext = $this->getExtension();
1965        $path = $this->repo->getZoneUrl( $zone, $ext ) . '/' . $this->getUrlRel();
1966        if ( $suffix !== false ) {
1967            $path .= '/' . rawurlencode( $suffix );
1968        }
1969
1970        return $path;
1971    }
1972
1973    /**
1974     * Get the URL of the thumbnail directory, or a particular file if $suffix is specified
1975     * @stable to override
1976     *
1977     * @param string|false $suffix If not false, the name of a thumbnail file
1978     * @return string Path
1979     */
1980    public function getThumbUrl( $suffix = false ) {
1981        return $this->getZoneUrl( 'thumb', $suffix );
1982    }
1983
1984    /**
1985     * Append a version parameter to the end of a file URL
1986     * Only to be used on File pages.
1987     * @internal
1988     *
1989     * @param string $url Unversioned URL
1990     * @return string
1991     */
1992    public function getFilePageThumbUrl( $url ) {
1993        if ( $this->repo->isLocal() ) {
1994            return wfAppendQuery( $url, urlencode( $this->getTimestamp() ) );
1995        } else {
1996            return $url;
1997        }
1998    }
1999
2000    /**
2001     * Get the URL of the transcoded directory, or a particular file if $suffix is specified
2002     *
2003     * @param string|false $suffix If not false, the name of a media file
2004     * @return string Path
2005     */
2006    public function getTranscodedUrl( $suffix = false ) {
2007        return $this->getZoneUrl( 'transcoded', $suffix );
2008    }
2009
2010    /**
2011     * Get the public zone virtual URL for a current version source file
2012     * @stable to override
2013     *
2014     * @param string|false $suffix If not false, the name of a thumbnail file
2015     * @return string
2016     */
2017    public function getVirtualUrl( $suffix = false ) {
2018        $this->assertRepoDefined();
2019        $path = $this->repo->getVirtualUrl() . '/public/' . $this->getUrlRel();
2020        if ( $suffix !== false ) {
2021            $path .= '/' . rawurlencode( $suffix );
2022        }
2023
2024        return $path;
2025    }
2026
2027    /**
2028     * Get the public zone virtual URL for an archived version source file
2029     * @stable to override
2030     *
2031     * @param string|false $suffix If not false, the name of a thumbnail file
2032     * @return string
2033     */
2034    public function getArchiveVirtualUrl( $suffix = false ) {
2035        $this->assertRepoDefined();
2036        $path = $this->repo->getVirtualUrl() . '/public/archive/' . $this->getHashPath();
2037        if ( $suffix === false ) {
2038            $path = rtrim( $path, '/' );
2039        } else {
2040            $path .= rawurlencode( $suffix );
2041        }
2042
2043        return $path;
2044    }
2045
2046    /**
2047     * Get the virtual URL for a thumbnail file or directory
2048     * @stable to override
2049     *
2050     * @param string|false $suffix If not false, the name of a thumbnail file
2051     * @return string
2052     */
2053    public function getThumbVirtualUrl( $suffix = false ) {
2054        $this->assertRepoDefined();
2055        $path = $this->repo->getVirtualUrl() . '/thumb/' . $this->getUrlRel();
2056        if ( $suffix !== false ) {
2057            $path .= '/' . rawurlencode( $suffix );
2058        }
2059
2060        return $path;
2061    }
2062
2063    /**
2064     * @return bool
2065     */
2066    protected function isHashed() {
2067        $this->assertRepoDefined();
2068
2069        return (bool)$this->repo->getHashLevels();
2070    }
2071
2072    /**
2073     * @return never
2074     */
2075    protected function readOnlyError() {
2076        throw new LogicException( static::class . ': write operations are not supported' );
2077    }
2078
2079    /**
2080     * Move or copy a file to its public location. If a file exists at the
2081     * destination, move it to an archive. Returns a Status object with
2082     * the archive name in the "value" member on success.
2083     *
2084     * The archive name should be passed through to recordUpload3 for database
2085     * registration.
2086     *
2087     * Options to $options include:
2088     *   - headers : name/value map of HTTP headers to use in response to GET/HEAD requests
2089     *
2090     * @param string|FSFile $src Local filesystem path to the source image
2091     * @param int $flags A bitwise combination of:
2092     *   File::DELETE_SOURCE    Delete the source file, i.e. move rather than copy
2093     * @param array $options Optional additional parameters
2094     * @return Status On success, the value member contains the
2095     *   archive name, or an empty string if it was a new file.
2096     *
2097     * STUB
2098     * Overridden by LocalFile
2099     * @stable to override
2100     */
2101    public function publish( $src, $flags = 0, array $options = [] ) {
2102        $this->readOnlyError();
2103    }
2104
2105    /**
2106     * @param IContextSource|false $context
2107     * @return array[]|false
2108     */
2109    public function formatMetadata( $context = false ) {
2110        $handler = $this->getHandler();
2111        return $handler ? $handler->formatMetadata( $this, $context ) : false;
2112    }
2113
2114    /**
2115     * Returns true if the file comes from the local file repository.
2116     *
2117     * @return bool
2118     */
2119    public function isLocal() {
2120        return $this->repo && $this->repo->isLocal();
2121    }
2122
2123    /**
2124     * Returns the name of the repository.
2125     *
2126     * @return string
2127     */
2128    public function getRepoName() {
2129        return $this->repo ? $this->repo->getName() : 'unknown';
2130    }
2131
2132    /**
2133     * Returns the repository
2134     * @stable to override
2135     *
2136     * @return FileRepo|false
2137     */
2138    public function getRepo() {
2139        return $this->repo;
2140    }
2141
2142    /**
2143     * Returns true if the image is an old version
2144     * STUB
2145     *
2146     * @stable to override
2147     * @return bool
2148     */
2149    public function isOld() {
2150        return false;
2151    }
2152
2153    /**
2154     * Is this file a "deleted" file in a private archive?
2155     * STUB
2156     *
2157     * @stable to override
2158     * @param int $field One of DELETED_* bitfield constants
2159     * @return bool
2160     */
2161    public function isDeleted( $field ) {
2162        return false;
2163    }
2164
2165    /**
2166     * Return the deletion bitfield
2167     * STUB
2168     * @stable to override
2169     * @return int
2170     */
2171    public function getVisibility() {
2172        return 0;
2173    }
2174
2175    /**
2176     * Was this file ever deleted from the wiki?
2177     *
2178     * @return bool
2179     */
2180    public function wasDeleted() {
2181        $title = $this->getTitle();
2182
2183        return $title && $title->hasDeletedEdits();
2184    }
2185
2186    /**
2187     * Move file to the new title
2188     *
2189     * Move current, old version and all thumbnails
2190     * to the new filename. Old file is deleted.
2191     *
2192     * Cache purging is done; checks for validity
2193     * and logging are caller's responsibility
2194     *
2195     * @stable to override
2196     * @param Title $target New file name
2197     * @return Status
2198     */
2199    public function move( $target ) {
2200        $this->readOnlyError();
2201    }
2202
2203    /**
2204     * Delete all versions of the file.
2205     *
2206     * @since 1.35
2207     *
2208     * Moves the files into an archive directory (or deletes them)
2209     * and removes the database rows.
2210     *
2211     * Cache purging is done; logging is caller's responsibility.
2212     *
2213     * @param string $reason
2214     * @param UserIdentity $user
2215     * @param bool $suppress Hide content from sysops?
2216     * @return Status
2217     * STUB
2218     * Overridden by LocalFile
2219     * @stable to override
2220     */
2221    public function deleteFile( $reason, UserIdentity $user, $suppress = false ) {
2222        $this->readOnlyError();
2223    }
2224
2225    /**
2226     * Restore all or specified deleted revisions to the given file.
2227     * Permissions and logging are left to the caller.
2228     *
2229     * May throw database exceptions on error.
2230     *
2231     * @param int[] $versions Set of record ids of deleted items to restore,
2232     *   or empty to restore all revisions.
2233     * @param bool $unsuppress Remove restrictions on content upon restoration?
2234     * @return Status
2235     * STUB
2236     * Overridden by LocalFile
2237     * @stable to override
2238     */
2239    public function restore( $versions = [], $unsuppress = false ) {
2240        $this->readOnlyError();
2241    }
2242
2243    /**
2244     * Returns 'true' if this file is a type which supports multiple pages,
2245     * e.g. DJVU or PDF. Note that this may be true even if the file in
2246     * question only has a single page.
2247     *
2248     * @stable to override
2249     * @return bool
2250     */
2251    public function isMultipage() {
2252        return $this->getHandler() && $this->handler->isMultiPage( $this );
2253    }
2254
2255    /**
2256     * Returns the number of pages of a multipage document, or false for
2257     * documents which aren't multipage documents
2258     *
2259     * @stable to override
2260     * @return int|false
2261     */
2262    public function pageCount() {
2263        if ( $this->pageCount === null ) {
2264            if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) {
2265                $this->pageCount = $this->handler->pageCount( $this );
2266            } else {
2267                $this->pageCount = false;
2268            }
2269        }
2270
2271        return $this->pageCount;
2272    }
2273
2274    /**
2275     * Calculate the height of a thumbnail using the source and destination width
2276     *
2277     * @param int $srcWidth
2278     * @param int $srcHeight
2279     * @param int $dstWidth
2280     *
2281     * @return int
2282     */
2283    public static function scaleHeight( $srcWidth, $srcHeight, $dstWidth ) {
2284        // Exact integer multiply followed by division
2285        if ( $srcWidth == 0 ) {
2286            return 0;
2287        } else {
2288            return (int)round( $srcHeight * $dstWidth / $srcWidth );
2289        }
2290    }
2291
2292    /**
2293     * Get the URL of the image description page. May return false if it is
2294     * unknown or not applicable.
2295     *
2296     * @stable to override
2297     * @return string|false
2298     */
2299    public function getDescriptionUrl() {
2300        if ( $this->repo ) {
2301            return $this->repo->getDescriptionUrl( $this->getName() );
2302        } else {
2303            return false;
2304        }
2305    }
2306
2307    /**
2308     * Get the HTML text of the description page, if available
2309     * @stable to override
2310     *
2311     * @param Language|null $lang Optional language to fetch description in
2312     * @return string|false HTML
2313     * @return-taint escaped
2314     */
2315    public function getDescriptionText( ?Language $lang = null ) {
2316        global $wgLang;
2317
2318        if ( !$this->repo || !$this->repo->fetchDescription ) {
2319            return false;
2320        }
2321
2322        $lang ??= $wgLang;
2323
2324        $renderUrl = $this->repo->getDescriptionRenderUrl( $this->getName(), $lang->getCode() );
2325        if ( $renderUrl ) {
2326            $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
2327            $key = $this->repo->getLocalCacheKey(
2328                'file-remote-description',
2329                $lang->getCode(),
2330                md5( $this->getName() )
2331            );
2332            $fname = __METHOD__;
2333
2334            return $cache->getWithSetCallback(
2335                $key,
2336                $this->repo->descriptionCacheExpiry ?: $cache::TTL_UNCACHEABLE,
2337                static function ( $oldValue, &$ttl, array &$setOpts ) use ( $renderUrl, $fname ) {
2338                    wfDebug( "Fetching shared description from $renderUrl" );
2339                    $res = MediaWikiServices::getInstance()->getHttpRequestFactory()->
2340                        get( $renderUrl, [], $fname );
2341                    if ( !$res ) {
2342                        $ttl = WANObjectCache::TTL_UNCACHEABLE;
2343                    }
2344
2345                    return $res;
2346                }
2347            );
2348        }
2349
2350        return false;
2351    }
2352
2353    /**
2354     * Get the identity of the file uploader.
2355     *
2356     * @note if the file does not exist, this will return null regardless of the permissions.
2357     *
2358     * @stable to override
2359     * @since 1.37
2360     * @param int $audience One of:
2361     *   File::FOR_PUBLIC       to be displayed to all users
2362     *   File::FOR_THIS_USER    to be displayed to the given user
2363     *   File::RAW              get the description regardless of permissions
2364     * @param Authority|null $performer to check for, only if FOR_THIS_USER is
2365     *   passed to the $audience parameter
2366     * @return UserIdentity|null
2367     */
2368    public function getUploader( int $audience = self::FOR_PUBLIC, ?Authority $performer = null ): ?UserIdentity {
2369        return null;
2370    }
2371
2372    /**
2373     * Get description of file revision
2374     * STUB
2375     *
2376     * @stable to override
2377     * @param int $audience One of:
2378     *   File::FOR_PUBLIC       to be displayed to all users
2379     *   File::FOR_THIS_USER    to be displayed to the given user
2380     *   File::RAW              get the description regardless of permissions
2381     * @param Authority|null $performer to check for, only if FOR_THIS_USER is
2382     *   passed to the $audience parameter
2383     * @return null|string
2384     */
2385    public function getDescription( $audience = self::FOR_PUBLIC, ?Authority $performer = null ) {
2386        return null;
2387    }
2388
2389    /**
2390     * Get the 14-character timestamp of the file upload
2391     *
2392     * @stable to override
2393     * @return string|false TS_MW timestamp or false on failure
2394     */
2395    public function getTimestamp() {
2396        $this->assertRepoDefined();
2397
2398        return $this->repo->getFileTimestamp( $this->getPath() );
2399    }
2400
2401    /**
2402     * Returns the timestamp (in TS_MW format) of the last change of the description page.
2403     * Returns false if the file does not have a description page, or retrieving the timestamp
2404     * would be expensive.
2405     * @since 1.25
2406     * @stable to override
2407     * @return string|false
2408     */
2409    public function getDescriptionTouched() {
2410        return false;
2411    }
2412
2413    /**
2414     * Get the SHA-1 base 36 hash of the file
2415     *
2416     * @stable to override
2417     * @return string|false
2418     */
2419    public function getSha1() {
2420        $this->assertRepoDefined();
2421
2422        return $this->repo->getFileSha1( $this->getPath() );
2423    }
2424
2425    /**
2426     * Get the deletion archive key, "<sha1>.<ext>"
2427     *
2428     * @return string|false
2429     */
2430    public function getStorageKey() {
2431        $hash = $this->getSha1();
2432        if ( !$hash ) {
2433            return false;
2434        }
2435        $ext = $this->getExtension();
2436        $dotExt = $ext === '' ? '' : ".$ext";
2437
2438        return $hash . $dotExt;
2439    }
2440
2441    /**
2442     * Determine if the current user is allowed to view a particular
2443     * field of this file, if it's marked as deleted.
2444     * STUB
2445     * @stable to override
2446     * @param int $field
2447     * @param Authority $performer user object to check
2448     * @return bool
2449     */
2450    public function userCan( $field, Authority $performer ) {
2451        return true;
2452    }
2453
2454    /**
2455     * @return string[] HTTP header name/value map to use for HEAD/GET request responses
2456     * @since 1.30
2457     */
2458    public function getContentHeaders() {
2459        $handler = $this->getHandler();
2460        if ( $handler ) {
2461            return $handler->getContentHeaders( $this->getMetadataArray() );
2462        }
2463
2464        return [];
2465    }
2466
2467    /**
2468     * @return string HTML
2469     */
2470    public function getLongDesc() {
2471        $handler = $this->getHandler();
2472        if ( $handler ) {
2473            return $handler->getLongDesc( $this );
2474        } else {
2475            return MediaHandler::getGeneralLongDesc( $this );
2476        }
2477    }
2478
2479    /**
2480     * @return string HTML
2481     */
2482    public function getShortDesc() {
2483        $handler = $this->getHandler();
2484        if ( $handler ) {
2485            return $handler->getShortDesc( $this );
2486        } else {
2487            return MediaHandler::getGeneralShortDesc( $this );
2488        }
2489    }
2490
2491    /**
2492     * @return string plain text
2493     */
2494    public function getDimensionsString() {
2495        $handler = $this->getHandler();
2496        if ( $handler ) {
2497            return $handler->getDimensionsString( $this );
2498        } else {
2499            return '';
2500        }
2501    }
2502
2503    /**
2504     * @return ?string The name that was used to access the file, before
2505     *         resolving redirects.
2506     */
2507    public function getRedirected(): ?string {
2508        return $this->redirected;
2509    }
2510
2511    /**
2512     * @return Title|null
2513     */
2514    protected function getRedirectedTitle() {
2515        if ( $this->redirected !== null ) {
2516            if ( !$this->redirectTitle ) {
2517                $this->redirectTitle = Title::makeTitle( NS_FILE, $this->redirected );
2518            }
2519
2520            return $this->redirectTitle;
2521        }
2522
2523        return null;
2524    }
2525
2526    /**
2527     * @param string $from The name that was used to access the file, before
2528     *        resolving redirects.
2529     */
2530    public function redirectedFrom( string $from ) {
2531        $this->redirected = $from;
2532    }
2533
2534    /**
2535     * @stable to override
2536     * @return bool
2537     */
2538    public function isMissing() {
2539        return false;
2540    }
2541
2542    /**
2543     * Check if this file object is small and can be cached
2544     * @stable to override
2545     * @return bool
2546     */
2547    public function isCacheable() {
2548        return true;
2549    }
2550
2551    /**
2552     * Assert that $this->repo is set to a valid FileRepo instance
2553     */
2554    protected function assertRepoDefined() {
2555        if ( !( $this->repo instanceof $this->repoClass ) ) {
2556            throw new LogicException( "{$this->repoClass} object is not set for this File.\n" );
2557        }
2558    }
2559
2560    /**
2561     * Assert that $this->title is set to a Title
2562     */
2563    protected function assertTitleDefined() {
2564        if ( !( $this->title instanceof Title ) ) {
2565            throw new LogicException( "A Title object is not set for this File.\n" );
2566        }
2567    }
2568
2569    /**
2570     * True if creating thumbnails from the file is large or otherwise resource-intensive.
2571     * @return bool
2572     */
2573    public function isExpensiveToThumbnail() {
2574        $handler = $this->getHandler();
2575        return $handler && $handler->isExpensiveToThumbnail( $this );
2576    }
2577
2578    /**
2579     * Whether the thumbnails created on the same server as this code is running.
2580     * @since 1.25
2581     * @stable to override
2582     * @return bool
2583     */
2584    public function isTransformedLocally() {
2585        return true;
2586    }
2587}
2588
2589/** @deprecated class alias since 1.44 */
2590class_alias( File::class, 'File' );