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/specials/SpecialUpload.php (7 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
 * Implements Special: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 SpecialPage
22
 * @ingroup Upload
23
 */
24
25
use MediaWiki\Linker\LinkRenderer;
26
use MediaWiki\MediaWikiServices;
27
28
/**
29
 * Form for handling uploads and special page.
30
 *
31
 * @ingroup SpecialPage
32
 * @ingroup Upload
33
 */
34
class SpecialUpload extends SpecialPage {
35
	/**
36
	 * Constructor : initialise object
37
	 * Get data POSTed through the form and assign them to the object
38
	 * @param WebRequest $request Data posted.
39
	 */
40
	public function __construct( $request = null ) {
41
		parent::__construct( 'Upload', 'upload' );
42
	}
43
44
	public function doesWrites() {
45
		return true;
46
	}
47
48
	/** Misc variables **/
49
50
	/** @var WebRequest|FauxRequest The request this form is supposed to handle */
51
	public $mRequest;
52
	public $mSourceType;
53
54
	/** @var UploadBase */
55
	public $mUpload;
56
57
	/** @var LocalFile */
58
	public $mLocalFile;
59
	public $mUploadClicked;
60
61
	/** User input variables from the "description" section **/
62
63
	/** @var string The requested target file name */
64
	public $mDesiredDestName;
65
	public $mComment;
66
	public $mLicense;
67
68
	/** User input variables from the root section **/
69
70
	public $mIgnoreWarning;
71
	public $mWatchthis;
72
	public $mCopyrightStatus;
73
	public $mCopyrightSource;
74
75
	/** Hidden variables **/
76
77
	public $mDestWarningAck;
78
79
	/** @var bool The user followed an "overwrite this file" link */
80
	public $mForReUpload;
81
82
	/** @var bool The user clicked "Cancel and return to upload form" button */
83
	public $mCancelUpload;
84
	public $mTokenOk;
85
86
	/** @var bool Subclasses can use this to determine whether a file was uploaded */
87
	public $mUploadSuccessful = false;
88
89
	/** Text injection points for hooks not using HTMLForm **/
90
	public $uploadFormTextTop;
91
	public $uploadFormTextAfterSummary;
92
93
	/**
94
	 * Initialize instance variables from request and create an Upload handler
95
	 */
96
	protected function loadRequest() {
97
		$this->mRequest = $request = $this->getRequest();
98
		$this->mSourceType = $request->getVal( 'wpSourceType', 'file' );
99
		$this->mUpload = UploadBase::createFromRequest( $request );
100
		$this->mUploadClicked = $request->wasPosted()
101
			&& ( $request->getCheck( 'wpUpload' )
102
				|| $request->getCheck( 'wpUploadIgnoreWarning' ) );
103
104
		// Guess the desired name from the filename if not provided
105
		$this->mDesiredDestName = $request->getText( 'wpDestFile' );
106
		if ( !$this->mDesiredDestName && $request->getFileName( 'wpUploadFile' ) !== null ) {
107
			$this->mDesiredDestName = $request->getFileName( 'wpUploadFile' );
108
		}
109
		$this->mLicense = $request->getText( 'wpLicense' );
110
111
		$this->mDestWarningAck = $request->getText( 'wpDestFileWarningAck' );
112
		$this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' )
113
			|| $request->getCheck( 'wpUploadIgnoreWarning' );
114
		$this->mWatchthis = $request->getBool( 'wpWatchthis' ) && $this->getUser()->isLoggedIn();
115
		$this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' );
116
		$this->mCopyrightSource = $request->getText( 'wpUploadSource' );
117
118
		$this->mForReUpload = $request->getBool( 'wpForReUpload' ); // updating a file
119
120
		$commentDefault = '';
121
		$commentMsg = wfMessage( 'upload-default-description' )->inContentLanguage();
122
		if ( !$this->mForReUpload && !$commentMsg->isDisabled() ) {
123
			$commentDefault = $commentMsg->plain();
124
		}
125
		$this->mComment = $request->getText( 'wpUploadDescription', $commentDefault );
126
127
		$this->mCancelUpload = $request->getCheck( 'wpCancelUpload' )
128
			|| $request->getCheck( 'wpReUpload' ); // b/w compat
129
130
		// If it was posted check for the token (no remote POST'ing with user credentials)
131
		$token = $request->getVal( 'wpEditToken' );
132
		$this->mTokenOk = $this->getUser()->matchEditToken( $token );
133
134
		$this->uploadFormTextTop = '';
135
		$this->uploadFormTextAfterSummary = '';
136
	}
137
138
	/**
139
	 * This page can be shown if uploading is enabled.
140
	 * Handle permission checking elsewhere in order to be able to show
141
	 * custom error messages.
142
	 *
143
	 * @param User $user
144
	 * @return bool
145
	 */
146
	public function userCanExecute( User $user ) {
147
		return UploadBase::isEnabled() && parent::userCanExecute( $user );
148
	}
149
150
	/**
151
	 * Special page entry point
152
	 * @param string $par
153
	 * @throws ErrorPageError
154
	 * @throws Exception
155
	 * @throws FatalError
156
	 * @throws MWException
157
	 * @throws PermissionsError
158
	 * @throws ReadOnlyError
159
	 * @throws UserBlockedError
160
	 */
161
	public function execute( $par ) {
162
		$this->useTransactionalTimeLimit();
163
164
		$this->setHeaders();
165
		$this->outputHeader();
166
167
		# Check uploading enabled
168
		if ( !UploadBase::isEnabled() ) {
169
			throw new ErrorPageError( 'uploaddisabled', 'uploaddisabledtext' );
170
		}
171
172
		$this->addHelpLink( 'Help:Managing files' );
173
174
		# Check permissions
175
		$user = $this->getUser();
176
		$permissionRequired = UploadBase::isAllowed( $user );
177
		if ( $permissionRequired !== true ) {
178
			throw new PermissionsError( $permissionRequired );
0 ignored issues
show
It seems like $permissionRequired defined by \UploadBase::isAllowed($user) on line 176 can also be of type boolean; however, PermissionsError::__construct() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
179
		}
180
181
		# Check blocks
182
		if ( $user->isBlocked() ) {
183
			throw new UserBlockedError( $user->getBlock() );
0 ignored issues
show
It seems like $user->getBlock() can be null; however, __construct() 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...
184
		}
185
186
		// Global blocks
187
		if ( $user->isBlockedGlobally() ) {
188
			throw new UserBlockedError( $user->getGlobalBlock() );
0 ignored issues
show
It seems like $user->getGlobalBlock() can be null; however, __construct() 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...
189
		}
190
191
		# Check whether we actually want to allow changing stuff
192
		$this->checkReadOnly();
193
194
		$this->loadRequest();
195
196
		# Unsave the temporary file in case this was a cancelled upload
197
		if ( $this->mCancelUpload ) {
198
			if ( !$this->unsaveUploadedFile() ) {
199
				# Something went wrong, so unsaveUploadedFile showed a warning
200
				return;
201
			}
202
		}
203
204
		# Process upload or show a form
205
		if (
206
			$this->mTokenOk && !$this->mCancelUpload &&
207
			( $this->mUpload && $this->mUploadClicked )
208
		) {
209
			$this->processUpload();
210
		} else {
211
			# Backwards compatibility hook
212
			if ( !Hooks::run( 'UploadForm:initial', [ &$this ] ) ) {
213
				wfDebug( "Hook 'UploadForm:initial' broke output of the upload form\n" );
214
215
				return;
216
			}
217
			$this->showUploadForm( $this->getUploadForm() );
218
		}
219
220
		# Cleanup
221
		if ( $this->mUpload ) {
222
			$this->mUpload->cleanupTempFile();
223
		}
224
	}
225
226
	/**
227
	 * Show the main upload form
228
	 *
229
	 * @param HTMLForm|string $form An HTMLForm instance or HTML string to show
230
	 */
231
	protected function showUploadForm( $form ) {
232
		# Add links if file was previously deleted
233
		if ( $this->mDesiredDestName ) {
234
			$this->showViewDeletedLinks();
235
		}
236
237
		if ( $form instanceof HTMLForm ) {
238
			$form->show();
239
		} else {
240
			$this->getOutput()->addHTML( $form );
241
		}
242
	}
243
244
	/**
245
	 * Get an UploadForm instance with title and text properly set.
246
	 *
247
	 * @param string $message HTML string to add to the form
248
	 * @param string $sessionKey Session key in case this is a stashed upload
249
	 * @param bool $hideIgnoreWarning Whether to hide "ignore warning" check box
250
	 * @return UploadForm
251
	 */
252
	protected function getUploadForm( $message = '', $sessionKey = '', $hideIgnoreWarning = false ) {
253
		# Initialize form
254
		$context = new DerivativeContext( $this->getContext() );
255
		$context->setTitle( $this->getPageTitle() ); // Remove subpage
256
		$form = new UploadForm( [
257
			'watch' => $this->getWatchCheck(),
258
			'forreupload' => $this->mForReUpload,
259
			'sessionkey' => $sessionKey,
260
			'hideignorewarning' => $hideIgnoreWarning,
261
			'destwarningack' => (bool)$this->mDestWarningAck,
262
263
			'description' => $this->mComment,
264
			'texttop' => $this->uploadFormTextTop,
265
			'textaftersummary' => $this->uploadFormTextAfterSummary,
266
			'destfile' => $this->mDesiredDestName,
267
		], $context, $this->getLinkRenderer() );
268
269
		# Check the token, but only if necessary
270
		if (
271
			!$this->mTokenOk && !$this->mCancelUpload &&
272
			( $this->mUpload && $this->mUploadClicked )
273
		) {
274
			$form->addPreText( $this->msg( 'session_fail_preview' )->parse() );
275
		}
276
277
		# Give a notice if the user is uploading a file that has been deleted or moved
278
		# Note that this is independent from the message 'filewasdeleted'
279
		$desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName );
280
		$delNotice = ''; // empty by default
281
		if ( $desiredTitleObj instanceof Title && !$desiredTitleObj->exists() ) {
282
			LogEventsList::showLogExtract( $delNotice, [ 'delete', 'move' ],
283
				$desiredTitleObj,
284
				'', [ 'lim' => 10,
285
					'conds' => [ "log_action != 'revision'" ],
286
					'showIfEmpty' => false,
287
					'msgKey' => [ 'upload-recreate-warning' ] ]
288
			);
289
		}
290
		$form->addPreText( $delNotice );
0 ignored issues
show
It seems like $delNotice can also be of type object<OutputPage>; however, HTMLForm::addPreText() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
291
292
		# Add text to form
293
		$form->addPreText( '<div id="uploadtext">' .
294
			$this->msg( 'uploadtext', [ $this->mDesiredDestName ] )->parseAsBlock() .
295
			'</div>' );
296
		# Add upload error message
297
		$form->addPreText( $message );
298
299
		# Add footer to form
300
		$uploadFooter = $this->msg( 'uploadfooter' );
301
		if ( !$uploadFooter->isDisabled() ) {
302
			$form->addPostText( '<div id="mw-upload-footer-message">'
303
				. $uploadFooter->parseAsBlock() . "</div>\n" );
304
		}
305
306
		return $form;
307
	}
308
309
	/**
310
	 * Shows the "view X deleted revivions link""
311
	 */
312
	protected function showViewDeletedLinks() {
313
		$title = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName );
314
		$user = $this->getUser();
315
		// Show a subtitle link to deleted revisions (to sysops et al only)
316
		if ( $title instanceof Title ) {
317
			$count = $title->isDeleted();
318
			if ( $count > 0 && $user->isAllowed( 'deletedhistory' ) ) {
319
				$restorelink = $this->getLinkRenderer()->makeKnownLink(
320
					SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ),
321
					$this->msg( 'restorelink' )->numParams( $count )->text()
322
				);
323
				$link = $this->msg( $user->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted' )
324
					->rawParams( $restorelink )->parseAsBlock();
325
				$this->getOutput()->addHTML( "<div id=\"contentSub2\">{$link}</div>" );
326
			}
327
		}
328
	}
