MediaWiki master
SpecialUpload.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Specials;
22
24use ChangeTags;
28use LocalFile;
29use LocalRepo;
48use Psr\Log\LoggerInterface;
49use RepoGroup;
50use UnexpectedValueException;
51use UploadBase;
52use UploadForm;
55use WikiFilePage;
56
64
65 private LocalRepo $localRepo;
66 private UserOptionsLookup $userOptionsLookup;
67 private NamespaceInfo $nsInfo;
68 private WatchlistManager $watchlistManager;
70 public bool $allowAsync;
71 private JobQueueGroup $jobQueueGroup;
72 private LoggerInterface $log;
73
80 public function __construct(
81 ?RepoGroup $repoGroup = null,
82 ?UserOptionsLookup $userOptionsLookup = null,
83 ?NamespaceInfo $nsInfo = null,
84 ?WatchlistManager $watchlistManager = null
85 ) {
86 parent::__construct( 'Upload', 'upload' );
87 // This class is extended and therefor fallback to global state - T265300
89 $this->jobQueueGroup = $services->getJobQueueGroup();
90 $repoGroup ??= $services->getRepoGroup();
91 $this->localRepo = $repoGroup->getLocalRepo();
92 $this->userOptionsLookup = $userOptionsLookup ?? $services->getUserOptionsLookup();
93 $this->nsInfo = $nsInfo ?? $services->getNamespaceInfo();
94 $this->watchlistManager = $watchlistManager ?? $services->getWatchlistManager();
95 $this->allowAsync = (
98 );
99 $this->log = LoggerFactory::getInstance( 'SpecialUpload' );
100 }
101
102 public function doesWrites() {
103 return true;
104 }
105
106 // Misc variables
107
109 public $mRequest;
112
115
117 public $mUpload;
118
123
124 // User input variables from the "description" section
125
129 public $mComment;
131 public $mLicense;
132
133 // User input variables from the root section
134
143
144 // Hidden variables
145
148
151
155 public $mTokenOk;
156
158 public $mUploadSuccessful = false;
159
164
168 protected function loadRequest() {
169 $this->mRequest = $request = $this->getRequest();
170 $this->mSourceType = $request->getVal( 'wpSourceType', 'file' );
171 $this->mUpload = UploadBase::createFromRequest( $request );
172 $this->mUploadClicked = $request->wasPosted()
173 && ( $request->getCheck( 'wpUpload' )
174 || $request->getCheck( 'wpUploadIgnoreWarning' ) );
175
176 // Guess the desired name from the filename if not provided
177 $this->mDesiredDestName = $request->getText( 'wpDestFile' );
178 if ( !$this->mDesiredDestName && $request->getFileName( 'wpUploadFile' ) !== null ) {
179 $this->mDesiredDestName = $request->getFileName( 'wpUploadFile' );
180 }
181 $this->mLicense = $request->getText( 'wpLicense' );
182
183 $this->mDestWarningAck = $request->getText( 'wpDestFileWarningAck' );
184 $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' )
185 || $request->getCheck( 'wpUploadIgnoreWarning' );
186 $this->mWatchthis = $request->getBool( 'wpWatchthis' ) && $this->getUser()->isRegistered();
187 $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' );
188 $this->mCopyrightSource = $request->getText( 'wpUploadSource' );
189
190 $this->mForReUpload = $request->getBool( 'wpForReUpload' ); // updating a file
191
192 $commentDefault = '';
193 $commentMsg = $this->msg( 'upload-default-description' )->inContentLanguage();
194 if ( !$this->mForReUpload && !$commentMsg->isDisabled() ) {
195 $commentDefault = $commentMsg->plain();
196 }
197 $this->mComment = $request->getText( 'wpUploadDescription', $commentDefault );
198
199 $this->mCancelUpload = $request->getCheck( 'wpCancelUpload' )
200 || $request->getCheck( 'wpReUpload' ); // b/w compat
201
202 // If it was posted check for the token (no remote POST'ing with user credentials)
203 $token = $request->getVal( 'wpEditToken' );
204 $this->mTokenOk = $this->getUser()->matchEditToken( $token );
205
206 // If this is an upload from Url and we're allowing async processing,
207 // check for the presence of the cache key parameter, or compute it. Else, it should be empty.
208 if ( $this->isAsyncUpload() ) {
209 $this->mCacheKey = \UploadFromUrl::getCacheKeyFromRequest( $request );
210 } else {
211 $this->mCacheKey = '';
212 }
213
214 $this->uploadFormTextTop = '';
215 $this->uploadFormTextAfterSummary = '';
216 }
217
223 protected function isAsyncUpload() {
224 return ( $this->mSourceType === 'url' && $this->allowAsync );
225 }
226
235 public function userCanExecute( User $user ) {
236 return UploadBase::isEnabled() && parent::userCanExecute( $user );
237 }
238
242 public function execute( $par ) {
244
245 $this->setHeaders();
246 $this->outputHeader();
247
248 # Check uploading enabled
249 if ( !UploadBase::isEnabled() ) {
250 throw new ErrorPageError( 'uploaddisabled', 'uploaddisabledtext' );
251 }
252
253 $this->addHelpLink( 'Help:Managing files' );
254
255 # Check permissions
256 $user = $this->getUser();
257 $permissionRequired = UploadBase::isAllowed( $user );
258 if ( $permissionRequired !== true ) {
259 throw new PermissionsError( $permissionRequired );
260 }
261
262 # Check blocks
263 if ( $user->isBlockedFromUpload() ) {
264 throw new UserBlockedError(
265 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
266 $user->getBlock(),
267 $user,
268 $this->getLanguage(),
269 $this->getRequest()->getIP()
270 );
271 }
272
273 # Check whether we actually want to allow changing stuff
274 $this->checkReadOnly();
275
276 $this->loadRequest();
277
278 # Unsave the temporary file in case this was a cancelled upload
279 if ( $this->mCancelUpload && !$this->unsaveUploadedFile() ) {
280 # Something went wrong, so unsaveUploadedFile showed a warning
281 return;
282 }
283
284 # If we have a cache key, show the upload status.
285 if ( $this->mTokenOk && $this->mCacheKey !== '' ) {
286 if ( $this->mUpload && $this->mUploadClicked && !$this->mCancelUpload ) {
287 # If the user clicked the upload button, we need to process the upload
288 $this->processAsyncUpload();
289 } else {
290 # Show the upload status
291 $this->showUploadStatus( $user );
292 }
293 } elseif (
294 # Process upload or show a form
295 $this->mTokenOk && !$this->mCancelUpload &&
296 ( $this->mUpload && $this->mUploadClicked )
297 ) {
298 $this->processUpload();
299 } else {
300 # Backwards compatibility hook
301 if ( !$this->getHookRunner()->onUploadForm_initial( $this ) ) {
302 wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" );
303
304 return;
305 }
306 $this->showUploadForm( $this->getUploadForm() );
307 }
308
309 # Cleanup
310 if ( $this->mUpload ) {
311 $this->mUpload->cleanupTempFile();
312 }
313 }
314
320 protected function showUploadStatus( $user ) {
321 // first, let's fetch the status from the main stash
322 $progress = UploadBase::getSessionStatus( $user, $this->mCacheKey );
323 if ( !$progress ) {
324 $progress = [ 'status' => Status::newFatal( 'invalid-cache-key' ) ];
325 }
326 $this->log->debug( 'Upload status: stage {stage}, result {result}', $progress );
327
328 $status = $progress['status'] ?? Status::newFatal( 'invalid-cache-key' );
329 $stage = $progress['stage'] ?? 'unknown';
330 $result = $progress['result'] ?? 'unknown';
331 switch ( $stage ) {
332 case 'publish':
333 switch ( $result ) {
334 case 'Success':
335 // The upload is done. Check the result and either show the form with the error
336 // occurred, or redirect to the file itself
337 // Success, redirect to description page
338 $this->mUploadSuccessful = true;
339 $this->getHookRunner()->onSpecialUploadComplete( $this );
340 // Redirect to the destination URL, but purge the cache of the file description page first
341 // TODO: understand why this is needed
342 $title = Title::makeTitleSafe( NS_FILE, $this->mRequest->getText( 'wpDestFile' ) );
343 if ( $title ) {
344 $this->log->debug( 'Purging page', [ 'title' => $title->getText() ] );
345 $page = new WikiFilePage( $title );
346 $page->doPurge();
347 }
348 $this->getOutput()->redirect( $this->mRequest->getText( 'wpDestUrl' ) );
349 break;
350 case 'Warning':
351 $this->showUploadWarning( UploadBase::unserializeWarnings( $progress['warnings'] ) );
352 break;
353 case 'Failure':
354 $details = $status->getValue();
355 // Verification failed.
356 if ( is_array( $details ) && isset( $details['verification'] ) ) {
357 $this->processVerificationError( $details['verification'] );
358 } else {
359 $this->showUploadError( $this->getOutput()->parseAsInterface(
360 $status->getWikiText( false, false, $this->getLanguage() ) )
361 );
362 }
363 break;
364 case 'Poll':
365 $this->showUploadProgress(
366 [ 'active' => true, 'msg' => 'upload-progress-processing' ]
367 );
368 break;
369 default:
370 // unknown result, just show a generic error
371 $this->showUploadError( $this->getOutput()->parseAsInterface(
372 $status->getWikiText( false, false, $this->getLanguage() ) )
373 );
374 break;
375 }
376 break;
377 case 'queued':
378 // show stalled progress bar
379 $this->showUploadProgress( [ 'active' => false, 'msg' => 'upload-progress-queued' ] );
380 break;
381 case 'fetching':
382 switch ( $result ) {
383 case 'Success':
384 // The file is being downloaded from a URL
385 // TODO: show active progress bar saying we're downloading the file
386 $this->showUploadProgress( [ 'active' => true, 'msg' => 'upload-progress-downloading' ] );
387 break;
388 case 'Failure':
389 // downloading failed
390 $this->showUploadError( $this->getOutput()->parseAsInterface(
391 $status->getWikiText( false, false, $this->getLanguage() ) )
392 );
393 break;
394 default:
395 // unknown result, just show a generic error
396 $this->showUploadError( $this->getOutput()->parseAsInterface(
397 $status->getWikiText( false, false, $this->getLanguage() ) )
398 );
399 break;
400 }
401 break;
402 default:
403 // unknown status, just show a generic error
404 if ( $status->isOK() ) {
405 $status = Status::newFatal( 'upload-progress-unknown' );
406 }
407 $statusmsg = $this->getOutput()->parseAsInterface(
408 $status->getWikiText( false, false, $this->getLanguage() )
409 );
410 $message = '<h2>' . $this->msg( 'uploaderror' )->escaped() . '</h2>' . HTML::errorBox( $statusmsg );
411 $this->showUploadForm( $this->getUploadForm( $message ) );
412 break;
413 }
414 }
415
426 private function showUploadProgress( $options ) {
427 // $isActive = $options['active'] ?? false;
428 //$progressBarProperty = $isActive ? '' : 'disabled';
429 $message = $this->msg( $options['msg'] )->escaped();
430 $destUrl = $this->mRequest->getText( 'wpDestUrl', '' );
431 if ( !$destUrl && $this->mUpload ) {
432 if ( !$this->mLocalFile ) {
433 $this->mLocalFile = $this->mUpload->getLocalFile();
434 }
435 // This probably means the title is bad, so we can't get the URL
436 // but we need to wait for the job to execute.
437 if ( $this->mLocalFile === null ) {
438 $destUrl = '';
439 } else {
440 $destUrl = $this->mLocalFile->getTitle()->getFullURL();
441 }
442 }
443
444 $destName = $this->mDesiredDestName;
445 if ( !$destName ) {
446 $destName = $this->mRequest->getText( 'wpDestFile' );
447 }
448
449 // Needed if we have warnings to show
450 $sourceURL = $this->mRequest->getText( 'wpUploadFileURL' );
451
452 $form = new HTMLForm( [
453 'CacheKey' => [
454 'type' => 'hidden',
455 'default' => $this->mCacheKey,
456 ],
457 'SourceType' => [
458 'type' => 'hidden',
459 'default' => $this->mSourceType,
460 ],
461 'DestUrl' => [
462 'type' => 'hidden',
463 'default' => $destUrl,
464 ],
465 'DestFile' => [
466 'type' => 'hidden',
467 'default' => $destName,
468 ],
469 'UploadFileURL' => [
470 'type' => 'hidden',
471 'default' => $sourceURL,
472 ],
473 ], $this->getContext(), 'uploadProgress' );
474 $form->setSubmitText( $this->msg( 'upload-refresh' )->escaped() );
475 // TODO: use codex, add a progress bar
476 //$preHtml = "<cdx-progress-bar aria--label='upload progressbar' $progressBarProperty />";
477 $preHtml = "<div id='upload-progress-message'>$message</div>";
478 $form->addPreHtml( $preHtml );
479 $form->setSubmitCallback(
480 static function ( $formData ) {
481 return true;
482 }
483 );
484 $form->prepareForm();
485 $this->getOutput()->addHTML( $form->getHTML( false ) );
486 }
487
493 protected function showUploadForm( $form ) {
494 if ( $form instanceof HTMLForm ) {
495 $form->show();
496 } else {
497 $this->getOutput()->addHTML( $form );
498 }
499 }
500
509 protected function getUploadForm( $message = '', $sessionKey = '', $hideIgnoreWarning = false ) {
510 # Initialize form
511 $form = new UploadForm(
512 [
513 'watch' => $this->getWatchCheck(),
514 'forreupload' => $this->mForReUpload,
515 'sessionkey' => $sessionKey,
516 'hideignorewarning' => $hideIgnoreWarning,
517 'destwarningack' => (bool)$this->mDestWarningAck,
518
519 'description' => $this->mComment,
520 'texttop' => $this->uploadFormTextTop,
521 'textaftersummary' => $this->uploadFormTextAfterSummary,
522 'destfile' => $this->mDesiredDestName,
523 ],
524 $this->getContext(),
525 $this->getLinkRenderer(),
526 $this->localRepo,
527 $this->getContentLanguage(),
528 $this->nsInfo,
529 $this->getHookContainer()
530 );
531 $form->setTitle( $this->getPageTitle() ); // Remove subpage
532
533 # Check the token, but only if necessary
534 if (
535 !$this->mTokenOk && !$this->mCancelUpload &&
536 ( $this->mUpload && $this->mUploadClicked )
537 ) {
538 $form->addPreHtml( $this->msg( 'session_fail_preview' )->parse() );
539 }
540
541 # Give a notice if the user is uploading a file that has been deleted or moved
542 # Note that this is independent from the message 'filewasdeleted'
543 $desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName );
544 $delNotice = ''; // empty by default
545 if ( $desiredTitleObj instanceof Title && !$desiredTitleObj->exists() ) {
546 LogEventsList::showLogExtract( $delNotice, [ 'delete', 'move' ],
547 $desiredTitleObj,
548 '', [ 'lim' => 10,
549 'conds' => [ $this->localRepo->getReplicaDB()->expr( 'log_action', '!=', 'revision' ) ],
550 'showIfEmpty' => false,
551 'msgKey' => [ 'upload-recreate-warning' ] ]
552 );
553 }
554 $form->addPreHtml( $delNotice );
555
556 # Add text to form
557 $form->addPreHtml( '<div id="uploadtext">' .
558 $this->msg( 'uploadtext', [ $this->mDesiredDestName ] )->parseAsBlock() .
559 '</div>' );
560 # Add upload error message
561 $form->addPreHtml( $message );
562
563 # Add footer to form
564 $uploadFooter = $this->msg( 'uploadfooter' );
565 if ( !$uploadFooter->isDisabled() ) {
566 $form->addPostHtml( '<div id="mw-upload-footer-message">'
567 . $uploadFooter->parseAsBlock() . "</div>\n" );
568 }
569
570 return $form;
571 }
572
584 protected function showRecoverableUploadError( $message ) {
585 $stashStatus = $this->mUpload->tryStashFile( $this->getUser() );
586 if ( $stashStatus->isGood() ) {
587 $sessionKey = $stashStatus->getValue()->getFileKey();
588 $uploadWarning = 'upload-tryagain';
589 } else {
590 $sessionKey = null;
591 $uploadWarning = 'upload-tryagain-nostash';
592 }
593 $message = '<h2>' . $this->msg( 'uploaderror' )->escaped() . '</h2>' .
594 Html::errorBox( $message );
595
596 $form = $this->getUploadForm( $message, $sessionKey );
597 $form->setSubmitText( $this->msg( $uploadWarning )->escaped() );
598 $this->showUploadForm( $form );
599 }
600
609 protected function showUploadWarning( $warnings ) {
610 # If there are no warnings, or warnings we can ignore, return early.
611 # mDestWarningAck is set when some javascript has shown the warning
612 # to the user. mForReUpload is set when the user clicks the "upload a
613 # new version" link.
614 if ( !$warnings || ( count( $warnings ) == 1
615 && isset( $warnings['exists'] )
616 && ( $this->mDestWarningAck || $this->mForReUpload ) )
617 ) {
618 return false;
619 }
620
621 if ( $this->mUpload ) {
622 $stashStatus = $this->mUpload->tryStashFile( $this->getUser() );
623 if ( $stashStatus->isGood() ) {
624 $sessionKey = $stashStatus->getValue()->getFileKey();
625 $uploadWarning = 'uploadwarning-text';
626 } else {
627 $sessionKey = null;
628 $uploadWarning = 'uploadwarning-text-nostash';
629 }
630 } else {
631 $sessionKey = null;
632 $uploadWarning = 'uploadwarning-text-nostash';
633 }
634
635 // Add styles for the warning, reused from the live preview
636 $this->getOutput()->addModuleStyles( 'mediawiki.special' );
637
638 $linkRenderer = $this->getLinkRenderer();
639 $warningHtml = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n"
640 . '<div class="mw-destfile-warning"><ul>';
641 foreach ( $warnings as $warning => $args ) {
642 if ( $warning == 'badfilename' ) {
643 $this->mDesiredDestName = Title::makeTitle( NS_FILE, $args )->getText();
644 }
645 if ( $warning == 'exists' ) {
646 $msg = "\t<li>" . self::getExistsWarning( $args ) . "</li>\n";
647 } elseif ( $warning == 'no-change' ) {
648 $file = $args;
649 $filename = $file->getTitle()->getPrefixedText();
650 $msg = "\t<li>" . $this->msg( 'fileexists-no-change', $filename )->parse() . "</li>\n";
651 } elseif ( $warning == 'duplicate-version' ) {
652 $file = $args[0];
653 $count = count( $args );
654 $filename = $file->getTitle()->getPrefixedText();
655 $message = $this->msg( 'fileexists-duplicate-version' )
656 ->params( $filename )
657 ->numParams( $count );
658 $msg = "\t<li>" . $message->parse() . "</li>\n";
659 } elseif ( $warning == 'was-deleted' ) {
660 # If the file existed before and was deleted, warn the user of this
661 $ltitle = SpecialPage::getTitleFor( 'Log' );
662 $llink = $linkRenderer->makeKnownLink(
663 $ltitle,
664 $this->msg( 'deletionlog' )->text(),
665 [],
666 [
667 'type' => 'delete',
668 'page' => Title::makeTitle( NS_FILE, $args )->getPrefixedText(),
669 ]
670 );
671 $msg = "\t<li>" . $this->msg( 'filewasdeleted' )->rawParams( $llink )->parse() . "</li>\n";
672 } elseif ( $warning == 'duplicate' ) {
673 $msg = $this->getDupeWarning( $args );
674 } elseif ( $warning == 'duplicate-archive' ) {
675 if ( $args === '' ) {
676 $msg = "\t<li>" . $this->msg( 'file-deleted-duplicate-notitle' )->parse()
677 . "</li>\n";
678 } else {
679 $msg = "\t<li>" . $this->msg( 'file-deleted-duplicate',
680 Title::makeTitle( NS_FILE, $args )->getPrefixedText() )->parse()
681 . "</li>\n";
682 }
683 } else {
684 if ( $args === true ) {
685 $args = [];
686 } elseif ( !is_array( $args ) ) {
687 $args = [ $args ];
688 }
689 $msg = "\t<li>" . $this->msg( $warning, $args )->parse() . "</li>\n";
690 }
691 $warningHtml .= $msg;
692 }
693 $warningHtml .= "</ul></div>\n";
694 $warningHtml .= $this->msg( $uploadWarning )->parseAsBlock();
695
696 $form = $this->getUploadForm( $warningHtml, $sessionKey, /* $hideIgnoreWarning */ true );
697 $form->setSubmitTextMsg( 'upload-tryagain' );
698 $form->addButton( [
699 'name' => 'wpUploadIgnoreWarning',
700 'value' => $this->msg( 'ignorewarning' )->text()
701 ] );
702 $form->addButton( [
703 'name' => 'wpCancelUpload',
704 'value' => $this->msg( 'reuploaddesc' )->text()
705 ] );
706
707 $this->showUploadForm( $form );
708
709 # Indicate that we showed a form
710 return true;
711 }
712
718 protected function showUploadError( $message ) {
719 $message = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . '</h2>' .
720 Html::errorBox( $message );
721 $this->showUploadForm( $this->getUploadForm( $message ) );
722 }
723
730 protected function performUploadChecks( $fetchFileStatus ): bool {
731 if ( !$fetchFileStatus->isOK() ) {
732 $this->showUploadError( $this->getOutput()->parseAsInterface(
733 $fetchFileStatus->getWikiText( false, false, $this->getLanguage() )
734 ) );
735
736 return false;
737 }
738 if ( !$this->getHookRunner()->onUploadForm_BeforeProcessing( $this ) ) {
739 wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file." );
740 // This code path is deprecated. If you want to break upload processing
741 // do so by hooking into the appropriate hooks in UploadBase::verifyUpload
742 // and UploadBase::verifyFile.
743 // If you use this hook to break uploading, the user will be returned
744 // an empty form with no error message whatsoever.
745 return false;
746 }
747
748 // Upload verification
749 // If this is an asynchronous upload-by-url, skip the verification
750 if ( $this->isAsyncUpload() ) {
751 return true;
752 }
753 $details = $this->mUpload->verifyUpload();
754 if ( $details['status'] != UploadBase::OK ) {
755 $this->processVerificationError( $details );
756
757 return false;
758 }
759
760 // Verify permissions for this title
761 $user = $this->getUser();
762 $permErrors = $this->mUpload->verifyTitlePermissions( $user );
763 if ( $permErrors !== true ) {
764 $code = array_shift( $permErrors[0] );
765 $this->showRecoverableUploadError( $this->msg( $code, $permErrors[0] )->parse() );
766
767 return false;
768 }
769
770 $this->mLocalFile = $this->mUpload->getLocalFile();
771
772 // Check warnings if necessary
773 if ( !$this->mIgnoreWarning ) {
774 $warnings = $this->mUpload->checkWarnings( $user );
775 if ( $this->showUploadWarning( $warnings ) ) {
776 return false;
777 }
778 }
779
780 return true;
781 }
782
788 protected function getPageTextAndTags() {
789 // Get the page text if this is not a reupload
790 if ( !$this->mForReUpload ) {
791 $pageText = self::getInitialPageText( $this->mComment, $this->mLicense,
792 $this->mCopyrightStatus, $this->mCopyrightSource,
793 $this->getConfig() );
794 } else {
795 $pageText = false;
796 }
797 $changeTags = $this->getRequest()->getVal( 'wpChangeTags' );
798 if ( $changeTags === null || $changeTags === '' ) {
799 $changeTags = [];
800 } else {
801 $changeTags = array_filter( array_map( 'trim', explode( ',', $changeTags ) ) );
802 }
803 if ( $changeTags ) {
805 $changeTags, $this->getUser() );
806 if ( !$changeTagsStatus->isOK() ) {
807 $this->showUploadError( $this->getOutput()->parseAsInterface(
808 $changeTagsStatus->getWikiText( false, false, $this->getLanguage() )
809 ) );
810
811 return null;
812 }
813 }
814 return [ $pageText, $changeTags ];
815 }
816
821 protected function processUpload() {
822 // Fetch the file if required
823 $status = $this->mUpload->fetchFile();
824 if ( !$this->performUploadChecks( $status ) ) {
825 return;
826 }
827 $user = $this->getUser();
828 $pageAndTags = $this->getPageTextAndTags();
829 if ( $pageAndTags === null ) {
830 return;
831 }
832 [ $pageText, $changeTags ] = $pageAndTags;
833
834 $status = $this->mUpload->performUpload(
835 $this->mComment,
836 $pageText,
837 $this->mWatchthis,
838 $user,
839 $changeTags
840 );
841
842 if ( !$status->isGood() ) {
843 $this->showRecoverableUploadError(
844 $this->getOutput()->parseAsInterface(
845 $status->getWikiText( false, false, $this->getLanguage() )
846 )
847 );
848
849 return;
850 }
851
852 // Success, redirect to description page
853 $this->mUploadSuccessful = true;
854 $this->getHookRunner()->onSpecialUploadComplete( $this );
855 $this->getOutput()->redirect( $this->mLocalFile->getTitle()->getFullURL() );
856 }
857
861 protected function processAsyncUpload() {
862 // Ensure the upload we're dealing with is an UploadFromUrl
863 if ( !$this->mUpload instanceof \UploadFromUrl ) {
864 $this->showUploadError( $this->msg( 'uploaderror' )->escaped() );
865
866 return;
867 }
868 // check we can fetch the file
869 $status = $this->mUpload->canFetchFile();
870 if ( !$this->performUploadChecks( $status ) ) {
871 $this->log->debug( 'Upload failed verification: {error}', [ 'error' => $status ] );
872 return;
873 }
874
875 $pageAndTags = $this->getPageTextAndTags();
876 if ( $pageAndTags === null ) {
877 return;
878 }
879 [ $pageText, $changeTags ] = $pageAndTags;
880
881 // Create a new job to process the upload from url
882 $job = new \UploadFromUrlJob(
883 [
884 'filename' => $this->mUpload->getDesiredDestName(),
885 'url' => $this->mUpload->getUrl(),
886 'comment' => $this->mComment,
887 'tags' => $changeTags,
888 'text' => $pageText,
889 'watch' => $this->mWatchthis,
890 'watchlistexpiry' => null,
891 'session' => $this->getContext()->exportSession(),
892 'reupload' => $this->mForReUpload,
893 'ignorewarnings' => $this->mIgnoreWarning,
894 ]
895 );
896 // Save the session status
897 $cacheKey = $job->getCacheKey();
898 UploadBase::setSessionStatus( $this->getUser(), $cacheKey, [
899 'status' => Status::newGood(),
900 'stage' => 'queued',
901 'result' => 'Poll'
902 ] );
903 $this->log->info( "Submitting UploadFromUrlJob for {filename}",
904 [ 'filename' => $this->mUpload->getDesiredDestName() ]
905 );
906 // Submit the job
907 $this->jobQueueGroup->push( $job );
908 // Show the upload status
909 $this->showUploadStatus( $this->getUser() );
910 }
911
921 public static function getInitialPageText( $comment = '', $license = '',
922 $copyStatus = '', $source = '', ?Config $config = null
923 ) {
924 if ( $config === null ) {
925 wfDebug( __METHOD__ . ' called without a Config instance passed to it' );
926 $config = MediaWikiServices::getInstance()->getMainConfig();
927 }
928
929 $msg = [];
930 $forceUIMsgAsContentMsg = (array)$config->get( MainConfigNames::ForceUIMsgAsContentMsg );
931 /* These messages are transcluded into the actual text of the description page.
932 * Thus, forcing them as content messages makes the upload to produce an int: template
933 * instead of hardcoding it there in the uploader language.
934 */
935 foreach ( [ 'license-header', 'filedesc', 'filestatus', 'filesource' ] as $msgName ) {
936 if ( in_array( $msgName, $forceUIMsgAsContentMsg ) ) {
937 $msg[$msgName] = "{{int:$msgName}}";
938 } else {
939 $msg[$msgName] = wfMessage( $msgName )->inContentLanguage()->text();
940 }
941 }
942
943 $licenseText = '';
944 if ( $license !== '' ) {
945 $licenseText = '== ' . $msg['license-header'] . " ==\n{{" . $license . "}}\n";
946 }
947
948 $pageText = $comment . "\n";
949 $headerText = '== ' . $msg['filedesc'] . ' ==';
950 if ( $comment !== '' && !str_contains( $comment, $headerText ) ) {
951 // prepend header to page text unless it's already there (or there is no content)
952 $pageText = $headerText . "\n" . $pageText;
953 }
954
955 if ( $config->get( MainConfigNames::UseCopyrightUpload ) ) {
956 $pageText .= '== ' . $msg['filestatus'] . " ==\n" . $copyStatus . "\n";
957 $pageText .= $licenseText;
958 $pageText .= '== ' . $msg['filesource'] . " ==\n" . $source;
959 } else {
960 $pageText .= $licenseText;
961 }
962
963 // allow extensions to modify the content
964 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
965 ->onUploadForm_getInitialPageText( $pageText, $msg, $config );
966
967 return $pageText;
968 }
969
982 protected function getWatchCheck() {
983 $user = $this->getUser();
984 if ( $this->userOptionsLookup->getBoolOption( $user, 'watchdefault' ) ) {
985 // Watch all edits!
986 return true;
987 }
988
989 $desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName );
990 if ( $desiredTitleObj instanceof Title &&
991 $this->watchlistManager->isWatched( $user, $desiredTitleObj ) ) {
992 // Already watched, don't change that
993 return true;
994 }
995
996 $local = $this->localRepo->newFile( $this->mDesiredDestName );
997 if ( $local && $local->exists() ) {
998 // We're uploading a new version of an existing file.
999 // No creation, so don't watch it if we're not already.
1000 return false;
1001 } else {
1002 // New page should get watched if that's our option.
1003 return $this->userOptionsLookup->getBoolOption( $user, 'watchcreations' ) ||
1004 $this->userOptionsLookup->getBoolOption( $user, 'watchuploads' );
1005 }
1006 }
1007
1013 protected function processVerificationError( $details ) {
1014 switch ( $details['status'] ) {
1016 case UploadBase::MIN_LENGTH_PARTNAME:
1017 $this->showRecoverableUploadError( $this->msg( 'minlength1' )->escaped() );
1018 break;
1019 case UploadBase::ILLEGAL_FILENAME:
1020 $this->showRecoverableUploadError( $this->msg( 'illegalfilename',
1021 $details['filtered'] )->parse() );
1022 break;
1023 case UploadBase::FILENAME_TOO_LONG:
1024 $this->showRecoverableUploadError( $this->msg( 'filename-toolong' )->escaped() );
1025 break;
1026 case UploadBase::FILETYPE_MISSING:
1027 $this->showRecoverableUploadError( $this->msg( 'filetype-missing' )->parse() );
1028 break;
1029 case UploadBase::WINDOWS_NONASCII_FILENAME:
1030 $this->showRecoverableUploadError( $this->msg( 'windows-nonascii-filename' )->parse() );
1031 break;
1032
1034 case UploadBase::EMPTY_FILE:
1035 $this->showUploadError( $this->msg( 'emptyfile' )->escaped() );
1036 break;
1037 case UploadBase::FILE_TOO_LARGE:
1038 $this->showUploadError( $this->msg( 'largefileserver' )->escaped() );
1039 break;
1040 case UploadBase::FILETYPE_BADTYPE:
1041 $msg = $this->msg( 'filetype-banned-type' );
1042 if ( isset( $details['blacklistedExt'] ) ) {
1043 $msg->params( $this->getLanguage()->commaList( $details['blacklistedExt'] ) );
1044 } else {
1045 $msg->params( $details['finalExt'] );
1046 }
1047 $extensions =
1048 array_unique( $this->getConfig()->get( MainConfigNames::FileExtensions ) );
1049 $msg->params( $this->getLanguage()->commaList( $extensions ),
1050 count( $extensions ) );
1051
1052 // Add PLURAL support for the first parameter. This results
1053 // in a bit unlogical parameter sequence, but does not break
1054 // old translations
1055 if ( isset( $details['blacklistedExt'] ) ) {
1056 $msg->params( count( $details['blacklistedExt'] ) );
1057 } else {
1058 $msg->params( 1 );
1059 }
1060
1061 $this->showUploadError( $msg->parse() );
1062 break;
1063 case UploadBase::VERIFICATION_ERROR:
1064 unset( $details['status'] );
1065 $code = array_shift( $details['details'] );
1066 $this->showUploadError( $this->msg( $code, $details['details'] )->parse() );
1067 break;
1068 case UploadBase::HOOK_ABORTED:
1069 if ( is_array( $details['error'] ) ) { # allow hooks to return error details in an array
1070 $args = $details['error'];
1071 $error = array_shift( $args );
1072 } else {
1073 $error = $details['error'];
1074 $args = null;
1075 }
1076
1077 $this->showUploadError( $this->msg( $error, $args )->parse() );
1078 break;
1079 default:
1080 throw new UnexpectedValueException( __METHOD__ . ": Unknown value `{$details['status']}`" );
1081 }
1082 }
1083
1089 protected function unsaveUploadedFile() {
1090 if ( !( $this->mUpload instanceof UploadFromStash ) ) {
1091 return true;
1092 }
1093 $success = $this->mUpload->unsaveUploadedFile();
1094 if ( !$success ) {
1095 $this->getOutput()->showErrorPage(
1096 'internalerror',
1097 'filedeleteerror',
1098 [ $this->mUpload->getTempPath() ]
1099 );
1100
1101 return false;
1102 } else {
1103 return true;
1104 }
1105 }
1106
1116 public static function getExistsWarning( $exists ) {
1117 if ( !$exists ) {
1118 return '';
1119 }
1120
1121 $file = $exists['file'];
1122 $filename = $file->getTitle()->getPrefixedText();
1123 $warnMsg = null;
1124
1125 if ( $exists['warning'] == 'exists' ) {
1126 // Exact match
1127 $warnMsg = wfMessage( 'fileexists', $filename );
1128 } elseif ( $exists['warning'] == 'page-exists' ) {
1129 // Page exists but file does not
1130 $warnMsg = wfMessage( 'filepageexists', $filename );
1131 } elseif ( $exists['warning'] == 'exists-normalized' ) {
1132 $warnMsg = wfMessage( 'fileexists-extension', $filename,
1133 $exists['normalizedFile']->getTitle()->getPrefixedText() );
1134 } elseif ( $exists['warning'] == 'thumb' ) {
1135 // Swapped argument order compared with other messages for backwards compatibility
1136 $warnMsg = wfMessage( 'fileexists-thumbnail-yes',
1137 $exists['thumbFile']->getTitle()->getPrefixedText(), $filename );
1138 } elseif ( $exists['warning'] == 'thumb-name' ) {
1139 // Image w/o '180px-' does not exists, but we do not like these filenames
1140 $name = $file->getName();
1141 $badPart = substr( $name, 0, strpos( $name, '-' ) + 1 );
1142 $warnMsg = wfMessage( 'file-thumbnail-no', $badPart );
1143 } elseif ( $exists['warning'] == 'bad-prefix' ) {
1144 $warnMsg = wfMessage( 'filename-bad-prefix', $exists['prefix'] );
1145 }
1146
1147 return $warnMsg ? $warnMsg->page( $file->getTitle() )->parse() : '';
1148 }
1149
1155 public function getDupeWarning( $dupes ) {
1156 if ( !$dupes ) {
1157 return '';
1158 }
1159
1160 $gallery = ImageGalleryBase::factory( false, $this->getContext() );
1161 $gallery->setShowBytes( false );
1162 $gallery->setShowDimensions( false );
1163 foreach ( $dupes as $file ) {
1164 $gallery->add( $file->getTitle() );
1165 }
1166
1167 return '<li>' .
1168 $this->msg( 'file-exists-duplicate' )->numParams( count( $dupes ) )->parse() .
1169 $gallery->toHTML() . "</li>\n";
1170 }
1171
1172 protected function getGroupName() {
1173 return 'media';
1174 }
1175
1184 public static function rotationEnabled() {
1185 $bitmapHandler = new BitmapHandler();
1186 return $bitmapHandler->autoRotateEnabled();
1187 }
1188}
1189
1194class_alias( SpecialUpload::class, 'SpecialUpload' );
const NS_FILE
Definition Defines.php:71
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Generic handler for bitmap images.
Recent changes tagging.
static canAddTagsAccompanyingChange(array $tags, ?Authority $performer=null, $checkBlock=true)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
An error page which can definitely be safely rendered using the OutputPage.
Handle enqueueing of background jobs.
Local file in the wiki's own database.
Definition LocalFile.php:75
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition LocalRepo.php:49
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:209
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
Create PSR-3 logger objects.
A class containing constants representing the names of configuration variables.
const EnableAsyncUploads
Name constant for the EnableAsyncUploads setting, for use with Config::get()
const ForceUIMsgAsContentMsg
Name constant for the ForceUIMsgAsContentMsg setting, for use with Config::get()
const UseCopyrightUpload
Name constant for the UseCopyrightUpload setting, for use with Config::get()
const FileExtensions
Name constant for the FileExtensions setting, for use with Config::get()
const EnableAsyncUploadsByURL
Name constant for the EnableAsyncUploadsByURL setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
WebRequest clone which takes values from a provided array.
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form,...
Parent class for all special pages.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
getUser()
Shortcut to get the User executing this instance.
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
getPageTitle( $subpage=false)
Get a self-referential title object.
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
getConfig()
Shortcut to get main config object.
getContext()
Gets the context this SpecialPage is executed in.
getRequest()
Get the WebRequest being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
getContentLanguage()
Shortcut to get content language.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages By default the message key is the canonical name of...
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Form for uploading media files.
bool $mForReUpload
The user followed an "overwrite this file" link.
doesWrites()
Indicates whether POST requests to this special page require write access to the wiki.
bool $mCancelUpload
The user clicked "Cancel and return to upload form" button.
getWatchCheck()
See if we should check the 'watch this page' checkbox on the form based on the user's preferences and...
bool $mUploadSuccessful
Subclasses can use this to determine whether a file was uploaded.
static rotationEnabled()
Should we rotate images in the preview on Special:Upload.
unsaveUploadedFile()
Remove a temporarily kept file stashed by saveTempUploadedFile().
string $mDesiredDestName
The requested target file name.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
string $uploadFormTextTop
Raw html injection point for hooks not using HTMLForm.
showUploadForm( $form)
Show the main upload form.
bool bool $allowAsync
wether uploads by url should be asynchronous or not
performUploadChecks( $fetchFileStatus)
Common steps for processing uploads.
showUploadError( $message)
Show the upload form with error message, but do not stash the file.
processVerificationError( $details)
Provides output to the user for a result of UploadBase::verifyUpload.
string $uploadFormTextAfterSummary
Raw html injection point for hooks not using HTMLForm.
static getExistsWarning( $exists)
Functions for formatting warnings.
showUploadWarning( $warnings)
Stashes the upload, shows the main form, but adds a "continue anyway button".
userCanExecute(User $user)
This page can be shown if uploading is enabled.
processAsyncUpload()
Process an asynchronous upload.
getDupeWarning( $dupes)
Construct a warning and a gallery from an array of duplicate files.
getUploadForm( $message='', $sessionKey='', $hideIgnoreWarning=false)
Get an UploadForm instance with title and text properly set.
loadRequest()
Initialize instance variables from request and create an Upload handler.
string $mCacheKey
The cache key to use to retreive the status of your async upload.
showRecoverableUploadError( $message)
Stashes the upload and shows the main upload form.
getPageTextAndTags()
Get the page text and tags for the upload.
static getInitialPageText( $comment='', $license='', $copyStatus='', $source='', ?Config $config=null)
Get the initial image page text based on a comment and optional file status information.
showUploadStatus( $user)
Show the upload status.
isAsyncUpload()
Check if the current request is an async upload by url request.
WebRequest FauxRequest $mRequest
The request this form is supposed to handle.
__construct(?RepoGroup $repoGroup=null, ?UserOptionsLookup $userOptionsLookup=null, ?NamespaceInfo $nsInfo=null, ?WatchlistManager $watchlistManager=null)
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Represents a title within MediaWiki.
Definition Title.php:78
exists( $flags=0)
Check if page exists.
Definition Title.php:3138
Provides access to user options.
internal since 1.36
Definition User.php:93
Show an error when a user tries to do something they do not have the necessary permissions for.
Prioritized list of file repositories.
Definition RepoGroup.php:32
UploadBase and subclasses are the backend of MediaWiki's file uploads.
Sub class of HTMLForm that provides the form section of SpecialUpload.
Implements uploading from previously stored file.
Implements uploading from a HTTP resource.
Show an error when the user tries to do something whilst blocked.
Special handling for representing file pages.
Interface for configuration instances.
Definition Config.php:32
$source
if(count( $args)< 1) $job