MediaWiki master
SpecialUpload.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Specials;
22
52use Psr\Log\LoggerInterface;
53use UnexpectedValueException;
54use UploadBase;
55use UploadForm;
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
75 public function __construct(
76 ?RepoGroup $repoGroup = null,
77 ?UserOptionsLookup $userOptionsLookup = null,
78 ?NamespaceInfo $nsInfo = null,
79 ?WatchlistManager $watchlistManager = null
80 ) {
81 parent::__construct( 'Upload', 'upload' );
82 // This class is extended and therefor fallback to global state - T265300
84 $this->jobQueueGroup = $services->getJobQueueGroup();
85 $repoGroup ??= $services->getRepoGroup();
86 $this->localRepo = $repoGroup->getLocalRepo();
87 $this->userOptionsLookup = $userOptionsLookup ?? $services->getUserOptionsLookup();
88 $this->nsInfo = $nsInfo ?? $services->getNamespaceInfo();
89 $this->watchlistManager = $watchlistManager ?? $services->getWatchlistManager();
90 $this->allowAsync = (
93 );
94 $this->log = LoggerFactory::getInstance( 'SpecialUpload' );
95 }
96
97 private function addMessageBoxStyling() {
98 $this->getOutput()->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
99 }
100
101 public function doesWrites() {
102 return true;
103 }
104
105 // Misc variables
106
108 public $mRequest;
111
114
116 public $mUpload;
117
122
123 // User input variables from the "description" section
124
128 public $mComment;
130 public $mLicense;
131
132 // User input variables from the root section
133
142
143 // Hidden variables
144
147
150
154 public $mTokenOk;
155
157 public $mUploadSuccessful = false;
158
163
167 protected function loadRequest() {
168 $this->mRequest = $request = $this->getRequest();
169 $this->mSourceType = $request->getVal( 'wpSourceType', 'file' );
170 $this->mUpload = UploadBase::createFromRequest( $request );
171 $this->mUploadClicked = $request->wasPosted()
172 && ( $request->getCheck( 'wpUpload' )
173 || $request->getCheck( 'wpUploadIgnoreWarning' ) );
174
175 // Guess the desired name from the filename if not provided
176 $this->mDesiredDestName = $request->getText( 'wpDestFile' );
177 if ( !$this->mDesiredDestName && $request->getFileName( 'wpUploadFile' ) !== null ) {
178 $this->mDesiredDestName = $request->getFileName( 'wpUploadFile' );
179 }
180 $this->mLicense = $request->getText( 'wpLicense' );
181
182 $this->mDestWarningAck = $request->getText( 'wpDestFileWarningAck' );
183 $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' )
184 || $request->getCheck( 'wpUploadIgnoreWarning' );
185 $this->mWatchthis = $request->getBool( 'wpWatchthis' ) && $this->getUser()->isRegistered();
186 $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' );
187 $this->mCopyrightSource = $request->getText( 'wpUploadSource' );
188
189 $this->mForReUpload = $request->getBool( 'wpForReUpload' ); // updating a file
190
191 $commentDefault = '';
192 $commentMsg = $this->msg( 'upload-default-description' )->inContentLanguage();
193 if ( !$this->mForReUpload && !$commentMsg->isDisabled() ) {
194 $commentDefault = $commentMsg->plain();
195 }
196 $this->mComment = $request->getText( 'wpUploadDescription', $commentDefault );
197
198 $this->mCancelUpload = $request->getCheck( 'wpCancelUpload' )
199 || $request->getCheck( 'wpReUpload' ); // b/w compat
200
201 // If it was posted check for the token (no remote POST'ing with user credentials)
202 $token = $request->getVal( 'wpEditToken' );
203 $this->mTokenOk = $this->getUser()->matchEditToken( $token );
204
205 // If this is an upload from Url and we're allowing async processing,
206 // check for the presence of the cache key parameter, or compute it. Else, it should be empty.
207 if ( $this->isAsyncUpload() ) {
208 $this->mCacheKey = \UploadFromUrl::getCacheKeyFromRequest( $request );
209 } else {
210 $this->mCacheKey = '';
211 }
212
213 $this->uploadFormTextTop = '';
214 $this->uploadFormTextAfterSummary = '';
215 }
216
222 protected function isAsyncUpload() {
223 return ( $this->mSourceType === 'url' && $this->allowAsync );
224 }
225
234 public function userCanExecute( User $user ) {
235 return UploadBase::isEnabled() && parent::userCanExecute( $user );
236 }
237
241 public function execute( $par ) {
243
244 $this->setHeaders();
245 $this->outputHeader();
246
247 # Check uploading enabled
248 if ( !UploadBase::isEnabled() ) {
249 throw new ErrorPageError( 'uploaddisabled', 'uploaddisabledtext' );
250 }
251
252 $this->addHelpLink( 'Help:Managing files' );
253
254 # Check permissions
255 $user = $this->getUser();
256 $permissionRequired = UploadBase::isAllowed( $user );
257 if ( $permissionRequired !== true ) {
258 throw new PermissionsError( $permissionRequired );
259 }
260
261 # Check blocks
262 if ( $user->isBlockedFromUpload() ) {
263 throw new UserBlockedError(
264 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
265 $user->getBlock(),
266 $user,
267 $this->getLanguage(),
268 $this->getRequest()->getIP()
269 );
270 }
271
272 # Check whether we actually want to allow changing stuff
273 $this->checkReadOnly();
274
275 $this->loadRequest();
276
277 # Unsave the temporary file in case this was a cancelled upload
278 if ( $this->mCancelUpload && !$this->unsaveUploadedFile() ) {
279 # Something went wrong, so unsaveUploadedFile showed a warning
280 return;
281 }
282
283 # If we have a cache key, show the upload status.
284 if ( $this->mTokenOk && $this->mCacheKey !== '' ) {
285 if ( $this->mUpload && $this->mUploadClicked && !$this->mCancelUpload ) {
286 # If the user clicked the upload button, we need to process the upload
287 $this->processAsyncUpload();
288 } else {
289 # Show the upload status
290 $this->showUploadStatus( $user );
291 }
292 } elseif (
293 # Process upload or show a form
294 $this->mTokenOk && !$this->mCancelUpload &&
295 ( $this->mUpload && $this->mUploadClicked )
296 ) {
297 $this->processUpload();
298 } else {
299 # Backwards compatibility hook
300 if ( !$this->getHookRunner()->onUploadForm_initial( $this ) ) {
301 wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" );
302
303 return;
304 }
305 $this->showUploadForm( $this->getUploadForm() );
306 }
307
308 # Cleanup
309 if ( $this->mUpload ) {
310 $this->mUpload->cleanupTempFile();
311 }
312 }
313
319 protected function showUploadStatus( $user ) {
320 // first, let's fetch the status from the main stash
321 $progress = UploadBase::getSessionStatus( $user, $this->mCacheKey );
322 if ( !$progress ) {
323 $progress = [ 'status' => Status::newFatal( 'invalid-cache-key' ) ];
324 }
325 $this->log->debug( 'Upload status: stage {stage}, result {result}', $progress );
326
327 $status = $progress['status'] ?? Status::newFatal( 'invalid-cache-key' );
328 $stage = $progress['stage'] ?? 'unknown';
329 $result = $progress['result'] ?? 'unknown';
330 switch ( $stage ) {
331 case 'publish':
332 switch ( $result ) {
333 case 'Success':
334 // The upload is done. Check the result and either show the form with the error
335 // occurred, or redirect to the file itself
336 // Success, redirect to description page
337 $this->mUploadSuccessful = true;
338 $this->getHookRunner()->onSpecialUploadComplete( $this );
339 // Redirect to the destination URL, but purge the cache of the file description page first
340 // TODO: understand why this is needed
341 $title = Title::makeTitleSafe( NS_FILE, $this->mRequest->getText( 'wpDestFile' ) );
342 if ( $title ) {
343 $this->log->debug( 'Purging page', [ 'title' => $title->getText() ] );
344 $page = new WikiFilePage( $title );
345 $page->doPurge();
346 }
347 $this->getOutput()->redirect( $this->mRequest->getText( 'wpDestUrl' ) );
348 break;
349 case 'Warning':
350 $this->showUploadWarning( UploadBase::unserializeWarnings( $progress['warnings'] ) );
351 break;
352 case 'Failure':
353 $details = $status->getValue();
354 // Verification failed.
355 if ( is_array( $details ) && isset( $details['verification'] ) ) {
356 $this->processVerificationError( $details['verification'] );
357 } else {
358 $this->showUploadError( $this->getOutput()->parseAsInterface(
359 $status->getWikiText( false, false, $this->getLanguage() ) )
360 );
361 }
362 break;
363 case 'Poll':
364 $this->showUploadProgress(
365 [ 'active' => true, 'msg' => 'upload-progress-processing' ]
366 );
367 break;
368 default:
369 // unknown result, just show a generic error
370 $this->showUploadError( $this->getOutput()->parseAsInterface(
371 $status->getWikiText( false, false, $this->getLanguage() ) )
372 );
373 break;
374 }
375 break;
376 case 'queued':
377 // show stalled progress bar
378 $this->showUploadProgress( [ 'active' => false, 'msg' => 'upload-progress-queued' ] );
379 break;
380 case 'fetching':
381 switch ( $result ) {
382 case 'Success':
383 // The file is being downloaded from a URL
384 // TODO: show active progress bar saying we're downloading the file
385 $this->showUploadProgress( [ 'active' => true, 'msg' => 'upload-progress-downloading' ] );
386 break;
387 case 'Failure':
388 // downloading failed
389 $this->showUploadError( $this->getOutput()->parseAsInterface(
390 $status->getWikiText( false, false, $this->getLanguage() ) )
391 );
392 break;
393 default:
394 // unknown result, just show a generic error
395 $this->showUploadError( $this->getOutput()->parseAsInterface(
396 $status->getWikiText( false, false, $this->getLanguage() ) )
397 );
398 break;
399 }
400 break;
401 default:
402 // unknown status, just show a generic error
403 if ( $status->isOK() ) {
404 $status = Status::newFatal( 'upload-progress-unknown' );
405 }
406 $statusmsg = $this->getOutput()->parseAsInterface(
407 $status->getWikiText( false, false, $this->getLanguage() )
408 );
409 $message = '<h2>' . $this->msg( 'uploaderror' )->escaped() . '</h2>' . HTML::errorBox( $statusmsg );
410 $this->addMessageBoxStyling();
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 $this->addMessageBoxStyling();
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 $this->addMessageBoxStyling();
724 }
725
732 protected function performUploadChecks( $fetchFileStatus ): bool {
733 if ( !$fetchFileStatus->isOK() ) {
734 $this->showUploadError( $this->getOutput()->parseAsInterface(
735 $fetchFileStatus->getWikiText( false, false, $this->getLanguage() )
736 ) );
737
738 return false;
739 }
740 if ( !$this->getHookRunner()->onUploadForm_BeforeProcessing( $this ) ) {
741 wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file." );
742 // This code path is deprecated. If you want to break upload processing
743 // do so by hooking into the appropriate hooks in UploadBase::verifyUpload
744 // and UploadBase::verifyFile.
745 // If you use this hook to break uploading, the user will be returned
746 // an empty form with no error message whatsoever.
747 return false;
748 }
749
750 // Upload verification
751 // If this is an asynchronous upload-by-url, skip the verification
752 if ( $this->isAsyncUpload() ) {
753 return true;
754 }
755 $details = $this->mUpload->verifyUpload();
756 if ( $details['status'] != UploadBase::OK ) {
757 $this->processVerificationError( $details );
758
759 return false;
760 }
761
762 // Verify permissions for this title
763 $user = $this->getUser();
764 $status = $this->mUpload->authorizeUpload( $user );
765 if ( !$status->isGood() ) {
767 $this->getOutput()->parseAsInterface(
768 Status::wrap( $status )->getWikiText( false, false, $this->getLanguage() )
769 )
770 );
771
772 return false;
773 }
774
775 $this->mLocalFile = $this->mUpload->getLocalFile();
776
777 // Check warnings if necessary
778 if ( !$this->mIgnoreWarning ) {
779 $warnings = $this->mUpload->checkWarnings( $user );
780 if ( $this->showUploadWarning( $warnings ) ) {
781 return false;
782 }
783 }
784
785 return true;
786 }
787
793 protected function getPageTextAndTags() {
794 // Get the page text if this is not a reupload
795 if ( !$this->mForReUpload ) {
796 $pageText = self::getInitialPageText( $this->mComment, $this->mLicense,
797 $this->mCopyrightStatus, $this->mCopyrightSource,
798 $this->getConfig() );
799 } else {
800 $pageText = false;
801 }
802 $changeTags = $this->getRequest()->getVal( 'wpChangeTags' );
803 if ( $changeTags === null || $changeTags === '' ) {
804 $changeTags = [];
805 } else {
806 $changeTags = array_filter( array_map( 'trim', explode( ',', $changeTags ) ) );
807 }
808 if ( $changeTags ) {
809 $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
810 $changeTags, $this->getUser() );
811 if ( !$changeTagsStatus->isOK() ) {
812 $this->showUploadError( $this->getOutput()->parseAsInterface(
813 $changeTagsStatus->getWikiText( false, false, $this->getLanguage() )
814 ) );
815
816 return null;
817 }
818 }
819 return [ $pageText, $changeTags ];
820 }
821
826 protected function processUpload() {
827 // Fetch the file if required
828 $status = $this->mUpload->fetchFile();
829 if ( !$this->performUploadChecks( $status ) ) {
830 return;
831 }
832 $user = $this->getUser();
833 $pageAndTags = $this->getPageTextAndTags();
834 if ( $pageAndTags === null ) {
835 return;
836 }
837 [ $pageText, $changeTags ] = $pageAndTags;
838
839 $status = $this->mUpload->performUpload(
840 $this->mComment,
841 $pageText,
842 $this->mWatchthis,
843 $user,
844 $changeTags
845 );
846
847 if ( !$status->isGood() ) {
848 $this->showRecoverableUploadError(
849 $this->getOutput()->parseAsInterface(
850 $status->getWikiText( false, false, $this->getLanguage() )
851 )
852 );
853
854 return;
855 }
856
857 // Success, redirect to description page
858 $this->mUploadSuccessful = true;
859 $this->getHookRunner()->onSpecialUploadComplete( $this );
860 $this->getOutput()->redirect( $this->mLocalFile->getTitle()->getFullURL() );
861 }
862
866 protected function processAsyncUpload() {
867 // Ensure the upload we're dealing with is an UploadFromUrl
868 if ( !$this->mUpload instanceof \UploadFromUrl ) {
869 $this->showUploadError( $this->msg( 'uploaderror' )->escaped() );
870
871 return;
872 }
873 // check we can fetch the file
874 $status = $this->mUpload->canFetchFile();
875 if ( !$this->performUploadChecks( $status ) ) {
876 $this->log->debug( 'Upload failed verification: {error}', [ 'error' => $status ] );
877 return;
878 }
879
880 $pageAndTags = $this->getPageTextAndTags();
881 if ( $pageAndTags === null ) {
882 return;
883 }
884 [ $pageText, $changeTags ] = $pageAndTags;
885
886 // Create a new job to process the upload from url
888 [
889 'filename' => $this->mUpload->getDesiredDestName(),
890 'url' => $this->mUpload->getUrl(),
891 'comment' => $this->mComment,
892 'tags' => $changeTags,
893 'text' => $pageText,
894 'watch' => $this->mWatchthis,
895 'watchlistexpiry' => null,
896 'session' => $this->getContext()->exportSession(),
897 'reupload' => $this->mForReUpload,
898 'ignorewarnings' => $this->mIgnoreWarning,
899 ]
900 );
901 // Save the session status
902 $cacheKey = $job->getCacheKey();
903 UploadBase::setSessionStatus( $this->getUser(), $cacheKey, [
904 'status' => Status::newGood(),
905 'stage' => 'queued',
906 'result' => 'Poll'
907 ] );
908 $this->log->info( "Submitting UploadFromUrlJob for {filename}",
909 [ 'filename' => $this->mUpload->getDesiredDestName() ]
910 );
911 // Submit the job
912 $this->jobQueueGroup->push( $job );
913 // Show the upload status
914 $this->showUploadStatus( $this->getUser() );
915 }
916
926 public static function getInitialPageText( $comment = '', $license = '',
927 $copyStatus = '', $source = '', ?Config $config = null
928 ) {
929 if ( $config === null ) {
930 wfDebug( __METHOD__ . ' called without a Config instance passed to it' );
931 $config = MediaWikiServices::getInstance()->getMainConfig();
932 }
933
934 $msg = [];
935 $forceUIMsgAsContentMsg = (array)$config->get( MainConfigNames::ForceUIMsgAsContentMsg );
936 /* These messages are transcluded into the actual text of the description page.
937 * Thus, forcing them as content messages makes the upload to produce an int: template
938 * instead of hardcoding it there in the uploader language.
939 */
940 foreach ( [ 'license-header', 'filedesc', 'filestatus', 'filesource' ] as $msgName ) {
941 if ( in_array( $msgName, $forceUIMsgAsContentMsg ) ) {
942 $msg[$msgName] = "{{int:$msgName}}";
943 } else {
944 $msg[$msgName] = wfMessage( $msgName )->inContentLanguage()->text();
945 }
946 }
947
948 $licenseText = '';
949 if ( $license !== '' ) {
950 $licenseText = '== ' . $msg['license-header'] . " ==\n{{" . $license . "}}\n";
951 }
952
953 $pageText = $comment . "\n";
954 $headerText = '== ' . $msg['filedesc'] . ' ==';
955 if ( $comment !== '' && !str_contains( $comment, $headerText ) ) {
956 // prepend header to page text unless it's already there (or there is no content)
957 $pageText = $headerText . "\n" . $pageText;
958 }
959
960 if ( $config->get( MainConfigNames::UseCopyrightUpload ) ) {
961 $pageText .= '== ' . $msg['filestatus'] . " ==\n" . $copyStatus . "\n";
962 $pageText .= $licenseText;
963 $pageText .= '== ' . $msg['filesource'] . " ==\n" . $source;
964 } else {
965 $pageText .= $licenseText;
966 }
967
968 // allow extensions to modify the content
969 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
970 ->onUploadForm_getInitialPageText( $pageText, $msg, $config );
971
972 return $pageText;
973 }
974
987 protected function getWatchCheck() {
988 $user = $this->getUser();
989 if ( $this->userOptionsLookup->getBoolOption( $user, 'watchdefault' ) ) {
990 // Watch all edits!
991 return true;
992 }
993
994 $desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName );
995 if ( $desiredTitleObj instanceof Title &&
996 $this->watchlistManager->isWatched( $user, $desiredTitleObj ) ) {
997 // Already watched, don't change that
998 return true;
999 }
1000
1001 $local = $this->localRepo->newFile( $this->mDesiredDestName );
1002 if ( $local && $local->exists() ) {
1003 // We're uploading a new version of an existing file.
1004 // No creation, so don't watch it if we're not already.
1005 return false;
1006 } else {
1007 // New page should get watched if that's our option.
1008 return $this->userOptionsLookup->getBoolOption( $user, 'watchcreations' ) ||
1009 $this->userOptionsLookup->getBoolOption( $user, 'watchuploads' );
1010 }
1011 }
1012
1018 protected function processVerificationError( $details ) {
1019 switch ( $details['status'] ) {
1021 case UploadBase::MIN_LENGTH_PARTNAME:
1022 $this->showRecoverableUploadError( $this->msg( 'minlength1' )->escaped() );
1023 break;
1024 case UploadBase::ILLEGAL_FILENAME:
1025 $this->showRecoverableUploadError( $this->msg( 'illegalfilename',
1026 $details['filtered'] )->parse() );
1027 break;
1028 case UploadBase::FILENAME_TOO_LONG:
1029 $this->showRecoverableUploadError( $this->msg( 'filename-toolong' )->escaped() );
1030 break;
1031 case UploadBase::FILETYPE_MISSING:
1032 $this->showRecoverableUploadError( $this->msg( 'filetype-missing' )->parse() );
1033 break;
1034 case UploadBase::WINDOWS_NONASCII_FILENAME:
1035 $this->showRecoverableUploadError( $this->msg( 'windows-nonascii-filename' )->parse() );
1036 break;
1037
1039 case UploadBase::EMPTY_FILE:
1040 $this->showUploadError( $this->msg( 'emptyfile' )->escaped() );
1041 break;
1042 case UploadBase::FILE_TOO_LARGE:
1043 $this->showUploadError( $this->msg( 'largefileserver' )->escaped() );
1044 break;
1045 case UploadBase::FILETYPE_BADTYPE:
1046 $msg = $this->msg( 'filetype-banned-type' );
1047 if ( isset( $details['blacklistedExt'] ) ) {
1048 $msg->params( $this->getLanguage()->commaList( $details['blacklistedExt'] ) );
1049 } else {
1050 $msg->params( $details['finalExt'] );
1051 }
1052 $extensions =
1053 array_unique( $this->getConfig()->get( MainConfigNames::FileExtensions ) );
1054 $msg->params( $this->getLanguage()->commaList( $extensions ),
1055 count( $extensions ) );
1056
1057 // Add PLURAL support for the first parameter. This results
1058 // in a bit unlogical parameter sequence, but does not break
1059 // old translations
1060 if ( isset( $details['blacklistedExt'] ) ) {
1061 $msg->params( count( $details['blacklistedExt'] ) );
1062 } else {
1063 $msg->params( 1 );
1064 }
1065
1066 $this->showUploadError( $msg->parse() );
1067 break;
1068 case UploadBase::VERIFICATION_ERROR:
1069 unset( $details['status'] );
1070 $code = array_shift( $details['details'] );
1071 $this->showUploadError( $this->msg( $code, $details['details'] )->parse() );
1072 break;
1073 default:
1074 throw new UnexpectedValueException( __METHOD__ . ": Unknown value `{$details['status']}`" );
1075 }
1076 }
1077
1083 protected function unsaveUploadedFile() {
1084 if ( !( $this->mUpload instanceof UploadFromStash ) ) {
1085 return true;
1086 }
1087 $success = $this->mUpload->unsaveUploadedFile();
1088 if ( !$success ) {
1089 $this->getOutput()->showErrorPage(
1090 'internalerror',
1091 'filedeleteerror',
1092 [ $this->mUpload->getTempPath() ]
1093 );
1094
1095 return false;
1096 } else {
1097 return true;
1098 }
1099 }
1100
1110 public static function getExistsWarning( $exists ) {
1111 if ( !$exists ) {
1112 return '';
1113 }
1114
1115 $file = $exists['file'];
1116 $filename = $file->getTitle()->getPrefixedText();
1117 $warnMsg = null;
1118
1119 if ( $exists['warning'] == 'exists' ) {
1120 // Exact match
1121 $warnMsg = wfMessage( 'fileexists', $filename );
1122 } elseif ( $exists['warning'] == 'page-exists' ) {
1123 // Page exists but file does not
1124 $warnMsg = wfMessage( 'filepageexists', $filename );
1125 } elseif ( $exists['warning'] == 'exists-normalized' ) {
1126 $warnMsg = wfMessage( 'fileexists-extension', $filename,
1127 $exists['normalizedFile']->getTitle()->getPrefixedText() );
1128 } elseif ( $exists['warning'] == 'thumb' ) {
1129 // Swapped argument order compared with other messages for backwards compatibility
1130 $warnMsg = wfMessage( 'fileexists-thumbnail-yes',
1131 $exists['thumbFile']->getTitle()->getPrefixedText(), $filename );
1132 } elseif ( $exists['warning'] == 'thumb-name' ) {
1133 // Image w/o '180px-' does not exists, but we do not like these filenames
1134 $name = $file->getName();
1135 $badPart = substr( $name, 0, strpos( $name, '-' ) + 1 );
1136 $warnMsg = wfMessage( 'file-thumbnail-no', $badPart );
1137 } elseif ( $exists['warning'] == 'bad-prefix' ) {
1138 $warnMsg = wfMessage( 'filename-bad-prefix', $exists['prefix'] );
1139 }
1140
1141 return $warnMsg ? $warnMsg->page( $file->getTitle() )->parse() : '';
1142 }
1143
1149 public function getDupeWarning( $dupes ) {
1150 if ( !$dupes ) {
1151 return '';
1152 }
1153
1154 $gallery = ImageGalleryBase::factory( false, $this->getContext() );
1155 $gallery->setShowBytes( false );
1156 $gallery->setShowDimensions( false );
1157 foreach ( $dupes as $file ) {
1158 $gallery->add( $file->getTitle() );
1159 }
1160
1161 return '<li>' .
1162 $this->msg( 'file-exists-duplicate' )->numParams( count( $dupes ) )->parse() .
1163 $gallery->toHTML() . "</li>\n";
1164 }
1165
1166 protected function getGroupName() {
1167 return 'media';
1168 }
1169
1178 public static function rotationEnabled() {
1179 $bitmapHandler = new BitmapHandler();
1180 return $bitmapHandler->autoRotateEnabled();
1181 }
1182}
1183
1188class_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:82
Generic handler for bitmap images.
Recent changes tagging.
An error page which can definitely be safely rendered using the OutputPage.
Show an error when a user tries to do something they do not have the necessary permissions for.
Show an error when the user tries to do something whilst blocked.
Local file in the wiki's own database.
Definition LocalFile.php:93
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition LocalRepo.php:57
Prioritized list of file repositories.
Definition RepoGroup.php:38
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:57
Handle enqueueing of background jobs.
Upload a file by URL, via the jobqueue.
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.
Special handling for representing file pages.
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.
getLanguage()
Shortcut to get user's 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:3143
Provides access to user options.
User class for the MediaWiki software.
Definition User.php:123
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.
Interface for configuration instances.
Definition Config.php:32
$source
if(count( $args)< 1) $job