Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
3.32% covered (danger)
3.32%
19 / 572
0.00% covered (danger)
0.00%
0 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialUpload
3.33% covered (danger)
3.33%
19 / 571
0.00% covered (danger)
0.00%
0 / 26
24761.70
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 addMessageBoxStyling
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 loadRequest
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
132
 isAsyncUpload
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 userCanExecute
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 execute
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
342
 showUploadStatus
0.00% covered (danger)
0.00%
0 / 65
0.00% covered (danger)
0.00%
0 / 1
342
 showUploadProgress
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
42
 showUploadForm
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getUploadForm
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
72
 showRecoverableUploadError
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 showUploadWarning
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 1
462
 showUploadError
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 performUploadChecks
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
72
 getPageTextAndTags
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
 processUpload
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
20
 processAsyncUpload
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
20
 getInitialPageText
79.17% covered (warning)
79.17%
19 / 24
0.00% covered (danger)
0.00%
0 / 1
8.58
 getWatchCheck
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
56
 processVerificationError
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
182
 unsaveUploadedFile
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 getExistsWarning
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
110
 getDupeWarning
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 rotationEnabled
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Specials;
8
9use BitmapHandler;
10use MediaWiki\ChangeTags\ChangeTags;
11use MediaWiki\Config\Config;
12use MediaWiki\Exception\ErrorPageError;
13use MediaWiki\Exception\PermissionsError;
14use MediaWiki\Exception\UserBlockedError;
15use MediaWiki\FileRepo\File\File;
16use MediaWiki\FileRepo\File\LocalFile;
17use MediaWiki\FileRepo\LocalRepo;
18use MediaWiki\FileRepo\RepoGroup;
19use MediaWiki\Gallery\ImageGalleryBase;
20use MediaWiki\HookContainer\HookRunner;
21use MediaWiki\Html\Html;
22use MediaWiki\HTMLForm\HTMLForm;
23use MediaWiki\JobQueue\JobQueueGroup;
24use MediaWiki\JobQueue\Jobs\UploadFromUrlJob;
25use MediaWiki\Logger\LoggerFactory;
26use MediaWiki\Logging\LogEventsList;
27use MediaWiki\MainConfigNames;
28use MediaWiki\MediaWikiServices;
29use MediaWiki\Page\WikiFilePage;
30use MediaWiki\Request\FauxRequest;
31use MediaWiki\Request\WebRequest;
32use MediaWiki\SpecialPage\SpecialPage;
33use MediaWiki\Status\Status;
34use MediaWiki\Title\NamespaceInfo;
35use MediaWiki\Title\Title;
36use MediaWiki\Upload\Exception\UploadStashException;
37use MediaWiki\Upload\UploadBase;
38use MediaWiki\Upload\UploadFromStash;
39use MediaWiki\User\Options\UserOptionsLookup;
40use MediaWiki\User\User;
41use MediaWiki\Watchlist\WatchlistManager;
42use Psr\Log\LoggerInterface;
43use UnexpectedValueException;
44use UploadForm;
45
46/**
47 * Form for uploading media files.
48 *
49 * @ingroup SpecialPage
50 * @ingroup Upload
51 */
52class SpecialUpload extends SpecialPage {
53
54    private LocalRepo $localRepo;
55    private UserOptionsLookup $userOptionsLookup;
56    private NamespaceInfo $nsInfo;
57    private WatchlistManager $watchlistManager;
58    private JobQueueGroup $jobQueueGroup;
59    private LoggerInterface $log;
60
61    public function __construct(
62        ?RepoGroup $repoGroup = null,
63        ?UserOptionsLookup $userOptionsLookup = null,
64        ?NamespaceInfo $nsInfo = null,
65        ?WatchlistManager $watchlistManager = null
66    ) {
67        parent::__construct( 'Upload', 'upload' );
68        // This class is extended and therefor fallback to global state - T265300
69        $services = MediaWikiServices::getInstance();
70        $this->jobQueueGroup = $services->getJobQueueGroup();
71        $repoGroup ??= $services->getRepoGroup();
72        $this->localRepo = $repoGroup->getLocalRepo();
73        $this->userOptionsLookup = $userOptionsLookup ?? $services->getUserOptionsLookup();
74        $this->nsInfo = $nsInfo ?? $services->getNamespaceInfo();
75        $this->watchlistManager = $watchlistManager ?? $services->getWatchlistManager();
76        $this->log = LoggerFactory::getInstance( 'SpecialUpload' );
77    }
78
79    private function addMessageBoxStyling() {
80        $this->getOutput()->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
81    }
82
83    /** @inheritDoc */
84    public function doesWrites() {
85        return true;
86    }
87
88    // Misc variables
89
90    /** @var WebRequest|FauxRequest The request this form is supposed to handle */
91    public $mRequest;
92    /** @var string */
93    public $mSourceType;
94
95    /** @var string The cache key to use to retreive the status of your async upload */
96    public $mCacheKey;
97
98    /** @var UploadBase */
99    public $mUpload;
100
101    /** @var LocalFile */
102    public $mLocalFile;
103    /** @var bool */
104    public $mUploadClicked;
105
106    // User input variables from the "description" section
107
108    /** @var string The requested target file name */
109    public $mDesiredDestName;
110    /** @var string */
111    public $mComment;
112    /** @var string */
113    public $mLicense;
114
115    // User input variables from the root section
116
117    /** @var bool */
118    public $mIgnoreWarning;
119    /** @var bool */
120    public $mWatchthis;
121    /** @var string */
122    public $mCopyrightStatus;
123    /** @var string */
124    public $mCopyrightSource;
125
126    // Hidden variables
127
128    /** @var string */
129    public $mDestWarningAck;
130
131    /** @var bool The user followed an "overwrite this file" link */
132    public $mForReUpload;
133
134    /** @var bool The user clicked "Cancel and return to upload form" button */
135    public $mCancelUpload;
136    /** @var bool */
137    public $mTokenOk;
138
139    /** @var bool Subclasses can use this to determine whether a file was uploaded */
140    public $mUploadSuccessful = false;
141
142    /** @var string Raw html injection point for hooks not using HTMLForm */
143    public $uploadFormTextTop;
144    /** @var string Raw html injection point for hooks not using HTMLForm */
145    public $uploadFormTextAfterSummary;
146
147    /**
148     * Initialize instance variables from request and create an Upload handler
149     */
150    protected function loadRequest() {
151        $this->mRequest = $request = $this->getRequest();
152        $this->mSourceType = $request->getVal( 'wpSourceType', 'file' );
153        $this->mUpload = UploadBase::createFromRequest( $request );
154        $this->mUploadClicked = $request->wasPosted()
155            && ( $request->getCheck( 'wpUpload' )
156                || $request->getCheck( 'wpUploadIgnoreWarning' ) );
157
158        // Guess the desired name from the filename if not provided
159        $this->mDesiredDestName = $request->getText( 'wpDestFile' );
160        if ( !$this->mDesiredDestName && $request->getFileName( 'wpUploadFile' ) !== null ) {
161            $this->mDesiredDestName = $request->getFileName( 'wpUploadFile' );
162        }
163        $this->mLicense = $request->getText( 'wpLicense' );
164
165        $this->mDestWarningAck = $request->getText( 'wpDestFileWarningAck' );
166        $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' )
167            || $request->getCheck( 'wpUploadIgnoreWarning' );
168        $this->mWatchthis = $request->getBool( 'wpWatchthis' ) && $this->getUser()->isRegistered();
169        $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' );
170        $this->mCopyrightSource = $request->getText( 'wpUploadSource' );
171
172        $this->mForReUpload = $request->getBool( 'wpForReUpload' ); // updating a file
173
174        $commentDefault = '';
175        $commentMsg = $this->msg( 'upload-default-description' )->inContentLanguage();
176        if ( !$this->mForReUpload && !$commentMsg->isDisabled() ) {
177            $commentDefault = $commentMsg->plain();
178        }
179        $this->mComment = $request->getText( 'wpUploadDescription', $commentDefault );
180
181        $this->mCancelUpload = $request->getCheck( 'wpCancelUpload' )
182            || $request->getCheck( 'wpReUpload' ); // b/w compat
183
184        // If it was posted check for the token (no remote POST'ing with user credentials)
185        $token = $request->getVal( 'wpEditToken' );
186        $this->mTokenOk = $this->getUser()->matchEditToken( $token );
187
188        // If this is an upload from Url and we're allowing async processing,
189        // check for the presence of the cache key parameter, or compute it. Else, it should be empty.
190        if ( $this->isAsyncUpload() ) {
191            $this->mCacheKey = \MediaWiki\Upload\UploadFromUrl::getCacheKeyFromRequest( $request );
192        } else {
193            $this->mCacheKey = '';
194        }
195
196        $this->uploadFormTextTop = '';
197        $this->uploadFormTextAfterSummary = '';
198    }
199
200    /**
201     * Check if the current request is an async upload by url request
202     *
203     * @return bool
204     */
205    protected function isAsyncUpload() {
206        return $this->mSourceType === 'url'
207            && $this->getConfig()->get( MainConfigNames::EnableAsyncUploads )
208            && $this->getConfig()->get( MainConfigNames::EnableAsyncUploadsByURL );
209    }
210
211    /**
212     * This page can be shown if uploading is enabled.
213     * Handle permission checking elsewhere in order to be able to show
214     * custom error messages.
215     *
216     * @param User $user
217     * @return bool
218     */
219    public function userCanExecute( User $user ) {
220        return UploadBase::isEnabled() && parent::userCanExecute( $user );
221    }
222
223    /**
224     * @param string|null $par
225     */
226    public function execute( $par ) {
227        $this->useTransactionalTimeLimit();
228
229        $this->setHeaders();
230        $this->outputHeader();
231
232        # Check uploading enabled
233        if ( !UploadBase::isEnabled() ) {
234            throw new ErrorPageError( 'uploaddisabled', 'uploaddisabledtext' );
235        }
236
237        $this->addHelpLink( 'Help:Managing files' );
238
239        # Check permissions
240        $user = $this->getUser();
241        $permissionRequired = UploadBase::isAllowed( $user );
242        if ( $permissionRequired !== true ) {
243            throw new PermissionsError( $permissionRequired );
244        }
245
246        # Check blocks
247        if ( $user->isBlockedFromUpload() ) {
248            throw new UserBlockedError(
249                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
250                $user->getBlock(),
251                $user,
252                $this->getLanguage(),
253                $this->getRequest()->getIP()
254            );
255        }
256
257        # Check whether we actually want to allow changing stuff
258        $this->checkReadOnly();
259
260        try {
261            $this->loadRequest();
262        } catch ( UploadStashException $e ) {
263            $this->showUploadError( $this->msg( 'upload-stash-error', $e->getMessageObject() )->escaped() );
264            return;
265        }
266
267        # Unsave the temporary file in case this was a cancelled upload
268        if ( $this->mCancelUpload && !$this->unsaveUploadedFile() ) {
269            # Something went wrong, so unsaveUploadedFile showed a warning
270            return;
271        }
272
273        # If we have a cache key, show the upload status.
274        if ( $this->mTokenOk && $this->mCacheKey !== '' ) {
275            if ( $this->mUpload && $this->mUploadClicked && !$this->mCancelUpload ) {
276                # If the user clicked the upload button, we need to process the upload
277                $this->processAsyncUpload();
278            } else {
279                # Show the upload status
280                $this->showUploadStatus( $user );
281            }
282        } elseif (
283            # Process upload or show a form
284            $this->mTokenOk && !$this->mCancelUpload &&
285            ( $this->mUpload && $this->mUploadClicked )
286        ) {
287            $this->processUpload();
288        } else {
289            # Backwards compatibility hook
290            if ( !$this->getHookRunner()->onUploadForm_initial( $this ) ) {
291                wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" );
292
293                return;
294            }
295            $this->showUploadForm( $this->getUploadForm( '', $this->mUpload?->getStashFile()?->getFileKey() ) );
296        }
297
298        # Cleanup
299        if ( $this->mUpload ) {
300            $this->mUpload->cleanupTempFile();
301        }
302    }
303
304    /**
305     * Show the upload status
306     *
307     * @param User $user The owner of the upload
308     */
309    protected function showUploadStatus( $user ) {
310        // first, let's fetch the status from the main stash
311        $progress = UploadBase::getSessionStatus( $user, $this->mCacheKey );
312        if ( !$progress ) {
313            $progress = [ 'status' => Status::newFatal( 'invalid-cache-key' ) ];
314        }
315        $this->log->debug( 'Upload status: stage {stage}, result {result}', $progress );
316
317        $status = $progress['status'] ?? Status::newFatal( 'invalid-cache-key' );
318        $stage = $progress['stage'] ?? 'unknown';
319        $result = $progress['result'] ?? 'unknown';
320        switch ( $stage ) {
321            case 'publish':
322                switch ( $result ) {
323                    case 'Success':
324                        // The upload is done. Check the result and either show the form with the error
325                        // occurred, or redirect to the file itself
326                        // Success, redirect to description page
327                        $this->mUploadSuccessful = true;
328                        $this->getHookRunner()->onSpecialUploadComplete( $this );
329                        // Redirect to the destination URL, but purge the cache of the file description page first
330                        // TODO: understand why this is needed
331                        $title = Title::makeTitleSafe( NS_FILE, $this->mRequest->getText( 'wpDestFile' ) );
332                        if ( $title ) {
333                            $this->log->debug( 'Purging page', [ 'title' => $title->getText() ] );
334                            $page = new WikiFilePage( $title );
335                            $page->doPurge();
336                        }
337                        $this->getOutput()->redirect( $this->mRequest->getText( 'wpDestUrl' ) );
338                        break;
339                    case 'Warning':
340                        $this->showUploadWarning( UploadBase::unserializeWarnings( $progress['warnings'] ) );
341                        break;
342                    case 'Failure':
343                        $details = $status->getValue();
344                        // Verification failed.
345                        if ( is_array( $details ) && isset( $details['verification'] ) ) {
346                            $this->processVerificationError( $details['verification'] );
347                        } else {
348                            $this->showUploadError( $this->getOutput()->parseAsInterface(
349                                $status->getWikiText( false, false, $this->getLanguage() ) )
350                            );
351                        }
352                        break;
353                    case 'Poll':
354                        $this->showUploadProgress(
355                            [ 'active' => true, 'msg' => 'upload-progress-processing' ]
356                        );
357                        break;
358                    default:
359                        // unknown result, just show a generic error
360                        $this->showUploadError( $this->getOutput()->parseAsInterface(
361                            $status->getWikiText( false, false, $this->getLanguage() ) )
362                        );
363                        break;
364                }
365                break;
366            case 'queued':
367                // show stalled progress bar
368                $this->showUploadProgress( [ 'active' => false, 'msg' => 'upload-progress-queued' ] );
369                break;
370            case 'fetching':
371                switch ( $result ) {
372                    case 'Success':
373                        // The file is being downloaded from a URL
374                        // TODO: show active progress bar saying we're downloading the file
375                        $this->showUploadProgress( [ 'active' => true, 'msg' => 'upload-progress-downloading' ] );
376                        break;
377                    case 'Failure':
378                        // downloading failed
379                        $this->showUploadError( $this->getOutput()->parseAsInterface(
380                            $status->getWikiText( false, false, $this->getLanguage() ) )
381                        );
382                        break;
383                    default:
384                        // unknown result, just show a generic error
385                        $this->showUploadError( $this->getOutput()->parseAsInterface(
386                            $status->getWikiText( false, false, $this->getLanguage() ) )
387                        );
388                        break;
389                }
390                break;
391            default:
392                // unknown status, just show a generic error
393                if ( $status->isOK() ) {
394                    $status = Status::newFatal( 'upload-progress-unknown' );
395                }
396                $statusmsg = $this->getOutput()->parseAsInterface(
397                    $status->getWikiText( false, false, $this->getLanguage() )
398                );
399                $message = '<h2>' . $this->msg( 'uploaderror' )->escaped() . '</h2>' . Html::errorBox( $statusmsg );
400                $this->addMessageBoxStyling();
401                $this->showUploadForm( $this->getUploadForm( $message ) );
402                break;
403        }
404    }
405
406    /**
407     * Show the upload progress in a form, with a refresh button
408     *
409     * This is used when the upload is being processed asynchronously. We're
410     * forced to use a refresh button because we need to poll the primary mainstash.
411     * See UploadBase::getSessionStatus for more information.
412     *
413     * @param array $options
414     * @return void
415     */
416    private function showUploadProgress( $options ) {
417        // $isActive = $options['active'] ?? false;
418        //$progressBarProperty = $isActive ? '' : 'disabled';
419        $message = $this->msg( $options['msg'] )->escaped();
420        $destUrl = $this->mRequest->getText( 'wpDestUrl', '' );
421        if ( !$destUrl && $this->mUpload ) {
422            if ( !$this->mLocalFile ) {
423                $this->mLocalFile = $this->mUpload->getLocalFile();
424            }
425            // This probably means the title is bad, so we can't get the URL
426            // but we need to wait for the job to execute.
427            if ( $this->mLocalFile === null ) {
428                $destUrl = '';
429            } else {
430                $destUrl = $this->mLocalFile->getTitle()->getFullURL();
431            }
432        }
433
434        $destName = $this->mDesiredDestName;
435        if ( !$destName ) {
436            $destName = $this->mRequest->getText( 'wpDestFile' );
437        }
438
439        $form = new HTMLForm( [
440            'CacheKey' => [
441                'type' => 'hidden',
442                'default' => $this->mCacheKey,
443            ],
444            'SourceType' => [
445                'type' => 'hidden',
446                'default' => $this->mSourceType,
447            ],
448            'DestUrl' => [
449                'type' => 'hidden',
450                'default' => $destUrl,
451            ],
452            'DestFile' => [
453                'type' => 'hidden',
454                'default' => $destName,
455            ],
456        ], $this->getContext(), 'uploadProgress' );
457        $form->setSubmitText( $this->msg( 'upload-refresh' )->escaped() );
458        // TODO: use codex, add a progress bar
459        //$preHtml = "<cdx-progress-bar aria--label='upload progressbar' $progressBarProperty />";
460        $preHtml = "<div id='upload-progress-message'>$message</div>";
461        $form->addPreHtml( $preHtml );
462        $form->setSubmitCallback(
463            static function ( $formData ) {
464                return true;
465            }
466        );
467        // Needed if we have warnings to show
468        $form->addHiddenFields( array_diff_key(
469            $this->mRequest->getValues(),
470            [
471                'title' => null,
472                'wpEditToken' => null,
473                'wpCacheKey' => null,
474                'wpSourceType' => null,
475                'wpDestUrl' => null,
476                'wpDestFile' => null,
477                'wpUpload' => null,
478                'wpUploadIgnoreWarning' => null,
479            ]
480        ) );
481        $form->prepareForm();
482        $this->getOutput()->addHTML( $form->getHTML( false ) );
483    }
484
485    /**
486     * Show the main upload form
487     *
488     * @param HTMLForm|string $form An HTMLForm instance or HTML string to show
489     */
490    protected function showUploadForm( $form ) {
491        if ( $form instanceof HTMLForm ) {
492            $form->show();
493        } else {
494            $this->getOutput()->addHTML( $form );
495        }
496    }
497
498    /**
499     * Get an UploadForm instance with title and text properly set.
500     *
501     * @param string $message HTML string to add to the form
502     * @param string|null $sessionKey Session key in case this is a stashed upload
503     * @param bool $hideIgnoreWarning Whether to hide "ignore warning" check box
504     * @return UploadForm
505     */
506    protected function getUploadForm( $message = '', $sessionKey = '', $hideIgnoreWarning = false ) {
507        # Initialize form
508        $form = new UploadForm(
509            [
510                'watch' => $this->getWatchCheck(),
511                'forreupload' => $this->mForReUpload,
512                'sessionkey' => $sessionKey,
513                'hideignorewarning' => $hideIgnoreWarning,
514                'destwarningack' => (bool)$this->mDestWarningAck,
515
516                'description' => $this->mComment,
517                'texttop' => $this->uploadFormTextTop,
518                'textaftersummary' => $this->uploadFormTextAfterSummary,
519                'destfile' => $this->mDesiredDestName,
520            ],
521            $this->getContext(),
522            $this->getLinkRenderer(),
523            $this->localRepo,
524            $this->getContentLanguage(),
525            $this->nsInfo,
526            $this->getHookContainer()
527        );
528        $form->setTitle( $this->getPageTitle() ); // Remove subpage
529
530        # Check the token, but only if necessary
531        if (
532            !$this->mTokenOk && !$this->mCancelUpload &&
533            ( $this->mUpload && $this->mUploadClicked )
534        ) {
535            $form->addPreHtml( $this->msg( 'session_fail_preview' )->parse() );
536        }
537
538        # Give a notice if the user is uploading a file that has been deleted or moved
539        # Note that this is independent from the message 'filewasdeleted'
540        $desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName );
541        $delNotice = ''; // empty by default
542        if ( $desiredTitleObj instanceof Title && !$desiredTitleObj->exists() ) {
543            LogEventsList::showLogExtract( $delNotice, [ 'delete', 'move' ],
544                $desiredTitleObj,
545                '', [ 'lim' => 10,
546                    'conds' => [ $this->localRepo->getReplicaDB()->expr( 'log_action', '!=', 'revision' ) ],
547                    'showIfEmpty' => false,
548                    'msgKey' => [ 'upload-recreate-warning' ] ]
549            );
550        }
551        $form->addPreHtml( $delNotice );
552
553        # Add text to form
554        $form->addPreHtml( '<div id="uploadtext">' .
555            $this->msg( 'uploadtext', [ $this->mDesiredDestName ] )->parseAsBlock() .
556            '</div>' );
557        # Add upload error message
558        $form->addPreHtml( $message );
559
560        # Add footer to form
561        $uploadFooter = $this->msg( 'uploadfooter' );
562        if ( !$uploadFooter->isDisabled() ) {
563            $form->addPostHtml( '<div id="mw-upload-footer-message">'
564                . $uploadFooter->parseAsBlock() . "</div>\n" );
565        }
566
567        return $form;
568    }
569
570    /**
571     * Stashes the upload and shows the main upload form.
572     *
573     * Note: only errors that can be handled by changing the name or
574     * description should be redirected here. It should be assumed that the
575     * file itself is sensible and has passed UploadBase::verifyFile. This
576     * essentially means that UploadBase::VERIFICATION_ERROR and
577     * UploadBase::EMPTY_FILE should not be passed here.
578     *
579     * @param string $message HTML message to be passed to mainUploadForm
580     */
581    protected function showRecoverableUploadError( $message ) {
582        $stashFile = $this->mUpload->getStashFile();
583        if ( !$stashFile ) {
584            $stashStatus = $this->mUpload->tryStashFile( $this->getUser() );
585            if ( $stashStatus->isGood() ) {
586                $stashFile = $stashStatus->getValue();
587            }
588        }
589        if ( $stashFile ) {
590            $sessionKey = $stashFile->getFileKey();
591            $uploadWarning = 'upload-tryagain';
592        } else {
593            $sessionKey = null;
594            $uploadWarning = 'upload-tryagain-nostash';
595        }
596        $message = '<h2>' . $this->msg( 'uploaderror' )->escaped() . '</h2>' .
597            Html::errorBox( $message );
598
599        $this->addMessageBoxStyling();
600        $form = $this->getUploadForm( $message, $sessionKey );
601        $form->setSubmitText( $this->msg( $uploadWarning )->escaped() );
602        $this->showUploadForm( $form );
603    }
604
605    /**
606     * Stashes the upload, shows the main form, but adds a "continue anyway button".
607     * Also checks whether there are actually warnings to display.
608     *
609     * @param array $warnings
610     * @return bool True if warnings were displayed, false if there are no
611     *   warnings and it should continue processing
612     */
613    protected function showUploadWarning( $warnings ) {
614        # If there are no warnings, or warnings we can ignore, return early.
615        # mDestWarningAck is set when some javascript has shown the warning
616        # to the user. mForReUpload is set when the user clicks the "upload a
617        # new version" link.
618        if ( !$warnings || ( count( $warnings ) == 1
619            && isset( $warnings['exists'] )
620            && ( $this->mDestWarningAck || $this->mForReUpload ) )
621        ) {
622            return false;
623        }
624
625        if ( $this->mUpload ) {
626            $stashFile = $this->mUpload->getStashFile();
627            if ( !$stashFile ) {
628                $stashStatus = $this->mUpload->tryStashFile( $this->getUser() );
629                if ( $stashStatus->isGood() ) {
630                    $stashFile = $stashStatus->getValue();
631                }
632            }
633            if ( $stashFile ) {
634                $sessionKey = $stashFile->getFileKey();
635                $uploadWarning = 'uploadwarning-text';
636            } else {
637                $sessionKey = null;
638                $uploadWarning = 'uploadwarning-text-nostash';
639            }
640        } else {
641            $sessionKey = null;
642            $uploadWarning = 'uploadwarning-text-nostash';
643        }
644
645        // Add styles for the warning, reused from the live preview
646        $this->getOutput()->addModuleStyles( 'mediawiki.special' );
647
648        $linkRenderer = $this->getLinkRenderer();
649        $warningHtml = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n"
650            . '<div class="mw-destfile-warning"><ul>';
651        foreach ( $warnings as $warning => $args ) {
652            if ( $warning == 'badfilename' ) {
653                $this->mDesiredDestName = Title::makeTitle( NS_FILE, $args )->getText();
654            }
655            if ( $warning == 'exists' ) {
656                $msg = "\t<li>" . self::getExistsWarning( $args ) . "</li>\n";
657            } elseif ( $warning == 'no-change' ) {
658                $file = $args;
659                $filename = $file->getTitle()->getPrefixedText();
660                $msg = "\t<li>" . $this->msg( 'fileexists-no-change', $filename )->parse() . "</li>\n";
661            } elseif ( $warning == 'duplicate-version' ) {
662                $file = $args[0];
663                $count = count( $args );
664                $filename = $file->getTitle()->getPrefixedText();
665                $message = $this->msg( 'fileexists-duplicate-version' )
666                    ->params( $filename )
667                    ->numParams( $count );
668                $msg = "\t<li>" . $message->parse() . "</li>\n";
669            } elseif ( $warning == 'was-deleted' ) {
670                # If the file existed before and was deleted, warn the user of this
671                $ltitle = SpecialPage::getTitleFor( 'Log' );
672                $llink = $linkRenderer->makeKnownLink(
673                    $ltitle,
674                    $this->msg( 'deletionlog' )->text(),
675                    [],
676                    [
677                        'type' => 'delete',
678                        'page' => Title::makeTitle( NS_FILE, $args )->getPrefixedText(),
679                    ]
680                );
681                $msg = "\t<li>" . $this->msg( 'filewasdeleted' )->rawParams( $llink )->parse() . "</li>\n";
682            } elseif ( $warning == 'duplicate' ) {
683                $msg = $this->getDupeWarning( $args );
684            } elseif ( $warning == 'duplicate-archive' ) {
685                if ( $args === '' ) {
686                    $msg = "\t<li>" . $this->msg( 'file-deleted-duplicate-notitle' )->parse()
687                        . "</li>\n";
688                } else {
689                    $msg = "\t<li>" . $this->msg( 'file-deleted-duplicate',
690                            Title::makeTitle( NS_FILE, $args )->getPrefixedText() )->parse()
691                        . "</li>\n";
692                }
693            } else {
694                if ( $args === true ) {
695                    $args = [];
696                } elseif ( !is_array( $args ) ) {
697                    $args = [ $args ];
698                }
699                $msg = "\t<li>" . $this->msg( $warning, $args )->parse() . "</li>\n";
700            }
701            $warningHtml .= $msg;
702        }
703        $warningHtml .= "</ul></div>\n";
704        $warningHtml .= $this->msg( $uploadWarning )->parseAsBlock();
705
706        $form = $this->getUploadForm( $warningHtml, $sessionKey, /* $hideIgnoreWarning */ true );
707        $form->setSubmitTextMsg( 'upload-tryagain' );
708        $form->addButton( [
709            'name' => 'wpUploadIgnoreWarning',
710            'value' => $this->msg( 'ignorewarning' )->text()
711        ] );
712        $form->addButton( [
713            'name' => 'wpCancelUpload',
714            'value' => $this->msg( 'reuploaddesc' )->text()
715        ] );
716
717        $this->showUploadForm( $form );
718
719        # Indicate that we showed a form
720        return true;
721    }
722
723    /**
724     * Show the upload form with error message, but do not stash the file.
725     *
726     * @param string $message HTML string
727     */
728    protected function showUploadError( $message ) {
729        $message = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . '</h2>' .
730            Html::errorBox( $message );
731        $this->showUploadForm( $this->getUploadForm( $message ) );
732        $this->addMessageBoxStyling();
733    }
734
735    /**
736     * Common steps for processing uploads
737     *
738     * @param Status $fetchFileStatus
739     * @return bool
740     */
741    protected function performUploadChecks( $fetchFileStatus ): bool {
742        if ( !$fetchFileStatus->isOK() ) {
743            $this->showUploadError( $this->getOutput()->parseAsInterface(
744                $fetchFileStatus->getWikiText( false, false, $this->getLanguage() )
745            ) );
746
747            return false;
748        }
749        if ( !$this->getHookRunner()->onUploadForm_BeforeProcessing( $this ) ) {
750            wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file." );
751            // This code path is deprecated. If you want to break upload processing
752            // do so by hooking into the appropriate hooks in UploadBase::verifyUpload
753            // and UploadBase::verifyFile.
754            // If you use this hook to break uploading, the user will be returned
755            // an empty form with no error message whatsoever.
756            return false;
757        }
758
759        // Upload verification
760        // If this is an asynchronous upload-by-url, skip the verification
761        if ( $this->isAsyncUpload() ) {
762            return true;
763        }
764        $details = $this->mUpload->verifyUpload();
765        if ( $details['status'] != UploadBase::OK ) {
766            $this->processVerificationError( $details );
767
768            return false;
769        }
770
771        // Verify permissions for this title
772        $user = $this->getUser();
773        $status = $this->mUpload->authorizeUpload( $user );
774        if ( !$status->isGood() ) {
775            $this->showRecoverableUploadError(
776                $this->getOutput()->parseAsInterface(
777                    Status::wrap( $status )->getWikiText( false, false, $this->getLanguage() )
778                )
779            );
780
781            return false;
782        }
783
784        $this->mLocalFile = $this->mUpload->getLocalFile();
785
786        // Check warnings if necessary
787        if ( !$this->mIgnoreWarning ) {
788            $warnings = $this->mUpload->checkWarnings( $user );
789            if ( $this->showUploadWarning( $warnings ) ) {
790                return false;
791            }
792        }
793
794        return true;
795    }
796
797    /**
798     * Get the page text and tags for the upload
799     *
800     * @return array|null
801     */
802    protected function getPageTextAndTags() {
803        // Get the page text if this is not a reupload
804        if ( !$this->mForReUpload ) {
805            $pageText = self::getInitialPageText( $this->mComment, $this->mLicense,
806                $this->mCopyrightStatus, $this->mCopyrightSource,
807                $this->getConfig() );
808        } else {
809            $pageText = false;
810        }
811        $changeTags = $this->getRequest()->getVal( 'wpChangeTags' );
812        if ( $changeTags === null || $changeTags === '' ) {
813            $changeTags = [];
814        } else {
815            $changeTags = array_filter( array_map( 'trim', explode( ',', $changeTags ) ) );
816        }
817        if ( $changeTags ) {
818            $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
819                $changeTags, $this->getUser() );
820            if ( !$changeTagsStatus->isOK() ) {
821                $this->showUploadError( $this->getOutput()->parseAsInterface(
822                    $changeTagsStatus->getWikiText( false, false, $this->getLanguage() )
823                ) );
824
825                return null;
826            }
827        }
828        return [ $pageText, $changeTags ];
829    }
830
831    /**
832     * Do the upload.
833     * Checks are made in SpecialUpload::execute()
834     */
835    protected function processUpload() {
836        // Fetch the file if required
837        $status = $this->mUpload->fetchFile();
838        if ( !$this->performUploadChecks( $status ) ) {
839            return;
840        }
841        $user = $this->getUser();
842        $pageAndTags = $this->getPageTextAndTags();
843        if ( $pageAndTags === null ) {
844            return;
845        }
846        [ $pageText, $changeTags ] = $pageAndTags;
847
848        $status = $this->mUpload->performUpload(
849            $this->mComment,
850            $pageText,
851            $this->mWatchthis,
852            $user,
853            $changeTags
854        );
855
856        if ( !$status->isGood() ) {
857            $this->showRecoverableUploadError(
858                $this->getOutput()->parseAsInterface(
859                    $status->getWikiText( false, false, $this->getLanguage() )
860                )
861            );
862
863            return;
864        }
865
866        // Success, redirect to description page
867        $this->mUploadSuccessful = true;
868        $this->getHookRunner()->onSpecialUploadComplete( $this );
869        $this->getOutput()->redirect( $this->mLocalFile->getTitle()->getFullURL() );
870    }
871
872    /**
873     * Process an asynchronous upload
874     */
875    protected function processAsyncUpload() {
876        // Ensure the upload we're dealing with is an UploadFromUrl
877        if ( !$this->mUpload instanceof \MediaWiki\Upload\UploadFromUrl ) {
878            $this->showUploadError( $this->msg( 'uploaderror' )->escaped() );
879
880            return;
881        }
882        // check we can fetch the file
883        $status = $this->mUpload->canFetchFile();
884        if ( !$this->performUploadChecks( $status ) ) {
885            $this->log->debug( 'Upload failed verification: {error}', [ 'error' => $status ] );
886            return;
887        }
888
889        $pageAndTags = $this->getPageTextAndTags();
890        if ( $pageAndTags === null ) {
891            return;
892        }
893        [ $pageText, $changeTags ] = $pageAndTags;
894
895        // Create a new job to process the upload from url
896        $job = new UploadFromUrlJob(
897            [
898                'filename' => $this->mUpload->getDesiredDestName(),
899                'url' => $this->mUpload->getUrl(),
900                'comment' => $this->mComment,
901                'tags' => $changeTags,
902                'text' => $pageText,
903                'watch' => $this->mWatchthis,
904                'watchlistexpiry' => null,
905                'session' => $this->getContext()->exportSession(),
906                'reupload' => $this->mForReUpload,
907                'ignorewarnings' => $this->mIgnoreWarning,
908            ]
909        );
910        // Save the session status
911        $cacheKey = $job->getCacheKey();
912        UploadBase::setSessionStatus( $this->getUser(), $cacheKey, [
913            'status' => Status::newGood(),
914            'stage' => 'queued',
915            'result' => 'Poll'
916        ] );
917        $this->log->info( "Submitting UploadFromUrlJob for {filename}",
918            [ 'filename' => $this->mUpload->getDesiredDestName() ]
919        );
920        // Submit the job
921        $this->jobQueueGroup->push( $job );
922        // Show the upload status
923        $this->showUploadStatus( $this->getUser() );
924    }
925
926    /**
927     * Get the initial image page text based on a comment and optional file status information
928     * @param string $comment
929     * @param string $license
930     * @param string $copyStatus
931     * @param string $source
932     * @param Config|null $config Configuration object to load data from
933     * @return string
934     */
935    public static function getInitialPageText( $comment = '', $license = '',
936        $copyStatus = '', $source = '', ?Config $config = null
937    ) {
938        if ( $config === null ) {
939            wfDebug( __METHOD__ . ' called without a Config instance passed to it' );
940            $config = MediaWikiServices::getInstance()->getMainConfig();
941        }
942
943        $msg = [];
944        $forceUIMsgAsContentMsg = (array)$config->get( MainConfigNames::ForceUIMsgAsContentMsg );
945        /* These messages are transcluded into the actual text of the description page.
946         * Thus, forcing them as content messages makes the upload to produce an int: template
947         * instead of hardcoding it there in the uploader language.
948         */
949        foreach ( [ 'license-header', 'filedesc', 'filestatus', 'filesource' ] as $msgName ) {
950            if ( in_array( $msgName, $forceUIMsgAsContentMsg ) ) {
951                $msg[$msgName] = "{{int:$msgName}}";
952            } else {
953                $msg[$msgName] = wfMessage( $msgName )->inContentLanguage()->text();
954            }
955        }
956
957        $licenseText = '';
958        if ( $license !== '' ) {
959            $licenseText = '== ' . $msg['license-header'] . " ==\n{{" . $license . "}}\n";
960        }
961
962        $pageText = $comment . "\n";
963        $headerText = '== ' . $msg['filedesc'] . ' ==';
964        if ( $comment !== '' && !str_contains( $comment, $headerText ) ) {
965            // prepend header to page text unless it's already there (or there is no content)
966            $pageText = $headerText . "\n" . $pageText;
967        }
968
969        if ( $config->get( MainConfigNames::UseCopyrightUpload ) ) {
970            $pageText .= '== ' . $msg['filestatus'] . " ==\n" . $copyStatus . "\n";
971            $pageText .= $licenseText;
972            $pageText .= '== ' . $msg['filesource'] . " ==\n" . $source;
973        } else {
974            $pageText .= $licenseText;
975        }
976
977        // allow extensions to modify the content
978        ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
979            ->onUploadForm_getInitialPageText( $pageText, $msg, $config );
980
981        return $pageText;
982    }
983
984    /**
985     * See if we should check the 'watch this page' checkbox on the form
986     * based on the user's preferences and whether we're being asked
987     * to create a new file or update an existing one.
988     *
989     * In the case where 'watch edits' is off but 'watch creations' is on,
990     * we'll leave the box unchecked.
991     *
992     * Note that the page target can be changed *on the form*, so our check
993     * state can get out of sync.
994     * @return bool
995     */
996    protected function getWatchCheck() {
997        $user = $this->getUser();
998        if ( $this->userOptionsLookup->getBoolOption( $user, 'watchdefault' ) ) {
999            // Watch all edits!
1000            return true;
1001        }
1002
1003        $desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName );
1004        if ( $desiredTitleObj instanceof Title &&
1005            $this->watchlistManager->isWatched( $user, $desiredTitleObj ) ) {
1006            // Already watched, don't change that
1007            return true;
1008        }
1009
1010        $local = $this->localRepo->newFile( $this->mDesiredDestName );
1011        if ( $local && $local->exists() ) {
1012            // We're uploading a new version of an existing file.
1013            // No creation, so don't watch it if we're not already.
1014            return false;
1015        } else {
1016            // New page should get watched if that's our option.
1017            return $this->userOptionsLookup->getBoolOption( $user, 'watchcreations' ) ||
1018                $this->userOptionsLookup->getBoolOption( $user, 'watchuploads' );
1019        }
1020    }
1021
1022    /**
1023     * Provides output to the user for a result of UploadBase::verifyUpload
1024     *
1025     * @param array $details Result of UploadBase::verifyUpload
1026     */
1027    protected function processVerificationError( $details ) {
1028        switch ( $details['status'] ) {
1029            /** Statuses that only require name changing */
1030            case UploadBase::MIN_LENGTH_PARTNAME:
1031                $this->showRecoverableUploadError( $this->msg( 'minlength1' )->escaped() );
1032                break;
1033            case UploadBase::ILLEGAL_FILENAME:
1034                $this->showRecoverableUploadError( $this->msg( 'illegalfilename',
1035                    $details['filtered'] )->parse() );
1036                break;
1037            case UploadBase::FILENAME_TOO_LONG:
1038                $this->showRecoverableUploadError( $this->msg( 'filename-toolong' )->escaped() );
1039                break;
1040            case UploadBase::FILETYPE_MISSING:
1041                $this->showRecoverableUploadError( $this->msg( 'filetype-missing' )->parse() );
1042                break;
1043            case UploadBase::WINDOWS_NONASCII_FILENAME:
1044                $this->showRecoverableUploadError( $this->msg( 'windows-nonascii-filename' )->parse() );
1045                break;
1046
1047            /** Statuses that require reuploading */
1048            case UploadBase::EMPTY_FILE:
1049                $this->showUploadError( $this->msg( 'emptyfile' )->escaped() );
1050                break;
1051            case UploadBase::FILE_TOO_LARGE:
1052                $this->showUploadError( $this->msg( 'largefileserver' )->escaped() );
1053                break;
1054            case UploadBase::FILETYPE_BADTYPE:
1055                $msg = $this->msg( 'filetype-banned-type' );
1056                if ( isset( $details['blacklistedExt'] ) ) {
1057                    $msg->params( $this->getLanguage()->commaList( $details['blacklistedExt'] ) );
1058                } else {
1059                    $msg->params( $details['finalExt'] );
1060                }
1061                $extensions =
1062                    array_unique( $this->getConfig()->get( MainConfigNames::FileExtensions ) );
1063                $msg->params( $this->getLanguage()->commaList( $extensions ),
1064                    count( $extensions ) );
1065
1066                // Add PLURAL support for the first parameter. This results
1067                // in a bit unlogical parameter sequence, but does not break
1068                // old translations
1069                if ( isset( $details['blacklistedExt'] ) ) {
1070                    $msg->params( count( $details['blacklistedExt'] ) );
1071                } else {
1072                    $msg->params( 1 );
1073                }
1074
1075                $this->showUploadError( $msg->parse() );
1076                break;
1077            case UploadBase::VERIFICATION_ERROR:
1078                unset( $details['status'] );
1079                $code = array_shift( $details['details'] );
1080                $this->showUploadError( $this->msg( $code, $details['details'] )->parse() );
1081                break;
1082            default:
1083                throw new UnexpectedValueException( __METHOD__ . ": Unknown value `{$details['status']}`" );
1084        }
1085    }
1086
1087    /**
1088     * Remove a temporarily kept file stashed by saveTempUploadedFile().
1089     *
1090     * @return bool Success
1091     */
1092    protected function unsaveUploadedFile() {
1093        if ( !( $this->mUpload instanceof UploadFromStash ) ) {
1094            return true;
1095        }
1096        $success = $this->mUpload->unsaveUploadedFile();
1097        if ( !$success ) {
1098            $this->getOutput()->showErrorPage(
1099                'internalerror',
1100                'filedeleteerror',
1101                [ $this->mUpload->getTempPath() ]
1102            );
1103
1104            return false;
1105        } else {
1106            return true;
1107        }
1108    }
1109
1110    /** Functions for formatting warnings */
1111
1112    /**
1113     * Formats a result of UploadBase::getExistsWarning as HTML
1114     * This check is static and can be done pre-upload via AJAX
1115     *
1116     * @param array|false $exists The result of UploadBase::getExistsWarning
1117     * @return string Empty string if there is no warning or an HTML fragment
1118     */
1119    public static function getExistsWarning( $exists ) {
1120        if ( !$exists ) {
1121            return '';
1122        }
1123
1124        $file = $exists['file'];
1125        if ( !$file instanceof File ) {
1126            // File deleted while showing entry from cache for async-url-upload
1127            // Or serialize error, see T409830
1128            return '';
1129        }
1130
1131        $filename = $file->getTitle()->getPrefixedText();
1132        $warnMsg = null;
1133
1134        if ( $exists['warning'] == 'exists' ) {
1135            // Exact match
1136            $warnMsg = wfMessage( 'fileexists', $filename );
1137        } elseif ( $exists['warning'] == 'page-exists' ) {
1138            // Page exists but file does not
1139            $warnMsg = wfMessage( 'filepageexists', $filename );
1140        } elseif ( $exists['warning'] == 'exists-normalized' ) {
1141            $warnMsg = wfMessage( 'fileexists-extension', $filename,
1142                $exists['normalizedFile']->getTitle()->getPrefixedText() );
1143        } elseif ( $exists['warning'] == 'thumb' ) {
1144            // Swapped argument order compared with other messages for backwards compatibility
1145            $warnMsg = wfMessage( 'fileexists-thumbnail-yes',
1146                $exists['thumbFile']->getTitle()->getPrefixedText(), $filename );
1147        } elseif ( $exists['warning'] == 'thumb-name' ) {
1148            // Image w/o '180px-' does not exists, but we do not like these filenames
1149            $name = $file->getName();
1150            $badPart = substr( $name, 0, strpos( $name, '-' ) + 1 );
1151            $warnMsg = wfMessage( 'file-thumbnail-no', $badPart );
1152        } elseif ( $exists['warning'] == 'bad-prefix' ) {
1153            $warnMsg = wfMessage( 'filename-bad-prefix', $exists['prefix'] );
1154        }
1155
1156        return $warnMsg ? $warnMsg->page( $file->getTitle() )->parse() : '';
1157    }
1158
1159    /**
1160     * Construct a warning and a gallery from an array of duplicate files.
1161     * @param array $dupes
1162     * @return string
1163     */
1164    public function getDupeWarning( $dupes ) {
1165        if ( !$dupes ) {
1166            return '';
1167        }
1168
1169        $gallery = ImageGalleryBase::factory( false, $this->getContext() );
1170        $gallery->setShowBytes( false );
1171        $gallery->setShowDimensions( false );
1172        foreach ( $dupes as $file ) {
1173            $gallery->add( $file->getTitle() );
1174        }
1175
1176        return '<li>' .
1177            $this->msg( 'file-exists-duplicate' )->numParams( count( $dupes ) )->parse() .
1178            $gallery->toHTML() . "</li>\n";
1179    }
1180
1181    /** @inheritDoc */
1182    protected function getGroupName() {
1183        return 'media';
1184    }
1185
1186    /**
1187     * Should we rotate images in the preview on Special:Upload.
1188     *
1189     * This controls js: mw.config.get( 'wgFileCanRotate' )
1190     *
1191     * @todo What about non-BitmapHandler handled files?
1192     * @return bool
1193     */
1194    public static function rotationEnabled() {
1195        $bitmapHandler = new BitmapHandler();
1196        return $bitmapHandler->autoRotateEnabled();
1197    }
1198}
1199
1200/**
1201 * Retain the old class name for backwards compatibility.
1202 * @deprecated since 1.41
1203 */
1204class_alias( SpecialUpload::class, 'SpecialUpload' );