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