Completed
Branch master (d7c4e6)
by
unknown
29:20
created

ApiUpload::dieStatusWithCode()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 5
eloc 10
c 2
b 1
f 0
nc 12
nop 3
dl 0
loc 13
rs 8.8571
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
			list( $msg, $code ) = $this->handleStashException( get_class( $e ), $e->getMessage() );
68
			$this->dieUsage( $msg, $code );
69
		}
70
71
		// First check permission to upload
72
		$this->checkPermissions( $user );
73
74
		// Fetch the file (usually a no-op)
75
		/** @var $status Status */
76
		$status = $this->mUpload->fetchFile();
77
		if ( !$status->isGood() ) {
78
			$errors = $status->getErrorsArray();
79
			$error = array_shift( $errors[0] );
80
			$this->dieUsage( 'Error fetching file from remote source', $error, 0, $errors[0] );
81
		}
82
83
		// Check if the uploaded file is sane
84
		if ( $this->mParams['chunk'] ) {
85
			$maxSize = UploadBase::getMaxUploadSize();
86
			if ( $this->mParams['filesize'] > $maxSize ) {
87
				$this->dieUsage( 'The file you submitted was too large', 'file-too-large' );
88
			}
89
			if ( !$this->mUpload->getTitle() ) {
90
				$this->dieUsage( 'Invalid file title supplied', 'internal-error' );
91
			}
92
		} 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...
93
			// defer verification to background process
94
		} else {
95
			wfDebug( __METHOD__ . " about to verify\n" );
96
			$this->verifyUpload();
97
		}
98
99
		// Check if the user has the rights to modify or overwrite the requested title
100
		// (This check is irrelevant if stashing is already requested, since the errors
101
		//  can always be fixed by changing the title)
102
		if ( !$this->mParams['stash'] ) {
103
			$permErrors = $this->mUpload->verifyTitlePermissions( $user );
104
			if ( $permErrors !== true ) {
105
				$this->dieRecoverableError( $permErrors[0], 'filename' );
106
			}
107
		}
108
109
		// Get the result based on the current upload context:
110
		try {
111
			$result = $this->getContextResult();
112
			if ( $result['result'] === 'Success' ) {
113
				$result['imageinfo'] = $this->mUpload->getImageInfo( $this->getResult() );
114
			}
115
		} catch ( UploadStashException $e ) { // XXX: don't spam exception log
116
			list( $msg, $code ) = $this->handleStashException( get_class( $e ), $e->getMessage() );
117
			$this->dieUsage( $msg, $code );
118
		}
119
120
		$this->getResult()->addValue( null, $this->getModuleName(), $result );
121
122
		// Cleanup any temporary mess
123
		$this->mUpload->cleanupTempFile();
124
	}
125
126
	/**
127
	 * Get an upload result based on upload context
128
	 * @return array
129
	 */
130
	private function getContextResult() {
131
		$warnings = $this->getApiWarnings();
132
		if ( $warnings && !$this->mParams['ignorewarnings'] ) {
133
			// Get warnings formatted in result array format
134
			return $this->getWarningsResult( $warnings );
135
		} elseif ( $this->mParams['chunk'] ) {
136
			// Add chunk, and get result
137
			return $this->getChunkResult( $warnings );
138
		} elseif ( $this->mParams['stash'] ) {
139
			// Stash the file and get stash result
140
			return $this->getStashResult( $warnings );
141
		}
142
143
		// Check throttle after we've handled warnings
144
		if ( UploadBase::isThrottled( $this->getUser() )
145
		) {
146
			$this->dieUsageMsg( 'actionthrottledtext' );
147
		}
148
149
		// This is the most common case -- a normal upload with no warnings
150
		// performUpload will return a formatted properly for the API with status
151
		return $this->performUpload( $warnings );
152
	}
153
154
	/**
155
	 * Get Stash Result, throws an exception if the file could not be stashed.
156
	 * @param array $warnings Array of Api upload warnings
157
	 * @return array
158
	 */
159
	private function getStashResult( $warnings ) {
160
		$result = [];
161
		$result['result'] = 'Success';
162
		if ( $warnings && count( $warnings ) > 0 ) {
163
			$result['warnings'] = $warnings;
164
		}
165
		// Some uploads can request they be stashed, so as not to publish them immediately.
166
		// In this case, a failure to stash ought to be fatal
167
		$this->performStash( 'critical', $result );
168
169
		return $result;
170
	}
