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
113 public $mRequest;
115
118
120 public $mUpload;
121
125
130 public $mComment;
131 public $mLicense;
132
139
143
146
149 public $mTokenOk;
150
152 public $mUploadSuccessful = false;
153
158
162 protected function loadRequest() {
163 $this->mRequest = $request = $this->getRequest();
164 $this->mSourceType = $request->getVal( 'wpSourceType', 'file' );
165 $this->mUpload = UploadBase::createFromRequest( $request );
166 $this->mUploadClicked = $request->wasPosted()
167 && ( $request->getCheck( 'wpUpload' )
168 || $request->getCheck( 'wpUploadIgnoreWarning' ) );
169
170 // Guess the desired name from the filename if not provided
171 $this->mDesiredDestName = $request->getText( 'wpDestFile' );
172 if ( !$this->mDesiredDestName && $request->getFileName( 'wpUploadFile' ) !== null ) {
173 $this->mDesiredDestName = $request->getFileName( 'wpUploadFile' );
174 }
175 $this->mLicense = $request->getText( 'wpLicense' );
176
177 $this->mDestWarningAck = $request->getText( 'wpDestFileWarningAck' );
178 $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' )
179 || $request->getCheck( 'wpUploadIgnoreWarning' );
180 $this->mWatchthis = $request->getBool( 'wpWatchthis' ) && $this->getUser()->isRegistered();
181 $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' );
182 $this->mCopyrightSource = $request->getText( 'wpUploadSource' );
183
184 $this->mForReUpload = $request->getBool( 'wpForReUpload' ); // updating a file
185
186 $commentDefault = '';
187 $commentMsg = $this->msg( 'upload-default-description' )->inContentLanguage();
188 if ( !$this->mForReUpload && !$commentMsg->isDisabled() ) {
189 $commentDefault = $commentMsg->plain();
190 }
191 $this->mComment = $request->getText( 'wpUploadDescription', $commentDefault );
192
193 $this->mCancelUpload = $request->getCheck( 'wpCancelUpload' )
194 || $request->getCheck( 'wpReUpload' ); // b/w compat
195
196 // If it was posted check for the token (no remote POST'ing with user credentials)
197 $token = $request->getVal( 'wpEditToken' );
198 $this->mTokenOk = $this->getUser()->matchEditToken( $token );
199
200 // If this is an upload from Url and we're allowing async processing,
201 // check for the presence of the cache key parameter, or compute it. Else, it should be empty.
202 if ( $this->isAsyncUpload() ) {
203 $this->mCacheKey = \UploadFromUrl::getCacheKeyFromRequest( $request );
204 } else {
205 $this->mCacheKey = '';
206 }
207
208 $this->uploadFormTextTop = '';
209 $this->uploadFormTextAfterSummary = '';
210 }
211
217 protected function isAsyncUpload() {
218 return ( $this->mSourceType === 'url' && $this->allowAsync );
219 }
220
229 public function userCanExecute( User $user ) {
230 return UploadBase::isEnabled() && parent::userCanExecute( $user );
231 }
232
236 public function execute( $par ) {
238
239 $this->setHeaders();
240 $this->outputHeader();
241
242 # Check uploading enabled
243 if ( !UploadBase::isEnabled() ) {
244 throw new ErrorPageError( 'uploaddisabled', 'uploaddisabledtext' );
245 }
246
247 $this->addHelpLink( 'Help:Managing files' );
248
249 # Check permissions
250 $user = $this->getUser();
251 $permissionRequired = UploadBase::isAllowed( $user );
252 if ( $permissionRequired !== true ) {
253 throw new PermissionsError( $permissionRequired );
254 }
255
256 # Check blocks
257 if ( $user->isBlockedFromUpload() ) {
258 throw new UserBlockedError(
259 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
260 $user->getBlock(),
261 $user,
262 $this->getLanguage(),
263 $this->getRequest()->getIP()
264 );
265 }
266
267 # Check whether we actually want to allow changing stuff
268 $this->checkReadOnly();
269
270 $this->loadRequest();
271
272 # Unsave the temporary file in case this was a cancelled upload
273 if ( $this->mCancelUpload && !$this->unsaveUploadedFile() ) {
274 # Something went wrong, so unsaveUploadedFile showed a warning
275 return;
276 }
277
278 # If we have a cache key, show the upload status.
279 if ( $this->mTokenOk && $this->mCacheKey !== '' ) {
280 if ( $this->mUpload && $this->mUploadClicked && !$this->mCancelUpload ) {
281 # If the user clicked the upload button, we need to process the upload
282 $this->processAsyncUpload();
283 } else {
284 # Show the upload status
285 $this->showUploadStatus( $user );
286 }
287 } elseif (
288 # Process upload or show a form
289 $this->mTokenOk && !$this->mCancelUpload &&
290 ( $this->mUpload && $this->mUploadClicked )
291 ) {
292 $this->processUpload();
293 } else {
294 # Backwards compatibility hook
295 if ( !$this->getHookRunner()->onUploadForm_initial( $this ) ) {
296 wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" );
297
298 return;
299 }
300 $this->showUploadForm( $this->getUploadForm() );
301 }
302
303 # Cleanup
304 if ( $this->mUpload ) {
305 $this->mUpload->cleanupTempFile();
306 }
307 }
308
314 protected function showUploadStatus( $user ) {
315 // first, let's fetch the status from the main stash
316 $progress = UploadBase::getSessionStatus( $user, $this->mCacheKey );
317 if ( $progress == false ) {
318 $progress = [ 'status' => Status::newFatal( 'invalid-cache-key' ) ];
319 }
320 $this->log->debug( 'Upload status: stage {stage}, result {result}', $progress );
321
322 $status = $progress['status'] ?? Status::newFatal( 'invalid-cache-key' );
323 $stage = $progress['stage'] ?? 'unknown';
324 $result = $progress['result'] ?? 'unknown';
325 switch ( $stage ) {
326 case 'publish':
327 switch ( $result ) {
328 case 'Success':
329 // The upload is done. Check the result and either show the form with the error
330 // occurred, or redirect to the file itself
331 // Success, redirect to description page
332 $this->mUploadSuccessful = true;
333 $this->getHookRunner()->onSpecialUploadComplete( $this );
334 // Redirect to the destination URL, but purge the cache of the file description page first
335 // TODO: understand why this is needed
336 $title = Title::makeTitleSafe( NS_FILE, $this->mRequest->getText( 'wpDestFile' ) );
337 if ( $title ) {
338 $this->log->debug( 'Purging page', [ 'title' => $title->getText() ] );
339 $page = new WikiFilePage( $title );
340 $page->doPurge();
341 }
342 $this->getOutput()->redirect( $this->mRequest->getText( 'wpDestUrl' ) );
343 break;
344 case 'Warning':
345 $this->showUploadWarning( UploadBase::unserializeWarnings( $progress['warnings'] ) );
346 break;
347 case 'Failure':
348 $details = $status->getValue();
349 // Verification failed.
350 if ( is_array( $details ) && isset( $details['verification'] ) ) {
351 $this->processVerificationError( $details['verification'] );
352 } else {
353 $this->showUploadError( $this->getOutput()->parseAsInterface(
354 $status->getWikiText( false, false, $this->getLanguage() ) )
355 );
356 }
357 break;
358 case 'Poll':
359 $this->showUploadProgress(
360 [ 'active' => true, 'msg' => 'upload-progress-processing' ]
361 );
362 break;
363 }
364 break;
365 case 'queued':
366 // show stalled progress bar
367 $this->showUploadProgress( [ 'active' => false, 'msg' => 'upload-progress-queued' ] );
368 break;
369 case 'fetching':
370 switch ( $result ) {
371 case 'Success':
372 // The file is being downloaded from a URL
373 // TODO: show active progress bar saying we're downloading the file
374 $this->showUploadProgress( [ 'active' => true, 'msg' => 'upload-progress-downloading' ] );
375 break;
376 case 'Failure':
377 // downloading failed
378 $this->showUploadError( $this->getOutput()->parseAsInterface(
379 $status->getWikiText( false, false, $this->getLanguage() ) )
380 );
381 break;
382 }
383 break;
384 default:
385 // unknown status, just show a generic error
386 if ( $status->isOK() ) {
387 $status = Status::newFatal( 'upload-progress-unknown' );
388 }
389 $statusmsg = $this->getOutput()->parseAsInterface(
390 $status->getWikiText( false, false, $this->getLanguage() )
391 );
392 $message = '<h2>' . $this->msg( 'uploaderror' )->escaped() . '</h2>' . HTML::errorBox( $statusmsg );
393 $this->showUploadForm( $this->getUploadForm( $message ) );
394 break;
395 }
396 }
397
408 private function showUploadProgress( $options ) {
409 // $isActive = $options['active'] ?? false;
410 //$progressBarProperty = $isActive ? '' : 'disabled';
411 $message = $this->msg( $options['msg'] )->escaped();
412 $destUrl = $this->mRequest->getText( 'wpDestUrl', '' );
413 if ( !$destUrl && $this->mUpload ) {
414 if ( !$this->mLocalFile ) {
415 $this->mLocalFile = $this->mUpload->getLocalFile();
416 }
417 // This probably means the title is bad, so we can't get the URL
418 // but we need to wait for the job to execute.
419 if ( $this->mLocalFile === null ) {
420 $destUrl = '';
421 } else {
422 $destUrl = $this->mLocalFile->getTitle()->getFullURL();
423 }
424 }
425
426 $destName = $this->mDesiredDestName;
427 if ( !$destName ) {
428 $destName = $this->mRequest->getText( 'wpDestFile' );
429 }
430
431 // Needed if we have warnings to show
432 $sourceURL = $this->mRequest->getText( 'wpUploadFileURL' );
433
434 $form = new HTMLForm( [
435 'CacheKey' => [
436 'type' => 'hidden',
437 'default' => $this->mCacheKey,
438 ],
439 'SourceType' => [
440 'type' => 'hidden',
441 'default' => $this->mSourceType,
442 ],
443 'DestUrl' => [
444 'type' => 'hidden',
445 'default' => $destUrl,
446 ],
447 'DestFile' => [
448 'type' => 'hidden',
449 'default' => $destName,
450 ],
451 'UploadFileURL' => [
452 'type' => 'hidden',
453 'default' => $sourceURL,
454 ],
455 ], $this->getContext(), 'uploadProgress' );
456 $form->setSubmitText( $this->msg( 'upload-refresh' )->escaped() );
457 // TODO: use codex, add a progress bar
458 //$preHtml = "<cdx-progress-bar aria--label='upload progressbar' $progressBarProperty />";
459 $preHtml = "<div id='upload-progress-message'>$message</div>";
460 $form->addPreHtml( $preHtml );
461 $form->setSubmitCallback(
462 static function ( $formData ) {
463 return true;
464 }
465 );
466 $form->prepareForm();
467 $this->getOutput()->addHTML( $form->getHTML( false ) );
468 }
469
475 protected function showUploadForm( $form ) {
476 if ( $form instanceof HTMLForm ) {
477 $form->show();
478 } else {
479 $this->getOutput()->addHTML( $form );
480 }
481 }
482
491 protected function getUploadForm( $message = '', $sessionKey = '', $hideIgnoreWarning = false ) {
492 # Initialize form
493 $form = new UploadForm(
494 [
495 'watch' => $this->getWatchCheck(),
496 'forreupload' => $this->mForReUpload,
497 'sessionkey' => $sessionKey,
498 'hideignorewarning' => $hideIgnoreWarning,
499 'destwarningack' => (bool)$this->mDestWarningAck,
500
501 'description' => $this->mComment,
502 'texttop' => $this->uploadFormTextTop,
503 'textaftersummary' => $this->uploadFormTextAfterSummary,
504 'destfile' => $this->mDesiredDestName,
505 ],
506 $this->getContext(),
507 $this->getLinkRenderer(),
508 $this->localRepo,
509 $this->getContentLanguage(),
510 $this->nsInfo,
511 $this->getHookContainer()
512 );
513 $form->setTitle( $this->getPageTitle() ); // Remove subpage
514
515 # Check the token, but only if necessary
516 if (
517 !$this->mTokenOk && !$this->mCancelUpload &&
518 ( $this->mUpload && $this->mUploadClicked )
519 ) {
520 $form->addPreText( $this->msg( 'session_fail_preview' )->parse() );
521 }
522
523 # Give a notice if the user is uploading a file that has been deleted or moved
524 # Note that this is independent from the message 'filewasdeleted'
525 $desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName );
526 $delNotice = ''; // empty by default
527 if ( $desiredTitleObj instanceof Title && !$desiredTitleObj->exists() ) {
528 LogEventsList::showLogExtract( $delNotice, [ 'delete', 'move' ],
529 $desiredTitleObj,
530 '', [ 'lim' => 10,
531 'conds' => [ $this->localRepo->getReplicaDB()->expr( 'log_action', '!=', 'revision' ) ],
532 'showIfEmpty' => false,
533 'msgKey' => [ 'upload-recreate-warning' ] ]
534 );
535 }
536 $form->addPreText( $delNotice );
537
538 # Add text to form
539 $form->addPreText( '<div id="uploadtext">' .
540 $this->msg( 'uploadtext', [ $this->mDesiredDestName ] )->parseAsBlock() .
541 '</div>' );
542 # Add upload error message
543 $form->addPreText( $message );
544
545 # Add footer to form
546 $uploadFooter = $this->msg( 'uploadfooter' );
547 if ( !$uploadFooter->isDisabled() ) {
548 $form->addPostText( '<div id="mw-upload-footer-message">'
549 . $uploadFooter->parseAsBlock() . "</div>\n" );
550 }
551
552 return $form;
553 }
554
566 protected function showRecoverableUploadError( $message ) {
567 $stashStatus = $this->mUpload->tryStashFile( $this->getUser() );
568 if ( $stashStatus->isGood() ) {
569 $sessionKey = $stashStatus->getValue()->getFileKey();
570 $uploadWarning = 'upload-tryagain';
571 } else {
572 $sessionKey = null;
573 $uploadWarning = 'upload-tryagain-nostash';
574 }
575 $message = '<h2>' . $this->msg( 'uploaderror' )->escaped() . '</h2>' .
576 Html::errorBox( $message );
577
578 $form = $this->getUploadForm( $message, $sessionKey );
579 $form->setSubmitText( $this->msg( $uploadWarning )->escaped() );
580 $this->showUploadForm( $form );
581 }
582
591 protected function showUploadWarning( $warnings ) {
592 # If there are no warnings, or warnings we can ignore, return early.
593 # mDestWarningAck is set when some javascript has shown the warning
594 # to the user. mForReUpload is set when the user clicks the "upload a
595 # new version" link.
596 if ( !$warnings || ( count( $warnings ) == 1
597 && isset( $warnings['exists'] )
598 && ( $this->mDestWarningAck || $this->mForReUpload ) )
599 ) {
600 return false;
601 }
602
603 if ( $this->mUpload ) {
604 $stashStatus = $this->mUpload->tryStashFile( $this->getUser() );
605 if ( $stashStatus->isGood() ) {
606 $sessionKey = $stashStatus->getValue()->getFileKey();
607 $uploadWarning = 'uploadwarning-text';
608 } else {
609 $sessionKey = null;
610 $uploadWarning = 'uploadwarning-text-nostash';
611 }
612 } else {
613 $sessionKey = null;
614 $uploadWarning = 'uploadwarning-text-nostash';
615 }
616
617 // Add styles for the warning, reused from the live preview
618 $this->getOutput()->addModuleStyles( 'mediawiki.special' );
619
620 $linkRenderer = $this->getLinkRenderer();
621 $warningHtml = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n"
622 . '<div class="mw-destfile-warning"><ul>';
623 foreach ( $warnings as $warning => $args ) {
624 if ( $warning == 'badfilename' ) {
625 $this->mDesiredDestName = Title::makeTitle( NS_FILE, $args )->getText();
626 }
627 if ( $warning == 'exists' ) {
628 $msg = "\t<li>" . self::getExistsWarning( $args ) . "</li>\n";
629 } elseif ( $warning == 'no-change' ) {
630 $file = $args;
631 $filename = $file->getTitle()->getPrefixedText();
632 $msg = "\t<li>" . $this->msg( 'fileexists-no-change', $filename )->parse() . "</li>\n";
633 } elseif ( $warning == 'duplicate-version' ) {
634 $file = $args[0];
635 $count = count( $args );
636 $filename = $file->getTitle()->getPrefixedText();
637 $message = $this->msg( 'fileexists-duplicate-version' )
638 ->params( $filename )
639 ->numParams( $count );
640 $msg = "\t<li>" . $message->parse() . "</li>\n";
641 } elseif ( $warning == 'was-deleted' ) {
642 # If the file existed before and was deleted, warn the user of this
643 $ltitle = SpecialPage::getTitleFor( 'Log' );
644 $llink = $linkRenderer->makeKnownLink(
645 $ltitle,
646 $this->msg( 'deletionlog' )->text(),
647 [],
648 [
649 'type' => 'delete',
650 'page' => Title::makeTitle( NS_FILE, $args )->getPrefixedText(),
651 ]
652 );
653 $msg = "\t<li>" . $this->msg( 'filewasdeleted' )->rawParams( $llink )->parse() . "</li>\n";
654 } elseif ( $warning == 'duplicate' ) {
655 $msg = $this->getDupeWarning( $args );
656 } elseif ( $warning == 'duplicate-archive' ) {
657 if ( $args === '' ) {
658 $msg = "\t<li>" . $this->msg( 'file-deleted-duplicate-notitle' )->parse()
659 . "</li>\n";
660 } else {
661 $msg = "\t<li>" . $this->msg( 'file-deleted-duplicate',
662 Title::makeTitle( NS_FILE, $args )->getPrefixedText() )->parse()
663 . "</li>\n";
664 }
665 } else {
666 if ( $args === true ) {
667 $args = [];
668 } elseif ( !is_array( $args ) ) {
669 $args = [ $args ];
670 }
671 $msg = "\t<li>" . $this->msg( $warning, $args )->parse() . "</li>\n";
672 }
673 $warningHtml .= $msg;
674 }
675 $warningHtml .= "</ul></div>\n";
676 $warningHtml .= $this->msg( $uploadWarning )->parseAsBlock();
677
678 $form = $this->getUploadForm( $warningHtml, $sessionKey, /* $hideIgnoreWarning */ true );
679 $form->setSubmitTextMsg( 'upload-tryagain' );
680 $form->addButton( [
681 'name' => 'wpUploadIgnoreWarning',
682 'value' => $this->msg( 'ignorewarning' )->text()
683 ] );
684 $form->addButton( [
685 'name' => 'wpCancelUpload',
686 'value' => $this->msg( 'reuploaddesc' )->text()
687 ] );
688
689 $this->showUploadForm( $form );
690
691 # Indicate that we showed a form
692 return true;
693 }
694
700 protected function showUploadError( $message ) {
701 $message = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . '</h2>' .
702 Html::errorBox( $message );
703 $this->showUploadForm( $this->getUploadForm( $message ) );
704 }
705
712 protected function performUploadChecks( $fetchFileStatus ): bool {
713 if ( !$fetchFileStatus->isOK() ) {
714 $this->showUploadError( $this->getOutput()->parseAsInterface(
715 $fetchFileStatus->getWikiText( false, false, $this->getLanguage() )
716 ) );
717
718 return false;
719 }
720 if ( !$this->getHookRunner()->onUploadForm_BeforeProcessing( $this ) ) {
721 wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file." );
722 // This code path is deprecated. If you want to break upload processing
723 // do so by hooking into the appropriate hooks in UploadBase::verifyUpload
724 // and UploadBase::verifyFile.
725 // If you use this hook to break uploading, the user will be returned
726 // an empty form with no error message whatsoever.
727 return false;
728 }
729
730 // Upload verification
731 // If this is an asynchronous upload-by-url, skip the verification
732 if ( $this->isAsyncUpload() ) {
733 return true;
734 }
735 $details = $this->mUpload->verifyUpload();
736 if ( $details['status'] != UploadBase::OK ) {
737 $this->processVerificationError( $details );
738
739 return false;
740 }
741
742 // Verify permissions for this title
743 $user = $this->getUser();
744 $permErrors = $this->mUpload->verifyTitlePermissions( $user );
745 if ( $permErrors !== true ) {
746 $code = array_shift( $permErrors[0] );
747 $this->showRecoverableUploadError( $this->msg( $code, $permErrors[0] )->parse() );
748
749 return false;
750 }
751
752 $this->mLocalFile = $this->mUpload->getLocalFile();
753
754 // Check warnings if necessary
755 if ( !$this->mIgnoreWarning ) {
756 $warnings = $this->mUpload->checkWarnings( $user );
757 if ( $this->showUploadWarning( $warnings ) ) {
758 return false;
759 }
760 }
761
762 return true;
763 }
764
770 protected function getPageTextAndTags() {
771 // Get the page text if this is not a reupload
772 if ( !$this->mForReUpload ) {
773 $pageText = self::getInitialPageText( $this->mComment, $this->mLicense,
774 $this->mCopyrightStatus, $this->mCopyrightSource,
775 $this->getConfig() );
776 } else {
777 $pageText = false;
778 }
779 $changeTags = $this->getRequest()->getVal( 'wpChangeTags' );
780 if ( $changeTags === null || $changeTags === '' ) {
781 $changeTags = [];
782 } else {
783 $changeTags = array_filter( array_map( 'trim', explode( ',', $changeTags ) ) );
784 }
785 if ( $changeTags ) {
787 $changeTags, $this->getUser() );
788 if ( !$changeTagsStatus->isOK() ) {
789 $this->showUploadError( $this->getOutput()->parseAsInterface(
790 $changeTagsStatus->getWikiText( false, false, $this->getLanguage() )
791 ) );
792
793 return null;
794 }
795 }
796 return [ $pageText, $changeTags ];
797 }
798
803 protected function processUpload() {
804 // Fetch the file if required
805 $status = $this->mUpload->fetchFile();
806 if ( !$this->performUploadChecks( $status ) ) {
807 return;
808 }
809 $user = $this->getUser();
810 $pageAndTags = $this->getPageTextAndTags();
811 if ( $pageAndTags === null ) {
812 return;
813 }
814 [ $pageText, $changeTags ] = $pageAndTags;
815
816 $status = $this->mUpload->performUpload(
817 $this->mComment,
818 $pageText,
819 $this->mWatchthis,
820 $user,
821 $changeTags
822 );
823
824 if ( !$status->isGood() ) {
825 $this->showRecoverableUploadError(
826 $this->getOutput()->parseAsInterface(
827 $status->getWikiText( false, false, $this->getLanguage() )
828 )
829 );
830
831 return;
832 }
833
834 // Success, redirect to description page
835 $this->mUploadSuccessful = true;
836 $this->getHookRunner()->onSpecialUploadComplete( $this );
837 $this->getOutput()->redirect( $this->mLocalFile->getTitle()->getFullURL() );
838 }
839
843 protected function processAsyncUpload() {
844 // Ensure the upload we're dealing with is an UploadFromUrl
845 if ( !$this->mUpload instanceof \UploadFromUrl ) {
846 $this->showUploadError( $this->msg( 'uploaderror' )->escaped() );
847
848 return;
849 }
850 // check we can fetch the file
851 $status = $this->mUpload->canFetchFile();
852 if ( !$this->performUploadChecks( $status ) ) {
853 $this->log->debug( 'Upload failed verification: {error}', [ 'error' => $status ] );
854 return;
855 }
856
857 $pageAndTags = $this->getPageTextAndTags();
858 if ( $pageAndTags === null ) {
859 return;
860 }
861 [ $pageText, $changeTags ] = $pageAndTags;
862
863 // Create a new job to process the upload from url
864 $job = new \UploadFromUrlJob(
865 [
866 'filename' => $this->mUpload->getDesiredDestName(),
867 'url' => $this->mUpload->getUrl(),
868 'comment' => $this->mComment,
869 'tags' => $changeTags,
870 'text' => $pageText,
871 'watch' => $this->mWatchthis,
872 'watchlistexpiry' => null,
873 'session' => $this->getContext()->exportSession(),
874 'reupload' => $this->mForReUpload,
875 'ignorewarnings' => $this->mIgnoreWarning,
876 ]
877 );
878 // Save the session status
879 $cacheKey = $job->getCacheKey();
880 UploadBase::setSessionStatus( $this->getUser(), $cacheKey, [
881 'status' => Status::newGood(),
882 'stage' => 'queued',
883 'result' => 'Poll'
884 ] );
885 $this->log->info( "Submitting UploadFromUrlJob for {filename}",
886 [ 'filename' => $this->mUpload->getDesiredDestName() ]
887 );
888 // Submit the job
889 $this->jobQueueGroup->push( $job );
890 // Show the upload status
891 $this->showUploadStatus( $this->getUser() );
892 }
893
903 public static function getInitialPageText( $comment = '', $license = '',
904 $copyStatus = '', $source = '', Config $config = null
905 ) {
906 if ( $config === null ) {
907 wfDebug( __METHOD__ . ' called without a Config instance passed to it' );
908 $config = MediaWikiServices::getInstance()->getMainConfig();
909 }
910
911 $msg = [];
912 $forceUIMsgAsContentMsg = (array)$config->get( MainConfigNames::ForceUIMsgAsContentMsg );
913 /* These messages are transcluded into the actual text of the description page.
914 * Thus, forcing them as content messages makes the upload to produce an int: template
915 * instead of hardcoding it there in the uploader language.
916 */
917 foreach ( [ 'license-header', 'filedesc', 'filestatus', 'filesource' ] as $msgName ) {
918 if ( in_array( $msgName, $forceUIMsgAsContentMsg ) ) {
919 $msg[$msgName] = "{{int:$msgName}}";
920 } else {
921 $msg[$msgName] = wfMessage( $msgName )->inContentLanguage()->text();
922 }
923 }
924
925 $licenseText = '';
926 if ( $license !== '' ) {
927 $licenseText = '== ' . $msg['license-header'] . " ==\n{{" . $license . "}}\n";
928 }
929
930 $pageText = $comment . "\n";
931 $headerText = '== ' . $msg['filedesc'] . ' ==';
932 if ( $comment !== '' && !str_contains( $comment, $headerText ) ) {
933 // prepend header to page text unless it's already there (or there is no content)
934 $pageText = $headerText . "\n" . $pageText;
935 }
936
937 if ( $config->get( MainConfigNames::UseCopyrightUpload ) ) {
938 $pageText .= '== ' . $msg['filestatus'] . " ==\n" . $copyStatus . "\n";
939 $pageText .= $licenseText;
940 $pageText .= '== ' . $msg['filesource'] . " ==\n" . $source;
941 } else {
942 $pageText .= $licenseText;
943 }
944
945 // allow extensions to modify the content
946 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
947 ->onUploadForm_getInitialPageText( $pageText, $msg, $config );
948
949 return $pageText;
950 }
951
964 protected function getWatchCheck() {
965 $user = $this->getUser();
966 if ( $this->userOptionsLookup->getBoolOption( $user, 'watchdefault' ) ) {
967 // Watch all edits!
968 return true;
969 }
970
971 $desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName );
972 if ( $desiredTitleObj instanceof Title &&
973 $this->watchlistManager->isWatched( $user, $desiredTitleObj ) ) {
974 // Already watched, don't change that
975 return true;
976 }
977
978 $local = $this->localRepo->newFile( $this->mDesiredDestName );
979 if ( $local && $local->exists() ) {
980 // We're uploading a new version of an existing file.
981 // No creation, so don't watch it if we're not already.
982 return false;
983 } else {
984 // New page should get watched if that's our option.
985 return $this->userOptionsLookup->getBoolOption( $user, 'watchcreations' ) ||
986 $this->userOptionsLookup->getBoolOption( $user, 'watchuploads' );
987 }
988 }
989
995 protected function processVerificationError( $details ) {
996 switch ( $details['status'] ) {
998 case UploadBase::MIN_LENGTH_PARTNAME:
999 $this->showRecoverableUploadError( $this->msg( 'minlength1' )->escaped() );
1000 break;
1001 case UploadBase::ILLEGAL_FILENAME:
1002 $this->showRecoverableUploadError( $this->msg( 'illegalfilename',
1003 $details['filtered'] )->parse() );
1004 break;
1005 case UploadBase::FILENAME_TOO_LONG:
1006 $this->showRecoverableUploadError( $this->msg( 'filename-toolong' )->escaped() );
1007 break;
1008 case UploadBase::FILETYPE_MISSING:
1009 $this->showRecoverableUploadError( $this->msg( 'filetype-missing' )->parse() );
1010 break;
1011 case UploadBase::WINDOWS_NONASCII_FILENAME:
1012 $this->showRecoverableUploadError( $this->msg( 'windows-nonascii-filename' )->parse() );
1013 break;
1014
1016 case UploadBase::EMPTY_FILE:
1017 $this->showUploadError( $this->msg( 'emptyfile' )->escaped() );
1018 break;
1019 case UploadBase::FILE_TOO_LARGE:
1020 $this->showUploadError( $this->msg( 'largefileserver' )->escaped() );
1021 break;
1022 case UploadBase::FILETYPE_BADTYPE:
1023 $msg = $this->msg( 'filetype-banned-type' );
1024 if ( isset( $details['blacklistedExt'] ) ) {
1025 $msg->params( $this->getLanguage()->commaList( $details['blacklistedExt'] ) );
1026 } else {
1027 $msg->params( $details['finalExt'] );
1028 }
1029 $extensions =
1030 array_unique( $this->getConfig()->get( MainConfigNames::FileExtensions ) );
1031 $msg->params( $this->getLanguage()->commaList( $extensions ),
1032 count( $extensions ) );
1033
1034 // Add PLURAL support for the first parameter. This results
1035 // in a bit unlogical parameter sequence, but does not break
1036 // old translations
1037 if ( isset( $details['blacklistedExt'] ) ) {
1038 $msg->params( count( $details['blacklistedExt'] ) );
1039 } else {
1040 $msg->params( 1 );
1041 }
1042
1043 $this->showUploadError( $msg->parse() );
1044 break;
1045 case UploadBase::VERIFICATION_ERROR:
1046 unset( $details['status'] );
1047 $code = array_shift( $details['details'] );
1048 $this->showUploadError( $this->msg( $code, $details['details'] )->parse() );
1049 break;
1050 case UploadBase::HOOK_ABORTED:
1051 if ( is_array( $details['error'] ) ) { # allow hooks to return error details in an array
1052 $args = $details['error'];
1053 $error = array_shift( $args );
1054 } else {
1055 $error = $details['error'];
1056 $args = null;
1057 }
1058
1059 $this->showUploadError( $this->msg( $error, $args )->parse() );
1060 break;
1061 default:
1062 throw new UnexpectedValueException( __METHOD__ . ": Unknown value `{$details['status']}`" );
1063 }
1064 }
1065
1071 protected function unsaveUploadedFile() {
1072 if ( !( $this->mUpload instanceof UploadFromStash ) ) {
1073 return true;
1074 }
1075 $success = $this->mUpload->unsaveUploadedFile();
1076 if ( !$success ) {
1077 $this->getOutput()->showFatalError(
1078 $this->msg( 'filedeleteerror' )
1079 ->params( $this->mUpload->getTempPath() )
1080 ->escaped()
1081 );
1082
1083 return false;
1084 } else {
1085 return true;
1086 }
1087 }
1088
1098 public static function getExistsWarning( $exists ) {
1099 if ( !$exists ) {
1100 return '';
1101 }
1102
1103 $file = $exists['file'];
1104 $filename = $file->getTitle()->getPrefixedText();
1105 $warnMsg = null;
1106
1107 if ( $exists['warning'] == 'exists' ) {
1108 // Exact match
1109 $warnMsg = wfMessage( 'fileexists', $filename );
1110 } elseif ( $exists['warning'] == 'page-exists' ) {
1111 // Page exists but file does not
1112 $warnMsg = wfMessage( 'filepageexists', $filename );
1113 } elseif ( $exists['warning'] == 'exists-normalized' ) {
1114 $warnMsg = wfMessage( 'fileexists-extension', $filename,
1115 $exists['normalizedFile']->getTitle()->getPrefixedText() );
1116 } elseif ( $exists['warning'] == 'thumb' ) {
1117 // Swapped argument order compared with other messages for backwards compatibility
1118 $warnMsg = wfMessage( 'fileexists-thumbnail-yes',
1119 $exists['thumbFile']->getTitle()->getPrefixedText(), $filename );
1120 } elseif ( $exists['warning'] == 'thumb-name' ) {
1121 // Image w/o '180px-' does not exists, but we do not like these filenames
1122 $name = $file->getName();
1123 $badPart = substr( $name, 0, strpos( $name, '-' ) + 1 );
1124 $warnMsg = wfMessage( 'file-thumbnail-no', $badPart );
1125 } elseif ( $exists['warning'] == 'bad-prefix' ) {
1126 $warnMsg = wfMessage( 'filename-bad-prefix', $exists['prefix'] );
1127 }
1128
1129 return $warnMsg ? $warnMsg->page( $file->getTitle() )->parse() : '';
1130 }
1131
1137 public function getDupeWarning( $dupes ) {
1138 if ( !$dupes ) {
1139 return '';
1140 }
1141
1142 $gallery = ImageGalleryBase::factory( false, $this->getContext() );
1143 $gallery->setShowBytes( false );
1144 $gallery->setShowDimensions( false );
1145 foreach ( $dupes as $file ) {
1146 $gallery->add( $file->getTitle() );
1147 }
1148
1149 return '<li>' .
1150 $this->msg( 'file-exists-duplicate' )->numParams( count( $dupes ) )->parse() .
1151 $gallery->toHTML() . "</li>\n";
1152 }
1153
1154 protected function getGroupName() {
1155 return 'media';
1156 }
1157
1166 public static function rotationEnabled() {
1167 $bitmapHandler = new BitmapHandler();
1168 return $bitmapHandler->autoRotateEnabled();
1169 }
1170}
1171
1176class_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 Per default the message key is the canonical name o...
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
User input variables from the "description" section.
__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.
$mIgnoreWarning
User input variables from the root section.
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
Misc variables.
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:3204
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