Completed
Branch master (33c24b)
by
unknown
30:03
created

ApiUpload::execute()   F

Complexity

Conditions 18
Paths 5768

Size

Total Lines 87
Code Lines 47

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 87
rs 2
cc 18
eloc 47
nc 5768
nop 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'] ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $warnings of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $warnings of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $warnings of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

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

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
766
			$result['warnings'] = $warnings;
767
		}
768
769
		return $result;
770
	}
771
772
	public function mustBePosted() {
773
		return true;
774
	}
775
776
	public function isWriteMode() {
777
		return true;
778
	}
779
780
	public function getAllowedParams() {
781
		$params = [
782
			'filename' => [
783
				ApiBase::PARAM_TYPE => 'string',
784
			],
785
			'comment' => [
786
				ApiBase::PARAM_DFLT => ''
787
			],
788
			'tags' => [
789
				ApiBase::PARAM_TYPE => 'tags',
790
				ApiBase::PARAM_ISMULTI => true,
791
			],
792
			'text' => [
793
				ApiBase::PARAM_TYPE => 'text',
794
			],
795
			'watch' => [
796
				ApiBase::PARAM_DFLT => false,
797
				ApiBase::PARAM_DEPRECATED => true,
798
			],
799
			'watchlist' => [
800
				ApiBase::PARAM_DFLT => 'preferences',
801
				ApiBase::PARAM_TYPE => [
802
					'watch',
803
					'preferences',
804
					'nochange'
805
				],
806
			],
807
			'ignorewarnings' => false,
808
			'file' => [
809
				ApiBase::PARAM_TYPE => 'upload',
810
			],
811
			'url' => null,
812
			'filekey' => null,
813
			'sessionkey' => [
814
				ApiBase::PARAM_DEPRECATED => true,
815
			],
816
			'stash' => false,
817
818
			'filesize' => [
819
				ApiBase::PARAM_TYPE => 'integer',
820
				ApiBase::PARAM_MIN => 0,
821
				ApiBase::PARAM_MAX => UploadBase::getMaxUploadSize(),
822
			],
823
			'offset' => [
824
				ApiBase::PARAM_TYPE => 'integer',
825
				ApiBase::PARAM_MIN => 0,
826
			],
827
			'chunk' => [
828
				ApiBase::PARAM_TYPE => 'upload',
829
			],
830
831
			'async' => false,
832
			'checkstatus' => false,
833
		];
834
835
		return $params;
836
	}
837
838
	public function needsToken() {
839
		return 'csrf';
840
	}
841
842
	protected function getExamplesMessages() {
843
		return [
844
			'action=upload&filename=Wiki.png' .
845
				'&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png&token=123ABC'
846
				=> 'apihelp-upload-example-url',
847
			'action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1&token=123ABC'
848
				=> 'apihelp-upload-example-filekey',
849
		];
850
	}
851
852
	public function getHelpUrls() {
853
		return 'https://www.mediawiki.org/wiki/API:Upload';
854
	}
855
}
856