1<?php 2/** 3 * Implements Special:Upload 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup SpecialPage 22 * @ingroup Upload 23 */ 24 25use MediaWiki\MediaWikiServices; 26use MediaWiki\User\UserOptionsLookup; 27 28/** 29 * Form for handling uploads and special page. 30 * 31 * @ingroup SpecialPage 32 * @ingroup Upload 33 */ 34class SpecialUpload extends SpecialPage { 35 36 /** @var LocalRepo */ 37 private $localRepo; 38 39 /** @var UserOptionsLookup */ 40 private $userOptionsLookup; 41 42 /** @var NamespaceInfo */ 43 private $nsInfo; 44 45 /** 46 * @param RepoGroup|null $repoGroup 47 * @param UserOptionsLookup|null $userOptionsLookup 48 * @param NamespaceInfo|null $nsInfo 49 */ 50 public function __construct( 51 RepoGroup $repoGroup = null, 52 UserOptionsLookup $userOptionsLookup = null, 53 NamespaceInfo $nsInfo = null 54 ) { 55 parent::__construct( 'Upload', 'upload' ); 56 // This class is extended and therefor fallback to global state - T265300 57 $services = MediaWikiServices::getInstance(); 58 $repoGroup = $repoGroup ?? $services->getRepoGroup(); 59 $this->localRepo = $repoGroup->getLocalRepo(); 60 $this->userOptionsLookup = $userOptionsLookup ?? $services->getUserOptionsLookup(); 61 $this->nsInfo = $nsInfo ?? $services->getNamespaceInfo(); 62 } 63 64 public function doesWrites() { 65 return true; 66 } 67 68 /** Misc variables */ 69 70 /** @var WebRequest|FauxRequest The request this form is supposed to handle */ 71 public $mRequest; 72 public $mSourceType; 73 74 /** @var UploadBase */ 75 public $mUpload; 76 77 /** @var LocalFile */ 78 public $mLocalFile; 79 public $mUploadClicked; 80 81 /** User input variables from the "description" section */ 82 83 /** @var string The requested target file name */ 84 public $mDesiredDestName; 85 public $mComment; 86 public $mLicense; 87 88 /** User input variables from the root section */ 89 90 public $mIgnoreWarning; 91 public $mWatchthis; 92 public $mCopyrightStatus; 93 public $mCopyrightSource; 94 95 /** Hidden variables */ 96 97 public $mDestWarningAck; 98 99 /** @var bool The user followed an "overwrite this file" link */ 100 public $mForReUpload; 101 102 /** @var bool The user clicked "Cancel and return to upload form" button */ 103 public $mCancelUpload; 104 public $mTokenOk; 105 106 /** @var bool Subclasses can use this to determine whether a file was uploaded */ 107 public $mUploadSuccessful = false; 108 109 /** @var string Raw html injection point for hooks not using HTMLForm */ 110 public $uploadFormTextTop; 111 /** @var string Raw html injection point for hooks not using HTMLForm */ 112 public $uploadFormTextAfterSummary; 113 114 /** 115 * Initialize instance variables from request and create an Upload handler 116 */ 117 protected function loadRequest() { 118 $this->mRequest = $request = $this->getRequest(); 119 $this->mSourceType = $request->getVal( 'wpSourceType', 'file' ); 120 $this->mUpload = UploadBase::createFromRequest( $request ); 121 $this->mUploadClicked = $request->wasPosted() 122 && ( $request->getCheck( 'wpUpload' ) 123 || $request->getCheck( 'wpUploadIgnoreWarning' ) ); 124 125 // Guess the desired name from the filename if not provided 126 $this->mDesiredDestName = $request->getText( 'wpDestFile' ); 127 if ( !$this->mDesiredDestName && $request->getFileName( 'wpUploadFile' ) !== null ) { 128 $this->mDesiredDestName = $request->getFileName( 'wpUploadFile' ); 129 } 130 $this->mLicense = $request->getText( 'wpLicense' ); 131 132 $this->mDestWarningAck = $request->getText( 'wpDestFileWarningAck' ); 133 $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' ) 134 || $request->getCheck( 'wpUploadIgnoreWarning' ); 135 $this->mWatchthis = $request->getBool( 'wpWatchthis' ) && $this->getUser()->isRegistered(); 136 $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' ); 137 $this->mCopyrightSource = $request->getText( 'wpUploadSource' ); 138 139 $this->mForReUpload = $request->getBool( 'wpForReUpload' ); // updating a file 140 141 $commentDefault = ''; 142 $commentMsg = $this->msg( 'upload-default-description' )->inContentLanguage(); 143 if ( !$this->mForReUpload && !$commentMsg->isDisabled() ) { 144 $commentDefault = $commentMsg->plain(); 145 } 146 $this->mComment = $request->getText( 'wpUploadDescription', $commentDefault ); 147 148 $this->mCancelUpload = $request->getCheck( 'wpCancelUpload' ) 149 || $request->getCheck( 'wpReUpload' ); // b/w compat 150 151 // If it was posted check for the token (no remote POST'ing with user credentials) 152 $token = $request->getVal( 'wpEditToken' ); 153 $this->mTokenOk = $this->getUser()->matchEditToken( $token ); 154 155 $this->uploadFormTextTop = ''; 156 $this->uploadFormTextAfterSummary = ''; 157 } 158 159 /** 160 * This page can be shown if uploading is enabled. 161 * Handle permission checking elsewhere in order to be able to show 162 * custom error messages. 163 * 164 * @param User $user 165 * @return bool 166 */ 167 public function userCanExecute( User $user ) { 168 return UploadBase::isEnabled() && parent::userCanExecute( $user ); 169 } 170 171 /** 172 * @param string|null $par 173 * @throws ErrorPageError 174 * @throws Exception 175 * @throws FatalError 176 * @throws MWException 177 * @throws PermissionsError 178 * @throws ReadOnlyError 179 * @throws UserBlockedError 180 */ 181 public function execute( $par ) { 182 $this->useTransactionalTimeLimit(); 183 184 $this->setHeaders(); 185 $this->outputHeader(); 186 187 # Check uploading enabled 188 if ( !UploadBase::isEnabled() ) { 189 throw new ErrorPageError( 'uploaddisabled', 'uploaddisabledtext' ); 190 } 191 192 $this->addHelpLink( 'Help:Managing files' ); 193 194 # Check permissions 195 $user = $this->getUser(); 196 $permissionRequired = UploadBase::isAllowed( $user ); 197 if ( $permissionRequired !== true ) { 198 throw new PermissionsError( $permissionRequired ); 199 } 200 201 # Check blocks 202 if ( $user->isBlockedFromUpload() ) { 203 throw new UserBlockedError( 204 $user->getBlock(), 205 $user, 206 $this->getLanguage(), 207 $this->getRequest()->getIP() 208 ); 209 } 210 211 // Global blocks 212 if ( $user->isBlockedGlobally() ) { 213 throw new UserBlockedError( 214 $user->getGlobalBlock(), 215 $user, 216 $this->getLanguage(), 217 $this->getRequest()->getIP() 218 ); 219 } 220 221 # Check whether we actually want to allow changing stuff 222 $this->checkReadOnly(); 223 224 $this->loadRequest(); 225 226 # Unsave the temporary file in case this was a cancelled upload 227 if ( $this->mCancelUpload && !$this->unsaveUploadedFile() ) { 228 # Something went wrong, so unsaveUploadedFile showed a warning 229 return; 230 } 231 232 # Process upload or show a form 233 if ( 234 $this->mTokenOk && !$this->mCancelUpload && 235 ( $this->mUpload && $this->mUploadClicked ) 236 ) { 237 $this->processUpload(); 238 } else { 239 # Backwards compatibility hook 240 if ( !$this->getHookRunner()->onUploadForm_initial( $this ) ) { 241 wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" ); 242 243 return; 244 } 245 $this->showUploadForm( $this->getUploadForm() ); 246 } 247 248 # Cleanup 249 if ( $this->mUpload ) { 250 $this->mUpload->cleanupTempFile(); 251 } 252 } 253 254 /** 255 * Show the main upload form 256 * 257 * @param HTMLForm|string $form An HTMLForm instance or HTML string to show 258 */ 259 protected function showUploadForm( $form ) { 260 # Add links if file was previously deleted 261 if ( $this->mDesiredDestName ) { 262 $this->showViewDeletedLinks(); 263 } 264 265 if ( $form instanceof HTMLForm ) { 266 $form->show(); 267 } else { 268 $this->getOutput()->addHTML( $form ); 269 } 270 } 271 272 /** 273 * Get an UploadForm instance with title and text properly set. 274 * 275 * @param string $message HTML string to add to the form 276 * @param string $sessionKey Session key in case this is a stashed upload 277 * @param bool $hideIgnoreWarning Whether to hide "ignore warning" check box 278 * @return UploadForm 279 */ 280 protected function getUploadForm( $message = '', $sessionKey = '', $hideIgnoreWarning = false ) { 281 # Initialize form 282 $context = new DerivativeContext( $this->getContext() ); 283 $context->setTitle( $this->getPageTitle() ); // Remove subpage 284 $form = new UploadForm( 285 [ 286 'watch' => $this->getWatchCheck(), 287 'forreupload' => $this->mForReUpload, 288 'sessionkey' => $sessionKey, 289 'hideignorewarning' => $hideIgnoreWarning, 290 'destwarningack' => (bool)$this->mDestWarningAck, 291 292 'description' => $this->mComment, 293 'texttop' => $this->uploadFormTextTop, 294 'textaftersummary' => $this->uploadFormTextAfterSummary, 295 'destfile' => $this->mDesiredDestName, 296 ], 297 $context, 298 $this->getLinkRenderer(), 299 $this->localRepo, 300 $this->getContentLanguage(), 301 $this->nsInfo 302 ); 303 304 # Check the token, but only if necessary 305 if ( 306 !$this->mTokenOk && !$this->mCancelUpload && 307 ( $this->mUpload && $this->mUploadClicked ) 308 ) { 309 $form->addPreText( $this->msg( 'session_fail_preview' )->parse() ); 310 } 311 312 # Give a notice if the user is uploading a file that has been deleted or moved 313 # Note that this is independent from the message 'filewasdeleted' 314 $desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName ); 315 $delNotice = ''; // empty by default 316 if ( $desiredTitleObj instanceof Title && !$desiredTitleObj->exists() ) { 317 LogEventsList::showLogExtract( $delNotice, [ 'delete', 'move' ], 318 $desiredTitleObj, 319 '', [ 'lim' => 10, 320 'conds' => [ 'log_action != ' . $this->localRepo->getReplicaDB()->addQuotes( 'revision' ) ], 321 'showIfEmpty' => false, 322 'msgKey' => [ 'upload-recreate-warning' ] ] 323 ); 324 } 325 $form->addPreText( $delNotice ); 326 327 # Add text to form 328 $form->addPreText( '<div id="uploadtext">' . 329 $this->msg( 'uploadtext', [ $this->mDesiredDestName ] )->parseAsBlock() . 330 '</div>' ); 331 # Add upload error message 332 $form->addPreText( $message ); 333 334 # Add footer to form 335 $uploadFooter = $this->msg( 'uploadfooter' ); 336 if ( !$uploadFooter->isDisabled() ) { 337 $form->addPostText( '<div id="mw-upload-footer-message">' 338 . $uploadFooter->parseAsBlock() . "</div>\n" ); 339 } 340 341 return $form; 342 } 343 344 /** 345 * Shows the "view X deleted revivions link"" 346 */ 347 protected function showViewDeletedLinks() { 348 $title = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName ); 349 $user = $this->getUser(); 350 // Show a subtitle link to deleted revisions (to sysops et al only) 351 if ( $title instanceof Title ) { 352 $count = $title->getDeletedEditsCount(); 353 if ( $count > 0 && $this->getAuthority()->isAllowed( 'deletedhistory' ) ) { 354 $restorelink = $this->getLinkRenderer()->makeKnownLink( 355 SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ), 356 $this->msg( 'restorelink' )->numParams( $count )->text() 357 ); 358 $link = $this->msg( 359 $this->getAuthority()->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted' 360 )->rawParams( $restorelink )->parseAsBlock(); 361 $this->getOutput()->addHTML( 362 Html::rawElement( 363 'div', 364 [ 'id' => 'contentSub2' ], 365 $link 366 ) 367 ); 368 } 369 } 370 } 371 372 /** 373 * Stashes the upload and shows the main upload form. 374 * 375 * Note: only errors that can be handled by changing the name or 376 * description should be redirected here. It should be assumed that the 377 * file itself is sane and has passed UploadBase::verifyFile. This 378 * essentially means that UploadBase::VERIFICATION_ERROR and 379 * UploadBase::EMPTY_FILE should not be passed here. 380 * 381 * @param string $message HTML message to be passed to mainUploadForm 382 */ 383 protected function showRecoverableUploadError( $message ) { 384 $stashStatus = $this->mUpload->tryStashFile( $this->getUser() ); 385 if ( $stashStatus->isGood() ) { 386 $sessionKey = $stashStatus->getValue()->getFileKey(); 387 $uploadWarning = 'upload-tryagain'; 388 } else { 389 $sessionKey = null; 390 $uploadWarning = 'upload-tryagain-nostash'; 391 } 392 $message = '<h2>' . $this->msg( 'uploaderror' )->escaped() . "</h2>\n" . 393 '<div class="error">' . $message . "</div>\n"; 394 395 $form = $this->getUploadForm( $message, $sessionKey ); 396 $form->setSubmitText( $this->msg( $uploadWarning )->escaped() ); 397 $this->showUploadForm( $form ); 398 } 399 400 /** 401 * Stashes the upload, shows the main form, but adds a "continue anyway button". 402 * Also checks whether there are actually warnings to display. 403 * 404 * @param array $warnings 405 * @return bool True if warnings were displayed, false if there are no 406 * warnings and it should continue processing 407 */ 408 protected function showUploadWarning( $warnings ) { 409 # If there are no warnings, or warnings we can ignore, return early. 410 # mDestWarningAck is set when some javascript has shown the warning 411 # to the user. mForReUpload is set when the user clicks the "upload a 412 # new version" link. 413 if ( !$warnings || ( count( $warnings ) == 1 414 && isset( $warnings['exists'] ) 415 && ( $this->mDestWarningAck || $this->mForReUpload ) ) 416 ) { 417 return false; 418 } 419 420 $stashStatus = $this->mUpload->tryStashFile( $this->getUser() ); 421 if ( $stashStatus->isGood() ) { 422 $sessionKey = $stashStatus->getValue()->getFileKey(); 423 $uploadWarning = 'uploadwarning-text'; 424 } else { 425 $sessionKey = null; 426 $uploadWarning = 'uploadwarning-text-nostash'; 427 } 428 429 // Add styles for the warning, reused from the live preview 430 $this->getOutput()->addModuleStyles( 'mediawiki.special' ); 431 432 $linkRenderer = $this->getLinkRenderer(); 433 $warningHtml = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n" 434 . '<div class="mw-destfile-warning"><ul>'; 435 foreach ( $warnings as $warning => $args ) { 436 if ( $warning == 'badfilename' ) { 437 $this->mDesiredDestName = Title::makeTitle( NS_FILE, $args )->getText(); 438 } 439 if ( $warning == 'exists' ) { 440 $msg = "\t<li>" . self::getExistsWarning( $args ) . "</li>\n"; 441 } elseif ( $warning == 'no-change' ) { 442 $file = $args; 443 $filename = $file->getTitle()->getPrefixedText(); 444 $msg = "\t<li>" . $this->msg( 'fileexists-no-change', $filename )->parse() . "</li>\n"; 445 } elseif ( $warning == 'duplicate-version' ) { 446 $file = $args[0]; 447 $count = count( $args ); 448 $filename = $file->getTitle()->getPrefixedText(); 449 $message = $this->msg( 'fileexists-duplicate-version' ) 450 ->params( $filename ) 451 ->numParams( $count ); 452 $msg = "\t<li>" . $message->parse() . "</li>\n"; 453 } elseif ( $warning == 'was-deleted' ) { 454 # If the file existed before and was deleted, warn the user of this 455 $ltitle = SpecialPage::getTitleFor( 'Log' ); 456 $llink = $linkRenderer->makeKnownLink( 457 $ltitle, 458 $this->msg( 'deletionlog' )->text(), 459 [], 460 [ 461 'type' => 'delete', 462 'page' => Title::makeTitle( NS_FILE, $args )->getPrefixedText(), 463 ] 464 ); 465 $msg = "\t<li>" . $this->msg( 'filewasdeleted' )->rawParams( $llink )->parse() . "</li>\n"; 466 } elseif ( $warning == 'duplicate' ) { 467 $msg = $this->getDupeWarning( $args ); 468 } elseif ( $warning == 'duplicate-archive' ) { 469 if ( $args === '' ) { 470 $msg = "\t<li>" . $this->msg( 'file-deleted-duplicate-notitle' )->parse() 471 . "</li>\n"; 472 } else { 473 $msg = "\t<li>" . $this->msg( 'file-deleted-duplicate', 474 Title::makeTitle( NS_FILE, $args )->getPrefixedText() )->parse() 475 . "</li>\n"; 476 } 477 } else { 478 if ( $args === true ) { 479 $args = []; 480 } elseif ( !is_array( $args ) ) { 481 $args = [ $args ]; 482 } 483 $msg = "\t<li>" . $this->msg( $warning, $args )->parse() . "</li>\n"; 484 } 485 $warningHtml .= $msg; 486 } 487 $warningHtml .= "</ul></div>\n"; 488 $warningHtml .= $this->msg( $uploadWarning )->parseAsBlock(); 489 490 $form = $this->getUploadForm( $warningHtml, $sessionKey, /* $hideIgnoreWarning */ true ); 491 $form->setSubmitText( $this->msg( 'upload-tryagain' )->text() ); 492 $form->addButton( [ 493 'name' => 'wpUploadIgnoreWarning', 494 'value' => $this->msg( 'ignorewarning' )->text() 495 ] ); 496 $form->addButton( [ 497 'name' => 'wpCancelUpload', 498 'value' => $this->msg( 'reuploaddesc' )->text() 499 ] ); 500 501 $this->showUploadForm( $form ); 502 503 # Indicate that we showed a form 504 return true; 505 } 506 507 /** 508 * Show the upload form with error message, but do not stash the file. 509 * 510 * @param string $message HTML string 511 */ 512 protected function showUploadError( $message ) { 513 $message = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n" . 514 '<div class="error">' . $message . "</div>\n"; 515 $this->showUploadForm( $this->getUploadForm( $message ) ); 516 } 517 518 /** 519 * Do the upload. 520 * Checks are made in SpecialUpload::execute() 521 */ 522 protected function processUpload() { 523 // Fetch the file if required 524 $status = $this->mUpload->fetchFile(); 525 if ( !$status->isOK() ) { 526 $this->showUploadError( $this->getOutput()->parseAsInterface( 527 $status->getWikiText( false, false, $this->getLanguage() ) 528 ) ); 529 530 return; 531 } 532 if ( !$this->getHookRunner()->onUploadForm_BeforeProcessing( $this ) ) { 533 wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file." ); 534 // This code path is deprecated. If you want to break upload processing 535 // do so by hooking into the appropriate hooks in UploadBase::verifyUpload 536 // and UploadBase::verifyFile. 537 // If you use this hook to break uploading, the user will be returned 538 // an empty form with no error message whatsoever. 539 return; 540 } 541 542 // Upload verification 543 $details = $this->mUpload->verifyUpload(); 544 if ( $details['status'] != UploadBase::OK ) { 545 $this->processVerificationError( $details ); 546 547 return; 548 } 549 550 // Verify permissions for this title 551 $user = $this->getUser(); 552 $permErrors = $this->mUpload->verifyTitlePermissions( $user ); 553 if ( $permErrors !== true ) { 554 $code = array_shift( $permErrors[0] ); 555 $this->showRecoverableUploadError( $this->msg( $code, $permErrors[0] )->parse() ); 556 557 return; 558 } 559 560 $this->mLocalFile = $this->mUpload->getLocalFile(); 561 562 // Check warnings if necessary 563 if ( !$this->mIgnoreWarning ) { 564 $warnings = $this->mUpload->checkWarnings( $user ); 565 if ( $this->showUploadWarning( $warnings ) ) { 566 return; 567 } 568 } 569 570 // This is as late as we can throttle, after expected issues have been handled 571 if ( UploadBase::isThrottled( $user ) ) { 572 $this->showRecoverableUploadError( 573 $this->msg( 'actionthrottledtext' )->escaped() 574 ); 575 return; 576 } 577 578 // Get the page text if this is not a reupload 579 if ( !$this->mForReUpload ) { 580 $pageText = self::getInitialPageText( $this->mComment, $this->mLicense, 581 $this->mCopyrightStatus, $this->mCopyrightSource, $this->getConfig() ); 582 } else { 583 $pageText = false; 584 } 585 586 $changeTags = $this->getRequest()->getVal( 'wpChangeTags' ); 587 if ( $changeTags === null || $changeTags === '' ) { 588 $changeTags = []; 589 } else { 590 $changeTags = array_filter( array_map( 'trim', explode( ',', $changeTags ) ) ); 591 } 592 593 if ( $changeTags ) { 594 $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange( 595 $changeTags, $user ); 596 if ( !$changeTagsStatus->isOK() ) { 597 $this->showUploadError( $this->getOutput()->parseAsInterface( 598 $changeTagsStatus->getWikiText( false, false, $this->getLanguage() ) 599 ) ); 600 601 return; 602 } 603 } 604 605 $status = $this->mUpload->performUpload( 606 $this->mComment, 607 $pageText, 608 $this->mWatchthis, 609 $user, 610 $changeTags 611 ); 612 613 if ( !$status->isGood() ) { 614 $this->showRecoverableUploadError( 615 $this->getOutput()->parseAsInterface( 616 $status->getWikiText( false, false, $this->getLanguage() ) 617 ) 618 ); 619 620 return; 621 } 622 623 // Success, redirect to description page 624 $this->mUploadSuccessful = true; 625 $this->getHookRunner()->onSpecialUploadComplete( $this ); 626 $this->getOutput()->redirect( $this->mLocalFile->getTitle()->getFullURL() ); 627 } 628 629 /** 630 * Get the initial image page text based on a comment and optional file status information 631 * @param string $comment 632 * @param string $license 633 * @param string $copyStatus 634 * @param string $source 635 * @param Config|null $config Configuration object to load data from 636 * @return string 637 */ 638 public static function getInitialPageText( $comment = '', $license = '', 639 $copyStatus = '', $source = '', Config $config = null 640 ) { 641 if ( $config === null ) { 642 wfDebug( __METHOD__ . ' called without a Config instance passed to it' ); 643 $config = MediaWikiServices::getInstance()->getMainConfig(); 644 } 645 646 $msg = []; 647 $forceUIMsgAsContentMsg = (array)$config->get( 'ForceUIMsgAsContentMsg' ); 648 /* These messages are transcluded into the actual text of the description page. 649 * Thus, forcing them as content messages makes the upload to produce an int: template 650 * instead of hardcoding it there in the uploader language. 651 */ 652 foreach ( [ 'license-header', 'filedesc', 'filestatus', 'filesource' ] as $msgName ) { 653 if ( in_array( $msgName, $forceUIMsgAsContentMsg ) ) { 654 $msg[$msgName] = "{{int:$msgName}}"; 655 } else { 656 $msg[$msgName] = wfMessage( $msgName )->inContentLanguage()->text(); 657 } 658 } 659 660 $licenseText = ''; 661 if ( $license !== '' ) { 662 $licenseText = '== ' . $msg['license-header'] . " ==\n{{" . $license . "}}\n"; 663 } 664 665 $pageText = $comment . "\n"; 666 $headerText = '== ' . $msg['filedesc'] . ' =='; 667 if ( $comment !== '' && strpos( $comment, $headerText ) === false ) { 668 // prepend header to page text unless it's already there (or there is no content) 669 $pageText = $headerText . "\n" . $pageText; 670 } 671 672 if ( $config->get( 'UseCopyrightUpload' ) ) { 673 $pageText .= '== ' . $msg['filestatus'] . " ==\n" . $copyStatus . "\n"; 674 $pageText .= $licenseText; 675 $pageText .= '== ' . $msg['filesource'] . " ==\n" . $source; 676 } else { 677 $pageText .= $licenseText; 678 } 679 680 // allow extensions to modify the content 681 Hooks::runner()->onUploadForm_getInitialPageText( $pageText, $msg, $config ); 682 683 return $pageText; 684 } 685 686 /** 687 * See if we should check the 'watch this page' checkbox on the form 688 * based on the user's preferences and whether we're being asked 689 * to create a new file or update an existing one. 690 * 691 * In the case where 'watch edits' is off but 'watch creations' is on, 692 * we'll leave the box unchecked. 693 * 694 * Note that the page target can be changed *on the form*, so our check 695 * state can get out of sync. 696 * @return bool 697 */ 698 protected function getWatchCheck() { 699 $user = $this->getUser(); 700 if ( $this->userOptionsLookup->getBoolOption( $user, 'watchdefault' ) ) { 701 // Watch all edits! 702 return true; 703 } 704 705 $desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName ); 706 if ( $desiredTitleObj instanceof Title && $user->isWatched( $desiredTitleObj ) ) { 707 // Already watched, don't change that 708 return true; 709 } 710 711 $local = $this->localRepo->newFile( $this->mDesiredDestName ); 712 if ( $local && $local->exists() ) { 713 // We're uploading a new version of an existing file. 714 // No creation, so don't watch it if we're not already. 715 return false; 716 } else { 717 // New page should get watched if that's our option. 718 return $this->userOptionsLookup->getBoolOption( $user, 'watchcreations' ) || 719 $this->userOptionsLookup->getBoolOption( $user, 'watchuploads' ); 720 } 721 } 722 723 /** 724 * Provides output to the user for a result of UploadBase::verifyUpload 725 * 726 * @param array $details Result of UploadBase::verifyUpload 727 * @throws MWException 728 */ 729 protected function processVerificationError( $details ) { 730 switch ( $details['status'] ) { 731 /** Statuses that only require name changing */ 732 case UploadBase::MIN_LENGTH_PARTNAME: 733 $this->showRecoverableUploadError( $this->msg( 'minlength1' )->escaped() ); 734 break; 735 case UploadBase::ILLEGAL_FILENAME: 736 $this->showRecoverableUploadError( $this->msg( 'illegalfilename', 737 $details['filtered'] )->parse() ); 738 break; 739 case UploadBase::FILENAME_TOO_LONG: 740 $this->showRecoverableUploadError( $this->msg( 'filename-toolong' )->escaped() ); 741 break; 742 case UploadBase::FILETYPE_MISSING: 743 $this->showRecoverableUploadError( $this->msg( 'filetype-missing' )->parse() ); 744 break; 745 case UploadBase::WINDOWS_NONASCII_FILENAME: 746 $this->showRecoverableUploadError( $this->msg( 'windows-nonascii-filename' )->parse() ); 747 break; 748 749 /** Statuses that require reuploading */ 750 case UploadBase::EMPTY_FILE: 751 $this->showUploadError( $this->msg( 'emptyfile' )->escaped() ); 752 break; 753 case UploadBase::FILE_TOO_LARGE: 754 $this->showUploadError( $this->msg( 'largefileserver' )->escaped() ); 755 break; 756 case UploadBase::FILETYPE_BADTYPE: 757 $msg = $this->msg( 'filetype-banned-type' ); 758 if ( isset( $details['blacklistedExt'] ) ) { 759 $msg->params( $this->getLanguage()->commaList( $details['blacklistedExt'] ) ); 760 } else { 761 $msg->params( $details['finalExt'] ); 762 } 763 $extensions = array_unique( $this->getConfig()->get( 'FileExtensions' ) ); 764 $msg->params( $this->getLanguage()->commaList( $extensions ), 765 count( $extensions ) ); 766 767 // Add PLURAL support for the first parameter. This results 768 // in a bit unlogical parameter sequence, but does not break 769 // old translations 770 if ( isset( $details['blacklistedExt'] ) ) { 771 $msg->params( count( $details['blacklistedExt'] ) ); 772 } else { 773 $msg->params( 1 ); 774 } 775 776 $this->showUploadError( $msg->parse() ); 777 break; 778 case UploadBase::VERIFICATION_ERROR: 779 unset( $details['status'] ); 780 $code = array_shift( $details['details'] ); 781 $this->showUploadError( $this->msg( $code, $details['details'] )->parse() ); 782 break; 783 case UploadBase::HOOK_ABORTED: 784 if ( is_array( $details['error'] ) ) { # allow hooks to return error details in an array 785 $args = $details['error']; 786 $error = array_shift( $args ); 787 } else { 788 $error = $details['error']; 789 $args = null; 790 } 791 792 $this->showUploadError( $this->msg( $error, $args )->parse() ); 793 break; 794 default: 795 throw new MWException( __METHOD__ . ": Unknown value `{$details['status']}`" ); 796 } 797 } 798 799 /** 800 * Remove a temporarily kept file stashed by saveTempUploadedFile(). 801 * 802 * @return bool Success 803 */ 804 protected function unsaveUploadedFile() { 805 if ( !( $this->mUpload instanceof UploadFromStash ) ) { 806 return true; 807 } 808 $success = $this->mUpload->unsaveUploadedFile(); 809 if ( !$success ) { 810 $this->getOutput()->showFatalError( 811 $this->msg( 'filedeleteerror' ) 812 ->params( $this->mUpload->getTempPath() ) 813 ->escaped() 814 ); 815 816 return false; 817 } else { 818 return true; 819 } 820 } 821 822 /** Functions for formatting warnings */ 823 824 /** 825 * Formats a result of UploadBase::getExistsWarning as HTML 826 * This check is static and can be done pre-upload via AJAX 827 * 828 * @param array $exists The result of UploadBase::getExistsWarning 829 * @return string Empty string if there is no warning or an HTML fragment 830 */ 831 public static function getExistsWarning( $exists ) { 832 if ( !$exists ) { 833 return ''; 834 } 835 836 $file = $exists['file']; 837 $filename = $file->getTitle()->getPrefixedText(); 838 $warnMsg = null; 839 840 if ( $exists['warning'] == 'exists' ) { 841 // Exact match 842 $warnMsg = wfMessage( 'fileexists', $filename ); 843 } elseif ( $exists['warning'] == 'page-exists' ) { 844 // Page exists but file does not 845 $warnMsg = wfMessage( 'filepageexists', $filename ); 846 } elseif ( $exists['warning'] == 'exists-normalized' ) { 847 $warnMsg = wfMessage( 'fileexists-extension', $filename, 848 $exists['normalizedFile']->getTitle()->getPrefixedText() ); 849 } elseif ( $exists['warning'] == 'thumb' ) { 850 // Swapped argument order compared with other messages for backwards compatibility 851 $warnMsg = wfMessage( 'fileexists-thumbnail-yes', 852 $exists['thumbFile']->getTitle()->getPrefixedText(), $filename ); 853 } elseif ( $exists['warning'] == 'thumb-name' ) { 854 // Image w/o '180px-' does not exists, but we do not like these filenames 855 $name = $file->getName(); 856 $badPart = substr( $name, 0, strpos( $name, '-' ) + 1 ); 857 $warnMsg = wfMessage( 'file-thumbnail-no', $badPart ); 858 } elseif ( $exists['warning'] == 'bad-prefix' ) { 859 $warnMsg = wfMessage( 'filename-bad-prefix', $exists['prefix'] ); 860 } 861 862 return $warnMsg ? $warnMsg->title( $file->getTitle() )->parse() : ''; 863 } 864 865 /** 866 * Construct a warning and a gallery from an array of duplicate files. 867 * @param array $dupes 868 * @return string 869 */ 870 public function getDupeWarning( $dupes ) { 871 if ( !$dupes ) { 872 return ''; 873 } 874 875 $gallery = ImageGalleryBase::factory( false, $this->getContext() ); 876 $gallery->setShowBytes( false ); 877 $gallery->setShowDimensions( false ); 878 foreach ( $dupes as $file ) { 879 $gallery->add( $file->getTitle() ); 880 } 881 882 return '<li>' . 883 $this->msg( 'file-exists-duplicate' )->numParams( count( $dupes ) )->parse() . 884 $gallery->toHTML() . "</li>\n"; 885 } 886 887 protected function getGroupName() { 888 return 'media'; 889 } 890 891 /** 892 * Should we rotate images in the preview on Special:Upload. 893 * 894 * This controls js: mw.config.get( 'wgFileCanRotate' ) 895 * 896 * @todo What about non-BitmapHandler handled files? 897 * @return bool 898 */ 899 public static function rotationEnabled() { 900 $bitmapHandler = new BitmapHandler(); 901 return $bitmapHandler->autoRotateEnabled(); 902 } 903} 904