171
172
	/**
173
	 * Get Warnings Result
174
	 * @param array $warnings Array of Api upload warnings
175
	 * @return array
176
	 */
177
	private function getWarningsResult( $warnings ) {
178
		$result = [];
179
		$result['result'] = 'Warning';
180
		$result['warnings'] = $warnings;
181
		// in case the warnings can be fixed with some further user action, let's stash this upload
182
		// and return a key they can use to restart it
183
		$this->performStash( 'optional', $result );
184
185
		return $result;
186
	}
187
188
	/**
189
	 * Get the result of a chunk upload.
190
	 * @param array $warnings Array of Api upload warnings
191
	 * @return array
192
	 */
193
	private function getChunkResult( $warnings ) {
194
		$result = [];
195
196
		if ( $warnings && count( $warnings ) > 0 ) {
197
			$result['warnings'] = $warnings;
198
		}
199
200
		$request = $this->getMain()->getRequest();
201
		$chunkPath = $request->getFileTempname( 'chunk' );
202
		$chunkSize = $request->getUpload( 'chunk' )->getSize();
203
		$totalSoFar = $this->mParams['offset'] + $chunkSize;
204
		$minChunkSize = $this->getConfig()->get( 'MinUploadChunkSize' );
205
206
		// Sanity check sizing
207
		if ( $totalSoFar > $this->mParams['filesize'] ) {
208
			$this->dieUsage(
209
				'Offset plus current chunk is greater than claimed file size', 'invalid-chunk'
210
			);
211
		}
212
213
		// Enforce minimum chunk size
214
		if ( $totalSoFar != $this->mParams['filesize'] && $chunkSize < $minChunkSize ) {
215
			$this->dieUsage(
216
				"Minimum chunk size is $minChunkSize bytes for non-final chunks", 'chunk-too-small'
217
			);
218
		}
219
220
		if ( $this->mParams['offset'] == 0 ) {
221
			$filekey = $this->performStash( 'critical' );
222
		} else {
223
			$filekey = $this->mParams['filekey'];
224
225
			// Don't allow further uploads to an already-completed session
226
			$progress = UploadBase::getSessionStatus( $this->getUser(), $filekey );
227
			if ( !$progress ) {
228
				// Probably can't get here, but check anyway just in case
229
				$this->dieUsage( 'No chunked upload session with this key', 'stashfailed' );
230
			} elseif ( $progress['result'] !== 'Continue' || $progress['stage'] !== 'uploading' ) {
231
				$this->dieUsage(
232
					'Chunked upload is already completed, check status for details', 'stashfailed'
233
				);
234
			}
235
236
			$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...
237
				$chunkPath, $chunkSize, $this->mParams['offset'] );
238
			if ( !$status->isGood() ) {
239
				$extradata = [
240
					'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...
241
				];
242
243
				$this->dieStatusWithCode( $status, 'stashfailed', $extradata );
244
			}
245
		}
246
247
		// Check we added the last chunk:
248
		if ( $totalSoFar == $this->mParams['filesize'] ) {
249
			if ( $this->mParams['async'] ) {
250
				UploadBase::setSessionStatus(
251
					$this->getUser(),
252
					$filekey,
253
					[ 'result' => 'Poll',
254
						'stage' => 'queued', 'status' => Status::newGood() ]
255
				);
256
				JobQueueGroup::singleton()->push( new AssembleUploadChunksJob(
257
					Title::makeTitle( NS_FILE, $filekey ),
258
					[
259
						'filename' => $this->mParams['filename'],
260
						'filekey' => $filekey,
261
						'session' => $this->getContext()->exportSession()
262
					]
263
				) );
264
				$result['result'] = 'Poll';
265
				$result['stage'] = 'queued';
266
			} else {
267
				$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...
268
				if ( !$status->isGood() ) {
269
					UploadBase::setSessionStatus(
270
						$this->getUser(),
271
						$filekey,
272
						[ 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status ]
273
					);
274
					$this->dieStatusWithCode( $status, 'stashfailed' );
275
				}
276
277
				// We can only get warnings like 'duplicate' after concatenating the chunks
278
				$warnings = $this->getApiWarnings();
279
				if ( $warnings ) {
280
					$result['warnings'] = $warnings;
281
				}
282
283
				// The fully concatenated file has a new filekey. So remove
284
				// the old filekey and fetch the new one.
285
				UploadBase::setSessionStatus( $this->getUser(), $filekey, false );
286
				$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...
287
				$filekey = $this->mUpload->getStashFile()->getFileKey();
288
289
				$result['result'] = 'Success';
290
			}
291
		} else {
292
			UploadBase::setSessionStatus(
293
				$this->getUser(),
294
				$filekey,
295
				[
296
					'result' => 'Continue',
297
					'stage' => 'uploading',
298
					'offset' => $totalSoFar,
299
					'status' => Status::newGood(),
300
				]
301
			);
302
			$result['result'] = 'Continue';
303
			$result['offset'] = $totalSoFar;
304
		}
