Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
47.05% covered (danger)
47.05%
319 / 678
38.46% covered (danger)
38.46%
10 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiUpload
47.05% covered (danger)
47.05%
319 / 678
38.46% covered (danger)
38.46%
10 / 26
3721.63
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 execute
61.90% covered (warning)
61.90%
26 / 42
0.00% covered (danger)
0.00%
0 / 1
30.15
 getDummyInstance
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 getUploadImageInfo
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
2
 getContextResult
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 getStashResult
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 getWarningsResult
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getMinUploadChunkSize
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 getChunkResult
50.00% covered (danger)
50.00%
69 / 138
0.00% covered (danger)
0.00%
0 / 1
43.12
 performStash
20.93% covered (danger)
20.93%
9 / 43
0.00% covered (danger)
0.00%
0 / 1
31.22
 dieRecoverableError
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 dieStatusWithCode
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 selectUploadModule
43.42% covered (danger)
43.42%
33 / 76
0.00% covered (danger)
0.00%
0 / 1
148.44
 checkPermissions
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
6.99
 verifyUpload
81.48% covered (warning)
81.48%
22 / 27
0.00% covered (danger)
0.00%
0 / 1
10.64
 checkVerification
4.35% covered (danger)
4.35%
2 / 46
0.00% covered (danger)
0.00%
0 / 1
211.91
 getApiWarnings
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 transformWarnings
51.72% covered (warning)
51.72%
15 / 29
0.00% covered (danger)
0.00%
0 / 1
18.11
 handleStashException
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
90
 performUpload
28.26% covered (danger)
28.26%
26 / 92
0.00% covered (danger)
0.00%
0 / 1
110.52
 mustBePosted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isWriteMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedParams
100.00% covered (success)
100.00%
51 / 51
100.00% covered (success)
100.00%
1 / 1
1
 needsToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Copyright © 2008 - 2010 Bryan Tong Minh <Bryan.TongMinh@Gmail.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22
