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