305
306
		$result['filekey'] = $filekey;
307
308
		return $result;
309
	}
310
311
	/**
312
	 * Stash the file and add the file key, or error information if it fails, to the data.
313
	 *
314
	 * @param string $failureMode What to do on failure to stash:
315
	 *   - When 'critical', use dieStatus() to produce an error response and throw an exception.
316
	 *     Use this when stashing the file was the primary purpose of the API request.
317
	 *   - When 'optional', only add a 'stashfailed' key to the data and return null.
318
	 *     Use this when some error happened for a non-stash upload and we're stashing the file
319
	 *     only to save the client the trouble of re-uploading it.
320
	 * @param array &$data API result to which to add the information
321
	 * @return string|null File key
322
	 */
323
	private function performStash( $failureMode, &$data = null ) {
324
		$isPartial = (bool)$this->mParams['chunk'];
325
		try {
326
			$status = $this->mUpload->tryStashFile( $this->getUser(), $isPartial );
327
328
			if ( $status->isGood() && !$status->getValue() ) {
329
				// Not actually a 'good' status...
330
				$status->fatal( new ApiRawMessage( 'Invalid stashed file', 'stashfailed' ) );
331
			}
332
		} catch ( Exception $e ) {
333
			$debugMessage = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage();
334
			wfDebug( __METHOD__ . ' ' . $debugMessage . "\n" );
335
			$status = Status::newFatal( new ApiRawMessage( $e->getMessage(), 'stashfailed' ) );
336
		}
337
338
		if ( $status->isGood() ) {
339
			$stashFile = $status->getValue();
340
			$data['filekey'] = $stashFile->getFileKey();
341
			// Backwards compatibility
342
			$data['sessionkey'] = $data['filekey'];
343
			return $data['filekey'];
344
		}
345
346
		if ( $status->getMessage()->getKey() === 'uploadstash-exception' ) {
347
			// The exceptions thrown by upload stash code and pretty silly and UploadBase returns poor
348
			// Statuses for it. Just extract the exception details and parse them ourselves.
349
			list( $exceptionType, $message ) = $status->getMessage()->getParams();
350
			$debugMessage = 'Stashing temporary file failed: ' . $exceptionType . ' ' . $message;
351
			wfDebug( __METHOD__ . ' ' . $debugMessage . "\n" );
352
			list( $msg, $code ) = $this->handleStashException( $exceptionType, $message );
353
			$status = Status::newFatal( new ApiRawMessage( $msg, $code ) );
354
		}
355
356
		// Bad status
357
		if ( $failureMode !== 'optional' ) {
358
			$this->dieStatus( $status );
359
		} else {
360
			list( $code, $msg ) = $this->getErrorFromStatus( $status );
0 ignored issues
show
Unused Code introduced by
The assignment to $code is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
361
			$data['stashfailed'] = $msg;
362
			return null;
363
		}
364
	}
365
366
	/**
367
	 * Throw an error that the user can recover from by providing a better
368
	 * value for $parameter
369
	 *
370
	 * @param array|string|MessageSpecifier $error Error suitable for passing to dieUsageMsg()
371
	 * @param string $parameter Parameter that needs revising
372
	 * @param array $data Optional extra data to pass to the user
373
	 * @param string $code Error code to use if the error is unknown
374
	 * @throws UsageException
375
	 */
