This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
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 | /** |
||
25 | * @defgroup Upload Upload related |
||
26 | */ |
||
27 | |||
28 | /** |
||
29 | * @ingroup Upload |
||
30 | * |
||
31 | * UploadBase and subclasses are the backend of MediaWiki's file uploads. |
||
32 | * The frontends are formed by ApiUpload and SpecialUpload. |
||
33 | * |
||
34 | * @author Brion Vibber |
||
35 | * @author Bryan Tong Minh |
||
36 | * @author Michael Dale |
||
37 | */ |
||
38 | abstract class UploadBase { |
||
39 | /** @var string Local file system path to the file to upload (or a local copy) */ |
||
40 | protected $mTempPath; |
||
41 | /** @var TempFSFile|null Wrapper to handle deleting the temp file */ |
||
42 | protected $tempFileObj; |
||
43 | |||
44 | protected $mDesiredDestName, $mDestName, $mRemoveTempFile, $mSourceType; |
||
0 ignored issues
–
show
|
|||
45 | protected $mTitle = false, $mTitleError = 0; |
||
0 ignored issues
–
show
|
|||
46 | protected $mFilteredName, $mFinalExtension; |
||
0 ignored issues
–
show
|
|||
47 | protected $mLocalFile, $mStashFile, $mFileSize, $mFileProps; |
||
0 ignored issues
–
show
|
|||
48 | protected $mBlackListedExtensions; |
||
49 | protected $mJavaDetected, $mSVGNSError; |
||
0 ignored issues
–
show
|
|||
50 | |||
51 | protected static $safeXmlEncodings = [ |
||
52 | 'UTF-8', |
||
53 | 'ISO-8859-1', |
||
54 | 'ISO-8859-2', |
||
55 | 'UTF-16', |
||
56 | 'UTF-32', |
||
57 | 'WINDOWS-1250', |
||
58 | 'WINDOWS-1251', |
||
59 | 'WINDOWS-1252', |
||
60 | 'WINDOWS-1253', |
||
61 | 'WINDOWS-1254', |
||
62 | 'WINDOWS-1255', |
||
63 | 'WINDOWS-1256', |
||
64 | 'WINDOWS-1257', |
||
65 | 'WINDOWS-1258', |
||
66 | ]; |
||
67 | |||
68 | const SUCCESS = 0; |
||
69 | const OK = 0; |
||
70 | const EMPTY_FILE = 3; |
||
71 | const MIN_LENGTH_PARTNAME = 4; |
||
72 | const ILLEGAL_FILENAME = 5; |
||
73 | const OVERWRITE_EXISTING_FILE = 7; # Not used anymore; handled by verifyTitlePermissions() |
||
74 | const FILETYPE_MISSING = 8; |
||
75 | const FILETYPE_BADTYPE = 9; |
||
76 | const VERIFICATION_ERROR = 10; |
||
77 | const HOOK_ABORTED = 11; |
||
78 | const FILE_TOO_LARGE = 12; |
||
79 | const WINDOWS_NONASCII_FILENAME = 13; |
||
80 | const FILENAME_TOO_LONG = 14; |
||
81 | |||
82 | /** |
||
83 | * @param int $error |
||
84 | * @return string |
||
85 | */ |
||
86 | public function getVerificationErrorCode( $error ) { |
||
87 | $code_to_status = [ |
||
88 | self::EMPTY_FILE => 'empty-file', |
||
89 | self::FILE_TOO_LARGE => 'file-too-large', |
||
90 | self::FILETYPE_MISSING => 'filetype-missing', |
||
91 | self::FILETYPE_BADTYPE => 'filetype-banned', |
||
92 | self::MIN_LENGTH_PARTNAME => 'filename-tooshort', |
||
93 | self::ILLEGAL_FILENAME => 'illegal-filename', |
||
94 | self::OVERWRITE_EXISTING_FILE => 'overwrite', |
||
95 | self::VERIFICATION_ERROR => 'verification-error', |
||
96 | self::HOOK_ABORTED => 'hookaborted', |
||
97 | self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename', |
||
98 | self::FILENAME_TOO_LONG => 'filename-toolong', |
||
99 | ]; |
||
100 | if ( isset( $code_to_status[$error] ) ) { |
||
101 | return $code_to_status[$error]; |
||
102 | } |
||
103 | |||
104 | return 'unknown-error'; |
||
105 | } |
||
106 | |||
107 | /** |
||
108 | * Returns true if uploads are enabled. |
||
109 | * Can be override by subclasses. |
||
110 | * @return bool |
||
111 | */ |
||
112 | public static function isEnabled() { |
||
113 | global $wgEnableUploads; |
||
114 | |||
115 | if ( !$wgEnableUploads ) { |
||
116 | return false; |
||
117 | } |
||
118 | |||
119 | # Check php's file_uploads setting |
||
120 | return wfIsHHVM() || wfIniGetBool( 'file_uploads' ); |
||
121 | } |
||
122 | |||
123 | /** |
||
124 | * Returns true if the user can use this upload module or else a string |
||
125 | * identifying the missing permission. |
||
126 | * Can be overridden by subclasses. |
||
127 | * |
||
128 | * @param User $user |
||
129 | * @return bool|string |
||
130 | */ |
||
131 | public static function isAllowed( $user ) { |
||
132 | foreach ( [ 'upload', 'edit' ] as $permission ) { |
||
133 | if ( !$user->isAllowed( $permission ) ) { |
||
134 | return $permission; |
||
135 | } |
||
136 | } |
||
137 | |||
138 | return true; |
||
139 | } |
||
140 | |||
141 | /** |
||
142 | * Returns true if the user has surpassed the upload rate limit, false otherwise. |
||
143 | * |
||
144 | * @param User $user |
||
145 | * @return bool |
||
146 | */ |
||
147 | public static function isThrottled( $user ) { |
||
148 | return $user->pingLimiter( 'upload' ); |
||
149 | } |
||
150 | |||
151 | // Upload handlers. Should probably just be a global. |
||
152 | private static $uploadHandlers = [ 'Stash', 'File', 'Url' ]; |
||
153 | |||
154 | /** |
||
155 | * Create a form of UploadBase depending on wpSourceType and initializes it |
||
156 | * |
||
157 | * @param WebRequest $request |
||
158 | * @param string|null $type |
||
159 | * @return null|UploadBase |
||
160 | */ |
||
161 | public static function createFromRequest( &$request, $type = null ) { |
||
162 | $type = $type ? $type : $request->getVal( 'wpSourceType', 'File' ); |
||
163 | |||
164 | if ( !$type ) { |
||
0 ignored issues
–
show
The expression
$type of type null|string is loosely compared to false ; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.
In PHP, under loose comparison (like For '' == false // true
'' == null // true
'ab' == false // false
'ab' == null // false
// It is often better to use strict comparison
'' === false // false
'' === null // false
![]() |
|||
165 | return null; |
||
166 | } |
||
167 | |||
168 | // Get the upload class |
||
169 | $type = ucfirst( $type ); |
||
170 | |||
171 | // Give hooks the chance to handle this request |
||
172 | $className = null; |
||
173 | Hooks::run( 'UploadCreateFromRequest', [ $type, &$className ] ); |
||
174 | if ( is_null( $className ) ) { |
||
175 | $className = 'UploadFrom' . $type; |
||
176 | wfDebug( __METHOD__ . ": class name: $className\n" ); |
||
177 | if ( !in_array( $type, self::$uploadHandlers ) ) { |
||
178 | return null; |
||
179 | } |
||
180 | } |
||
181 | |||
182 | // Check whether this upload class is enabled |
||
183 | if ( !call_user_func( [ $className, 'isEnabled' ] ) ) { |
||
184 | return null; |
||
185 | } |
||
186 | |||
187 | // Check whether the request is valid |
||
188 | if ( !call_user_func( [ $className, 'isValidRequest' ], $request ) ) { |
||
189 | return null; |
||
190 | } |
||
191 | |||
192 | /** @var UploadBase $handler */ |
||
193 | $handler = new $className; |
||
194 | |||
195 | $handler->initializeFromRequest( $request ); |
||
196 | |||
197 | return $handler; |
||
198 | } |
||
199 | |||
200 | /** |
||
201 | * Check whether a request if valid for this handler |
||
202 | * @param WebRequest $request |
||
203 | * @return bool |
||
204 | */ |
||
205 | public static function isValidRequest( $request ) { |
||
206 | return false; |
||
207 | } |
||
208 | |||
209 | public function __construct() { |
||
210 | } |
||
211 | |||
212 | /** |
||
213 | * Returns the upload type. Should be overridden by child classes |
||
214 | * |
||
215 | * @since 1.18 |
||
216 | * @return string |
||
217 | */ |
||
218 | public function getSourceType() { |
||
219 | return null; |
||
220 | } |
||
221 | |||
222 | /** |
||
223 | * Initialize the path information |
||
224 | * @param string $name The desired destination name |
||
225 | * @param string $tempPath The temporary path |
||
226 | * @param int $fileSize The file size |
||
227 | * @param bool $removeTempFile (false) remove the temporary file? |
||
228 | * @throws MWException |
||
229 | */ |
||
230 | public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) { |
||
231 | $this->mDesiredDestName = $name; |
||
232 | if ( FileBackend::isStoragePath( $tempPath ) ) { |
||
233 | throw new MWException( __METHOD__ . " given storage path `$tempPath`." ); |
||
234 | } |
||
235 | |||
236 | $this->setTempFile( $tempPath, $fileSize ); |
||
237 | $this->mRemoveTempFile = $removeTempFile; |
||
238 | } |
||
239 | |||
240 | /** |
||
241 | * Initialize from a WebRequest. Override this in a subclass. |
||
242 | * |
||
243 | * @param WebRequest $request |
||
244 | */ |
||
245 | abstract public function initializeFromRequest( &$request ); |
||
246 | |||
247 | /** |
||
248 | * @param string $tempPath File system path to temporary file containing the upload |
||
249 | * @param integer $fileSize |
||
250 | */ |
||
251 | protected function setTempFile( $tempPath, $fileSize = null ) { |
||
252 | $this->mTempPath = $tempPath; |
||
253 | $this->mFileSize = $fileSize ?: null; |
||
254 | if ( strlen( $this->mTempPath ) && file_exists( $this->mTempPath ) ) { |
||
255 | $this->tempFileObj = new TempFSFile( $this->mTempPath ); |
||
256 | if ( !$fileSize ) { |
||
0 ignored issues
–
show
The expression
$fileSize of type integer|null is loosely compared to false ; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.
In PHP, under loose comparison (like For 0 == false // true
0 == null // true
123 == false // false
123 == null // false
// It is often better to use strict comparison
0 === false // false
0 === null // false
![]() |
|||
257 | $this->mFileSize = filesize( $this->mTempPath ); |
||
258 | } |
||
259 | } else { |
||
260 | $this->tempFileObj = null; |
||
261 | } |
||
262 | } |
||
263 | |||
264 | /** |
||
265 | * Fetch the file. Usually a no-op |
||
266 | * @return Status |
||
267 | */ |
||
268 | public function fetchFile() { |
||
269 | return Status::newGood(); |
||
270 | } |
||
271 | |||
272 | /** |
||
273 | * Return true if the file is empty |
||
274 | * @return bool |
||
275 | */ |
||
276 | public function isEmptyFile() { |
||
277 | return empty( $this->mFileSize ); |
||
278 | } |
||
279 | |||
280 | /** |
||
281 | * Return the file size |
||
282 | * @return int |
||
283 | */ |
||
284 | public function getFileSize() { |
||
285 | return $this->mFileSize; |
||
286 | } |
||
287 | |||
288 | /** |
||
289 | * Get the base 36 SHA1 of the file |
||
290 | * @return string |
||
291 | */ |
||
292 | public function getTempFileSha1Base36() { |
||
293 | return FSFile::getSha1Base36FromPath( $this->mTempPath ); |
||
294 | } |
||
295 | |||
296 | /** |
||
297 | * @param string $srcPath The source path |
||
298 | * @return string|bool The real path if it was a virtual URL Returns false on failure |
||
299 | */ |
||
300 | function getRealPath( $srcPath ) { |
||
301 | $repo = RepoGroup::singleton()->getLocalRepo(); |
||
302 | if ( $repo->isVirtualUrl( $srcPath ) ) { |
||
303 | /** @todo Just make uploads work with storage paths UploadFromStash |
||
304 | * loads files via virtual URLs. |
||
305 | */ |
||
306 | $tmpFile = $repo->getLocalCopy( $srcPath ); |
||
307 | if ( $tmpFile ) { |
||
308 | $tmpFile->bind( $this ); // keep alive with $this |
||
309 | } |
||
310 | $path = $tmpFile ? $tmpFile->getPath() : false; |
||
311 | } else { |
||
312 | $path = $srcPath; |
||
313 | } |
||
314 | |||
315 | return $path; |
||
316 | } |
||
317 | |||
318 | /** |
||
319 | * Verify whether the upload is sane. |
||
320 | * @return mixed Const self::OK or else an array with error information |
||
321 | */ |
||
322 | public function verifyUpload() { |
||
323 | |||
324 | /** |
||
325 | * If there was no filename or a zero size given, give up quick. |
||
326 | */ |
||
327 | if ( $this->isEmptyFile() ) { |
||
328 | return [ 'status' => self::EMPTY_FILE ]; |
||
329 | } |
||
330 | |||
331 | /** |
||
332 | * Honor $wgMaxUploadSize |
||
333 | */ |
||
334 | $maxSize = self::getMaxUploadSize( $this->getSourceType() ); |
||
335 | if ( $this->mFileSize > $maxSize ) { |
||
336 | return [ |
||
337 | 'status' => self::FILE_TOO_LARGE, |
||
338 | 'max' => $maxSize, |
||
339 | ]; |
||
340 | } |
||
341 | |||
342 | /** |
||
343 | * Look at the contents of the file; if we can recognize the |
||
344 | * type but it's corrupt or data of the wrong type, we should |
||
345 | * probably not accept it. |
||
346 | */ |
||
347 | $verification = $this->verifyFile(); |
||
348 | if ( $verification !== true ) { |
||
349 | return [ |
||
350 | 'status' => self::VERIFICATION_ERROR, |
||
351 | 'details' => $verification |
||
352 | ]; |
||
353 | } |
||
354 | |||
355 | /** |
||
356 | * Make sure this file can be created |
||
357 | */ |
||
358 | $result = $this->validateName(); |
||
359 | if ( $result !== true ) { |
||
360 | return $result; |
||
361 | } |
||
362 | |||
363 | $error = ''; |
||
364 | if ( !Hooks::run( 'UploadVerification', |
||
365 | [ $this->mDestName, $this->mTempPath, &$error ], '1.28' ) |
||
366 | ) { |
||
367 | return [ 'status' => self::HOOK_ABORTED, 'error' => $error ]; |
||
368 | } |
||
369 | |||
370 | return [ 'status' => self::OK ]; |
||
371 | } |
||
372 | |||
373 | /** |
||
374 | * Verify that the name is valid and, if necessary, that we can overwrite |
||
375 | * |
||
376 | * @return mixed True if valid, otherwise and array with 'status' |
||
377 | * and other keys |
||
378 | */ |
||
379 | public function validateName() { |
||
380 | $nt = $this->getTitle(); |
||
381 | if ( is_null( $nt ) ) { |
||
382 | $result = [ 'status' => $this->mTitleError ]; |
||
383 | if ( $this->mTitleError == self::ILLEGAL_FILENAME ) { |
||
384 | $result['filtered'] = $this->mFilteredName; |
||
385 | } |
||
386 | if ( $this->mTitleError == self::FILETYPE_BADTYPE ) { |
||
387 | $result['finalExt'] = $this->mFinalExtension; |
||
388 | if ( count( $this->mBlackListedExtensions ) ) { |
||
389 | $result['blacklistedExt'] = $this->mBlackListedExtensions; |
||
390 | } |
||
391 | } |
||
392 | |||
393 | return $result; |
||
394 | } |
||
395 | $this->mDestName = $this->getLocalFile()->getName(); |
||
396 | |||
397 | return true; |
||
398 | } |
||
399 | |||
400 | /** |
||
401 | * Verify the MIME type. |
||
402 | * |
||
403 | * @note Only checks that it is not an evil MIME. The "does it have |
||
404 | * correct extension given its MIME type?" check is in verifyFile. |
||
405 | * in `verifyFile()` that MIME type and file extension correlate. |
||
406 | * @param string $mime Representing the MIME |
||
407 | * @return mixed True if the file is verified, an array otherwise |
||
408 | */ |
||
409 | protected function verifyMimeType( $mime ) { |
||
410 | global $wgVerifyMimeType; |
||
411 | if ( $wgVerifyMimeType ) { |
||
412 | wfDebug( "mime: <$mime> extension: <{$this->mFinalExtension}>\n" ); |
||
413 | global $wgMimeTypeBlacklist; |
||
414 | if ( $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) { |
||
415 | return [ 'filetype-badmime', $mime ]; |
||
416 | } |
||
417 | |||
418 | # Check what Internet Explorer would detect |
||
419 | $fp = fopen( $this->mTempPath, 'rb' ); |
||
420 | $chunk = fread( $fp, 256 ); |
||
421 | fclose( $fp ); |
||
422 | |||
423 | $magic = MimeMagic::singleton(); |
||
424 | $extMime = $magic->guessTypesForExtension( $this->mFinalExtension ); |
||
425 | $ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime ); |
||
426 | foreach ( $ieTypes as $ieType ) { |
||
427 | if ( $this->checkFileExtension( $ieType, $wgMimeTypeBlacklist ) ) { |
||
428 | return [ 'filetype-bad-ie-mime', $ieType ]; |
||
429 | } |
||
430 | } |
||
431 | } |
||
432 | |||
433 | return true; |
||
434 | } |
||
435 | |||
436 | /** |
||
437 | * Verifies that it's ok to include the uploaded file |
||
438 | * |
||
439 | * @return mixed True of the file is verified, array otherwise. |
||
440 | */ |
||
441 | protected function verifyFile() { |
||
442 | global $wgVerifyMimeType, $wgDisableUploadScriptChecks; |
||
443 | |||
444 | $status = $this->verifyPartialFile(); |
||
445 | if ( $status !== true ) { |
||
446 | return $status; |
||
447 | } |
||
448 | |||
449 | $mwProps = new MWFileProps( MimeMagic::singleton() ); |
||
450 | $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension ); |
||
451 | $mime = $this->mFileProps['mime']; |
||
452 | |||
453 | if ( $wgVerifyMimeType ) { |
||
454 | # XXX: Missing extension will be caught by validateName() via getTitle() |
||
455 | if ( $this->mFinalExtension != '' && !$this->verifyExtension( $mime, $this->mFinalExtension ) ) { |
||
456 | return [ 'filetype-mime-mismatch', $this->mFinalExtension, $mime ]; |
||
457 | } |
||
458 | } |
||
459 | |||
460 | # check for htmlish code and javascript |
||
461 | View Code Duplication | if ( !$wgDisableUploadScriptChecks ) { |
|
462 | if ( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) { |
||
463 | $svgStatus = $this->detectScriptInSvg( $this->mTempPath, false ); |
||
464 | if ( $svgStatus !== false ) { |
||
465 | return $svgStatus; |
||
466 | } |
||
467 | } |
||
468 | } |
||
469 | |||
470 | $handler = MediaHandler::getHandler( $mime ); |
||
471 | if ( $handler ) { |
||
472 | $handlerStatus = $handler->verifyUpload( $this->mTempPath ); |
||
473 | if ( !$handlerStatus->isOK() ) { |
||
474 | $errors = $handlerStatus->getErrorsArray(); |
||
475 | |||
476 | return reset( $errors ); |
||
477 | } |
||
478 | } |
||
479 | |||
480 | $error = true; |
||
481 | Hooks::run( 'UploadVerifyFile', [ $this, $mime, &$error ] ); |
||
482 | View Code Duplication | if ( $error !== true ) { |
|
483 | if ( !is_array( $error ) ) { |
||
484 | $error = [ $error ]; |
||
485 | } |
||
486 | return $error; |
||
487 | } |
||
488 | |||
489 | wfDebug( __METHOD__ . ": all clear; passing.\n" ); |
||
490 | |||
491 | return true; |
||
492 | } |
||
493 | |||
494 | /** |
||
495 | * A verification routine suitable for partial files |
||
496 | * |
||
497 | * Runs the blacklist checks, but not any checks that may |
||
498 | * assume the entire file is present. |
||
499 | * |
||
500 | * @return mixed True for valid or array with error message key. |
||
501 | */ |
||
502 | protected function verifyPartialFile() { |
||
503 | global $wgAllowJavaUploads, $wgDisableUploadScriptChecks; |
||
504 | |||
505 | # getTitle() sets some internal parameters like $this->mFinalExtension |
||
506 | $this->getTitle(); |
||
507 | |||
508 | $mwProps = new MWFileProps( MimeMagic::singleton() ); |
||
509 | $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension ); |
||
510 | |||
511 | # check MIME type, if desired |
||
512 | $mime = $this->mFileProps['file-mime']; |
||
513 | $status = $this->verifyMimeType( $mime ); |
||
514 | if ( $status !== true ) { |
||
515 | return $status; |
||
516 | } |
||
517 | |||
518 | # check for htmlish code and javascript |
||
519 | if ( !$wgDisableUploadScriptChecks ) { |
||
520 | if ( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) { |
||
521 | return [ 'uploadscripted' ]; |
||
522 | } |
||
523 | View Code Duplication | if ( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) { |
|
524 | $svgStatus = $this->detectScriptInSvg( $this->mTempPath, true ); |
||
525 | if ( $svgStatus !== false ) { |
||
526 | return $svgStatus; |
||
527 | } |
||
528 | } |
||
529 | } |
||
530 | |||
531 | # Check for Java applets, which if uploaded can bypass cross-site |
||
532 | # restrictions. |
||
533 | if ( !$wgAllowJavaUploads ) { |
||
534 | $this->mJavaDetected = false; |
||
535 | $zipStatus = ZipDirectoryReader::read( $this->mTempPath, |
||
536 | [ $this, 'zipEntryCallback' ] ); |
||
537 | if ( !$zipStatus->isOK() ) { |
||
538 | $errors = $zipStatus->getErrorsArray(); |
||
539 | $error = reset( $errors ); |
||
540 | if ( $error[0] !== 'zip-wrong-format' ) { |
||
541 | return $error; |
||
542 | } |
||
543 | } |
||
544 | if ( $this->mJavaDetected ) { |
||
545 | return [ 'uploadjava' ]; |
||
546 | } |
||
547 | } |
||
548 | |||
549 | # Scan the uploaded file for viruses |
||
550 | $virus = $this->detectVirus( $this->mTempPath ); |
||
551 | if ( $virus ) { |
||
552 | return [ 'uploadvirus', $virus ]; |
||
553 | } |
||
554 | |||
555 | return true; |
||
556 | } |
||
557 | |||
558 | /** |
||
559 | * Callback for ZipDirectoryReader to detect Java class files. |
||
560 | * |
||
561 | * @param array $entry |
||
562 | */ |
||
563 | function zipEntryCallback( $entry ) { |
||
564 | $names = [ $entry['name'] ]; |
||
565 | |||
566 | // If there is a null character, cut off the name at it, because JDK's |
||
567 | // ZIP_GetEntry() uses strcmp() if the name hashes match. If a file name |
||
568 | // were constructed which had ".class\0" followed by a string chosen to |
||
569 | // make the hash collide with the truncated name, that file could be |
||
570 | // returned in response to a request for the .class file. |
||
571 | $nullPos = strpos( $entry['name'], "\000" ); |
||
572 | if ( $nullPos !== false ) { |
||
573 | $names[] = substr( $entry['name'], 0, $nullPos ); |
||
574 | } |
||
575 | |||
576 | // If there is a trailing slash in the file name, we have to strip it, |
||
577 | // because that's what ZIP_GetEntry() does. |
||
578 | if ( preg_grep( '!\.class/?$!', $names ) ) { |
||
579 | $this->mJavaDetected = true; |
||
580 | } |
||
581 | } |
||
582 | |||
583 | /** |
||
584 | * Alias for verifyTitlePermissions. The function was originally |
||
585 | * 'verifyPermissions', but that suggests it's checking the user, when it's |
||
586 | * really checking the title + user combination. |
||
587 | * |
||
588 | * @param User $user User object to verify the permissions against |
||
589 | * @return mixed An array as returned by getUserPermissionsErrors or true |
||
590 | * in case the user has proper permissions. |
||
591 | */ |
||
592 | public function verifyPermissions( $user ) { |
||
593 | return $this->verifyTitlePermissions( $user ); |
||
594 | } |
||
595 | |||
596 | /** |
||
597 | * Check whether the user can edit, upload and create the image. This |
||
598 | * checks only against the current title; if it returns errors, it may |
||
599 | * very well be that another title will not give errors. Therefore |
||
600 | * isAllowed() should be called as well for generic is-user-blocked or |
||
601 | * can-user-upload checking. |
||
602 | * |
||
603 | * @param User $user User object to verify the permissions against |
||
604 | * @return mixed An array as returned by getUserPermissionsErrors or true |
||
605 | * in case the user has proper permissions. |
||
606 | */ |
||
607 | public function verifyTitlePermissions( $user ) { |
||
608 | /** |
||
609 | * If the image is protected, non-sysop users won't be able |
||
610 | * to modify it by uploading a new revision. |
||
611 | */ |
||
612 | $nt = $this->getTitle(); |
||
613 | if ( is_null( $nt ) ) { |
||
614 | return true; |
||
615 | } |
||
616 | $permErrors = $nt->getUserPermissionsErrors( 'edit', $user ); |
||
617 | $permErrorsUpload = $nt->getUserPermissionsErrors( 'upload', $user ); |
||
618 | if ( !$nt->exists() ) { |
||
619 | $permErrorsCreate = $nt->getUserPermissionsErrors( 'create', $user ); |
||
620 | } else { |
||
621 | $permErrorsCreate = []; |
||
622 | } |
||
623 | if ( $permErrors || $permErrorsUpload || $permErrorsCreate ) { |
||
624 | $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsUpload, $permErrors ) ); |
||
625 | $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsCreate, $permErrors ) ); |
||
626 | |||
627 | return $permErrors; |
||
628 | } |
||
629 | |||
630 | $overwriteError = $this->checkOverwrite( $user ); |
||
631 | if ( $overwriteError !== true ) { |
||
632 | return [ $overwriteError ]; |
||
633 | } |
||
634 | |||
635 | return true; |
||
636 | } |
||
637 | |||
638 | /** |
||
639 | * Check for non fatal problems with the file. |
||
640 | * |
||
641 | * This should not assume that mTempPath is set. |
||
642 | * |
||
643 | * @return array Array of warnings |
||
644 | */ |
||
645 | public function checkWarnings() { |
||
646 | global $wgLang; |
||
647 | |||
648 | $warnings = []; |
||
649 | |||
650 | $localFile = $this->getLocalFile(); |
||
651 | $localFile->load( File::READ_LATEST ); |
||
652 | $filename = $localFile->getName(); |
||
653 | |||
654 | /** |
||
655 | * Check whether the resulting filename is different from the desired one, |
||
656 | * but ignore things like ucfirst() and spaces/underscore things |
||
657 | */ |
||
658 | $comparableName = str_replace( ' ', '_', $this->mDesiredDestName ); |
||
659 | $comparableName = Title::capitalize( $comparableName, NS_FILE ); |
||
660 | |||
661 | if ( $this->mDesiredDestName != $filename && $comparableName != $filename ) { |
||
662 | $warnings['badfilename'] = $filename; |
||
663 | } |
||
664 | |||
665 | // Check whether the file extension is on the unwanted list |
||
666 | global $wgCheckFileExtensions, $wgFileExtensions; |
||
667 | if ( $wgCheckFileExtensions ) { |
||
668 | $extensions = array_unique( $wgFileExtensions ); |
||
669 | if ( !$this->checkFileExtension( $this->mFinalExtension, $extensions ) ) { |
||
670 | $warnings['filetype-unwanted-type'] = [ $this->mFinalExtension, |
||
671 | $wgLang->commaList( $extensions ), count( $extensions ) ]; |
||
672 | } |
||
673 | } |
||
674 | |||
675 | global $wgUploadSizeWarning; |
||
676 | if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) ) { |
||
677 | $warnings['large-file'] = [ $wgUploadSizeWarning, $this->mFileSize ]; |
||
678 | } |
||
679 | |||
680 | if ( $this->mFileSize == 0 ) { |
||
0 ignored issues
–
show
|
|||
681 | $warnings['empty-file'] = true; |
||
682 | } |
||
683 | |||
684 | $hash = $this->getTempFileSha1Base36(); |
||
685 | $exists = self::getExistsWarning( $localFile ); |
||
0 ignored issues
–
show
It seems like
$localFile defined by $this->getLocalFile() on line 650 can be null ; however, UploadBase::getExistsWarning() does not accept null , maybe add an additional type check?
Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code: /** @return stdClass|null */
function mayReturnNull() { }
function doesNotAcceptNull(stdClass $x) { }
// With potential error.
function withoutCheck() {
$x = mayReturnNull();
doesNotAcceptNull($x); // Potential error here.
}
// Safe - Alternative 1
function withCheck1() {
$x = mayReturnNull();
if ( ! $x instanceof stdClass) {
throw new \LogicException('$x must be defined.');
}
doesNotAcceptNull($x);
}
// Safe - Alternative 2
function withCheck2() {
$x = mayReturnNull();
if ($x instanceof stdClass) {
doesNotAcceptNull($x);
}
}
![]() |
|||
686 | if ( $exists !== false ) { |
||
687 | $warnings['exists'] = $exists; |
||
688 | |||
689 | // check if file is an exact duplicate of current file version |
||
690 | if ( $hash === $localFile->getSha1() ) { |
||
691 | $warnings['no-change'] = $localFile; |
||
692 | } |
||
693 | |||
694 | // check if file is an exact duplicate of older versions of this file |
||
695 | $history = $localFile->getHistory(); |
||
696 | foreach ( $history as $oldFile ) { |
||
697 | if ( $hash === $oldFile->getSha1() ) { |
||
698 | $warnings['duplicate-version'][] = $oldFile; |
||
699 | } |
||
700 | } |
||
701 | } |
||
702 | |||
703 | if ( $localFile->wasDeleted() && !$localFile->exists() ) { |
||
704 | $warnings['was-deleted'] = $filename; |
||
705 | } |
||
706 | |||
707 | // Check dupes against existing files |
||
708 | $dupes = RepoGroup::singleton()->findBySha1( $hash ); |
||
709 | $title = $this->getTitle(); |
||
710 | // Remove all matches against self |
||
711 | foreach ( $dupes as $key => $dupe ) { |
||
712 | if ( $title->equals( $dupe->getTitle() ) ) { |
||
713 | unset( $dupes[$key] ); |
||
714 | } |
||
715 | } |
||
716 | if ( $dupes ) { |
||
717 | $warnings['duplicate'] = $dupes; |
||
718 | } |
||
719 | |||
720 | // Check dupes against archives |
||
721 | $archivedFile = new ArchivedFile( null, 0, '', $hash ); |
||
722 | if ( $archivedFile->getID() > 0 ) { |
||
723 | if ( $archivedFile->userCan( File::DELETED_FILE ) ) { |
||
724 | $warnings['duplicate-archive'] = $archivedFile->getName(); |
||
725 | } else { |
||
726 | $warnings['duplicate-archive'] = ''; |
||
727 | } |
||
728 | } |
||
729 | |||
730 | return $warnings; |
||
731 | } |
||
732 | |||
733 | /** |
||
734 | * Really perform the upload. Stores the file in the local repo, watches |
||
735 | * if necessary and runs the UploadComplete hook. |
||
736 | * |
||
737 | * @param string $comment |
||
738 | * @param string $pageText |
||
739 | * @param bool $watch Whether the file page should be added to user's watchlist. |
||
740 | * (This doesn't check $user's permissions.) |
||
741 | * @param User $user |
||
742 | * @param string[] $tags Change tags to add to the log entry and page revision. |
||
743 | * (This doesn't check $user's permissions.) |
||
744 | * @return Status Indicating the whether the upload succeeded. |
||
745 | */ |
||
746 | public function performUpload( $comment, $pageText, $watch, $user, $tags = [] ) { |
||
747 | $this->getLocalFile()->load( File::READ_LATEST ); |
||
748 | $props = $this->mFileProps; |
||
749 | |||
750 | $error = null; |
||
751 | Hooks::run( 'UploadVerifyUpload', [ $this, $user, $props, $comment, $pageText, &$error ] ); |
||
752 | if ( $error ) { |
||
753 | if ( !is_array( $error ) ) { |
||
754 | $error = [ $error ]; |
||
755 | } |
||
756 | return call_user_func_array( 'Status::newFatal', $error ); |
||
757 | } |
||
758 | |||
759 | $status = $this->getLocalFile()->upload( |
||
760 | $this->mTempPath, |
||
761 | $comment, |
||
762 | $pageText, |
||
763 | File::DELETE_SOURCE, |
||
764 | $props, |
||
765 | false, |
||
766 | $user, |
||
767 | $tags |
||
768 | ); |
||
769 | |||
770 | if ( $status->isGood() ) { |
||
771 | if ( $watch ) { |
||
772 | WatchAction::doWatch( |
||
773 | $this->getLocalFile()->getTitle(), |
||
774 | $user, |
||
775 | User::IGNORE_USER_RIGHTS |
||
776 | ); |
||
777 | } |
||
778 | Hooks::run( 'UploadComplete', [ &$this ] ); |
||
779 | |||
780 | $this->postProcessUpload(); |
||
781 | } |
||
782 | |||
783 | return $status; |
||
784 | } |
||
785 | |||
786 | /** |
||
787 | * Perform extra steps after a successful upload. |
||
788 | * |
||
789 | * @since 1.25 |
||
790 | */ |
||
791 | public function postProcessUpload() { |
||
792 | } |
||
793 | |||
794 | /** |
||
795 | * Returns the title of the file to be uploaded. Sets mTitleError in case |
||
796 | * the name was illegal. |
||
797 | * |
||
798 | * @return Title The title of the file or null in case the name was illegal |
||
799 | */ |
||
800 | public function getTitle() { |
||
801 | if ( $this->mTitle !== false ) { |
||
802 | return $this->mTitle; |
||
803 | } |
||
804 | if ( !is_string( $this->mDesiredDestName ) ) { |
||
805 | $this->mTitleError = self::ILLEGAL_FILENAME; |
||
806 | $this->mTitle = null; |
||
807 | |||
808 | return $this->mTitle; |
||
809 | } |
||
810 | /* Assume that if a user specified File:Something.jpg, this is an error |
||
811 | * and that the namespace prefix needs to be stripped of. |
||
812 | */ |
||
813 | $title = Title::newFromText( $this->mDesiredDestName ); |
||
814 | if ( $title && $title->getNamespace() == NS_FILE ) { |
||
815 | $this->mFilteredName = $title->getDBkey(); |
||
816 | } else { |
||
817 | $this->mFilteredName = $this->mDesiredDestName; |
||
818 | } |
||
819 | |||
820 | # oi_archive_name is max 255 bytes, which include a timestamp and an |
||
821 | # exclamation mark, so restrict file name to 240 bytes. |
||
822 | if ( strlen( $this->mFilteredName ) > 240 ) { |
||
823 | $this->mTitleError = self::FILENAME_TOO_LONG; |
||
824 | $this->mTitle = null; |
||
825 | |||
826 | return $this->mTitle; |
||
827 | } |
||
828 | |||
829 | /** |
||
830 | * Chop off any directories in the given filename. Then |
||
831 | * filter out illegal characters, and try to make a legible name |
||
832 | * out of it. We'll strip some silently that Title would die on. |
||
833 | */ |
||
834 | $this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName ); |
||
835 | /* Normalize to title form before we do any further processing */ |
||
836 | $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName ); |
||
837 | if ( is_null( $nt ) ) { |
||
838 | $this->mTitleError = self::ILLEGAL_FILENAME; |
||
839 | $this->mTitle = null; |
||
840 | |||
841 | return $this->mTitle; |
||
842 | } |
||
843 | $this->mFilteredName = $nt->getDBkey(); |
||
844 | |||
845 | /** |
||
846 | * We'll want to blacklist against *any* 'extension', and use |
||
847 | * only the final one for the whitelist. |
||
848 | */ |
||
849 | list( $partname, $ext ) = $this->splitExtensions( $this->mFilteredName ); |
||
850 | |||
851 | if ( count( $ext ) ) { |
||
852 | $this->mFinalExtension = trim( $ext[count( $ext ) - 1] ); |
||
853 | } else { |
||
854 | $this->mFinalExtension = ''; |
||
855 | |||
856 | # No extension, try guessing one |
||
857 | $magic = MimeMagic::singleton(); |
||
858 | $mime = $magic->guessMimeType( $this->mTempPath ); |
||
859 | if ( $mime !== 'unknown/unknown' ) { |
||
860 | # Get a space separated list of extensions |
||
861 | $extList = $magic->getExtensionsForType( $mime ); |
||
862 | if ( $extList ) { |
||
0 ignored issues
–
show
The expression
$extList of type string|null is loosely compared to true ; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.
In PHP, under loose comparison (like For '' == false // true
'' == null // true
'ab' == false // false
'ab' == null // false
// It is often better to use strict comparison
'' === false // false
'' === null // false
![]() |
|||
863 | # Set the extension to the canonical extension |
||
864 | $this->mFinalExtension = strtok( $extList, ' ' ); |
||
865 | |||
866 | # Fix up the other variables |
||
867 | $this->mFilteredName .= ".{$this->mFinalExtension}"; |
||
868 | $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName ); |
||
869 | $ext = [ $this->mFinalExtension ]; |
||
870 | } |
||
871 | } |
||
872 | } |
||
873 | |||
874 | /* Don't allow users to override the blacklist (check file extension) */ |
||
875 | global $wgCheckFileExtensions, $wgStrictFileExtensions; |
||
876 | global $wgFileExtensions, $wgFileBlacklist; |
||
877 | |||
878 | $blackListedExtensions = $this->checkFileExtensionList( $ext, $wgFileBlacklist ); |
||
879 | |||
880 | if ( $this->mFinalExtension == '' ) { |
||
881 | $this->mTitleError = self::FILETYPE_MISSING; |
||
882 | $this->mTitle = null; |
||
883 | |||
884 | return $this->mTitle; |
||
885 | } elseif ( $blackListedExtensions || |
||
886 | ( $wgCheckFileExtensions && $wgStrictFileExtensions && |
||
887 | !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) |
||
888 | ) { |
||
889 | $this->mBlackListedExtensions = $blackListedExtensions; |
||
890 | $this->mTitleError = self::FILETYPE_BADTYPE; |
||
891 | $this->mTitle = null; |
||
892 | |||
893 | return $this->mTitle; |
||
894 | } |
||
895 | |||
896 | // Windows may be broken with special characters, see bug 1780 |
||
897 | if ( !preg_match( '/^[\x0-\x7f]*$/', $nt->getText() ) |
||
898 | && !RepoGroup::singleton()->getLocalRepo()->backendSupportsUnicodePaths() |
||
899 | ) { |
||
900 | $this->mTitleError = self::WINDOWS_NONASCII_FILENAME; |
||
901 | $this->mTitle = null; |
||
902 | |||
903 | return $this->mTitle; |
||
904 | } |
||
905 | |||
906 | # If there was more than one "extension", reassemble the base |
||
907 | # filename to prevent bogus complaints about length |
||
908 | if ( count( $ext ) > 1 ) { |
||
909 | $iterations = count( $ext ) - 1; |
||
910 | for ( $i = 0; $i < $iterations; $i++ ) { |
||
911 | $partname .= '.' . $ext[$i]; |
||
912 | } |
||
913 | } |
||
914 | |||
915 | if ( strlen( $partname ) < 1 ) { |
||
916 | $this->mTitleError = self::MIN_LENGTH_PARTNAME; |
||
917 | $this->mTitle = null; |
||
918 | |||
919 | return $this->mTitle; |
||
920 | } |
||
921 | |||
922 | $this->mTitle = $nt; |
||
0 ignored issues
–
show
It seems like
$nt can also be of type object<Title> . However, the property $mTitle is declared as type boolean . Maybe add an additional type check?
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly. For example, imagine you have a variable Either this assignment is in error or a type check should be added for that assignment. class Id
{
public $id;
public function __construct($id)
{
$this->id = $id;
}
}
class Account
{
/** @var Id $id */
public $id;
}
$account_id = false;
if (starsAreRight()) {
$account_id = new Id(42);
}
$account = new Account();
if ($account instanceof Id)
{
$account->id = $account_id;
}
![]() |
|||
923 | |||
924 | return $this->mTitle; |
||
925 | } |
||
926 | |||
927 | /** |
||
928 | * Return the local file and initializes if necessary. |
||
929 | * |
||
930 | * @return LocalFile|null |
||
931 | */ |
||
932 | public function getLocalFile() { |
||
933 | if ( is_null( $this->mLocalFile ) ) { |
||
934 | $nt = $this->getTitle(); |
||
935 | $this->mLocalFile = is_null( $nt ) ? null : wfLocalFile( $nt ); |
||
936 | } |
||
937 | |||
938 | return $this->mLocalFile; |
||
939 | } |
||
940 | |||
941 | /** |
||
942 | * @return UploadStashFile|null |
||
943 | */ |
||
944 | public function getStashFile() { |
||
945 | return $this->mStashFile; |
||
946 | } |
||
947 | |||
948 | /** |
||
949 | * Like stashFile(), but respects extensions' wishes to prevent the stashing. verifyUpload() must |
||
950 | * be called before calling this method (unless $isPartial is true). |
||
951 | * |
||
952 | * Upload stash exceptions are also caught and converted to an error status. |
||
953 | * |
||
954 | * @since 1.28 |
||
955 | * @param User $user |
||
956 | * @param bool $isPartial Pass `true` if this is a part of a chunked upload (not a complete file). |
||
957 | * @return Status If successful, value is an UploadStashFile instance |
||
958 | */ |
||
959 | public function tryStashFile( User $user, $isPartial = false ) { |
||
960 | if ( !$isPartial ) { |
||
961 | $error = $this->runUploadStashFileHook( $user ); |
||
962 | if ( $error ) { |
||
963 | return call_user_func_array( 'Status::newFatal', $error ); |
||
964 | } |
||
965 | } |
||
966 | try { |
||
967 | $file = $this->doStashFile( $user ); |
||
968 | return Status::newGood( $file ); |
||
969 | } catch ( UploadStashException $e ) { |
||
970 | return Status::newFatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() ); |
||
971 | } |
||
972 | } |
||
973 | |||
974 | /** |
||
975 | * @param User $user |
||
976 | * @return array|null Error message and parameters, null if there's no error |
||
977 | */ |
||
978 | protected function runUploadStashFileHook( User $user ) { |
||
979 | $props = $this->mFileProps; |
||
980 | $error = null; |
||
981 | Hooks::run( 'UploadStashFile', [ $this, $user, $props, &$error ] ); |
||
982 | if ( $error ) { |
||
983 | if ( !is_array( $error ) ) { |
||
984 | $error = [ $error ]; |
||
985 | } |
||
986 | } |
||
987 | return $error; |
||
988 | } |
||
989 | |||
990 | /** |
||
991 | * If the user does not supply all necessary information in the first upload |
||
992 | * form submission (either by accident or by design) then we may want to |
||
993 | * stash the file temporarily, get more information, and publish the file |
||
994 | * later. |
||
995 | * |
||
996 | * This method will stash a file in a temporary directory for later |
||
997 | * processing, and save the necessary descriptive info into the database. |
||
998 | * This method returns the file object, which also has a 'fileKey' property |
||
999 | * which can be passed through a form or API request to find this stashed |
||
1000 | * file again. |
||
1001 | * |
||
1002 | * @deprecated since 1.28 Use tryStashFile() instead |
||
1003 | * @param User $user |
||
1004 | * @return UploadStashFile Stashed file |
||
1005 | * @throws UploadStashBadPathException |
||
1006 | * @throws UploadStashFileException |
||
1007 | * @throws UploadStashNotLoggedInException |
||
1008 | */ |
||
1009 | public function stashFile( User $user = null ) { |
||
1010 | return $this->doStashFile( $user ); |
||
1011 | } |
||
1012 | |||
1013 | /** |
||
1014 | * Implementation for stashFile() and tryStashFile(). |
||
1015 | * |
||
1016 | * @param User $user |
||
1017 | * @return UploadStashFile Stashed file |
||
1018 | */ |
||
1019 | protected function doStashFile( User $user = null ) { |
||
1020 | $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $user ); |
||
1021 | $file = $stash->stashFile( $this->mTempPath, $this->getSourceType() ); |
||
1022 | $this->mStashFile = $file; |
||
1023 | |||
1024 | return $file; |
||
1025 | } |
||
1026 | |||
1027 | /** |
||
1028 | * Stash a file in a temporary directory, returning a key which can be used |
||
1029 | * to find the file again. See stashFile(). |
||
1030 | * |
||
1031 | * @deprecated since 1.28 |
||
1032 | * @return string File key |
||
1033 | */ |
||
1034 | public function stashFileGetKey() { |
||
1035 | wfDeprecated( __METHOD__, '1.28' ); |
||
1036 | return $this->doStashFile()->getFileKey(); |
||
1037 | } |
||
1038 | |||
1039 | /** |
||
1040 | * alias for stashFileGetKey, for backwards compatibility |
||
1041 | * |
||
1042 | * @deprecated since 1.28 |
||
1043 | * @return string File key |
||
1044 | */ |
||
1045 | public function stashSession() { |
||
1046 | wfDeprecated( __METHOD__, '1.28' ); |
||
1047 | return $this->doStashFile()->getFileKey(); |
||
1048 | } |
||
1049 | |||
1050 | /** |
||
1051 | * If we've modified the upload file we need to manually remove it |
||
1052 | * on exit to clean up. |
||
1053 | */ |
||
1054 | public function cleanupTempFile() { |
||
1055 | if ( $this->mRemoveTempFile && $this->tempFileObj ) { |
||
1056 | // Delete when all relevant TempFSFile handles go out of scope |
||
1057 | wfDebug( __METHOD__ . ": Marked temporary file '{$this->mTempPath}' for removal\n" ); |
||
1058 | $this->tempFileObj->autocollect(); |
||
1059 | } |
||
1060 | } |
||
1061 | |||
1062 | public function getTempPath() { |
||
1063 | return $this->mTempPath; |
||
1064 | } |
||
1065 | |||
1066 | /** |
||
1067 | * Split a file into a base name and all dot-delimited 'extensions' |
||
1068 | * on the end. Some web server configurations will fall back to |
||
1069 | * earlier pseudo-'extensions' to determine type and execute |
||
1070 | * scripts, so the blacklist needs to check them all. |
||
1071 | * |
||
1072 | * @param string $filename |
||
1073 | * @return array |
||
1074 | */ |
||
1075 | public static function splitExtensions( $filename ) { |
||
1076 | $bits = explode( '.', $filename ); |
||
1077 | $basename = array_shift( $bits ); |
||
1078 | |||
1079 | return [ $basename, $bits ]; |
||
1080 | } |
||
1081 | |||
1082 | /** |
||
1083 | * Perform case-insensitive match against a list of file extensions. |
||
1084 | * Returns true if the extension is in the list. |
||
1085 | * |
||
1086 | * @param string $ext |
||
1087 | * @param array $list |
||
1088 | * @return bool |
||
1089 | */ |
||
1090 | public static function checkFileExtension( $ext, $list ) { |
||
1091 | return in_array( strtolower( $ext ), $list ); |
||
1092 | } |
||
1093 | |||
1094 | /** |
||
1095 | * Perform case-insensitive match against a list of file extensions. |
||
1096 | * Returns an array of matching extensions. |
||
1097 | * |
||
1098 | * @param array $ext |
||
1099 | * @param array $list |
||
1100 | * @return bool |
||
1101 | */ |
||
1102 | public static function checkFileExtensionList( $ext, $list ) { |
||
1103 | return array_intersect( array_map( 'strtolower', $ext ), $list ); |
||
1104 | } |
||
1105 | |||
1106 | /** |
||
1107 | * Checks if the MIME type of the uploaded file matches the file extension. |
||
1108 | * |
||
1109 | * @param string $mime The MIME type of the uploaded file |
||
1110 | * @param string $extension The filename extension that the file is to be served with |
||
1111 | * @return bool |
||
1112 | */ |
||
1113 | public static function verifyExtension( $mime, $extension ) { |
||
1114 | $magic = MimeMagic::singleton(); |
||
1115 | |||
1116 | if ( !$mime || $mime == 'unknown' || $mime == 'unknown/unknown' ) { |
||
1117 | if ( !$magic->isRecognizableExtension( $extension ) ) { |
||
1118 | wfDebug( __METHOD__ . ": passing file with unknown detected mime type; " . |
||
1119 | "unrecognized extension '$extension', can't verify\n" ); |
||
1120 | |||
1121 | return true; |
||
1122 | } else { |
||
1123 | wfDebug( __METHOD__ . ": rejecting file with unknown detected mime type; " . |
||
1124 | "recognized extension '$extension', so probably invalid file\n" ); |
||
1125 | |||
1126 | return false; |
||
1127 | } |
||
1128 | } |
||
1129 | |||
1130 | $match = $magic->isMatchingExtension( $extension, $mime ); |
||
1131 | |||
1132 | if ( $match === null ) { |
||
1133 | if ( $magic->getTypesForExtension( $extension ) !== null ) { |
||
1134 | wfDebug( __METHOD__ . ": No extension known for $mime, but we know a mime for $extension\n" ); |
||
1135 | |||
1136 | return false; |
||
1137 | } else { |
||
1138 | wfDebug( __METHOD__ . ": no file extension known for mime type $mime, passing file\n" ); |
||
1139 | |||
1140 | return true; |
||
1141 | } |
||
1142 | } elseif ( $match === true ) { |
||
1143 | wfDebug( __METHOD__ . ": mime type $mime matches extension $extension, passing file\n" ); |
||
1144 | |||
1145 | /** @todo If it's a bitmap, make sure PHP or ImageMagick resp. can handle it! */ |
||
1146 | return true; |
||
1147 | } else { |
||
1148 | wfDebug( __METHOD__ |
||
1149 | . ": mime type $mime mismatches file extension $extension, rejecting file\n" ); |
||
1150 | |||
1151 | return false; |
||
1152 | } |
||
1153 | } |
||
1154 | |||
1155 | /** |
||
1156 | * Heuristic for detecting files that *could* contain JavaScript instructions or |
||
1157 | * things that may look like HTML to a browser and are thus |
||
1158 | * potentially harmful. The present implementation will produce false |
||
1159 | * positives in some situations. |
||
1160 | * |
||
1161 | * @param string $file Pathname to the temporary upload file |
||
1162 | * @param string $mime The MIME type of the file |
||
1163 | * @param string $extension The extension of the file |
||
1164 | * @return bool True if the file contains something looking like embedded scripts |
||
1165 | */ |
||
1166 | public static function detectScript( $file, $mime, $extension ) { |
||
1167 | global $wgAllowTitlesInSVG; |
||
1168 | |||
1169 | # ugly hack: for text files, always look at the entire file. |
||
1170 | # For binary field, just check the first K. |
||
1171 | |||
1172 | if ( strpos( $mime, 'text/' ) === 0 ) { |
||
1173 | $chunk = file_get_contents( $file ); |
||
1174 | } else { |
||
1175 | $fp = fopen( $file, 'rb' ); |
||
1176 | $chunk = fread( $fp, 1024 ); |
||
1177 | fclose( $fp ); |
||
1178 | } |
||
1179 | |||
1180 | $chunk = strtolower( $chunk ); |
||
1181 | |||
1182 | if ( !$chunk ) { |
||
1183 | return false; |
||
1184 | } |
||
1185 | |||
1186 | # decode from UTF-16 if needed (could be used for obfuscation). |
||
1187 | if ( substr( $chunk, 0, 2 ) == "\xfe\xff" ) { |
||
1188 | $enc = 'UTF-16BE'; |
||
1189 | } elseif ( substr( $chunk, 0, 2 ) == "\xff\xfe" ) { |
||
1190 | $enc = 'UTF-16LE'; |
||
1191 | } else { |
||
1192 | $enc = null; |
||
1193 | } |
||
1194 | |||
1195 | if ( $enc ) { |
||
0 ignored issues
–
show
The expression
$enc of type string|null is loosely compared to true ; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.
In PHP, under loose comparison (like For '' == false // true
'' == null // true
'ab' == false // false
'ab' == null // false
// It is often better to use strict comparison
'' === false // false
'' === null // false
![]() |
|||
1196 | $chunk = iconv( $enc, "ASCII//IGNORE", $chunk ); |
||
1197 | } |
||
1198 | |||
1199 | $chunk = trim( $chunk ); |
||
1200 | |||
1201 | /** @todo FIXME: Convert from UTF-16 if necessary! */ |
||
1202 | wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff\n" ); |
||
1203 | |||
1204 | # check for HTML doctype |
||
1205 | if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) { |
||
1206 | return true; |
||
1207 | } |
||
1208 | |||
1209 | // Some browsers will interpret obscure xml encodings as UTF-8, while |
||
1210 | // PHP/expat will interpret the given encoding in the xml declaration (bug 47304) |
||
1211 | if ( $extension == 'svg' || strpos( $mime, 'image/svg' ) === 0 ) { |
||
1212 | if ( self::checkXMLEncodingMissmatch( $file ) ) { |
||
1213 | return true; |
||
1214 | } |
||
1215 | } |
||
1216 | |||
1217 | /** |
||
1218 | * Internet Explorer for Windows performs some really stupid file type |
||
1219 | * autodetection which can cause it to interpret valid image files as HTML |
||
1220 | * and potentially execute JavaScript, creating a cross-site scripting |
||
1221 | * attack vectors. |
||
1222 | * |
||
1223 | * Apple's Safari browser also performs some unsafe file type autodetection |
||
1224 | * which can cause legitimate files to be interpreted as HTML if the |
||
1225 | * web server is not correctly configured to send the right content-type |
||
1226 | * (or if you're really uploading plain text and octet streams!) |
||
1227 | * |
||
1228 | * Returns true if IE is likely to mistake the given file for HTML. |
||
1229 | * Also returns true if Safari would mistake the given file for HTML |
||
1230 | * when served with a generic content-type. |
||
1231 | */ |
||
1232 | $tags = [ |
||
1233 | '<a href', |
||
1234 | '<body', |
||
1235 | '<head', |
||
1236 | '<html', # also in safari |
||
1237 | '<img', |
||
1238 | '<pre', |
||
1239 | '<script', # also in safari |
||
1240 | '<table' |
||
1241 | ]; |
||
1242 | |||
1243 | if ( !$wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) { |
||
1244 | $tags[] = '<title'; |
||
1245 | } |
||
1246 | |||
1247 | foreach ( $tags as $tag ) { |
||
1248 | if ( false !== strpos( $chunk, $tag ) ) { |
||
1249 | wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag\n" ); |
||
1250 | |||
1251 | return true; |
||
1252 | } |
||
1253 | } |
||
1254 | |||
1255 | /* |
||
1256 | * look for JavaScript |
||
1257 | */ |
||
1258 | |||
1259 | # resolve entity-refs to look at attributes. may be harsh on big files... cache result? |
||
1260 | $chunk = Sanitizer::decodeCharReferences( $chunk ); |
||
1261 | |||
1262 | # look for script-types |
||
1263 | if ( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim', $chunk ) ) { |
||
1264 | wfDebug( __METHOD__ . ": found script types\n" ); |
||
1265 | |||
1266 | return true; |
||
1267 | } |
||
1268 | |||
1269 | # look for html-style script-urls |
||
1270 | if ( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) { |
||
1271 | wfDebug( __METHOD__ . ": found html-style script urls\n" ); |
||
1272 | |||
1273 | return true; |
||
1274 | } |
||
1275 | |||
1276 | # look for css-style script-urls |
||
1277 | if ( preg_match( '!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) { |
||
1278 | wfDebug( __METHOD__ . ": found css-style script urls\n" ); |
||
1279 | |||
1280 | return true; |
||
1281 | } |
||
1282 | |||
1283 | wfDebug( __METHOD__ . ": no scripts found\n" ); |
||
1284 | |||
1285 | return false; |
||
1286 | } |
||
1287 | |||
1288 | /** |
||
1289 | * Check a whitelist of xml encodings that are known not to be interpreted differently |
||
1290 | * by the server's xml parser (expat) and some common browsers. |
||
1291 | * |
||
1292 | * @param string $file Pathname to the temporary upload file |
||
1293 | * @return bool True if the file contains an encoding that could be misinterpreted |
||
1294 | */ |
||
1295 | public static function checkXMLEncodingMissmatch( $file ) { |
||
1296 | global $wgSVGMetadataCutoff; |
||
1297 | $contents = file_get_contents( $file, false, null, -1, $wgSVGMetadataCutoff ); |
||
1298 | $encodingRegex = '!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si'; |
||
1299 | |||
1300 | if ( preg_match( "!<\?xml\b(.*?)\?>!si", $contents, $matches ) ) { |
||
1301 | View Code Duplication | if ( preg_match( $encodingRegex, $matches[1], $encMatch ) |
|
1302 | && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings ) |
||
1303 | ) { |
||
1304 | wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'\n" ); |
||
1305 | |||
1306 | return true; |
||
1307 | } |
||
1308 | } elseif ( preg_match( "!<\?xml\b!si", $contents ) ) { |
||
1309 | // Start of XML declaration without an end in the first $wgSVGMetadataCutoff |
||
1310 | // bytes. There shouldn't be a legitimate reason for this to happen. |
||
1311 | wfDebug( __METHOD__ . ": Unmatched XML declaration start\n" ); |
||
1312 | |||
1313 | return true; |
||
1314 | } elseif ( substr( $contents, 0, 4 ) == "\x4C\x6F\xA7\x94" ) { |
||
1315 | // EBCDIC encoded XML |
||
1316 | wfDebug( __METHOD__ . ": EBCDIC Encoded XML\n" ); |
||
1317 | |||
1318 | return true; |
||
1319 | } |
||
1320 | |||
1321 | // It's possible the file is encoded with multi-byte encoding, so re-encode attempt to |
||
1322 | // detect the encoding in case is specifies an encoding not whitelisted in self::$safeXmlEncodings |
||
1323 | $attemptEncodings = [ 'UTF-16', 'UTF-16BE', 'UTF-32', 'UTF-32BE' ]; |
||
1324 | foreach ( $attemptEncodings as $encoding ) { |
||
1325 | MediaWiki\suppressWarnings(); |
||
1326 | $str = iconv( $encoding, 'UTF-8', $contents ); |
||
1327 | MediaWiki\restoreWarnings(); |
||
1328 | if ( $str != '' && preg_match( "!<\?xml\b(.*?)\?>!si", $str, $matches ) ) { |
||
1329 | View Code Duplication | if ( preg_match( $encodingRegex, $matches[1], $encMatch ) |
|
1330 | && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings ) |
||
1331 | ) { |
||
1332 | wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'\n" ); |
||
1333 | |||
1334 | return true; |
||
1335 | } |
||
1336 | } elseif ( $str != '' && preg_match( "!<\?xml\b!si", $str ) ) { |
||
1337 | // Start of XML declaration without an end in the first $wgSVGMetadataCutoff |
||
1338 | // bytes. There shouldn't be a legitimate reason for this to happen. |
||
1339 | wfDebug( __METHOD__ . ": Unmatched XML declaration start\n" ); |
||
1340 | |||
1341 | return true; |
||
1342 | } |
||
1343 | } |
||
1344 | |||
1345 | return false; |
||
1346 | } |
||
1347 | |||
1348 | /** |
||
1349 | * @param string $filename |
||
1350 | * @param bool $partial |
||
1351 | * @return mixed False of the file is verified (does not contain scripts), array otherwise. |
||
1352 | */ |
||
1353 | protected function detectScriptInSvg( $filename, $partial ) { |
||
1354 | $this->mSVGNSError = false; |
||
1355 | $check = new XmlTypeCheck( |
||
1356 | $filename, |
||
1357 | [ $this, 'checkSvgScriptCallback' ], |
||
1358 | true, |
||
1359 | [ 'processing_instruction_handler' => 'UploadBase::checkSvgPICallback' ] |
||
1360 | ); |
||
1361 | if ( $check->wellFormed !== true ) { |
||
1362 | // Invalid xml (bug 58553) |
||
1363 | // But only when non-partial (bug 65724) |
||
1364 | return $partial ? false : [ 'uploadinvalidxml' ]; |
||
1365 | } elseif ( $check->filterMatch ) { |
||
1366 | if ( $this->mSVGNSError ) { |
||
1367 | return [ 'uploadscriptednamespace', $this->mSVGNSError ]; |
||
1368 | } |
||
1369 | |||
1370 | return $check->filterMatchType; |
||
1371 | } |
||
1372 | |||
1373 | return false; |
||
1374 | } |
||
1375 | |||
1376 | /** |
||
1377 | * Callback to filter SVG Processing Instructions. |
||
1378 | * @param string $target Processing instruction name |
||
1379 | * @param string $data Processing instruction attribute and value |
||
1380 | * @return bool (true if the filter identified something bad) |
||
1381 | */ |
||
1382 | public static function checkSvgPICallback( $target, $data ) { |
||
1383 | // Don't allow external stylesheets (bug 57550) |
||
1384 | if ( preg_match( '/xml-stylesheet/i', $target ) ) { |
||
1385 | return [ 'upload-scripted-pi-callback' ]; |
||
1386 | } |
||
1387 | |||
1388 | return false; |
||
1389 | } |
||
1390 | |||
1391 | /** |
||
1392 | * @todo Replace this with a whitelist filter! |
||
1393 | * @param string $element |
||
1394 | * @param array $attribs |
||
1395 | * @return bool |
||
1396 | */ |
||
1397 | public function checkSvgScriptCallback( $element, $attribs, $data = null ) { |
||
1398 | |||
1399 | list( $namespace, $strippedElement ) = $this->splitXmlNamespace( $element ); |
||
1400 | |||
1401 | // We specifically don't include: |
||
1402 | // http://www.w3.org/1999/xhtml (bug 60771) |
||
1403 | static $validNamespaces = [ |
||
1404 | '', |
||
1405 | 'adobe:ns:meta/', |
||
1406 | 'http://creativecommons.org/ns#', |
||
1407 | 'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd', |
||
1408 | 'http://ns.adobe.com/adobeillustrator/10.0/', |
||
1409 | 'http://ns.adobe.com/adobesvgviewerextensions/3.0/', |
||
1410 | 'http://ns.adobe.com/extensibility/1.0/', |
||
1411 | 'http://ns.adobe.com/flows/1.0/', |
||
1412 | 'http://ns.adobe.com/illustrator/1.0/', |
||
1413 | 'http://ns.adobe.com/imagereplacement/1.0/', |
||
1414 | 'http://ns.adobe.com/pdf/1.3/', |
||
1415 | 'http://ns.adobe.com/photoshop/1.0/', |
||
1416 | 'http://ns.adobe.com/saveforweb/1.0/', |
||
1417 | 'http://ns.adobe.com/variables/1.0/', |
||
1418 | 'http://ns.adobe.com/xap/1.0/', |
||
1419 | 'http://ns.adobe.com/xap/1.0/g/', |
||
1420 | 'http://ns.adobe.com/xap/1.0/g/img/', |
||
1421 | 'http://ns.adobe.com/xap/1.0/mm/', |
||
1422 | 'http://ns.adobe.com/xap/1.0/rights/', |
||
1423 | 'http://ns.adobe.com/xap/1.0/stype/dimensions#', |
||
1424 | 'http://ns.adobe.com/xap/1.0/stype/font#', |
||
1425 | 'http://ns.adobe.com/xap/1.0/stype/manifestitem#', |
||
1426 | 'http://ns.adobe.com/xap/1.0/stype/resourceevent#', |
||
1427 | 'http://ns.adobe.com/xap/1.0/stype/resourceref#', |
||
1428 | 'http://ns.adobe.com/xap/1.0/t/pg/', |
||
1429 | 'http://purl.org/dc/elements/1.1/', |
||
1430 | 'http://purl.org/dc/elements/1.1', |
||
1431 | 'http://schemas.microsoft.com/visio/2003/svgextensions/', |
||
1432 | 'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd', |
||
1433 | 'http://taptrix.com/inkpad/svg_extensions', |
||
1434 | 'http://web.resource.org/cc/', |
||
1435 | 'http://www.freesoftware.fsf.org/bkchem/cdml', |
||
1436 | 'http://www.inkscape.org/namespaces/inkscape', |
||
1437 | 'http://www.opengis.net/gml', |
||
1438 | 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', |
||
1439 | 'http://www.w3.org/2000/svg', |
||
1440 | 'http://www.w3.org/tr/rec-rdf-syntax/', |
||
1441 | ]; |
||
1442 | |||
1443 | // Inkscape mangles namespace definitions created by Adobe Illustrator. |
||
1444 | // This is nasty but harmless. (T144827) |
||
1445 | $isBuggyInkscape = preg_match( '/^&(#38;)*ns_[a-z_]+;$/', $namespace ); |
||
1446 | |||
1447 | if ( !( $isBuggyInkscape || in_array( $namespace, $validNamespaces ) ) ) { |
||
1448 | wfDebug( __METHOD__ . ": Non-svg namespace '$namespace' in uploaded file.\n" ); |
||
1449 | /** @todo Return a status object to a closure in XmlTypeCheck, for MW1.21+ */ |
||
1450 | $this->mSVGNSError = $namespace; |
||
1451 | |||
1452 | return true; |
||
1453 | } |
||
1454 | |||
1455 | /* |
||
1456 | * check for elements that can contain javascript |
||
1457 | */ |
||
1458 | View Code Duplication | if ( $strippedElement == 'script' ) { |
|
1459 | wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file.\n" ); |
||
1460 | |||
1461 | return [ 'uploaded-script-svg', $strippedElement ]; |
||
1462 | } |
||
1463 | |||
1464 | # e.g., <svg xmlns="http://www.w3.org/2000/svg"> |
||
1465 | # <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg> |
||
1466 | View Code Duplication | if ( $strippedElement == 'handler' ) { |
|
1467 | wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file.\n" ); |
||
1468 | |||
1469 | return [ 'uploaded-script-svg', $strippedElement ]; |
||
1470 | } |
||
1471 | |||
1472 | # SVG reported in Feb '12 that used xml:stylesheet to generate javascript block |
||
1473 | View Code Duplication | if ( $strippedElement == 'stylesheet' ) { |
|
1474 | wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file.\n" ); |
||
1475 | |||
1476 | return [ 'uploaded-script-svg', $strippedElement ]; |
||
1477 | } |
||
1478 | |||
1479 | # Block iframes, in case they pass the namespace check |
||
1480 | if ( $strippedElement == 'iframe' ) { |
||
1481 | wfDebug( __METHOD__ . ": iframe in uploaded file.\n" ); |
||
1482 | |||
1483 | return [ 'uploaded-script-svg', $strippedElement ]; |
||
1484 | } |
||
1485 | |||
1486 | # Check <style> css |
||
1487 | if ( $strippedElement == 'style' |
||
1488 | && self::checkCssFragment( Sanitizer::normalizeCss( $data ) ) |
||
1489 | ) { |
||
1490 | wfDebug( __METHOD__ . ": hostile css in style element.\n" ); |
||
1491 | return [ 'uploaded-hostile-svg' ]; |
||
1492 | } |
||
1493 | |||
1494 | foreach ( $attribs as $attrib => $value ) { |
||
1495 | $stripped = $this->stripXmlNamespace( $attrib ); |
||
1496 | $value = strtolower( $value ); |
||
1497 | |||
1498 | if ( substr( $stripped, 0, 2 ) == 'on' ) { |
||
1499 | wfDebug( __METHOD__ |
||
1500 | . ": Found event-handler attribute '$attrib'='$value' in uploaded file.\n" ); |
||
1501 | |||
1502 | return [ 'uploaded-event-handler-on-svg', $attrib, $value ]; |
||
1503 | } |
||
1504 | |||
1505 | # Do not allow relative links, or unsafe url schemas. |
||
1506 | # For <a> tags, only data:, http: and https: and same-document |
||
1507 | # fragment links are allowed. For all other tags, only data: |
||
1508 | # and fragment are allowed. |
||
1509 | if ( $stripped == 'href' |
||
1510 | && $value !== '' |
||
1511 | && strpos( $value, 'data:' ) !== 0 |
||
1512 | && strpos( $value, '#' ) !== 0 |
||
1513 | ) { |
||
1514 | View Code Duplication | if ( !( $strippedElement === 'a' |
|
1515 | && preg_match( '!^https?://!i', $value ) ) |
||
1516 | ) { |
||
1517 | wfDebug( __METHOD__ . ": Found href attribute <$strippedElement " |
||
1518 | . "'$attrib'='$value' in uploaded file.\n" ); |
||
1519 | |||
1520 | return [ 'uploaded-href-attribute-svg', $strippedElement, $attrib, $value ]; |
||
1521 | } |
||
1522 | } |
||
1523 | |||
1524 | # only allow data: targets that should be safe. This prevents vectors like, |
||
1525 | # image/svg, text/xml, application/xml, and text/html, which can contain scripts |
||
1526 | if ( $stripped == 'href' && strncasecmp( 'data:', $value, 5 ) === 0 ) { |
||
1527 | // rfc2397 parameters. This is only slightly slower than (;[\w;]+)*. |
||
1528 | // @codingStandardsIgnoreStart Generic.Files.LineLength |
||
1529 | $parameters = '(?>;[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+|"(?>[\0-\x0c\x0e-\x21\x23-\x5b\x5d-\x7f]+|\\\\[\0-\x7f])*"))*(?:;base64)?'; |
||
1530 | // @codingStandardsIgnoreEnd |
||
1531 | |||
1532 | View Code Duplication | if ( !preg_match( "!^data:\s*image/(gif|jpeg|jpg|png)$parameters,!i", $value ) ) { |
|
1533 | wfDebug( __METHOD__ . ": Found href to unwhitelisted data: uri " |
||
1534 | . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" ); |
||
1535 | return [ 'uploaded-href-unsafe-target-svg', $strippedElement, $attrib, $value ]; |
||
1536 | } |
||
1537 | } |
||
1538 | |||
1539 | # Change href with animate from (http://html5sec.org/#137). |
||
1540 | if ( $stripped === 'attributename' |
||
1541 | && $strippedElement === 'animate' |
||
1542 | && $this->stripXmlNamespace( $value ) == 'href' |
||
1543 | ) { |
||
1544 | wfDebug( __METHOD__ . ": Found animate that might be changing href using from " |
||
1545 | . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" ); |
||
1546 | |||
1547 | return [ 'uploaded-animate-svg', $strippedElement, $attrib, $value ]; |
||
1548 | } |
||
1549 | |||
1550 | # use set/animate to add event-handler attribute to parent |
||
1551 | if ( ( $strippedElement == 'set' || $strippedElement == 'animate' ) |
||
1552 | && $stripped == 'attributename' |
||
1553 | && substr( $value, 0, 2 ) == 'on' |
||
1554 | ) { |
||
1555 | wfDebug( __METHOD__ . ": Found svg setting event-handler attribute with " |
||
1556 | . "\"<$strippedElement $stripped='$value'...\" in uploaded file.\n" ); |
||
1557 | |||
1558 | return [ 'uploaded-setting-event-handler-svg', $strippedElement, $stripped, $value ]; |
||
1559 | } |
||
1560 | |||
1561 | # use set to add href attribute to parent element |
||
1562 | if ( $strippedElement == 'set' |
||
1563 | && $stripped == 'attributename' |
||
1564 | && strpos( $value, 'href' ) !== false |
||
1565 | ) { |
||
1566 | wfDebug( __METHOD__ . ": Found svg setting href attribute '$value' in uploaded file.\n" ); |
||
1567 | |||
1568 | return [ 'uploaded-setting-href-svg' ]; |
||
1569 | } |
||
1570 | |||
1571 | # use set to add a remote / data / script target to an element |
||
1572 | if ( $strippedElement == 'set' |
||
1573 | && $stripped == 'to' |
||
1574 | && preg_match( '!(http|https|data|script):!sim', $value ) |
||
1575 | ) { |
||
1576 | wfDebug( __METHOD__ . ": Found svg setting attribute to '$value' in uploaded file.\n" ); |
||
1577 | |||
1578 | return [ 'uploaded-wrong-setting-svg', $value ]; |
||
1579 | } |
||
1580 | |||
1581 | # use handler attribute with remote / data / script |
||
1582 | if ( $stripped == 'handler' && preg_match( '!(http|https|data|script):!sim', $value ) ) { |
||
1583 | wfDebug( __METHOD__ . ": Found svg setting handler with remote/data/script " |
||
1584 | . "'$attrib'='$value' in uploaded file.\n" ); |
||
1585 | |||
1586 | return [ 'uploaded-setting-handler-svg', $attrib, $value ]; |
||
1587 | } |
||
1588 | |||
1589 | # use CSS styles to bring in remote code |
||
1590 | if ( $stripped == 'style' |
||
1591 | && self::checkCssFragment( Sanitizer::normalizeCss( $value ) ) |
||
1592 | ) { |
||
1593 | wfDebug( __METHOD__ . ": Found svg setting a style with " |
||
1594 | . "remote url '$attrib'='$value' in uploaded file.\n" ); |
||
1595 | return [ 'uploaded-remote-url-svg', $attrib, $value ]; |
||
1596 | } |
||
1597 | |||
1598 | # Several attributes can include css, css character escaping isn't allowed |
||
1599 | $cssAttrs = [ 'font', 'clip-path', 'fill', 'filter', 'marker', |
||
1600 | 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke' ]; |
||
1601 | if ( in_array( $stripped, $cssAttrs ) |
||
1602 | && self::checkCssFragment( $value ) |
||
1603 | ) { |
||
1604 | wfDebug( __METHOD__ . ": Found svg setting a style with " |
||
1605 | . "remote url '$attrib'='$value' in uploaded file.\n" ); |
||
1606 | return [ 'uploaded-remote-url-svg', $attrib, $value ]; |
||
1607 | } |
||
1608 | |||
1609 | # image filters can pull in url, which could be svg that executes scripts |
||
1610 | if ( $strippedElement == 'image' |
||
1611 | && $stripped == 'filter' |
||
1612 | && preg_match( '!url\s*\(!sim', $value ) |
||
1613 | ) { |
||
1614 | wfDebug( __METHOD__ . ": Found image filter with url: " |
||
1615 | . "\"<$strippedElement $stripped='$value'...\" in uploaded file.\n" ); |
||
1616 | |||
1617 | return [ 'uploaded-image-filter-svg', $strippedElement, $stripped, $value ]; |
||
1618 | } |
||
1619 | } |
||
1620 | |||
1621 | return false; // No scripts detected |
||
1622 | } |
||
1623 | |||
1624 | /** |
||
1625 | * Check a block of CSS or CSS fragment for anything that looks like |
||
1626 | * it is bringing in remote code. |
||
1627 | * @param string $value a string of CSS |
||
1628 | * @param bool $propOnly only check css properties (start regex with :) |
||
0 ignored issues
–
show
There is no parameter named
$propOnly . Was it maybe removed?
This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. Consider the following example. The parameter /**
* @param array $germany
* @param array $island
* @param array $italy
*/
function finale($germany, $island) {
return "2:1";
}
The most likely cause is that the parameter was removed, but the annotation was not. ![]() |
|||
1629 | * @return bool true if the CSS contains an illegal string, false if otherwise |
||
1630 | */ |
||
1631 | private static function checkCssFragment( $value ) { |
||
1632 | |||
1633 | # Forbid external stylesheets, for both reliability and to protect viewer's privacy |
||
1634 | if ( stripos( $value, '@import' ) !== false ) { |
||
1635 | return true; |
||
1636 | } |
||
1637 | |||
1638 | # We allow @font-face to embed fonts with data: urls, so we snip the string |
||
1639 | # 'url' out so this case won't match when we check for urls below |
||
1640 | $pattern = '!(@font-face\s*{[^}]*src:)url(\("data:;base64,)!im'; |
||
1641 | $value = preg_replace( $pattern, '$1$2', $value ); |
||
1642 | |||
1643 | # Check for remote and executable CSS. Unlike in Sanitizer::checkCss, the CSS |
||
1644 | # properties filter and accelerator don't seem to be useful for xss in SVG files. |
||
1645 | # Expression and -o-link don't seem to work either, but filtering them here in case. |
||
1646 | # Additionally, we catch remote urls like url("http:..., url('http:..., url(http:..., |
||
1647 | # but not local ones such as url("#..., url('#..., url(#.... |
||
1648 | if ( preg_match( '!expression |
||
1649 | | -o-link\s*: |
||
1650 | | -o-link-source\s*: |
||
1651 | | -o-replace\s*:!imx', $value ) ) { |
||
1652 | return true; |
||
1653 | } |
||
1654 | |||
1655 | if ( preg_match_all( |
||
1656 | "!(\s*(url|image|image-set)\s*\(\s*[\"']?\s*[^#]+.*?\))!sim", |
||
1657 | $value, |
||
1658 | $matches |
||
1659 | ) !== 0 |
||
1660 | ) { |
||
1661 | # TODO: redo this in one regex. Until then, url("#whatever") matches the first |
||
1662 | foreach ( $matches[1] as $match ) { |
||
1663 | if ( !preg_match( "!\s*(url|image|image-set)\s*\(\s*(#|'#|\"#)!im", $match ) ) { |
||
1664 | return true; |
||
1665 | } |
||
1666 | } |
||
1667 | } |
||
1668 | |||
1669 | if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ) { |
||
1670 | return true; |
||
1671 | } |
||
1672 | |||
1673 | return false; |
||
1674 | } |
||
1675 | |||
1676 | /** |
||
1677 | * Divide the element name passed by the xml parser to the callback into URI and prifix. |
||
1678 | * @param string $element |
||
1679 | * @return array Containing the namespace URI and prefix |
||
1680 | */ |
||
1681 | private static function splitXmlNamespace( $element ) { |
||
1682 | // 'http://www.w3.org/2000/svg:script' -> [ 'http://www.w3.org/2000/svg', 'script' ] |
||
1683 | $parts = explode( ':', strtolower( $element ) ); |
||
1684 | $name = array_pop( $parts ); |
||
1685 | $ns = implode( ':', $parts ); |
||
1686 | |||
1687 | return [ $ns, $name ]; |
||
1688 | } |
||
1689 | |||
1690 | /** |
||
1691 | * @param string $name |
||
1692 | * @return string |
||
1693 | */ |
||
1694 | private function stripXmlNamespace( $name ) { |
||
1695 | // 'http://www.w3.org/2000/svg:script' -> 'script' |
||
1696 | $parts = explode( ':', strtolower( $name ) ); |
||
1697 | |||
1698 | return array_pop( $parts ); |
||
1699 | } |
||
1700 | |||
1701 | /** |
||
1702 | * Generic wrapper function for a virus scanner program. |
||
1703 | * This relies on the $wgAntivirus and $wgAntivirusSetup variables. |
||
1704 | * $wgAntivirusRequired may be used to deny upload if the scan fails. |
||
1705 | * |
||
1706 | * @param string $file Pathname to the temporary upload file |
||
1707 | * @return mixed False if not virus is found, null if the scan fails or is disabled, |
||
1708 | * or a string containing feedback from the virus scanner if a virus was found. |
||
1709 | * If textual feedback is missing but a virus was found, this function returns true. |
||
1710 | */ |
||
1711 | public static function detectVirus( $file ) { |
||
1712 | global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut; |
||
1713 | |||
1714 | if ( !$wgAntivirus ) { |
||
1715 | wfDebug( __METHOD__ . ": virus scanner disabled\n" ); |
||
1716 | |||
1717 | return null; |
||
1718 | } |
||
1719 | |||
1720 | if ( !$wgAntivirusSetup[$wgAntivirus] ) { |
||
1721 | wfDebug( __METHOD__ . ": unknown virus scanner: $wgAntivirus\n" ); |
||
1722 | $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>", |
||
1723 | [ 'virus-badscanner', $wgAntivirus ] ); |
||
1724 | |||
1725 | return wfMessage( 'virus-unknownscanner' )->text() . " $wgAntivirus"; |
||
1726 | } |
||
1727 | |||
1728 | # look up scanner configuration |
||
1729 | $command = $wgAntivirusSetup[$wgAntivirus]['command']; |
||
1730 | $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]['codemap']; |
||
1731 | $msgPattern = isset( $wgAntivirusSetup[$wgAntivirus]['messagepattern'] ) ? |
||
1732 | $wgAntivirusSetup[$wgAntivirus]['messagepattern'] : null; |
||
1733 | |||
1734 | if ( strpos( $command, "%f" ) === false ) { |
||
1735 | # simple pattern: append file to scan |
||
1736 | $command .= " " . wfEscapeShellArg( $file ); |
||
1737 | } else { |
||
1738 | # complex pattern: replace "%f" with file to scan |
||
1739 | $command = str_replace( "%f", wfEscapeShellArg( $file ), $command ); |
||
1740 | } |
||
1741 | |||
1742 | wfDebug( __METHOD__ . ": running virus scan: $command \n" ); |
||
1743 | |||
1744 | # execute virus scanner |
||
1745 | $exitCode = false; |
||
1746 | |||
1747 | # NOTE: there's a 50 line workaround to make stderr redirection work on windows, too. |
||
1748 | # that does not seem to be worth the pain. |
||
1749 | # Ask me (Duesentrieb) about it if it's ever needed. |
||
1750 | $output = wfShellExecWithStderr( $command, $exitCode ); |
||
1751 | |||
1752 | # map exit code to AV_xxx constants. |
||
1753 | $mappedCode = $exitCode; |
||
1754 | if ( $exitCodeMap ) { |
||
1755 | if ( isset( $exitCodeMap[$exitCode] ) ) { |
||
1756 | $mappedCode = $exitCodeMap[$exitCode]; |
||
1757 | } elseif ( isset( $exitCodeMap["*"] ) ) { |
||
1758 | $mappedCode = $exitCodeMap["*"]; |
||
1759 | } |
||
1760 | } |
||
1761 | |||
1762 | /* NB: AV_NO_VIRUS is 0 but AV_SCAN_FAILED is false, |
||
1763 | * so we need the strict equalities === and thus can't use a switch here |
||
1764 | */ |
||
1765 | if ( $mappedCode === AV_SCAN_FAILED ) { |
||
1766 | # scan failed (code was mapped to false by $exitCodeMap) |
||
1767 | wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode).\n" ); |
||
1768 | |||
1769 | $output = $wgAntivirusRequired |
||
1770 | ? wfMessage( 'virus-scanfailed', [ $exitCode ] )->text() |
||
1771 | : null; |
||
1772 | } elseif ( $mappedCode === AV_SCAN_ABORTED ) { |
||
1773 | # scan failed because filetype is unknown (probably imune) |
||
1774 | wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode).\n" ); |
||
1775 | $output = null; |
||
1776 | } elseif ( $mappedCode === AV_NO_VIRUS ) { |
||
1777 | # no virus found |
||
1778 | wfDebug( __METHOD__ . ": file passed virus scan.\n" ); |
||
1779 | $output = false; |
||
1780 | } else { |
||
1781 | $output = trim( $output ); |
||
1782 | |||
1783 | if ( !$output ) { |
||
1784 | $output = true; # if there's no output, return true |
||
1785 | } elseif ( $msgPattern ) { |
||
1786 | $groups = []; |
||
1787 | if ( preg_match( $msgPattern, $output, $groups ) ) { |
||
1788 | if ( $groups[1] ) { |
||
1789 | $output = $groups[1]; |
||
1790 | } |
||
1791 | } |
||
1792 | } |
||
1793 | |||
1794 | wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output \n" ); |
||
1795 | } |
||
1796 | |||
1797 | return $output; |
||
1798 | } |
||
1799 | |||
1800 | /** |
||
1801 | * Check if there's an overwrite conflict and, if so, if restrictions |
||
1802 | * forbid this user from performing the upload. |
||
1803 | * |
||
1804 | * @param User $user |
||
1805 | * |
||
1806 | * @return mixed True on success, array on failure |
||
1807 | */ |
||
1808 | private function checkOverwrite( $user ) { |
||
1809 | // First check whether the local file can be overwritten |
||
1810 | $file = $this->getLocalFile(); |
||
1811 | $file->load( File::READ_LATEST ); |
||
1812 | if ( $file->exists() ) { |
||
1813 | if ( !self::userCanReUpload( $user, $file ) ) { |
||
0 ignored issues
–
show
It seems like
$file defined by $this->getLocalFile() on line 1810 can be null ; however, UploadBase::userCanReUpload() does not accept null , maybe add an additional type check?
Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code: /** @return stdClass|null */
function mayReturnNull() { }
function doesNotAcceptNull(stdClass $x) { }
// With potential error.
function withoutCheck() {
$x = mayReturnNull();
doesNotAcceptNull($x); // Potential error here.
}
// Safe - Alternative 1
function withCheck1() {
$x = mayReturnNull();
if ( ! $x instanceof stdClass) {
throw new \LogicException('$x must be defined.');
}
doesNotAcceptNull($x);
}
// Safe - Alternative 2
function withCheck2() {
$x = mayReturnNull();
if ($x instanceof stdClass) {
doesNotAcceptNull($x);
}
}
![]() |
|||
1814 | return [ 'fileexists-forbidden', $file->getName() ]; |
||
1815 | } else { |
||
1816 | return true; |
||
1817 | } |
||
1818 | } |
||
1819 | |||
1820 | /* Check shared conflicts: if the local file does not exist, but |
||
1821 | * wfFindFile finds a file, it exists in a shared repository. |
||
1822 | */ |
||
1823 | $file = wfFindFile( $this->getTitle(), [ 'latest' => true ] ); |
||
1824 | if ( $file && !$user->isAllowed( 'reupload-shared' ) ) { |
||
1825 | return [ 'fileexists-shared-forbidden', $file->getName() ]; |
||
1826 | } |
||
1827 | |||
1828 | return true; |
||
1829 | } |
||
1830 | |||
1831 | /** |
||
1832 | * Check if a user is the last uploader |
||
1833 | * |
||
1834 | * @param User $user |
||
1835 | * @param File $img |
||
1836 | * @return bool |
||
1837 | */ |
||
1838 | public static function userCanReUpload( User $user, File $img ) { |
||
1839 | if ( $user->isAllowed( 'reupload' ) ) { |
||
1840 | return true; // non-conditional |
||
1841 | } elseif ( !$user->isAllowed( 'reupload-own' ) ) { |
||
1842 | return false; |
||
1843 | } |
||
1844 | |||
1845 | if ( !( $img instanceof LocalFile ) ) { |
||
1846 | return false; |
||
1847 | } |
||
1848 | |||
1849 | $img->load(); |
||
1850 | |||
1851 | return $user->getId() == $img->getUser( 'id' ); |
||
1852 | } |
||
1853 | |||
1854 | /** |
||
1855 | * Helper function that does various existence checks for a file. |
||
1856 | * The following checks are performed: |
||
1857 | * - The file exists |
||
1858 | * - Article with the same name as the file exists |
||
1859 | * - File exists with normalized extension |
||
1860 | * - The file looks like a thumbnail and the original exists |
||
1861 | * |
||
1862 | * @param File $file The File object to check |
||
1863 | * @return mixed False if the file does not exists, else an array |
||
1864 | */ |
||
1865 | public static function getExistsWarning( $file ) { |
||
1866 | if ( $file->exists() ) { |
||
1867 | return [ 'warning' => 'exists', 'file' => $file ]; |
||
1868 | } |
||
1869 | |||
1870 | if ( $file->getTitle()->getArticleID() ) { |
||
1871 | return [ 'warning' => 'page-exists', 'file' => $file ]; |
||
1872 | } |
||
1873 | |||
1874 | if ( strpos( $file->getName(), '.' ) == false ) { |
||
0 ignored issues
–
show
|
|||
1875 | $partname = $file->getName(); |
||
1876 | $extension = ''; |
||
1877 | } else { |
||
1878 | $n = strrpos( $file->getName(), '.' ); |
||
1879 | $extension = substr( $file->getName(), $n + 1 ); |
||
1880 | $partname = substr( $file->getName(), 0, $n ); |
||
1881 | } |
||
1882 | $normalizedExtension = File::normalizeExtension( $extension ); |
||
1883 | |||
1884 | if ( $normalizedExtension != $extension ) { |
||
1885 | // We're not using the normalized form of the extension. |
||
1886 | // Normal form is lowercase, using most common of alternate |
||
1887 | // extensions (eg 'jpg' rather than 'JPEG'). |
||
1888 | |||
1889 | // Check for another file using the normalized form... |
||
1890 | $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" ); |
||
1891 | $file_lc = wfLocalFile( $nt_lc ); |
||
1892 | |||
1893 | if ( $file_lc->exists() ) { |
||
1894 | return [ |
||
1895 | 'warning' => 'exists-normalized', |
||
1896 | 'file' => $file, |
||
1897 | 'normalizedFile' => $file_lc |
||
1898 | ]; |
||
1899 | } |
||
1900 | } |
||
1901 | |||
1902 | // Check for files with the same name but a different extension |
||
1903 | $similarFiles = RepoGroup::singleton()->getLocalRepo()->findFilesByPrefix( |
||
1904 | "{$partname}.", 1 ); |
||
1905 | if ( count( $similarFiles ) ) { |
||
1906 | return [ |
||
1907 | 'warning' => 'exists-normalized', |
||
1908 | 'file' => $file, |
||
1909 | 'normalizedFile' => $similarFiles[0], |
||
1910 | ]; |
||
1911 | } |
||
1912 | |||
1913 | if ( self::isThumbName( $file->getName() ) ) { |
||
1914 | # Check for filenames like 50px- or 180px-, these are mostly thumbnails |
||
1915 | $nt_thb = Title::newFromText( |
||
1916 | substr( $partname, strpos( $partname, '-' ) + 1 ) . '.' . $extension, |
||
1917 | NS_FILE |
||
1918 | ); |
||
1919 | $file_thb = wfLocalFile( $nt_thb ); |
||
0 ignored issues
–
show
It seems like
$nt_thb defined by \Title::newFromText(subs... . $extension, NS_FILE) on line 1915 can be null ; however, wfLocalFile() does not accept null , maybe add an additional type check?
Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code: /** @return stdClass|null */
function mayReturnNull() { }
function doesNotAcceptNull(stdClass $x) { }
// With potential error.
function withoutCheck() {
$x = mayReturnNull();
doesNotAcceptNull($x); // Potential error here.
}
// Safe - Alternative 1
function withCheck1() {
$x = mayReturnNull();
if ( ! $x instanceof stdClass) {
throw new \LogicException('$x must be defined.');
}
doesNotAcceptNull($x);
}
// Safe - Alternative 2
function withCheck2() {
$x = mayReturnNull();
if ($x instanceof stdClass) {
doesNotAcceptNull($x);
}
}
![]() |
|||
1920 | if ( $file_thb->exists() ) { |
||
1921 | return [ |
||
1922 | 'warning' => 'thumb', |
||
1923 | 'file' => $file, |
||
1924 | 'thumbFile' => $file_thb |
||
1925 | ]; |
||
1926 | } else { |
||
1927 | // File does not exist, but we just don't like the name |
||
1928 | return [ |
||
1929 | 'warning' => 'thumb-name', |
||
1930 | 'file' => $file, |
||
1931 | 'thumbFile' => $file_thb |
||
1932 | ]; |
||
1933 | } |
||
1934 | } |
||
1935 | |||
1936 | foreach ( self::getFilenamePrefixBlacklist() as $prefix ) { |
||
1937 | if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) { |
||
1938 | return [ |
||
1939 | 'warning' => 'bad-prefix', |
||
1940 | 'file' => $file, |
||
1941 | 'prefix' => $prefix |
||
1942 | ]; |
||
1943 | } |
||
1944 | } |
||
1945 | |||
1946 | return false; |
||
1947 | } |
||
1948 | |||
1949 | /** |
||
1950 | * Helper function that checks whether the filename looks like a thumbnail |
||
1951 | * @param string $filename |
||
1952 | * @return bool |
||
1953 | */ |
||
1954 | public static function isThumbName( $filename ) { |
||
1955 | $n = strrpos( $filename, '.' ); |
||
1956 | $partname = $n ? substr( $filename, 0, $n ) : $filename; |
||
1957 | |||
1958 | return ( |
||
1959 | substr( $partname, 3, 3 ) == 'px-' || |
||
1960 | substr( $partname, 2, 3 ) == 'px-' |
||
1961 | ) && |
||
1962 | preg_match( "/[0-9]{2}/", substr( $partname, 0, 2 ) ); |
||
1963 | } |
||
1964 | |||
1965 | /** |
||
1966 | * Get a list of blacklisted filename prefixes from [[MediaWiki:Filename-prefix-blacklist]] |
||
1967 | * |
||
1968 | * @return array List of prefixes |
||
1969 | */ |
||
1970 | public static function getFilenamePrefixBlacklist() { |
||
1971 | $blacklist = []; |
||
1972 | $message = wfMessage( 'filename-prefix-blacklist' )->inContentLanguage(); |
||
1973 | if ( !$message->isDisabled() ) { |
||
1974 | $lines = explode( "\n", $message->plain() ); |
||
1975 | foreach ( $lines as $line ) { |
||
1976 | // Remove comment lines |
||
1977 | $comment = substr( trim( $line ), 0, 1 ); |
||
1978 | if ( $comment == '#' || $comment == '' ) { |
||
1979 | continue; |
||
1980 | } |
||
1981 | // Remove additional comments after a prefix |
||
1982 | $comment = strpos( $line, '#' ); |
||
1983 | if ( $comment > 0 ) { |
||
1984 | $line = substr( $line, 0, $comment - 1 ); |
||
1985 | } |
||
1986 | $blacklist[] = trim( $line ); |
||
1987 | } |
||
1988 | } |
||
1989 | |||
1990 | return $blacklist; |
||
1991 | } |
||
1992 | |||
1993 | /** |
||
1994 | * Gets image info about the file just uploaded. |
||
1995 | * |
||
1996 | * Also has the effect of setting metadata to be an 'indexed tag name' in |
||
1997 | * returned API result if 'metadata' was requested. Oddly, we have to pass |
||
1998 | * the "result" object down just so it can do that with the appropriate |
||
1999 | * format, presumably. |
||
2000 | * |
||
2001 | * @param ApiResult $result |
||
2002 | * @return array Image info |
||
2003 | */ |
||
2004 | public function getImageInfo( $result ) { |
||
2005 | $localFile = $this->getLocalFile(); |
||
2006 | $stashFile = $this->getStashFile(); |
||
2007 | // Calling a different API module depending on whether the file was stashed is less than optimal. |
||
2008 | // In fact, calling API modules here at all is less than optimal. Maybe it should be refactored. |
||
2009 | if ( $stashFile ) { |
||
2010 | $imParam = ApiQueryStashImageInfo::getPropertyNames(); |
||
2011 | $info = ApiQueryStashImageInfo::getInfo( $stashFile, array_flip( $imParam ), $result ); |
||
2012 | } else { |
||
2013 | $imParam = ApiQueryImageInfo::getPropertyNames(); |
||
2014 | $info = ApiQueryImageInfo::getInfo( $localFile, array_flip( $imParam ), $result ); |
||
0 ignored issues
–
show
It seems like
$localFile defined by $this->getLocalFile() on line 2005 can be null ; however, ApiQueryImageInfo::getInfo() does not accept null , maybe add an additional type check?
Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code: /** @return stdClass|null */
function mayReturnNull() { }
function doesNotAcceptNull(stdClass $x) { }
// With potential error.
function withoutCheck() {
$x = mayReturnNull();
doesNotAcceptNull($x); // Potential error here.
}
// Safe - Alternative 1
function withCheck1() {
$x = mayReturnNull();
if ( ! $x instanceof stdClass) {
throw new \LogicException('$x must be defined.');
}
doesNotAcceptNull($x);
}
// Safe - Alternative 2
function withCheck2() {
$x = mayReturnNull();
if ($x instanceof stdClass) {
doesNotAcceptNull($x);
}
}
![]() |
|||
2015 | } |
||
2016 | |||
2017 | return $info; |
||
2018 | } |
||
2019 | |||
2020 | /** |
||
2021 | * @param array $error |
||
2022 | * @return Status |
||
2023 | */ |
||
2024 | public function convertVerifyErrorToStatus( $error ) { |
||
2025 | $code = $error['status']; |
||
2026 | unset( $code['status'] ); |
||
2027 | |||
2028 | return Status::newFatal( $this->getVerificationErrorCode( $code ), $error ); |
||
2029 | } |
||
2030 | |||
2031 | /** |
||
2032 | * Get the MediaWiki maximum uploaded file size for given type of upload, based on |
||
2033 | * $wgMaxUploadSize. |
||
2034 | * |
||
2035 | * @param null|string $forType |
||
2036 | * @return int |
||
2037 | */ |
||
2038 | public static function getMaxUploadSize( $forType = null ) { |
||
2039 | global $wgMaxUploadSize; |
||
2040 | |||
2041 | if ( is_array( $wgMaxUploadSize ) ) { |
||
2042 | if ( !is_null( $forType ) && isset( $wgMaxUploadSize[$forType] ) ) { |
||
2043 | return $wgMaxUploadSize[$forType]; |
||
2044 | } else { |
||
2045 | return $wgMaxUploadSize['*']; |
||
2046 | } |
||
2047 | } else { |
||
2048 | return intval( $wgMaxUploadSize ); |
||
2049 | } |
||
2050 | } |
||
2051 | |||
2052 | /** |
||
2053 | * Get the PHP maximum uploaded file size, based on ini settings. If there is no limit or the |
||
2054 | * limit can't be guessed, returns a very large number (PHP_INT_MAX). |
||
2055 | * |
||
2056 | * @since 1.27 |
||
2057 | * @return int |
||
2058 | */ |
||
2059 | public static function getMaxPhpUploadSize() { |
||
2060 | $phpMaxFileSize = wfShorthandToInteger( |
||
2061 | ini_get( 'upload_max_filesize' ) ?: ini_get( 'hhvm.server.upload.upload_max_file_size' ), |
||
2062 | PHP_INT_MAX |
||
2063 | ); |
||
2064 | $phpMaxPostSize = wfShorthandToInteger( |
||
2065 | ini_get( 'post_max_size' ) ?: ini_get( 'hhvm.server.max_post_size' ), |
||
2066 | PHP_INT_MAX |
||
2067 | ) ?: PHP_INT_MAX; |
||
2068 | return min( $phpMaxFileSize, $phpMaxPostSize ); |
||
2069 | } |
||
2070 | |||
2071 | /** |
||
2072 | * Get the current status of a chunked upload (used for polling) |
||
2073 | * |
||
2074 | * The value will be read from cache. |
||
2075 | * |
||
2076 | * @param User $user |
||
2077 | * @param string $statusKey |
||
2078 | * @return Status[]|bool |
||
2079 | */ |
||
2080 | public static function getSessionStatus( User $user, $statusKey ) { |
||
2081 | $key = wfMemcKey( 'uploadstatus', $user->getId() ?: md5( $user->getName() ), $statusKey ); |
||
2082 | |||
2083 | return ObjectCache::getMainStashInstance()->get( $key ); |
||
2084 | } |
||
2085 | |||
2086 | /** |
||
2087 | * Set the current status of a chunked upload (used for polling) |
||
2088 | * |
||
2089 | * The value will be set in cache for 1 day |
||
2090 | * |
||
2091 | * @param User $user |
||
2092 | * @param string $statusKey |
||
2093 | * @param array|bool $value |
||
2094 | * @return void |
||
2095 | */ |
||
2096 | public static function setSessionStatus( User $user, $statusKey, $value ) { |
||
2097 | $key = wfMemcKey( 'uploadstatus', $user->getId() ?: md5( $user->getName() ), $statusKey ); |
||
2098 | |||
2099 | $cache = ObjectCache::getMainStashInstance(); |
||
2100 | if ( $value === false ) { |
||
2101 | $cache->delete( $key ); |
||
2102 | } else { |
||
2103 | $cache->set( $key, $value, $cache::TTL_DAY ); |
||
2104 | } |
||
2105 | } |
||
2106 | } |
||
2107 |
Only declaring a single property per statement allows you to later on add doc comments more easily.
It is also recommended by PSR2, so it is a common style that many people expect.