1<?php 2/** 3 * Implements Special:Undelete 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 */ 23 24use MediaWiki\Cache\LinkBatchFactory; 25use MediaWiki\Content\IContentHandlerFactory; 26use MediaWiki\Page\WikiPageFactory; 27use MediaWiki\Permissions\PermissionManager; 28use MediaWiki\Revision\RevisionRecord; 29use MediaWiki\Revision\RevisionRenderer; 30use MediaWiki\Revision\RevisionStore; 31use MediaWiki\Revision\SlotRecord; 32use MediaWiki\Storage\NameTableAccessException; 33use MediaWiki\Storage\NameTableStore; 34use MediaWiki\User\UserOptionsLookup; 35use Wikimedia\Rdbms\ILoadBalancer; 36use Wikimedia\Rdbms\IResultWrapper; 37 38/** 39 * Special page allowing users with the appropriate permissions to view 40 * and restore deleted content. 41 * 42 * @ingroup SpecialPage 43 */ 44class SpecialUndelete extends SpecialPage { 45 private $mAction; 46 private $mTarget; 47 private $mTimestamp; 48 private $mRestore; 49 private $mRevdel; 50 private $mInvert; 51 private $mFilename; 52 private $mTargetTimestamp; 53 private $mAllowed; 54 private $mCanView; 55 private $mComment; 56 private $mToken; 57 /** @var bool|null */ 58 private $mPreview; 59 /** @var bool|null */ 60 private $mDiff; 61 /** @var bool|null */ 62 private $mDiffOnly; 63 /** @var bool|null */ 64 private $mUnsuppress; 65 /** @var int[]|null */ 66 private $mFileVersions; 67 68 /** @var Title */ 69 private $mTargetObj; 70 /** 71 * @var string Search prefix 72 */ 73 private $mSearchPrefix; 74 75 /** @var PermissionManager */ 76 private $permissionManager; 77 78 /** @var RevisionStore */ 79 private $revisionStore; 80 81 /** @var RevisionRenderer */ 82 private $revisionRenderer; 83 84 /** @var IContentHandlerFactory */ 85 private $contentHandlerFactory; 86 87 /** @var NameTableStore */ 88 private $changeTagDefStore; 89 90 /** @var LinkBatchFactory */ 91 private $linkBatchFactory; 92 93 /** @var LocalRepo */ 94 private $localRepo; 95 96 /** @var ILoadBalancer */ 97 private $loadBalancer; 98 99 /** @var UserOptionsLookup */ 100 private $userOptionsLookup; 101 102 /** @var WikiPageFactory */ 103 private $wikiPageFactory; 104 105 /** @var SearchEngineFactory */ 106 private $searchEngineFactory; 107 108 /** 109 * @param PermissionManager $permissionManager 110 * @param RevisionStore $revisionStore 111 * @param RevisionRenderer $revisionRenderer 112 * @param IContentHandlerFactory $contentHandlerFactory 113 * @param NameTableStore $changeTagDefStore 114 * @param LinkBatchFactory $linkBatchFactory 115 * @param RepoGroup $repoGroup 116 * @param ILoadBalancer $loadBalancer 117 * @param UserOptionsLookup $userOptionsLookup 118 * @param WikiPageFactory $wikiPageFactory 119 * @param SearchEngineFactory $searchEngineFactory 120 */ 121 public function __construct( 122 PermissionManager $permissionManager, 123 RevisionStore $revisionStore, 124 RevisionRenderer $revisionRenderer, 125 IContentHandlerFactory $contentHandlerFactory, 126 NameTableStore $changeTagDefStore, 127 LinkBatchFactory $linkBatchFactory, 128 RepoGroup $repoGroup, 129 ILoadBalancer $loadBalancer, 130 UserOptionsLookup $userOptionsLookup, 131 WikiPageFactory $wikiPageFactory, 132 SearchEngineFactory $searchEngineFactory 133 ) { 134 parent::__construct( 'Undelete', 'deletedhistory' ); 135 $this->permissionManager = $permissionManager; 136 $this->revisionStore = $revisionStore; 137 $this->revisionRenderer = $revisionRenderer; 138 $this->contentHandlerFactory = $contentHandlerFactory; 139 $this->changeTagDefStore = $changeTagDefStore; 140 $this->linkBatchFactory = $linkBatchFactory; 141 $this->localRepo = $repoGroup->getLocalRepo(); 142 $this->loadBalancer = $loadBalancer; 143 $this->userOptionsLookup = $userOptionsLookup; 144 $this->wikiPageFactory = $wikiPageFactory; 145 $this->searchEngineFactory = $searchEngineFactory; 146 } 147 148 public function doesWrites() { 149 return true; 150 } 151 152 private function loadRequest( $par ) { 153 $request = $this->getRequest(); 154 $user = $this->getUser(); 155 156 $this->mAction = $request->getVal( 'action' ); 157 if ( $par !== null && $par !== '' ) { 158 $this->mTarget = $par; 159 } else { 160 $this->mTarget = $request->getVal( 'target' ); 161 } 162 163 $this->mTargetObj = null; 164 165 if ( $this->mTarget !== null && $this->mTarget !== '' ) { 166 $this->mTargetObj = Title::newFromText( $this->mTarget ); 167 } 168 169 $this->mSearchPrefix = $request->getText( 'prefix' ); 170 $time = $request->getVal( 'timestamp' ); 171 $this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : ''; 172 $this->mFilename = $request->getVal( 'file' ); 173 174 $posted = $request->wasPosted() && 175 $user->matchEditToken( $request->getVal( 'wpEditToken' ) ); 176 $this->mRestore = $request->getCheck( 'restore' ) && $posted; 177 $this->mRevdel = $request->getCheck( 'revdel' ) && $posted; 178 $this->mInvert = $request->getCheck( 'invert' ) && $posted; 179 $this->mPreview = $request->getCheck( 'preview' ) && $posted; 180 $this->mDiff = $request->getCheck( 'diff' ); 181 $this->mDiffOnly = $request->getBool( 'diffonly', 182 $this->userOptionsLookup->getOption( $this->getUser(), 'diffonly' ) ); 183 $this->mComment = $request->getText( 'wpComment' ); 184 $this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) && 185 $this->permissionManager->userHasRight( $user, 'suppressrevision' ); 186 $this->mToken = $request->getVal( 'token' ); 187 188 if ( $this->isAllowed( 'undelete' ) ) { 189 $this->mAllowed = true; // user can restore 190 $this->mCanView = true; // user can view content 191 } elseif ( $this->isAllowed( 'deletedtext' ) ) { 192 $this->mAllowed = false; // user cannot restore 193 $this->mCanView = true; // user can view content 194 $this->mRestore = false; 195 } else { // user can only view the list of revisions 196 $this->mAllowed = false; 197 $this->mCanView = false; 198 $this->mTimestamp = ''; 199 $this->mRestore = false; 200 } 201 202 if ( $this->mRestore || $this->mInvert ) { 203 $timestamps = []; 204 $this->mFileVersions = []; 205 foreach ( $request->getValues() as $key => $val ) { 206 $matches = []; 207 if ( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) { 208 array_push( $timestamps, $matches[1] ); 209 } 210 211 if ( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) { 212 $this->mFileVersions[] = intval( $matches[1] ); 213 } 214 } 215 rsort( $timestamps ); 216 $this->mTargetTimestamp = $timestamps; 217 } 218 } 219 220 /** 221 * Checks whether a user is allowed the permission for the 222 * specific title if one is set. 223 * 224 * @param string $permission 225 * @param User|null $user 226 * @return bool 227 */ 228 protected function isAllowed( $permission, User $user = null ) { 229 $user = $user ?: $this->getUser(); 230 $block = $user->getBlock(); 231 232 if ( $this->mTargetObj !== null ) { 233 return $this->permissionManager->userCan( $permission, $user, $this->mTargetObj ); 234 } else { 235 $hasRight = $this->permissionManager->userHasRight( $user, $permission ); 236 $sitewideBlock = $block && $block->isSitewide(); 237 return $permission === 'undelete' ? ( $hasRight && !$sitewideBlock ) : $hasRight; 238 } 239 } 240 241 public function userCanExecute( User $user ) { 242 return $this->isAllowed( $this->mRestriction, $user ); 243 } 244 245 /** 246 * @inheritDoc 247 */ 248 public function checkPermissions() { 249 $user = $this->getUser(); 250 251 // First check if user has the right to use this page. If not, 252 // show a permissions error whether they are blocked or not. 253 if ( !parent::userCanExecute( $user ) ) { 254 $this->displayRestrictionError(); 255 } 256 257 // If a user has the right to use this page, but is blocked from 258 // the target, show a block error. 259 if ( 260 $this->mTargetObj && $this->permissionManager->isBlockedFrom( $user, $this->mTargetObj ) ) { 261 throw new UserBlockedError( $user->getBlock() ); 262 } 263 264 // Finally, do the comprehensive permission check via isAllowed. 265 if ( !$this->userCanExecute( $user ) ) { 266 $this->displayRestrictionError(); 267 } 268 } 269 270 public function execute( $par ) { 271 $this->useTransactionalTimeLimit(); 272 273 $user = $this->getUser(); 274 275 $this->setHeaders(); 276 $this->outputHeader(); 277 $this->addHelpLink( 'Help:Deletion_and_undeletion' ); 278 279 $this->loadRequest( $par ); 280 $this->checkPermissions(); // Needs to be after mTargetObj is set 281 282 $out = $this->getOutput(); 283 284 if ( $this->mTargetObj === null ) { 285 $out->addWikiMsg( 'undelete-header' ); 286 287 # Not all users can just browse every deleted page from the list 288 if ( $this->permissionManager->userHasRight( $user, 'browsearchive' ) ) { 289 $this->showSearchForm(); 290 } 291 292 return; 293 } 294 295 $this->addHelpLink( 'Help:Undelete' ); 296 if ( $this->mAllowed ) { 297 $out->setPageTitle( $this->msg( 'undeletepage' ) ); 298 } else { 299 $out->setPageTitle( $this->msg( 'viewdeletedpage' ) ); 300 } 301 302 $this->getSkin()->setRelevantTitle( $this->mTargetObj ); 303 304 if ( $this->mTimestamp !== '' ) { 305 $this->showRevision( $this->mTimestamp ); 306 } elseif ( $this->mFilename !== null && $this->mTargetObj->inNamespace( NS_FILE ) ) { 307 $file = new ArchivedFile( $this->mTargetObj, 0, $this->mFilename ); 308 // Check if user is allowed to see this file 309 if ( !$file->exists() ) { 310 $out->addWikiMsg( 'filedelete-nofile', $this->mFilename ); 311 } elseif ( !$file->userCan( File::DELETED_FILE, $user ) ) { 312 if ( $file->isDeleted( File::DELETED_RESTRICTED ) ) { 313 throw new PermissionsError( 'suppressrevision' ); 314 } else { 315 throw new PermissionsError( 'deletedtext' ); 316 } 317 } elseif ( !$user->matchEditToken( $this->mToken, $this->mFilename ) ) { 318 $this->showFileConfirmationForm( $this->mFilename ); 319 } else { 320 $this->showFile( $this->mFilename ); 321 } 322 } elseif ( $this->mAction === 'submit' ) { 323 if ( $this->mRestore ) { 324 $this->undelete(); 325 } elseif ( $this->mRevdel ) { 326 $this->redirectToRevDel(); 327 } 328 329 } else { 330 $this->showHistory(); 331 } 332 } 333 334 /** 335 * Convert submitted form data to format expected by RevisionDelete and 336 * redirect the request 337 */ 338 private function redirectToRevDel() { 339 $archive = new PageArchive( $this->mTargetObj ); 340 341 $revisions = []; 342 343 foreach ( $this->getRequest()->getValues() as $key => $val ) { 344 $matches = []; 345 if ( preg_match( "/^ts(\d{14})$/", $key, $matches ) ) { 346 $revisionRecord = $archive->getRevisionRecordByTimestamp( $matches[1] ); 347 if ( $revisionRecord ) { 348 // Can return null 349 $revisions[ $revisionRecord->getId() ] = 1; 350 } 351 } 352 } 353 354 $query = [ 355 'type' => 'revision', 356 'ids' => $revisions, 357 'target' => $this->mTargetObj->getPrefixedText() 358 ]; 359 $url = SpecialPage::getTitleFor( 'Revisiondelete' )->getFullURL( $query ); 360 $this->getOutput()->redirect( $url ); 361 } 362 363 private function showSearchForm() { 364 $out = $this->getOutput(); 365 $out->setPageTitle( $this->msg( 'undelete-search-title' ) ); 366 $fuzzySearch = $this->getRequest()->getVal( 'fuzzy', true ); 367 368 $out->enableOOUI(); 369 370 $fields = []; 371 $fields[] = new OOUI\ActionFieldLayout( 372 new OOUI\TextInputWidget( [ 373 'name' => 'prefix', 374 'inputId' => 'prefix', 375 'infusable' => true, 376 'value' => $this->mSearchPrefix, 377 'autofocus' => true, 378 ] ), 379 new OOUI\ButtonInputWidget( [ 380 'label' => $this->msg( 'undelete-search-submit' )->text(), 381 'flags' => [ 'primary', 'progressive' ], 382 'inputId' => 'searchUndelete', 383 'type' => 'submit', 384 ] ), 385 [ 386 'label' => new OOUI\HtmlSnippet( 387 $this->msg( 388 $fuzzySearch ? 'undelete-search-full' : 'undelete-search-prefix' 389 )->parse() 390 ), 391 'align' => 'left', 392 ] 393 ); 394 395 $fieldset = new OOUI\FieldsetLayout( [ 396 'label' => $this->msg( 'undelete-search-box' )->text(), 397 'items' => $fields, 398 ] ); 399 400 $form = new OOUI\FormLayout( [ 401 'method' => 'get', 402 'action' => wfScript(), 403 ] ); 404 405 $form->appendContent( 406 $fieldset, 407 new OOUI\HtmlSnippet( 408 Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) . 409 Html::hidden( 'fuzzy', $fuzzySearch ) 410 ) 411 ); 412 413 $out->addHTML( 414 new OOUI\PanelLayout( [ 415 'expanded' => false, 416 'padded' => true, 417 'framed' => true, 418 'content' => $form, 419 ] ) 420 ); 421 422 # List undeletable articles 423 if ( $this->mSearchPrefix ) { 424 // For now, we enable search engine match only when specifically asked to 425 // by using fuzzy=1 parameter. 426 if ( $fuzzySearch ) { 427 $result = PageArchive::listPagesBySearch( $this->mSearchPrefix ); 428 } else { 429 $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix ); 430 } 431 $this->showList( $result ); 432 } 433 } 434 435 /** 436 * Generic list of deleted pages 437 * 438 * @param IResultWrapper $result 439 * @return bool 440 */ 441 private function showList( $result ) { 442 $out = $this->getOutput(); 443 444 if ( $result->numRows() == 0 ) { 445 $out->addWikiMsg( 'undelete-no-results' ); 446 447 return false; 448 } 449 450 $out->addWikiMsg( 'undeletepagetext', $this->getLanguage()->formatNum( $result->numRows() ) ); 451 452 $linkRenderer = $this->getLinkRenderer(); 453 $undelete = $this->getPageTitle(); 454 $out->addHTML( "<ul id='undeleteResultsList'>\n" ); 455 foreach ( $result as $row ) { 456 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title ); 457 if ( $title !== null ) { 458 $item = $linkRenderer->makeKnownLink( 459 $undelete, 460 $title->getPrefixedText(), 461 [], 462 [ 'target' => $title->getPrefixedText() ] 463 ); 464 } else { 465 // The title is no longer valid, show as text 466 $item = Html::element( 467 'span', 468 [ 'class' => 'mw-invalidtitle' ], 469 Linker::getInvalidTitleDescription( 470 $this->getContext(), 471 $row->ar_namespace, 472 $row->ar_title 473 ) 474 ); 475 } 476 $revs = $this->msg( 'undeleterevisions' )->numParams( $row->count )->parse(); 477 $out->addHTML( 478 Html::rawElement( 479 'li', 480 [ 'class' => 'undeleteResult' ], 481 "{$item} ({$revs})" 482 ) 483 ); 484 } 485 $result->free(); 486 $out->addHTML( "</ul>\n" ); 487 488 return true; 489 } 490 491 private function showRevision( $timestamp ) { 492 if ( !preg_match( '/[0-9]{14}/', $timestamp ) ) { 493 return; 494 } 495 496 $archive = new PageArchive( $this->mTargetObj, $this->getConfig() ); 497 if ( !$this->getHookRunner()->onUndeleteForm__showRevision( 498 $archive, $this->mTargetObj ) 499 ) { 500 return; 501 } 502 $revRecord = $archive->getRevisionRecordByTimestamp( $timestamp ); 503 504 $out = $this->getOutput(); 505 $user = $this->getUser(); 506 507 if ( !$revRecord ) { 508 $out->addWikiMsg( 'undeleterevision-missing' ); 509 return; 510 } 511 512 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) { 513 // Used in wikilinks, should not contain whitespaces 514 $titleText = $this->mTargetObj->getPrefixedDBkey(); 515 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) { 516 $msg = $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) 517 ? [ 'rev-suppressed-text-permission', $titleText ] 518 : [ 'rev-deleted-text-permission', $titleText ]; 519 $out->addHtml( 520 Html::warningBox( 521 $this->msg( $msg[0], $msg[1] )->parse(), 522 'plainlinks' 523 ) 524 ); 525 return; 526 } 527 528 $msg = $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) 529 ? [ 'rev-suppressed-text-view', $titleText ] 530 : [ 'rev-deleted-text-view', $titleText ]; 531 $out->addHtml( 532 Html::warningBox( 533 $this->msg( $msg[0], $msg[1] )->parse(), 534 'plainlinks' 535 ) 536 ); 537 // and we are allowed to see... 538 } 539 540 if ( $this->mDiff ) { 541 $previousRevRecord = $archive->getPreviousRevisionRecord( $timestamp ); 542 if ( $previousRevRecord ) { 543 $this->showDiff( $previousRevRecord, $revRecord ); 544 if ( $this->mDiffOnly ) { 545 return; 546 } 547 548 $out->addHTML( '<hr />' ); 549 } else { 550 $out->addWikiMsg( 'undelete-nodiff' ); 551 } 552 } 553 554 $link = $this->getLinkRenderer()->makeKnownLink( 555 $this->getPageTitle( $this->mTargetObj->getPrefixedDBkey() ), 556 $this->mTargetObj->getPrefixedText() 557 ); 558 559 $lang = $this->getLanguage(); 560 561 // date and time are separate parameters to facilitate localisation. 562 // $time is kept for backward compat reasons. 563 $time = $lang->userTimeAndDate( $timestamp, $user ); 564 $d = $lang->userDate( $timestamp, $user ); 565 $t = $lang->userTime( $timestamp, $user ); 566 $userLink = Linker::revUserTools( $revRecord ); 567 568 $content = $revRecord->getContent( 569 SlotRecord::MAIN, 570 RevisionRecord::FOR_THIS_USER, 571 $user 572 ); 573 574 // TODO: MCR: this will have to become something like $hasTextSlots and $hasNonTextSlots 575 $isText = ( $content instanceof TextContent ); 576 577 if ( $this->mPreview || $isText ) { 578 $openDiv = '<div id="mw-undelete-revision" class="warningbox">'; 579 } else { 580 $openDiv = '<div id="mw-undelete-revision">'; 581 } 582 $out->addHTML( $openDiv ); 583 584 // Revision delete links 585 if ( !$this->mDiff ) { 586 $revdel = Linker::getRevDeleteLink( 587 $user, 588 $revRecord, 589 $this->mTargetObj 590 ); 591 if ( $revdel ) { 592 $out->addHTML( "$revdel " ); 593 } 594 } 595 596 $out->addWikiMsg( 597 'undelete-revision', 598 Message::rawParam( $link ), $time, 599 Message::rawParam( $userLink ), $d, $t 600 ); 601 $out->addHTML( '</div>' ); 602 603 // Hook hard deprecated since 1.35 604 if ( $this->getHookContainer()->isRegistered( 'UndeleteShowRevision' ) ) { 605 // Only create the Revision object if needed 606 $rev = new Revision( $revRecord ); 607 if ( !$this->getHookRunner()->onUndeleteShowRevision( 608 $this->mTargetObj, 609 $rev 610 ) ) { 611 return; 612 } 613 } 614 615 if ( $this->mPreview || !$isText ) { 616 // NOTE: non-text content has no source view, so always use rendered preview 617 618 $popts = $out->parserOptions(); 619 620 $rendered = $this->revisionRenderer->getRenderedRevision( 621 $revRecord, 622 $popts, 623 $user, 624 [ 'audience' => RevisionRecord::FOR_THIS_USER ] 625 ); 626 627 // Fail hard if the audience check fails, since we already checked 628 // at the beginning of this method. 629 $pout = $rendered->getRevisionParserOutput(); 630 631 $out->addParserOutput( $pout, [ 632 'enableSectionEditLinks' => false, 633 ] ); 634 } 635 636 $out->enableOOUI(); 637 $buttonFields = []; 638 639 if ( $isText ) { 640 '@phan-var TextContent $content'; 641 // TODO: MCR: make this work for multiple slots 642 // source view for textual content 643 $sourceView = Xml::element( 'textarea', [ 644 'readonly' => 'readonly', 645 'cols' => 80, 646 'rows' => 25 647 ], $content->getText() . "\n" ); 648 649 $buttonFields[] = new OOUI\ButtonInputWidget( [ 650 'type' => 'submit', 651 'name' => 'preview', 652 'label' => $this->msg( 'showpreview' )->text() 653 ] ); 654 } else { 655 $sourceView = ''; 656 } 657 658 $buttonFields[] = new OOUI\ButtonInputWidget( [ 659 'name' => 'diff', 660 'type' => 'submit', 661 'label' => $this->msg( 'showdiff' )->text() 662 ] ); 663 664 $out->addHTML( 665 $sourceView . 666 Xml::openElement( 'div', [ 667 'style' => 'clear: both' ] ) . 668 Xml::openElement( 'form', [ 669 'method' => 'post', 670 'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ) ] ) . 671 Xml::element( 'input', [ 672 'type' => 'hidden', 673 'name' => 'target', 674 'value' => $this->mTargetObj->getPrefixedDBkey() ] ) . 675 Xml::element( 'input', [ 676 'type' => 'hidden', 677 'name' => 'timestamp', 678 'value' => $timestamp ] ) . 679 Xml::element( 'input', [ 680 'type' => 'hidden', 681 'name' => 'wpEditToken', 682 'value' => $user->getEditToken() ] ) . 683 new OOUI\FieldLayout( 684 new OOUI\Widget( [ 685 'content' => new OOUI\HorizontalLayout( [ 686 'items' => $buttonFields 687 ] ) 688 ] ) 689 ) . 690 Xml::closeElement( 'form' ) . 691 Xml::closeElement( 'div' ) 692 ); 693 } 694 695 /** 696 * Build a diff display between this and the previous either deleted 697 * or non-deleted edit. 698 * 699 * @param RevisionRecord $previousRevRecord 700 * @param RevisionRecord $currentRevRecord 701 */ 702 private function showDiff( 703 RevisionRecord $previousRevRecord, 704 RevisionRecord $currentRevRecord 705 ) { 706 $currentTitle = Title::newFromLinkTarget( $currentRevRecord->getPageAsLinkTarget() ); 707 708 $diffContext = clone $this->getContext(); 709 $diffContext->setTitle( $currentTitle ); 710 $diffContext->setWikiPage( $this->wikiPageFactory->newFromTitle( $currentTitle ) ); 711 712 $contentModel = $currentRevRecord->getSlot( 713 SlotRecord::MAIN, 714 RevisionRecord::RAW 715 )->getModel(); 716 717 $diffEngine = $this->contentHandlerFactory->getContentHandler( $contentModel ) 718 ->createDifferenceEngine( $diffContext ); 719 720 $diffEngine->setRevisions( $previousRevRecord, $currentRevRecord ); 721 $diffEngine->showDiffStyle(); 722 $formattedDiff = $diffEngine->getDiff( 723 $this->diffHeader( $previousRevRecord, 'o' ), 724 $this->diffHeader( $currentRevRecord, 'n' ) 725 ); 726 727 $this->getOutput()->addHTML( "<div>$formattedDiff</div>\n" ); 728 } 729 730 /** 731 * @param RevisionRecord $revRecord 732 * @param string $prefix 733 * @return string 734 */ 735 private function diffHeader( RevisionRecord $revRecord, $prefix ) { 736 $isDeleted = !( $revRecord->getId() && $revRecord->getPageAsLinkTarget() ); 737 if ( $isDeleted ) { 738 // @todo FIXME: $rev->getTitle() is null for deleted revs...? 739 $targetPage = $this->getPageTitle(); 740 $targetQuery = [ 741 'target' => $this->mTargetObj->getPrefixedText(), 742 'timestamp' => wfTimestamp( TS_MW, $revRecord->getTimestamp() ) 743 ]; 744 } else { 745 // @todo FIXME: getId() may return non-zero for deleted revs... 746 $targetPage = $revRecord->getPageAsLinkTarget(); 747 $targetQuery = [ 'oldid' => $revRecord->getId() ]; 748 } 749 750 // Add show/hide deletion links if available 751 $user = $this->getUser(); 752 $lang = $this->getLanguage(); 753 $rdel = Linker::getRevDeleteLink( $user, $revRecord, $this->mTargetObj ); 754 755 if ( $rdel ) { 756 $rdel = " $rdel"; 757 } 758 759 $minor = $revRecord->isMinor() ? ChangesList::flag( 'minor' ) : ''; 760 761 $dbr = $this->loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA ); 762 $tagIds = $dbr->selectFieldValues( 763 'change_tag', 764 'ct_tag_id', 765 [ 'ct_rev_id' => $revRecord->getId() ], 766 __METHOD__ 767 ); 768 $tags = []; 769 foreach ( $tagIds as $tagId ) { 770 try { 771 $tags[] = $this->changeTagDefStore->getName( (int)$tagId ); 772 } catch ( NameTableAccessException $exception ) { 773 continue; 774 } 775 } 776 $tags = implode( ',', $tags ); 777 $tagSummary = ChangeTags::formatSummaryRow( $tags, 'deleteddiff', $this->getContext() ); 778 779 // FIXME This is reimplementing DifferenceEngine#getRevisionHeader 780 // and partially #showDiffPage, but worse 781 return '<div id="mw-diff-' . $prefix . 'title1"><strong>' . 782 $this->getLinkRenderer()->makeLink( 783 $targetPage, 784 $this->msg( 785 'revisionasof', 786 $lang->userTimeAndDate( $revRecord->getTimestamp(), $user ), 787 $lang->userDate( $revRecord->getTimestamp(), $user ), 788 $lang->userTime( $revRecord->getTimestamp(), $user ) 789 )->text(), 790 [], 791 $targetQuery 792 ) . 793 '</strong></div>' . 794 '<div id="mw-diff-' . $prefix . 'title2">' . 795 Linker::revUserTools( $revRecord ) . '<br />' . 796 '</div>' . 797 '<div id="mw-diff-' . $prefix . 'title3">' . 798 $minor . Linker::revComment( $revRecord ) . $rdel . '<br />' . 799 '</div>' . 800 '<div id="mw-diff-' . $prefix . 'title5">' . 801 $tagSummary[0] . '<br />' . 802 '</div>'; 803 } 804 805 /** 806 * Show a form confirming whether a tokenless user really wants to see a file 807 * @param string $key 808 */ 809 private function showFileConfirmationForm( $key ) { 810 $out = $this->getOutput(); 811 $lang = $this->getLanguage(); 812 $user = $this->getUser(); 813 $file = new ArchivedFile( $this->mTargetObj, 0, $this->mFilename ); 814 $out->addWikiMsg( 'undelete-show-file-confirm', 815 $this->mTargetObj->getText(), 816 $lang->userDate( $file->getTimestamp(), $user ), 817 $lang->userTime( $file->getTimestamp(), $user ) ); 818 $out->addHTML( 819 Xml::openElement( 'form', [ 820 'method' => 'POST', 821 'action' => $this->getPageTitle()->getLocalURL( [ 822 'target' => $this->mTarget, 823 'file' => $key, 824 'token' => $user->getEditToken( $key ), 825 ] ), 826 ] 827 ) . 828 Xml::submitButton( $this->msg( 'undelete-show-file-submit' )->text() ) . 829 '</form>' 830 ); 831 } 832 833 /** 834 * Show a deleted file version requested by the visitor. 835 * @param string $key 836 */ 837 private function showFile( $key ) { 838 $this->getOutput()->disable(); 839 840 # We mustn't allow the output to be CDN cached, otherwise 841 # if an admin previews a deleted image, and it's cached, then 842 # a user without appropriate permissions can toddle off and 843 # nab the image, and CDN will serve it 844 $response = $this->getRequest()->response(); 845 $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); 846 $response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' ); 847 $response->header( 'Pragma: no-cache' ); 848 849 $path = $this->localRepo->getZonePath( 'deleted' ) . '/' . $this->localRepo->getDeletedHashPath( $key ) . $key; 850 $this->localRepo->streamFileWithStatus( $path ); 851 } 852 853 protected function showHistory() { 854 $this->checkReadOnly(); 855 856 $out = $this->getOutput(); 857 if ( $this->mAllowed ) { 858 $out->addModules( 'mediawiki.misc-authed-ooui' ); 859 } 860 $out->wrapWikiMsg( 861 "<div class='mw-undelete-pagetitle'>\n$1\n</div>\n", 862 [ 'undeletepagetitle', wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) ] 863 ); 864 865 $archive = new PageArchive( $this->mTargetObj, $this->getConfig() ); 866 $this->getHookRunner()->onUndeleteForm__showHistory( $archive, $this->mTargetObj ); 867 868 $out->addHTML( '<div class="mw-undelete-history">' ); 869 if ( $this->mAllowed ) { 870 $out->addWikiMsg( 'undeletehistory' ); 871 $out->addWikiMsg( 'undeleterevdel' ); 872 } else { 873 $out->addWikiMsg( 'undeletehistorynoadmin' ); 874 } 875 $out->addHTML( '</div>' ); 876 877 # List all stored revisions 878 $revisions = $archive->listRevisions(); 879 $files = $archive->listFiles(); 880 881 $haveRevisions = $revisions && $revisions->numRows() > 0; 882 $haveFiles = $files && $files->numRows() > 0; 883 884 # Batch existence check on user and talk pages 885 if ( $haveRevisions || $haveFiles ) { 886 $batch = $this->linkBatchFactory->newLinkBatch(); 887 if ( $haveRevisions ) { 888 foreach ( $revisions as $row ) { 889 $batch->add( NS_USER, $row->ar_user_text ); 890 $batch->add( NS_USER_TALK, $row->ar_user_text ); 891 } 892 $revisions->seek( 0 ); 893 } 894 if ( $haveFiles ) { 895 foreach ( $files as $row ) { 896 $batch->add( NS_USER, $row->fa_user_text ); 897 $batch->add( NS_USER_TALK, $row->fa_user_text ); 898 } 899 $files->seek( 0 ); 900 } 901 $batch->execute(); 902 } 903 904 if ( $this->mAllowed ) { 905 $out->enableOOUI(); 906 907 $action = $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ); 908 # Start the form here 909 $form = new OOUI\FormLayout( [ 910 'method' => 'post', 911 'action' => $action, 912 'id' => 'undelete', 913 ] ); 914 } 915 916 # Show relevant lines from the deletion log: 917 $deleteLogPage = new LogPage( 'delete' ); 918 $out->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) . "\n" ); 919 LogEventsList::showLogExtract( $out, 'delete', $this->mTargetObj ); 920 # Show relevant lines from the suppression log: 921 $suppressLogPage = new LogPage( 'suppress' ); 922 if ( $this->permissionManager->userHasRight( $this->getUser(), 'suppressionlog' ) ) { 923 $out->addHTML( Xml::element( 'h2', null, $suppressLogPage->getName()->text() ) . "\n" ); 924 LogEventsList::showLogExtract( $out, 'suppress', $this->mTargetObj ); 925 } 926 927 if ( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) { 928 $fields = []; 929 $fields[] = new OOUI\Layout( [ 930 'content' => new OOUI\HtmlSnippet( $this->msg( 'undeleteextrahelp' )->parseAsBlock() ) 931 ] ); 932 933 $fields[] = new OOUI\FieldLayout( 934 new OOUI\TextInputWidget( [ 935 'name' => 'wpComment', 936 'inputId' => 'wpComment', 937 'infusable' => true, 938 'value' => $this->mComment, 939 'autofocus' => true, 940 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP 941 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count 942 // Unicode codepoints. 943 'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT, 944 ] ), 945 [ 946 'label' => $this->msg( 'undeletecomment' )->text(), 947 'align' => 'top', 948 ] 949 ); 950 951 $fields[] = new OOUI\FieldLayout( 952 new OOUI\Widget( [ 953 'content' => new OOUI\HorizontalLayout( [ 954 'items' => [ 955 new OOUI\ButtonInputWidget( [ 956 'name' => 'restore', 957 'inputId' => 'mw-undelete-submit', 958 'value' => '1', 959 'label' => $this->msg( 'undeletebtn' )->text(), 960 'flags' => [ 'primary', 'progressive' ], 961 'type' => 'submit', 962 ] ), 963 new OOUI\ButtonInputWidget( [ 964 'name' => 'invert', 965 'inputId' => 'mw-undelete-invert', 966 'value' => '1', 967 'label' => $this->msg( 'undeleteinvert' )->text() 968 ] ), 969 ] 970 ] ) 971 ] ) 972 ); 973 974 if ( $this->permissionManager->userHasRight( $this->getUser(), 'suppressrevision' ) ) { 975 $fields[] = new OOUI\FieldLayout( 976 new OOUI\CheckboxInputWidget( [ 977 'name' => 'wpUnsuppress', 978 'inputId' => 'mw-undelete-unsuppress', 979 'value' => '1', 980 ] ), 981 [ 982 'label' => $this->msg( 'revdelete-unsuppress' )->text(), 983 'align' => 'inline', 984 ] 985 ); 986 } 987 988 $fieldset = new OOUI\FieldsetLayout( [ 989 'label' => $this->msg( 'undelete-fieldset-title' )->text(), 990 'id' => 'mw-undelete-table', 991 'items' => $fields, 992 ] ); 993 994 $form->appendContent( 995 new OOUI\PanelLayout( [ 996 'expanded' => false, 997 'padded' => true, 998 'framed' => true, 999 'content' => $fieldset, 1000 ] ), 1001 new OOUI\HtmlSnippet( 1002 Html::hidden( 'target', $this->mTarget ) . 1003 Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ) 1004 ) 1005 ); 1006 } 1007 1008 $history = ''; 1009 $history .= Xml::element( 'h2', null, $this->msg( 'history' )->text() ) . "\n"; 1010 1011 if ( $haveRevisions ) { 1012 # Show the page's stored (deleted) history 1013 1014 if ( $this->permissionManager->userHasRight( $this->getUser(), 'deleterevision' ) ) { 1015 $history .= Html::element( 1016 'button', 1017 [ 1018 'name' => 'revdel', 1019 'type' => 'submit', 1020 'class' => 'deleterevision-log-submit mw-log-deleterevision-button' 1021 ], 1022 $this->msg( 'showhideselectedversions' )->text() 1023 ) . "\n"; 1024 } 1025 1026 $history .= '<ul class="mw-undelete-revlist">'; 1027 $remaining = $revisions->numRows(); 1028 $firstRev = $this->revisionStore->getFirstRevision( $this->mTargetObj ); 1029 $earliestLiveTime = $firstRev ? $firstRev->getTimestamp() : null; 1030 1031 foreach ( $revisions as $row ) { 1032 $remaining--; 1033 $history .= $this->formatRevisionRow( $row, $earliestLiveTime, $remaining ); 1034 } 1035 $revisions->free(); 1036 $history .= '</ul>'; 1037 } else { 1038 $out->addWikiMsg( 'nohistory' ); 1039 } 1040 1041 if ( $haveFiles ) { 1042 $history .= Xml::element( 'h2', null, $this->msg( 'filehist' )->text() ) . "\n"; 1043 $history .= '<ul class="mw-undelete-revlist">'; 1044 foreach ( $files as $row ) { 1045 $history .= $this->formatFileRow( $row ); 1046 } 1047 $files->free(); 1048 $history .= '</ul>'; 1049 } 1050 1051 if ( $this->mAllowed ) { 1052 # Slip in the hidden controls here 1053 $misc = Html::hidden( 'target', $this->mTarget ); 1054 $misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ); 1055 $history .= $misc; 1056 1057 $form->appendContent( new OOUI\HtmlSnippet( $history ) ); 1058 $out->addHTML( $form ); 1059 } else { 1060 $out->addHTML( $history ); 1061 } 1062 1063 return true; 1064 } 1065 1066 protected function formatRevisionRow( $row, $earliestLiveTime, $remaining ) { 1067 $revRecord = $this->revisionStore->newRevisionFromArchiveRow( 1068 $row, 1069 RevisionStore::READ_NORMAL, 1070 $this->mTargetObj 1071 ); 1072 1073 $revTextSize = ''; 1074 $ts = wfTimestamp( TS_MW, $row->ar_timestamp ); 1075 // Build checkboxen... 1076 if ( $this->mAllowed ) { 1077 if ( $this->mInvert ) { 1078 if ( in_array( $ts, $this->mTargetTimestamp ) ) { 1079 $checkBox = Xml::check( "ts$ts" ); 1080 } else { 1081 $checkBox = Xml::check( "ts$ts", true ); 1082 } 1083 } else { 1084 $checkBox = Xml::check( "ts$ts" ); 1085 } 1086 } else { 1087 $checkBox = ''; 1088 } 1089 1090 // Build page & diff links... 1091 $user = $this->getUser(); 1092 if ( $this->mCanView ) { 1093 $titleObj = $this->getPageTitle(); 1094 # Last link 1095 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) { 1096 $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) ); 1097 $last = $this->msg( 'diff' )->escaped(); 1098 } elseif ( $remaining > 0 || ( $earliestLiveTime && $ts > $earliestLiveTime ) ) { 1099 $pageLink = $this->getPageLink( $revRecord, $titleObj, $ts ); 1100 $last = $this->getLinkRenderer()->makeKnownLink( 1101 $titleObj, 1102 $this->msg( 'diff' )->text(), 1103 [], 1104 [ 1105 'target' => $this->mTargetObj->getPrefixedText(), 1106 'timestamp' => $ts, 1107 'diff' => 'prev' 1108 ] 1109 ); 1110 } else { 1111 $pageLink = $this->getPageLink( $revRecord, $titleObj, $ts ); 1112 $last = $this->msg( 'diff' )->escaped(); 1113 } 1114 } else { 1115 $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) ); 1116 $last = $this->msg( 'diff' )->escaped(); 1117 } 1118 1119 // User links 1120 $userLink = Linker::revUserTools( $revRecord ); 1121 1122 // Minor edit 1123 $minor = $revRecord->isMinor() ? ChangesList::flag( 'minor' ) : ''; 1124 1125 // Revision text size 1126 $size = $row->ar_len; 1127 if ( $size !== null ) { 1128 $revTextSize = Linker::formatRevisionSize( $size ); 1129 } 1130 1131 // Edit summary 1132 $comment = Linker::revComment( $revRecord ); 1133 1134 // Tags 1135 $attribs = []; 1136 list( $tagSummary, $classes ) = ChangeTags::formatSummaryRow( 1137 $row->ts_tags, 1138 'deletedhistory', 1139 $this->getContext() 1140 ); 1141 if ( $classes ) { 1142 $attribs['class'] = implode( ' ', $classes ); 1143 } 1144 1145 $revisionRow = $this->msg( 'undelete-revision-row2' ) 1146 ->rawParams( 1147 $checkBox, 1148 $last, 1149 $pageLink, 1150 $userLink, 1151 $minor, 1152 $revTextSize, 1153 $comment, 1154 $tagSummary 1155 ) 1156 ->escaped(); 1157 1158 return Xml::tags( 'li', $attribs, $revisionRow ) . "\n"; 1159 } 1160 1161 private function formatFileRow( $row ) { 1162 $file = ArchivedFile::newFromRow( $row ); 1163 $ts = wfTimestamp( TS_MW, $row->fa_timestamp ); 1164 $user = $this->getUser(); 1165 1166 $checkBox = ''; 1167 if ( $this->mCanView && $row->fa_storage_key ) { 1168 if ( $this->mAllowed ) { 1169 $checkBox = Xml::check( 'fileid' . $row->fa_id ); 1170 } 1171 $key = urlencode( $row->fa_storage_key ); 1172 $pageLink = $this->getFileLink( $file, $this->getPageTitle(), $ts, $key ); 1173 } else { 1174 $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) ); 1175 } 1176 $userLink = $this->getFileUser( $file ); 1177 $data = $this->msg( 'widthheight' )->numParams( $row->fa_width, $row->fa_height )->text(); 1178 $bytes = $this->msg( 'parentheses' ) 1179 ->plaintextParams( $this->msg( 'nbytes' )->numParams( $row->fa_size )->text() ) 1180 ->plain(); 1181 $data = htmlspecialchars( $data . ' ' . $bytes ); 1182 $comment = $this->getFileComment( $file ); 1183 1184 // Add show/hide deletion links if available 1185 $canHide = $this->isAllowed( 'deleterevision' ); 1186 if ( $canHide || ( $file->getVisibility() && $this->isAllowed( 'deletedhistory' ) ) ) { 1187 if ( !$file->userCan( File::DELETED_RESTRICTED, $user ) ) { 1188 // Revision was hidden from sysops 1189 $revdlink = Linker::revDeleteLinkDisabled( $canHide ); 1190 } else { 1191 $query = [ 1192 'type' => 'filearchive', 1193 'target' => $this->mTargetObj->getPrefixedDBkey(), 1194 'ids' => $row->fa_id 1195 ]; 1196 $revdlink = Linker::revDeleteLink( $query, 1197 $file->isDeleted( File::DELETED_RESTRICTED ), $canHide ); 1198 } 1199 } else { 1200 $revdlink = ''; 1201 } 1202 1203 return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n"; 1204 } 1205 1206 /** 1207 * Fetch revision text link if it's available to all users 1208 * 1209 * @param RevisionRecord $revRecord 1210 * @param Title $titleObj 1211 * @param string $ts Timestamp 1212 * @return string 1213 */ 1214 private function getPageLink( RevisionRecord $revRecord, $titleObj, $ts ) { 1215 $user = $this->getUser(); 1216 $time = $this->getLanguage()->userTimeAndDate( $ts, $user ); 1217 1218 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) { 1219 // TODO The condition cannot be true when the function is called 1220 return '<span class="history-deleted">' . htmlspecialchars( $time ) . '</span>'; 1221 } 1222 1223 $link = $this->getLinkRenderer()->makeKnownLink( 1224 $titleObj, 1225 $time, 1226 [], 1227 [ 1228 'target' => $this->mTargetObj->getPrefixedText(), 1229 'timestamp' => $ts 1230 ] 1231 ); 1232 1233 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) { 1234 $link = '<span class="history-deleted">' . $link . '</span>'; 1235 } 1236 1237 return $link; 1238 } 1239 1240 /** 1241 * Fetch image view link if it's available to all users 1242 * 1243 * @param File|ArchivedFile $file 1244 * @param Title $titleObj 1245 * @param string $ts A timestamp 1246 * @param string $key A storage key 1247 * 1248 * @return string HTML fragment 1249 */ 1250 private function getFileLink( $file, $titleObj, $ts, $key ) { 1251 $user = $this->getUser(); 1252 $time = $this->getLanguage()->userTimeAndDate( $ts, $user ); 1253 1254 if ( !$file->userCan( File::DELETED_FILE, $user ) ) { 1255 return '<span class="history-deleted">' . htmlspecialchars( $time ) . '</span>'; 1256 } 1257 1258 $link = $this->getLinkRenderer()->makeKnownLink( 1259 $titleObj, 1260 $time, 1261 [], 1262 [ 1263 'target' => $this->mTargetObj->getPrefixedText(), 1264 'file' => $key, 1265 'token' => $user->getEditToken( $key ) 1266 ] 1267 ); 1268 1269 if ( $file->isDeleted( File::DELETED_FILE ) ) { 1270 $link = '<span class="history-deleted">' . $link . '</span>'; 1271 } 1272 1273 return $link; 1274 } 1275 1276 /** 1277 * Fetch file's user id if it's available to this user 1278 * 1279 * @param File|ArchivedFile $file 1280 * @return string HTML fragment 1281 */ 1282 private function getFileUser( $file ) { 1283 if ( !$file->userCan( File::DELETED_USER, $this->getUser() ) ) { 1284 return '<span class="history-deleted">' . 1285 $this->msg( 'rev-deleted-user' )->escaped() . 1286 '</span>'; 1287 } 1288 1289 $link = Linker::userLink( $file->getRawUser(), $file->getRawUserText() ) . 1290 Linker::userToolLinks( $file->getRawUser(), $file->getRawUserText() ); 1291 1292 if ( $file->isDeleted( File::DELETED_USER ) ) { 1293 $link = '<span class="history-deleted">' . $link . '</span>'; 1294 } 1295 1296 return $link; 1297 } 1298 1299 /** 1300 * Fetch file upload comment if it's available to this user 1301 * 1302 * @param File|ArchivedFile $file 1303 * @return string HTML fragment 1304 */ 1305 private function getFileComment( $file ) { 1306 if ( !$file->userCan( File::DELETED_COMMENT, $this->getUser() ) ) { 1307 return '<span class="history-deleted"><span class="comment">' . 1308 $this->msg( 'rev-deleted-comment' )->escaped() . '</span></span>'; 1309 } 1310 1311 $link = Linker::commentBlock( $file->getRawDescription() ); 1312 1313 if ( $file->isDeleted( File::DELETED_COMMENT ) ) { 1314 $link = '<span class="history-deleted">' . $link . '</span>'; 1315 } 1316 1317 return $link; 1318 } 1319 1320 private function undelete() { 1321 if ( $this->getConfig()->get( 'UploadMaintenance' ) 1322 && $this->mTargetObj->getNamespace() === NS_FILE 1323 ) { 1324 throw new ErrorPageError( 'undelete-error', 'filedelete-maintenance' ); 1325 } 1326 1327 $this->checkReadOnly(); 1328 1329 $out = $this->getOutput(); 1330 $archive = new PageArchive( $this->mTargetObj, $this->getConfig() ); 1331 $this->getHookRunner()->onUndeleteForm__undelete( $archive, $this->mTargetObj ); 1332 $ok = $archive->undeleteAsUser( 1333 $this->mTargetTimestamp, 1334 $this->getUser(), 1335 $this->mComment, 1336 $this->mFileVersions, 1337 $this->mUnsuppress 1338 ); 1339 1340 if ( is_array( $ok ) ) { 1341 if ( $ok[1] ) { // Undeleted file count 1342 $this->getHookRunner()->onFileUndeleteComplete( 1343 $this->mTargetObj, $this->mFileVersions, $this->getUser(), $this->mComment ); 1344 } 1345 1346 $link = $this->getLinkRenderer()->makeKnownLink( $this->mTargetObj ); 1347 $out->addWikiMsg( 'undeletedpage', Message::rawParam( $link ) ); 1348 } else { 1349 $out->setPageTitle( $this->msg( 'undelete-error' ) ); 1350 } 1351 1352 // Show revision undeletion warnings and errors 1353 $status = $archive->getRevisionStatus(); 1354 if ( $status && !$status->isGood() ) { 1355 $out->wrapWikiTextAsInterface( 1356 'error', 1357 '<div id="mw-error-cannotundelete">' . 1358 $status->getWikiText( 1359 'cannotundelete', 1360 'cannotundelete', 1361 $this->getLanguage() 1362 ) . '</div>' 1363 ); 1364 } 1365 1366 // Show file undeletion warnings and errors 1367 $status = $archive->getFileStatus(); 1368 if ( $status && !$status->isGood() ) { 1369 $out->wrapWikiTextAsInterface( 1370 'error', 1371 $status->getWikiText( 1372 'undelete-error-short', 1373 'undelete-error-long', 1374 $this->getLanguage() 1375 ) 1376 ); 1377 } 1378 } 1379 1380 /** 1381 * Return an array of subpages beginning with $search that this special page will accept. 1382 * 1383 * @param string $search Prefix to search for 1384 * @param int $limit Maximum number of results to return (usually 10) 1385 * @param int $offset Number of results to skip (usually 0) 1386 * @return string[] Matching subpages 1387 */ 1388 public function prefixSearchSubpages( $search, $limit, $offset ) { 1389 return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory ); 1390 } 1391 1392 protected function getGroupName() { 1393 return 'pagetools'; 1394 } 1395} 1396