376
	private function dieRecoverableError( $error, $parameter, $data = [], $code = 'unknownerror' ) {
377
		$this->performStash( 'optional', $data );
378
		$data['invalidparameter'] = $parameter;
379
380
		$parsed = $this->parseMsg( $error );
381
		if ( isset( $parsed['data'] ) ) {
382
			$data = array_merge( $data, $parsed['data'] );
383
		}
384
		if ( $parsed['code'] === 'unknownerror' ) {
385
			$parsed['code'] = $code;
386
		}
387
388
		$this->dieUsage( $parsed['info'], $parsed['code'], 0, $data );
389
	}
390
391
	/**
392
	 * Like dieStatus(), but always uses $overrideCode for the error code, unless the code comes from
393
	 * IApiMessage.
394
	 *
395
	 * @param Status $status
396
	 * @param string $overrideCode Error code to use if there isn't one from IApiMessage
397
	 * @param array|null $moreExtraData
398
	 * @throws UsageException
399
	 */
400
	public function dieStatusWithCode( $status, $overrideCode, $moreExtraData = null ) {
401
		$extraData = null;
402
		list( $code, $msg ) = $this->getErrorFromStatus( $status, $extraData );
403
		$errors = $status->getErrorsByType( 'error' ) ?: $status->getErrorsByType( 'warning' );
404
		if ( !( $errors[0]['message'] instanceof IApiMessage ) ) {
405
			$code = $overrideCode;
406
		}
407
		if ( $moreExtraData ) {
408
			$extraData = $extraData ?: [];
409
			$extraData += $moreExtraData;
410
		}
411
		$this->dieUsage( $msg, $code, 0, $extraData );
412
	}
413
414
	/**
415
	 * Select an upload module and set it to mUpload. Dies on failure. If the
416
	 * request was a status request and not a true upload, returns false;
417
	 * otherwise true
418
	 *
419
	 * @return bool
420
	 */
421
	protected function selectUploadModule() {
422
		$request = $this->getMain()->getRequest();
423
424
		// chunk or one and only one of the following parameters is needed
425
		if ( !$this->mParams['chunk'] ) {
426
			$this->requireOnlyOneParameter( $this->mParams,
427
				'filekey', 'file', 'url' );
428
		}
429
430
		// Status report for "upload to stash"/"upload from stash"
431
		if ( $this->mParams['filekey'] && $this->mParams['checkstatus'] ) {
432
			$progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
433
			if ( !$progress ) {
434
				$this->dieUsage( 'No result in status data', 'missingresult' );
435
			} elseif ( !$progress['status']->isGood() ) {
436
				$this->dieStatusWithCode( $progress['status'], 'stashfailed' );
437
			}
438
			if ( isset( $progress['status']->value['verification'] ) ) {
439
				$this->checkVerification( $progress['status']->value['verification'] );
440
			}
441
			if ( isset( $progress['status']->value['warnings'] ) ) {
442
				$warnings = $this->transformWarnings( $progress['status']->value['warnings'] );
443
				if ( $warnings ) {
444
					$progress['warnings'] = $warnings;
445
				}
446
			}
447
			unset( $progress['status'] ); // remove Status object
448
			$this->getResult()->addValue( null, $this->getModuleName(), $progress );
449
450
			return false;
451
		}
452
453
		// The following modules all require the filename parameter to be set
454
		if ( is_null( $this->mParams['filename'] ) ) {
455
			$this->dieUsageMsg( [ 'missingparam', 'filename' ] );
456
		}
457
458
		if ( $this->mParams['chunk'] ) {
459
			// Chunk upload
460
			$this->mUpload = new UploadFromChunks( $this->getUser() );
461
			if ( isset( $this->mParams['filekey'] ) ) {
462
				if ( $this->mParams['offset'] === 0 ) {
463
					$this->dieUsage( 'Cannot supply a filekey when offset is 0', 'badparams' );
464
				}
465
466
				// handle new chunk
467
				$this->mUpload->continueChunks(
468
					$this->mParams['filename'],
469
					$this->mParams['filekey'],
470
					$request->getUpload( 'chunk' )
471
				);
472
			} else {
473
				if ( $this->mParams['offset'] !== 0 ) {
474
					$this->dieUsage( 'Must supply a filekey when offset is non-zero', 'badparams' );
475
				}
476
477
				// handle first chunk
478
				$this->mUpload->initialize(
479
					$this->mParams['filename'],
480
					$request->getUpload( 'chunk' )
481
				);
482
			}
483
		} elseif ( isset( $this->mParams['filekey'] ) ) {
484
			// Upload stashed in a previous request
485
			if ( !UploadFromStash::isValidKey( $this->mParams['filekey'] ) ) {
486
				$this->dieUsageMsg( 'invalid-file-key' );
487
			}
488
489
			$this->mUpload = new UploadFromStash( $this->getUser() );
490
			// This will not download the temp file in initialize() in async mode.
491
			// We still have enough information to call checkWarnings() and such.
492
			$this->mUpload->initialize(
493
				$this->mParams['filekey'], $this->mParams['filename'], !$this->mParams['async']
494
			);
495
		} elseif ( isset( $this->mParams['file'] ) ) {
496
			$this->mUpload = new UploadFromFile();
497
			$this->mUpload->initialize(
498
				$this->mParams['filename'],
499
				$request->getUpload( 'file' )
500
			);
501
		} elseif ( isset( $this->mParams['url'] ) ) {
502
			// Make sure upload by URL is enabled:
503
			if ( !UploadFromUrl::isEnabled() ) {
504
				$this->dieUsageMsg( 'copyuploaddisabled' );
505
			}
506
507
			if ( !UploadFromUrl::isAllowedHost( $this->mParams['url'] ) ) {
508
				$this->dieUsageMsg( 'copyuploadbaddomain' );
509
			}
510
511
			if ( !UploadFromUrl::isAllowedUrl( $this->mParams['url'] ) ) {
512
				$this->dieUsageMsg( 'copyuploadbadurl' );
513
			}
514
515
			$this->mUpload = new UploadFromUrl;
516
			$this->mUpload->initialize( $this->mParams['filename'],
517
				$this->mParams['url'] );
518
		}
519
520
		return true;
521
	}
