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