329
330
	/**
331
	 * Stashes the upload and shows the main upload form.
332
	 *
333
	 * Note: only errors that can be handled by changing the name or
334
	 * description should be redirected here. It should be assumed that the
335
	 * file itself is sane and has passed UploadBase::verifyFile. This
336
	 * essentially means that UploadBase::VERIFICATION_ERROR and
337
	 * UploadBase::EMPTY_FILE should not be passed here.
338
	 *
339
	 * @param string $message HTML message to be passed to mainUploadForm
340
	 */
341
	protected function showRecoverableUploadError( $message ) {
342
		$stashStatus = $this->mUpload->tryStashFile( $this->getUser() );
343
		if ( $stashStatus->isGood() ) {
344
			$sessionKey = $stashStatus->getValue()->getFileKey();
345
		} else {
346
			$sessionKey = null;
347
			// TODO Add a warning message about the failure to stash here?
348
		}
349
		$message = '<h2>' . $this->msg( 'uploaderror' )->escaped() . "</h2>\n" .
350
			'<div class="error">' . $message . "</div>\n";
351
352
		$form = $this->getUploadForm( $message, $sessionKey );
353
		$form->setSubmitText( $this->msg( 'upload-tryagain' )->escaped() );
354
		$this->showUploadForm( $form );
355
	}