522
523
	/**
524
	 * Checks that the user has permissions to perform this upload.
525
	 * Dies with usage message on inadequate permissions.
526
	 * @param User $user The user to check.
527
	 */
528
	protected function checkPermissions( $user ) {
529
		// Check whether the user has the appropriate permissions to upload anyway
530
		$permission = $this->mUpload->isAllowed( $user );
531
532
		if ( $permission !== true ) {
533
			if ( !$user->isLoggedIn() ) {
534
				$this->dieUsageMsg( [ 'mustbeloggedin', 'upload' ] );
535
			}
536
537
			$this->dieUsageMsg( 'badaccess-groups' );
538
		}
539
540
		// Check blocks
541
		if ( $user->isBlocked() ) {
542
			$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...
543
		}
544
545
		// Global blocks
546
		if ( $user->isBlockedGlobally() ) {
547
			$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...
548
		}
549
	}
550
551
	/**
552
	 * Performs file verification, dies on error.
553
	 */
554
	protected function verifyUpload() {
555
		$verification = $this->mUpload->verifyUpload();
556
		if ( $verification['status'] === UploadBase::OK ) {
557
			return;
558
		}
559
560
		$this->checkVerification( $verification );
561
	}
562
563
	/**
564
	 * Performs file verification, dies on error.
565
	 * @param array $verification
566
	 */
