Completed
Branch master (af7ffa)
by
unknown
24:08
created

ApiUpload::checkVerification()   D

Complexity

Conditions 17
Paths 21

Size

Total Lines 88
Code Lines 71

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 17
eloc 71
nc 21
nop 1
dl 0
loc 88
rs 4.8361
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 *
4
 *
5
 * Created on Aug 21, 2008
6
 *
7
 * Copyright © 2008 - 2010 Bryan Tong Minh <[email protected]>
8
 *
9
 * This program is free software; you can redistribute it and/or modify
10
 * it under the terms of the GNU General Public License as published by
11
 * the Free Software Foundation; either version 2 of the License, or
12
 * (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU General Public License along
20
 * with this program; if not, write to the Free Software Foundation, Inc.,
21
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22
 * http://www.gnu.org/copyleft/gpl.html
23
 *
24
 * @file
25
 */
26
27
/**
28
 * @ingroup API
29
 */
30
class ApiUpload extends ApiBase {
31
	/** @var UploadBase|UploadFromChunks */
32
	protected $mUpload = null;
33
34
	protected $mParams;
35
36
	public function execute() {
37
		// Check whether upload is enabled
38
		if ( !UploadBase::isEnabled() ) {
39
			$this->dieUsageMsg( 'uploaddisabled' );
40
		}
41
42
		$user = $this->getUser();
43
44
		// Parameter handling
45
		$this->mParams = $this->extractRequestParams();
46
		$request = $this->getMain()->getRequest();
47
		// Check if async mode is actually supported (jobs done in cli mode)
48
		$this->mParams['async'] = ( $this->mParams['async'] &&
49
			$this->getConfig()->get( 'EnableAsyncUploads' ) );
50
		// Add the uploaded file to the params array
51
		$this->mParams['file'] = $request->getFileName( 'file' );
52
		$this->mParams['chunk'] = $request->getFileName( 'chunk' );
53
54
		// Copy the session key to the file key, for backward compatibility.
55
		if ( !$this->mParams['filekey'] && $this->mParams['sessionkey'] ) {
56
			$this->mParams['filekey'] = $this->mParams['sessionkey'];
57
		}
58
59
		// Select an upload module
60
		try {
61
			if ( !$this->selectUploadModule() ) {
62
				return; // not a true upload, but a status request or similar
63
			} elseif ( !isset( $this->mUpload ) ) {
64
				$this->dieUsage( 'No upload module set', 'nomodule' );
65
			}
66
		} catch ( UploadStashException $e ) { // XXX: don't spam exception log
67
			$this->handleStashException( $e );
68
		}
69
70
		// First check permission to upload
71
		$this->checkPermissions( $user );
72
73
		// Fetch the file (usually a no-op)
74
		/** @var $status Status */
75
		$status = $this->mUpload->fetchFile();
76
		if ( !$status->isGood() ) {
77
			$errors = $status->getErrorsArray();
0 ignored issues
show
Deprecated Code introduced by
The method Status::getErrorsArray() has been deprecated with message: 1.25

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
78
			$error = array_shift( $errors[0] );
79
			$this->dieUsage( 'Error fetching file from remote source', $error, 0, $errors[0] );
80
		}
81
82
		// Check if the uploaded file is sane
83
		if ( $this->mParams['chunk'] ) {
84
			$maxSize = UploadBase::getMaxUploadSize();
85
			if ( $this->mParams['filesize'] > $maxSize ) {
86
				$this->dieUsage( 'The file you submitted was too large', 'file-too-large' );
87
			}
88
			if ( !$this->mUpload->getTitle() ) {
89
				$this->dieUsage( 'Invalid file title supplied', 'internal-error' );
90
			}
91
		} elseif ( $this->mParams['async'] && $this->mParams['filekey'] ) {
0 ignored issues
show
Unused Code introduced by
This elseif statement is empty, and could be removed.

This check looks for the bodies of elseif statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These elseif bodies can be removed. If you have an empty elseif but statements in the else branch, consider inverting the condition.

Loading history...
92
			// defer verification to background process
93
		} else {
94
			wfDebug( __METHOD__ . " about to verify\n" );
95
			$this->verifyUpload();
96
		}
97
98
		// Check if the user has the rights to modify or overwrite the requested title
99
		// (This check is irrelevant if stashing is already requested, since the errors
100
		//  can always be fixed by changing the title)
101
		if ( !$this->mParams['stash'] ) {
102
			$permErrors = $this->mUpload->verifyTitlePermissions( $user );
103
			if ( $permErrors !== true ) {
104
				$this->dieRecoverableError( $permErrors[0], 'filename' );
105
			}
106
		}
107
108
		// Get the result based on the current upload context:
109
		try {
110
			$result = $this->getContextResult();
111
			if ( $result['result'] === 'Success' ) {
112
				$result['imageinfo'] = $this->mUpload->getImageInfo( $this->getResult() );
113
			}
114
		} catch ( UploadStashException $e ) { // XXX: don't spam exception log
115
			$this->handleStashException( $e );
116
		}
117
118
		$this->getResult()->addValue( null, $this->getModuleName(), $result );
119
120
		// Cleanup any temporary mess
121
		$this->mUpload->cleanupTempFile();
122
	}
123
124
	/**
125
	 * Get an upload result based on upload context
126
	 * @return array
127
	 */
128
	private function getContextResult() {
129
		$warnings = $this->getApiWarnings();
130
		if ( $warnings && !$this->mParams['ignorewarnings'] ) {
131
			// Get warnings formatted in result array format
132
			return $this->getWarningsResult( $warnings );
133
		} elseif ( $this->mParams['chunk'] ) {
134
			// Add chunk, and get result
135
			return $this->getChunkResult( $warnings );
136
		} elseif ( $this->mParams['stash'] ) {
137
			// Stash the file and get stash result
138
			return $this->getStashResult( $warnings );
139
		}
140
141
		// Check throttle after we've handled warnings
142
		if ( UploadBase::isThrottled( $this->getUser() )
143
		) {
144
			$this->dieUsageMsg( 'actionthrottledtext' );
145
		}
146
147
		// This is the most common case -- a normal upload with no warnings
148
		// performUpload will return a formatted properly for the API with status
149
		return $this->performUpload( $warnings );
150
	}
151
152
	/**
153
	 * Get Stash Result, throws an exception if the file could not be stashed.
154
	 * @param array $warnings Array of Api upload warnings
155
	 * @return array
156
	 */
157
	private function getStashResult( $warnings ) {
158
		$result = [];
159
		// Some uploads can request they be stashed, so as not to publish them immediately.
160
		// In this case, a failure to stash ought to be fatal
161
		try {
162
			$result['result'] = 'Success';
163
			$result['filekey'] = $this->performStash();
164
			$result['sessionkey'] = $result['filekey']; // backwards compatibility
165
			if ( $warnings && count( $warnings ) > 0 ) {
166
				$result['warnings'] = $warnings;
167
			}
168
		} catch ( UploadStashException $e ) {
169
			$this->handleStashException( $e );
170
		} catch ( Exception $e ) {
171
			$this->dieUsage( $e->getMessage(), 'stashfailed' );
172
		}
173
174
		return $result;
175
	}
176
177
	/**
178
	 * Get Warnings Result
179
	 * @param array $warnings Array of Api upload warnings
180
	 * @return array
181
	 */
182
	private function getWarningsResult( $warnings ) {
183
		$result = [];
184
		$result['result'] = 'Warning';
185
		$result['warnings'] = $warnings;
186
		// in case the warnings can be fixed with some further user action, let's stash this upload
187
		// and return a key they can use to restart it
188
		try {
189
			$result['filekey'] = $this->performStash();
190
			$result['sessionkey'] = $result['filekey']; // backwards compatibility
191
		} catch ( Exception $e ) {
192
			$result['warnings']['stashfailed'] = $e->getMessage();
193
		}
194
195
		return $result;
196
	}
197
198
	/**
199
	 * Get the result of a chunk upload.
200
	 * @param array $warnings Array of Api upload warnings
201
	 * @return array
202
	 */
203
	private function getChunkResult( $warnings ) {
204
		$result = [];
205
206
		if ( $warnings && count( $warnings ) > 0 ) {
207
			$result['warnings'] = $warnings;
208
		}
209
210
		$request = $this->getMain()->getRequest();
211
		$chunkPath = $request->getFileTempname( 'chunk' );
212
		$chunkSize = $request->getUpload( 'chunk' )->getSize();
213
		$totalSoFar = $this->mParams['offset'] + $chunkSize;
214
		$minChunkSize = $this->getConfig()->get( 'MinUploadChunkSize' );
215
216
		// Sanity check sizing
217
		if ( $totalSoFar > $this->mParams['filesize'] ) {
218
			$this->dieUsage(
219
				'Offset plus current chunk is greater than claimed file size', 'invalid-chunk'
220
			);
221
		}
222
223
		// Enforce minimum chunk size
224
		if ( $totalSoFar != $this->mParams['filesize'] && $chunkSize < $minChunkSize ) {
225
			$this->dieUsage(
226
				"Minimum chunk size is $minChunkSize bytes for non-final chunks", 'chunk-too-small'
227
			);
228
		}
229
230
		if ( $this->mParams['offset'] == 0 ) {
231
			try {
232
				$filekey = $this->performStash();
233
			} catch ( UploadStashException $e ) {
234
				$this->handleStashException( $e );
235
			} catch ( Exception $e ) {
236
				// FIXME: Error handling here is wrong/different from rest of this
237
				$this->dieUsage( $e->getMessage(), 'stashfailed' );
238
			}
239
		} else {
240
			$filekey = $this->mParams['filekey'];
241
242
			// Don't allow further uploads to an already-completed session
243
			$progress = UploadBase::getSessionStatus( $this->getUser(), $filekey );
244
			if ( !$progress ) {
245
				// Probably can't get here, but check anyway just in case
246
				$this->dieUsage( 'No chunked upload session with this key', 'stashfailed' );
247
			} elseif ( $progress['result'] !== 'Continue' || $progress['stage'] !== 'uploading' ) {
248
				$this->dieUsage(
249
					'Chunked upload is already completed, check status for details', 'stashfailed'
250
				);
251
			}
252
253
			$status = $this->mUpload->addChunk(
0 ignored issues
show
Bug introduced by
The method addChunk does only exist in UploadFromChunks, but not in UploadBase.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
254
				$chunkPath, $chunkSize, $this->mParams['offset'] );
255
			if ( !$status->isGood() ) {
256
				$extradata = [
257
					'offset' => $this->mUpload->getOffset(),
0 ignored issues
show
Bug introduced by
The method getOffset does only exist in UploadFromChunks, but not in UploadBase.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
258
				];
259
260
				$this->dieUsage( $status->getWikiText( false, false, 'en' ), 'stashfailed', 0, $extradata );
261
			}
262
		}
263
264
		// Check we added the last chunk:
265
		if ( $totalSoFar == $this->mParams['filesize'] ) {
266
			if ( $this->mParams['async'] ) {
267
				UploadBase::setSessionStatus(
268
					$this->getUser(),
269
					$filekey,
270
					[ 'result' => 'Poll',
271
						'stage' => 'queued', 'status' => Status::newGood() ]
272
				);
273
				JobQueueGroup::singleton()->push( new AssembleUploadChunksJob(
274
					Title::makeTitle( NS_FILE, $filekey ),
275
					[
276
						'filename' => $this->mParams['filename'],
277
						'filekey' => $filekey,
278
						'session' => $this->getContext()->exportSession()
279
					]
280
				) );
281
				$result['result'] = 'Poll';
282
				$result['stage'] = 'queued';
283
			} else {
284
				$status = $this->mUpload->concatenateChunks();
0 ignored issues
show
Bug introduced by
The method concatenateChunks does only exist in UploadFromChunks, but not in UploadBase.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
285
				if ( !$status->isGood() ) {
286
					UploadBase::setSessionStatus(
287
						$this->getUser(),
288
						$filekey,
289
						[ 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status ]
290
					);
291
					$this->dieUsage( $status->getWikiText( false, false, 'en' ), 'stashfailed' );
292
				}
293
294
				// The fully concatenated file has a new filekey. So remove
295
				// the old filekey and fetch the new one.
296
				UploadBase::setSessionStatus( $this->getUser(), $filekey, false );
297
				$this->mUpload->stash->removeFile( $filekey );
0 ignored issues
show
Bug introduced by
The property stash does not seem to exist in UploadBase.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
298
				$filekey = $this->mUpload->getLocalFile()->getFileKey();
0 ignored issues
show
Bug introduced by
The method getFileKey does only exist in UploadStashFile, but not in LocalFile.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
299
300
				$result['result'] = 'Success';
301
			}
302
		} else {
303
			UploadBase::setSessionStatus(
304
				$this->getUser(),
305
				$filekey,
306
				[
307
					'result' => 'Continue',
308
					'stage' => 'uploading',
309
					'offset' => $totalSoFar,
310
					'status' => Status::newGood(),
311
				]
312
			);
313
			$result['result'] = 'Continue';
314
			$result['offset'] = $totalSoFar;
315
		}
316
317
		$result['filekey'] = $filekey;
318
319
		return $result;
320
	}
321
322
	/**
323
	 * Stash the file and return the file key
324
	 * Also re-raises exceptions with slightly more informative message strings (useful for API)
325
	 * @throws MWException
326
	 * @return string File key
327
	 */
328
	private function performStash() {
329
		try {
330
			$stashFile = $this->mUpload->stashFile( $this->getUser() );
331
332
			if ( !$stashFile ) {
333
				throw new MWException( 'Invalid stashed file' );
334
			}
335
			$fileKey = $stashFile->getFileKey();
336
		} catch ( Exception $e ) {
337
			$message = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage();
338
			wfDebug( __METHOD__ . ' ' . $message . "\n" );
339
			$className = get_class( $e );
340
			throw new $className( $message );
341
		}
342
343
		return $fileKey;
344
	}
345
346
	/**
347
	 * Throw an error that the user can recover from by providing a better
348
	 * value for $parameter
349
	 *
350
	 * @param array|string|MessageSpecifier $error Error suitable for passing to dieUsageMsg()
351
	 * @param string $parameter Parameter that needs revising
352
	 * @param array $data Optional extra data to pass to the user
353
	 * @param string $code Error code to use if the error is unknown
354
	 * @throws UsageException
355
	 */
356
	private function dieRecoverableError( $error, $parameter, $data = [], $code = 'unknownerror' ) {
357
		try {
358
			$data['filekey'] = $this->performStash();
359
			$data['sessionkey'] = $data['filekey'];
360
		} catch ( Exception $e ) {
361
			$data['stashfailed'] = $e->getMessage();
362
		}
363
		$data['invalidparameter'] = $parameter;
364
365
		$parsed = $this->parseMsg( $error );
366
		if ( isset( $parsed['data'] ) ) {
367
			$data = array_merge( $data, $parsed['data'] );
368
		}
369
		if ( $parsed['code'] === 'unknownerror' ) {
370
			$parsed['code'] = $code;
371
		}
372
373
		$this->dieUsage( $parsed['info'], $parsed['code'], 0, $data );
374
	}
375
376
	/**
377
	 * Select an upload module and set it to mUpload. Dies on failure. If the
378
	 * request was a status request and not a true upload, returns false;
379
	 * otherwise true
380
	 *
381
	 * @return bool
382
	 */
383
	protected function selectUploadModule() {
384
		$request = $this->getMain()->getRequest();
385
386
		// chunk or one and only one of the following parameters is needed
387
		if ( !$this->mParams['chunk'] ) {
388
			$this->requireOnlyOneParameter( $this->mParams,
389
				'filekey', 'file', 'url' );
390
		}
391
392
		// Status report for "upload to stash"/"upload from stash"
393
		if ( $this->mParams['filekey'] && $this->mParams['checkstatus'] ) {
394
			$progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
395
			if ( !$progress ) {
396
				$this->dieUsage( 'No result in status data', 'missingresult' );
397
			} elseif ( !$progress['status']->isGood() ) {
398
				$this->dieUsage( $progress['status']->getWikiText( false, false, 'en' ), 'stashfailed' );
399
			}
400
			if ( isset( $progress['status']->value['verification'] ) ) {
401
				$this->checkVerification( $progress['status']->value['verification'] );
402
			}
403
			unset( $progress['status'] ); // remove Status object
404
			$this->getResult()->addValue( null, $this->getModuleName(), $progress );
405
406
			return false;
407
		}
408
409
		// The following modules all require the filename parameter to be set
410
		if ( is_null( $this->mParams['filename'] ) ) {
411
			$this->dieUsageMsg( [ 'missingparam', 'filename' ] );
412
		}
413
414
		if ( $this->mParams['chunk'] ) {
415
			// Chunk upload
416
			$this->mUpload = new UploadFromChunks();
417
			if ( isset( $this->mParams['filekey'] ) ) {
418
				if ( $this->mParams['offset'] === 0 ) {
419
					$this->dieUsage( 'Cannot supply a filekey when offset is 0', 'badparams' );
420
				}
421
422
				// handle new chunk
423
				$this->mUpload->continueChunks(
424
					$this->mParams['filename'],
425
					$this->mParams['filekey'],
426
					$request->getUpload( 'chunk' )
427
				);
428
			} else {
429
				if ( $this->mParams['offset'] !== 0 ) {
430
					$this->dieUsage( 'Must supply a filekey when offset is non-zero', 'badparams' );
431
				}
432
433
				// handle first chunk
434
				$this->mUpload->initialize(
435
					$this->mParams['filename'],
436
					$request->getUpload( 'chunk' )
437
				);
438
			}
439
		} elseif ( isset( $this->mParams['filekey'] ) ) {
440
			// Upload stashed in a previous request
441
			if ( !UploadFromStash::isValidKey( $this->mParams['filekey'] ) ) {
442
				$this->dieUsageMsg( 'invalid-file-key' );
443
			}
444
445
			$this->mUpload = new UploadFromStash( $this->getUser() );
446
			// This will not download the temp file in initialize() in async mode.
447
			// We still have enough information to call checkWarnings() and such.
448
			$this->mUpload->initialize(
449
				$this->mParams['filekey'], $this->mParams['filename'], !$this->mParams['async']
450
			);
451
		} elseif ( isset( $this->mParams['file'] ) ) {
452
			$this->mUpload = new UploadFromFile();
453
			$this->mUpload->initialize(
454
				$this->mParams['filename'],
455
				$request->getUpload( 'file' )
456
			);
457
		} elseif ( isset( $this->mParams['url'] ) ) {
458
			// Make sure upload by URL is enabled:
459
			if ( !UploadFromUrl::isEnabled() ) {
460
				$this->dieUsageMsg( 'copyuploaddisabled' );
461
			}
462
463
			if ( !UploadFromUrl::isAllowedHost( $this->mParams['url'] ) ) {
464
				$this->dieUsageMsg( 'copyuploadbaddomain' );
465
			}
466
467
			if ( !UploadFromUrl::isAllowedUrl( $this->mParams['url'] ) ) {
468
				$this->dieUsageMsg( 'copyuploadbadurl' );
469
			}
470
471
			$this->mUpload = new UploadFromUrl;
472
			$this->mUpload->initialize( $this->mParams['filename'],
473
				$this->mParams['url'] );
474
		}
475
476
		return true;
477
	}
478
479
	/**
480
	 * Checks that the user has permissions to perform this upload.
481
	 * Dies with usage message on inadequate permissions.
482
	 * @param User $user The user to check.
483
	 */
484
	protected function checkPermissions( $user ) {
485
		// Check whether the user has the appropriate permissions to upload anyway
486
		$permission = $this->mUpload->isAllowed( $user );
487
488
		if ( $permission !== true ) {
489
			if ( !$user->isLoggedIn() ) {
490
				$this->dieUsageMsg( [ 'mustbeloggedin', 'upload' ] );
491
			}
492
493
			$this->dieUsageMsg( 'badaccess-groups' );
494
		}
495
496
		// Check blocks
497
		if ( $user->isBlocked() ) {
498
			$this->dieBlocked( $user->getBlock() );
0 ignored issues
show
Bug introduced by
It seems like $user->getBlock() can be null; however, dieBlocked() 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...
499
		}
500
501
		// Global blocks
502
		if ( $user->isBlockedGlobally() ) {
503
			$this->dieBlocked( $user->getGlobalBlock() );
0 ignored issues
show
Bug introduced by
It seems like $user->getGlobalBlock() can be null; however, dieBlocked() 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...
504
		}
505
	}
506
507
	/**
508
	 * Performs file verification, dies on error.
509
	 */
510
	protected function verifyUpload() {
511
		$verification = $this->mUpload->verifyUpload();
512
		if ( $verification['status'] === UploadBase::OK ) {
513
			return;
514
		}
515
516
		$this->checkVerification( $verification );
517
	}
518
519
	/**
520
	 * Performs file verification, dies on error.
521
	 * @param array $verification
522
	 */
523
	protected function checkVerification( array $verification ) {
524
		// @todo Move them to ApiBase's message map
525
		switch ( $verification['status'] ) {
526
			// Recoverable errors
527
			case UploadBase::MIN_LENGTH_PARTNAME:
528
				$this->dieRecoverableError( 'filename-tooshort', 'filename' );
529
				break;
530
			case UploadBase::ILLEGAL_FILENAME:
531
				$this->dieRecoverableError( 'illegal-filename', 'filename',
532
					[ 'filename' => $verification['filtered'] ] );
533
				break;
534
			case UploadBase::FILENAME_TOO_LONG:
535
				$this->dieRecoverableError( 'filename-toolong', 'filename' );
536
				break;
537
			case UploadBase::FILETYPE_MISSING:
538
				$this->dieRecoverableError( 'filetype-missing', 'filename' );
539
				break;
540
			case UploadBase::WINDOWS_NONASCII_FILENAME:
541
				$this->dieRecoverableError( 'windows-nonascii-filename', 'filename' );
542
				break;
543
544
			// Unrecoverable errors
545
			case UploadBase::EMPTY_FILE:
546
				$this->dieUsage( 'The file you submitted was empty', 'empty-file' );
547
				break;
548
			case UploadBase::FILE_TOO_LARGE:
549
				$this->dieUsage( 'The file you submitted was too large', 'file-too-large' );
550
				break;
551
552
			case UploadBase::FILETYPE_BADTYPE:
553
				$extradata = [
554
					'filetype' => $verification['finalExt'],
555
					'allowed' => array_values( array_unique( $this->getConfig()->get( 'FileExtensions' ) ) )
556
				];
557
				ApiResult::setIndexedTagName( $extradata['allowed'], 'ext' );
558
559
				$msg = 'Filetype not permitted: ';
560
				if ( isset( $verification['blacklistedExt'] ) ) {
561
					$msg .= implode( ', ', $verification['blacklistedExt'] );
562
					$extradata['blacklisted'] = array_values( $verification['blacklistedExt'] );
563
					ApiResult::setIndexedTagName( $extradata['blacklisted'], 'ext' );
564
				} else {
565
					$msg .= $verification['finalExt'];
566
				}
567
				$this->dieUsage( $msg, 'filetype-banned', 0, $extradata );
568
				break;
569
			case UploadBase::VERIFICATION_ERROR:
570
				$parsed = $this->parseMsg( $verification['details'] );
571
				$info = "This file did not pass file verification: {$parsed['info']}";
572
				if ( $verification['details'][0] instanceof IApiMessage ) {
573
					$code = $parsed['code'];
574
				} else {
575
					// For backwards-compatibility, all of the errors from UploadBase::verifyFile() are
576
					// reported as 'verification-error', and the real error code is reported in 'details'.
577
					$code = 'verification-error';
578
				}
579
				if ( $verification['details'][0] instanceof IApiMessage ) {
580
					$msg = $verification['details'][0];
581
					$details = array_merge( [ $msg->getKey() ], $msg->getParams() );
582
				} else {
583
					$details = $verification['details'];
584
				}
585
				ApiResult::setIndexedTagName( $details, 'detail' );
586
				$data = [ 'details' => $details ];
587
				if ( isset( $parsed['data'] ) ) {
588
					$data = array_merge( $data, $parsed['data'] );
589
				}
590
591
				$this->dieUsage( $info, $code, 0, $data );
592
				break;
593
			case UploadBase::HOOK_ABORTED:
594
				if ( is_array( $verification['error'] ) ) {
595
					$params = $verification['error'];
596
				} elseif ( $verification['error'] !== '' ) {
597
					$params = [ $verification['error'] ];
598
				} else {
599
					$params = [ 'hookaborted' ];
600
				}
601
				$key = array_shift( $params );
602
				$msg = $this->msg( $key, $params )->inLanguage( 'en' )->useDatabase( false )->text();
603
				$this->dieUsage( $msg, 'hookaborted', 0, [ 'details' => $verification['error'] ] );
604
				break;
605
			default:
606
				$this->dieUsage( 'An unknown error occurred', 'unknown-error',
607
					0, [ 'details' => [ 'code' => $verification['status'] ] ] );
608
				break;
609
		}
610
	}
611
612
	/**
613
	 * Check warnings.
614
	 * Returns a suitable array for inclusion into API results if there were warnings
615
	 * Returns the empty array if there were no warnings
616
	 *
617
	 * @return array
618
	 */
619
	protected function getApiWarnings() {
620
		$warnings = $this->mUpload->checkWarnings();
621
622
		return $this->transformWarnings( $warnings );
623
	}
624
625
	protected function transformWarnings( $warnings ) {
626
		if ( $warnings ) {
627
			// Add indices
628
			ApiResult::setIndexedTagName( $warnings, 'warning' );
629
630
			if ( isset( $warnings['duplicate'] ) ) {
631
				$dupes = [];
632
				/** @var File $dupe */
633
				foreach ( $warnings['duplicate'] as $dupe ) {
634
					$dupes[] = $dupe->getName();
635
				}
636
				ApiResult::setIndexedTagName( $dupes, 'duplicate' );
637
				$warnings['duplicate'] = $dupes;
638
			}
639
640
			if ( isset( $warnings['exists'] ) ) {
641
				$warning = $warnings['exists'];
642
				unset( $warnings['exists'] );
643
				/** @var LocalFile $localFile */
644
				$localFile = isset( $warning['normalizedFile'] )
645
					? $warning['normalizedFile']
646
					: $warning['file'];
647
				$warnings[$warning['warning']] = $localFile->getName();
648
			}
649
		}
650
651
		return $warnings;
652
	}
653
654
	/**
655
	 * Handles a stash exception, giving a useful error to the user.
656
	 * @param Exception $e The exception we encountered.
657
	 */
658
	protected function handleStashException( $e ) {
659
		$exceptionType = get_class( $e );
660
661
		switch ( $exceptionType ) {
662
			case 'UploadStashFileNotFoundException':
663
				$this->dieUsage(
664
					'Could not find the file in the stash: ' . $e->getMessage(),
665
					'stashedfilenotfound'
666
				);
667
				break;
668
			case 'UploadStashBadPathException':
669
				$this->dieUsage(
670
					'File key of improper format or otherwise invalid: ' . $e->getMessage(),
671
					'stashpathinvalid'
672
				);
673
				break;
674
			case 'UploadStashFileException':
675
				$this->dieUsage(
676
					'Could not store upload in the stash: ' . $e->getMessage(),
677
					'stashfilestorage'
678
				);
679
				break;
680
			case 'UploadStashZeroLengthFileException':
681
				$this->dieUsage(
682
					'File is of zero length, and could not be stored in the stash: ' .
683
						$e->getMessage(),
684
					'stashzerolength'
685
				);
686
				break;
687
			case 'UploadStashNotLoggedInException':
688
				$this->dieUsage( 'Not logged in: ' . $e->getMessage(), 'stashnotloggedin' );
689
				break;
690
			case 'UploadStashWrongOwnerException':
691
				$this->dieUsage( 'Wrong owner: ' . $e->getMessage(), 'stashwrongowner' );
692
				break;
693
			case 'UploadStashNoSuchKeyException':
694
				$this->dieUsage( 'No such filekey: ' . $e->getMessage(), 'stashnosuchfilekey' );
695
				break;
696
			default:
697
				$this->dieUsage( $exceptionType . ': ' . $e->getMessage(), 'stasherror' );
698
				break;
699
		}
700
	}
701
702
	/**
703
	 * Perform the actual upload. Returns a suitable result array on success;
704
	 * dies on failure.
705
	 *
706
	 * @param array $warnings Array of Api upload warnings
707
	 * @return array
708
	 */
709
	protected function performUpload( $warnings ) {
710
		// Use comment as initial page text by default
711
		if ( is_null( $this->mParams['text'] ) ) {
712
			$this->mParams['text'] = $this->mParams['comment'];
713
		}
714
715
		/** @var $file File */
716
		$file = $this->mUpload->getLocalFile();
717
718
		// For preferences mode, we want to watch if 'watchdefault' is set,
719
		// or if the *file* doesn't exist, and either 'watchuploads' or
720
		// 'watchcreations' is set. But getWatchlistValue()'s automatic
721
		// handling checks if the *title* exists or not, so we need to check
722
		// all three preferences manually.
723
		$watch = $this->getWatchlistValue(
724
			$this->mParams['watchlist'], $file->getTitle(), 'watchdefault'
725
		);
726
727
		if ( !$watch && $this->mParams['watchlist'] == 'preferences' && !$file->exists() ) {
728
			$watch = (
729
				$this->getWatchlistValue( 'preferences', $file->getTitle(), 'watchuploads' ) ||
730
				$this->getWatchlistValue( 'preferences', $file->getTitle(), 'watchcreations' )
731
			);
732
		}
733
734
		// Deprecated parameters
735
		if ( $this->mParams['watch'] ) {
736
			$watch = true;
737
		}
738
739
		if ( $this->mParams['tags'] ) {
740
			$status = ChangeTags::canAddTagsAccompanyingChange( $this->mParams['tags'], $this->getUser() );
741
			if ( !$status->isOK() ) {
742
				$this->dieStatus( $status );
743
			}
744
		}
745
746
		// No errors, no warnings: do the upload
747
		if ( $this->mParams['async'] ) {
748
			$progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
749
			if ( $progress && $progress['result'] === 'Poll' ) {
750
				$this->dieUsage( 'Upload from stash already in progress.', 'publishfailed' );
751
			}
752
			UploadBase::setSessionStatus(
753
				$this->getUser(),
754
				$this->mParams['filekey'],
755
				[ 'result' => 'Poll', 'stage' => 'queued', 'status' => Status::newGood() ]
756
			);
757
			JobQueueGroup::singleton()->push( new PublishStashedFileJob(
758
				Title::makeTitle( NS_FILE, $this->mParams['filename'] ),
759
				[
760
					'filename' => $this->mParams['filename'],
761
					'filekey' => $this->mParams['filekey'],
762
					'comment' => $this->mParams['comment'],
763
					'tags' => $this->mParams['tags'],
764
					'text' => $this->mParams['text'],
765
					'watch' => $watch,
766
					'session' => $this->getContext()->exportSession()
767
				]
768
			) );
769
			$result['result'] = 'Poll';
0 ignored issues
show
Coding Style Comprehensibility introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
770
			$result['stage'] = 'queued';
771
		} else {
772
			/** @var $status Status */
773
			$status = $this->mUpload->performUpload( $this->mParams['comment'],
774
				$this->mParams['text'], $watch, $this->getUser(), $this->mParams['tags'] );
775
776
			if ( !$status->isGood() ) {
777
				// Is there really no better way to do this?
778
				$errors = $status->getErrorsByType( 'error' );
779
				$msg = array_merge( [ $errors[0]['message'] ], $errors[0]['params'] );
780
				$data = $status->getErrorsArray();
0 ignored issues
show
Deprecated Code introduced by
The method Status::getErrorsArray() has been deprecated with message: 1.25

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
781
				ApiResult::setIndexedTagName( $data, 'error' );
782
				// For backwards-compatibility, we use the 'internal-error' fallback key and merge $data
783
				// into the root of the response (rather than something sane like [ 'details' => $data ]).
784
				$this->dieRecoverableError( $msg, null, $data, 'internal-error' );
785
			}
786
			$result['result'] = 'Success';
0 ignored issues
show
Coding Style Comprehensibility introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
787
		}
788
789
		$result['filename'] = $file->getName();
790
		if ( $warnings && count( $warnings ) > 0 ) {
791
			$result['warnings'] = $warnings;
792
		}
793
794
		return $result;
795
	}
796
797
	public function mustBePosted() {
798
		return true;
799
	}
800
801
	public function isWriteMode() {
802
		return true;
803
	}
804
805
	public function getAllowedParams() {
806
		$params = [
807
			'filename' => [
808
				ApiBase::PARAM_TYPE => 'string',
809
			],
810
			'comment' => [
811
				ApiBase::PARAM_DFLT => ''
812
			],
813
			'tags' => [
814
				ApiBase::PARAM_TYPE => 'tags',
815
				ApiBase::PARAM_ISMULTI => true,
816
			],
817
			'text' => [
818
				ApiBase::PARAM_TYPE => 'text',
819
			],
820
			'watch' => [
821
				ApiBase::PARAM_DFLT => false,
822
				ApiBase::PARAM_DEPRECATED => true,
823
			],
824
			'watchlist' => [
825
				ApiBase::PARAM_DFLT => 'preferences',
826
				ApiBase::PARAM_TYPE => [
827
					'watch',
828
					'preferences',
829
					'nochange'
830
				],
831
			],
832
			'ignorewarnings' => false,
833
			'file' => [
834
				ApiBase::PARAM_TYPE => 'upload',
835
			],
836
			'url' => null,
837
			'filekey' => null,
838
			'sessionkey' => [
839
				ApiBase::PARAM_DEPRECATED => true,
840
			],
841
			'stash' => false,
842
843
			'filesize' => [
844
				ApiBase::PARAM_TYPE => 'integer',
845
				ApiBase::PARAM_MIN => 0,
846
				ApiBase::PARAM_MAX => UploadBase::getMaxUploadSize(),
847
			],
848
			'offset' => [
849
				ApiBase::PARAM_TYPE => 'integer',
850
				ApiBase::PARAM_MIN => 0,
851
			],
852
			'chunk' => [
853
				ApiBase::PARAM_TYPE => 'upload',
854
			],
855
856
			'async' => false,
857
			'checkstatus' => false,
858
		];
859
860
		return $params;
861
	}
862
863
	public function needsToken() {
864
		return 'csrf';
865
	}
866
867
	protected function getExamplesMessages() {
868
		return [
869
			'action=upload&filename=Wiki.png' .
870
				'&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png&token=123ABC'
871
				=> 'apihelp-upload-example-url',
872
			'action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1&token=123ABC'
873
				=> 'apihelp-upload-example-filekey',
874
		];
875
	}
876
877
	public function getHelpUrls() {
878
		return 'https://www.mediawiki.org/wiki/API:Upload';
879
	}
880
}
881