MediaWiki 1.43.1
SpecialUndelete.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Specials;
22
23use ArchivedFile;
24use ChangesList;
25use ChangeTags;
27use File;
28use LocalRepo;
30use LogPage;
63use OOUI\ActionFieldLayout;
64use OOUI\ButtonInputWidget;
65use OOUI\CheckboxInputWidget;
66use OOUI\DropdownInputWidget;
67use OOUI\FieldLayout;
68use OOUI\FieldsetLayout;
69use OOUI\FormLayout;
70use OOUI\HorizontalLayout;
71use OOUI\HtmlSnippet;
72use OOUI\Layout;
73use OOUI\PanelLayout;
74use OOUI\TextInputWidget;
75use OOUI\Widget;
76use PageArchive;
78use RepoGroup;
84
92
97 private const REVISION_HISTORY_LIMIT = 500;
98
100 private $mAction;
102 private $mTarget;
104 private $mTimestamp;
106 private $mRestore;
108 private $mRevdel;
110 private $mInvert;
112 private $mFilename;
114 private $mTargetTimestamp = [];
116 private $mAllowed;
118 private $mCanView;
120 private $mComment = '';
122 private $mToken;
124 private $mPreview;
126 private $mDiff;
128 private $mDiffOnly;
130 private $mUnsuppress;
132 private $mFileVersions = [];
134 private $mUndeleteTalk;
136 private $mHistoryOffset;
137
139 private $mTargetObj;
143 private $mSearchPrefix;
144
145 private PermissionManager $permissionManager;
146 private RevisionStore $revisionStore;
147 private RevisionRenderer $revisionRenderer;
148 private IContentHandlerFactory $contentHandlerFactory;
149 private NameTableStore $changeTagDefStore;
150 private LinkBatchFactory $linkBatchFactory;
151 private LocalRepo $localRepo;
152 private IConnectionProvider $dbProvider;
153 private UserOptionsLookup $userOptionsLookup;
154 private WikiPageFactory $wikiPageFactory;
155 private SearchEngineFactory $searchEngineFactory;
156 private UndeletePageFactory $undeletePageFactory;
157 private ArchivedRevisionLookup $archivedRevisionLookup;
158 private CommentFormatter $commentFormatter;
159 private WatchlistManager $watchlistManager;
160
178 public function __construct(
179 PermissionManager $permissionManager,
180 RevisionStore $revisionStore,
181 RevisionRenderer $revisionRenderer,
182 IContentHandlerFactory $contentHandlerFactory,
183 NameTableStore $changeTagDefStore,
184 LinkBatchFactory $linkBatchFactory,
185 RepoGroup $repoGroup,
186 IConnectionProvider $dbProvider,
187 UserOptionsLookup $userOptionsLookup,
188 WikiPageFactory $wikiPageFactory,
189 SearchEngineFactory $searchEngineFactory,
190 UndeletePageFactory $undeletePageFactory,
191 ArchivedRevisionLookup $archivedRevisionLookup,
192 CommentFormatter $commentFormatter,
193 WatchlistManager $watchlistManager
194 ) {
195 parent::__construct( 'Undelete', 'deletedhistory' );
196 $this->permissionManager = $permissionManager;
197 $this->revisionStore = $revisionStore;
198 $this->revisionRenderer = $revisionRenderer;
199 $this->contentHandlerFactory = $contentHandlerFactory;
200 $this->changeTagDefStore = $changeTagDefStore;
201 $this->linkBatchFactory = $linkBatchFactory;
202 $this->localRepo = $repoGroup->getLocalRepo();
203 $this->dbProvider = $dbProvider;
204 $this->userOptionsLookup = $userOptionsLookup;
205 $this->wikiPageFactory = $wikiPageFactory;
206 $this->searchEngineFactory = $searchEngineFactory;
207 $this->undeletePageFactory = $undeletePageFactory;
208 $this->archivedRevisionLookup = $archivedRevisionLookup;
209 $this->commentFormatter = $commentFormatter;
210 $this->watchlistManager = $watchlistManager;
211 }
212
213 public function doesWrites() {
214 return true;
215 }
216
217 private function loadRequest( $par ) {
218 $request = $this->getRequest();
219 $user = $this->getUser();
220
221 $this->mAction = $request->getRawVal( 'action' );
222 if ( $par !== null && $par !== '' ) {
223 $this->mTarget = $par;
224 } else {
225 $this->mTarget = $request->getVal( 'target' );
226 }
227
228 $this->mTargetObj = null;
229
230 if ( $this->mTarget !== null && $this->mTarget !== '' ) {
231 $this->mTargetObj = Title::newFromText( $this->mTarget );
232 }
233
234 $this->mSearchPrefix = $request->getText( 'prefix' );
235 $time = $request->getVal( 'timestamp' );
236 $this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : '';
237 $this->mFilename = $request->getVal( 'file' );
238
239 $posted = $request->wasPosted() &&
240 $user->matchEditToken( $request->getVal( 'wpEditToken' ) );
241 $this->mRestore = $request->getCheck( 'restore' ) && $posted;
242 $this->mRevdel = $request->getCheck( 'revdel' ) && $posted;
243 $this->mInvert = $request->getCheck( 'invert' ) && $posted;
244 $this->mPreview = $request->getCheck( 'preview' ) && $posted;
245 $this->mDiff = $request->getCheck( 'diff' );
246 $this->mDiffOnly = $request->getBool( 'diffonly',
247 $this->userOptionsLookup->getOption( $this->getUser(), 'diffonly' ) );
248 $commentList = $request->getText( 'wpCommentList', 'other' );
249 $comment = $request->getText( 'wpComment' );
250 if ( $commentList === 'other' ) {
251 $this->mComment = $comment;
252 } elseif ( $comment !== '' ) {
253 $this->mComment = $commentList . $this->msg( 'colon-separator' )->inContentLanguage()->text() . $comment;
254 } else {
255 $this->mComment = $commentList;
256 }
257 $this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) &&
258 $this->permissionManager->userHasRight( $user, 'suppressrevision' );
259 $this->mToken = $request->getVal( 'token' );
260 $this->mUndeleteTalk = $request->getCheck( 'undeletetalk' );
261 $this->mHistoryOffset = $request->getVal( 'historyoffset' );
262
263 if ( $this->isAllowed( 'undelete' ) ) {
264 $this->mAllowed = true; // user can restore
265 $this->mCanView = true; // user can view content
266 } elseif ( $this->isAllowed( 'deletedtext' ) ) {
267 $this->mAllowed = false; // user cannot restore
268 $this->mCanView = true; // user can view content
269 $this->mRestore = false;
270 } else { // user can only view the list of revisions
271 $this->mAllowed = false;
272 $this->mCanView = false;
273 $this->mTimestamp = '';
274 $this->mRestore = false;
275 }
276
277 if ( $this->mRestore || $this->mInvert ) {
278 $timestamps = [];
279 $this->mFileVersions = [];
280 foreach ( $request->getValues() as $key => $val ) {
281 $matches = [];
282 if ( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) {
283 $timestamps[] = $matches[1];
284 }
285
286 if ( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) {
287 $this->mFileVersions[] = intval( $matches[1] );
288 }
289 }
290 rsort( $timestamps );
291 $this->mTargetTimestamp = $timestamps;
292 }
293 }
294
303 protected function isAllowed( $permission, ?User $user = null ) {
304 $user ??= $this->getUser();
305 $block = $user->getBlock();
306
307 if ( $this->mTargetObj !== null ) {
308 return $this->permissionManager->userCan( $permission, $user, $this->mTargetObj );
309 } else {
310 $hasRight = $this->permissionManager->userHasRight( $user, $permission );
311 $sitewideBlock = $block && $block->isSitewide();
312 return $permission === 'undelete' ? ( $hasRight && !$sitewideBlock ) : $hasRight;
313 }
314 }
315
316 public function userCanExecute( User $user ) {
317 return $this->isAllowed( $this->mRestriction, $user );
318 }
319
323 public function checkPermissions() {
324 $user = $this->getUser();
325
326 // First check if user has the right to use this page. If not,
327 // show a permissions error whether they are blocked or not.
328 if ( !parent::userCanExecute( $user ) ) {
330 }
331
332 // If a user has the right to use this page, but is blocked from
333 // the target, show a block error.
334 if (
335 $this->mTargetObj && $this->permissionManager->isBlockedFrom( $user, $this->mTargetObj ) ) {
336 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
337 throw new UserBlockedError( $user->getBlock() );
338 }
339
340 // Finally, do the comprehensive permission check via isAllowed.
341 if ( !$this->userCanExecute( $user ) ) {
343 }
344 }
345
346 public function execute( $par ) {
348
349 $user = $this->getUser();
350
351 $this->setHeaders();
352 $this->outputHeader();
353 $this->addHelpLink( 'Help:Deletion_and_undeletion' );
354
355 $this->loadRequest( $par );
356 $this->checkPermissions(); // Needs to be after mTargetObj is set
357
358 $out = $this->getOutput();
359
360 if ( $this->mTargetObj === null ) {
361 $out->addWikiMsg( 'undelete-header' );
362
363 # Not all users can just browse every deleted page from the list
364 if ( $this->permissionManager->userHasRight( $user, 'browsearchive' ) ) {
365 $this->showSearchForm();
366 }
367
368 return;
369 }
370
371 $this->addHelpLink( 'Help:Undelete' );
372 if ( $this->mAllowed ) {
373 $out->setPageTitleMsg( $this->msg( 'undeletepage' ) );
374 } else {
375 $out->setPageTitleMsg( $this->msg( 'viewdeletedpage' ) );
376 }
377
378 $this->getSkin()->setRelevantTitle( $this->mTargetObj );
379
380 if ( $this->mTimestamp !== '' ) {
381 $this->showRevision( $this->mTimestamp );
382 } elseif ( $this->mFilename !== null && $this->mTargetObj->inNamespace( NS_FILE ) ) {
383 $file = new ArchivedFile( $this->mTargetObj, 0, $this->mFilename );
384 // Check if user is allowed to see this file
385 if ( !$file->exists() ) {
386 $out->addWikiMsg( 'filedelete-nofile', $this->mFilename );
387 } elseif ( !$file->userCan( File::DELETED_FILE, $user ) ) {
388 if ( $file->isDeleted( File::DELETED_RESTRICTED ) ) {
389 throw new PermissionsError( 'suppressrevision' );
390 } else {
391 throw new PermissionsError( 'deletedtext' );
392 }
393 } elseif ( !$user->matchEditToken( $this->mToken, $this->mFilename ) ) {
394 $this->showFileConfirmationForm( $this->mFilename );
395 } else {
396 $this->showFile( $this->mFilename );
397 }
398 } elseif ( $this->mAction === 'submit' ) {
399 if ( $this->mRestore ) {
400 $this->undelete();
401 } elseif ( $this->mRevdel ) {
402 $this->redirectToRevDel();
403 }
404 } elseif ( $this->mAction === 'render' ) {
405 $this->showMoreHistory();
406 } else {
407 $this->showHistory();
408 }
409 }
410
415 private function redirectToRevDel() {
416 $revisions = [];
417
418 foreach ( $this->getRequest()->getValues() as $key => $val ) {
419 $matches = [];
420 if ( preg_match( "/^ts(\d{14})$/", $key, $matches ) ) {
421 $revisionRecord = $this->archivedRevisionLookup
422 ->getRevisionRecordByTimestamp( $this->mTargetObj, $matches[1] );
423 if ( $revisionRecord ) {
424 // Can return null
425 $revisions[ $revisionRecord->getId() ] = 1;
426 }
427 }
428 }
429
430 $query = [
431 'type' => 'revision',
432 'ids' => $revisions,
433 'target' => $this->mTargetObj->getPrefixedText()
434 ];
435 $url = SpecialPage::getTitleFor( 'Revisiondelete' )->getFullURL( $query );
436 $this->getOutput()->redirect( $url );
437 }
438
439 private function showSearchForm() {
440 $out = $this->getOutput();
441 $out->setPageTitleMsg( $this->msg( 'undelete-search-title' ) );
442 $fuzzySearch = $this->getRequest()->getVal( 'fuzzy', '1' );
443
444 $out->enableOOUI();
445
446 $fields = [];
447 $fields[] = new ActionFieldLayout(
448 new TextInputWidget( [
449 'name' => 'prefix',
450 'inputId' => 'prefix',
451 'infusable' => true,
452 'value' => $this->mSearchPrefix,
453 'autofocus' => true,
454 ] ),
455 new ButtonInputWidget( [
456 'label' => $this->msg( 'undelete-search-submit' )->text(),
457 'flags' => [ 'primary', 'progressive' ],
458 'inputId' => 'searchUndelete',
459 'type' => 'submit',
460 ] ),
461 [
462 'label' => new HtmlSnippet(
463 $this->msg(
464 $fuzzySearch ? 'undelete-search-full' : 'undelete-search-prefix'
465 )->parse()
466 ),
467 'align' => 'left',
468 ]
469 );
470
471 $fieldset = new FieldsetLayout( [
472 'label' => $this->msg( 'undelete-search-box' )->text(),
473 'items' => $fields,
474 ] );
475
476 $form = new FormLayout( [
477 'method' => 'get',
478 'action' => wfScript(),
479 ] );
480
481 $form->appendContent(
482 $fieldset,
483 new HtmlSnippet(
484 Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) .
485 Html::hidden( 'fuzzy', $fuzzySearch )
486 )
487 );
488
489 $out->addHTML(
490 new PanelLayout( [
491 'expanded' => false,
492 'padded' => true,
493 'framed' => true,
494 'content' => $form,
495 ] )
496 );
497
498 # List undeletable articles
499 if ( $this->mSearchPrefix ) {
500 // For now, we enable search engine match only when specifically asked to
501 // by using fuzzy=1 parameter.
502 if ( $fuzzySearch ) {
503 $result = PageArchive::listPagesBySearch( $this->mSearchPrefix );
504 } else {
505 $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix );
506 }
507 $this->showList( $result );
508 }
509 }
510
517 private function showList( $result ) {
518 $out = $this->getOutput();
519
520 if ( $result->numRows() == 0 ) {
521 $out->addWikiMsg( 'undelete-no-results' );
522
523 return false;
524 }
525
526 $out->addWikiMsg( 'undeletepagetext', $this->getLanguage()->formatNum( $result->numRows() ) );
527
528 $linkRenderer = $this->getLinkRenderer();
529 $undelete = $this->getPageTitle();
530 $out->addHTML( "<ul id='undeleteResultsList'>\n" );
531 foreach ( $result as $row ) {
532 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
533 if ( $title !== null ) {
534 $item = $linkRenderer->makeKnownLink(
535 $undelete,
536 $title->getPrefixedText(),
537 [],
538 [ 'target' => $title->getPrefixedText() ]
539 );
540 } else {
541 // The title is no longer valid, show as text
542 $item = Html::element(
543 'span',
544 [ 'class' => 'mw-invalidtitle' ],
545 Linker::getInvalidTitleDescription(
546 $this->getContext(),
547 $row->ar_namespace,
548 $row->ar_title
549 )
550 );
551 }
552 $revs = $this->msg( 'undeleterevisions' )->numParams( $row->count )->parse();
553 $out->addHTML(
554 Html::rawElement(
555 'li',
556 [ 'class' => 'undeleteResult' ],
557 $item . $this->msg( 'word-separator' )->escaped() .
558 $this->msg( 'parentheses' )->rawParams( $revs )->escaped()
559 )
560 );
561 }
562 $result->free();
563 $out->addHTML( "</ul>\n" );
564
565 return true;
566 }
567
568 private function showRevision( $timestamp ) {
569 if ( !preg_match( '/[0-9]{14}/', $timestamp ) ) {
570 return;
571 }
572 $out = $this->getOutput();
573 $out->addModuleStyles( 'mediawiki.interface.helpers.styles' );
574
575 // When viewing a specific revision, add a subtitle link back to the overall
576 // history, see T284114
577 $listLink = $this->getLinkRenderer()->makeKnownLink(
578 $this->getPageTitle(),
579 $this->msg( 'undelete-back-to-list' )->text(),
580 [],
581 [ 'target' => $this->mTargetObj->getPrefixedText() ]
582 );
583 // same < arrow as with subpages
584 $subtitle = "&lt; $listLink";
585 $out->setSubtitle( $subtitle );
586
587 $archive = new PageArchive( $this->mTargetObj );
588 // FIXME: This hook must be deprecated, passing PageArchive by ref is awful.
589 if ( !$this->getHookRunner()->onUndeleteForm__showRevision(
590 $archive, $this->mTargetObj )
591 ) {
592 return;
593 }
594 $revRecord = $this->archivedRevisionLookup->getRevisionRecordByTimestamp( $this->mTargetObj, $timestamp );
595
596 $user = $this->getUser();
597
598 if ( !$revRecord ) {
599 $out->addWikiMsg( 'undeleterevision-missing' );
600 return;
601 }
602
603 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
604 // Used in wikilinks, should not contain whitespaces
605 $titleText = $this->mTargetObj->getPrefixedDBkey();
606 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
607 $msg = $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
608 ? [ 'rev-suppressed-text-permission', $titleText ]
609 : [ 'rev-deleted-text-permission', $titleText ];
610 $out->addHTML(
611 Html::warningBox(
612 $this->msg( $msg[0], $msg[1] )->parse(),
613 'plainlinks'
614 )
615 );
616 return;
617 }
618
619 $msg = $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
620 ? [ 'rev-suppressed-text-view', $titleText ]
621 : [ 'rev-deleted-text-view', $titleText ];
622 $out->addHTML(
623 Html::warningBox(
624 $this->msg( $msg[0], $msg[1] )->parse(),
625 'plainlinks'
626 )
627 );
628 // and we are allowed to see...
629 }
630
631 if ( $this->mDiff ) {
632 $previousRevRecord = $this->archivedRevisionLookup
633 ->getPreviousRevisionRecord( $this->mTargetObj, $timestamp );
634 if ( $previousRevRecord ) {
635 $this->showDiff( $previousRevRecord, $revRecord );
636 if ( $this->mDiffOnly ) {
637 return;
638 }
639
640 $out->addHTML( '<hr />' );
641 } else {
642 $out->addWikiMsg( 'undelete-nodiff' );
643 }
644 }
645
646 $link = $this->getLinkRenderer()->makeKnownLink(
647 $this->getPageTitle( $this->mTargetObj->getPrefixedDBkey() ),
648 $this->mTargetObj->getPrefixedText()
649 );
650
651 $lang = $this->getLanguage();
652
653 // date and time are separate parameters to facilitate localisation.
654 // $time is kept for backward compat reasons.
655 $time = $lang->userTimeAndDate( $timestamp, $user );
656 $d = $lang->userDate( $timestamp, $user );
657 $t = $lang->userTime( $timestamp, $user );
658 $userLink = Linker::revUserTools( $revRecord );
659
660 try {
661 $content = $revRecord->getContent(
662 SlotRecord::MAIN,
663 RevisionRecord::FOR_THIS_USER,
664 $user
665 );
666 } catch ( RevisionAccessException $e ) {
667 $content = null;
668 }
669
670 // TODO: MCR: this will have to become something like $hasTextSlots and $hasNonTextSlots
671 $isText = ( $content instanceof TextContent );
672
673 $undeleteRevisionContent = '';
674 // Revision delete links
675 if ( !$this->mDiff ) {
676 $revdel = Linker::getRevDeleteLink(
677 $user,
678 $revRecord,
679 $this->mTargetObj
680 );
681 if ( $revdel ) {
682 $undeleteRevisionContent = $revdel . ' ';
683 }
684 }
685
686 $undeleteRevisionContent .= $out->msg(
687 'undelete-revision',
688 Message::rawParam( $link ),
689 $time,
690 Message::rawParam( $userLink ),
691 $d,
692 $t
693 )->parseAsBlock();
694
695 if ( $this->mPreview || $isText ) {
696 $out->addHTML(
697 Html::warningBox(
698 $undeleteRevisionContent,
699 'mw-undelete-revision'
700 )
701 );
702 } else {
703 $out->addHTML(
704 Html::rawElement(
705 'div',
706 [ 'class' => 'mw-undelete-revision', ],
707 $undeleteRevisionContent
708 )
709 );
710 }
711
712 if ( $this->mPreview || !$isText ) {
713 // NOTE: non-text content has no source view, so always use rendered preview
714
715 $popts = $out->parserOptions();
716
717 try {
718 $rendered = $this->revisionRenderer->getRenderedRevision(
719 $revRecord,
720 $popts,
721 $user,
722 [ 'audience' => RevisionRecord::FOR_THIS_USER, 'causeAction' => 'undelete-preview' ]
723 );
724
725 // Fail hard if the audience check fails, since we already checked
726 // at the beginning of this method.
727 $pout = $rendered->getRevisionParserOutput();
728
729 $out->addParserOutput( $pout, [
730 'enableSectionEditLinks' => false,
731 ] );
732 } catch ( RevisionAccessException $e ) {
733 }
734 }
735
736 $out->enableOOUI();
737 $buttonFields = [];
738
739 if ( $isText ) {
740 '@phan-var TextContent $content';
741 // TODO: MCR: make this work for multiple slots
742 // source view for textual content
743 $sourceView = Xml::element( 'textarea', [
744 'readonly' => 'readonly',
745 'cols' => 80,
746 'rows' => 25
747 ], $content->getText() . "\n" );
748
749 $buttonFields[] = new ButtonInputWidget( [
750 'type' => 'submit',
751 'name' => 'preview',
752 'label' => $this->msg( 'showpreview' )->text()
753 ] );
754 } else {
755 $sourceView = '';
756 }
757
758 $buttonFields[] = new ButtonInputWidget( [
759 'name' => 'diff',
760 'type' => 'submit',
761 'label' => $this->msg( 'showdiff' )->text()
762 ] );
763
764 $out->addHTML(
765 $sourceView .
766 Xml::openElement( 'div', [
767 'style' => 'clear: both' ] ) .
768 Xml::openElement( 'form', [
769 'method' => 'post',
770 'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ) ] ) .
771 Xml::element( 'input', [
772 'type' => 'hidden',
773 'name' => 'target',
774 'value' => $this->mTargetObj->getPrefixedDBkey() ] ) .
775 Xml::element( 'input', [
776 'type' => 'hidden',
777 'name' => 'timestamp',
778 'value' => $timestamp ] ) .
779 Xml::element( 'input', [
780 'type' => 'hidden',
781 'name' => 'wpEditToken',
782 'value' => $user->getEditToken() ] ) .
783 new FieldLayout(
784 new Widget( [
785 'content' => new HorizontalLayout( [
786 'items' => $buttonFields
787 ] )
788 ] )
789 ) .
790 Xml::closeElement( 'form' ) .
791 Xml::closeElement( 'div' )
792 );
793 }
794
802 private function showDiff(
803 RevisionRecord $previousRevRecord,
804 RevisionRecord $currentRevRecord
805 ) {
806 $currentTitle = Title::newFromLinkTarget( $currentRevRecord->getPageAsLinkTarget() );
807
808 $diffContext = new DerivativeContext( $this->getContext() );
809 $diffContext->setTitle( $currentTitle );
810 $diffContext->setWikiPage( $this->wikiPageFactory->newFromTitle( $currentTitle ) );
811
812 $contentModel = $currentRevRecord->getSlot(
813 SlotRecord::MAIN,
814 RevisionRecord::RAW
815 )->getModel();
816
817 $diffEngine = $this->contentHandlerFactory->getContentHandler( $contentModel )
818 ->createDifferenceEngine( $diffContext );
819
820 $diffEngine->setRevisions( $previousRevRecord, $currentRevRecord );
821 $diffEngine->showDiffStyle();
822 $formattedDiff = $diffEngine->getDiff(
823 $this->diffHeader( $previousRevRecord, 'o' ),
824 $this->diffHeader( $currentRevRecord, 'n' )
825 );
826
827 $this->getOutput()->addHTML( "<div>$formattedDiff</div>\n" );
828 }
829
835 private function diffHeader( RevisionRecord $revRecord, $prefix ) {
836 if ( $revRecord instanceof RevisionArchiveRecord ) {
837 // Revision in the archive table, only viewable via this special page
838 $targetPage = $this->getPageTitle();
839 $targetQuery = [
840 'target' => $this->mTargetObj->getPrefixedText(),
841 'timestamp' => wfTimestamp( TS_MW, $revRecord->getTimestamp() )
842 ];
843 } else {
844 // Revision in the revision table, viewable by oldid
845 $targetPage = $revRecord->getPageAsLinkTarget();
846 $targetQuery = [ 'oldid' => $revRecord->getId() ];
847 }
848
849 // Add show/hide deletion links if available
850 $user = $this->getUser();
851 $lang = $this->getLanguage();
852 $rdel = Linker::getRevDeleteLink( $user, $revRecord, $this->mTargetObj );
853
854 if ( $rdel ) {
855 $rdel = " $rdel";
856 }
857
858 $minor = $revRecord->isMinor() ? ChangesList::flag( 'minor' ) : '';
859
860 $dbr = $this->dbProvider->getReplicaDatabase();
861 $tagIds = $dbr->newSelectQueryBuilder()
862 ->select( 'ct_tag_id' )
863 ->from( 'change_tag' )
864 ->where( [ 'ct_rev_id' => $revRecord->getId() ] )
865 ->caller( __METHOD__ )->fetchFieldValues();
866 $tags = [];
867 foreach ( $tagIds as $tagId ) {
868 try {
869 $tags[] = $this->changeTagDefStore->getName( (int)$tagId );
870 } catch ( NameTableAccessException $exception ) {
871 continue;
872 }
873 }
874 $tags = implode( ',', $tags );
875 $tagSummary = ChangeTags::formatSummaryRow( $tags, 'deleteddiff', $this->getContext() );
876 $asof = $this->getLinkRenderer()->makeLink(
877 $targetPage,
878 $this->msg(
879 'revisionasof',
880 $lang->userTimeAndDate( $revRecord->getTimestamp(), $user ),
881 $lang->userDate( $revRecord->getTimestamp(), $user ),
882 $lang->userTime( $revRecord->getTimestamp(), $user )
883 )->text(),
884 [],
885 $targetQuery
886 );
887 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
888 $asof = Html::rawElement(
889 'span',
890 [ 'class' => Linker::getRevisionDeletedClass( $revRecord ) ],
891 $asof
892 );
893 }
894
895 // FIXME This is reimplementing DifferenceEngine#getRevisionHeader
896 // and partially #showDiffPage, but worse
897 return '<div id="mw-diff-' . $prefix . 'title1"><strong>' .
898 $asof .
899 '</strong></div>' .
900 '<div id="mw-diff-' . $prefix . 'title2">' .
901 Linker::revUserTools( $revRecord ) . '<br />' .
902 '</div>' .
903 '<div id="mw-diff-' . $prefix . 'title3">' .
904 $minor . $this->commentFormatter->formatRevision( $revRecord, $user ) . $rdel . '<br />' .
905 '</div>' .
906 '<div id="mw-diff-' . $prefix . 'title5">' .
907 $tagSummary[0] . '<br />' .
908 '</div>';
909 }
910
915 private function showFileConfirmationForm( $key ) {
916 $out = $this->getOutput();
917 $lang = $this->getLanguage();
918 $user = $this->getUser();
919 $file = new ArchivedFile( $this->mTargetObj, 0, $this->mFilename );
920 $out->addWikiMsg( 'undelete-show-file-confirm',
921 $this->mTargetObj->getText(),
922 $lang->userDate( $file->getTimestamp(), $user ),
923 $lang->userTime( $file->getTimestamp(), $user ) );
924 $out->addHTML(
925 Html::rawElement( 'form', [
926 'method' => 'POST',
927 'action' => $this->getPageTitle()->getLocalURL( [
928 'target' => $this->mTarget,
929 'file' => $key,
930 'token' => $user->getEditToken( $key ),
931 ] ),
932 ],
933 Xml::submitButton( $this->msg( 'undelete-show-file-submit' )->text() )
934 )
935 );
936 }
937
942 private function showFile( $key ) {
943 $this->getOutput()->disable();
944
945 # We mustn't allow the output to be CDN cached, otherwise
946 # if an admin previews a deleted image, and it's cached, then
947 # a user without appropriate permissions can toddle off and
948 # nab the image, and CDN will serve it
949 $response = $this->getRequest()->response();
950 $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
951 $response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
952
953 $path = $this->localRepo->getZonePath( 'deleted' ) . '/' . $this->localRepo->getDeletedHashPath( $key ) . $key;
954 $this->localRepo->streamFileWithStatus( $path );
955 }
956
961 private function addRevisionsToBatch( LinkBatch $batch, IResultWrapper $revisions ) {
962 foreach ( $revisions as $row ) {
963 $batch->add( NS_USER, $row->ar_user_text );
964 $batch->add( NS_USER_TALK, $row->ar_user_text );
965 }
966 }
967
972 private function addFilesToBatch( LinkBatch $batch, IResultWrapper $files ) {
973 foreach ( $files as $row ) {
974 $batch->add( NS_USER, $row->fa_user_text );
975 $batch->add( NS_USER_TALK, $row->fa_user_text );
976 }
977 }
978
982 protected function showMoreHistory() {
983 $out = $this->getOutput();
984 $out->setArticleBodyOnly( true );
985 $dbr = $this->dbProvider->getReplicaDatabase();
986 if ( $this->mHistoryOffset ) {
987 $extraConds = [ $dbr->expr( 'ar_timestamp', '<', $dbr->timestamp( $this->mHistoryOffset ) ) ];
988 } else {
989 $extraConds = [];
990 }
991 $revisions = $this->archivedRevisionLookup->listRevisions(
992 $this->mTargetObj,
993 $extraConds,
994 self::REVISION_HISTORY_LIMIT + 1
995 );
996 $batch = $this->linkBatchFactory->newLinkBatch();
997 $this->addRevisionsToBatch( $batch, $revisions );
998 $batch->execute();
999 $out->addHTML( $this->formatRevisionHistory( $revisions ) );
1000
1001 if ( $revisions->numRows() > self::REVISION_HISTORY_LIMIT ) {
1002 // Indicate to JS that the "show more" button should remain active
1003 $out->setStatusCode( 206 );
1004 }
1005 }
1006
1013 protected function formatRevisionHistory( IResultWrapper $revisions ) {
1014 $history = Html::openElement( 'ul', [ 'class' => 'mw-undelete-revlist' ] );
1015
1016 // Exclude the last data row if there is more data than history limit amount
1017 $numRevisions = $revisions->numRows();
1018 $displayCount = min( $numRevisions, self::REVISION_HISTORY_LIMIT );
1019 $firstRev = $this->revisionStore->getFirstRevision( $this->mTargetObj );
1020 $earliestLiveTime = $firstRev ? $firstRev->getTimestamp() : null;
1021
1022 $revisions->rewind();
1023 for ( $i = 0; $i < $displayCount; $i++ ) {
1024 $row = $revisions->fetchObject();
1025 // The $remaining parameter controls diff links and so must
1026 // include the undisplayed row beyond the display limit.
1027 $history .= $this->formatRevisionRow( $row, $earliestLiveTime, $numRevisions - $i );
1028 }
1029 $history .= Html::closeElement( 'ul' );
1030 return $history;
1031 }
1032
1033 protected function showHistory() {
1034 $this->checkReadOnly();
1035
1036 $out = $this->getOutput();
1037 if ( $this->mAllowed ) {
1038 $out->addModules( 'mediawiki.misc-authed-ooui' );
1039 $out->addModuleStyles( 'mediawiki.special' );
1040 }
1041 $out->addModuleStyles( 'mediawiki.interface.helpers.styles' );
1042 $out->wrapWikiMsg(
1043 "<div class='mw-undelete-pagetitle'>\n$1\n</div>\n",
1044 [ 'undeletepagetitle', wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) ]
1045 );
1046
1047 $archive = new PageArchive( $this->mTargetObj );
1048 // FIXME: This hook must be deprecated, passing PageArchive by ref is awful.
1049 $this->getHookRunner()->onUndeleteForm__showHistory( $archive, $this->mTargetObj );
1050
1051 $out->addHTML( Html::openElement( 'div', [ 'class' => 'mw-undelete-history' ] ) );
1052 if ( $this->mAllowed ) {
1053 $out->addWikiMsg( 'undeletehistory' );
1054 $out->addWikiMsg( 'undeleterevdel' );
1055 } else {
1056 $out->addWikiMsg( 'undeletehistorynoadmin' );
1057 }
1058 $out->addHTML( Html::closeElement( 'div' ) );
1059
1060 # List all stored revisions
1061 $revisions = $this->archivedRevisionLookup->listRevisions(
1062 $this->mTargetObj,
1063 [],
1064 self::REVISION_HISTORY_LIMIT + 1
1065 );
1066 $files = $archive->listFiles();
1067 $numRevisions = $revisions->numRows();
1068 $showLoadMore = $numRevisions > self::REVISION_HISTORY_LIMIT;
1069 $haveRevisions = $numRevisions > 0;
1070 $haveFiles = $files && $files->numRows() > 0;
1071
1072 # Batch existence check on user and talk pages
1073 if ( $haveRevisions || $haveFiles ) {
1074 $batch = $this->linkBatchFactory->newLinkBatch();
1075 $this->addRevisionsToBatch( $batch, $revisions );
1076 if ( $haveFiles ) {
1077 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable -- $files is non-null
1078 $this->addFilesToBatch( $batch, $files );
1079 }
1080 $batch->execute();
1081 }
1082
1083 if ( $this->mAllowed ) {
1084 $out->enableOOUI();
1085
1086 $action = $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] );
1087 # Start the form here
1088 $form = new FormLayout( [
1089 'method' => 'post',
1090 'action' => $action,
1091 'id' => 'undelete',
1092 ] );
1093 }
1094
1095 # Show relevant lines from the deletion log:
1096 $deleteLogPage = new LogPage( 'delete' );
1097 $out->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) . "\n" );
1098 LogEventsList::showLogExtract( $out, 'delete', $this->mTargetObj );
1099 # Show relevant lines from the suppression log:
1100 $suppressLogPage = new LogPage( 'suppress' );
1101 if ( $this->permissionManager->userHasRight( $this->getUser(), 'suppressionlog' ) ) {
1102 $out->addHTML( Xml::element( 'h2', null, $suppressLogPage->getName()->text() ) . "\n" );
1103 LogEventsList::showLogExtract( $out, 'suppress', $this->mTargetObj );
1104 }
1105
1106 if ( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
1107 $unsuppressAllowed = $this->permissionManager->userHasRight( $this->getUser(), 'suppressrevision' );
1108 $fields = [];
1109 $fields[] = new Layout( [
1110 'content' => new HtmlSnippet( $this->msg( 'undeleteextrahelp' )->parseAsBlock() )
1111 ] );
1112
1113 $dropdownComment = $this->msg( 'undelete-comment-dropdown' )
1114 ->page( $this->mTargetObj )->inContentLanguage()->text();
1115 // Add additional specific reasons for unsuppress
1116 if ( $unsuppressAllowed ) {
1117 $dropdownComment .= "\n" . $this->msg( 'undelete-comment-dropdown-unsuppress' )
1118 ->page( $this->mTargetObj )->inContentLanguage()->text();
1119 }
1120 $options = Xml::listDropdownOptions(
1121 $dropdownComment,
1122 [ 'other' => $this->msg( 'undeletecommentotherlist' )->text() ]
1123 );
1124 $options = Xml::listDropdownOptionsOoui( $options );
1125
1126 $fields[] = new FieldLayout(
1127 new DropdownInputWidget( [
1128 'name' => 'wpCommentList',
1129 'inputId' => 'wpCommentList',
1130 'infusable' => true,
1131 'value' => $this->getRequest()->getText( 'wpCommentList', 'other' ),
1132 'options' => $options,
1133 ] ),
1134 [
1135 'label' => $this->msg( 'undeletecomment' )->text(),
1136 'align' => 'top',
1137 ]
1138 );
1139
1140 $fields[] = new FieldLayout(
1141 new TextInputWidget( [
1142 'name' => 'wpComment',
1143 'inputId' => 'wpComment',
1144 'infusable' => true,
1145 'value' => $this->getRequest()->getText( 'wpComment' ),
1146 'autofocus' => true,
1147 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
1148 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
1149 // Unicode codepoints.
1150 'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT,
1151 ] ),
1152 [
1153 'label' => $this->msg( 'undeleteothercomment' )->text(),
1154 'align' => 'top',
1155 ]
1156 );
1157
1158 if ( $this->getUser()->isRegistered() ) {
1159 $checkWatch = $this->watchlistManager->isWatched( $this->getUser(), $this->mTargetObj )
1160 || $this->getRequest()->getText( 'wpWatch' );
1161 $fields[] = new FieldLayout(
1162 new CheckboxInputWidget( [
1163 'name' => 'wpWatch',
1164 'inputId' => 'mw-undelete-watch',
1165 'value' => '1',
1166 'selected' => $checkWatch,
1167 ] ),
1168 [
1169 'label' => $this->msg( 'watchthis' )->text(),
1170 'align' => 'inline',
1171 ]
1172 );
1173 }
1174
1175 if ( $unsuppressAllowed ) {
1176 $fields[] = new FieldLayout(
1177 new CheckboxInputWidget( [
1178 'name' => 'wpUnsuppress',
1179 'inputId' => 'mw-undelete-unsuppress',
1180 'value' => '1',
1181 ] ),
1182 [
1183 'label' => $this->msg( 'revdelete-unsuppress' )->text(),
1184 'align' => 'inline',
1185 ]
1186 );
1187 }
1188
1189 $undelPage = $this->undeletePageFactory->newUndeletePage(
1190 $this->wikiPageFactory->newFromTitle( $this->mTargetObj ),
1191 $this->getContext()->getAuthority()
1192 );
1193 if ( $undelPage->canProbablyUndeleteAssociatedTalk()->isGood() ) {
1194 $fields[] = new FieldLayout(
1195 new CheckboxInputWidget( [
1196 'name' => 'undeletetalk',
1197 'inputId' => 'mw-undelete-undeletetalk',
1198 'selected' => false,
1199 ] ),
1200 [
1201 'label' => $this->msg( 'undelete-undeletetalk' )->text(),
1202 'align' => 'inline',
1203 ]
1204 );
1205 }
1206
1207 $fields[] = new FieldLayout(
1208 new Widget( [
1209 'content' => new HorizontalLayout( [
1210 'items' => [
1211 new ButtonInputWidget( [
1212 'name' => 'restore',
1213 'inputId' => 'mw-undelete-submit',
1214 'value' => '1',
1215 'label' => $this->msg( 'undeletebtn' )->text(),
1216 'flags' => [ 'primary', 'progressive' ],
1217 'type' => 'submit',
1218 ] ),
1219 new ButtonInputWidget( [
1220 'name' => 'invert',
1221 'inputId' => 'mw-undelete-invert',
1222 'value' => '1',
1223 'label' => $this->msg( 'undeleteinvert' )->text()
1224 ] ),
1225 ]
1226 ] )
1227 ] )
1228 );
1229
1230 $fieldset = new FieldsetLayout( [
1231 'label' => $this->msg( 'undelete-fieldset-title' )->text(),
1232 'id' => 'mw-undelete-table',
1233 'items' => $fields,
1234 ] );
1235
1236 $link = '';
1237 if ( $this->getAuthority()->isAllowed( 'editinterface' ) ) {
1238 if ( $unsuppressAllowed ) {
1239 $link .= $this->getLinkRenderer()->makeKnownLink(
1240 $this->msg( 'undelete-comment-dropdown-unsuppress' )->inContentLanguage()->getTitle(),
1241 $this->msg( 'undelete-edit-commentlist-unsuppress' )->text(),
1242 [],
1243 [ 'action' => 'edit' ]
1244 );
1245 $link .= $this->msg( 'pipe-separator' )->escaped();
1246 }
1247 $link .= $this->getLinkRenderer()->makeKnownLink(
1248 $this->msg( 'undelete-comment-dropdown' )->inContentLanguage()->getTitle(),
1249 $this->msg( 'undelete-edit-commentlist' )->text(),
1250 [],
1251 [ 'action' => 'edit' ]
1252 );
1253
1254 $link = Html::rawElement( 'p', [ 'class' => 'mw-undelete-editcomments' ], $link );
1255 }
1256
1257 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable form is set, when used here
1258 $form->appendContent(
1259 new PanelLayout( [
1260 'expanded' => false,
1261 'padded' => true,
1262 'framed' => true,
1263 'content' => $fieldset,
1264 ] ),
1265 new HtmlSnippet(
1266 $link .
1267 Html::hidden( 'target', $this->mTarget ) .
1268 Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() )
1269 )
1270 );
1271 }
1272
1273 $history = '';
1274 $history .= Xml::element( 'h2', null, $this->msg( 'history' )->text() ) . "\n";
1275
1276 if ( $haveRevisions ) {
1277 # Show the page's stored (deleted) history
1278
1279 if ( $this->permissionManager->userHasRight( $this->getUser(), 'deleterevision' ) ) {
1280 $history .= Html::element(
1281 'button',
1282 [
1283 'name' => 'revdel',
1284 'type' => 'submit',
1285 'class' => 'deleterevision-log-submit mw-log-deleterevision-button'
1286 ],
1287 $this->msg( 'showhideselectedversions' )->text()
1288 ) . "\n";
1289 }
1290
1291 $history .= $this->formatRevisionHistory( $revisions );
1292
1293 if ( $showLoadMore ) {
1294 $history .=
1295 Html::openElement( 'div' ) .
1297 'span',
1298 [ 'id' => 'mw-load-more-revisions' ],
1299 $this->msg( 'undelete-load-more-revisions' )->text()
1300 ) .
1301 Html::closeElement( 'div' ) .
1302 "\n";
1303 }
1304 } else {
1305 $out->addWikiMsg( 'nohistory' );
1306 }
1307
1308 if ( $haveFiles ) {
1309 $history .= Xml::element( 'h2', null, $this->msg( 'filehist' )->text() ) . "\n";
1310 $history .= Html::openElement( 'ul', [ 'class' => 'mw-undelete-revlist' ] );
1311 foreach ( $files as $row ) {
1312 $history .= $this->formatFileRow( $row );
1313 }
1314 $files->free();
1315 $history .= Html::closeElement( 'ul' );
1316 }
1317
1318 if ( $this->mAllowed ) {
1319 # Slip in the hidden controls here
1320 $misc = Html::hidden( 'target', $this->mTarget );
1321 $misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
1322 $history .= $misc;
1323
1324 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable form is set, when used here
1325 $form->appendContent( new HtmlSnippet( $history ) );
1326 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable form is set, when used here
1327 $out->addHTML( (string)$form );
1328 } else {
1329 $out->addHTML( $history );
1330 }
1331
1332 return true;
1333 }
1334
1335 protected function formatRevisionRow( $row, $earliestLiveTime, $remaining ) {
1336 $revRecord = $this->revisionStore->newRevisionFromArchiveRow(
1337 $row,
1338 IDBAccessObject::READ_NORMAL,
1339 $this->mTargetObj
1340 );
1341
1342 $revTextSize = '';
1343 $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
1344 // Build checkboxen...
1345 if ( $this->mAllowed ) {
1346 if ( $this->mInvert ) {
1347 if ( in_array( $ts, $this->mTargetTimestamp ) ) {
1348 $checkBox = Xml::check( "ts$ts" );
1349 } else {
1350 $checkBox = Xml::check( "ts$ts", true );
1351 }
1352 } else {
1353 $checkBox = Xml::check( "ts$ts" );
1354 }
1355 } else {
1356 $checkBox = '';
1357 }
1358
1359 // Build page & diff links...
1360 $user = $this->getUser();
1361 if ( $this->mCanView ) {
1362 $titleObj = $this->getPageTitle();
1363 # Last link
1364 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1365 $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1366 $last = $this->msg( 'diff' )->escaped();
1367 } elseif ( $remaining > 0 || ( $earliestLiveTime && $ts > $earliestLiveTime ) ) {
1368 $pageLink = $this->getPageLink( $revRecord, $titleObj, $ts );
1369 $last = $this->getLinkRenderer()->makeKnownLink(
1370 $titleObj,
1371 $this->msg( 'diff' )->text(),
1372 [],
1373 [
1374 'target' => $this->mTargetObj->getPrefixedText(),
1375 'timestamp' => $ts,
1376 'diff' => 'prev'
1377 ]
1378 );
1379 } else {
1380 $pageLink = $this->getPageLink( $revRecord, $titleObj, $ts );
1381 $last = $this->msg( 'diff' )->escaped();
1382 }
1383 } else {
1384 $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1385 $last = $this->msg( 'diff' )->escaped();
1386 }
1387
1388 // User links
1389 $userLink = Linker::revUserTools( $revRecord );
1390
1391 // Minor edit
1392 $minor = $revRecord->isMinor() ? ChangesList::flag( 'minor' ) : '';
1393
1394 // Revision text size
1395 $size = $row->ar_len;
1396 if ( $size !== null ) {
1397 $revTextSize = Linker::formatRevisionSize( $size );
1398 }
1399
1400 // Edit summary
1401 $comment = $this->commentFormatter->formatRevision( $revRecord, $user );
1402
1403 // Tags
1404 $attribs = [];
1405 [ $tagSummary, $classes ] = ChangeTags::formatSummaryRow(
1406 $row->ts_tags,
1407 'deletedhistory',
1408 $this->getContext()
1409 );
1410 if ( $classes ) {
1411 $attribs['class'] = implode( ' ', $classes );
1412 }
1413
1414 $revisionRow = $this->msg( 'undelete-revision-row2' )
1415 ->rawParams(
1416 $checkBox,
1417 $last,
1418 $pageLink,
1419 $userLink,
1420 $minor,
1421 $revTextSize,
1422 $comment,
1423 $tagSummary
1424 )
1425 ->escaped();
1426
1427 return Xml::tags( 'li', $attribs, $revisionRow ) . "\n";
1428 }
1429
1430 private function formatFileRow( $row ) {
1431 $file = ArchivedFile::newFromRow( $row );
1432 $ts = wfTimestamp( TS_MW, $row->fa_timestamp );
1433 $user = $this->getUser();
1434
1435 $checkBox = '';
1436 if ( $this->mCanView && $row->fa_storage_key ) {
1437 if ( $this->mAllowed ) {
1438 $checkBox = Xml::check( 'fileid' . $row->fa_id );
1439 }
1440 $key = urlencode( $row->fa_storage_key );
1441 $pageLink = $this->getFileLink( $file, $this->getPageTitle(), $ts, $key );
1442 } else {
1443 $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1444 }
1445 $userLink = $this->getFileUser( $file );
1446 $data = $this->msg( 'widthheight' )->numParams( $row->fa_width, $row->fa_height )->text();
1447 $bytes = $this->msg( 'parentheses' )
1448 ->plaintextParams( $this->msg( 'nbytes' )->numParams( $row->fa_size )->text() )
1449 ->plain();
1450 $data = htmlspecialchars( $data . ' ' . $bytes );
1451 $comment = $this->getFileComment( $file );
1452
1453 // Add show/hide deletion links if available
1454 $canHide = $this->isAllowed( 'deleterevision' );
1455 if ( $canHide || ( $file->getVisibility() && $this->isAllowed( 'deletedhistory' ) ) ) {
1456 if ( !$file->userCan( File::DELETED_RESTRICTED, $user ) ) {
1457 // Revision was hidden from sysops
1458 $revdlink = Linker::revDeleteLinkDisabled( $canHide );
1459 } else {
1460 $query = [
1461 'type' => 'filearchive',
1462 'target' => $this->mTargetObj->getPrefixedDBkey(),
1463 'ids' => $row->fa_id
1464 ];
1465 $revdlink = Linker::revDeleteLink( $query,
1466 $file->isDeleted( File::DELETED_RESTRICTED ), $canHide );
1467 }
1468 } else {
1469 $revdlink = '';
1470 }
1471
1472 return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
1473 }
1474
1483 private function getPageLink( RevisionRecord $revRecord, LinkTarget $target, $ts ) {
1484 $user = $this->getUser();
1485 $time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1486
1487 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1488 // TODO The condition cannot be true when the function is called
1489 return Html::element(
1490 'span',
1491 [ 'class' => 'history-deleted' ],
1492 $time
1493 );
1494 }
1495
1496 $link = $this->getLinkRenderer()->makeKnownLink(
1497 $target,
1498 $time,
1499 [],
1500 [
1501 'target' => $this->mTargetObj->getPrefixedText(),
1502 'timestamp' => $ts
1503 ]
1504 );
1505
1506 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1507 $class = Linker::getRevisionDeletedClass( $revRecord );
1508 $link = '<span class="' . $class . '">' . $link . '</span>';
1509 }
1510
1511 return $link;
1512 }
1513
1524 private function getFileLink( $file, LinkTarget $target, $ts, $key ) {
1525 $user = $this->getUser();
1526 $time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1527
1528 if ( !$file->userCan( File::DELETED_FILE, $user ) ) {
1529 return Html::element(
1530 'span',
1531 [ 'class' => 'history-deleted' ],
1532 $time
1533 );
1534 }
1535
1536 if ( $file->exists() ) {
1537 $link = $this->getLinkRenderer()->makeKnownLink(
1538 $target,
1539 $time,
1540 [],
1541 [
1542 'target' => $this->mTargetObj->getPrefixedText(),
1543 'file' => $key,
1544 'token' => $user->getEditToken( $key )
1545 ]
1546 );
1547 } else {
1548 $link = htmlspecialchars( $time );
1549 }
1550
1551 if ( $file->isDeleted( File::DELETED_FILE ) ) {
1552 $link = '<span class="history-deleted">' . $link . '</span>';
1553 }
1554
1555 return $link;
1556 }
1557
1564 private function getFileUser( $file ) {
1565 $uploader = $file->getUploader( File::FOR_THIS_USER, $this->getAuthority() );
1566 if ( !$uploader ) {
1567 return Html::rawElement(
1568 'span',
1569 [ 'class' => 'history-deleted' ],
1570 $this->msg( 'rev-deleted-user' )->escaped()
1571 );
1572 }
1573
1574 $link = Linker::userLink( $uploader->getId(), $uploader->getName() ) .
1575 Linker::userToolLinks( $uploader->getId(), $uploader->getName() );
1576
1577 if ( $file->isDeleted( File::DELETED_USER ) ) {
1578 $link = Html::rawElement(
1579 'span',
1580 [ 'class' => 'history-deleted' ],
1581 $link
1582 );
1583 }
1584
1585 return $link;
1586 }
1587
1594 private function getFileComment( $file ) {
1595 if ( !$file->userCan( File::DELETED_COMMENT, $this->getAuthority() ) ) {
1596 return Html::rawElement(
1597 'span',
1598 [ 'class' => 'history-deleted' ],
1599 Html::rawElement(
1600 'span',
1601 [ 'class' => 'comment' ],
1602 $this->msg( 'rev-deleted-comment' )->escaped()
1603 )
1604 );
1605 }
1606
1607 $comment = $file->getDescription( File::FOR_THIS_USER, $this->getAuthority() );
1608 $link = $this->commentFormatter->formatBlock( $comment );
1609
1610 if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
1611 $link = Html::rawElement(
1612 'span',
1613 [ 'class' => 'history-deleted' ],
1614 $link
1615 );
1616 }
1617
1618 return $link;
1619 }
1620
1621 private function undelete() {
1622 if ( $this->getConfig()->get( MainConfigNames::UploadMaintenance )
1623 && $this->mTargetObj->getNamespace() === NS_FILE
1624 ) {
1625 throw new ErrorPageError( 'undelete-error', 'filedelete-maintenance' );
1626 }
1627
1628 $this->checkReadOnly();
1629
1630 $out = $this->getOutput();
1631 $undeletePage = $this->undeletePageFactory->newUndeletePage(
1632 $this->wikiPageFactory->newFromTitle( $this->mTargetObj ),
1633 $this->getAuthority()
1634 );
1635 if ( $this->mUndeleteTalk && $undeletePage->canProbablyUndeleteAssociatedTalk()->isGood() ) {
1636 $undeletePage->setUndeleteAssociatedTalk( true );
1637 }
1638 $status = $undeletePage
1639 ->setUndeleteOnlyTimestamps( $this->mTargetTimestamp )
1640 ->setUndeleteOnlyFileVersions( $this->mFileVersions )
1641 ->setUnsuppress( $this->mUnsuppress )
1642 // TODO This is currently duplicating some permission checks, but we do need it (T305680)
1643 ->undeleteIfAllowed( $this->mComment );
1644
1645 if ( !$status->isGood() ) {
1646 $out->setPageTitleMsg( $this->msg( 'undelete-error' ) );
1647 $out->wrapWikiTextAsInterface(
1648 'error',
1649 Status::wrap( $status )->getWikiText(
1650 'cannotundelete',
1651 'cannotundelete',
1652 $this->getLanguage()
1653 )
1654 );
1655 return;
1656 }
1657
1658 $restoredRevs = $status->getValue()[UndeletePage::REVISIONS_RESTORED];
1659 $restoredFiles = $status->getValue()[UndeletePage::FILES_RESTORED];
1660
1661 if ( $restoredRevs === 0 && $restoredFiles === 0 ) {
1662 // TODO Should use a different message here
1663 $out->setPageTitleMsg( $this->msg( 'undelete-error' ) );
1664 } else {
1665 if ( $status->getValue()[UndeletePage::FILES_RESTORED] !== 0 ) {
1666 $this->getHookRunner()->onFileUndeleteComplete(
1667 $this->mTargetObj, $this->mFileVersions, $this->getUser(), $this->mComment );
1668 }
1669
1670 $link = $this->getLinkRenderer()->makeKnownLink( $this->mTargetObj );
1671 $out->addWikiMsg( 'undeletedpage', Message::rawParam( $link ) );
1672
1673 $this->watchlistManager->setWatch(
1674 $this->getRequest()->getCheck( 'wpWatch' ),
1675 $this->getAuthority(),
1676 $this->mTargetObj
1677 );
1678 }
1679 }
1680
1689 public function prefixSearchSubpages( $search, $limit, $offset ) {
1690 return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );
1691 }
1692
1693 protected function getGroupName() {
1694 return 'pagetools';
1695 }
1696}
1697
1702class_alias( SpecialUndelete::class, 'SpecialUndelete' );
const NS_USER
Definition Defines.php:67
const NS_FILE
Definition Defines.php:71
const NS_USER_TALK
Definition Defines.php:68
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfScript( $script='index')
Get the URL path to a MediaWiki entry point.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Deleted file in the 'filearchive' table.
Recent changes tagging.
static formatSummaryRow( $tags, $unused, ?MessageLocalizer $localizer=null)
Creates HTML for the given tags.
Base class for lists of recent changes shown on special pages.
static flag( $flag, ?IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
An error page which can definitely be safely rendered using the OutputPage.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:79
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition LocalRepo.php:49
Class to simplify the use of log pages.
Definition LogPage.php:46
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition LinkBatch.php:48
This is the main service interface for converting single-line comments from various DB comment fields...
Handle database storage of comments such as edit summaries and log reasons.
Content object implementation for representing flat text.
An IContextSource implementation which will inherit context from another source but allow individual ...
This class is a collection of static functions that serve two purposes:
Definition Html.php:57
static listDropdownOptionsOoui( $options)
Convert options for a drop-down box into a format accepted by OOUI\DropdownInputWidget etc.
Definition Html.php:1215
static check( $name, $checked=false, array $attribs=[])
Convenience function to produce a checkbox (input element with type=checkbox)
Definition Html.php:674
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition Html.php:242
static closeElement( $element)
Returns "</$element>".
Definition Html.php:306
static submitButton( $contents, array $attrs=[], array $modifiers=[])
Returns an HTML input element in a string.
Definition Html.php:164
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition Html.php:218
static listDropdownOptions( $list, $params=[])
Build options for a drop-down box from a textual list.
Definition Html.php:1165
Some internal bits split of from Skin.php.
Definition Linker.php:63
A class containing constants representing the names of configuration variables.
const UploadMaintenance
Name constant for the UploadMaintenance setting, for use with Config::get()
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:156
Backend logic for performing a page undelete action.
Service for creating WikiPage objects.
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Exception representing a failure to look up a revision.
A RevisionRecord representing a revision of a deleted page persisted in the archive table.
Page revision base class.
The RevisionRenderer service provides access to rendered output for revisions.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
Parent class for all special pages.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getSkin()
Shortcut to get the skin being used for this instance.
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,...
displayRestrictionError()
Output an error message telling the user what access level they have to have.
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.
prefixSearchString( $search, $limit, $offset, ?SearchEngineFactory $searchEngineFactory=null)
Perform a regular substring search for prefixSearchSubpages.
getAuthority()
Shortcut to get the Authority executing this instance.
getLanguage()
Shortcut to get user's language.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages By default the message key is the canonical name of...
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Special page allowing users with the appropriate permissions to view and restore deleted content.
doesWrites()
Indicates whether POST requests to this special page require write access to the wiki.
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.to override 1.19 void
formatRevisionRow( $row, $earliestLiveTime, $remaining)
isAllowed( $permission, ?User $user=null)
Checks whether a user is allowed the permission for the specific title if one is set.
__construct(PermissionManager $permissionManager, RevisionStore $revisionStore, RevisionRenderer $revisionRenderer, IContentHandlerFactory $contentHandlerFactory, NameTableStore $changeTagDefStore, LinkBatchFactory $linkBatchFactory, RepoGroup $repoGroup, IConnectionProvider $dbProvider, UserOptionsLookup $userOptionsLookup, WikiPageFactory $wikiPageFactory, SearchEngineFactory $searchEngineFactory, UndeletePageFactory $undeletePageFactory, ArchivedRevisionLookup $archivedRevisionLookup, CommentFormatter $commentFormatter, WatchlistManager $watchlistManager)
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
formatRevisionHistory(IResultWrapper $revisions)
Generate the.
execute( $par)
Default execute method Checks user permissions.
userCanExecute(User $user)
Checks if the given user (identified by an object) can execute this special page (as defined by $mRes...
showMoreHistory()
Handle XHR "show more history" requests (T249977)
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Exception representing a failure to look up a row from a name table.
Represents a title within MediaWiki.
Definition Title.php:78
Provides access to user options.
internal since 1.36
Definition User.php:94
Module of static functions for generating XML.
Definition Xml.php:37
Used to show archived pages and eventually restore them.
static listPagesBySearch( $term)
List deleted pages recorded in the archive matching the given term, using search engine archive.
static listPagesByPrefix( $prefix)
List deleted pages recorded in the archive table matching the given title prefix.
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:32
getLocalRepo()
Get the local repository, i.e.
Factory class for SearchEngine.
Show an error when the user tries to do something whilst blocked.
Represents the target of a wiki link.
Service for page undelete actions.
Provide primary and replica IDatabase connections.
Interface for database access objects.
Result wrapper for grabbing data queried from an IDatabase object.
fetchObject()
Fetch the next row from the given result object, in object form.
numRows()
Get the number of rows in a result object.
element(SerializerNode $parent, SerializerNode $node, $contents)