567
	protected function checkVerification( array $verification ) {
568
		// @todo Move them to ApiBase's message map
569
		switch ( $verification['status'] ) {
570
			// Recoverable errors
571
			case UploadBase::MIN_LENGTH_PARTNAME:
572
				$this->dieRecoverableError( 'filename-tooshort', 'filename' );
573
				break;
574
			case UploadBase::ILLEGAL_FILENAME:
575
				$this->dieRecoverableError( 'illegal-filename', 'filename',
576
					[ 'filename' => $verification['filtered'] ] );
577
				break;
578
			case UploadBase::FILENAME_TOO_LONG:
579
				$this->dieRecoverableError( 'filename-toolong', 'filename' );
580
				break;
581
			case UploadBase::FILETYPE_MISSING:
582
				$this->dieRecoverableError( 'filetype-missing', 'filename' );
583
				break;
584
			case UploadBase::WINDOWS_NONASCII_FILENAME:
585
				$this->dieRecoverableError( 'windows-nonascii-filename', 'filename' );
586
				break;
587
588
			// Unrecoverable errors
589
			case UploadBase::EMPTY_FILE:
590
				$this->dieUsage( 'The file you submitted was empty', 'empty-file' );
591
				break;
592
			case UploadBase::FILE_TOO_LARGE:
593
				$this->dieUsage( 'The file you submitted was too large', 'file-too-large' );
594
				break;
595
596
			case UploadBase::FILETYPE_BADTYPE:
597
				$extradata = [
598
					'filetype' => $verification['finalExt'],
599
					'allowed' => array_values( array_unique( $this->getConfig()->get( 'FileExtensions' ) ) )
600
				];
601
				ApiResult::setIndexedTagName( $extradata['allowed'], 'ext' );
602
603
				$msg = 'Filetype not permitted: ';
604
				if ( isset( $verification['blacklistedExt'] ) ) {
605
					$msg .= implode( ', ', $verification['blacklistedExt'] );
606
					$extradata['blacklisted'] = array_values( $verification['blacklistedExt'] );
607
					ApiResult::setIndexedTagName( $extradata['blacklisted'], 'ext' );
608
				} else {
609
					$msg .= $verification['finalExt'];
610
				}
611
				$this->dieUsage( $msg, 'filetype-banned', 0, $extradata );
612
				break;
613
			case UploadBase::VERIFICATION_ERROR:
614
				$parsed = $this->parseMsg( $verification['details'] );
615
				$info = "This file did not pass file verification: {$parsed['info']}";
616
				if ( $verification['details'][0] instanceof IApiMessage ) {
617
					$code = $parsed['code'];
618
				} else {
619
					// For backwards-compatibility, all of the errors from UploadBase::verifyFile() are
620
					// reported as 'verification-error', and the real error code is reported in 'details'.
621
					$code = 'verification-error';
622
				}
623
				if ( $verification['details'][0] instanceof IApiMessage ) {
624
					$msg = $verification['details'][0];
625
					$details = array_merge( [ $msg->getKey() ], $msg->getParams() );
626
				} else {
627
					$details = $verification['details'];
628
				}
629
				ApiResult::setIndexedTagName( $details, 'detail' );
630
				$data = [ 'details' => $details ];
631
				if ( isset( $parsed['data'] ) ) {
632
					$data = array_merge( $data, $parsed['data'] );
633
				}
634
635
				$this->dieUsage( $info, $code, 0, $data );
636
				break;
637
			case UploadBase::HOOK_ABORTED:
638
				if ( is_array( $verification['error'] ) ) {
639
					$params = $verification['error'];
640
				} elseif ( $verification['error'] !== '' ) {
641
					$params = [ $verification['error'] ];
642
				} else {
643
					$params = [ 'hookaborted' ];
644
				}
645
				$key = array_shift( $params );
646
				$msg = $this->msg( $key, $params )->inLanguage( 'en' )->useDatabase( false )->text();
647
				$this->dieUsage( $msg, 'hookaborted', 0, [ 'details' => $verification['error'] ] );
648
				break;
649
			default:
650
				$this->dieUsage( 'An unknown error occurred', 'unknown-error',
651
					0, [ 'details' => [ 'code' => $verification['status'] ] ] );
652
				break;
653
		}
654
	}
655
656
	/**
657
	 * Check warnings.
658
	 * Returns a suitable array for inclusion into API results if there were warnings
659
	 * Returns the empty array if there were no warnings
660
	 *
661
	 * @return array
662
	 */
663
	protected function getApiWarnings() {
664
		$warnings = $this->mUpload->checkWarnings();
665
666
		return $this->transformWarnings( $warnings );
667
	}
668
669
	protected function transformWarnings( $warnings ) {
670
		if ( $warnings ) {
671
			// Add indices
672
			ApiResult::setIndexedTagName( $warnings, 'warning' );
673
674
			if ( isset( $warnings['duplicate'] ) ) {
675
				$dupes = [];
676
				/** @var File $dupe */
677
				foreach ( $warnings['duplicate'] as $dupe ) {
678
					$dupes[] = $dupe->getName();
679
				}
680
				ApiResult::setIndexedTagName( $dupes, 'duplicate' );
681
				$warnings['duplicate'] = $dupes;
682
			}
683
684
			if ( isset( $warnings['exists'] ) ) {
685
				$warning = $warnings['exists'];
686
				unset( $warnings['exists'] );
687
				/** @var LocalFile $localFile */
688
				$localFile = isset( $warning['normalizedFile'] )
689
					? $warning['normalizedFile']
690
					: $warning['file'];
691
				$warnings[$warning['warning']] = $localFile->getName();
692
			}
693
		}
694
695
		return $warnings;
696
	}