356
357
	/**
358
	 * Stashes the upload, shows the main form, but adds a "continue anyway button".
359
	 * Also checks whether there are actually warnings to display.
360
	 *
361
	 * @param array $warnings
362
	 * @return bool True if warnings were displayed, false if there are no
363
	 *   warnings and it should continue processing
364
	 */
365
	protected function showUploadWarning( $warnings ) {
366
		# If there are no warnings, or warnings we can ignore, return early.
367
		# mDestWarningAck is set when some javascript has shown the warning
368
		# to the user. mForReUpload is set when the user clicks the "upload a
369
		# new version" link.
370
		if ( !$warnings || ( count( $warnings ) == 1
371
			&& isset( $warnings['exists'] )
372
			&& ( $this->mDestWarningAck || $this->mForReUpload ) )
373
		) {
374
			return false;
375
		}
376
377
		$stashStatus = $this->mUpload->tryStashFile( $this->getUser() );
378
		if ( $stashStatus->isGood() ) {
379
			$sessionKey = $stashStatus->getValue()->getFileKey();
380
		} else {
381
			$sessionKey = null;
382
			// TODO Add a warning message about the failure to stash here?
383
		}
384
385
		// Add styles for the warning, reused from the live preview
386
		$this->getOutput()->addModuleStyles( 'mediawiki.special.upload.styles' );
387
388
		$linkRenderer = $this->getLinkRenderer();
389
		$warningHtml = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n"
390
			. '<div class="mw-destfile-warning"><ul>';
391
		foreach ( $warnings as $warning => $args ) {
392
			if ( $warning == 'badfilename' ) {
393
				$this->mDesiredDestName = Title::makeTitle( NS_FILE, $args )->getText();
394
			}
395
			if ( $warning == 'exists' ) {
396
				$msg = "\t<li>" . self::getExistsWarning( $args ) . "</li>\n";
397
			} elseif ( $warning == 'no-change' ) {
398
				$file = $args;
399
				$filename = $file->getTitle()->getPrefixedText();
400
				$msg = "\t<li>" . wfMessage( 'fileexists-no-change', $filename )->parse() . "</li>\n";
401
			} elseif ( $warning == 'duplicate-version' ) {
402
				$file = $args[0];
403
				$count = count( $args );
404
				$filename = $file->getTitle()->getPrefixedText();
405
				$message = wfMessage( 'fileexists-duplicate-version' )
406
					->params( $filename )
407
					->numParams( $count );
408
				$msg = "\t<li>" . $message->parse() . "</li>\n";
409
			} elseif ( $warning == 'was-deleted' ) {
410
				# If the file existed before and was deleted, warn the user of this
411
				$ltitle = SpecialPage::getTitleFor( 'Log' );
412
				$llink = $linkRenderer->makeKnownLink(
413
					$ltitle,
414
					wfMessage( 'deletionlog' )->text(),
415
					[],
416
					[
417
						'type' => 'delete',
418
						'page' => Title::makeTitle( NS_FILE, $args )->getPrefixedText(),
419
					]
420
				);
421
				$msg = "\t<li>" . wfMessage( 'filewasdeleted' )->rawParams( $llink )->parse() . "</li>\n";
422
			} elseif ( $warning == 'duplicate' ) {
423
				$msg = $this->getDupeWarning( $args );
424
			} elseif ( $warning == 'duplicate-archive' ) {
425
				if ( $args === '' ) {
426
					$msg = "\t<li>" . $this->msg( 'file-deleted-duplicate-notitle' )->parse()
427
						. "</li>\n";
428
				} else {
429
					$msg = "\t<li>" . $this->msg( 'file-deleted-duplicate',
430
							Title::makeTitle( NS_FILE, $args )->getPrefixedText() )->parse()
431
						. "</li>\n";
432
				}
433
			} else {
434 View Code Duplication
				if ( $args === true ) {
435
					$args = [];
436
				} elseif ( !is_array( $args ) ) {
437
					$args = [ $args ];
438
				}
439
				$msg = "\t<li>" . $this->msg( $warning, $args )->parse() . "</li>\n";
440
			}
441
			$warningHtml .= $msg;
442
		}
443
		$warningHtml .= "</ul></div>\n";
444
		$warningHtml .= $this->msg( 'uploadwarning-text' )->parseAsBlock();
445
446
		$form = $this->getUploadForm( $warningHtml, $sessionKey, /* $hideIgnoreWarning */ true );
447
		$form->setSubmitText( $this->msg( 'upload-tryagain' )->text() );
448
		$form->addButton( [
449
			'name' => 'wpUploadIgnoreWarning',
450
			'value' => $this->msg( 'ignorewarning' )->text()
451
		] );
452
		$form->addButton( [
453
			'name' => 'wpCancelUpload',
454
			'value' => $this->msg( 'reuploaddesc' )->text()
455
		] );
456
457
		$this->showUploadForm( $form );
458
459
		# Indicate that we showed a form
460
		return true;
461
	}
462
463
	/**
464
	 * Show the upload form with error message, but do not stash the file.
465
	 *
466
	 * @param string $message HTML string
467
	 */
468
	protected function showUploadError( $message ) {
469
		$message = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n" .
470
			'<div class="error">' . $message . "</div>\n";
471
		$this->showUploadForm( $this->getUploadForm( $message ) );
472
	}
473
474
	/**
475
	 * Do the upload.
476
	 * Checks are made in SpecialUpload::execute()
477
	 */
