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