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