478
	protected function processUpload() {
479
		// Fetch the file if required
480
		$status = $this->mUpload->fetchFile();
481
		if ( !$status->isOK() ) {
482
			$this->showUploadError( $this->getOutput()->parse( $status->getWikiText() ) );
483
484
			return;
485
		}
486
487
		if ( !Hooks::run( 'UploadForm:BeforeProcessing', [ &$this ] ) ) {
488
			wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file.\n" );
489
			// This code path is deprecated. If you want to break upload processing
490
			// do so by hooking into the appropriate hooks in UploadBase::verifyUpload
491
			// and UploadBase::verifyFile.
492
			// If you use this hook to break uploading, the user will be returned
493
			// an empty form with no error message whatsoever.
494
			return;
495
		}
496
497
		// Upload verification
498
		$details = $this->mUpload->verifyUpload();
499
		if ( $details['status'] != UploadBase::OK ) {
500
			$this->processVerificationError( $details );
0 ignored issues
show
It seems like $details defined by $this->mUpload->verifyUpload() on line 498 can also be of type boolean; however, SpecialUpload::processVerificationError() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
501
502
			return;
503
		}
504
505
		// Verify permissions for this title
506
		$permErrors = $this->mUpload->verifyTitlePermissions( $this->getUser() );
507
		if ( $permErrors !== true ) {
508
			$code = array_shift( $permErrors[0] );
509
			$this->showRecoverableUploadError( $this->msg( $code, $permErrors[0] )->parse() );
510
511
			return;
512
		}
513
514
		$this->mLocalFile = $this->mUpload->getLocalFile();
515
516
		// Check warnings if necessary
517
		if ( !$this->mIgnoreWarning ) {
518
			$warnings = $this->mUpload->checkWarnings();
519
			if ( $this->showUploadWarning( $warnings ) ) {
520
				return;
521
			}
522
		}
523
524
		// This is as late as we can throttle, after expected issues have been handled
525
		if ( UploadBase::isThrottled( $this->getUser() ) ) {
526
			$this->showRecoverableUploadError(
527
				$this->msg( 'actionthrottledtext' )->escaped()
528
			);
529
			return;
530
		}
531
532
		// Get the page text if this is not a reupload
533
		if ( !$this->mForReUpload ) {
534
			$pageText = self::getInitialPageText( $this->mComment, $this->mLicense,
535
				$this->mCopyrightStatus, $this->mCopyrightSource, $this->getConfig() );
536
		} else {
537
			$pageText = false;
538
		}
539
540
		$changeTags = $this->getRequest()->getVal( 'wpChangeTags' );
541
		if ( is_null( $changeTags ) || $changeTags === '' ) {
542
			$changeTags = [];
543
		} else {
544
			$changeTags = array_filter( array_map( 'trim', explode( ',', $changeTags ) ) );
545
		}
546
547
		if ( $changeTags ) {
548
			$changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
549
				$changeTags, $this->getUser() );
550
			if ( !$changeTagsStatus->isOK() ) {
551
				$this->showUploadError( $this->getOutput()->parse( $changeTagsStatus->getWikiText() ) );
552
553
				return;
554
			}
555
		}
556
557
		$status = $this->mUpload->performUpload(
558
			$this->mComment,
559
			$pageText,
0 ignored issues
show
It seems like $pageText defined by false on line 537 can also be of type false; however, UploadBase::performUpload() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
560
			$this->mWatchthis,
561
			$this->getUser(),
562
			$changeTags
563
		);
564
565
		if ( !$status->isGood() ) {
566
			$this->showRecoverableUploadError( $this->getOutput()->parse( $status->getWikiText() ) );
567
568
			return;
569
		}
570
571
		// Success, redirect to description page
572
		$this->mUploadSuccessful = true;
573
		Hooks::run( 'SpecialUploadComplete', [ &$this ] );
574
		$this->getOutput()->redirect( $this->mLocalFile->getTitle()->getFullURL() );
575
	}
576
577
	/**
578
	 * Get the initial image page text based on a comment and optional file status information
579
	 * @param string $comment
580
	 * @param string $license
581
	 * @param string $copyStatus
582
	 * @param string $source
583
	 * @param Config $config Configuration object to load data from
584
	 * @return string
585
	 */
586
	public static function getInitialPageText( $comment = '', $license = '',
587
		$copyStatus = '', $source = '', Config $config = null
588
	) {
589 View Code Duplication
		if ( $config === null ) {
590
			wfDebug( __METHOD__ . ' called without a Config instance passed to it' );
591
			$config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
592
		}
593
594
		$msg = [];
595
		$forceUIMsgAsContentMsg = (array)$config->get( 'ForceUIMsgAsContentMsg' );
596
		/* These messages are transcluded into the actual text of the description page.
597
		 * Thus, forcing them as content messages makes the upload to produce an int: template
598
		 * instead of hardcoding it there in the uploader language.
599
		 */
600
		foreach ( [ 'license-header', 'filedesc', 'filestatus', 'filesource' ] as $msgName ) {
601
			if ( in_array( $msgName, $forceUIMsgAsContentMsg ) ) {
602
				$msg[$msgName] = "{{int:$msgName}}";
603
			} else {
604
				$msg[$msgName] = wfMessage( $msgName )->inContentLanguage()->text();
605
			}
606
		}
607
608
		if ( $config->get( 'UseCopyrightUpload' ) ) {
609
			$licensetxt = '';
610
			if ( $license != '' ) {
611
				$licensetxt = '== ' . $msg['license-header'] . " ==\n" . '{{' . $license . '}}' . "\n";
612
			}
613
			$pageText = '== ' . $msg['filedesc'] . " ==\n" . $comment . "\n" .
614
				'== ' . $msg['filestatus'] . " ==\n" . $copyStatus . "\n" .
615
				"$licensetxt" .
616
				'== ' . $msg['filesource'] . " ==\n" . $source;
617
		} else {
618
			if ( $license != '' ) {
619
				$filedesc = $comment == '' ? '' : '== ' . $msg['filedesc'] . " ==\n" . $comment . "\n";
620
					$pageText = $filedesc .
621
					'== ' . $msg['license-header'] . " ==\n" . '{{' . $license . '}}' . "\n";
622
			} else {
623
				$pageText = $comment;
624
			}
625
		}
626
627
		return $pageText;
628
	}
629
630
	/**
631
	 * See if we should check the 'watch this page' checkbox on the form
632
	 * based on the user's preferences and whether we're being asked
633
	 * to create a new file or update an existing one.
634
	 *
635
	 * In the case where 'watch edits' is off but 'watch creations' is on,
636
	 * we'll leave the box unchecked.
637
	 *
638
	 * Note that the page target can be changed *on the form*, so our check
639
	 * state can get out of sync.
640
	 * @return bool|string
641
	 */
642
	protected function getWatchCheck() {
643
		if ( $this->getUser()->getOption( 'watchdefault' ) ) {
644
			// Watch all edits!
645
			return true;
646
		}
647
648
		$desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName );
649
		if ( $desiredTitleObj instanceof Title && $this->getUser()->isWatched( $desiredTitleObj ) ) {
650
			// Already watched, don't change that
651
			return true;
652
		}
653
654
		$local = wfLocalFile( $this->mDesiredDestName );
