MediaWiki master
SpecialUpload.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Specials;
8
43use Psr\Log\LoggerInterface;
44use UnexpectedValueException;
45
53
54 private LocalRepo $localRepo;
55 private UserOptionsLookup $userOptionsLookup;
56 private NamespaceInfo $nsInfo;
57 private WatchlistManager $watchlistManager;
58 private JobQueueGroup $jobQueueGroup;
59 private LoggerInterface $log;
60
61 public function __construct(
62 ?RepoGroup $repoGroup = null,
63 ?UserOptionsLookup $userOptionsLookup = null,
64 ?NamespaceInfo $nsInfo = null,
65 ?WatchlistManager $watchlistManager = null
66 ) {
67 parent::__construct( 'Upload', 'upload' );
68 // This class is extended and therefor fallback to global state - T265300
70 $this->jobQueueGroup = $services->getJobQueueGroup();
71 $repoGroup ??= $services->getRepoGroup();
72 $this->localRepo = $repoGroup->getLocalRepo();
73 $this->userOptionsLookup = $userOptionsLookup ?? $services->getUserOptionsLookup();
74 $this->nsInfo = $nsInfo ?? $services->getNamespaceInfo();
75 $this->watchlistManager = $watchlistManager ?? $services->getWatchlistManager();
76 $this->log = LoggerFactory::getInstance( 'SpecialUpload' );
77 }
78
79 private function addMessageBoxStyling() {
80 $this->getOutput()->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
81 }
82
84 public function doesWrites() {
85 return true;
86 }
87
88 // Misc variables
89
91 public $mRequest;
94
96 public $mCacheKey;
97
99 public $mUpload;
100
105
106 // User input variables from the "description" section
107
111 public $mComment;
113 public $mLicense;
114
115 // User input variables from the root section
116
125
126 // Hidden variables
127
130
133
137 public $mTokenOk;
138
140 public $mUploadSuccessful = false;
141
146
150 protected function loadRequest() {
151 $this->mRequest = $request = $this->getRequest();
152 $this->mSourceType = $request->getVal( 'wpSourceType', 'file' );
153 $this->mUpload = UploadBase::createFromRequest( $request );
154 $this->mUploadClicked = $request->wasPosted()
155 && ( $request->getCheck( 'wpUpload' )
156 || $request->getCheck( 'wpUploadIgnoreWarning' ) );
157
158 // Guess the desired name from the filename if not provided
159 $this->mDesiredDestName = $request->getText( 'wpDestFile' );
160 if ( !$this->mDesiredDestName && $request->getFileName( 'wpUploadFile' ) !== null ) {
161 $this->mDesiredDestName = $request->getFileName( 'wpUploadFile' );
162 }
163 $this->mLicense = $request->getText( 'wpLicense' );
164
165 $this->mDestWarningAck = $request->getText( 'wpDestFileWarningAck' );
166 $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' )
167 || $request->getCheck( 'wpUploadIgnoreWarning' );
168 $this->mWatchthis = $request->getBool( 'wpWatchthis' ) && $this->getUser()->isRegistered();
169 $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' );
170 $this->mCopyrightSource = $request->getText( 'wpUploadSource' );
171
172 $this->mForReUpload = $request->getBool( 'wpForReUpload' ); // updating a file
173
174 $commentDefault = '';
175 $commentMsg = $this->msg( 'upload-default-description' )->inContentLanguage();
176 if ( !$this->mForReUpload && !$commentMsg->isDisabled() ) {
177 $commentDefault = $commentMsg->plain();
178 }
179 $this->mComment = $request->getText( 'wpUploadDescription', $commentDefault );
180
181 $this->mCancelUpload = $request->getCheck( 'wpCancelUpload' )
182 || $request->getCheck( 'wpReUpload' ); // b/w compat
183
184 // If it was posted check for the token (no remote POST'ing with user credentials)
185 $token = $request->getVal( 'wpEditToken' );
186 $this->mTokenOk = $this->getUser()->matchEditToken( $token );
187
188 // If this is an upload from Url and we're allowing async processing,
189 // check for the presence of the cache key parameter, or compute it. Else, it should be empty.
190 if ( $this->isAsyncUpload() ) {
191 $this->mCacheKey = \MediaWiki\Upload\UploadFromUrl::getCacheKeyFromRequest( $request );
192 } else {
193 $this->mCacheKey = '';
194 }
195
196 $this->uploadFormTextTop = '';
197 $this->uploadFormTextAfterSummary = '';
198 }
199
205 protected function isAsyncUpload() {
206 return $this->mSourceType === 'url'
209 }
210
219 public function userCanExecute( User $user ) {
220 return UploadBase::isEnabled() && parent::userCanExecute( $user );
221 }
222
226 public function execute( $par ) {
228
229 $this->setHeaders();
230 $this->outputHeader();
231
232 # Check uploading enabled
233 if ( !UploadBase::isEnabled() ) {
234 throw new ErrorPageError( 'uploaddisabled', 'uploaddisabledtext' );
235 }
236
237 $this->addHelpLink( 'Help:Managing files' );
238
239 # Check permissions
240 $user = $this->getUser();
241 $permissionRequired = UploadBase::isAllowed( $user );
242 if ( $permissionRequired !== true ) {
243 throw new PermissionsError( $permissionRequired );
244 }
245
246 # Check blocks
247 if ( $user->isBlockedFromUpload() ) {
248 throw new UserBlockedError(
249 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
250 $user->getBlock(),
251 $user,
252 $this->getLanguage(),
253 $this->getRequest()->getIP()
254 );
255 }
256
257 # Check whether we actually want to allow changing stuff
258 $this->checkReadOnly();
259
260 try {
261 $this->loadRequest();
262 } catch ( UploadStashException $e ) {
263 $this->showUploadError( $this->msg( 'upload-stash-error', $e->getMessageObject() )->escaped() );
264 return;
265 }
266
267 # Unsave the temporary file in case this was a cancelled upload
268 if ( $this->mCancelUpload && !$this->unsaveUploadedFile() ) {
269 # Something went wrong, so unsaveUploadedFile showed a warning
270 return;
271 }
272
273 # If we have a cache key, show the upload status.
274 if ( $this->mTokenOk && $this->mCacheKey !== '' ) {
275 if ( $this->mUpload && $this->mUploadClicked && !$this->mCancelUpload ) {
276 # If the user clicked the upload button, we need to process the upload
277 $this->processAsyncUpload();
278 } else {
279 # Show the upload status
280 $this->showUploadStatus( $user );
281 }
282 } elseif (
283 # Process upload or show a form
284 $this->mTokenOk && !$this->mCancelUpload &&
285 ( $this->mUpload && $this->mUploadClicked )
286 ) {
287 $this->processUpload();
288 } else {
289 # Backwards compatibility hook
290 if ( !$this->getHookRunner()->onUploadForm_initial( $this ) ) {
291 wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" );
292
293 return;
294 }
295 $this->showUploadForm( $this->getUploadForm( '', $this->mUpload?->getStashFile()?->getFileKey() ) );
296 }
297
298 # Cleanup
299 if ( $this->mUpload ) {
300 $this->mUpload->cleanupTempFile();
301 }
302 }
303
309 protected function showUploadStatus( $user ) {
310 // first, let's fetch the status from the main stash
311 $progress = UploadBase::getSessionStatus( $user, $this->mCacheKey );
312 if ( !$progress ) {
313 $progress = [ 'status' => Status::newFatal( 'invalid-cache-key' ) ];
314 }
315 $this->log->debug( 'Upload status: stage {stage}, result {result}', $progress );
316
317 $status = $progress['status'] ?? Status::newFatal( 'invalid-cache-key' );
318 $stage = $progress['stage'] ?? 'unknown';
319 $result = $progress['result'] ?? 'unknown';
320 switch ( $stage ) {
321 case 'publish':
322 switch ( $result ) {
323 case 'Success':
324 // The upload is done. Check the result and either show the form with the error
325 // occurred, or redirect to the file itself
326 // Success, redirect to description page
327 $this->mUploadSuccessful = true;
328 $this->getHookRunner()->onSpecialUploadComplete( $this );
329 // Redirect to the destination URL, but purge the cache of the file description page first
330 // TODO: understand why this is needed
331 $title = Title::makeTitleSafe( NS_FILE, $this->mRequest->getText( 'wpDestFile' ) );
332 if ( $title ) {
333 $this->log->debug( 'Purging page', [ 'title' => $title->getText() ] );
334 $page = new WikiFilePage( $title );
335 $page->doPurge();
336 }
337 $this->getOutput()->redirect( $this->mRequest->getText( 'wpDestUrl' ) );
338 break;
339 case 'Warning':
340 $this->showUploadWarning( UploadBase::unserializeWarnings( $progress['warnings'] ) );
341 break;
342 case 'Failure':
343 $details = $status->getValue();
344 // Verification failed.
345 if ( is_array( $details ) && isset( $details['verification'] ) ) {
346 $this->processVerificationError( $details['verification'] );
347 } else {
348 $this->showUploadError( $this->getOutput()->parseAsInterface(
349 $status->getWikiText( false, false, $this->getLanguage() ) )
350 );
351 }
352 break;
353 case 'Poll':
354 $this->showUploadProgress(
355 [ 'active' => true, 'msg' => 'upload-progress-processing' ]
356 );
357 break;
358 default:
359 // unknown result, just show a generic error
360 if ( $status->isOK() ) {
361 $status = Status::newFatal( 'upload-progress-unknown' );
362 }
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 'Poll':
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 if ( $status->isOK() ) {
389 $status = Status::newFatal( 'upload-progress-unknown' );
390 }
391 $this->showUploadError( $this->getOutput()->parseAsInterface(
392 $status->getWikiText( false, false, $this->getLanguage() ) )
393 );
394 break;
395 }
396 break;
397 default:
398 // unknown status, just show a generic error
399 if ( $status->isOK() ) {
400 $status = Status::newFatal( 'upload-progress-unknown' );
401 }
402 $statusmsg = $this->getOutput()->parseAsInterface(
403 $status->getWikiText( false, false, $this->getLanguage() )
404 );
405 $message = '<h2>' . $this->msg( 'uploaderror' )->escaped() . '</h2>' . Html::errorBox( $statusmsg );
406 $this->addMessageBoxStyling();
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 $form = new HTMLForm( [
446 'CacheKey' => [
447 'type' => 'hidden',
448 'default' => $this->mCacheKey,
449 ],
450 'SourceType' => [
451 'type' => 'hidden',
452 'default' => $this->mSourceType,
453 ],
454 'DestUrl' => [
455 'type' => 'hidden',
456 'default' => $destUrl,
457 ],
458 'DestFile' => [
459 'type' => 'hidden',
460 'default' => $destName,
461 ],
462 ], $this->getContext(), 'uploadProgress' );
463 $form->setSubmitText( $this->msg( 'upload-refresh' )->text() );
464 // TODO: use codex, add a progress bar
465 //$preHtml = "<cdx-progress-bar aria--label='upload progressbar' $progressBarProperty />";
466 $preHtml = "<div id='upload-progress-message'>$message</div>";
467 $form->addPreHtml( $preHtml );
468 $form->setSubmitCallback( static fn ( $formData ) => true );
469 // Needed if we have warnings to show
470 $form->addHiddenFields( array_diff_key(
471 $this->mRequest->getValues(),
472 [
473 'title' => null,
474 'wpEditToken' => null,
475 'wpCacheKey' => null,
476 'wpSourceType' => null,
477 'wpDestUrl' => null,
478 'wpDestFile' => null,
479 'wpUpload' => null,
480 'wpUploadIgnoreWarning' => null,
481 ]
482 ) );
483 $form->prepareForm();
484 $this->getOutput()->addHTML( $form->getHTML( false ) );
485 }
486
492 protected function showUploadForm( $form ) {
493 if ( $form instanceof HTMLForm ) {
494 $form->show();
495 } else {
496 $this->getOutput()->addHTML( $form );
497 }
498 }
499
508 protected function getUploadForm( $message = '', $sessionKey = '', $hideIgnoreWarning = false ) {
509 # Initialize form
510 $form = new UploadForm(
511 [
512 'watch' => $this->getWatchCheck(),
513 'forreupload' => $this->mForReUpload,
514 'sessionkey' => $sessionKey,
515 'hideignorewarning' => $hideIgnoreWarning,
516 'destwarningack' => (bool)$this->mDestWarningAck,
517
518 'description' => $this->mComment,
519 'texttop' => $this->uploadFormTextTop,
520 'textaftersummary' => $this->uploadFormTextAfterSummary,
521 'destfile' => $this->mDesiredDestName,
522 ],
523 $this->getContext(),
524 $this->getLinkRenderer(),
525 $this->localRepo,
526 $this->getContentLanguage(),
527 $this->nsInfo,
528 $this->getHookContainer()
529 );
530 $form->setTitle( $this->getPageTitle() ); // Remove subpage
531
532 # Check the token, but only if necessary
533 if (
534 !$this->mTokenOk && !$this->mCancelUpload &&
535 ( $this->mUpload && $this->mUploadClicked )
536 ) {
537 $form->addPreHtml( $this->msg( 'session_fail_preview' )->parse() );
538 }
539
540 # Give a notice if the user is uploading a file that has been deleted or moved
541 # Note that this is independent from the message 'filewasdeleted'
542 $desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName );
543 $delNotice = ''; // empty by default
544 if ( $desiredTitleObj instanceof Title && !$desiredTitleObj->exists() ) {
545 LogEventsList::showLogExtract( $delNotice, [ 'delete', 'move' ],
546 $desiredTitleObj,
547 '', [ 'lim' => 10,
548 'conds' => [ $this->localRepo->getReplicaDB()->expr( 'log_action', '!=', 'revision' ) ],
549 'showIfEmpty' => false,
550 'msgKey' => [ 'upload-recreate-warning' ] ]
551 );
552 }
553 $form->addPreHtml( $delNotice );
554
555 # Add text to form
556 $form->addPreHtml( '<div id="uploadtext">' .
557 $this->msg( 'uploadtext', [ $this->mDesiredDestName ] )->parseAsBlock() .
558 '</div>' );
559 # Add upload error message
560 $form->addPreHtml( $message );
561
562 # Add footer to form
563 $uploadFooter = $this->msg( 'uploadfooter' );
564 if ( !$uploadFooter->isDisabled() ) {
565 $form->addPostHtml( '<div id="mw-upload-footer-message">'
566 . $uploadFooter->parseAsBlock() . "</div>\n" );
567 }
568
569 return $form;
570 }
571
583 protected function showRecoverableUploadError( $message ) {
584 $stashFile = $this->mUpload->getStashFile();
585 if ( !$stashFile ) {
586 $stashStatus = $this->mUpload->tryStashFile( $this->getUser() );
587 if ( $stashStatus->isGood() ) {
588 $stashFile = $stashStatus->getValue();
589 }
590 }
591 if ( $stashFile ) {
592 $sessionKey = $stashFile->getFileKey();
593 $uploadWarning = 'upload-tryagain';
594 } else {
595 $sessionKey = null;
596 $uploadWarning = 'upload-tryagain-nostash';
597 }
598 $message = '<h2>' . $this->msg( 'uploaderror' )->escaped() . '</h2>' .
599 Html::errorBox( $message );
600
601 $this->addMessageBoxStyling();
602 $form = $this->getUploadForm( $message, $sessionKey );
603 $form->setSubmitText( $this->msg( $uploadWarning )->text() );
604 $this->showUploadForm( $form );
605 }
606
615 protected function showUploadWarning( $warnings ) {
616 # If there are no warnings, or warnings we can ignore, return early.
617 # mDestWarningAck is set when some javascript has shown the warning
618 # to the user. mForReUpload is set when the user clicks the "upload a
619 # new version" link.
620 if ( !$warnings || ( count( $warnings ) == 1
621 && isset( $warnings['exists'] )
622 && ( $this->mDestWarningAck || $this->mForReUpload ) )
623 ) {
624 return false;
625 }
626
627 if ( $this->mUpload ) {
628 $stashFile = $this->mUpload->getStashFile();
629 if ( !$stashFile ) {
630 $stashStatus = $this->mUpload->tryStashFile( $this->getUser() );
631 if ( $stashStatus->isGood() ) {
632 $stashFile = $stashStatus->getValue();
633 }
634 }
635 if ( $stashFile ) {
636 $sessionKey = $stashFile->getFileKey();
637 $uploadWarning = 'uploadwarning-text';
638 } else {
639 $sessionKey = null;
640 $uploadWarning = 'uploadwarning-text-nostash';
641 }
642 } else {
643 $sessionKey = null;
644 $uploadWarning = 'uploadwarning-text-nostash';
645 }
646
647 // Add styles for the warning, reused from the live preview
648 $this->getOutput()->addModuleStyles( 'mediawiki.special' );
649
650 $linkRenderer = $this->getLinkRenderer();
651 $warningHtml = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n"
652 . '<div class="mw-destfile-warning"><ul>';
653 foreach ( $warnings as $warning => $args ) {
654 if ( $warning == 'badfilename' ) {
655 $this->mDesiredDestName = Title::makeTitle( NS_FILE, $args )->getText();
656 }
657 if ( $warning == 'exists' ) {
658 $msg = "\t<li>" . self::getExistsWarning( $args ) . "</li>\n";
659 } elseif ( $warning == 'no-change' ) {
660 $file = $args;
661 $filename = $file->getTitle()->getPrefixedText();
662 $msg = "\t<li>" . $this->msg( 'fileexists-no-change', $filename )->parse() . "</li>\n";
663 } elseif ( $warning == 'duplicate-version' ) {
664 $file = $args[0];
665 $count = count( $args );
666 $filename = $file->getTitle()->getPrefixedText();
667 $message = $this->msg( 'fileexists-duplicate-version' )
668 ->params( $filename )
669 ->numParams( $count );
670 $msg = "\t<li>" . $message->parse() . "</li>\n";
671 } elseif ( $warning == 'was-deleted' ) {
672 # If the file existed before and was deleted, warn the user of this
673 $ltitle = SpecialPage::getTitleFor( 'Log' );
674 $llink = $linkRenderer->makeKnownLink(
675 $ltitle,
676 $this->msg( 'deletionlog' )->text(),
677 [],
678 [
679 'type' => 'delete',
680 'page' => Title::makeTitle( NS_FILE, $args )->getPrefixedText(),
681 ]
682 );
683 $msg = "\t<li>" . $this->msg( 'filewasdeleted' )->rawParams( $llink )->parse() . "</li>\n";
684 } elseif ( $warning == 'duplicate' ) {
685 $msg = $this->getDupeWarning( $args );
686 } elseif ( $warning == 'duplicate-archive' ) {
687 if ( $args === '' ) {
688 $msg = "\t<li>" . $this->msg( 'file-deleted-duplicate-notitle' )->parse()
689 . "</li>\n";
690 } else {
691 $msg = "\t<li>" . $this->msg( 'file-deleted-duplicate',
692 Title::makeTitle( NS_FILE, $args )->getPrefixedText() )->parse()
693 . "</li>\n";
694 }
695 } else {
696 if ( $args === true ) {
697 $args = [];
698 } elseif ( !is_array( $args ) ) {
699 $args = [ $args ];
700 }
701 $msg = "\t<li>" . $this->msg( $warning, $args )->parse() . "</li>\n";
702 }
703 $warningHtml .= $msg;
704 }
705 $warningHtml .= "</ul></div>\n";
706 $warningHtml .= $this->msg( $uploadWarning )->parseAsBlock();
707
708 $form = $this->getUploadForm( $warningHtml, $sessionKey, /* $hideIgnoreWarning */ true );
709 $form->setSubmitTextMsg( 'upload-tryagain' );
710 $form->addButton( [
711 'name' => 'wpUploadIgnoreWarning',
712 'value' => $this->msg( 'ignorewarning' )->text()
713 ] );
714 $form->addButton( [
715 'name' => 'wpCancelUpload',
716 'value' => $this->msg( 'reuploaddesc' )->text()
717 ] );
718
719 $this->showUploadForm( $form );
720
721 # Indicate that we showed a form
722 return true;
723 }
724
730 protected function showUploadError( $message ) {
731 $message = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . '</h2>' .
732 Html::errorBox( $message );
733 $this->showUploadForm( $this->getUploadForm( $message ) );
734 $this->addMessageBoxStyling();
735 }
736
743 protected function performUploadChecks( $fetchFileStatus ): bool {
744 if ( !$fetchFileStatus->isOK() ) {
745 $this->showUploadError( $this->getOutput()->parseAsInterface(
746 $fetchFileStatus->getWikiText( false, false, $this->getLanguage() )
747 ) );
748
749 return false;
750 }
751 if ( !$this->getHookRunner()->onUploadForm_BeforeProcessing( $this ) ) {
752 wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file." );
753 // This code path is deprecated. If you want to break upload processing
754 // do so by hooking into the appropriate hooks in UploadBase::verifyUpload
755 // and UploadBase::verifyFile.
756 // If you use this hook to break uploading, the user will be returned
757 // an empty form with no error message whatsoever.
758 return false;
759 }
760
761 // Upload verification
762 // If this is an asynchronous upload-by-url, skip the verification
763 if ( $this->isAsyncUpload() ) {
764 return true;
765 }
766 $details = $this->mUpload->verifyUpload();
767 if ( $details['status'] != UploadBase::OK ) {
768 $this->processVerificationError( $details );
769
770 return false;
771 }
772
773 // Verify permissions for this title
774 $user = $this->getUser();
775 $status = $this->mUpload->authorizeUpload( $user );
776 if ( !$status->isGood() ) {
778 $this->getOutput()->parseAsInterface(
779 Status::wrap( $status )->getWikiText( false, false, $this->getLanguage() )
780 )
781 );
782
783 return false;
784 }
785
786 $this->mLocalFile = $this->mUpload->getLocalFile();
787
788 // Check warnings if necessary
789 if ( !$this->mIgnoreWarning ) {
790 $warnings = $this->mUpload->checkWarnings( $user );
791 if ( $this->showUploadWarning( $warnings ) ) {
792 return false;
793 }
794 }
795
796 return true;
797 }
798
804 protected function getPageTextAndTags() {
805 // Get the page text if this is not a reupload
806 if ( !$this->mForReUpload ) {
807 $pageText = self::getInitialPageText( $this->mComment, $this->mLicense,
808 $this->mCopyrightStatus, $this->mCopyrightSource,
809 $this->getConfig() );
810 } else {
811 $pageText = false;
812 }
813 $changeTags = $this->getRequest()->getVal( 'wpChangeTags' );
814 if ( $changeTags === null || $changeTags === '' ) {
815 $changeTags = [];
816 } else {
817 $changeTags = array_filter( array_map( 'trim', explode( ',', $changeTags ) ) );
818 }
819 if ( $changeTags ) {
820 $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
821 $changeTags, $this->getUser() );
822 if ( !$changeTagsStatus->isOK() ) {
823 $this->showUploadError( $this->getOutput()->parseAsInterface(
824 $changeTagsStatus->getWikiText( false, false, $this->getLanguage() )
825 ) );
826
827 return null;
828 }
829 }
830 return [ $pageText, $changeTags ];
831 }
832
837 protected function processUpload() {
838 // Fetch the file if required
839 $status = $this->mUpload->fetchFile();
840 if ( !$this->performUploadChecks( $status ) ) {
841 return;
842 }
843 $user = $this->getUser();
844 $pageAndTags = $this->getPageTextAndTags();
845 if ( $pageAndTags === null ) {
846 return;
847 }
848 [ $pageText, $changeTags ] = $pageAndTags;
849
850 $status = $this->mUpload->performUpload(
851 $this->mComment,
852 $pageText,
853 $this->mWatchthis,
854 $user,
855 $changeTags
856 );
857
858 if ( !$status->isGood() ) {
859 $this->showRecoverableUploadError(
860 $this->getOutput()->parseAsInterface(
861 $status->getWikiText( false, false, $this->getLanguage() )
862 )
863 );
864
865 return;
866 }
867
868 // Success, redirect to description page
869 $this->mUploadSuccessful = true;
870 $this->getHookRunner()->onSpecialUploadComplete( $this );
871 $this->getOutput()->redirect( $this->mLocalFile->getTitle()->getFullURL() );
872 }
873
877 protected function processAsyncUpload() {
878 // Ensure the upload we're dealing with is an UploadFromUrl
879 if ( !$this->mUpload instanceof \MediaWiki\Upload\UploadFromUrl ) {
880 $this->showUploadError( $this->msg( 'uploaderror' )->escaped() );
881
882 return;
883 }
884 // check we can fetch the file
885 $status = $this->mUpload->canFetchFile();
886 if ( !$this->performUploadChecks( $status ) ) {
887 $this->log->debug( 'Upload failed verification: {error}', [ 'error' => $status ] );
888 return;
889 }
890
891 $pageAndTags = $this->getPageTextAndTags();
892 if ( $pageAndTags === null ) {
893 return;
894 }
895 [ $pageText, $changeTags ] = $pageAndTags;
896
897 // Create a new job to process the upload from url
899 [
900 'filename' => $this->mUpload->getDesiredDestName(),
901 'url' => $this->mUpload->getUrl(),
902 'comment' => $this->mComment,
903 'tags' => $changeTags,
904 'text' => $pageText,
905 'watch' => $this->mWatchthis,
906 'watchlistexpiry' => null,
907 'session' => $this->getContext()->exportSession(),
908 'reupload' => $this->mForReUpload,
909 'ignorewarnings' => $this->mIgnoreWarning,
910 ]
911 );
912 // Save the session status
913 $cacheKey = $job->getCacheKey();
914 UploadBase::setSessionStatus( $this->getUser(), $cacheKey, [
915 'status' => Status::newGood(),
916 'stage' => 'queued',
917 'result' => 'Poll'
918 ] );
919 $this->log->info( "Submitting UploadFromUrlJob for {filename}",
920 [ 'filename' => $this->mUpload->getDesiredDestName() ]
921 );
922 // Submit the job
923 $this->jobQueueGroup->push( $job );
924 // Show the upload status
925 $this->showUploadStatus( $this->getUser() );
926 }
927
937 public static function getInitialPageText( $comment = '', $license = '',
938 $copyStatus = '', $source = '', ?Config $config = null
939 ) {
940 if ( $config === null ) {
941 wfDebug( __METHOD__ . ' called without a Config instance passed to it' );
942 $config = MediaWikiServices::getInstance()->getMainConfig();
943 }
944
945 $msg = [];
946 $forceUIMsgAsContentMsg = (array)$config->get( MainConfigNames::ForceUIMsgAsContentMsg );
947 /* These messages are transcluded into the actual text of the description page.
948 * Thus, forcing them as content messages makes the upload to produce an int: template
949 * instead of hardcoding it there in the uploader language.
950 */
951 foreach ( [ 'license-header', 'filedesc', 'filestatus', 'filesource' ] as $msgName ) {
952 if ( in_array( $msgName, $forceUIMsgAsContentMsg ) ) {
953 $msg[$msgName] = "{{int:$msgName}}";
954 } else {
955 $msg[$msgName] = wfMessage( $msgName )->inContentLanguage()->text();
956 }
957 }
958
959 $licenseText = '';
960 if ( $license !== '' ) {
961 $licenseText = '== ' . $msg['license-header'] . " ==\n{{" . $license . "}}\n";
962 }
963
964 $pageText = $comment . "\n";
965 $headerText = '== ' . $msg['filedesc'] . ' ==';
966 if ( $comment !== '' && !str_contains( $comment, $headerText ) ) {
967 // prepend header to page text unless it's already there (or there is no content)
968 $pageText = $headerText . "\n" . $pageText;
969 }
970
971 if ( $config->get( MainConfigNames::UseCopyrightUpload ) ) {
972 $pageText .= '== ' . $msg['filestatus'] . " ==\n" . $copyStatus . "\n";
973 $pageText .= $licenseText;
974 $pageText .= '== ' . $msg['filesource'] . " ==\n" . $source;
975 } else {
976 $pageText .= $licenseText;
977 }
978
979 // allow extensions to modify the content
980 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
981 ->onUploadForm_getInitialPageText( $pageText, $msg, $config );
982
983 return $pageText;
984 }
985
998 protected function getWatchCheck() {
999 $user = $this->getUser();
1000 if ( $this->userOptionsLookup->getBoolOption( $user, 'watchdefault' ) ) {
1001 // Watch all edits!
1002 return true;
1003 }
1004
1005 $desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName );
1006 if ( $desiredTitleObj instanceof Title &&
1007 $this->watchlistManager->isWatched( $user, $desiredTitleObj ) ) {
1008 // Already watched, don't change that
1009 return true;
1010 }
1011
1012 $local = $this->localRepo->newFile( $this->mDesiredDestName );
1013 if ( $local && $local->exists() ) {
1014 // We're uploading a new version of an existing file.
1015 // No creation, so don't watch it if we're not already.
1016 return false;
1017 } else {
1018 // New page should get watched if that's our option.
1019 return $this->userOptionsLookup->getBoolOption( $user, 'watchcreations' ) ||
1020 $this->userOptionsLookup->getBoolOption( $user, 'watchuploads' );
1021 }
1022 }
1023
1029 protected function processVerificationError( $details ) {
1030 switch ( $details['status'] ) {
1032 case UploadBase::MIN_LENGTH_PARTNAME:
1033 $this->showRecoverableUploadError( $this->msg( 'minlength1' )->escaped() );
1034 break;
1035 case UploadBase::ILLEGAL_FILENAME:
1036 $this->showRecoverableUploadError( $this->msg( 'illegalfilename',
1037 $details['filtered'] )->parse() );
1038 break;
1039 case UploadBase::FILENAME_TOO_LONG:
1040 $this->showRecoverableUploadError( $this->msg( 'filename-toolong' )->escaped() );
1041 break;
1042 case UploadBase::FILETYPE_MISSING:
1043 $this->showRecoverableUploadError( $this->msg( 'filetype-missing' )->parse() );
1044 break;
1045 case UploadBase::WINDOWS_NONASCII_FILENAME:
1046 $this->showRecoverableUploadError( $this->msg( 'windows-nonascii-filename' )->parse() );
1047 break;
1048
1050 case UploadBase::EMPTY_FILE:
1051 $this->showUploadError( $this->msg( 'emptyfile' )->escaped() );
1052 break;
1053 case UploadBase::FILE_TOO_LARGE:
1054 $this->showUploadError( $this->msg( 'largefileserver' )->escaped() );
1055 break;
1056 case UploadBase::FILETYPE_BADTYPE:
1057 $msg = $this->msg( 'filetype-banned-type' );
1058 if ( isset( $details['blacklistedExt'] ) ) {
1059 $msg->params( $this->getLanguage()->commaList( $details['blacklistedExt'] ) );
1060 } else {
1061 $msg->params( $details['finalExt'] );
1062 }
1063 $extensions =
1064 array_unique( $this->getConfig()->get( MainConfigNames::FileExtensions ) );
1065 $msg->params( $this->getLanguage()->commaList( $extensions ),
1066 count( $extensions ) );
1067
1068 // Add PLURAL support for the first parameter. This results
1069 // in a bit unlogical parameter sequence, but does not break
1070 // old translations
1071 if ( isset( $details['blacklistedExt'] ) ) {
1072 $msg->params( count( $details['blacklistedExt'] ) );
1073 } else {
1074 $msg->params( 1 );
1075 }
1076
1077 $this->showUploadError( $msg->parse() );
1078 break;
1079 case UploadBase::VERIFICATION_ERROR:
1080 unset( $details['status'] );
1081 $code = array_shift( $details['details'] );
1082 $this->showUploadError( $this->msg( $code, $details['details'] )->parse() );
1083 break;
1084 default:
1085 throw new UnexpectedValueException( __METHOD__ . ": Unknown value `{$details['status']}`" );
1086 }
1087 }
1088
1094 protected function unsaveUploadedFile() {
1095 if ( !( $this->mUpload instanceof UploadFromStash ) ) {
1096 return true;
1097 }
1098 $success = $this->mUpload->unsaveUploadedFile();
1099 if ( !$success ) {
1100 $this->getOutput()->showErrorPage(
1101 'internalerror',
1102 'filedeleteerror',
1103 [ $this->mUpload->getTempPath() ]
1104 );
1105
1106 return false;
1107 } else {
1108 return true;
1109 }
1110 }
1111
1121 public static function getExistsWarning( $exists ) {
1122 if ( !$exists ) {
1123 return '';
1124 }
1125
1126 $file = $exists['file'];
1127 if ( !$file instanceof File ) {
1128 // File deleted while showing entry from cache for async-url-upload
1129 // Or serialize error, see T409830
1130 return '';
1131 }
1132
1133 $filename = $file->getTitle()->getPrefixedText();
1134 $warnMsg = null;
1135
1136 if ( $exists['warning'] == 'exists' ) {
1137 // Exact match
1138 $warnMsg = wfMessage( 'fileexists', $filename );
1139 } elseif ( $exists['warning'] == 'page-exists' ) {
1140 // Page exists but file does not
1141 $warnMsg = wfMessage( 'filepageexists', $filename );
1142 } elseif ( $exists['warning'] == 'exists-normalized' ) {
1143 $warnMsg = wfMessage( 'fileexists-extension', $filename,
1144 $exists['normalizedFile']->getTitle()->getPrefixedText() );
1145 } elseif ( $exists['warning'] == 'thumb' ) {
1146 // Swapped argument order compared with other messages for backwards compatibility
1147 $warnMsg = wfMessage( 'fileexists-thumbnail-yes',
1148 $exists['thumbFile']->getTitle()->getPrefixedText(), $filename );
1149 } elseif ( $exists['warning'] == 'thumb-name' ) {
1150 // Image w/o '180px-' does not exists, but we do not like these filenames
1151 $name = $file->getName();
1152 $badPart = substr( $name, 0, strpos( $name, '-' ) + 1 );
1153 $warnMsg = wfMessage( 'file-thumbnail-no', $badPart );
1154 } elseif ( $exists['warning'] == 'bad-prefix' ) {
1155 $warnMsg = wfMessage( 'filename-bad-prefix', $exists['prefix'] );
1156 }
1157
1158 return $warnMsg ? $warnMsg->page( $file->getTitle() )->parse() : '';
1159 }
1160
1166 public function getDupeWarning( $dupes ) {
1167 if ( !$dupes ) {
1168 return '';
1169 }
1170
1171 $gallery = ImageGalleryBase::factory( false, $this->getContext() );
1172 $gallery->setShowBytes( false );
1173 $gallery->setShowDimensions( false );
1174 foreach ( $dupes as $file ) {
1175 $gallery->add( $file->getTitle() );
1176 }
1177
1178 return '<li>' .
1179 $this->msg( 'file-exists-duplicate' )->numParams( count( $dupes ) )->parse() .
1180 $gallery->toHTML() . "</li>\n";
1181 }
1182
1184 protected function getGroupName() {
1185 return 'media';
1186 }
1187
1196 public static function rotationEnabled() {
1197 $bitmapHandler = new BitmapHandler();
1198 return $bitmapHandler->autoRotateEnabled();
1199 }
1200}
1201
1206class_alias( SpecialUpload::class, 'SpecialUpload' );
const NS_FILE
Definition Defines.php:57
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:69
Recent changes tagging.
makeTitle( $linkId)
Convert a link ID to a Title.to override Title
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.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:79
Local file in the wiki's own database.
Definition LocalFile.php:81
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition LocalRepo.php:45
Prioritized list of file repositories.
Definition RepoGroup.php:30
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:195
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:43
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.
Generic handler for bitmap images.
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.
Sub class of HTMLForm that provides the form section of SpecialUpload.
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.
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:44
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:69
exists( $flags=0)
Check if page exists.
Definition Title.php:3129
getMessageObject()
Return a Message object for this exception.Message
UploadBase and subclasses are the backend of MediaWiki's file uploads.
Implements uploading from previously stored file.
Provides access to user options.
User class for the MediaWiki software.
Definition User.php:130
Interface for configuration instances.
Definition Config.php:18
$source
Helper trait for implementations \DAO.
if(count( $args)< 1) $job