Completed
Branch master (bbf110)
by
unknown
25:51
created

ApiUpload::getChunkResult()   D

Complexity

Conditions 14
Paths 224

Size

Total Lines 111
Code Lines 69

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 14
eloc 69
c 1
b 0
f 0
nc 224
nop 1
dl 0
loc 111
rs 4.2078

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

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

Let’s take a look at an example:

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

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

    // do something with $myArray
}

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

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

Loading history...
770
			$result['stage'] = 'queued';
771
		} else {
772
			/** @var $status Status */
773
			$status = $this->mUpload->performUpload( $this->mParams['comment'],
774
				$this->mParams['text'], $watch, $this->getUser(), $this->mParams['tags'] );
775
776
			if ( !$status->isGood() ) {
777
				// Is there really no better way to do this?
778
				$errors = $status->getErrorsByType( 'error' );
779
				$msg = array_merge( [ $errors[0]['message'] ], $errors[0]['params'] );
780
				$data = $status->getErrorsArray();
781
				ApiResult::setIndexedTagName( $data, 'error' );
782
				// For backwards-compatibility, we use the 'internal-error' fallback key and merge $data
783
				// into the root of the response (rather than something sane like [ 'details' => $data ]).
784
				$this->dieRecoverableError( $msg, null, $data, 'internal-error' );
785
			}
786
			$result['result'] = 'Success';
0 ignored issues
show
Coding Style Comprehensibility introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.

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

Let’s take a look at an example:

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

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

    // do something with $myArray
}

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

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

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