655
		if ( $local && $local->exists() ) {
656
			// We're uploading a new version of an existing file.
657
			// No creation, so don't watch it if we're not already.
658
			return false;
659
		} else {
660
			// New page should get watched if that's our option.
661
			return $this->getUser()->getOption( 'watchcreations' ) ||
662
				$this->getUser()->getOption( 'watchuploads' );
663
		}
664
	}
665
666
	/**
667
	 * Provides output to the user for a result of UploadBase::verifyUpload
668
	 *
669
	 * @param array $details Result of UploadBase::verifyUpload
670
	 * @throws MWException
671
	 */
672
	protected function processVerificationError( $details ) {
673
		switch ( $details['status'] ) {
674
675
			/** Statuses that only require name changing **/
676
			case UploadBase::MIN_LENGTH_PARTNAME:
677
				$this->showRecoverableUploadError( $this->msg( 'minlength1' )->escaped() );
678
				break;
679
			case UploadBase::ILLEGAL_FILENAME:
680
				$this->showRecoverableUploadError( $this->msg( 'illegalfilename',
681
					$details['filtered'] )->parse() );
682
				break;
683
			case UploadBase::FILENAME_TOO_LONG:
684
				$this->showRecoverableUploadError( $this->msg( 'filename-toolong' )->escaped() );
685
				break;
686
			case UploadBase::FILETYPE_MISSING:
687
				$this->showRecoverableUploadError( $this->msg( 'filetype-missing' )->parse() );
688
				break;
689
			case UploadBase::WINDOWS_NONASCII_FILENAME:
690
				$this->showRecoverableUploadError( $this->msg( 'windows-nonascii-filename' )->parse() );
691
				break;
692
693
			/** Statuses that require reuploading **/
694
			case UploadBase::EMPTY_FILE:
695
				$this->showUploadError( $this->msg( 'emptyfile' )->escaped() );
696
				break;
697
			case UploadBase::FILE_TOO_LARGE:
698
				$this->showUploadError( $this->msg( 'largefileserver' )->escaped() );
699
				break;
700
			case UploadBase::FILETYPE_BADTYPE:
701
				$msg = $this->msg( 'filetype-banned-type' );
702
				if ( isset( $details['blacklistedExt'] ) ) {
703
					$msg->params( $this->getLanguage()->commaList( $details['blacklistedExt'] ) );
704
				} else {
705
					$msg->params( $details['finalExt'] );
706
				}
707
				$extensions = array_unique( $this->getConfig()->get( 'FileExtensions' ) );
708
				$msg->params( $this->getLanguage()->commaList( $extensions ),
709
					count( $extensions ) );
710
711
				// Add PLURAL support for the first parameter. This results
712
				// in a bit unlogical parameter sequence, but does not break
713
				// old translations
714
				if ( isset( $details['blacklistedExt'] ) ) {
715
					$msg->params( count( $details['blacklistedExt'] ) );
716
				} else {
717
					$msg->params( 1 );
718
				}
719
720
				$this->showUploadError( $msg->parse() );
721
				break;
722
			case UploadBase::VERIFICATION_ERROR:
723
				unset( $details['status'] );
724
				$code = array_shift( $details['details'] );
725
				$this->showUploadError( $this->msg( $code, $details['details'] )->parse() );
726
				break;
727
			case UploadBase::HOOK_ABORTED:
728
				if ( is_array( $details['error'] ) ) { # allow hooks to return error details in an array
729
					$args = $details['error'];
730
					$error = array_shift( $args );
731
				} else {
732
					$error = $details['error'];
733
					$args = null;
734
				}
735
736
				$this->showUploadError( $this->msg( $error, $args )->parse() );
737
				break;
738
			default:
739
				throw new MWException( __METHOD__ . ": Unknown value `{$details['status']}`" );
740
		}
741
	}
742
743
	/**
744
	 * Remove a temporarily kept file stashed by saveTempUploadedFile().
745
	 *
746
	 * @return bool Success
747
	 */
748
	protected function unsaveUploadedFile() {
749
		if ( !( $this->mUpload instanceof UploadFromStash ) ) {
750
			return true;
751
		}
752
		$success = $this->mUpload->unsaveUploadedFile();
753
		if ( !$success ) {
754
			$this->getOutput()->showFileDeleteError( $this->mUpload->getTempPath() );
755
756
			return false;
757
		} else {
758
			return true;
759
		}
760
	}
761
762
	/*** Functions for formatting warnings ***/
763
764
	/**
765
	 * Formats a result of UploadBase::getExistsWarning as HTML
766
	 * This check is static and can be done pre-upload via AJAX
767
	 *
768
	 * @param array $exists The result of UploadBase::getExistsWarning
769
	 * @return string Empty string if there is no warning or an HTML fragment
770
	 */
771
	public static function getExistsWarning( $exists ) {
772
		if ( !$exists ) {
773
			return '';
774
		}
775
776
		$file = $exists['file'];
777
		$filename = $file->getTitle()->getPrefixedText();
778
		$warnMsg = null;
779
780
		if ( $exists['warning'] == 'exists' ) {
781
			// Exact match
782
			$warnMsg = wfMessage( 'fileexists', $filename );
783
		} elseif ( $exists['warning'] == 'page-exists' ) {
784
			// Page exists but file does not
785
			$warnMsg = wfMessage( 'filepageexists', $filename );
786
		} elseif ( $exists['warning'] == 'exists-normalized' ) {
787
			$warnMsg = wfMessage( 'fileexists-extension', $filename,
788
				$exists['normalizedFile']->getTitle()->getPrefixedText() );
789
		} elseif ( $exists['warning'] == 'thumb' ) {
790
			// Swapped argument order compared with other messages for backwards compatibility
791
			$warnMsg = wfMessage( 'fileexists-thumbnail-yes',
792
				$exists['thumbFile']->getTitle()->getPrefixedText(), $filename );
793
		} elseif ( $exists['warning'] == 'thumb-name' ) {
794
			// Image w/o '180px-' does not exists, but we do not like these filenames
795
			$name = $file->getName();
796
			$badPart = substr( $name, 0, strpos( $name, '-' ) + 1 );
797
			$warnMsg = wfMessage( 'file-thumbnail-no', $badPart );
798
		} elseif ( $exists['warning'] == 'bad-prefix' ) {
799
			$warnMsg = wfMessage( 'filename-bad-prefix', $exists['prefix'] );
800
		}
801
802
		return $warnMsg ? $warnMsg->title( $file->getTitle() )->parse() : '';
803
	}
