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