23/**
24 * @todo: create a UploadCommandFactory and UploadComand classes to share logic with Special:Upload
25 * @todo: split the different cases of upload in subclasses or submethods.
26 */
27
28use MediaWiki\Config\Config;
29use MediaWiki\Logger\LoggerFactory;
30use MediaWiki\MainConfigNames;
31use MediaWiki\MediaWikiServices;
32use MediaWiki\Message\Message;
33use MediaWiki\Status\Status;
34use MediaWiki\User\Options\UserOptionsLookup;
35use MediaWiki\User\User;
36use MediaWiki\Watchlist\WatchlistManager;
37use Psr\Log\LoggerInterface;
38use Wikimedia\ParamValidator\ParamValidator;
39use Wikimedia\ParamValidator\TypeDef\IntegerDef;
40
41/**
42 * @ingroup API
43 */
44class ApiUpload extends ApiBase {
45
46    use ApiWatchlistTrait;
47
48    /** @var UploadBase|UploadFromChunks */
49    protected $mUpload = null;
50
51    protected $mParams;
52
53    private JobQueueGroup $jobQueueGroup;
54
55    private LoggerInterface $log;
56
57    /**
58     * @param ApiMain $mainModule
59     * @param string $moduleName
60     * @param JobQueueGroup $jobQueueGroup
61     * @param WatchlistManager $watchlistManager
62     * @param UserOptionsLookup $userOptionsLookup
63     */
64    public function __construct(
65        ApiMain $mainModule,
66        $moduleName,
67        JobQueueGroup $jobQueueGroup,
68        WatchlistManager $watchlistManager,
69        UserOptionsLookup $userOptionsLookup
70    ) {
71        parent::__construct( $mainModule, $moduleName );
72        $this->jobQueueGroup = $jobQueueGroup;
73
74        // Variables needed in ApiWatchlistTrait trait
75        $this->watchlistExpiryEnabled = $this->getConfig()->get( MainConfigNames::WatchlistExpiry );
76        $this->watchlistMaxDuration =
77            $this->getConfig()->get( MainConfigNames::WatchlistExpiryMaxDuration );
78        $this->watchlistManager = $watchlistManager;
79        $this->userOptionsLookup = $userOptionsLookup;
80        $this->log = LoggerFactory::getInstance( 'upload' );
81    }
82
83    public function execute() {
84        // Check whether upload is enabled
85        if ( !UploadBase::isEnabled() ) {
86            $this->dieWithError( 'uploaddisabled' );
87        }
88
89        $user = $this->getUser();
90
91        // Parameter handling
92        $this->mParams = $this->extractRequestParams();
93        // Check if async mode is actually supported (jobs done in cli mode)
94        $this->mParams['async'] = ( $this->mParams['async'] &&
95            $this->getConfig()->get( MainConfigNames::EnableAsyncUploads ) );
96
97        // Copy the session key to the file key, for backward compatibility.
98        if ( !$this->mParams['filekey'] && $this->mParams['sessionkey'] ) {
99            $this->mParams['filekey'] = $this->mParams['sessionkey'];
100        }
101
102        if ( !$this->mParams['checkstatus'] ) {
103            $this->useTransactionalTimeLimit();
104        }
105
106        // Select an upload module
107        try {
108            if ( !$this->selectUploadModule() ) {
109                return; // not a true upload, but a status request or similar
110            } elseif ( !isset( $this->mUpload ) ) {
111                $this->dieDebug( __METHOD__, 'No upload module set' );
112            }
113        } catch ( UploadStashException $e ) { // XXX: don't spam exception log
114            $this->dieStatus( $this->handleStashException( $e ) );
115        }
116
117        // First check permission to upload
118        $this->checkPermissions( $user );
119
120        // Fetch the file (usually a no-op)
121        // Skip for async upload from URL, where we just want to run checks.
122        /** @var Status $status */
123        if ( $this->mParams['async'] && $this->mParams['url'] ) {
124            $status = $this->mUpload->canFetchFile();
125        } else {
126            $status = $this->mUpload->fetchFile();
127        }
128
129        if ( !$status->isGood() ) {
130            $this->log->info( "Unable to fetch file {filename} for {user} because {status}",
131                [
132                    'user' => $this->getUser()->getName(),
133                    'status' => (string)$status,
134                    'filename' => $this->mParams['filename'] ?? '-',
135                ]
136            );
137            $this->dieStatus( $status );
138        }
139
140        // Check the uploaded file
141        $this->verifyUpload();
142
143        // Check if the user has the rights to modify or overwrite the requested title
144        // (This check is irrelevant if stashing is already requested, since the errors
145        //  can always be fixed by changing the title)
146        if ( !$this->mParams['stash'] ) {
147            $permErrors = $this->mUpload->verifyTitlePermissions( $user );
148            if ( $permErrors !== true ) {
149                $this->dieRecoverableError( $permErrors, 'filename' );
150            }
151        }
152
153        // Get the result based on the current upload context:
154        try {
155            $result = $this->getContextResult();
156        } catch ( UploadStashException $e ) { // XXX: don't spam exception log
157            $this->dieStatus( $this->handleStashException( $e ) );
158        }
159        $this->getResult()->addValue( null, $this->getModuleName(), $result );
160
161        // Add 'imageinfo' in a separate addValue() call. File metadata can be unreasonably large,
162        // so otherwise when it exceeded $wgAPIMaxResultSize, no result would be returned (T143993).
163        // @phan-suppress-next-line PhanTypeArraySuspiciousNullable False positive
164        if ( $result['result'] === 'Success' ) {
165            $imageinfo = $this->getUploadImageInfo( $this->mUpload );
166            $this->getResult()->addValue( $this->getModuleName(), 'imageinfo', $imageinfo );
167        }
168
169        // Cleanup any temporary mess
170        $this->mUpload->cleanupTempFile();
171    }
172
173    public static function getDummyInstance(): self {
174        $services = MediaWikiServices::getInstance();
175        $apiMain = new ApiMain(); // dummy object (XXX)
176        $apiUpload = new ApiUpload(
177            $apiMain,
178            'upload',
179            $services->getJobQueueGroup(),
180            $services->getWatchlistManager(),
181            $services->getUserOptionsLookup()
182        );
183
184        return $apiUpload;
185    }
186
187    /**
188     * Gets image info about the file just uploaded.
189     *
190     * Also has the effect of setting metadata to be an 'indexed tag name' in
191     * returned API result if 'metadata' was requested. Oddly, we have to pass
192     * the "result" object down just so it can do that with the appropriate
193     * format, presumably.
194     *
195     * @internal For use in upload jobs and a deprecated method on UploadBase.
196     * @todo Extract the logic actually needed by the jobs, and separate it
197     *       from the structure used in API responses.
198     *
199     * @return array Image info
200     */
201    public function getUploadImageInfo( UploadBase $upload ): array {
202        $result = $this->getResult();
203        $stashFile = $upload->getStashFile();
204
205        // Calling a different API module depending on whether the file was stashed is less than optimal.
206        // In fact, calling API modules here at all is less than optimal. Maybe it should be refactored.
207        if ( $stashFile ) {
208            $imParam = ApiQueryStashImageInfo::getPropertyNames();
209            $info = ApiQueryStashImageInfo::getInfo(
210                $stashFile,
211                array_fill_keys( $imParam, true ),
212                $result
213            );
214        } else {
215            $localFile = $upload->getLocalFile();
216            $imParam = ApiQueryImageInfo::getPropertyNames();
217            $info = ApiQueryImageInfo::getInfo(
218                $localFile,
219                array_fill_keys( $imParam, true ),
220                $result
221            );
222        }
223
224        return $info;
225    }
226
227    /**
228     * Get an upload result based on upload context
229     * @return array
230     */
231    private function getContextResult() {
232        $warnings = $this->getApiWarnings();
233        if ( $warnings && !$this->mParams['ignorewarnings'] ) {
234            // Get warnings formatted in result array format
235            return $this->getWarningsResult( $warnings );
236        } elseif ( $this->mParams['chunk'] ) {
237            // Add chunk, and get result
238            return $this->getChunkResult( $warnings );
239        } elseif ( $this->mParams['stash'] ) {
240            // Stash the file and get stash result
241            return $this->getStashResult( $warnings );
242        }
243
244        // This is the most common case -- a normal upload with no warnings
245        // performUpload will return a formatted properly for the API with status
246        return $this->performUpload( $warnings );
247    }
248
249    /**
250     * Get Stash Result, throws an exception if the file could not be stashed.
251     * @param array $warnings Array of Api upload warnings
252     * @return array
253     */
254    private function getStashResult( $warnings ) {
255        $result = [];
256        $result['result'] = 'Success';
257        if ( $warnings && count( $warnings ) > 0 ) {
258            $result['warnings'] = $warnings;
259        }
260        // Some uploads can request they be stashed, so as not to publish them immediately.
261        // In this case, a failure to stash ought to be fatal
262        $this->performStash( 'critical', $result );
263
264        return $result;
265    }
266
267    /**
268     * Get Warnings Result
269     * @param array $warnings Array of Api upload warnings
270     * @return array
271     */
272    private function getWarningsResult( $warnings ) {
273        $result = [];
274        $result['result'] = 'Warning';
275        $result['warnings'] = $warnings;
276        // in case the warnings can be fixed with some further user action, let's stash this upload
277        // and return a key they can use to restart it
278        $this->performStash( 'optional', $result );
279
280        return $result;
281    }
282
283    /**
284     * @since 1.35
285     * @see $wgMinUploadChunkSize
286     * @param Config $config Site configuration for MinUploadChunkSize
287     * @return int
288     */
289    public static function getMinUploadChunkSize( Config $config ) {
290        $configured = $config->get( MainConfigNames::MinUploadChunkSize );
291
292        // Leave some room for other POST parameters
293        $postMax = (
294            wfShorthandToInteger(
295                ini_get( 'post_max_size' ),
296                PHP_INT_MAX
297            ) ?: PHP_INT_MAX
298        ) - 1024;
299
300        // Ensure the minimum chunk size is less than PHP upload limits
301        // or the maximum upload size.
302        return min(
303            $configured,
304            UploadBase::getMaxUploadSize( 'file' ),
305            UploadBase::getMaxPhpUploadSize(),
306            $postMax
307        );
308    }
309
310    /**
311     * Get the result of a chunk upload.
312     * @param array $warnings Array of Api upload warnings
313     * @return array
314     */
315    private function getChunkResult( $warnings ) {
316        $result = [];
317
318        if ( $warnings && count( $warnings ) > 0 ) {
319            $result['warnings'] = $warnings;
320        }
321
322        $chunkUpload = $this->getMain()->getUpload( 'chunk' );
323        $chunkPath = $chunkUpload->getTempName();
324        $chunkSize = $chunkUpload->getSize();
325        $totalSoFar = $this->mParams['offset'] + $chunkSize;
326        $minChunkSize = self::getMinUploadChunkSize( $this->getConfig() );
327
328        // Double check sizing
329        if ( $totalSoFar > $this->mParams['filesize'] ) {
330            $this->dieWithError( 'apierror-invalid-chunk' );
331        }
332
333        // Enforce minimum chunk size
334        if ( $totalSoFar != $this->mParams['filesize'] && $chunkSize < $minChunkSize ) {
335            $this->dieWithError( [ 'apierror-chunk-too-small', Message::numParam( $minChunkSize ) ] );
336        }
337
338        if ( $this->mParams['offset'] == 0 ) {
339            $this->log->debug( "Started first chunk of chunked upload of {filename} for {user}",
340                [
341                    'user' => $this->getUser()->getName(),
342                    'filename' => $this->mParams['filename'] ?? '-',
343                    'filesize' => $this->mParams['filesize'],
344                    'chunkSize' => $chunkSize
345                ]
346            );
347            $filekey = $this->performStash( 'critical' );
348        } else {
349            $filekey = $this->mParams['filekey'];
350
351            // Don't allow further uploads to an already-completed session
352            $progress = UploadBase::getSessionStatus( $this->getUser(), $filekey );
353            if ( !$progress ) {
354                // Probably can't get here, but check anyway just in case
355                $this->log->info( "Stash failed due to no session for {user}",
356                    [
357                        'user' => $this->getUser()->getName(),
358                        'filename' => $this->mParams['filename'] ?? '-',
359                        'filekey' => $this->mParams['filekey'] ?? '-',
360                        'filesize' => $this->mParams['filesize'],
361                        'chunkSize' => $chunkSize
362                    ]
363                );
364                $this->dieWithError( 'apierror-stashfailed-nosession', 'stashfailed' );
365            } elseif ( $progress['result'] !== 'Continue' || $progress['stage'] !== 'uploading' ) {
366                $this->dieWithError( 'apierror-stashfailed-complete', 'stashfailed' );
367            }
368
369            $status = $this->mUpload->addChunk(
370                $chunkPath, $chunkSize, $this->mParams['offset'] );
371            if ( !$status->isGood() ) {
372                $extradata = [
373                    'offset' => $this->mUpload->getOffset(),
374                ];
375                $this->log->info( "Chunked upload stash failure {status} for {user}",
376                    [
377                        'status' => (string)$status,
378                        'user' => $this->getUser()->getName(),
379                        'filename' => $this->mParams['filename'] ?? '-',
380                        'filekey' => $this->mParams['filekey'] ?? '-',
381                        'filesize' => $this->mParams['filesize'],
382                        'chunkSize' => $chunkSize,
383                        'offset' => $this->mUpload->getOffset()
384                    ]
385                );
386                $this->dieStatusWithCode( $status, 'stashfailed', $extradata );
387            } else {
388                $this->log->debug( "Got chunk for {filename} with offset {offset} for {user}",
389                    [
390                        'user' => $this->getUser()->getName(),
391                        'filename' => $this->mParams['filename'] ?? '-',
392                        'filekey' => $this->mParams['filekey'] ?? '-',
393                        'filesize' => $this->mParams['filesize'],
394                        'chunkSize' => $chunkSize,
395                        'offset' => $this->mUpload->getOffset()
396                    ]
397                );
398            }
399        }
400
401        // Check we added the last chunk:
402        if ( $totalSoFar == $this->mParams['filesize'] ) {
403            if ( $this->mParams['async'] ) {
404                UploadBase::setSessionStatus(
405                    $this->getUser(),
406                    $filekey,
407                    [ 'result' => 'Poll',
408                        'stage' => 'queued', 'status' => Status::newGood() ]
409                );
410                // It is important that this be lazyPush, as we do not want to insert
411                // into job queue until after the current transaction has completed since
412                // this depends on values in uploadstash table that were updated during
413                // the current transaction. (T350917)
414                $this->jobQueueGroup->lazyPush( new AssembleUploadChunksJob( [
415                    'filename' => $this->mParams['filename'],
416                    'filekey' => $filekey,
417                    'filesize' => $this->mParams['filesize'],
418                    'session' => $this->getContext()->exportSession()
419                ] ) );
420                $this->log->info( "Received final chunk of {filename} for {user}, queuing assemble job",
421                    [
422                        'user' => $this->getUser()->getName(),
423                        'filename' => $this->mParams['filename'] ?? '-',
424                        'filekey' => $this->mParams['filekey'] ?? '-',
425                        'filesize' => $this->mParams['filesize'],
426                        'chunkSize' => $chunkSize,
427                    ]
428                );
429                $result['result'] = 'Poll';
430                $result['stage'] = 'queued';
431            } else {
432                $this->log->info( "Received final chunk of {filename} for {user}, assembling immediately",
433                    [
434                        'user' => $this->getUser()->getName(),
435                        'filename' => $this->mParams['filename'] ?? '-',
436                        'filekey' => $this->mParams['filekey'] ?? '-',
437                        'filesize' => $this->mParams['filesize'],
438                        'chunkSize' => $chunkSize,
439                    ]
440                );
441
442                $status = $this->mUpload->concatenateChunks();
443                if ( !$status->isGood() ) {
444                    UploadBase::setSessionStatus(
445                        $this->getUser(),
446                        $filekey,
447                        [ 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status ]
448                    );
449                    $this->log->info( "Non jobqueue assembly of {filename} failed because {status}",
450                        [
451                            'user' => $this->getUser()->getName(),
452                            'filename' => $this->mParams['filename'] ?? '-',
453                            'filekey' => $this->mParams['filekey'] ?? '-',
454                            'filesize' => $this->mParams['filesize'],
455                            'chunkSize' => $chunkSize,
456                            'status' => (string)$status
457                        ]
458                    );
459                    $this->dieStatusWithCode( $status, 'stashfailed' );
460                }
461
462                // We can only get warnings like 'duplicate' after concatenating the chunks
463                $warnings = $this->getApiWarnings();
464                if ( $warnings ) {
465                    $result['warnings'] = $warnings;
466                }
467
468                // The fully concatenated file has a new filekey. So remove
469                // the old filekey and fetch the new one.
470                UploadBase::setSessionStatus( $this->getUser(), $filekey, false );
471                $this->mUpload->stash->removeFile( $filekey );
472                $filekey = $this->mUpload->getStashFile()->getFileKey();
473
474                $result['result'] = 'Success';
475            }
476        } else {
477            UploadBase::setSessionStatus(
478                $this->getUser(),
479                $filekey,
480                [
481                    'result' => 'Continue',
482                    'stage' => 'uploading',
483                    'offset' => $totalSoFar,
484                    'status' => Status::newGood(),
485                ]
486            );
487            $result['result'] = 'Continue';
488            $result['offset'] = $totalSoFar;
489        }
490
491        $result['filekey'] = $filekey;
492
493        return $result;
494    }
495
496    /**
497     * Stash the file and add the file key, or error information if it fails, to the data.
498     *
499     * @param string $failureMode What to do on failure to stash:
500     *   - When 'critical', use dieStatus() to produce an error response and throw an exception.
501     *     Use this when stashing the file was the primary purpose of the API request.
502     *   - When 'optional', only add a 'stashfailed' key to the data and return null.
503     *     Use this when some error happened for a non-stash upload and we're stashing the file
504     *     only to save the client the trouble of re-uploading it.
505     * @param array|null &$data API result to which to add the information
506     * @return string|null File key
507     */
508    private function performStash( $failureMode, &$data = null ) {
509        $isPartial = (bool)$this->mParams['chunk'];
510        try {
511            $status = $this->mUpload->tryStashFile( $this->getUser(), $isPartial );
512
513            if ( $status->isGood() && !$status->getValue() ) {
514                // Not actually a 'good' status...
515                $status->fatal( new ApiMessage( 'apierror-stashinvalidfile', 'stashfailed' ) );
516            }
517        } catch ( Exception $e ) {
518            $debugMessage = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage();
519            $this->log->info( $debugMessage,
520                [
521                    'user' => $this->getUser()->getName(),
522                    'filename' => $this->mParams['filename'] ?? '-',
523                    'filekey' => $this->mParams['filekey'] ?? '-'
524                ]
525            );
526
527            $status = Status::newFatal( $this->getErrorFormatter()->getMessageFromException(
528                $e, [ 'wrap' => new ApiMessage( 'apierror-stashexception', 'stashfailed' ) ]
529            ) );
530        }
531
532        if ( $status->isGood() ) {
533            $stashFile = $status->getValue();
534            $data['filekey'] = $stashFile->getFileKey();
535            // Backwards compatibility
536            $data['sessionkey'] = $data['filekey'];
537            return $data['filekey'];
538        }
539
540        if ( $status->getMessage()->getKey() === 'uploadstash-exception' ) {
541            // The exceptions thrown by upload stash code and pretty silly and UploadBase returns poor
542            // Statuses for it. Just extract the exception details and parse them ourselves.
543            [ $exceptionType, $message ] = $status->getMessage()->getParams();
544            $debugMessage = 'Stashing temporary file failed: ' . $exceptionType . ' ' . $message;
545            $this->log->info( $debugMessage,
546                [
547                    'user' => $this->getUser()->getName(),
548                    'filename' => $this->mParams['filename'] ?? '-',
549                    'filekey' => $this->mParams['filekey'] ?? '-'
550                ]
551            );
552        }
553
554        $this->log->info( "Stash upload failure {status}",
555            [
556                'status' => (string)$status,
557                'user' => $this->getUser()->getName(),
558                'filename' => $this->mParams['filename'] ?? '-',
559                'filekey' => $this->mParams['filekey'] ?? '-'
560            ]
561        );
562        // Bad status
563        if ( $failureMode !== 'optional' ) {
564            $this->dieStatus( $status );
565        } else {
566            $data['stasherrors'] = $this->getErrorFormatter()->arrayFromStatus( $status );
567            return null;
568        }
569    }
570
571    /**
572     * Throw an error that the user can recover from by providing a better
573     * value for $parameter
574     *
575     * @param array $errors Array of Message objects, message keys, key+param
576     *  arrays, or StatusValue::getErrors()-style arrays
577     * @param string|null $parameter Parameter that needs revising
578     * @throws ApiUsageException
579     * @return never
580     */
581    private function dieRecoverableError( $errors, $parameter = null ) {
582        $this->performStash( 'optional', $data );
583
584        if ( $parameter ) {
585            $data['invalidparameter'] = $parameter;
586        }
587
588        $sv = StatusValue::newGood();
589        foreach ( $errors as $error ) {
590            $msg = ApiMessage::create( $error );
591            $msg->setApiData( $msg->getApiData() + $data );
592            $sv->fatal( $msg );
593        }
594        $this->dieStatus( $sv );
595    }
596
597    /**
598     * Like dieStatus(), but always uses $overrideCode for the error code, unless the code comes from
599     * IApiMessage.
600     *
601     * @param Status $status
602     * @param string $overrideCode Error code to use if there isn't one from IApiMessage
603     * @param array|null $moreExtraData
604     * @throws ApiUsageException
605     * @return never
606     */
607    public function dieStatusWithCode( $status, $overrideCode, $moreExtraData = null ) {
608        $sv = StatusValue::newGood();
609        foreach ( $status->getMessages() as $error ) {
610            $msg = ApiMessage::create( $error, $overrideCode );
611            if ( $moreExtraData ) {
612                $msg->setApiData( $msg->getApiData() + $moreExtraData );
613            }
614            $sv->fatal( $msg );
615        }
616        $this->dieStatus( $sv );
617    }
618
619    /**
620     * Select an upload module and set it to mUpload. Dies on failure. If the
621     * request was a status request and not a true upload, returns false;
622     * otherwise true
623     *
624     * @return bool
625     */
626    protected function selectUploadModule() {
627        // chunk or one and only one of the following parameters is needed
628        if ( !$this->mParams['chunk'] ) {
629            $this->requireOnlyOneParameter( $this->mParams,
630                'filekey', 'file', 'url' );
631        }
632
633        // Status report for "upload to stash"/"upload from stash"/"upload by url"
634        if ( $this->mParams['checkstatus'] && ( $this->mParams['filekey'] || $this->mParams['url'] ) ) {
635            $statusKey = $this->mParams['filekey'] ?: UploadFromUrl::getCacheKey( $this->mParams );
636            $progress = UploadBase::getSessionStatus( $this->getUser(), $statusKey );
637            if ( !$progress ) {
638                $this->log->info( "Cannot check upload status due to missing upload session for {user}",
639                    [
640                        'user' => $this->getUser()->getName(),
641                        'filename' => $this->mParams['filename'] ?? '-',
642                        'filekey' => $this->mParams['filekey'] ?? '-'
643                    ]
644                );
645                $this->dieWithError( 'apierror-upload-missingresult', 'missingresult' );
646            } elseif ( !$progress['status']->isGood() ) {
647                $this->dieStatusWithCode( $progress['status'], 'stashfailed' );
648            }
649            if ( isset( $progress['status']->value['verification'] ) ) {
650                $this->checkVerification( $progress['status']->value['verification'] );
651            }
652            if ( isset( $progress['status']->value['warnings'] ) ) {
653                $warnings = $this->transformWarnings( $progress['status']->value['warnings'] );
654                if ( $warnings ) {
655                    $progress['warnings'] = $warnings;
656                }
657            }
658            unset( $progress['status'] ); // remove Status object
659            $imageinfo = null;
660            if ( isset( $progress['imageinfo'] ) ) {
661                $imageinfo = $progress['imageinfo'];
662                unset( $progress['imageinfo'] );
663            }
664
665            $this->getResult()->addValue( null, $this->getModuleName(), $progress );
666            // Add 'imageinfo' in a separate addValue() call. File metadata can be unreasonably large,
667            // so otherwise when it exceeded $wgAPIMaxResultSize, no result would be returned (T143993).
668            if ( $imageinfo ) {
669                $this->getResult()->addValue( $this->getModuleName(), 'imageinfo', $imageinfo );
670            }
671
672            return false;
673        }
674
675        // The following modules all require the filename parameter to be set
676        if ( $this->mParams['filename'] === null ) {
677            $this->dieWithError( [ 'apierror-missingparam', 'filename' ] );
678        }
679
680        if ( $this->mParams['chunk'] ) {
681            // Chunk upload
682            $this->mUpload = new UploadFromChunks( $this->getUser() );
683            if ( isset( $this->mParams['filekey'] ) ) {
684                if ( $this->mParams['offset'] === 0 ) {
685                    $this->dieWithError( 'apierror-upload-filekeynotallowed', 'filekeynotallowed' );
686                }
687
688                // handle new chunk
689                $this->mUpload->continueChunks(
690                    $this->mParams['filename'],
691                    $this->mParams['filekey'],
692                    $this->getMain()->getUpload( 'chunk' )
693                );
694            } else {
695                if ( $this->mParams['offset'] !== 0 ) {
696                    $this->dieWithError( 'apierror-upload-filekeyneeded', 'filekeyneeded' );
697                }
698
699                // handle first chunk
700                $this->mUpload->initialize(
701                    $this->mParams['filename'],
702                    $this->getMain()->getUpload( 'chunk' )
703                );
704            }
705        } elseif ( isset( $this->mParams['filekey'] ) ) {
706            // Upload stashed in a previous request
707            if ( !UploadFromStash::isValidKey( $this->mParams['filekey'] ) ) {
708                $this->dieWithError( 'apierror-invalid-file-key' );
709            }
710
711            $this->mUpload = new UploadFromStash( $this->getUser() );
712            // This will not download the temp file in initialize() in async mode.
713            // We still have enough information to call checkWarnings() and such.
714            $this->mUpload->initialize(
715                $this->mParams['filekey'], $this->mParams['filename'], !$this->mParams['async']
716            );
717        } elseif ( isset( $this->mParams['file'] ) ) {
718            // Can't async upload directly from a POSTed file, we'd have to
719            // stash the file and then queue the publish job. The user should
720            // just submit the two API queries to perform those two steps.
721            if ( $this->mParams['async'] ) {
722                $this->dieWithError( 'apierror-cannot-async-upload-file' );
723            }
724
725            $this->mUpload = new UploadFromFile();
726            $this->mUpload->initialize(
727                $this->mParams['filename'],
728                $this->getMain()->getUpload( 'file' )
729            );
730        } elseif ( isset( $this->mParams['url'] ) ) {
731            // Make sure upload by URL is enabled:
732            if ( !UploadFromUrl::isEnabled() ) {
733                $this->dieWithError( 'copyuploaddisabled' );
734            }
735
736            if ( !UploadFromUrl::isAllowedHost( $this->mParams['url'] ) ) {
737                $this->dieWithError( 'apierror-copyuploadbaddomain' );
738            }
739
740            if ( !UploadFromUrl::isAllowedUrl( $this->mParams['url'] ) ) {
741                $this->dieWithError( 'apierror-copyuploadbadurl' );
742            }
743
744            $this->mUpload = new UploadFromUrl;
745            $this->mUpload->initialize( $this->mParams['filename'],
746                $this->mParams['url'] );
747        }
748
749        return true;
750    }
751
752    /**
753     * Checks that the user has permissions to perform this upload.
754     * Dies with usage message on inadequate permissions.
755     * @param User $user The user to check.
756     */
757    protected function checkPermissions( $user ) {
758        // Check whether the user has the appropriate permissions to upload anyway
759        $permission = $this->mUpload->isAllowed( $user );
760
761        if ( $permission !== true ) {
762            if ( !$user->isNamed() ) {
763                $this->dieWithError( [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ] );
764            }
765
766            $this->dieStatus( User::newFatalPermissionDeniedStatus( $permission ) );
767        }
768
769        // Check blocks
770        if ( $user->isBlockedFromUpload() ) {
771            // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
772            $this->dieBlocked( $user->getBlock() );
773        }
774    }
775
776    /**
777     * Performs file verification, dies on error.
778     */
779    protected function verifyUpload() {
780        if ( $this->mParams['chunk'] ) {
781            $maxSize = UploadBase::getMaxUploadSize();
782            if ( $this->mParams['filesize'] > $maxSize ) {
783                $this->dieWithError( 'file-too-large' );
784            }
785            if ( !$this->mUpload->getTitle() ) {
786                $this->dieWithError( 'illegal-filename' );
787            }
788            // file will be assembled after having uploaded the last chunk,
789            // so we can only validate the name at this point
790            $verification = $this->mUpload->validateName();
791            if ( $verification === true ) {
792                return;
793            }
794        } elseif ( $this->mParams['async'] && ( $this->mParams['filekey'] || $this->mParams['url'] ) ) {
795            // file will be assembled/downloaded in a background process, so we
796            // can only validate the name at this point
797            // file verification will happen in background process
798            $verification = $this->mUpload->validateName();
799            if ( $verification === true ) {
800                return;
801            }
802        } else {
803            wfDebug( __METHOD__ . " about to verify" );
804
805            $verification = $this->mUpload->verifyUpload();
806
807            if ( $verification['status'] === UploadBase::OK ) {
808                return;
809            } else {
810                $this->log->info( "File verification of {filename} failed for {user} because {result}",
811                    [
812                        'user' => $this->getUser()->getName(),
813                        'resultCode' => $verification['status'],
814                        'result' => $this->mUpload->getVerificationErrorCode( $verification['status'] ),
815                        'filename' => $this->mParams['filename'] ?? '-',
816                        'details' => $verification['details'] ?? ''
817                    ]
818                );
819            }
820        }
821
822        $this->checkVerification( $verification );
823    }
824
825    /**
826     * Performs file verification, dies on error.
827     * @param array $verification
828     * @return never
829     */
830    protected function checkVerification( array $verification ) {
831        switch ( $verification['status'] ) {
832            // Recoverable errors
833            case UploadBase::MIN_LENGTH_PARTNAME:
834                $this->dieRecoverableError( [ 'filename-tooshort' ], 'filename' );
835                // dieRecoverableError prevents continuation
836            case UploadBase::ILLEGAL_FILENAME:
837                $this->dieRecoverableError(
838                    [ ApiMessage::create(
839                        'illegal-filename', null, [ 'filename' => $verification['filtered'] ]
840                    ) ], 'filename'
841                );
842                // dieRecoverableError prevents continuation
843            case UploadBase::FILENAME_TOO_LONG:
844                $this->dieRecoverableError( [ 'filename-toolong' ], 'filename' );
845                // dieRecoverableError prevents continuation
846            case UploadBase::FILETYPE_MISSING:
847                $this->dieRecoverableError( [ 'filetype-missing' ], 'filename' );
848                // dieRecoverableError prevents continuation
849            case UploadBase::WINDOWS_NONASCII_FILENAME:
850                $this->dieRecoverableError( [ 'windows-nonascii-filename' ], 'filename' );
851
852            // Unrecoverable errors
853            case UploadBase::EMPTY_FILE:
854                $this->dieWithError( 'empty-file' );
855                // dieWithError prevents continuation
856            case UploadBase::FILE_TOO_LARGE:
857                $this->dieWithError( 'file-too-large' );
858                // dieWithError prevents continuation
859
860            case UploadBase::FILETYPE_BADTYPE:
861                $extradata = [
862                    'filetype' => $verification['finalExt'],
863                    'allowed' => array_values( array_unique(
864                        $this->getConfig()->get( MainConfigNames::FileExtensions ) ) )
865                ];
866                $extensions =
867                    array_unique( $this->getConfig()->get( MainConfigNames::FileExtensions ) );
868                $msg = [
869                    'filetype-banned-type',
870                    null, // filled in below
871                    Message::listParam( $extensions, 'comma' ),
872                    count( $extensions ),
873                    null, // filled in below
874                ];
875                ApiResult::setIndexedTagName( $extradata['allowed'], 'ext' );
876
877                if ( isset( $verification['blacklistedExt'] ) ) {
878                    $msg[1] = Message::listParam( $verification['blacklistedExt'], 'comma' );
879                    $msg[4] = count( $verification['blacklistedExt'] );
880                    $extradata['blacklisted'] = array_values( $verification['blacklistedExt'] );
881                    ApiResult::setIndexedTagName( $extradata['blacklisted'], 'ext' );
882                } else {
883                    $msg[1] = $verification['finalExt'];
884                    $msg[4] = 1;
885                }
886
887                $this->dieWithError( $msg, 'filetype-banned', $extradata );
888                // dieWithError prevents continuation
889
890            case UploadBase::VERIFICATION_ERROR:
891                $msg = ApiMessage::create( $verification['details'], 'verification-error' );
892                if ( $verification['details'][0] instanceof MessageSpecifier ) {
893                    $details = [ $msg->getKey(), ...$msg->getParams() ];
894                } else {
895                    $details = $verification['details'];
896                }
897                ApiResult::setIndexedTagName( $details, 'detail' );
898                $msg->setApiData( $msg->getApiData() + [ 'details' => $details ] );
899                $this->dieWithError( $msg );
900                // dieWithError prevents continuation
901
902            case UploadBase::HOOK_ABORTED:
903                $msg = $verification['error'] === '' ? 'hookaborted' : $verification['error'];
904                $this->dieWithError( $msg, 'hookaborted', [ 'details' => $verification['error'] ] );
905                // dieWithError prevents continuation
906            default:
907                $this->dieWithError( 'apierror-unknownerror-nocode', 'unknown-error',
908                    [ 'details' => [ 'code' => $verification['status'] ] ] );
909        }
910    }
911
912    /**
913     * Check warnings.
914     * Returns a suitable array for inclusion into API results if there were warnings
915     * Returns the empty array if there were no warnings
916     *
917     * @return array
918     */
919    protected function getApiWarnings() {
920        $warnings = UploadBase::makeWarningsSerializable(
921            $this->mUpload->checkWarnings( $this->getUser() )
922        );
923
924        return $this->transformWarnings( $warnings );
925    }
926
927    protected function transformWarnings( $warnings ) {
928        if ( $warnings ) {
929            // Add indices
930            ApiResult::setIndexedTagName( $warnings, 'warning' );
931
932            if ( isset( $warnings['duplicate'] ) ) {
933                $dupes = array_column( $warnings['duplicate'], 'fileName' );
934                ApiResult::setIndexedTagName( $dupes, 'duplicate' );
935                $warnings['duplicate'] = $dupes;
936            }
937
938            if ( isset( $warnings['exists'] ) ) {
939                $warning = $warnings['exists'];
940                unset( $warnings['exists'] );
941                $localFile = $warning['normalizedFile'] ?? $warning['file'];
942                $warnings[$warning['warning']] = $localFile['fileName'];
943            }
944
945            if ( isset( $warnings['no-change'] ) ) {
946                $file = $warnings['no-change'];
947                unset( $warnings['no-change'] );
948
949                $warnings['nochange'] = [
950                    'timestamp' => wfTimestamp( TS_ISO_8601, $file['timestamp'] )
951                ];
952            }
953
954            if ( isset( $warnings['duplicate-version'] ) ) {
955                $dupes = [];
956                foreach ( $warnings['duplicate-version'] as $dupe ) {
957                    $dupes[] = [
958                        'timestamp' => wfTimestamp( TS_ISO_8601, $dupe['timestamp'] )
959                    ];
960                }
961                unset( $warnings['duplicate-version'] );
962
963                ApiResult::setIndexedTagName( $dupes, 'ver' );
964                $warnings['duplicateversions'] = $dupes;
965            }
966            // We haven't downloaded the file, so this will result in an empty file warning
967            if ( $this->mParams['async'] && $this->mParams['url'] ) {
968                unset( $warnings['empty-file'] );
969            }
970        }
971
972        return $warnings;
973    }
974
975    /**
976     * Handles a stash exception, giving a useful error to the user.
977     * @todo Internationalize the exceptions then get rid of this
978     * @param Exception $e
979     * @return StatusValue
980     */
981    protected function handleStashException( $e ) {
982        $this->log->info( "Upload stashing of {filename} failed for {user} because {error}",
983            [
984                'user' => $this->getUser()->getName(),
985                'error' => get_class( $e ),
986                'filename' => $this->mParams['filename'] ?? '-',
987                'filekey' => $this->mParams['filekey'] ?? '-'
988            ]
989        );
990
991        switch ( get_class( $e ) ) {
992            case UploadStashFileNotFoundException::class:
993                $wrap = 'apierror-stashedfilenotfound';
994                break;
995            case UploadStashBadPathException::class:
996                $wrap = 'apierror-stashpathinvalid';
997                break;
998            case UploadStashFileException::class:
999                $wrap = 'apierror-stashfilestorage';
1000                break;
1001            case UploadStashZeroLengthFileException::class:
1002                $wrap = 'apierror-stashzerolength';
1003                break;
1004            case UploadStashNotLoggedInException::class:
1005                return StatusValue::newFatal( ApiMessage::create(
1006                    [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ], 'stashnotloggedin'
1007                ) );
1008            case UploadStashWrongOwnerException::class:
1009                $wrap = 'apierror-stashwrongowner';
1010                break;
1011            case UploadStashNoSuchKeyException::class:
1012                $wrap = 'apierror-stashnosuchfilekey';
1013                break;
1014            default:
1015                $wrap = [ 'uploadstash-exception', get_class( $e ) ];
1016                break;
1017        }
1018        return StatusValue::newFatal(
1019            $this->getErrorFormatter()->getMessageFromException( $e, [ 'wrap' => $wrap ] )
1020        );
1021    }
1022
1023    /**
1024     * Perform the actual upload. Returns a suitable result array on success;
1025     * dies on failure.
1026     *
1027     * @param array $warnings Array of Api upload warnings
1028     * @return array
1029     */
1030    protected function performUpload( $warnings ) {
1031        // Use comment as initial page text by default
1032        $this->mParams['text'] ??= $this->mParams['comment'];
1033
1034        /** @var LocalFile $file */
1035        $file = $this->mUpload->getLocalFile();
1036        $user = $this->getUser();
1037        $title = $file->getTitle();
1038
1039        // for preferences mode, we want to watch if 'watchdefault' is set,
1040        // or if the *file* doesn't exist, and either 'watchuploads' or
1041        // 'watchcreations' is set. But getWatchlistValue()'s automatic
1042        // handling checks if the *title* exists or not, so we need to check
1043        // all three preferences manually.
1044        $watch = $this->getWatchlistValue(
1045            $this->mParams['watchlist'], $title, $user, 'watchdefault'
1046        );
1047
1048        if ( !$watch && $this->mParams['watchlist'] == 'preferences' && !$file->exists() ) {
1049            $watch = (
1050                $this->getWatchlistValue( 'preferences', $title, $user, 'watchuploads' ) ||
1051                $this->getWatchlistValue( 'preferences', $title, $user, 'watchcreations' )
1052            );
1053        }
1054        $watchlistExpiry = $this->getExpiryFromParams( $this->mParams );
1055
1056        // Deprecated parameters
1057        if ( $this->mParams['watch'] ) {
1058            $watch = true;
1059        }
1060
1061        if ( $this->mParams['tags'] ) {
1062            $status = ChangeTags::canAddTagsAccompanyingChange( $this->mParams['tags'], $this->getAuthority() );
1063            if ( !$status->isOK() ) {
1064                $this->dieStatus( $status );
1065            }
1066        }
1067
1068        // No errors, no warnings: do the upload
1069        $result = [];
1070        if ( $this->mParams['async'] ) {
1071            // Only stash uploads and copy uploads support async
1072            if ( $this->mParams['filekey'] ) {
1073                $job = new PublishStashedFileJob(
1074                    [
1075                        'filename' => $this->mParams['filename'],
1076                        'filekey' => $this->mParams['filekey'],
1077                        'comment' => $this->mParams['comment'],
1078                        'tags' => $this->mParams['tags'] ?? [],
1079                        'text' => $this->mParams['text'],
1080                        'watch' => $watch,
1081                        'watchlistexpiry' => $watchlistExpiry,
1082                        'session' => $this->getContext()->exportSession(),
1083                        'ignorewarnings' => $this->mParams['ignorewarnings']
1084                    ]
1085                    );
1086            } elseif ( $this->mParams['url'] ) {
1087                $job = new UploadFromUrlJob(
1088                    [
1089                        'filename' => $this->mParams['filename'],
1090                        'url' => $this->mParams['url'],
1091                        'comment' => $this->mParams['comment'],
1092                        'tags' => $this->mParams['tags'] ?? [],
1093                        'text' => $this->mParams['text'],
1094                        'watch' => $watch,
1095                        'watchlistexpiry' => $watchlistExpiry,
1096                        'session' => $this->getContext()->exportSession(),
1097                        'ignorewarnings' => $this->mParams['ignorewarnings']
1098                    ]
1099                    );
1100            } else {
1101                $this->dieWithError( 'apierror-no-async-support', 'publishfailed' );
1102                // We will never reach this, but it's here to help phan figure out
1103                // $job is never null
1104                // @phan-suppress-next-line PhanPluginUnreachableCode On purpose
1105                return [];
1106            }
1107            $cacheKey = $job->getCacheKey();
1108            // Check if an upload is already in progress.
1109            // the result can be Poll / Failure / Success
1110            $progress = UploadBase::getSessionStatus( $this->getUser(), $cacheKey );
1111            if ( $progress && $progress['result'] === 'Poll' ) {
1112                $this->dieWithError( 'apierror-upload-inprogress', 'publishfailed' );
1113            }
1114            UploadBase::setSessionStatus(
1115                $this->getUser(),
1116                $cacheKey,
1117                [ 'result' => 'Poll', 'stage' => 'queued', 'status' => Status::newGood() ]
1118            );
1119
1120            $this->jobQueueGroup->push( $job );
1121            $this->log->info( "Sending publish job of {filename} for {user}",
1122                [
1123                    'user' => $this->getUser()->getName(),
1124                    'filename' => $this->mParams['filename'] ?? '-'
1125                ]
1126            );
1127            $result['result'] = 'Poll';
1128            $result['stage'] = 'queued';
1129        } else {
1130            /** @var Status $status */
1131            $status = $this->mUpload->performUpload(
1132                $this->mParams['comment'],
1133                $this->mParams['text'],
1134                $watch,
1135                $this->getUser(),
1136                $this->mParams['tags'] ?? [],
1137                $watchlistExpiry
1138            );
1139
1140            if ( !$status->isGood() ) {
1141                $this->log->info( "Non-async API upload publish failed for {user} because {status}",
1142                    [
1143                        'user' => $this->getUser()->getName(),
1144                        'filename' => $this->mParams['filename'] ?? '-',
1145                        'filekey' => $this->mParams['filekey'] ?? '-',
1146                        'status' => (string)$status
1147                    ]
1148                );
1149                $this->dieRecoverableError( $status->getMessages() );
1150            }
1151            $result['result'] = 'Success';
1152        }
1153
1154        $result['filename'] = $file->getName();
1155        if ( $warnings && count( $warnings ) > 0 ) {
1156            $result['warnings'] = $warnings;
1157        }
1158
1159        return $result;
1160    }
1161
1162    public function mustBePosted() {
1163        return true;
1164    }
1165
1166    public function isWriteMode() {
1167        return true;
1168    }
1169
1170    public function getAllowedParams() {
1171        $params = [
1172            'filename' => [
1173                ParamValidator::PARAM_TYPE => 'string',
1174            ],
1175            'comment' => [
1176                ParamValidator::PARAM_DEFAULT => ''
1177            ],
1178            'tags' => [
1179                ParamValidator::PARAM_TYPE => 'tags',
1180                ParamValidator::PARAM_ISMULTI => true,
1181            ],
1182            'text' => [
1183                ParamValidator::PARAM_TYPE => 'text',
1184            ],
1185            'watch' => [
1186                ParamValidator::PARAM_DEFAULT => false,
1187                ParamValidator::PARAM_DEPRECATED => true,
1188            ],
1189        ];
1190
1191        // Params appear in the docs in the order they are defined,
1192        // which is why this is here and not at the bottom.
1193        $params += $this->getWatchlistParams( [
1194            'watch',
1195            'preferences',
1196            'nochange',
1197        ] );
1198
1199        $params += [
1200            'ignorewarnings' => false,
1201            'file' => [
1202                ParamValidator::PARAM_TYPE => 'upload',
1203            ],
1204            'url' => null,
1205            'filekey' => null,
1206            'sessionkey' => [
1207                ParamValidator::PARAM_DEPRECATED => true,
1208            ],
1209            'stash' => false,
1210
1211            'filesize' => [
1212                ParamValidator::PARAM_TYPE => 'integer',
1213                IntegerDef::PARAM_MIN => 0,
1214                IntegerDef::PARAM_MAX => UploadBase::getMaxUploadSize(),
1215            ],
1216            'offset' => [
1217                ParamValidator::PARAM_TYPE => 'integer',
1218                IntegerDef::PARAM_MIN => 0,
1219            ],
1220            'chunk' => [
1221                ParamValidator::PARAM_TYPE => 'upload',
1222            ],
1223
1224            'async' => false,
1225            'checkstatus' => false,
1226        ];
1227
1228        return $params;
1229    }
1230
1231    public function needsToken() {
1232        return 'csrf';
1233    }
1234
1235    protected function getExamplesMessages() {
1236        return [
1237            'action=upload&filename=Wiki.png' .
1238                '&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png&token=123ABC'
1239                => 'apihelp-upload-example-url',
1240            'action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1&token=123ABC'
1241                => 'apihelp-upload-example-filekey',
1242        ];
1243    }
1244
1245    public function getHelpUrls() {
1246        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Upload';
1247    }
1248}