97 private const REVISION_HISTORY_LIMIT = 500;
114 private $mTargetTimestamp = [];
120 private $mComment =
'';
130 private $mUnsuppress;
132 private $mFileVersions = [];
134 private $mUndeleteTalk;
136 private $mHistoryOffset;
143 private $mSearchPrefix;
178 parent::__construct(
'Undelete',
'deletedhistory' );
179 $this->permissionManager = $permissionManager;
180 $this->revisionStore = $revisionStore;
181 $this->revisionRenderer = $revisionRenderer;
182 $this->contentHandlerFactory = $contentHandlerFactory;
183 $this->changeTagDefStore = $changeTagDefStore;
184 $this->linkBatchFactory = $linkBatchFactory;
186 $this->dbProvider = $dbProvider;
187 $this->userOptionsLookup = $userOptionsLookup;
188 $this->wikiPageFactory = $wikiPageFactory;
189 $this->searchEngineFactory = $searchEngineFactory;
190 $this->undeletePageFactory = $undeletePageFactory;
191 $this->archivedRevisionLookup = $archivedRevisionLookup;
192 $this->commentFormatter = $commentFormatter;
193 $this->watchlistManager = $watchlistManager;
200 private function loadRequest( ?
string $par ) {
204 $this->mAction = $request->getRawVal(
'action' );
205 if ( $par !==
null && $par !==
'' ) {
206 $this->mTarget = $par;
208 $this->mTarget = $request->getVal(
'target' );
211 $this->mTargetObj =
null;
213 if ( $this->mTarget !==
null && $this->mTarget !==
'' ) {
214 $this->mTargetObj = Title::newFromText( $this->mTarget );
217 $this->mSearchPrefix = $request->getText(
'prefix' );
218 $time = $request->getVal(
'timestamp' );
219 $this->mTimestamp = $time ?
wfTimestamp( TS_MW, $time ) :
'';
220 $this->mFilename = $request->getVal(
'file' );
222 $posted = $request->wasPosted() &&
223 $user->matchEditToken( $request->getVal(
'wpEditToken' ) );
224 $this->mRestore = $request->getCheck(
'restore' ) && $posted;
225 $this->mRevdel = $request->getCheck(
'revdel' ) && $posted;
226 $this->mInvert = $request->getCheck(
'invert' ) && $posted;
227 $this->mPreview = $request->getCheck(
'preview' ) && $posted;
228 $this->mDiff = $request->getCheck(
'diff' );
229 $this->mDiffOnly = $request->getBool(
'diffonly',
230 $this->userOptionsLookup->getOption( $this->getUser(),
'diffonly' ) );
231 $commentList = $request->getText(
'wpCommentList',
'other' );
232 $comment = $request->getText(
'wpComment' );
233 if ( $commentList ===
'other' ) {
234 $this->mComment = $comment;
235 } elseif ( $comment !==
'' ) {
236 $this->mComment = $commentList . $this->
msg(
'colon-separator' )->inContentLanguage()->text() . $comment;
238 $this->mComment = $commentList;
240 $this->mUnsuppress = $request->getVal(
'wpUnsuppress' ) &&
241 $this->permissionManager->userHasRight( $user,
'suppressrevision' );
242 $this->mToken = $request->getVal(
'token' );
243 $this->mUndeleteTalk = $request->getCheck(
'undeletetalk' );
244 $this->mHistoryOffset = $request->getVal(
'historyoffset' );
247 $this->mAllowed =
true;
248 $this->mCanView =
true;
249 } elseif ( $this->
isAllowed(
'deletedtext' ) ) {
250 $this->mAllowed =
false;
251 $this->mCanView =
true;
252 $this->mRestore =
false;
254 $this->mAllowed =
false;
255 $this->mCanView =
false;
256 $this->mTimestamp =
'';
257 $this->mRestore =
false;
260 if ( $this->mRestore || $this->mInvert ) {
262 $this->mFileVersions = [];
263 foreach ( $request->getValues() as $key => $val ) {
265 if ( preg_match(
'/^ts(\d{14})$/', $key,
$matches ) ) {
269 if ( preg_match(
'/^fileid(\d+)$/', $key,
$matches ) ) {
270 $this->mFileVersions[] = intval(
$matches[1] );
273 rsort( $timestamps );
274 $this->mTargetTimestamp = $timestamps;
288 $block = $user->getBlock();
290 if ( $this->mTargetObj !==
null ) {
291 return $this->permissionManager->userCan( $permission, $user, $this->mTargetObj );
293 $hasRight = $this->permissionManager->userHasRight( $user, $permission );
294 $sitewideBlock = $block && $block->isSitewide();
295 return $permission ===
'undelete' ? ( $hasRight && !$sitewideBlock ) : $hasRight;
300 return $this->
isAllowed( $this->mRestriction, $user );
311 if ( !parent::userCanExecute( $user ) ) {
318 $this->mTargetObj && $this->permissionManager->isBlockedFrom( $user, $this->mTargetObj ) ) {
336 $this->
addHelpLink(
'Help:Deletion_and_undeletion' );
338 $this->loadRequest( $par );
343 $out->addModuleStyles(
'mediawiki.codex.messagebox.styles' );
345 if ( $this->mTargetObj ===
null ) {
346 $out->addWikiMsg(
'undelete-header' );
348 # Not all users can just browse every deleted page from the list
349 if ( $this->permissionManager->userHasRight( $user,
'browsearchive' ) ) {
350 $this->showSearchForm();
357 if ( $this->mAllowed ) {
358 $out->setPageTitleMsg( $this->
msg(
'undeletepage' ) );
360 $out->setPageTitleMsg( $this->
msg(
'viewdeletedpage' ) );
363 $this->
getSkin()->setRelevantTitle( $this->mTargetObj );
365 if ( $this->mTimestamp !==
'' ) {
366 $this->showRevision( $this->mTimestamp );
367 } elseif ( $this->mFilename !==
null && $this->mTargetObj->inNamespace(
NS_FILE ) ) {
368 $file =
new ArchivedFile( $this->mTargetObj, 0, $this->mFilename );
370 if ( !$file->exists() ) {
371 $out->addWikiMsg(
'filedelete-nofile', $this->mFilename );
372 } elseif ( !$file->userCan( File::DELETED_FILE, $user ) ) {
373 if ( $file->isDeleted( File::DELETED_RESTRICTED ) ) {
378 } elseif ( !$user->matchEditToken( $this->mToken, $this->mFilename ) ) {
379 $this->showFileConfirmationForm( $this->mFilename );
381 $this->showFile( $this->mFilename );
383 } elseif ( $this->mAction ===
'submit' ) {
384 if ( $this->mRestore ) {
386 } elseif ( $this->mRevdel ) {
387 $this->redirectToRevDel();
389 } elseif ( $this->mAction ===
'render' ) {
400 private function redirectToRevDel() {
403 foreach ( $this->
getRequest()->getValues() as $key => $val ) {
405 if ( preg_match(
"/^ts(\d{14})$/", $key,
$matches ) ) {
406 $revisionRecord = $this->archivedRevisionLookup
407 ->getRevisionRecordByTimestamp( $this->mTargetObj,
$matches[1] );
408 if ( $revisionRecord ) {
410 $revisions[ $revisionRecord->getId() ] = 1;
416 'type' =>
'revision',
418 'target' => $this->mTargetObj->getPrefixedText()
424 private function showSearchForm() {
426 $out->setPageTitleMsg( $this->
msg(
'undelete-search-title' ) );
427 $fuzzySearch = $this->
getRequest()->getVal(
'fuzzy',
'1' );
432 $fields[] =
new ActionFieldLayout(
433 new TextInputWidget( [
435 'inputId' =>
'prefix',
437 'value' => $this->mSearchPrefix,
440 new ButtonInputWidget( [
441 'label' => $this->
msg(
'undelete-search-submit' )->text(),
442 'flags' => [
'primary',
'progressive' ],
443 'inputId' =>
'searchUndelete',
447 'label' =>
new HtmlSnippet(
449 $fuzzySearch ?
'undelete-search-full' :
'undelete-search-prefix'
456 $fieldset =
new FieldsetLayout( [
457 'label' => $this->
msg(
'undelete-search-box' )->text(),
461 $form =
new FormLayout( [
466 $form->appendContent(
469 Html::hidden(
'title', $this->
getPageTitle()->getPrefixedDBkey() ) .
470 Html::hidden(
'fuzzy', $fuzzySearch )
483 # List undeletable articles
484 if ( $this->mSearchPrefix ) {
487 if ( $fuzzySearch ) {
488 $result = PageArchive::listPagesBySearch( $this->mSearchPrefix );
490 $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix );
492 $this->showList( $result );
502 private function showList( $result ) {
505 if ( $result->numRows() == 0 ) {
506 $out->addWikiMsg(
'undelete-no-results' );
511 $out->addWikiMsg(
'undeletepagetext', $this->
getLanguage()->formatNum( $result->numRows() ) );
515 $out->addHTML(
"<ul id='undeleteResultsList'>\n" );
516 foreach ( $result as $row ) {
517 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
518 if ( $title !==
null ) {
519 $item = $linkRenderer->makeKnownLink(
521 $title->getPrefixedText(),
523 [
'target' => $title->getPrefixedText() ]
529 [
'class' =>
'mw-invalidtitle' ],
530 Linker::getInvalidTitleDescription(
537 $revs = $this->
msg(
'undeleterevisions' )->numParams( $row->count )->parse();
541 [
'class' =>
'undeleteResult' ],
542 $item . $this->
msg(
'word-separator' )->escaped() .
543 $this->
msg(
'parentheses' )->rawParams( $revs )->escaped()
548 $out->addHTML(
"</ul>\n" );
553 private function showRevision(
string $timestamp ) {
554 if ( !preg_match(
'/[0-9]{14}/', $timestamp ) ) {
558 $out->addModuleStyles(
'mediawiki.interface.helpers.styles' );
564 $this->
msg(
'undelete-back-to-list' )->text(),
566 [
'target' => $this->mTargetObj->getPrefixedText() ]
569 $subtitle =
"< $listLink";
570 $out->setSubtitle( $subtitle );
572 $archive =
new PageArchive( $this->mTargetObj );
575 $archive, $this->mTargetObj )
579 $revRecord = $this->archivedRevisionLookup->getRevisionRecordByTimestamp( $this->mTargetObj, $timestamp );
584 $out->addWikiMsg(
'undeleterevision-missing' );
588 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
590 $titleText = $this->mTargetObj->getPrefixedDBkey();
591 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
592 $msg = $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
593 ? [
'rev-suppressed-text-permission', $titleText ]
594 : [
'rev-deleted-text-permission', $titleText ];
597 $this->
msg( $msg[0], $msg[1] )->parse(),
604 $msg = $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
605 ? [
'rev-suppressed-text-view', $titleText ]
606 : [
'rev-deleted-text-view', $titleText ];
609 $this->
msg( $msg[0], $msg[1] )->parse(),
616 if ( $this->mDiff ) {
617 $previousRevRecord = $this->archivedRevisionLookup
618 ->getPreviousRevisionRecord( $this->mTargetObj, $timestamp );
619 if ( $previousRevRecord ) {
620 $this->showDiff( $previousRevRecord, $revRecord );
621 if ( $this->mDiffOnly ) {
625 $out->addHTML(
'<hr />' );
627 $out->addWikiMsg(
'undelete-nodiff' );
632 $this->
getPageTitle( $this->mTargetObj->getPrefixedDBkey() ),
633 $this->mTargetObj->getPrefixedText()
640 $time = $lang->userTimeAndDate( $timestamp, $user );
641 $d = $lang->userDate( $timestamp, $user );
642 $t = $lang->userTime( $timestamp, $user );
643 $userLink = Linker::revUserTools( $revRecord );
646 $content = $revRecord->getContent(
648 RevisionRecord::FOR_THIS_USER,
651 }
catch ( RevisionAccessException $e ) {
656 $isText = ( $content instanceof TextContent );
658 $undeleteRevisionContent =
'';
660 if ( !$this->mDiff ) {
661 $revdel = Linker::getRevDeleteLink(
667 $undeleteRevisionContent = $revdel .
' ';
671 $undeleteRevisionContent .= $out->msg(
680 if ( $this->mPreview || $isText ) {
683 $undeleteRevisionContent,
684 'mw-undelete-revision'
691 [
'class' =>
'mw-undelete-revision', ],
692 $undeleteRevisionContent
697 if ( $this->mPreview || !$isText ) {
700 $popts = ParserOptions::newFromContext( $this->
getContext() );
703 $rendered = $this->revisionRenderer->getRenderedRevision(
707 [
'audience' => RevisionRecord::FOR_THIS_USER,
'causeAction' =>
'undelete-preview' ]
712 $pout = $rendered->getRevisionParserOutput();
714 $out->addParserOutput( $pout, $popts, [
715 'enableSectionEditLinks' =>
false,
717 }
catch ( RevisionAccessException $e ) {
725 '@phan-var TextContent $content';
729 'readonly' =>
'readonly',
732 ], $content->getText() .
"\n" );
734 $buttonFields[] =
new ButtonInputWidget( [
737 'label' => $this->
msg(
'showpreview' )->text()
743 $buttonFields[] =
new ButtonInputWidget( [
746 'label' => $this->
msg(
'showdiff' )->text()
751 Html::openElement(
'div', [
752 'style' =>
'clear: both' ] ) .
753 Html::openElement(
'form', [
755 'action' => $this->
getPageTitle()->getLocalURL( [
'action' =>
'submit' ] ) ] ) .
759 'value' => $this->mTargetObj->getPrefixedDBkey() ] ) .
762 'name' =>
'timestamp',
763 'value' => $timestamp ] ) .
766 'name' =>
'wpEditToken',
767 'value' => $user->getEditToken() ] ) .
770 'content' =>
new HorizontalLayout( [
771 'items' => $buttonFields
775 Html::closeElement(
'form' ) .
776 Html::closeElement(
'div' )
787 private function showDiff(
788 RevisionRecord $previousRevRecord,
789 RevisionRecord $currentRevRecord
791 $currentTitle = Title::newFromPageIdentity( $currentRevRecord->getPage() );
793 $diffContext =
new DerivativeContext( $this->
getContext() );
794 $diffContext->setTitle( $currentTitle );
795 $diffContext->setWikiPage( $this->wikiPageFactory->newFromTitle( $currentTitle ) );
797 $contentModel = $currentRevRecord->getSlot(
802 $diffEngine = $this->contentHandlerFactory->getContentHandler( $contentModel )
803 ->createDifferenceEngine( $diffContext );
805 $diffEngine->setRevisions( $previousRevRecord, $currentRevRecord );
806 $diffEngine->showDiffStyle();
807 $formattedDiff = $diffEngine->getDiff(
808 $this->diffHeader( $previousRevRecord,
'o' ),
809 $this->diffHeader( $currentRevRecord,
'n' )
812 if ( $formattedDiff ===
false ) {
813 if ( $diffEngine->hasSuppressedRevision() ) {
814 $error =
'rev-suppressed-no-diff';
815 } elseif ( $diffEngine->hasDeletedRevision() ) {
816 $error =
'rev-deleted-no-diff';
819 $error =
'undelete-error-loading-diff';
821 $this->
getOutput()->addHTML( $this->
msg( $error )->parse() );
823 $this->
getOutput()->addHTML(
"<div>$formattedDiff</div>\n" );
832 private function diffHeader( RevisionRecord $revRecord, $prefix ) {
833 if ( $revRecord instanceof RevisionArchiveRecord ) {
837 'target' => $this->mTargetObj->getPrefixedText(),
838 'timestamp' =>
wfTimestamp( TS_MW, $revRecord->getTimestamp() )
842 $targetPage = $revRecord->getPageAsLinkTarget();
843 $targetQuery = [
'oldid' => $revRecord->getId() ];
849 $rdel = Linker::getRevDeleteLink( $user, $revRecord, $this->mTargetObj );
855 $minor = $revRecord->isMinor() ? ChangesList::flag(
'minor' ) :
'';
857 $dbr = $this->dbProvider->getReplicaDatabase();
858 $tagIds = $dbr->newSelectQueryBuilder()
859 ->select(
'ct_tag_id' )
860 ->from(
'change_tag' )
861 ->where( [
'ct_rev_id' => $revRecord->getId() ] )
862 ->caller( __METHOD__ )->fetchFieldValues();
864 foreach ( $tagIds as $tagId ) {
866 $tags[] = $this->changeTagDefStore->getName( (
int)$tagId );
867 }
catch ( NameTableAccessException $exception ) {
871 $tags = implode(
',', $tags );
872 $tagSummary = ChangeTags::formatSummaryRow( $tags,
'deleteddiff', $this->
getContext() );
877 $lang->userTimeAndDate( $revRecord->getTimestamp(), $user ),
878 $lang->userDate( $revRecord->getTimestamp(), $user ),
879 $lang->userTime( $revRecord->getTimestamp(), $user )
884 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
885 $asof = Html::rawElement(
887 [
'class' => Linker::getRevisionDeletedClass( $revRecord ) ],
894 return '<div id="mw-diff-' . $prefix .
'title1"><strong>' .
897 '<div id="mw-diff-' . $prefix .
'title2">' .
898 Linker::revUserTools( $revRecord ) .
'<br />' .
900 '<div id="mw-diff-' . $prefix .
'title3">' .
901 $minor . $this->commentFormatter->formatRevision( $revRecord, $user ) . $rdel .
'<br />' .
903 '<div id="mw-diff-' . $prefix .
'title5">' .
904 $tagSummary[0] .
'<br />' .
912 private function showFileConfirmationForm( $key ) {
916 $file =
new ArchivedFile( $this->mTargetObj, 0, $this->mFilename );
917 $out->addWikiMsg(
'undelete-show-file-confirm',
918 $this->mTargetObj->getText(),
919 $lang->userDate( $file->getTimestamp(), $user ),
920 $lang->userTime( $file->getTimestamp(), $user ) );
922 Html::rawElement(
'form', [
925 'target' => $this->mTarget,
927 'token' => $user->getEditToken( $key ),
930 Html::submitButton( $this->msg(
'undelete-show-file-submit' )->text() )
939 private function showFile( $key ) {
942 # We mustn't allow the output to be CDN cached, otherwise
943 # if an admin previews a deleted image, and it's cached, then
944 # a user without appropriate permissions can toddle off and
945 # nab the image, and CDN will serve it
947 $response->header(
'Expires: ' . gmdate(
'D, d M Y H:i:s', 0 ) .
' GMT' );
948 $response->header(
'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
950 $path = $this->localRepo->getZonePath(
'deleted' ) .
'/' . $this->localRepo->getDeletedHashPath( $key ) . $key;
951 $this->localRepo->streamFileWithStatus(
$path );
954 private function addRevisionsToBatch( LinkBatch $batch, IResultWrapper $revisions ) {
955 foreach ( $revisions as $row ) {
956 $batch->addUser(
new UserIdentityValue( (
int)$row->ar_user, $row->ar_user_text ) );
960 private function addFilesToBatch( LinkBatch $batch, IResultWrapper $files ) {
961 foreach ( $files as $row ) {
962 $batch->add(
NS_USER, $row->fa_user_text );
972 $out->setArticleBodyOnly(
true );
973 $dbr = $this->dbProvider->getReplicaDatabase();
974 if ( $this->mHistoryOffset ) {
975 $extraConds = [ $dbr->expr(
'ar_timestamp',
'<', $dbr->timestamp( $this->mHistoryOffset ) ) ];
979 $revisions = $this->archivedRevisionLookup->listRevisions(
982 self::REVISION_HISTORY_LIMIT + 1
984 $batch = $this->linkBatchFactory->newLinkBatch();
985 $this->addRevisionsToBatch( $batch, $revisions );
989 if ( $revisions->numRows() > self::REVISION_HISTORY_LIMIT ) {
991 $out->setStatusCode( 206 );
1002 $history = Html::openElement(
'ul', [
'class' =>
'mw-undelete-revlist' ] );
1005 $numRevisions = $revisions->
numRows();
1006 $displayCount = min( $numRevisions, self::REVISION_HISTORY_LIMIT );
1007 $firstRev = $this->revisionStore->getFirstRevision( $this->mTargetObj );
1008 $earliestLiveTime = $firstRev ? $firstRev->getTimestamp() :
null;
1010 $revisions->rewind();
1011 for ( $i = 0; $i < $displayCount; $i++ ) {
1015 $history .= $this->
formatRevisionRow( $row, $earliestLiveTime, $numRevisions - $i );
1017 $history .= Html::closeElement(
'ul' );
1025 if ( $this->mAllowed ) {
1026 $out->addModules(
'mediawiki.misc-authed-ooui' );
1027 $out->addModuleStyles(
'mediawiki.special' );
1029 $out->addModuleStyles(
'mediawiki.interface.helpers.styles' );
1031 "<div class='mw-undelete-pagetitle'>\n$1\n</div>\n",
1032 [
'undeletepagetitle',
wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) ]
1037 $this->
getHookRunner()->onUndeleteForm__showHistory( $archive, $this->mTargetObj );
1039 $out->addHTML( Html::openElement(
'div', [
'class' =>
'mw-undelete-history' ] ) );
1040 if ( $this->mAllowed ) {
1041 $out->addWikiMsg(
'undeletehistory' );
1042 $out->addWikiMsg(
'undeleterevdel' );
1044 $out->addWikiMsg(
'undeletehistorynoadmin' );
1046 $out->addHTML( Html::closeElement(
'div' ) );
1048 # List all stored revisions
1049 $revisions = $this->archivedRevisionLookup->listRevisions(
1052 self::REVISION_HISTORY_LIMIT + 1
1054 $files = $archive->listFiles();
1055 $numRevisions = $revisions->numRows();
1056 $showLoadMore = $numRevisions > self::REVISION_HISTORY_LIMIT;
1057 $haveRevisions = $numRevisions > 0;
1058 $haveFiles = $files && $files->numRows() > 0;
1060 # Batch existence check on user and talk pages
1061 if ( $haveRevisions || $haveFiles ) {
1062 $batch = $this->linkBatchFactory->newLinkBatch();
1063 $this->addRevisionsToBatch( $batch, $revisions );
1066 $this->addFilesToBatch( $batch, $files );
1071 if ( $this->mAllowed ) {
1074 $action = $this->
getPageTitle()->getLocalURL( [
'action' =>
'submit' ] );
1075 # Start the form here
1076 $form =
new FormLayout( [
1078 'action' => $action,
1083 # Show relevant lines from the deletion log:
1084 $deleteLogPage =
new LogPage(
'delete' );
1085 $out->addHTML(
Html::element(
'h2', [], $deleteLogPage->getName()->text() ) .
"\n" );
1086 LogEventsList::showLogExtract( $out,
'delete', $this->mTargetObj );
1087 # Show relevant lines from the suppression log:
1088 $suppressLogPage =
new LogPage(
'suppress' );
1089 if ( $this->permissionManager->userHasRight( $this->getUser(),
'suppressionlog' ) ) {
1090 $out->addHTML(
Html::element(
'h2', [], $suppressLogPage->getName()->text() ) .
"\n" );
1091 LogEventsList::showLogExtract( $out,
'suppress', $this->mTargetObj );
1094 if ( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
1095 $unsuppressAllowed = $this->permissionManager->userHasRight( $this->
getUser(),
'suppressrevision' );
1097 $fields[] =
new Layout( [
1098 'content' =>
new HtmlSnippet( $this->
msg(
'undeleteextrahelp' )->parseAsBlock() )
1101 $dropdownComment = $this->
msg(
'undelete-comment-dropdown' )
1102 ->page( $this->mTargetObj )->inContentLanguage()->text();
1104 if ( $unsuppressAllowed ) {
1105 $dropdownComment .=
"\n" . $this->
msg(
'undelete-comment-dropdown-unsuppress' )
1106 ->page( $this->mTargetObj )->inContentLanguage()->text();
1108 $options = Html::listDropdownOptions(
1110 [
'other' => $this->
msg(
'undeletecommentotherlist' )->text() ]
1112 $options = Html::listDropdownOptionsOoui( $options );
1114 $fields[] =
new FieldLayout(
1115 new DropdownInputWidget( [
1116 'name' =>
'wpCommentList',
1117 'inputId' =>
'wpCommentList',
1118 'infusable' =>
true,
1119 'value' => $this->
getRequest()->getText(
'wpCommentList',
'other' ),
1120 'options' => $options,
1123 'label' => $this->
msg(
'undeletecomment' )->text(),
1128 $fields[] =
new FieldLayout(
1129 new TextInputWidget( [
1130 'name' =>
'wpComment',
1131 'inputId' =>
'wpComment',
1132 'infusable' =>
true,
1133 'value' => $this->
getRequest()->getText(
'wpComment' ),
1134 'autofocus' =>
true,
1138 'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT,
1141 'label' => $this->
msg(
'undeleteothercomment' )->text(),
1146 if ( $this->
getUser()->isRegistered() ) {
1147 $checkWatch = $this->watchlistManager->isWatched( $this->
getUser(), $this->mTargetObj )
1148 || $this->
getRequest()->getText(
'wpWatch' );
1149 $fields[] =
new FieldLayout(
1150 new CheckboxInputWidget( [
1151 'name' =>
'wpWatch',
1152 'inputId' =>
'mw-undelete-watch',
1154 'selected' => $checkWatch,
1157 'label' => $this->
msg(
'watchthis' )->text(),
1158 'align' =>
'inline',
1163 if ( $unsuppressAllowed ) {
1164 $fields[] =
new FieldLayout(
1165 new CheckboxInputWidget( [
1166 'name' =>
'wpUnsuppress',
1167 'inputId' =>
'mw-undelete-unsuppress',
1171 'label' => $this->
msg(
'revdelete-unsuppress' )->text(),
1172 'align' =>
'inline',
1177 $undelPage = $this->undeletePageFactory->newUndeletePage(
1178 $this->wikiPageFactory->newFromTitle( $this->mTargetObj ),
1179 $this->getContext()->getAuthority()
1181 if ( $undelPage->canProbablyUndeleteAssociatedTalk()->isGood() ) {
1182 $fields[] =
new FieldLayout(
1183 new CheckboxInputWidget( [
1184 'name' =>
'undeletetalk',
1185 'inputId' =>
'mw-undelete-undeletetalk',
1186 'selected' =>
false,
1189 'label' => $this->
msg(
'undelete-undeletetalk' )->text(),
1190 'align' =>
'inline',
1195 $fields[] =
new FieldLayout(
1197 'content' =>
new HorizontalLayout( [
1199 new ButtonInputWidget( [
1200 'name' =>
'restore',
1201 'inputId' =>
'mw-undelete-submit',
1203 'label' => $this->
msg(
'undeletebtn' )->text(),
1204 'flags' => [
'primary',
'progressive' ],
1207 new ButtonInputWidget( [
1209 'inputId' =>
'mw-undelete-invert',
1211 'label' => $this->
msg(
'undeleteinvert' )->text()
1218 $fieldset =
new FieldsetLayout( [
1219 'label' => $this->
msg(
'undelete-fieldset-title' )->text(),
1220 'id' =>
'mw-undelete-table',
1226 if ( $unsuppressAllowed ) {
1228 $this->
msg(
'undelete-comment-dropdown-unsuppress' )->inContentLanguage()->
getTitle(),
1229 $this->
msg(
'undelete-edit-commentlist-unsuppress' )->text(),
1231 [
'action' =>
'edit' ]
1233 $link .= $this->
msg(
'pipe-separator' )->escaped();
1236 $this->
msg(
'undelete-comment-dropdown' )->inContentLanguage()->
getTitle(),
1237 $this->
msg(
'undelete-edit-commentlist' )->text(),
1239 [
'action' =>
'edit' ]
1242 $link = Html::rawElement(
'p', [
'class' =>
'mw-undelete-editcomments' ], $link );
1246 $form->appendContent(
1248 'expanded' =>
false,
1251 'content' => $fieldset,
1255 Html::hidden(
'target', $this->mTarget ) .
1256 Html::hidden(
'wpEditToken', $this->
getUser()->getEditToken() )
1262 $history .=
Html::element(
'h2', [], $this->
msg(
'history' )->text() ) .
"\n";
1264 if ( $haveRevisions ) {
1265 # Show the page's stored (deleted) history
1267 if ( $this->mAllowed && $this->permissionManager->userHasRight( $this->getUser(),
'deleterevision' ) ) {
1273 'class' => [
'deleterevision-log-submit',
'mw-log-deleterevision-button' ]
1275 $this->
msg(
'showhideselectedversions' )->text()
1281 if ( $showLoadMore ) {
1283 Html::openElement(
'div' ) .
1286 [
'id' =>
'mw-load-more-revisions' ],
1287 $this->
msg(
'undelete-load-more-revisions' )->text()
1289 Html::closeElement(
'div' ) .
1293 $out->addWikiMsg(
'nohistory' );
1297 $history .=
Html::element(
'h2', [], $this->
msg(
'filehist' )->text() ) .
"\n";
1298 $history .= Html::openElement(
'ul', [
'class' =>
'mw-undelete-revlist' ] );
1299 foreach ( $files as $row ) {
1300 $history .= $this->formatFileRow( $row );
1303 $history .= Html::closeElement(
'ul' );
1306 if ( $this->mAllowed ) {
1307 # Slip in the hidden controls here
1308 $misc = Html::hidden(
'target', $this->mTarget );
1309 $misc .= Html::hidden(
'wpEditToken', $this->
getUser()->getEditToken() );
1313 $form->appendContent(
new HtmlSnippet( $history ) );
1315 $out->addHTML( (
string)$form );
1317 $out->addHTML( $history );
1324 $revRecord = $this->revisionStore->newRevisionFromArchiveRow(
1326 IDBAccessObject::READ_NORMAL,
1333 if ( $this->mAllowed ) {
1334 if ( $this->mInvert ) {
1335 if ( in_array( $ts, $this->mTargetTimestamp ) ) {
1336 $checkBox = Html::check(
"ts$ts" );
1338 $checkBox = Html::check(
"ts$ts",
true );
1341 $checkBox = Html::check(
"ts$ts" );
1349 if ( $this->mCanView ) {
1352 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1353 $pageLink = htmlspecialchars( $this->
getLanguage()->userTimeAndDate( $ts, $user ) );
1354 $last = $this->
msg(
'diff' )->escaped();
1355 } elseif ( $remaining > 0 || ( $earliestLiveTime && $ts > $earliestLiveTime ) ) {
1356 $pageLink = $this->getPageLink( $revRecord, $titleObj, $ts );
1359 $this->
msg(
'diff' )->text(),
1362 'target' => $this->mTargetObj->getPrefixedText(),
1368 $pageLink = $this->getPageLink( $revRecord, $titleObj, $ts );
1369 $last = $this->
msg(
'diff' )->escaped();
1372 $pageLink = htmlspecialchars( $this->
getLanguage()->userTimeAndDate( $ts, $user ) );
1373 $last = $this->
msg(
'diff' )->escaped();
1377 $userLink = Linker::revUserTools( $revRecord );
1380 $minor = $revRecord->isMinor() ? ChangesList::flag(
'minor' ) :
'';
1383 $size = $row->ar_len;
1384 if ( $size !==
null ) {
1385 $revTextSize = Linker::formatRevisionSize( $size );
1389 $comment = $this->commentFormatter->formatRevision( $revRecord, $user );
1393 [ $tagSummary, $classes ] = ChangeTags::formatSummaryRow(
1398 $attribs[
'class'] = $classes;
1400 $revisionRow = $this->
msg(
'undelete-revision-row2' )
1413 return Html::rawElement(
'li', $attribs, $revisionRow ) .
"\n";
1416 private function formatFileRow( \stdClass $row ): string {
1422 if ( $this->mCanView && $row->fa_storage_key ) {
1423 if ( $this->mAllowed ) {
1424 $checkBox = Html::check(
'fileid' . $row->fa_id );
1426 $key = urlencode( $row->fa_storage_key );
1427 $pageLink = $this->getFileLink( $file, $this->
getPageTitle(), $ts, $key );
1429 $pageLink = htmlspecialchars( $this->
getLanguage()->userTimeAndDate( $ts, $user ) );
1431 $userLink = $this->getFileUser( $file );
1432 $data = $this->
msg(
'widthheight' )->numParams( $row->fa_width, $row->fa_height )->text();
1433 $bytes = $this->
msg(
'parentheses' )
1434 ->plaintextParams( $this->
msg(
'nbytes' )->numParams( $row->fa_size )->text() )
1436 $data = htmlspecialchars( $data .
' ' . $bytes );
1437 $comment = $this->getFileComment( $file );
1440 $canHide = $this->
isAllowed(
'deleterevision' );
1441 if ( $canHide || ( $file->getVisibility() && $this->isAllowed(
'deletedhistory' ) ) ) {
1442 if ( !$file->userCan( File::DELETED_RESTRICTED, $user ) ) {
1444 $revdlink = Linker::revDeleteLinkDisabled( $canHide );
1447 'type' =>
'filearchive',
1448 'target' => $this->mTargetObj->getPrefixedDBkey(),
1449 'ids' => $row->fa_id
1451 $revdlink = Linker::revDeleteLink( $query,
1452 $file->isDeleted( File::DELETED_RESTRICTED ), $canHide );
1458 return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
1469 private function getPageLink( RevisionRecord $revRecord, LinkTarget $target, $ts ) {
1471 $time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1473 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1477 [
'class' =>
'history-deleted' ],
1482 $link = $this->getLinkRenderer()->makeKnownLink(
1487 'target' => $this->mTargetObj->getPrefixedText(),
1492 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1493 $class = Linker::getRevisionDeletedClass( $revRecord );
1494 $link =
'<span class="' . $class .
'">' . $link .
'</span>';
1510 private function getFileLink( $file, LinkTarget $target, $ts, $key ) {
1512 $time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1514 if ( !$file->userCan( File::DELETED_FILE, $user ) ) {
1517 [
'class' =>
'history-deleted' ],
1522 if ( $file->exists() ) {
1523 $link = $this->getLinkRenderer()->makeKnownLink(
1528 'target' => $this->mTargetObj->getPrefixedText(),
1530 'token' => $user->getEditToken( $key )
1534 $link = htmlspecialchars( $time );
1537 if ( $file->isDeleted( File::DELETED_FILE ) ) {
1538 $link =
'<span class="history-deleted">' . $link .
'</span>';
1550 private function getFileUser( $file ) {
1551 $uploader = $file->getUploader( File::FOR_THIS_USER, $this->
getAuthority() );
1553 return Html::rawElement(
1555 [
'class' =>
'history-deleted' ],
1556 $this->msg(
'rev-deleted-user' )->escaped()
1560 $link = Linker::userLink( $uploader->getId(), $uploader->getName() ) .
1561 Linker::userToolLinks( $uploader->getId(), $uploader->getName() );
1563 if ( $file->isDeleted( File::DELETED_USER ) ) {
1564 $link = Html::rawElement(
1566 [
'class' =>
'history-deleted' ],
1580 private function getFileComment( $file ) {
1581 if ( !$file->userCan( File::DELETED_COMMENT, $this->getAuthority() ) ) {
1582 return Html::rawElement(
1584 [
'class' =>
'history-deleted' ],
1587 [
'class' =>
'comment' ],
1588 $this->msg(
'rev-deleted-comment' )->escaped()
1593 $comment = $file->getDescription( File::FOR_THIS_USER, $this->
getAuthority() );
1594 $link = $this->commentFormatter->formatBlock( $comment );
1596 if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
1597 $link = Html::rawElement(
1599 [
'class' =>
'history-deleted' ],
1607 private function undelete() {
1609 && $this->mTargetObj->getNamespace() ===
NS_FILE
1611 throw new ErrorPageError(
'undelete-error',
'filedelete-maintenance' );
1614 $this->checkReadOnly();
1616 $out = $this->getOutput();
1617 $undeletePage = $this->undeletePageFactory->newUndeletePage(
1618 $this->wikiPageFactory->newFromTitle( $this->mTargetObj ),
1619 $this->getAuthority()
1621 if ( $this->mUndeleteTalk && $undeletePage->canProbablyUndeleteAssociatedTalk()->isGood() ) {
1622 $undeletePage->setUndeleteAssociatedTalk(
true );
1624 $status = $undeletePage
1625 ->setUndeleteOnlyTimestamps( $this->mTargetTimestamp )
1626 ->setUndeleteOnlyFileVersions( $this->mFileVersions )
1627 ->setUnsuppress( $this->mUnsuppress )
1629 ->undeleteIfAllowed( $this->mComment );
1631 if ( !$status->isGood() ) {
1632 $out->setPageTitleMsg( $this->msg(
'undelete-error' ) );
1633 foreach ( $status->getMessages() as $msg ) {
1634 $out->addHTML( Html::errorBox(
1635 $this->msg( $msg )->parse()
1641 $restoredRevs = $status->getValue()[UndeletePage::REVISIONS_RESTORED];
1642 $restoredFiles = $status->getValue()[UndeletePage::FILES_RESTORED];
1644 if ( $restoredRevs === 0 && $restoredFiles === 0 ) {
1646 $out->setPageTitleMsg( $this->msg(
'undelete-error' ) );
1648 if ( $status->getValue()[UndeletePage::FILES_RESTORED] !== 0 ) {
1649 $this->getHookRunner()->onFileUndeleteComplete(
1650 $this->mTargetObj, $this->mFileVersions, $this->
getUser(), $this->mComment );
1653 $link = $this->getLinkRenderer()->makeKnownLink( $this->mTargetObj );
1656 $this->watchlistManager->setWatch(
1673 return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );