Completed
Branch master (86dc85)
by
unknown
23:45
created

ApiUpload::performUpload()   F

Complexity

Conditions 15
Paths 288

Size

Total Lines 82
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
dl 0
loc 82
rs 3.756
c 1
b 0
f 1
cc 15
eloc 47
nc 288
nop 1

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
493
	/**
494
	 * Performs file verification, dies on error.
495
	 */
496
	protected function verifyUpload() {
497
		$verification = $this->mUpload->verifyUpload();
498
		if ( $verification['status'] === UploadBase::OK ) {
499
			return;
500
		}
501
502
		$this->checkVerification( $verification );
503
	}
504
505
	/**
506
	 * Performs file verification, dies on error.
507
	 * @param array $verification
508
	 */
509
	protected function checkVerification( array $verification ) {
510
		// @todo Move them to ApiBase's message map
511
		switch ( $verification['status'] ) {
512
			// Recoverable errors
513
			case UploadBase::MIN_LENGTH_PARTNAME:
514
				$this->dieRecoverableError( 'filename-tooshort', 'filename' );
515
				break;
516
			case UploadBase::ILLEGAL_FILENAME:
517
				$this->dieRecoverableError( 'illegal-filename', 'filename',
518
					[ 'filename' => $verification['filtered'] ] );
519
				break;
520
			case UploadBase::FILENAME_TOO_LONG:
521
				$this->dieRecoverableError( 'filename-toolong', 'filename' );
522
				break;
523
			case UploadBase::FILETYPE_MISSING:
524
				$this->dieRecoverableError( 'filetype-missing', 'filename' );
525
				break;
526
			case UploadBase::WINDOWS_NONASCII_FILENAME:
527
				$this->dieRecoverableError( 'windows-nonascii-filename', 'filename' );
528
				break;
529
530
			// Unrecoverable errors
531
			case UploadBase::EMPTY_FILE:
532
				$this->dieUsage( 'The file you submitted was empty', 'empty-file' );
533
				break;
534
			case UploadBase::FILE_TOO_LARGE:
535
				$this->dieUsage( 'The file you submitted was too large', 'file-too-large' );
536
				break;
537
538
			case UploadBase::FILETYPE_BADTYPE:
539
				$extradata = [
540
					'filetype' => $verification['finalExt'],
541
					'allowed' => array_values( array_unique( $this->getConfig()->get( 'FileExtensions' ) ) )
542
				];
543
				ApiResult::setIndexedTagName( $extradata['allowed'], 'ext' );
544
545
				$msg = 'Filetype not permitted: ';
546
				if ( isset( $verification['blacklistedExt'] ) ) {
547
					$msg .= implode( ', ', $verification['blacklistedExt'] );
548
					$extradata['blacklisted'] = array_values( $verification['blacklistedExt'] );
549
					ApiResult::setIndexedTagName( $extradata['blacklisted'], 'ext' );
550
				} else {
551
					$msg .= $verification['finalExt'];
552
				}
553
				$this->dieUsage( $msg, 'filetype-banned', 0, $extradata );
554
				break;
555
			case UploadBase::VERIFICATION_ERROR:
556
				$params = $verification['details'];
557
				$key = array_shift( $params );
558
				$msg = $this->msg( $key, $params )->inLanguage( 'en' )->useDatabase( false )->text();
559
				ApiResult::setIndexedTagName( $verification['details'], 'detail' );
560
				$this->dieUsage( "This file did not pass file verification: $msg", 'verification-error',
561
					0, [ 'details' => $verification['details'] ] );
562
				break;
563
			case UploadBase::HOOK_ABORTED:
564
				if ( is_array( $verification['error'] ) ) {
565
					$params = $verification['error'];
566
				} elseif ( $verification['error'] !== '' ) {
567
					$params = [ $verification['error'] ];
568
				} else {
569
					$params = [ 'hookaborted' ];
570
				}
571
				$key = array_shift( $params );
572
				$msg = $this->msg( $key, $params )->inLanguage( 'en' )->useDatabase( false )->text();
573
				$this->dieUsage( $msg, 'hookaborted', 0, [ 'details' => $verification['error'] ] );
574
				break;
575
			default:
576
				$this->dieUsage( 'An unknown error occurred', 'unknown-error',
577
					0, [ 'details' => [ 'code' => $verification['status'] ] ] );
578
				break;
579
		}
580
	}
581
582
	/**
583
	 * Check warnings.
584
	 * Returns a suitable array for inclusion into API results if there were warnings
585
	 * Returns the empty array if there were no warnings
586
	 *
587
	 * @return array
588
	 */
589
	protected function getApiWarnings() {
590
		$warnings = $this->mUpload->checkWarnings();
591
592
		return $this->transformWarnings( $warnings );
593
	}
594
595
	protected function transformWarnings( $warnings ) {
596
		if ( $warnings ) {
597
			// Add indices
598
			ApiResult::setIndexedTagName( $warnings, 'warning' );
599
600
			if ( isset( $warnings['duplicate'] ) ) {
601
				$dupes = [];
602
				/** @var File $dupe */
603
				foreach ( $warnings['duplicate'] as $dupe ) {
604
					$dupes[] = $dupe->getName();
605
				}
606
				ApiResult::setIndexedTagName( $dupes, 'duplicate' );
607
				$warnings['duplicate'] = $dupes;
608
			}
609
610
			if ( isset( $warnings['exists'] ) ) {
611
				$warning = $warnings['exists'];
612
				unset( $warnings['exists'] );
613
				/** @var LocalFile $localFile */
614
				$localFile = isset( $warning['normalizedFile'] )
615
					? $warning['normalizedFile']
616
					: $warning['file'];
617
				$warnings[$warning['warning']] = $localFile->getName();
618
			}
619
		}
620
621
		return $warnings;
622
	}
623
624
	/**
625
	 * Handles a stash exception, giving a useful error to the user.
626
	 * @param Exception $e The exception we encountered.
627
	 */
628
	protected function handleStashException( $e ) {
629
		$exceptionType = get_class( $e );
630
631
		switch ( $exceptionType ) {
632
			case 'UploadStashFileNotFoundException':
633
				$this->dieUsage(
634
					'Could not find the file in the stash: ' . $e->getMessage(),
635
					'stashedfilenotfound'
636
				);
637
				break;
638
			case 'UploadStashBadPathException':
639
				$this->dieUsage(
640
					'File key of improper format or otherwise invalid: ' . $e->getMessage(),
641
					'stashpathinvalid'
642
				);
643
				break;
644
			case 'UploadStashFileException':
645
				$this->dieUsage(
646
					'Could not store upload in the stash: ' . $e->getMessage(),
647
					'stashfilestorage'
648
				);
649
				break;
650
			case 'UploadStashZeroLengthFileException':
651
				$this->dieUsage(
652
					'File is of zero length, and could not be stored in the stash: ' .
653
						$e->getMessage(),
654
					'stashzerolength'
655
				);
656
				break;
657
			case 'UploadStashNotLoggedInException':
658
				$this->dieUsage( 'Not logged in: ' . $e->getMessage(), 'stashnotloggedin' );
659
				break;
660
			case 'UploadStashWrongOwnerException':
661
				$this->dieUsage( 'Wrong owner: ' . $e->getMessage(), 'stashwrongowner' );
662
				break;
663
			case 'UploadStashNoSuchKeyException':
664
				$this->dieUsage( 'No such filekey: ' . $e->getMessage(), 'stashnosuchfilekey' );
665
				break;
666
			default:
667
				$this->dieUsage( $exceptionType . ': ' . $e->getMessage(), 'stasherror' );
668
				break;
669
		}
670
	}
671
672
	/**
673
	 * Perform the actual upload. Returns a suitable result array on success;
674
	 * dies on failure.
675
	 *
676
	 * @param array $warnings Array of Api upload warnings
677
	 * @return array
678
	 */
