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