Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
26.49% |
235 / 887 |
|
0.00% |
0 / 72 |
CRAP | |
0.00% |
0 / 1 |
UploadBase | |
26.49% |
235 / 887 |
|
0.00% |
0 / 72 |
44109.21 | |
0.00% |
0 / 1 |
getVerificationErrorCode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isEnabled | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
isAllowed | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
isThrottled | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
createFromRequest | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
56 | |||
isValidRequest | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDesiredDestName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSourceType | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
initializePathInfo | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
initializeFromRequest | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
setTempFile | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
30 | |||
fetchFile | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
canFetchFile | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isEmptyFile | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFileSize | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTempFileSha1Base36 | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
getRealPath | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
verifyUpload | |
50.00% |
9 / 18 |
|
0.00% |
0 / 1 |
8.12 | |||
validateName | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
verifyMimeType | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
verifyFile | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
210 | |||
verifyPartialFile | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
90 | |||
zipEntryCallback | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
verifyPermissions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
verifyTitlePermissions | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
checkWarnings | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
90 | |||
makeWarningsSerializable | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
unserializeWarnings | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
checkBadFileName | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
checkUnwantedFileExtensions | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
checkFileSize | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
checkLocalFileExists | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
42 | |||
checkLocalFileWasDeleted | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
checkAgainstExistingDupes | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
42 | |||
checkAgainstArchiveDupes | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
performUpload | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
42 | |||
postProcessUpload | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTitle | |
67.65% |
46 / 68 |
|
0.00% |
0 / 1 |
35.93 | |||
getLocalFile | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getStashFile | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
tryStashFile | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
runUploadStashFileHook | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
doStashFile | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
cleanupTempFile | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
getTempPath | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
splitExtensions | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
checkFileExtension | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
checkFileExtensionList | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
verifyExtension | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
72 | |||
detectScript | |
67.39% |
31 / 46 |
|
0.00% |
0 / 1 |
24.88 | |||
checkXMLEncodingMissmatch | |
55.56% |
15 / 27 |
|
0.00% |
0 / 1 |
27.84 | |||
detectScriptInSvg | |
88.24% |
15 / 17 |
|
0.00% |
0 / 1 |
5.04 | |||
checkSvgPICallback | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
checkSvgExternalDTD | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
checkSvgScriptCallback | |
90.84% |
119 / 131 |
|
0.00% |
0 / 1 |
45.49 | |||
checkCssFragment | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
42 | |||
splitXmlNamespace | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
stripXmlNamespace | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
detectVirus | |
0.00% |
0 / 47 |
|
0.00% |
0 / 1 |
240 | |||
checkOverwrite | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
userCanReUpload | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
getExistsWarning | |
0.00% |
0 / 53 |
|
0.00% |
0 / 1 |
132 | |||
isThumbName | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
getFilenamePrefixBlacklist | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
42 | |||
getImageInfo | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
convertVerifyErrorToStatus | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getMaxUploadSize | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getMaxPhpUploadSize | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
getSessionStatus | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
setSessionStatus | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
30 | |||
getUploadSessionKey | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getUploadSessionStore | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Base class for the backend of file upload. |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | * @ingroup Upload |
22 | */ |
23 | |
24 | use MediaWiki\Context\RequestContext; |
25 | use MediaWiki\HookContainer\HookRunner; |
26 | use MediaWiki\HookContainer\ProtectedHookAccessorTrait; |
27 | use MediaWiki\Logger\LoggerFactory; |
28 | use MediaWiki\MainConfigNames; |
29 | use MediaWiki\MediaWikiServices; |
30 | use MediaWiki\Message\Message; |
31 | use MediaWiki\Parser\Sanitizer; |
32 | use MediaWiki\Permissions\Authority; |
33 | use MediaWiki\Permissions\PermissionStatus; |
34 | use MediaWiki\Request\WebRequest; |
35 | use MediaWiki\Shell\Shell; |
36 | use MediaWiki\Status\Status; |
37 | use MediaWiki\Title\Title; |
38 | use MediaWiki\User\User; |
39 | use MediaWiki\User\UserIdentity; |
40 | use Wikimedia\AtEase\AtEase; |
41 | use Wikimedia\FileBackend\FileBackend; |
42 | use Wikimedia\ObjectCache\BagOStuff; |
43 | |
44 | /** |
45 | * @defgroup Upload Upload related |
46 | */ |
47 | |
48 | /** |
49 | * @ingroup Upload |
50 | * |
51 | * UploadBase and subclasses are the backend of MediaWiki's file uploads. |
52 | * The frontends are formed by ApiUpload and SpecialUpload. |
53 | * |
54 | * @stable to extend |
55 | * |
56 | * @author Brooke Vibber |
57 | * @author Bryan Tong Minh |
58 | * @author Michael Dale |
59 | */ |
60 | abstract class UploadBase { |
61 | use ProtectedHookAccessorTrait; |
62 | |
63 | /** @var string|null Local file system path to the file to upload (or a local copy) */ |
64 | protected $mTempPath; |
65 | /** @var TempFSFile|null Wrapper to handle deleting the temp file */ |
66 | protected $tempFileObj; |
67 | /** @var string|null */ |
68 | protected $mDesiredDestName; |
69 | /** @var string|null */ |
70 | protected $mDestName; |
71 | /** @var bool|null */ |
72 | protected $mRemoveTempFile; |
73 | /** @var string|null */ |
74 | protected $mSourceType; |
75 | /** @var Title|false|null */ |
76 | protected $mTitle = false; |
77 | /** @var int */ |
78 | protected $mTitleError = 0; |
79 | /** @var string|null */ |
80 | protected $mFilteredName; |
81 | /** @var string|null */ |
82 | protected $mFinalExtension; |
83 | /** @var LocalFile|null */ |
84 | protected $mLocalFile; |
85 | /** @var UploadStashFile|null */ |
86 | protected $mStashFile; |
87 | /** @var int|null */ |
88 | protected $mFileSize; |
89 | /** @var array|null */ |
90 | protected $mFileProps; |
91 | /** @var string[] */ |
92 | protected $mBlackListedExtensions; |
93 | /** @var bool|null */ |
94 | protected $mJavaDetected; |
95 | /** @var string|false */ |
96 | protected $mSVGNSError; |
97 | |
98 | private const SAFE_XML_ENCONDINGS = [ |
99 | 'UTF-8', |
100 | 'US-ASCII', |
101 | 'ISO-8859-1', |
102 | 'ISO-8859-2', |
103 | 'UTF-16', |
104 | 'UTF-32', |
105 | 'WINDOWS-1250', |
106 | 'WINDOWS-1251', |
107 | 'WINDOWS-1252', |
108 | 'WINDOWS-1253', |
109 | 'WINDOWS-1254', |
110 | 'WINDOWS-1255', |
111 | 'WINDOWS-1256', |
112 | 'WINDOWS-1257', |
113 | 'WINDOWS-1258', |
114 | ]; |
115 | |
116 | public const SUCCESS = 0; |
117 | public const OK = 0; |
118 | public const EMPTY_FILE = 3; |
119 | public const MIN_LENGTH_PARTNAME = 4; |
120 | public const ILLEGAL_FILENAME = 5; |
121 | public const OVERWRITE_EXISTING_FILE = 7; # Not used anymore; handled by verifyTitlePermissions() |
122 | public const FILETYPE_MISSING = 8; |
123 | public const FILETYPE_BADTYPE = 9; |
124 | public const VERIFICATION_ERROR = 10; |
125 | public const HOOK_ABORTED = 11; |
126 | public const FILE_TOO_LARGE = 12; |
127 | public const WINDOWS_NONASCII_FILENAME = 13; |
128 | public const FILENAME_TOO_LONG = 14; |
129 | |
130 | private const CODE_TO_STATUS = [ |
131 | self::EMPTY_FILE => 'empty-file', |
132 | self::FILE_TOO_LARGE => 'file-too-large', |
133 | self::FILETYPE_MISSING => 'filetype-missing', |
134 | self::FILETYPE_BADTYPE => 'filetype-banned', |
135 | self::MIN_LENGTH_PARTNAME => 'filename-tooshort', |
136 | self::ILLEGAL_FILENAME => 'illegal-filename', |
137 | self::OVERWRITE_EXISTING_FILE => 'overwrite', |
138 | self::VERIFICATION_ERROR => 'verification-error', |
139 | self::HOOK_ABORTED => 'hookaborted', |
140 | self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename', |
141 | self::FILENAME_TOO_LONG => 'filename-toolong', |
142 | ]; |
143 | |
144 | /** |
145 | * @param int $error |
146 | * @return string |
147 | */ |
148 | public function getVerificationErrorCode( $error ) { |
149 | return self::CODE_TO_STATUS[$error] ?? 'unknown-error'; |
150 | } |
151 | |
152 | /** |
153 | * Returns true if uploads are enabled. |
154 | * Can be override by subclasses. |
155 | * @stable to override |
156 | * @return bool |
157 | */ |
158 | public static function isEnabled() { |
159 | $enableUploads = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnableUploads ); |
160 | |
161 | return $enableUploads && wfIniGetBool( 'file_uploads' ); |
162 | } |
163 | |
164 | /** |
165 | * Returns true if the user can use this upload module or else a string |
166 | * identifying the missing permission. |
167 | * Can be overridden by subclasses. |
168 | * |
169 | * @param Authority $performer |
170 | * @return bool|string |
171 | */ |
172 | public static function isAllowed( Authority $performer ) { |
173 | foreach ( [ 'upload', 'edit' ] as $permission ) { |
174 | if ( !$performer->isAllowed( $permission ) ) { |
175 | return $permission; |
176 | } |
177 | } |
178 | |
179 | return true; |
180 | } |
181 | |
182 | /** |
183 | * Returns true if the user has surpassed the upload rate limit, false otherwise. |
184 | * |
185 | * @deprecated since 1.41, use verifyTitlePermissions() instead. |
186 | * Rate limit checks are now implicit in permission checks. |
187 | * |
188 | * @param User $user |
189 | * @return bool |
190 | */ |
191 | public static function isThrottled( $user ) { |
192 | wfDeprecated( __METHOD__, '1.41' ); |
193 | return $user->pingLimiter( 'upload' ); |
194 | } |
195 | |
196 | /** @var string[] Upload handlers. Should probably just be a configuration variable. */ |
197 | private static $uploadHandlers = [ 'Stash', 'File', 'Url' ]; |
198 | |
199 | /** |
200 | * Create a form of UploadBase depending on wpSourceType and initializes it. |
201 | * |
202 | * @param WebRequest &$request |
203 | * @param string|null $type |
204 | * @return null|self |
205 | */ |
206 | public static function createFromRequest( &$request, $type = null ) { |
207 | $type = $type ?: $request->getVal( 'wpSourceType', 'File' ); |
208 | |
209 | if ( !$type ) { |
210 | return null; |
211 | } |
212 | |
213 | // Get the upload class |
214 | $type = ucfirst( $type ); |
215 | |
216 | // Give hooks the chance to handle this request |
217 | /** @var self|null $className */ |
218 | $className = null; |
219 | ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) ) |
220 | // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args |
221 | ->onUploadCreateFromRequest( $type, $className ); |
222 | if ( $className === null ) { |
223 | $className = 'UploadFrom' . $type; |
224 | wfDebug( __METHOD__ . ": class name: $className" ); |
225 | if ( !in_array( $type, self::$uploadHandlers ) ) { |
226 | return null; |
227 | } |
228 | } |
229 | |
230 | if ( !$className::isEnabled() || !$className::isValidRequest( $request ) ) { |
231 | return null; |
232 | } |
233 | |
234 | /** @var self $handler */ |
235 | $handler = new $className; |
236 | |
237 | $handler->initializeFromRequest( $request ); |
238 | |
239 | return $handler; |
240 | } |
241 | |
242 | /** |
243 | * Check whether a request if valid for this handler. |
244 | * @param WebRequest $request |
245 | * @return bool |
246 | */ |
247 | public static function isValidRequest( $request ) { |
248 | return false; |
249 | } |
250 | |
251 | /** |
252 | * Get the desired destination name. |
253 | * @return string|null |
254 | */ |
255 | public function getDesiredDestName() { |
256 | return $this->mDesiredDestName; |
257 | } |
258 | |
259 | /** |
260 | * @stable to call |
261 | */ |
262 | public function __construct() { |
263 | } |
264 | |
265 | /** |
266 | * Returns the upload type. Should be overridden by child classes. |
267 | * |
268 | * @since 1.18 |
269 | * @stable to override |
270 | * @return string|null |
271 | */ |
272 | public function getSourceType() { |
273 | return null; |
274 | } |
275 | |
276 | /** |
277 | * @param string $name The desired destination name |
278 | * @param string|null $tempPath Callers should make sure this is not a storage path |
279 | * @param int|null $fileSize |
280 | * @param bool $removeTempFile (false) remove the temporary file? |
281 | */ |
282 | public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) { |
283 | $this->mDesiredDestName = $name; |
284 | if ( FileBackend::isStoragePath( $tempPath ) ) { |
285 | throw new InvalidArgumentException( __METHOD__ . " given storage path `$tempPath`." ); |
286 | } |
287 | |
288 | $this->setTempFile( $tempPath, $fileSize ); |
289 | $this->mRemoveTempFile = $removeTempFile; |
290 | } |
291 | |
292 | /** |
293 | * Initialize from a WebRequest. Override this in a subclass. |
294 | * |
295 | * @param WebRequest &$request |
296 | */ |
297 | abstract public function initializeFromRequest( &$request ); |
298 | |
299 | /** |
300 | * @param string|null $tempPath File system path to temporary file containing the upload |
301 | * @param int|null $fileSize |
302 | */ |
303 | protected function setTempFile( $tempPath, $fileSize = null ) { |
304 | $this->mTempPath = $tempPath ?? ''; |
305 | $this->mFileSize = $fileSize ?: null; |
306 | $this->mFileProps = null; |
307 | if ( strlen( $this->mTempPath ) && file_exists( $this->mTempPath ) ) { |
308 | $this->tempFileObj = new TempFSFile( $this->mTempPath ); |
309 | if ( !$fileSize ) { |
310 | $this->mFileSize = filesize( $this->mTempPath ); |
311 | } |
312 | } else { |
313 | $this->tempFileObj = null; |
314 | } |
315 | } |
316 | |
317 | /** |
318 | * Fetch the file. Usually a no-op. |
319 | * @stable to override |
320 | * @return Status |
321 | */ |
322 | public function fetchFile() { |
323 | return Status::newGood(); |
324 | } |
325 | |
326 | /** |
327 | * Perform checks to see if the file can be fetched. Usually a no-op. |
328 | * @stable to override |
329 | * @return Status |
330 | */ |
331 | public function canFetchFile() { |
332 | return Status::newGood(); |
333 | } |
334 | |
335 | /** |
336 | * Return true if the file is empty. |
337 | * @return bool |
338 | */ |
339 | public function isEmptyFile() { |
340 | return !$this->mFileSize; |
341 | } |
342 | |
343 | /** |
344 | * Return the file size. |
345 | * @return int |
346 | */ |
347 | public function getFileSize() { |
348 | return $this->mFileSize; |
349 | } |
350 | |
351 | /** |
352 | * Get the base 36 SHA1 of the file. |
353 | * @stable to override |
354 | * @return string|false |
355 | */ |
356 | public function getTempFileSha1Base36() { |
357 | // Use cached version if we already have it. |
358 | if ( $this->mFileProps && is_string( $this->mFileProps['sha1'] ) ) { |
359 | return $this->mFileProps['sha1']; |
360 | } |
361 | return FSFile::getSha1Base36FromPath( $this->mTempPath ); |
362 | } |
363 | |
364 | /** |
365 | * @param string $srcPath The source path |
366 | * @return string|false The real path if it was a virtual URL Returns false on failure |
367 | */ |
368 | public function getRealPath( $srcPath ) { |
369 | $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo(); |
370 | if ( FileRepo::isVirtualUrl( $srcPath ) ) { |
371 | /** @todo Just make uploads work with storage paths UploadFromStash |
372 | * loads files via virtual URLs. |
373 | */ |
374 | $tmpFile = $repo->getLocalCopy( $srcPath ); |
375 | if ( $tmpFile ) { |
376 | $tmpFile->bind( $this ); // keep alive with $this |
377 | } |
378 | $path = $tmpFile ? $tmpFile->getPath() : false; |
379 | } else { |
380 | $path = $srcPath; |
381 | } |
382 | |
383 | return $path; |
384 | } |
385 | |
386 | /** |
387 | * Verify whether the upload is sensible. |
388 | * |
389 | * Return a status array representing the outcome of the verification. |
390 | * Possible keys are: |
391 | * - 'status': set to self::OK in case of success, or to one of the error constants defined in |
392 | * this class in case of failure |
393 | * - 'max': set to the maximum allowed file size ($wgMaxUploadSize) if the upload is too large |
394 | * - 'details': set to error details if the file type is valid but contents are corrupt |
395 | * - 'filtered': set to the sanitized file name if the requested file name is invalid |
396 | * - 'finalExt': set to the file's file extension if it is not an allowed file extension |
397 | * - 'blacklistedExt': set to the list of disallowed file extensions if the current file extension |
398 | * is not allowed for uploads and the list is not empty |
399 | * |
400 | * @stable to override |
401 | * @return mixed[] array representing the result of the verification |
402 | */ |
403 | public function verifyUpload() { |
404 | /** |
405 | * If there was no filename or a zero size given, give up quick. |
406 | */ |
407 | if ( $this->isEmptyFile() ) { |
408 | return [ 'status' => self::EMPTY_FILE ]; |
409 | } |
410 | |
411 | /** |
412 | * Honor $wgMaxUploadSize |
413 | */ |
414 | $maxSize = self::getMaxUploadSize( $this->getSourceType() ); |
415 | if ( $this->mFileSize > $maxSize ) { |
416 | return [ |
417 | 'status' => self::FILE_TOO_LARGE, |
418 | 'max' => $maxSize, |
419 | ]; |
420 | } |
421 | |
422 | /** |
423 | * Look at the contents of the file; if we can recognize the |
424 | * type, but it's corrupt or data of the wrong type, we should |
425 | * probably not accept it. |
426 | */ |
427 | $verification = $this->verifyFile(); |
428 | if ( $verification !== true ) { |
429 | return [ |
430 | 'status' => self::VERIFICATION_ERROR, |
431 | 'details' => $verification |
432 | ]; |
433 | } |
434 | |
435 | /** |
436 | * Make sure this file can be created |
437 | */ |
438 | $result = $this->validateName(); |
439 | if ( $result !== true ) { |
440 | return $result; |
441 | } |
442 | |
443 | return [ 'status' => self::OK ]; |
444 | } |
445 | |
446 | /** |
447 | * Verify that the name is valid and, if necessary, that we can overwrite |
448 | * |
449 | * @return array|bool True if valid, otherwise an array with 'status' |
450 | * and other keys |
451 | */ |
452 | public function validateName() { |
453 | $nt = $this->getTitle(); |
454 | if ( $nt === null ) { |
455 | $result = [ 'status' => $this->mTitleError ]; |
456 | if ( $this->mTitleError === self::ILLEGAL_FILENAME ) { |
457 | $result['filtered'] = $this->mFilteredName; |
458 | } |
459 | if ( $this->mTitleError === self::FILETYPE_BADTYPE ) { |
460 | $result['finalExt'] = $this->mFinalExtension; |
461 | if ( count( $this->mBlackListedExtensions ) ) { |
462 | $result['blacklistedExt'] = $this->mBlackListedExtensions; |
463 | } |
464 | } |
465 | |
466 | return $result; |
467 | } |
468 | $this->mDestName = $this->getLocalFile()->getName(); |
469 | |
470 | return true; |
471 | } |
472 | |
473 | /** |
474 | * Verify the MIME type. |
475 | * |
476 | * @note Only checks that it is not an evil MIME. |
477 | * The "does it have the correct file extension given its MIME type?" check is in verifyFile. |
478 | * @param string $mime Representing the MIME |
479 | * @return array|bool True if the file is verified, an array otherwise |
480 | */ |
481 | protected function verifyMimeType( $mime ) { |
482 | $verifyMimeType = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::VerifyMimeType ); |
483 | if ( $verifyMimeType ) { |
484 | wfDebug( "mime: <$mime> extension: <{$this->mFinalExtension}>" ); |
485 | $mimeTypeExclusions = MediaWikiServices::getInstance()->getMainConfig() |
486 | ->get( MainConfigNames::MimeTypeExclusions ); |
487 | if ( self::checkFileExtension( $mime, $mimeTypeExclusions ) ) { |
488 | return [ 'filetype-badmime', $mime ]; |
489 | } |
490 | } |
491 | |
492 | return true; |
493 | } |
494 | |
495 | /** |
496 | * Verifies that it's ok to include the uploaded file |
497 | * |
498 | * @return array|true True of the file is verified, array otherwise. |
499 | */ |
500 | protected function verifyFile() { |
501 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
502 | $verifyMimeType = $config->get( MainConfigNames::VerifyMimeType ); |
503 | $disableUploadScriptChecks = $config->get( MainConfigNames::DisableUploadScriptChecks ); |
504 | $status = $this->verifyPartialFile(); |
505 | if ( $status !== true ) { |
506 | return $status; |
507 | } |
508 | |
509 | // Calculating props calculates the sha1 which is expensive. |
510 | // reuse props if we already have them |
511 | if ( !is_array( $this->mFileProps ) ) { |
512 | $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() ); |
513 | $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension ); |
514 | } |
515 | $mime = $this->mFileProps['mime']; |
516 | |
517 | if ( $verifyMimeType ) { |
518 | # XXX: Missing extension will be caught by validateName() via getTitle() |
519 | if ( (string)$this->mFinalExtension !== '' && |
520 | !self::verifyExtension( $mime, $this->mFinalExtension ) |
521 | ) { |
522 | return [ 'filetype-mime-mismatch', $this->mFinalExtension, $mime ]; |
523 | } |
524 | } |
525 | |
526 | # check for htmlish code and javascript |
527 | if ( !$disableUploadScriptChecks ) { |
528 | if ( $this->mFinalExtension === 'svg' || $mime === 'image/svg+xml' ) { |
529 | $svgStatus = $this->detectScriptInSvg( $this->mTempPath, false ); |
530 | if ( $svgStatus !== false ) { |
531 | return $svgStatus; |
532 | } |
533 | } |
534 | } |
535 | |
536 | $handler = MediaHandler::getHandler( $mime ); |
537 | if ( $handler ) { |
538 | $handlerStatus = $handler->verifyUpload( $this->mTempPath ); |
539 | if ( !$handlerStatus->isOK() ) { |
540 | $errors = $handlerStatus->getErrorsArray(); |
541 | |
542 | return reset( $errors ); |
543 | } |
544 | } |
545 | |
546 | $error = true; |
547 | $this->getHookRunner()->onUploadVerifyFile( $this, $mime, $error ); |
548 | if ( $error !== true ) { |
549 | if ( !is_array( $error ) ) { |
550 | $error = [ $error ]; |
551 | } |
552 | return $error; |
553 | } |
554 | |
555 | wfDebug( __METHOD__ . ": all clear; passing." ); |
556 | |
557 | return true; |
558 | } |
559 | |
560 | /** |
561 | * A verification routine suitable for partial files |
562 | * |
563 | * Runs the deny list checks, but not any checks that may |
564 | * assume the entire file is present. |
565 | * |
566 | * @return array|true True, if the file is valid, else an array with error message key. |
567 | * @phan-return non-empty-array|true |
568 | */ |
569 | protected function verifyPartialFile() { |
570 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
571 | $disableUploadScriptChecks = $config->get( MainConfigNames::DisableUploadScriptChecks ); |
572 | # getTitle() sets some internal parameters like $this->mFinalExtension |
573 | $this->getTitle(); |
574 | |
575 | // Calculating props calculates the sha1 which is expensive. |
576 | // reuse props if we already have them (e.g. During stashed upload) |
577 | if ( !is_array( $this->mFileProps ) ) { |
578 | $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() ); |
579 | $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension ); |
580 | } |
581 | |
582 | # check MIME type, if desired |
583 | $mime = $this->mFileProps['file-mime']; |
584 | $status = $this->verifyMimeType( $mime ); |
585 | if ( $status !== true ) { |
586 | return $status; |
587 | } |
588 | |
589 | # check for htmlish code and javascript |
590 | if ( !$disableUploadScriptChecks ) { |
591 | if ( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) { |
592 | return [ 'uploadscripted' ]; |
593 | } |
594 | if ( $this->mFinalExtension === 'svg' || $mime === 'image/svg+xml' ) { |
595 | $svgStatus = $this->detectScriptInSvg( $this->mTempPath, true ); |
596 | if ( $svgStatus !== false ) { |
597 | return $svgStatus; |
598 | } |
599 | } |
600 | } |
601 | |
602 | # Scan the uploaded file for viruses |
603 | $virus = self::detectVirus( $this->mTempPath ); |
604 | if ( $virus ) { |
605 | return [ 'uploadvirus', $virus ]; |
606 | } |
607 | |
608 | return true; |
609 | } |
610 | |
611 | /** |
612 | * Callback for ZipDirectoryReader to detect Java class files. |
613 | * |
614 | * @param array $entry |
615 | */ |
616 | public function zipEntryCallback( $entry ) { |
617 | $names = [ $entry['name'] ]; |
618 | |
619 | // If there is a null character, cut off the name at it, because JDK's |
620 | // ZIP_GetEntry() uses strcmp() if the name hashes match. If a file name |
621 | // were constructed which had ".class\0" followed by a string chosen to |
622 | // make the hash collide with the truncated name, that file could be |
623 | // returned in response to a request for the .class file. |
624 | $nullPos = strpos( $entry['name'], "\000" ); |
625 | if ( $nullPos !== false ) { |
626 | $names[] = substr( $entry['name'], 0, $nullPos ); |
627 | } |
628 | |
629 | // If there is a trailing slash in the file name, we have to strip it, |
630 | // because that's what ZIP_GetEntry() does. |
631 | if ( preg_grep( '!\.class/?$!', $names ) ) { |
632 | $this->mJavaDetected = true; |
633 | } |
634 | } |
635 | |
636 | /** |
637 | * Alias for verifyTitlePermissions. The function was originally |
638 | * 'verifyPermissions', but that suggests it's checking the user, when it's |
639 | * really checking the title + user combination. |
640 | * |
641 | * @param Authority $performer to verify the permissions against |
642 | * @return array|bool An array as returned by getPermissionErrors or true |
643 | * in case the user has proper permissions. |
644 | */ |
645 | public function verifyPermissions( Authority $performer ) { |
646 | return $this->verifyTitlePermissions( $performer ); |
647 | } |
648 | |
649 | /** |
650 | * Check whether the user can edit, upload and create the image. This |
651 | * checks only against the current title; if it returns errors, it may |
652 | * very well be that another title will not give errors. Therefore |
653 | * isAllowed() should be called as well for generic is-user-blocked or |
654 | * can-user-upload checking. |
655 | * |
656 | * @param Authority $performer to verify the permissions against |
657 | * @return array|bool An array as returned by getPermissionErrors or true |
658 | * in case the user has proper permissions. |
659 | */ |
660 | public function verifyTitlePermissions( Authority $performer ) { |
661 | /** |
662 | * If the image is protected, non-sysop users won't be able |
663 | * to modify it by uploading a new revision. |
664 | */ |
665 | $nt = $this->getTitle(); |
666 | if ( $nt === null ) { |
667 | return true; |
668 | } |
669 | |
670 | $status = PermissionStatus::newEmpty(); |
671 | $performer->authorizeWrite( 'edit', $nt, $status ); |
672 | $performer->authorizeWrite( 'upload', $nt, $status ); |
673 | if ( !$status->isGood() ) { |
674 | return $status->toLegacyErrorArray(); |
675 | } |
676 | |
677 | $overwriteError = $this->checkOverwrite( $performer ); |
678 | if ( $overwriteError !== true ) { |
679 | return [ $overwriteError ]; |
680 | } |
681 | |
682 | return true; |
683 | } |
684 | |
685 | /** |
686 | * Check for non fatal problems with the file. |
687 | * |
688 | * This should not assume that mTempPath is set. |
689 | * |
690 | * @param User|null $user Accepted since 1.35 |
691 | * |
692 | * @return mixed[] Array of warnings |
693 | */ |
694 | public function checkWarnings( $user = null ) { |
695 | if ( $user === null ) { |
696 | // TODO check uses and hard deprecate |
697 | $user = RequestContext::getMain()->getUser(); |
698 | } |
699 | |
700 | $warnings = []; |
701 | |
702 | $localFile = $this->getLocalFile(); |
703 | $localFile->load( IDBAccessObject::READ_LATEST ); |
704 | $filename = $localFile->getName(); |
705 | $hash = $this->getTempFileSha1Base36(); |
706 | |
707 | $badFileName = $this->checkBadFileName( $filename, $this->mDesiredDestName ); |
708 | if ( $badFileName !== null ) { |
709 | $warnings['badfilename'] = $badFileName; |
710 | } |
711 | |
712 | $unwantedFileExtensionDetails = $this->checkUnwantedFileExtensions( (string)$this->mFinalExtension ); |
713 | if ( $unwantedFileExtensionDetails !== null ) { |
714 | $warnings['filetype-unwanted-type'] = $unwantedFileExtensionDetails; |
715 | } |
716 | |
717 | $fileSizeWarnings = $this->checkFileSize( $this->mFileSize ); |
718 | if ( $fileSizeWarnings ) { |
719 | $warnings = array_merge( $warnings, $fileSizeWarnings ); |
720 | } |
721 | |
722 | $localFileExistsWarnings = $this->checkLocalFileExists( $localFile, $hash ); |
723 | if ( $localFileExistsWarnings ) { |
724 | $warnings = array_merge( $warnings, $localFileExistsWarnings ); |
725 | } |
726 | |
727 | if ( $this->checkLocalFileWasDeleted( $localFile ) ) { |
728 | $warnings['was-deleted'] = $filename; |
729 | } |
730 | |
731 | // If a file with the same name exists locally then the local file has already been tested |
732 | // for duplication of content |
733 | $ignoreLocalDupes = isset( $warnings['exists'] ); |
734 | $dupes = $this->checkAgainstExistingDupes( $hash, $ignoreLocalDupes ); |
735 | if ( $dupes ) { |
736 | $warnings['duplicate'] = $dupes; |
737 | } |
738 | |
739 | $archivedDupes = $this->checkAgainstArchiveDupes( $hash, $user ); |
740 | if ( $archivedDupes !== null ) { |
741 | $warnings['duplicate-archive'] = $archivedDupes; |
742 | } |
743 | |
744 | return $warnings; |
745 | } |
746 | |
747 | /** |
748 | * Convert the warnings array returned by checkWarnings() to something that |
749 | * can be serialized. File objects will be converted to an associative array |
750 | * with the following keys: |
751 | * |
752 | * - fileName: The name of the file |
753 | * - timestamp: The upload timestamp |
754 | * |
755 | * @param mixed[] $warnings |
756 | * @return mixed[] |
757 | */ |
758 | public static function makeWarningsSerializable( $warnings ) { |
759 | array_walk_recursive( $warnings, static function ( &$param, $key ) { |
760 | if ( $param instanceof File ) { |
761 | $param = [ |
762 | 'fileName' => $param->getName(), |
763 | 'timestamp' => $param->getTimestamp() |
764 | ]; |
765 | } elseif ( is_object( $param ) ) { |
766 | throw new InvalidArgumentException( |
767 | 'UploadBase::makeWarningsSerializable: ' . |
768 | 'Unexpected object of class ' . get_class( $param ) ); |
769 | } |
770 | } ); |
771 | return $warnings; |
772 | } |
773 | |
774 | /** |
775 | * Convert the serialized warnings array created by makeWarningsSerializable() |
776 | * back to the output of checkWarnings(). |
777 | * |
778 | * @param mixed[] $warnings |
779 | * @return mixed[] |
780 | */ |
781 | public static function unserializeWarnings( $warnings ) { |
782 | foreach ( $warnings as $key => $value ) { |
783 | if ( is_array( $value ) ) { |
784 | if ( isset( $value['fileName'] ) && isset( $value['timestamp'] ) ) { |
785 | $warnings[$key] = MediaWikiServices::getInstance()->getRepoGroup()->findFile( |
786 | $value['fileName'], |
787 | [ 'time' => $value['timestamp'] ] |
788 | ); |
789 | } else { |
790 | $warnings[$key] = self::unserializeWarnings( $value ); |
791 | } |
792 | } |
793 | } |
794 | return $warnings; |
795 | } |
796 | |
797 | /** |
798 | * Check whether the resulting filename is different from the desired one, |
799 | * but ignore things like ucfirst() and spaces/underscore things |
800 | * |
801 | * @param string $filename |
802 | * @param string $desiredFileName |
803 | * |
804 | * @return string|null String that was determined to be bad or null if the filename is okay |
805 | */ |
806 | private function checkBadFileName( $filename, $desiredFileName ) { |
807 | $comparableName = str_replace( ' ', '_', $desiredFileName ); |
808 | $comparableName = Title::capitalize( $comparableName, NS_FILE ); |
809 | |
810 | if ( $desiredFileName != $filename && $comparableName != $filename ) { |
811 | return $filename; |
812 | } |
813 | |
814 | return null; |
815 | } |
816 | |
817 | /** |
818 | * @param string $fileExtension The file extension to check |
819 | * |
820 | * @return array|null array with the following keys: |
821 | * 0 => string The final extension being used |
822 | * 1 => string[] The extensions that are allowed |
823 | * 2 => int The number of extensions that are allowed. |
824 | */ |
825 | private function checkUnwantedFileExtensions( $fileExtension ) { |
826 | $checkFileExtensions = MediaWikiServices::getInstance()->getMainConfig() |
827 | ->get( MainConfigNames::CheckFileExtensions ); |
828 | $fileExtensions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::FileExtensions ); |
829 | if ( $checkFileExtensions ) { |
830 | $extensions = array_unique( $fileExtensions ); |
831 | if ( !self::checkFileExtension( $fileExtension, $extensions ) ) { |
832 | return [ |
833 | $fileExtension, |
834 | Message::listParam( $extensions, 'comma' ), |
835 | count( $extensions ) |
836 | ]; |
837 | } |
838 | } |
839 | |
840 | return null; |
841 | } |
842 | |
843 | /** |
844 | * @param int $fileSize |
845 | * |
846 | * @return array warnings |
847 | */ |
848 | private function checkFileSize( $fileSize ) { |
849 | $uploadSizeWarning = MediaWikiServices::getInstance()->getMainConfig() |
850 | ->get( MainConfigNames::UploadSizeWarning ); |
851 | |
852 | $warnings = []; |
853 | |
854 | if ( $uploadSizeWarning && ( $fileSize > $uploadSizeWarning ) ) { |
855 | $warnings['large-file'] = [ |
856 | Message::sizeParam( $uploadSizeWarning ), |
857 | Message::sizeParam( $fileSize ), |
858 | ]; |
859 | } |
860 | |
861 | if ( $fileSize == 0 ) { |
862 | $warnings['empty-file'] = true; |
863 | } |
864 | |
865 | return $warnings; |
866 | } |
867 | |
868 | /** |
869 | * @param LocalFile $localFile |
870 | * @param string|false $hash sha1 hash of the file to check |
871 | * |
872 | * @return array warnings |
873 | */ |
874 | private function checkLocalFileExists( LocalFile $localFile, $hash ) { |
875 | $warnings = []; |
876 | |
877 | $exists = self::getExistsWarning( $localFile ); |
878 | if ( $exists !== false ) { |
879 | $warnings['exists'] = $exists; |
880 | |
881 | // check if file is an exact duplicate of current file version |
882 | if ( $hash !== false && $hash === $localFile->getSha1() ) { |
883 | $warnings['no-change'] = $localFile; |
884 | } |
885 | |
886 | // check if file is an exact duplicate of older versions of this file |
887 | $history = $localFile->getHistory(); |
888 | foreach ( $history as $oldFile ) { |
889 | if ( $hash === $oldFile->getSha1() ) { |
890 | $warnings['duplicate-version'][] = $oldFile; |
891 | } |
892 | } |
893 | } |
894 | |
895 | return $warnings; |
896 | } |
897 | |
898 | private function checkLocalFileWasDeleted( LocalFile $localFile ) { |
899 | return $localFile->wasDeleted() && !$localFile->exists(); |
900 | } |
901 | |
902 | /** |
903 | * @param string|false $hash sha1 hash of the file to check |
904 | * @param bool $ignoreLocalDupes True to ignore local duplicates |
905 | * |
906 | * @return File[] Duplicate files, if found. |
907 | */ |
908 | private function checkAgainstExistingDupes( $hash, $ignoreLocalDupes ) { |
909 | if ( $hash === false ) { |
910 | return []; |
911 | } |
912 | $dupes = MediaWikiServices::getInstance()->getRepoGroup()->findBySha1( $hash ); |
913 | $title = $this->getTitle(); |
914 | foreach ( $dupes as $key => $dupe ) { |
915 | if ( |
916 | ( $dupe instanceof LocalFile ) && |
917 | $ignoreLocalDupes && |
918 | $title->equals( $dupe->getTitle() ) |
919 | ) { |
920 | unset( $dupes[$key] ); |
921 | } |
922 | } |
923 | |
924 | return $dupes; |
925 | } |
926 | |
927 | /** |
928 | * @param string|false $hash sha1 hash of the file to check |
929 | * @param Authority $performer |
930 | * |
931 | * @return string|null Name of the dupe or empty string if discovered (depending on visibility) |
932 | * null if the check discovered no dupes. |
933 | */ |
934 | private function checkAgainstArchiveDupes( $hash, Authority $performer ) { |
935 | if ( $hash === false ) { |
936 | return null; |
937 | } |
938 | $archivedFile = new ArchivedFile( null, 0, '', $hash ); |
939 | if ( $archivedFile->getID() > 0 ) { |
940 | if ( $archivedFile->userCan( File::DELETED_FILE, $performer ) ) { |
941 | return $archivedFile->getName(); |
942 | } |
943 | return ''; |
944 | } |
945 | |
946 | return null; |
947 | } |
948 | |
949 | /** |
950 | * Really perform the upload. Stores the file in the local repo, watches |
951 | * if necessary and runs the UploadComplete hook. |
952 | * |
953 | * @param string $comment |
954 | * @param string|false $pageText |
955 | * @param bool $watch Whether the file page should be added to user's watchlist. |
956 | * (This doesn't check $user's permissions.) |
957 | * @param User $user |
958 | * @param string[] $tags Change tags to add to the log entry and page revision. |
959 | * (This doesn't check $user's permissions.) |
960 | * @param string|null $watchlistExpiry Optional watchlist expiry timestamp in any format |
961 | * acceptable to wfTimestamp(). |
962 | * @return Status Indicating the whether the upload succeeded. |
963 | * |
964 | * @since 1.35 Accepts $watchlistExpiry parameter. |
965 | */ |
966 | public function performUpload( |
967 | $comment, $pageText, $watch, $user, $tags = [], ?string $watchlistExpiry = null |
968 | ) { |
969 | $this->getLocalFile()->load( IDBAccessObject::READ_LATEST ); |
970 | $props = $this->mFileProps; |
971 | |
972 | $error = null; |
973 | $this->getHookRunner()->onUploadVerifyUpload( $this, $user, $props, $comment, $pageText, $error ); |
974 | if ( $error ) { |
975 | if ( !is_array( $error ) ) { |
976 | $error = [ $error ]; |
977 | } |
978 | return Status::newFatal( ...$error ); |
979 | } |
980 | |
981 | $status = $this->getLocalFile()->upload( |
982 | $this->mTempPath, |
983 | $comment, |
984 | $pageText !== false ? $pageText : '', |
985 | File::DELETE_SOURCE, |
986 | $props, |
987 | false, |
988 | $user, |
989 | $tags |
990 | ); |
991 | |
992 | if ( $status->isGood() ) { |
993 | if ( $watch ) { |
994 | MediaWikiServices::getInstance()->getWatchlistManager()->addWatchIgnoringRights( |
995 | $user, |
996 | $this->getLocalFile()->getTitle(), |
997 | $watchlistExpiry |
998 | ); |
999 | } |
1000 | $this->getHookRunner()->onUploadComplete( $this ); |
1001 | |
1002 | $this->postProcessUpload(); |
1003 | } |
1004 | |
1005 | return $status; |
1006 | } |
1007 | |
1008 | /** |
1009 | * Perform extra steps after a successful upload. |
1010 | * |
1011 | * @stable to override |
1012 | * @since 1.25 |
1013 | */ |
1014 | public function postProcessUpload() { |
1015 | } |
1016 | |
1017 | /** |
1018 | * Returns the title of the file to be uploaded. Sets mTitleError in case |
1019 | * the name was illegal. |
1020 | * |
1021 | * @return Title|null The title of the file or null in case the name was illegal |
1022 | */ |
1023 | public function getTitle() { |
1024 | if ( $this->mTitle !== false ) { |
1025 | return $this->mTitle; |
1026 | } |
1027 | if ( !is_string( $this->mDesiredDestName ) ) { |
1028 | $this->mTitleError = self::ILLEGAL_FILENAME; |
1029 | $this->mTitle = null; |
1030 | |
1031 | return $this->mTitle; |
1032 | } |
1033 | /* Assume that if a user specified File:Something.jpg, this is an error |
1034 | * and that the namespace prefix needs to be stripped of. |
1035 | */ |
1036 | $title = Title::newFromText( $this->mDesiredDestName ); |
1037 | if ( $title && $title->getNamespace() === NS_FILE ) { |
1038 | $this->mFilteredName = $title->getDBkey(); |
1039 | } else { |
1040 | $this->mFilteredName = $this->mDesiredDestName; |
1041 | } |
1042 | |
1043 | # oi_archive_name is max 255 bytes, which include a timestamp and an |
1044 | # exclamation mark, so restrict file name to 240 bytes. |
1045 | if ( strlen( $this->mFilteredName ) > 240 ) { |
1046 | $this->mTitleError = self::FILENAME_TOO_LONG; |
1047 | $this->mTitle = null; |
1048 | |
1049 | return $this->mTitle; |
1050 | } |
1051 | |
1052 | /** |
1053 | * Chop off any directories in the given filename. Then |
1054 | * filter out illegal characters, and try to make a legible name |
1055 | * out of it. We'll strip some silently that Title would die on. |
1056 | */ |
1057 | $this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName ); |
1058 | /* Normalize to title form before we do any further processing */ |
1059 | $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName ); |
1060 | if ( $nt === null ) { |
1061 | $this->mTitleError = self::ILLEGAL_FILENAME; |
1062 | $this->mTitle = null; |
1063 | |
1064 | return $this->mTitle; |
1065 | } |
1066 | $this->mFilteredName = $nt->getDBkey(); |
1067 | |
1068 | /** |
1069 | * We'll want to prevent against *any* 'extension', and use |
1070 | * only the final one for the allow list. |
1071 | */ |
1072 | [ $partname, $ext ] = self::splitExtensions( $this->mFilteredName ); |
1073 | |
1074 | if ( $ext !== [] ) { |
1075 | $this->mFinalExtension = trim( end( $ext ) ); |
1076 | } else { |
1077 | $this->mFinalExtension = ''; |
1078 | |
1079 | // No extension, try guessing one from the temporary file |
1080 | // FIXME: Sometimes we mTempPath isn't set yet here, possibly due to an unrealistic |
1081 | // or incomplete test case in UploadBaseTest (T272328) |
1082 | if ( $this->mTempPath !== null ) { |
1083 | $magic = MediaWikiServices::getInstance()->getMimeAnalyzer(); |
1084 | $mime = $magic->guessMimeType( $this->mTempPath ); |
1085 | if ( $mime !== 'unknown/unknown' ) { |
1086 | # Get a space separated list of extensions |
1087 | $mimeExt = $magic->getExtensionFromMimeTypeOrNull( $mime ); |
1088 | if ( $mimeExt !== null ) { |
1089 | # Set the extension to the canonical extension |
1090 | $this->mFinalExtension = $mimeExt; |
1091 | |
1092 | # Fix up the other variables |
1093 | $this->mFilteredName .= ".{$this->mFinalExtension}"; |
1094 | $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName ); |
1095 | $ext = [ $this->mFinalExtension ]; |
1096 | } |
1097 | } |
1098 | } |
1099 | } |
1100 | |
1101 | // Don't allow users to override the list of prohibited file extensions (check file extension) |
1102 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
1103 | $checkFileExtensions = $config->get( MainConfigNames::CheckFileExtensions ); |
1104 | $strictFileExtensions = $config->get( MainConfigNames::StrictFileExtensions ); |
1105 | $fileExtensions = $config->get( MainConfigNames::FileExtensions ); |
1106 | $prohibitedFileExtensions = $config->get( MainConfigNames::ProhibitedFileExtensions ); |
1107 | |
1108 | $badList = self::checkFileExtensionList( $ext, $prohibitedFileExtensions ); |
1109 | |
1110 | if ( $this->mFinalExtension == '' ) { |
1111 | $this->mTitleError = self::FILETYPE_MISSING; |
1112 | $this->mTitle = null; |
1113 | |
1114 | return $this->mTitle; |
1115 | } |
1116 | |
1117 | if ( $badList || |
1118 | ( $checkFileExtensions && $strictFileExtensions && |
1119 | !self::checkFileExtension( $this->mFinalExtension, $fileExtensions ) ) |
1120 | ) { |
1121 | $this->mBlackListedExtensions = $badList; |
1122 | $this->mTitleError = self::FILETYPE_BADTYPE; |
1123 | $this->mTitle = null; |
1124 | |
1125 | return $this->mTitle; |
1126 | } |
1127 | |
1128 | // Windows may be broken with special characters, see T3780 |
1129 | if ( !preg_match( '/^[\x0-\x7f]*$/', $nt->getText() ) |
1130 | && !MediaWikiServices::getInstance()->getRepoGroup() |
1131 | ->getLocalRepo()->backendSupportsUnicodePaths() |
1132 | ) { |
1133 | $this->mTitleError = self::WINDOWS_NONASCII_FILENAME; |
1134 | $this->mTitle = null; |
1135 | |
1136 | return $this->mTitle; |
1137 | } |
1138 | |
1139 | # If there was more than one file "extension", reassemble the base |
1140 | # filename to prevent bogus complaints about length |
1141 | if ( count( $ext ) > 1 ) { |
1142 | $iterations = count( $ext ) - 1; |
1143 | for ( $i = 0; $i < $iterations; $i++ ) { |
1144 | $partname .= '.' . $ext[$i]; |
1145 | } |
1146 | } |
1147 | |
1148 | if ( strlen( $partname ) < 1 ) { |
1149 | $this->mTitleError = self::MIN_LENGTH_PARTNAME; |
1150 | $this->mTitle = null; |
1151 | |
1152 | return $this->mTitle; |
1153 | } |
1154 | |
1155 | $this->mTitle = $nt; |
1156 | |
1157 | return $this->mTitle; |
1158 | } |
1159 | |
1160 | /** |
1161 | * Return the local file and initializes if necessary. |
1162 | * |
1163 | * @stable to override |
1164 | * @return LocalFile|null |
1165 | */ |
1166 | public function getLocalFile() { |
1167 | if ( $this->mLocalFile === null ) { |
1168 | $nt = $this->getTitle(); |
1169 | $this->mLocalFile = $nt === null |
1170 | ? null |
1171 | : MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $nt ); |
1172 | } |
1173 | |
1174 | return $this->mLocalFile; |
1175 | } |
1176 | |
1177 | /** |
1178 | * @return UploadStashFile|null |
1179 | */ |
1180 | public function getStashFile() { |
1181 | return $this->mStashFile; |
1182 | } |
1183 | |
1184 | /** |
1185 | * Like stashFile(), but respects extensions' wishes to prevent the stashing. verifyUpload() must |
1186 | * be called before calling this method (unless $isPartial is true). |
1187 | * |
1188 | * Upload stash exceptions are also caught and converted to an error status. |
1189 | * |
1190 | * @since 1.28 |
1191 | * @stable to override |
1192 | * @param User $user |
1193 | * @param bool $isPartial Pass `true` if this is a part of a chunked upload (not a complete file). |
1194 | * @return Status If successful, value is an UploadStashFile instance |
1195 | */ |
1196 | public function tryStashFile( User $user, $isPartial = false ) { |
1197 | if ( !$isPartial ) { |
1198 | $error = $this->runUploadStashFileHook( $user ); |
1199 | if ( $error ) { |
1200 | return Status::newFatal( ...$error ); |
1201 | } |
1202 | } |
1203 | try { |
1204 | $file = $this->doStashFile( $user ); |
1205 | return Status::newGood( $file ); |
1206 | } catch ( UploadStashException $e ) { |
1207 | return Status::newFatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() ); |
1208 | } |
1209 | } |
1210 | |
1211 | /** |
1212 | * @param User $user |
1213 | * @return array|null Error message and parameters, null if there's no error |
1214 | */ |
1215 | protected function runUploadStashFileHook( User $user ) { |
1216 | $props = $this->mFileProps; |
1217 | $error = null; |
1218 | $this->getHookRunner()->onUploadStashFile( $this, $user, $props, $error ); |
1219 | if ( $error && !is_array( $error ) ) { |
1220 | $error = [ $error ]; |
1221 | } |
1222 | return $error; |
1223 | } |
1224 | |
1225 | /** |
1226 | * Implementation for stashFile() and tryStashFile(). |
1227 | * |
1228 | * @stable to override |
1229 | * @param User|null $user |
1230 | * @return UploadStashFile Stashed file |
1231 | */ |
1232 | protected function doStashFile( User $user = null ) { |
1233 | $stash = MediaWikiServices::getInstance()->getRepoGroup() |
1234 | ->getLocalRepo()->getUploadStash( $user ); |
1235 | $file = $stash->stashFile( $this->mTempPath, $this->getSourceType(), $this->mFileProps ); |
1236 | $this->mStashFile = $file; |
1237 | |
1238 | return $file; |
1239 | } |
1240 | |
1241 | /** |
1242 | * If we've modified the upload file, then we need to manually remove it |
1243 | * on exit to clean up. |
1244 | */ |
1245 | public function cleanupTempFile() { |
1246 | if ( $this->mRemoveTempFile && $this->tempFileObj ) { |
1247 | // Delete when all relevant TempFSFile handles go out of scope |
1248 | wfDebug( __METHOD__ . ": Marked temporary file '{$this->mTempPath}' for removal" ); |
1249 | $this->tempFileObj->autocollect(); |
1250 | } |
1251 | } |
1252 | |
1253 | /** |
1254 | * @return string|null |
1255 | */ |
1256 | public function getTempPath() { |
1257 | return $this->mTempPath; |
1258 | } |
1259 | |
1260 | /** |
1261 | * Split a file into a base name and all dot-delimited 'extensions' |
1262 | * on the end. Some web server configurations will fall back to |
1263 | * earlier pseudo-'extensions' to determine type and execute |
1264 | * scripts, so we need to check them all. |
1265 | * |
1266 | * @param string $filename |
1267 | * @return array [ string, string[] ] |
1268 | */ |
1269 | public static function splitExtensions( $filename ) { |
1270 | $bits = explode( '.', $filename ); |
1271 | $basename = array_shift( $bits ); |
1272 | |
1273 | return [ $basename, $bits ]; |
1274 | } |
1275 | |
1276 | /** |
1277 | * Perform case-insensitive match against a list of file extensions. |
1278 | * |
1279 | * @param string $ext File extension |
1280 | * @param array $list |
1281 | * @return bool Returns true if the extension is in the list. |
1282 | */ |
1283 | public static function checkFileExtension( $ext, $list ) { |
1284 | return in_array( strtolower( $ext ?? '' ), $list, true ); |
1285 | } |
1286 | |
1287 | /** |
1288 | * Perform case-insensitive match against a list of file extensions. |
1289 | * Returns an array of matching extensions. |
1290 | * |
1291 | * @param string[] $ext File extensions |
1292 | * @param string[] $list |
1293 | * @return string[] |
1294 | */ |
1295 | public static function checkFileExtensionList( $ext, $list ) { |
1296 | return array_intersect( array_map( 'strtolower', $ext ), $list ); |
1297 | } |
1298 | |
1299 | /** |
1300 | * Checks if the MIME type of the uploaded file matches the file extension. |
1301 | * |
1302 | * @param string $mime The MIME type of the uploaded file |
1303 | * @param string $extension The filename extension that the file is to be served with |
1304 | * @return bool |
1305 | */ |
1306 | public static function verifyExtension( $mime, $extension ) { |
1307 | $magic = MediaWikiServices::getInstance()->getMimeAnalyzer(); |
1308 | |
1309 | if ( !$mime || $mime === 'unknown' || $mime === 'unknown/unknown' ) { |
1310 | if ( !$magic->isRecognizableExtension( $extension ) ) { |
1311 | wfDebug( __METHOD__ . ": passing file with unknown detected mime type; " . |
1312 | "unrecognized extension '$extension', can't verify" ); |
1313 | |
1314 | return true; |
1315 | } |
1316 | |
1317 | wfDebug( __METHOD__ . ": rejecting file with unknown detected mime type; " . |
1318 | "recognized extension '$extension', so probably invalid file" ); |
1319 | return false; |
1320 | } |
1321 | |
1322 | $match = $magic->isMatchingExtension( $extension, $mime ); |
1323 | |
1324 | if ( $match === null ) { |
1325 | if ( $magic->getMimeTypesFromExtension( $extension ) !== [] ) { |
1326 | wfDebug( __METHOD__ . ": No extension known for $mime, but we know a mime for $extension" ); |
1327 | |
1328 | return false; |
1329 | } |
1330 | |
1331 | wfDebug( __METHOD__ . ": no file extension known for mime type $mime, passing file" ); |
1332 | return true; |
1333 | } |
1334 | |
1335 | if ( $match ) { |
1336 | wfDebug( __METHOD__ . ": mime type $mime matches extension $extension, passing file" ); |
1337 | |
1338 | /** @todo If it's a bitmap, make sure PHP or ImageMagick resp. can handle it! */ |
1339 | return true; |
1340 | } |
1341 | |
1342 | wfDebug( __METHOD__ |
1343 | . ": mime type $mime mismatches file extension $extension, rejecting file" ); |
1344 | |
1345 | return false; |
1346 | } |
1347 | |
1348 | /** |
1349 | * Heuristic for detecting files that *could* contain JavaScript instructions or |
1350 | * things that may look like HTML to a browser and are thus |
1351 | * potentially harmful. The present implementation will produce false |
1352 | * positives in some situations. |
1353 | * |
1354 | * @param string|null $file Pathname to the temporary upload file |
1355 | * @param string $mime The MIME type of the file |
1356 | * @param string|null $extension The extension of the file |
1357 | * @return bool True if the file contains something looking like embedded scripts |
1358 | */ |
1359 | public static function detectScript( $file, $mime, $extension ) { |
1360 | # ugly hack: for text files, always look at the entire file. |
1361 | # For binary field, just check the first K. |
1362 | |
1363 | if ( str_starts_with( $mime ?? '', 'text/' ) ) { |
1364 | $chunk = file_get_contents( $file ); |
1365 | } else { |
1366 | $fp = fopen( $file, 'rb' ); |
1367 | if ( !$fp ) { |
1368 | return false; |
1369 | } |
1370 | $chunk = fread( $fp, 1024 ); |
1371 | fclose( $fp ); |
1372 | } |
1373 | |
1374 | $chunk = strtolower( $chunk ); |
1375 | |
1376 | if ( !$chunk ) { |
1377 | return false; |
1378 | } |
1379 | |
1380 | # decode from UTF-16 if needed (could be used for obfuscation). |
1381 | if ( str_starts_with( $chunk, "\xfe\xff" ) ) { |
1382 | $enc = 'UTF-16BE'; |
1383 | } elseif ( str_starts_with( $chunk, "\xff\xfe" ) ) { |
1384 | $enc = 'UTF-16LE'; |
1385 | } else { |
1386 | $enc = null; |
1387 | } |
1388 | |
1389 | if ( $enc !== null ) { |
1390 | $chunk = iconv( $enc, "ASCII//IGNORE", $chunk ); |
1391 | } |
1392 | |
1393 | $chunk = trim( $chunk ); |
1394 | |
1395 | /** @todo FIXME: Convert from UTF-16 if necessary! */ |
1396 | wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff" ); |
1397 | |
1398 | # check for HTML doctype |
1399 | if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) { |
1400 | return true; |
1401 | } |
1402 | |
1403 | // Some browsers will interpret obscure xml encodings as UTF-8, while |
1404 | // PHP/expat will interpret the given encoding in the xml declaration (T49304) |
1405 | if ( $extension === 'svg' || str_starts_with( $mime ?? '', 'image/svg' ) ) { |
1406 | if ( self::checkXMLEncodingMissmatch( $file ) ) { |
1407 | return true; |
1408 | } |
1409 | } |
1410 | |
1411 | // Quick check for HTML heuristics in old IE and Safari. |
1412 | // |
1413 | // The exact heuristics IE uses are checked separately via verifyMimeType(), so we |
1414 | // don't need them all here as it can cause many false positives. |
1415 | // |
1416 | // Check for `<script` and such still to forbid script tags and embedded HTML in SVG: |
1417 | $tags = [ |
1418 | '<body', |
1419 | '<head', |
1420 | '<html', # also in safari |
1421 | '<script', # also in safari |
1422 | ]; |
1423 | |
1424 | foreach ( $tags as $tag ) { |
1425 | if ( strpos( $chunk, $tag ) !== false ) { |
1426 | wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag" ); |
1427 | |
1428 | return true; |
1429 | } |
1430 | } |
1431 | |
1432 | /* |
1433 | * look for JavaScript |
1434 | */ |
1435 | |
1436 | # resolve entity-refs to look at attributes. may be harsh on big files... cache result? |
1437 | $chunk = Sanitizer::decodeCharReferences( $chunk ); |
1438 | |
1439 | # look for script-types |
1440 | if ( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!im', $chunk ) ) { |
1441 | wfDebug( __METHOD__ . ": found script types" ); |
1442 | |
1443 | return true; |
1444 | } |
1445 | |
1446 | # look for html-style script-urls |
1447 | if ( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!im', $chunk ) ) { |
1448 | wfDebug( __METHOD__ . ": found html-style script urls" ); |
1449 | |
1450 | return true; |
1451 | } |
1452 | |
1453 | # look for css-style script-urls |
1454 | if ( preg_match( '!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!im', $chunk ) ) { |
1455 | wfDebug( __METHOD__ . ": found css-style script urls" ); |
1456 | |
1457 | return true; |
1458 | } |
1459 | |
1460 | wfDebug( __METHOD__ . ": no scripts found" ); |
1461 | |
1462 | return false; |
1463 | } |
1464 | |
1465 | /** |
1466 | * Check an allowed list of xml encodings that are known not to be interpreted differently |
1467 | * by the server's xml parser (expat) and some common browsers. |
1468 | * |
1469 | * @param string $file Pathname to the temporary upload file |
1470 | * @return bool True if the file contains an encoding that could be misinterpreted |
1471 | */ |
1472 | public static function checkXMLEncodingMissmatch( $file ) { |
1473 | // https://mimesniff.spec.whatwg.org/#resource-header says browsers |
1474 | // should read the first 1445 bytes. Do 4096 bytes for good measure. |
1475 | // XML Spec says XML declaration if present must be first thing in file |
1476 | // other than BOM |
1477 | $contents = file_get_contents( $file, false, null, 0, 4096 ); |
1478 | $encodingRegex = '!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si'; |
1479 | |
1480 | if ( preg_match( "!<\?xml\b(.*?)\?>!si", $contents, $matches ) ) { |
1481 | if ( preg_match( $encodingRegex, $matches[1], $encMatch ) |
1482 | && !in_array( strtoupper( $encMatch[1] ), self::SAFE_XML_ENCONDINGS ) |
1483 | ) { |
1484 | wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" ); |
1485 | |
1486 | return true; |
1487 | } |
1488 | } elseif ( preg_match( "!<\?xml\b!i", $contents ) ) { |
1489 | // Start of XML declaration without an end in the first 4096 bytes |
1490 | // bytes. There shouldn't be a legitimate reason for this to happen. |
1491 | wfDebug( __METHOD__ . ": Unmatched XML declaration start" ); |
1492 | |
1493 | return true; |
1494 | } elseif ( str_starts_with( $contents, "\x4C\x6F\xA7\x94" ) ) { |
1495 | // EBCDIC encoded XML |
1496 | wfDebug( __METHOD__ . ": EBCDIC Encoded XML" ); |
1497 | |
1498 | return true; |
1499 | } |
1500 | |
1501 | // It's possible the file is encoded with multibyte encoding, so re-encode attempt to |
1502 | // detect the encoding in case it specifies an encoding not allowed in self::SAFE_XML_ENCONDINGS |
1503 | $attemptEncodings = [ 'UTF-16', 'UTF-16BE', 'UTF-32', 'UTF-32BE' ]; |
1504 | foreach ( $attemptEncodings as $encoding ) { |
1505 | AtEase::suppressWarnings(); |
1506 | $str = iconv( $encoding, 'UTF-8', $contents ); |
1507 | AtEase::restoreWarnings(); |
1508 | if ( $str != '' && preg_match( "!<\?xml\b(.*?)\?>!si", $str, $matches ) ) { |
1509 | if ( preg_match( $encodingRegex, $matches[1], $encMatch ) |
1510 | && !in_array( strtoupper( $encMatch[1] ), self::SAFE_XML_ENCONDINGS ) |
1511 | ) { |
1512 | wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" ); |
1513 | |
1514 | return true; |
1515 | } |
1516 | } elseif ( $str != '' && preg_match( "!<\?xml\b!i", $str ) ) { |
1517 | // Start of XML declaration without an end in the first 4096 bytes |
1518 | // bytes. There shouldn't be a legitimate reason for this to happen. |
1519 | wfDebug( __METHOD__ . ": Unmatched XML declaration start" ); |
1520 | |
1521 | return true; |
1522 | } |
1523 | } |
1524 | |
1525 | return false; |
1526 | } |
1527 | |
1528 | /** |
1529 | * @param string $filename |
1530 | * @param bool $partial |
1531 | * @return bool|array |
1532 | */ |
1533 | protected function detectScriptInSvg( $filename, $partial ) { |
1534 | $this->mSVGNSError = false; |
1535 | $check = new XmlTypeCheck( |
1536 | $filename, |
1537 | [ $this, 'checkSvgScriptCallback' ], |
1538 | true, |
1539 | [ |
1540 | 'processing_instruction_handler' => [ __CLASS__, 'checkSvgPICallback' ], |
1541 | 'external_dtd_handler' => [ __CLASS__, 'checkSvgExternalDTD' ], |
1542 | ] |
1543 | ); |
1544 | if ( $check->wellFormed !== true ) { |
1545 | // Invalid xml (T60553) |
1546 | // But only when non-partial (T67724) |
1547 | return $partial ? false : [ 'uploadinvalidxml' ]; |
1548 | } |
1549 | |
1550 | if ( $check->filterMatch ) { |
1551 | if ( $this->mSVGNSError ) { |
1552 | return [ 'uploadscriptednamespace', $this->mSVGNSError ]; |
1553 | } |
1554 | return $check->filterMatchType; |
1555 | } |
1556 | |
1557 | return false; |
1558 | } |
1559 | |
1560 | /** |
1561 | * Callback to filter SVG Processing Instructions. |
1562 | * |
1563 | * @param string $target Processing instruction name |
1564 | * @param string $data Processing instruction attribute and value |
1565 | * @return bool|array |
1566 | */ |
1567 | public static function checkSvgPICallback( $target, $data ) { |
1568 | // Don't allow external stylesheets (T59550) |
1569 | if ( preg_match( '/xml-stylesheet/i', $target ) ) { |
1570 | return [ 'upload-scripted-pi-callback' ]; |
1571 | } |
1572 | |
1573 | return false; |
1574 | } |
1575 | |
1576 | /** |
1577 | * Verify that DTD URLs referenced are only the standard DTDs. |
1578 | * |
1579 | * Browsers seem to ignore external DTDs. |
1580 | * |
1581 | * However, just to be on the safe side, only allow DTDs from the SVG standard. |
1582 | * |
1583 | * @param string $type PUBLIC or SYSTEM |
1584 | * @param string $publicId The well-known public identifier for the dtd |
1585 | * @param string $systemId The url for the external dtd |
1586 | * @return bool|array |
1587 | */ |
1588 | public static function checkSvgExternalDTD( $type, $publicId, $systemId ) { |
1589 | // This doesn't include the XHTML+MathML+SVG doctype since we don't |
1590 | // allow XHTML anyway. |
1591 | static $allowedDTDs = [ |
1592 | 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd', |
1593 | 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd', |
1594 | 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd', |
1595 | 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd', |
1596 | // https://phabricator.wikimedia.org/T168856 |
1597 | 'http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd', |
1598 | ]; |
1599 | if ( $type !== 'PUBLIC' |
1600 | || !in_array( $systemId, $allowedDTDs ) |
1601 | || !str_starts_with( $publicId, "-//W3C//" ) |
1602 | ) { |
1603 | return [ 'upload-scripted-dtd' ]; |
1604 | } |
1605 | return false; |
1606 | } |
1607 | |
1608 | /** |
1609 | * @todo Replace this with a allow list filter! |
1610 | * @param string $element |
1611 | * @param array $attribs |
1612 | * @param string|null $data |
1613 | * @return bool|array |
1614 | */ |
1615 | public function checkSvgScriptCallback( $element, $attribs, $data = null ) { |
1616 | [ $namespace, $strippedElement ] = self::splitXmlNamespace( $element ); |
1617 | |
1618 | // We specifically don't include: |
1619 | // http://www.w3.org/1999/xhtml (T62771) |
1620 | static $validNamespaces = [ |
1621 | '', |
1622 | 'adobe:ns:meta/', |
1623 | 'http://creativecommons.org/ns#', |
1624 | 'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd', |
1625 | 'http://ns.adobe.com/adobeillustrator/10.0/', |
1626 | 'http://ns.adobe.com/adobesvgviewerextensions/3.0/', |
1627 | 'http://ns.adobe.com/extensibility/1.0/', |
1628 | 'http://ns.adobe.com/flows/1.0/', |
1629 | 'http://ns.adobe.com/illustrator/1.0/', |
1630 | 'http://ns.adobe.com/imagereplacement/1.0/', |
1631 | 'http://ns.adobe.com/pdf/1.3/', |
1632 | 'http://ns.adobe.com/photoshop/1.0/', |
1633 | 'http://ns.adobe.com/saveforweb/1.0/', |
1634 | 'http://ns.adobe.com/variables/1.0/', |
1635 | 'http://ns.adobe.com/xap/1.0/', |
1636 | 'http://ns.adobe.com/xap/1.0/g/', |
1637 | 'http://ns.adobe.com/xap/1.0/g/img/', |
1638 | 'http://ns.adobe.com/xap/1.0/mm/', |
1639 | 'http://ns.adobe.com/xap/1.0/rights/', |
1640 | 'http://ns.adobe.com/xap/1.0/stype/dimensions#', |
1641 | 'http://ns.adobe.com/xap/1.0/stype/font#', |
1642 | 'http://ns.adobe.com/xap/1.0/stype/manifestitem#', |
1643 | 'http://ns.adobe.com/xap/1.0/stype/resourceevent#', |
1644 | 'http://ns.adobe.com/xap/1.0/stype/resourceref#', |
1645 | 'http://ns.adobe.com/xap/1.0/t/pg/', |
1646 | 'http://purl.org/dc/elements/1.1/', |
1647 | 'http://purl.org/dc/elements/1.1', |
1648 | 'http://schemas.microsoft.com/visio/2003/svgextensions/', |
1649 | 'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd', |
1650 | 'http://taptrix.com/inkpad/svg_extensions', |
1651 | 'http://web.resource.org/cc/', |
1652 | 'http://www.freesoftware.fsf.org/bkchem/cdml', |
1653 | 'http://www.inkscape.org/namespaces/inkscape', |
1654 | 'http://www.opengis.net/gml', |
1655 | 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', |