697
698
	/**
699
	 * Handles a stash exception, giving a useful error to the user.
700
	 * @param string $exceptionType Class name of the exception we encountered.
701
	 * @param string $message Message of the exception we encountered.
702
	 * @return array Array of message and code, suitable for passing to dieUsage()
703
	 */
704
	protected function handleStashException( $exceptionType, $message ) {
705
		switch ( $exceptionType ) {
706
			case 'UploadStashFileNotFoundException':
707
				return [
708
					'Could not find the file in the stash: ' . $message,
709
					'stashedfilenotfound'
710
				];
711
			case 'UploadStashBadPathException':
712
				return [
713
					'File key of improper format or otherwise invalid: ' . $message,
714
					'stashpathinvalid'
715
				];
716
			case 'UploadStashFileException':
717
				return [
718
					'Could not store upload in the stash: ' . $message,
719
					'stashfilestorage'
720
				];
721
			case 'UploadStashZeroLengthFileException':
722
				return [
723
					'File is of zero length, and could not be stored in the stash: ' .
724
						$message,
725
					'stashzerolength'
726
				];
727
			case 'UploadStashNotLoggedInException':
728
				return [ 'Not logged in: ' . $message, 'stashnotloggedin' ];
729
			case 'UploadStashWrongOwnerException':
730
				return [ 'Wrong owner: ' . $message, 'stashwrongowner' ];
731
			case 'UploadStashNoSuchKeyException':
732
				return [ 'No such filekey: ' . $message, 'stashnosuchfilekey' ];
733
			default:
734
				return [ $exceptionType . ': ' . $message, 'stasherror' ];
735
		}
736
	}
737
738
	/**
739
	 * Perform the actual upload. Returns a suitable result array on success;
740
	 * dies on failure.
741
	 *
742
	 * @param array $warnings Array of Api upload warnings
743
	 * @return array
744
	 */
745
	protected function performUpload( $warnings ) {
746
		// Use comment as initial page text by default
747
		if ( is_null( $this->mParams['text'] ) ) {
748
			$this->mParams['text'] = $this->mParams['comment'];
749
		}
750
751
		/** @var $file LocalFile */
752
		$file = $this->mUpload->getLocalFile();
753
754
		// For preferences mode, we want to watch if 'watchdefault' is set,
755
		// or if the *file* doesn't exist, and either 'watchuploads' or
756
		// 'watchcreations' is set. But getWatchlistValue()'s automatic
757
		// handling checks if the *title* exists or not, so we need to check
758
		// all three preferences manually.
759
		$watch = $this->getWatchlistValue(
760
			$this->mParams['watchlist'], $file->getTitle(), 'watchdefault'
761
		);
762
763
		if ( !$watch && $this->mParams['watchlist'] == 'preferences' && !$file->exists() ) {
764
			$watch = (
765
				$this->getWatchlistValue( 'preferences', $file->getTitle(), 'watchuploads' ) ||
766
				$this->getWatchlistValue( 'preferences', $file->getTitle(), 'watchcreations' )
767
			);
768
		}
769
770
		// Deprecated parameters
771
		if ( $this->mParams['watch'] ) {
772
			$watch = true;
773
		}
774
775
		if ( $this->mParams['tags'] ) {
776
			$status = ChangeTags::canAddTagsAccompanyingChange( $this->mParams['tags'], $this->getUser() );
777
			if ( !$status->isOK() ) {
778
				$this->dieStatus( $status );
779
			}
780
		}
781
782
		// No errors, no warnings: do the upload
783
		if ( $this->mParams['async'] ) {
784
			$progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
785
			if ( $progress && $progress['result'] === 'Poll' ) {
786
				$this->dieUsage( 'Upload from stash already in progress.', 'publishfailed' );
787
			}
788
			UploadBase::setSessionStatus(
789
				$this->getUser(),
790
				$this->mParams['filekey'],
791
				[ 'result' => 'Poll', 'stage' => 'queued', 'status' => Status::newGood() ]
792
			);
793
			JobQueueGroup::singleton()->push( new PublishStashedFileJob(
794
				Title::makeTitle( NS_FILE, $this->mParams['filename'] ),
795
				[
796
					'filename' => $this->mParams['filename'],
797
					'filekey' => $this->mParams['filekey'],
798
					'comment' => $this->mParams['comment'],
799
					'tags' => $this->mParams['tags'],
800
					'text' => $this->mParams['text'],
801
					'watch' => $watch,
802
					'session' => $this->getContext()->exportSession()
803
				]
804
			) );
805
			$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...
806
			$result['stage'] = 'queued';
807
		} else {
808
			/** @var $status Status */
809
			$status = $this->mUpload->performUpload( $this->mParams['comment'],
810
				$this->mParams['text'], $watch, $this->getUser(), $this->mParams['tags'] );
811
812
			if ( !$status->isGood() ) {
813
				// Is there really no better way to do this?
814
				$errors = $status->getErrorsByType( 'error' );
815
				$msg = array_merge( [ $errors[0]['message'] ], $errors[0]['params'] );
816
				$data = $status->getErrorsArray();
817
				ApiResult::setIndexedTagName( $data, 'error' );
818
				// For backwards-compatibility, we use the 'internal-error' fallback key and merge $data
819
				// into the root of the response (rather than something sane like [ 'details' => $data ]).
820
				$this->dieRecoverableError( $msg, null, $data, 'internal-error' );
821
			}
822
			$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...
823
		}
824
825
		$result['filename'] = $file->getName();
826
		if ( $warnings && count( $warnings ) > 0 ) {
827
			$result['warnings'] = $warnings;
828
		}
829
830
		return $result;
831
	}
