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