804
805
	/**
806
	 * Construct a warning and a gallery from an array of duplicate files.
807
	 * @param array $dupes
808
	 * @return string
809
	 */
810
	public function getDupeWarning( $dupes ) {
811
		if ( !$dupes ) {
812
			return '';
813
		}
814
815
		$gallery = ImageGalleryBase::factory( false, $this->getContext() );
816
		$gallery->setShowBytes( false );
817
		foreach ( $dupes as $file ) {
818
			$gallery->add( $file->getTitle() );
819
		}
820
821
		return '<li>' .
822
			$this->msg( 'file-exists-duplicate' )->numParams( count( $dupes ) )->parse() .
823
			$gallery->toHTML() . "</li>\n";
824
	}
825
826
	protected function getGroupName() {
827
		return 'media';
828
	}
829
830
	/**
831
	 * Should we rotate images in the preview on Special:Upload.
832
	 *
833
	 * This controls js: mw.config.get( 'wgFileCanRotate' )
834
	 *
835
	 * @todo What about non-BitmapHandler handled files?
836
	 */
837
	public static function rotationEnabled() {
838
		$bitmapHandler = new BitmapHandler();
839
		return $bitmapHandler->autoRotateEnabled();
840
	}
841
}
842
843
/**
844
 * Sub class of HTMLForm that provides the form section of SpecialUpload
845
 */
846
class UploadForm extends HTMLForm {
847
	protected $mWatch;
848
	protected $mForReUpload;
849
	protected $mSessionKey;
850
	protected $mHideIgnoreWarning;
851
	protected $mDestWarningAck;
852
	protected $mDestFile;
853
854
	protected $mComment;
855
	protected $mTextTop;
856
	protected $mTextAfterSummary;
857
858
	protected $mSourceIds;
859
860
	protected $mMaxFileSize = [];
861
862
	protected $mMaxUploadSize = [];
863
864
	public function __construct( array $options = [], IContextSource $context = null,
865
		LinkRenderer $linkRenderer = null
866
	) {
867
		if ( $context instanceof IContextSource ) {
868
			$this->setContext( $context );
869
		}
870
871
		if ( !$linkRenderer ) {
872
			$linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
873
		}
874
875
		$this->mWatch = !empty( $options['watch'] );
876
		$this->mForReUpload = !empty( $options['forreupload'] );
877
		$this->mSessionKey = isset( $options['sessionkey'] ) ? $options['sessionkey'] : '';
878
		$this->mHideIgnoreWarning = !empty( $options['hideignorewarning'] );
879
		$this->mDestWarningAck = !empty( $options['destwarningack'] );
880
		$this->mDestFile = isset( $options['destfile'] ) ? $options['destfile'] : '';
881
882
		$this->mComment = isset( $options['description'] ) ?
883
			$options['description'] : '';
884
885
		$this->mTextTop = isset( $options['texttop'] )
886
			? $options['texttop'] : '';
887
888
		$this->mTextAfterSummary = isset( $options['textaftersummary'] )
889
			? $options['textaftersummary'] : '';
890
891
		$sourceDescriptor = $this->getSourceSection();
892
		$descriptor = $sourceDescriptor
893
			+ $this->getDescriptionSection()
894
			+ $this->getOptionsSection();
895
896
		Hooks::run( 'UploadFormInitDescriptor', [ &$descriptor ] );
897
		parent::__construct( $descriptor, $context, 'upload' );
898
899
		# Add a link to edit MediaWiki:Licenses
900
		if ( $this->getUser()->isAllowed( 'editinterface' ) ) {
901
			$this->getOutput()->addModuleStyles( 'mediawiki.special.upload.styles' );
902
			$licensesLink = $linkRenderer->makeKnownLink(
903
				$this->msg( 'licenses' )->inContentLanguage()->getTitle(),
904
				$this->msg( 'licenses-edit' )->text(),
905
				[],
906
				[ 'action' => 'edit' ]
907
			);
908
			$editLicenses = '<p class="mw-upload-editlicenses">' . $licensesLink . '</p>';
909
			$this->addFooterText( $editLicenses, 'description' );
910
		}
911
912
		# Set some form properties
913
		$this->setSubmitText( $this->msg( 'uploadbtn' )->text() );
914
		$this->setSubmitName( 'wpUpload' );
915
		# Used message keys: 'accesskey-upload', 'tooltip-upload'
916
		$this->setSubmitTooltip( 'upload' );
917
		$this->setId( 'mw-upload-form' );
918
919
		# Build a list of IDs for javascript insertion
920
		$this->mSourceIds = [];
921
		foreach ( $sourceDescriptor as $field ) {
922
			if ( !empty( $field['id'] ) ) {
923
				$this->mSourceIds[] = $field['id'];
924
			}
925
		}
926
	}
927
928
	/**
929
	 * Get the descriptor of the fieldset that contains the file source
930
	 * selection. The section is 'source'
931
	 *
932
	 * @return array Descriptor array
933
	 */
934
	protected function getSourceSection() {
935
		if ( $this->mSessionKey ) {
936
			return [
937
				'SessionKey' => [
938
					'type' => 'hidden',
939
					'default' => $this->mSessionKey,
940
				],
941
				'SourceType' => [
942
					'type' => 'hidden',
943
					'default' => 'Stash',
944
				],
945
			];
946
		}
947
948
		$canUploadByUrl = UploadFromUrl::isEnabled()
949
			&& ( UploadFromUrl::isAllowed( $this->getUser() ) === true )
950
			&& $this->getConfig()->get( 'CopyUploadsFromSpecialUpload' );
951
		$radio = $canUploadByUrl;
952
		$selectedSourceType = strtolower( $this->getRequest()->getText( 'wpSourceType', 'File' ) );
953
954
		$descriptor = [];
955
		if ( $this->mTextTop ) {
956
			$descriptor['UploadFormTextTop'] = [
957
				'type' => 'info',
958
				'section' => 'source',
959
				'default' => $this->mTextTop,
960
				'raw' => true,
961
			];
962
		}
963
964
		$this->mMaxUploadSize['file'] = min(
965
			UploadBase::getMaxUploadSize( 'file' ),
966
			UploadBase::getMaxPhpUploadSize()
967
		);
968
969
		$help = $this->msg( 'upload-maxfilesize',
970
				$this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['file'] )
971
			)->parse();
972
973
		// If the user can also upload by URL, there are 2 different file size limits.
974
		// This extra message helps stress which limit corresponds to what.
975
		if ( $canUploadByUrl ) {
976
			$help .= $this->msg( 'word-separator' )->escaped();
977
			$help .= $this->msg( 'upload_source_file' )->parse();
978
		}
979
980
		$descriptor['UploadFile'] = [
981
			'class' => 'UploadSourceField',
982
			'section' => 'source',
983
			'type' => 'file',
984
			'id' => 'wpUploadFile',
985
			'radio-id' => 'wpSourceTypeFile',
986
			'label-message' => 'sourcefilename',
987
			'upload-type' => 'File',
988
			'radio' => &$radio,
989
			'help' => $help,
990
			'checked' => $selectedSourceType == 'file',
991
		];
992
993
		if ( $canUploadByUrl ) {
994
			$this->mMaxUploadSize['url'] = UploadBase::getMaxUploadSize( 'url' );
995
			$descriptor['UploadFileURL'] = [
996
				'class' => 'UploadSourceField',
997
				'section' => 'source',
998
				'id' => 'wpUploadFileURL',
999
				'radio-id' => 'wpSourceTypeurl',
1000
				'label-message' => 'sourceurl',
1001
				'upload-type' => 'url',
1002
				'radio' => &$radio,
1003
				'help' => $this->msg( 'upload-maxfilesize',
1004
					$this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['url'] )
1005
				)->parse() .
1006
					$this->msg( 'word-separator' )->escaped() .
1007
					$this->msg( 'upload_source_url' )->parse(),
1008
				'checked' => $selectedSourceType == 'url',
1009
			];
1010
		}