832
833
	public function mustBePosted() {
834
		return true;
835
	}
836
837
	public function isWriteMode() {
838
		return true;
839
	}
840
841
	public function getAllowedParams() {
842
		$params = [
843
			'filename' => [
844
				ApiBase::PARAM_TYPE => 'string',
845
			],
846
			'comment' => [
847
				ApiBase::PARAM_DFLT => ''
848
			],
849
			'tags' => [
850
				ApiBase::PARAM_TYPE => 'tags',
851
				ApiBase::PARAM_ISMULTI => true,
852
			],
853
			'text' => [
854
				ApiBase::PARAM_TYPE => 'text',
855
			],
856
			'watch' => [
857
				ApiBase::PARAM_DFLT => false,
858
				ApiBase::PARAM_DEPRECATED => true,
859
			],
860
			'watchlist' => [
861
				ApiBase::PARAM_DFLT => 'preferences',
862
				ApiBase::PARAM_TYPE => [
863
					'watch',
864
					'preferences',
865
					'nochange'
866
				],
867
			],
868
			'ignorewarnings' => false,
869
			'file' => [
870
				ApiBase::PARAM_TYPE => 'upload',
871
			],
872
			'url' => null,
873
			'filekey' => null,
874
			'sessionkey' => [
875
				ApiBase::PARAM_DEPRECATED => true,
876
			],
877
			'stash' => false,
878
879
			'filesize' => [
880
				ApiBase::PARAM_TYPE => 'integer',
881
				ApiBase::PARAM_MIN => 0,
882
				ApiBase::PARAM_MAX => UploadBase::getMaxUploadSize(),
883
			],
884
			'offset' => [
885
				ApiBase::PARAM_TYPE => 'integer',
886
				ApiBase::PARAM_MIN => 0,
887
			],
888
			'chunk' => [
889
				ApiBase::PARAM_TYPE => 'upload',
890
			],
891
892
			'async' => false,
893
			'checkstatus' => false,
894
		];
895
896
		return $params;
897
	}
898
899
	public function needsToken() {
900
		return 'csrf';
901
	}
902
903
	protected function getExamplesMessages() {
904
		return [
905
			'action=upload&filename=Wiki.png' .
906
				'&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png&token=123ABC'
907
				=> 'apihelp-upload-example-url',
908
			'action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1&token=123ABC'
909
				=> 'apihelp-upload-example-filekey',
910
		];
911
	}
912
913
	public function getHelpUrls() {
914
		return 'https://www.mediawiki.org/wiki/API:Upload';
915
	}
916
}
917