Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/upload/UploadBase.php (17 issues)

Upgrade to new PHP Analysis Engine

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
It is generally advisable to only define one property per statement.

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.

Loading history...
45
	protected $mTitle = false, $mTitleError = 0;
0 ignored issues
show
It is generally advisable to only define one property per statement.

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.

Loading history...
46
	protected $mFilteredName, $mFinalExtension;
0 ignored issues
show
It is generally advisable to only define one property per statement.

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.

Loading history...
47
	protected $mLocalFile, $mStashFile, $mFileSize, $mFileProps;
0 ignored issues
show
It is generally advisable to only define one property per statement.

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.

Loading history...
48
	protected $mBlackListedExtensions;
49
	protected $mJavaDetected, $mSVGNSError;
0 ignored issues
show
It is generally advisable to only define one property per statement.

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.

Loading history...
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
Bug Best Practice introduced by
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 ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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
Bug Best Practice introduced by
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 ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

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
Loading history...
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
It seems like you are loosely comparing $this->mFileSize of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
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);
    }
}
Loading history...
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
Bug Best Practice introduced by
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 ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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
Documentation Bug introduced by
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 $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

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;
}
Loading history...
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
Bug Best Practice introduced by
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 ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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 $italy is not defined by the method finale(...).

/**
 * @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.

Loading history...
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);
    }
}
Loading history...
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
Bug Best Practice introduced by
It seems like you are loosely comparing strpos($file->getName(), '.') of type integer to the boolean false. If you are specifically checking for 0, consider using something more explicit like === 0 instead.
Loading history...
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);
    }
}
Loading history...
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);
    }
}
Loading history...
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