1011
		Hooks::run( 'UploadFormSourceDescriptors', [ &$descriptor, &$radio, $selectedSourceType ] );
1012
1013
		$descriptor['Extensions'] = [
1014
			'type' => 'info',
1015
			'section' => 'source',
1016
			'default' => $this->getExtensionsMessage(),
1017
			'raw' => true,
1018
		];
1019
1020
		return $descriptor;
1021
	}
1022
1023
	/**
1024
	 * Get the messages indicating which extensions are preferred and prohibitted.
1025
	 *
1026
	 * @return string HTML string containing the message
1027
	 */
1028
	protected function getExtensionsMessage() {
1029
		# Print a list of allowed file extensions, if so configured.  We ignore
1030
		# MIME type here, it's incomprehensible to most people and too long.
1031
		$config = $this->getConfig();
1032
1033
		if ( $config->get( 'CheckFileExtensions' ) ) {
1034
			$fileExtensions = array_unique( $config->get( 'FileExtensions' ) );
1035
			if ( $config->get( 'StrictFileExtensions' ) ) {
1036
				# Everything not permitted is banned
1037
				$extensionsList =
1038
					'<div id="mw-upload-permitted">' .
1039
					$this->msg( 'upload-permitted' )
1040
						->params( $this->getLanguage()->commaList( $fileExtensions ) )
1041
						->numParams( count( $fileExtensions ) )
1042
						->parseAsBlock() .
1043
					"</div>\n";
1044
			} else {
1045
				# We have to list both preferred and prohibited
1046
				$fileBlacklist = array_unique( $config->get( 'FileBlacklist' ) );
1047
				$extensionsList =
1048
					'<div id="mw-upload-preferred">' .
1049
						$this->msg( 'upload-preferred' )
1050
							->params( $this->getLanguage()->commaList( $fileExtensions ) )
1051
							->numParams( count( $fileExtensions ) )
1052
							->parseAsBlock() .
1053
					"</div>\n" .
1054
					'<div id="mw-upload-prohibited">' .
1055
						$this->msg( 'upload-prohibited' )
1056
							->params( $this->getLanguage()->commaList( $fileBlacklist ) )
1057
							->numParams( count( $fileBlacklist ) )
1058
							->parseAsBlock() .
1059
					"</div>\n";
1060
			}
1061
		} else {
1062
			# Everything is permitted.
1063
			$extensionsList = '';
1064
		}
1065
1066
		return $extensionsList;
1067
	}
1068
1069
	/**
1070
	 * Get the descriptor of the fieldset that contains the file description
1071
	 * input. The section is 'description'
1072
	 *
1073
	 * @return array Descriptor array
1074
	 */
1075
	protected function getDescriptionSection() {
1076
		$config = $this->getConfig();
1077
		if ( $this->mSessionKey ) {
1078
			$stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $this->getUser() );
1079
			try {
1080
				$file = $stash->getFile( $this->mSessionKey );
1081
			} catch ( Exception $e ) {
1082
				$file = null;
1083
			}
1084
			if ( $file ) {
1085
				global $wgContLang;
1086
1087
				$mto = $file->transform( [ 'width' => 120 ] );
1088
				$this->addHeaderText(
1089
					'<div class="thumb t' . $wgContLang->alignEnd() . '">' .
1090
					Html::element( 'img', [
1091
						'src' => $mto->getUrl(),
1092
						'class' => 'thumbimage',
1093
					] ) . '</div>', 'description' );
1094
			}
1095
		}
1096
1097
		$descriptor = [
1098
			'DestFile' => [
1099
				'type' => 'text',
1100
				'section' => 'description',
1101
				'id' => 'wpDestFile',
1102
				'label-message' => 'destfilename',
1103
				'size' => 60,
1104
				'default' => $this->mDestFile,
1105
				# @todo FIXME: Hack to work around poor handling of the 'default' option in HTMLForm
1106
				'nodata' => strval( $this->mDestFile ) !== '',
1107
			],
1108
			'UploadDescription' => [
1109
				'type' => 'textarea',
1110
				'section' => 'description',
1111
				'id' => 'wpUploadDescription',
1112
				'label-message' => $this->mForReUpload
1113
					? 'filereuploadsummary'
1114
					: 'fileuploadsummary',
1115
				'default' => $this->mComment,
1116
				'cols' => $this->getUser()->getIntOption( 'cols' ),
1117
				'rows' => 8,
1118
			]
1119
		];
1120
		if ( $this->mTextAfterSummary ) {
1121
			$descriptor['UploadFormTextAfterSummary'] = [
1122
				'type' => 'info',
1123
				'section' => 'description',
1124
				'default' => $this->mTextAfterSummary,
1125
				'raw' => true,
1126
			];
1127
		}
1128
1129
		$descriptor += [
1130
			'EditTools' => [
1131
				'type' => 'edittools',
1132
				'section' => 'description',
1133
				'message' => 'edittools-upload',
1134
			]
1135
		];