679
	protected function performUpload( $warnings ) {
680
		// Use comment as initial page text by default
681
		if ( is_null( $this->mParams['text'] ) ) {
682
			$this->mParams['text'] = $this->mParams['comment'];
683
		}
684
685
		/** @var $file File */
686
		$file = $this->mUpload->getLocalFile();
687
688
		// For preferences mode, we want to watch if 'watchdefault' is set,
689
		// or if the *file* doesn't exist, and either 'watchuploads' or
690
		// 'watchcreations' is set. But getWatchlistValue()'s automatic
691
		// handling checks if the *title* exists or not, so we need to check
692
		// all three preferences manually.
693
		$watch = $this->getWatchlistValue(
694
			$this->mParams['watchlist'], $file->getTitle(), 'watchdefault'
695
		);
696
697
		if ( !$watch && $this->mParams['watchlist'] == 'preferences' && !$file->exists() ) {
698
			$watch = (
699
				$this->getWatchlistValue( 'preferences', $file->getTitle(), 'watchuploads' ) ||
700
				$this->getWatchlistValue( 'preferences', $file->getTitle(), 'watchcreations' )
701
			);
702
		}
703
704
		// Deprecated parameters
705
		if ( $this->mParams['watch'] ) {
706
			$watch = true;
707
		}
708
709
		if ( $this->mParams['tags'] ) {
710
			$status = ChangeTags::canAddTagsAccompanyingChange( $this->mParams['tags'], $this->getUser() );
711
			if ( !$status->isOK() ) {
712
				$this->dieStatus( $status );
713
			}
714
		}
715
716
		// No errors, no warnings: do the upload
717
		if ( $this->mParams['async'] ) {
718
			$progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
719
			if ( $progress && $progress['result'] === 'Poll' ) {
720
				$this->dieUsage( 'Upload from stash already in progress.', 'publishfailed' );
721
			}
722
			UploadBase::setSessionStatus(
723
				$this->getUser(),
724
				$this->mParams['filekey'],
725
				[ 'result' => 'Poll', 'stage' => 'queued', 'status' => Status::newGood() ]
726
			);
727
			JobQueueGroup::singleton()->push( new PublishStashedFileJob(
728
				Title::makeTitle( NS_FILE, $this->mParams['filename'] ),
729
				[
730
					'filename' => $this->mParams['filename'],
731
					'filekey' => $this->mParams['filekey'],
732
					'comment' => $this->mParams['comment'],
733
					'tags' => $this->mParams['tags'],
734
					'text' => $this->mParams['text'],
735
					'watch' => $watch,
736
					'session' => $this->getContext()->exportSession()
737
				]
738
			) );
739
			$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...
740
			$result['stage'] = 'queued';
741
		} else {
742
			/** @var $status Status */
743
			$status = $this->mUpload->performUpload( $this->mParams['comment'],
744
				$this->mParams['text'], $watch, $this->getUser(), $this->mParams['tags'] );
745
746
			if ( !$status->isGood() ) {
747
				$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...
748
				ApiResult::setIndexedTagName( $error, 'error' );
749
				$this->dieUsage( 'An internal error occurred', 'internal-error', 0, $error );
750
			}
751
			$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...
752
		}
753
754
		$result['filename'] = $file->getName();
755
		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...
756
			$result['warnings'] = $warnings;
757
		}
758
759
		return $result;
760
	}
761
762
	public function mustBePosted() {
763
		return true;
764
	}
765
766
	public function isWriteMode() {
767
		return true;
768
	}
769
770
	public function getAllowedParams() {
771
		$params = [
772
			'filename' => [
773
				ApiBase::PARAM_TYPE => 'string',
774
			],
775
			'comment' => [
776
				ApiBase::PARAM_DFLT => ''
777
			],
778
			'tags' => [
779
				ApiBase::PARAM_TYPE => 'tags',
780
				ApiBase::PARAM_ISMULTI => true,
781
			],
782
			'text' => [
783
				ApiBase::PARAM_TYPE => 'text',
784
			],
785
			'watch' => [
786
				ApiBase::PARAM_DFLT => false,
787
				ApiBase::PARAM_DEPRECATED => true,
788
			],
789
			'watchlist' => [
790
				ApiBase::PARAM_DFLT => 'preferences',
791
				ApiBase::PARAM_TYPE => [
792
					'watch',
793
					'preferences',
794
					'nochange'
795
				],
796
			],
797
			'ignorewarnings' => false,
798
			'file' => [
799
				ApiBase::PARAM_TYPE => 'upload',
800
			],
801
			'url' => null,
802
			'filekey' => null,
803
			'sessionkey' => [
804
				ApiBase::PARAM_DEPRECATED => true,
805
			],
806
			'stash' => false,
807
808
			'filesize' => [
809
				ApiBase::PARAM_TYPE => 'integer',
810
				ApiBase::PARAM_MIN => 0,
811
				ApiBase::PARAM_MAX => UploadBase::getMaxUploadSize(),
812
			],
813
			'offset' => [
814
				ApiBase::PARAM_TYPE => 'integer',
815
				ApiBase::PARAM_MIN => 0,
816
			],
817
			'chunk' => [
818
				ApiBase::PARAM_TYPE => 'upload',
819
			],
820
821
			'async' => false,
822
			'checkstatus' => false,
823
		];
824
825
		return $params;
826
	}
827
828
	public function needsToken() {
829
		return 'csrf';
830
	}
831
832
	protected function getExamplesMessages() {
833
		return [
834
			'action=upload&filename=Wiki.png' .
835
				'&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png&token=123ABC'
836
				=> 'apihelp-upload-example-url',
837
			'action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1&token=123ABC'
838
				=> 'apihelp-upload-example-filekey',
839
		];
840
	}
841
842
	public function getHelpUrls() {
843
		return 'https://www.mediawiki.org/wiki/API:Upload';
844
	}
845
}
846