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