1136
1137
		if ( $this->mForReUpload ) {
1138
			$descriptor['DestFile']['readonly'] = true;
1139
		} else {
1140
			$descriptor['License'] = [
1141
				'type' => 'select',
1142
				'class' => 'Licenses',
1143
				'section' => 'description',
1144
				'id' => 'wpLicense',
1145
				'label-message' => 'license',
1146
			];
1147
		}
1148
1149
		if ( $config->get( 'UseCopyrightUpload' ) ) {
1150
			$descriptor['UploadCopyStatus'] = [
1151
				'type' => 'text',
1152
				'section' => 'description',
1153
				'id' => 'wpUploadCopyStatus',
1154
				'label-message' => 'filestatus',
1155
			];
1156
			$descriptor['UploadSource'] = [
1157
				'type' => 'text',
1158
				'section' => 'description',
1159
				'id' => 'wpUploadSource',
1160
				'label-message' => 'filesource',
1161
			];
1162
		}
1163
1164
		return $descriptor;
1165
	}
1166
1167
	/**
1168
	 * Get the descriptor of the fieldset that contains the upload options,
1169
	 * such as "watch this file". The section is 'options'
1170
	 *
1171
	 * @return array Descriptor array
1172
	 */
1173
	protected function getOptionsSection() {
1174
		$user = $this->getUser();
1175
		if ( $user->isLoggedIn() ) {
1176
			$descriptor = [
1177
				'Watchthis' => [
1178
					'type' => 'check',
1179
					'id' => 'wpWatchthis',
1180
					'label-message' => 'watchthisupload',
1181
					'section' => 'options',
1182
					'default' => $this->mWatch,
1183
				]
1184
			];
1185
		}
1186
		if ( !$this->mHideIgnoreWarning ) {
1187
			$descriptor['IgnoreWarning'] = [
0 ignored issues
show
The variable $descriptor does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1188
				'type' => 'check',
1189
				'id' => 'wpIgnoreWarning',
1190
				'label-message' => 'ignorewarnings',
1191
				'section' => 'options',
1192
			];
1193
		}
1194
1195
		$descriptor['DestFileWarningAck'] = [
1196
			'type' => 'hidden',
1197
			'id' => 'wpDestFileWarningAck',
1198
			'default' => $this->mDestWarningAck ? '1' : '',
1199
		];
1200
1201
		if ( $this->mForReUpload ) {
1202
			$descriptor['ForReUpload'] = [
1203
				'type' => 'hidden',
1204
				'id' => 'wpForReUpload',
1205
				'default' => '1',
1206
			];
1207
		}
1208
1209
		return $descriptor;
1210
	}
1211
1212
	/**
1213
	 * Add the upload JS and show the form.
1214
	 */
1215
	public function show() {
1216
		$this->addUploadJS();
1217
		parent::show();
1218
	}
1219
1220
	/**
1221
	 * Add upload JS to the OutputPage
1222
	 */
1223
	protected function addUploadJS() {
1224
		$config = $this->getConfig();
1225
1226
		$useAjaxDestCheck = $config->get( 'UseAjax' ) && $config->get( 'AjaxUploadDestCheck' );
1227
		$useAjaxLicensePreview = $config->get( 'UseAjax' ) &&
1228
			$config->get( 'AjaxLicensePreview' ) && $config->get( 'EnableAPI' );
1229
		$this->mMaxUploadSize['*'] = UploadBase::getMaxUploadSize();
1230
1231
		$scriptVars = [
1232
			'wgAjaxUploadDestCheck' => $useAjaxDestCheck,
1233
			'wgAjaxLicensePreview' => $useAjaxLicensePreview,
1234
			'wgUploadAutoFill' => !$this->mForReUpload &&
1235
				// If we received mDestFile from the request, don't autofill
1236
				// the wpDestFile textbox
1237
				$this->mDestFile === '',
1238
			'wgUploadSourceIds' => $this->mSourceIds,
1239
			'wgCheckFileExtensions' => $config->get( 'CheckFileExtensions' ),
1240
			'wgStrictFileExtensions' => $config->get( 'StrictFileExtensions' ),
1241
			'wgFileExtensions' => array_values( array_unique( $config->get( 'FileExtensions' ) ) ),
1242
			'wgCapitalizeUploads' => MWNamespace::isCapitalized( NS_FILE ),
1243
			'wgMaxUploadSize' => $this->mMaxUploadSize,
1244
			'wgFileCanRotate' => SpecialUpload::rotationEnabled(),
1245
		];
1246
1247
		$out = $this->getOutput();
1248
		$out->addJsConfigVars( $scriptVars );
1249
1250
		$out->addModules( [
1251
			'mediawiki.action.edit', // For <charinsert> support
1252
			'mediawiki.special.upload', // Extras for thumbnail and license preview.
1253
		] );
1254
	}
1255
1256
	/**
1257
	 * Empty function; submission is handled elsewhere.
1258
	 *
1259
	 * @return bool False
1260
	 */
1261
	function trySubmit() {
1262
		return false;
1263
	}
1264
}
1265
1266
/**
1267
 * A form field that contains a radio box in the label
1268
 */
1269
class UploadSourceField extends HTMLTextField {
1270
1271
	/**
1272
	 * @param array $cellAttributes
1273
	 * @return string
1274
	 */
1275
	function getLabelHtml( $cellAttributes = [] ) {
1276
		$id = $this->mParams['id'];
1277
		$label = Html::rawElement( 'label', [ 'for' => $id ], $this->mLabel );
1278
1279
		if ( !empty( $this->mParams['radio'] ) ) {
1280
			if ( isset( $this->mParams['radio-id'] ) ) {
1281
				$radioId = $this->mParams['radio-id'];
1282
			} else {
1283
				// Old way. For the benefit of extensions that do not define
1284
				// the 'radio-id' key.
1285
				$radioId = 'wpSourceType' . $this->mParams['upload-type'];
1286
			}
1287
1288
			$attribs = [
1289
				'name' => 'wpSourceType',
1290
				'type' => 'radio',
1291
				'id' => $radioId,
1292
				'value' => $this->mParams['upload-type'],
1293
			];
1294
1295
			if ( !empty( $this->mParams['checked'] ) ) {
1296
				$attribs['checked'] = 'checked';
1297
			}
1298
1299
			$label .= Html::element( 'input', $attribs );
1300
		}
1301
1302
		return Html::rawElement( 'td', [ 'class' => 'mw-label' ] + $cellAttributes, $label );
1303
	}
1304
1305
	/**
1306
	 * @return int
1307
	 */
1308
	function getSize() {
1309
		return isset( $this->mParams['size'] )
1310
			? $this->mParams['size']
1311
			: 60;
1312
	}
1313
}
1314