97 private const REVISION_HISTORY_LIMIT = 500;
107 private $mTargetTimestamp = [];
111 private $mComment =
'';
120 private $mUnsuppress;
122 private $mFileVersions = [];
124 private $mUndeleteTalk;
126 private $mHistoryOffset;
133 private $mSearchPrefix;
182 parent::__construct(
'Undelete',
'deletedhistory' );
183 $this->permissionManager = $permissionManager;
184 $this->revisionStore = $revisionStore;
185 $this->revisionRenderer = $revisionRenderer;
186 $this->contentHandlerFactory = $contentHandlerFactory;
187 $this->changeTagDefStore = $changeTagDefStore;
188 $this->linkBatchFactory = $linkBatchFactory;
190 $this->dbProvider = $dbProvider;
191 $this->userOptionsLookup = $userOptionsLookup;
192 $this->wikiPageFactory = $wikiPageFactory;
193 $this->searchEngineFactory = $searchEngineFactory;
194 $this->undeletePageFactory = $undeletePageFactory;
195 $this->archivedRevisionLookup = $archivedRevisionLookup;
196 $this->commentFormatter = $commentFormatter;
203 private function loadRequest( $par ) {
207 $this->mAction = $request->getRawVal(
'action' );
208 if ( $par !==
null && $par !==
'' ) {
209 $this->mTarget = $par;
211 $this->mTarget = $request->getVal(
'target' );
214 $this->mTargetObj =
null;
216 if ( $this->mTarget !==
null && $this->mTarget !==
'' ) {
217 $this->mTargetObj = Title::newFromText( $this->mTarget );
220 $this->mSearchPrefix = $request->getText(
'prefix' );
221 $time = $request->getVal(
'timestamp' );
222 $this->mTimestamp = $time ?
wfTimestamp( TS_MW, $time ) :
'';
223 $this->mFilename = $request->getVal(
'file' );
225 $posted = $request->wasPosted() &&
226 $user->matchEditToken( $request->getVal(
'wpEditToken' ) );
227 $this->mRestore = $request->getCheck(
'restore' ) && $posted;
228 $this->mRevdel = $request->getCheck(
'revdel' ) && $posted;
229 $this->mInvert = $request->getCheck(
'invert' ) && $posted;
230 $this->mPreview = $request->getCheck(
'preview' ) && $posted;
231 $this->mDiff = $request->getCheck(
'diff' );
232 $this->mDiffOnly = $request->getBool(
'diffonly',
233 $this->userOptionsLookup->getOption( $this->getUser(),
'diffonly' ) );
234 $commentList = $request->getText(
'wpCommentList',
'other' );
235 $comment = $request->getText(
'wpComment' );
236 if ( $commentList ===
'other' ) {
237 $this->mComment = $comment;
238 } elseif ( $comment !==
'' ) {
239 $this->mComment = $commentList . $this->
msg(
'colon-separator' )->inContentLanguage()->text() . $comment;
241 $this->mComment = $commentList;
243 $this->mUnsuppress = $request->getVal(
'wpUnsuppress' ) &&
244 $this->permissionManager->userHasRight( $user,
'suppressrevision' );
245 $this->mToken = $request->getVal(
'token' );
246 $this->mUndeleteTalk = $request->getCheck(
'undeletetalk' );
247 $this->mHistoryOffset = $request->getVal(
'historyoffset' );
250 $this->mAllowed =
true;
251 $this->mCanView =
true;
252 } elseif ( $this->
isAllowed(
'deletedtext' ) ) {
253 $this->mAllowed =
false;
254 $this->mCanView =
true;
255 $this->mRestore =
false;
257 $this->mAllowed =
false;
258 $this->mCanView =
false;
259 $this->mTimestamp =
'';
260 $this->mRestore =
false;
263 if ( $this->mRestore || $this->mInvert ) {
265 $this->mFileVersions = [];
266 foreach ( $request->getValues() as $key => $val ) {
268 if ( preg_match(
'/^ts(\d{14})$/', $key,
$matches ) ) {
272 if ( preg_match(
'/^fileid(\d+)$/', $key,
$matches ) ) {
273 $this->mFileVersions[] = intval(
$matches[1] );
276 rsort( $timestamps );
277 $this->mTargetTimestamp = $timestamps;
290 $user = $user ?: $this->
getUser();
291 $block = $user->getBlock();
293 if ( $this->mTargetObj !==
null ) {
294 return $this->permissionManager->userCan( $permission, $user, $this->mTargetObj );
296 $hasRight = $this->permissionManager->userHasRight( $user, $permission );
297 $sitewideBlock = $block && $block->isSitewide();
298 return $permission ===
'undelete' ? ( $hasRight && !$sitewideBlock ) : $hasRight;
303 return $this->
isAllowed( $this->mRestriction, $user );
314 if ( !parent::userCanExecute( $user ) ) {
321 $this->mTargetObj && $this->permissionManager->isBlockedFrom( $user, $this->mTargetObj ) ) {
339 $this->
addHelpLink(
'Help:Deletion_and_undeletion' );
341 $this->loadRequest( $par );
346 if ( $this->mTargetObj ===
null ) {
347 $out->addWikiMsg(
'undelete-header' );
349 # Not all users can just browse every deleted page from the list
350 if ( $this->permissionManager->userHasRight( $user,
'browsearchive' ) ) {
351 $this->showSearchForm();
358 if ( $this->mAllowed ) {
359 $out->setPageTitleMsg( $this->
msg(
'undeletepage' ) );
361 $out->setPageTitleMsg( $this->
msg(
'viewdeletedpage' ) );
364 $this->
getSkin()->setRelevantTitle( $this->mTargetObj );
366 if ( $this->mTimestamp !==
'' ) {
367 $this->showRevision( $this->mTimestamp );
368 } elseif ( $this->mFilename !==
null && $this->mTargetObj->inNamespace(
NS_FILE ) ) {
369 $file =
new ArchivedFile( $this->mTargetObj, 0, $this->mFilename );
371 if ( !$file->exists() ) {
372 $out->addWikiMsg(
'filedelete-nofile', $this->mFilename );
373 } elseif ( !$file->userCan( File::DELETED_FILE, $user ) ) {
374 if ( $file->isDeleted( File::DELETED_RESTRICTED ) ) {
379 } elseif ( !$user->matchEditToken( $this->mToken, $this->mFilename ) ) {
380 $this->showFileConfirmationForm( $this->mFilename );
382 $this->showFile( $this->mFilename );
384 } elseif ( $this->mAction ===
'submit' ) {
385 if ( $this->mRestore ) {
387 } elseif ( $this->mRevdel ) {
388 $this->redirectToRevDel();
390 } elseif ( $this->mAction ===
'render' ) {
401 private function redirectToRevDel() {
404 foreach ( $this->
getRequest()->getValues() as $key => $val ) {
406 if ( preg_match(
"/^ts(\d{14})$/", $key,
$matches ) ) {
407 $revisionRecord = $this->archivedRevisionLookup
408 ->getRevisionRecordByTimestamp( $this->mTargetObj,
$matches[1] );
409 if ( $revisionRecord ) {
411 $revisions[ $revisionRecord->getId() ] = 1;
417 'type' =>
'revision',
419 'target' => $this->mTargetObj->getPrefixedText()
425 private function showSearchForm() {
427 $out->setPageTitleMsg( $this->
msg(
'undelete-search-title' ) );
428 $fuzzySearch = $this->
getRequest()->getVal(
'fuzzy',
'1' );
433 $fields[] =
new ActionFieldLayout(
434 new TextInputWidget( [
436 'inputId' =>
'prefix',
438 'value' => $this->mSearchPrefix,
441 new ButtonInputWidget( [
442 'label' => $this->
msg(
'undelete-search-submit' )->text(),
443 'flags' => [
'primary',
'progressive' ],
444 'inputId' =>
'searchUndelete',
448 'label' =>
new HtmlSnippet(
450 $fuzzySearch ?
'undelete-search-full' :
'undelete-search-prefix'
457 $fieldset =
new FieldsetLayout( [
458 'label' => $this->
msg(
'undelete-search-box' )->text(),
462 $form =
new FormLayout( [
467 $form->appendContent(
470 Html::hidden(
'title', $this->
getPageTitle()->getPrefixedDBkey() ) .
471 Html::hidden(
'fuzzy', $fuzzySearch )
484 # List undeletable articles
485 if ( $this->mSearchPrefix ) {
488 if ( $fuzzySearch ) {
493 $this->showList( $result );
503 private function showList( $result ) {
506 if ( $result->numRows() == 0 ) {
507 $out->addWikiMsg(
'undelete-no-results' );
512 $out->addWikiMsg(
'undeletepagetext', $this->
getLanguage()->formatNum( $result->numRows() ) );
516 $out->addHTML(
"<ul id='undeleteResultsList'>\n" );
517 foreach ( $result as $row ) {
518 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
519 if ( $title !==
null ) {
520 $item = $linkRenderer->makeKnownLink(
522 $title->getPrefixedText(),
524 [
'target' => $title->getPrefixedText() ]
530 [
'class' =>
'mw-invalidtitle' ],
531 Linker::getInvalidTitleDescription(
538 $revs = $this->
msg(
'undeleterevisions' )->numParams( $row->count )->parse();
542 [
'class' =>
'undeleteResult' ],
543 $item . $this->
msg(
'word-separator' )->escaped() .
544 $this->
msg(
'parentheses' )->rawParams( $revs )->escaped()
549 $out->addHTML(
"</ul>\n" );
554 private function showRevision( $timestamp ) {
555 if ( !preg_match(
'/[0-9]{14}/', $timestamp ) ) {
559 $out->addModuleStyles(
'mediawiki.interface.helpers.styles' );
565 $this->
msg(
'undelete-back-to-list' )->text(),
567 [
'target' => $this->mTargetObj->getPrefixedText() ]
570 $subtitle =
"< $listLink";
571 $out->setSubtitle( $subtitle );
576 $archive, $this->mTargetObj )
580 $revRecord = $this->archivedRevisionLookup->getRevisionRecordByTimestamp( $this->mTargetObj, $timestamp );
585 $out->addWikiMsg(
'undeleterevision-missing' );
589 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
591 $titleText = $this->mTargetObj->getPrefixedDBkey();
592 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
593 $msg = $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
594 ? [
'rev-suppressed-text-permission', $titleText ]
595 : [
'rev-deleted-text-permission', $titleText ];
598 $this->
msg( $msg[0], $msg[1] )->parse(),
605 $msg = $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
606 ? [
'rev-suppressed-text-view', $titleText ]
607 : [
'rev-deleted-text-view', $titleText ];
610 $this->
msg( $msg[0], $msg[1] )->parse(),
617 if ( $this->mDiff ) {
618 $previousRevRecord = $this->archivedRevisionLookup
619 ->getPreviousRevisionRecord( $this->mTargetObj, $timestamp );
620 if ( $previousRevRecord ) {
621 $this->showDiff( $previousRevRecord, $revRecord );
622 if ( $this->mDiffOnly ) {
626 $out->addHTML(
'<hr />' );
628 $out->addWikiMsg(
'undelete-nodiff' );
633 $this->
getPageTitle( $this->mTargetObj->getPrefixedDBkey() ),
634 $this->mTargetObj->getPrefixedText()
641 $time = $lang->userTimeAndDate( $timestamp, $user );
642 $d = $lang->userDate( $timestamp, $user );
643 $t = $lang->userTime( $timestamp, $user );
644 $userLink = Linker::revUserTools( $revRecord );
646 $content = $revRecord->getContent(
648 RevisionRecord::FOR_THIS_USER,
655 $undeleteRevisionContent =
'';
657 if ( !$this->mDiff ) {
658 $revdel = Linker::getRevDeleteLink(
664 $undeleteRevisionContent = $revdel .
' ';
668 $undeleteRevisionContent .= $out->msg(
677 if ( $this->mPreview || $isText ) {
680 $undeleteRevisionContent,
681 'mw-undelete-revision'
688 [
'class' =>
'mw-undelete-revision', ],
689 $undeleteRevisionContent
694 if ( $this->mPreview || !$isText ) {
697 $popts = $out->parserOptions();
699 $rendered = $this->revisionRenderer->getRenderedRevision(
703 [
'audience' => RevisionRecord::FOR_THIS_USER,
'causeAction' =>
'undelete-preview' ]
708 $pout = $rendered->getRevisionParserOutput();
710 $out->addParserOutput( $pout, [
711 'enableSectionEditLinks' =>
false,
719 '@phan-var TextContent $content';
723 'readonly' =>
'readonly',
726 ], $content->getText() .
"\n" );
728 $buttonFields[] =
new ButtonInputWidget( [
731 'label' => $this->
msg(
'showpreview' )->text()
737 $buttonFields[] =
new ButtonInputWidget( [
740 'label' => $this->
msg(
'showdiff' )->text()
745 Xml::openElement(
'div', [
746 'style' =>
'clear: both' ] ) .
747 Xml::openElement(
'form', [
749 'action' => $this->
getPageTitle()->getLocalURL( [
'action' =>
'submit' ] ) ] ) .
750 Xml::element(
'input', [
753 'value' => $this->mTargetObj->getPrefixedDBkey() ] ) .
754 Xml::element(
'input', [
756 'name' =>
'timestamp',
757 'value' => $timestamp ] ) .
758 Xml::element(
'input', [
760 'name' =>
'wpEditToken',
761 'value' => $user->getEditToken() ] ) .
764 'content' =>
new HorizontalLayout( [
765 'items' => $buttonFields
769 Xml::closeElement(
'form' ) .
770 Xml::closeElement(
'div' )
781 private function showDiff(
782 RevisionRecord $previousRevRecord,
783 RevisionRecord $currentRevRecord
785 $currentTitle = Title::newFromLinkTarget( $currentRevRecord->getPageAsLinkTarget() );
787 $diffContext =
new DerivativeContext( $this->
getContext() );
788 $diffContext->setTitle( $currentTitle );
789 $diffContext->setWikiPage( $this->wikiPageFactory->newFromTitle( $currentTitle ) );
791 $contentModel = $currentRevRecord->getSlot(
796 $diffEngine = $this->contentHandlerFactory->getContentHandler( $contentModel )
797 ->createDifferenceEngine( $diffContext );
799 $diffEngine->setRevisions( $previousRevRecord, $currentRevRecord );
800 $diffEngine->showDiffStyle();
801 $formattedDiff = $diffEngine->getDiff(
802 $this->diffHeader( $previousRevRecord,
'o' ),
803 $this->diffHeader( $currentRevRecord,
'n' )
806 $this->
getOutput()->addHTML(
"<div>$formattedDiff</div>\n" );
814 private function diffHeader( RevisionRecord $revRecord, $prefix ) {
815 if ( $revRecord instanceof RevisionArchiveRecord ) {
819 'target' => $this->mTargetObj->getPrefixedText(),
820 'timestamp' =>
wfTimestamp( TS_MW, $revRecord->getTimestamp() )
824 $targetPage = $revRecord->getPageAsLinkTarget();
825 $targetQuery = [
'oldid' => $revRecord->getId() ];
831 $rdel = Linker::getRevDeleteLink( $user, $revRecord, $this->mTargetObj );
839 $dbr = $this->dbProvider->getReplicaDatabase();
840 $tagIds = $dbr->newSelectQueryBuilder()
841 ->select(
'ct_tag_id' )
842 ->from(
'change_tag' )
843 ->where( [
'ct_rev_id' => $revRecord->getId() ] )
844 ->caller( __METHOD__ )->fetchFieldValues();
846 foreach ( $tagIds as $tagId ) {
848 $tags[] = $this->changeTagDefStore->getName( (
int)$tagId );
849 }
catch ( NameTableAccessException $exception ) {
853 $tags = implode(
',', $tags );
859 $lang->userTimeAndDate( $revRecord->getTimestamp(), $user ),
860 $lang->userDate( $revRecord->getTimestamp(), $user ),
861 $lang->userTime( $revRecord->getTimestamp(), $user )
866 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
867 $asof = Html::rawElement(
869 [
'class' => Linker::getRevisionDeletedClass( $revRecord ) ],
876 return '<div id="mw-diff-' . $prefix .
'title1"><strong>' .
879 '<div id="mw-diff-' . $prefix .
'title2">' .
880 Linker::revUserTools( $revRecord ) .
'<br />' .
882 '<div id="mw-diff-' . $prefix .
'title3">' .
883 $minor . $this->commentFormatter->formatRevision( $revRecord, $user ) . $rdel .
'<br />' .
885 '<div id="mw-diff-' . $prefix .
'title5">' .
886 $tagSummary[0] .
'<br />' .
894 private function showFileConfirmationForm( $key ) {
898 $file =
new ArchivedFile( $this->mTargetObj, 0, $this->mFilename );
899 $out->addWikiMsg(
'undelete-show-file-confirm',
900 $this->mTargetObj->getText(),
901 $lang->userDate( $file->getTimestamp(), $user ),
902 $lang->userTime( $file->getTimestamp(), $user ) );
904 Xml::openElement(
'form', [
907 'target' => $this->mTarget,
909 'token' => $user->getEditToken( $key ),
913 Xml::submitButton( $this->msg(
'undelete-show-file-submit' )->text() ) .
922 private function showFile( $key ) {
925 # We mustn't allow the output to be CDN cached, otherwise
926 # if an admin previews a deleted image, and it's cached, then
927 # a user without appropriate permissions can toddle off and
928 # nab the image, and CDN will serve it
930 $response->header(
'Expires: ' . gmdate(
'D, d M Y H:i:s', 0 ) .
' GMT' );
931 $response->header(
'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
933 $path = $this->localRepo->getZonePath(
'deleted' ) .
'/' . $this->localRepo->getDeletedHashPath( $key ) . $key;
934 $this->localRepo->streamFileWithStatus(
$path );
941 private function addRevisionsToBatch( LinkBatch $batch, IResultWrapper $revisions ) {
942 foreach ( $revisions as $row ) {
943 $batch->add(
NS_USER, $row->ar_user_text );
952 private function addFilesToBatch( LinkBatch $batch, IResultWrapper $files ) {
953 foreach ( $files as $row ) {
954 $batch->add(
NS_USER, $row->fa_user_text );
964 $out->setArticleBodyOnly(
true );
965 $dbr = $this->dbProvider->getReplicaDatabase();
966 if ( $this->mHistoryOffset ) {
967 $extraConds = [ $dbr->expr(
'ar_timestamp',
'<', $dbr->timestamp( $this->mHistoryOffset ) ) ];
971 $revisions = $this->archivedRevisionLookup->listRevisions(
974 self::REVISION_HISTORY_LIMIT + 1
976 $batch = $this->linkBatchFactory->newLinkBatch();
977 $this->addRevisionsToBatch( $batch, $revisions );
981 if ( $revisions->numRows() > self::REVISION_HISTORY_LIMIT ) {
983 $out->setStatusCode( 206 );
994 $history = Html::openElement(
'ul', [
'class' =>
'mw-undelete-revlist' ] );
997 $numRevisions = $revisions->
numRows();
998 $displayCount = min( $numRevisions, self::REVISION_HISTORY_LIMIT );
999 $firstRev = $this->revisionStore->getFirstRevision( $this->mTargetObj );
1000 $earliestLiveTime = $firstRev ? $firstRev->getTimestamp() :
null;
1002 $revisions->rewind();
1003 for ( $i = 0; $i < $displayCount; $i++ ) {
1007 $history .= $this->
formatRevisionRow( $row, $earliestLiveTime, $numRevisions - $i );
1009 $history .= Html::closeElement(
'ul' );
1017 if ( $this->mAllowed ) {
1018 $out->addModules(
'mediawiki.misc-authed-ooui' );
1019 $out->addModuleStyles(
'mediawiki.special' );
1021 $out->addModuleStyles(
'mediawiki.interface.helpers.styles' );
1023 "<div class='mw-undelete-pagetitle'>\n$1\n</div>\n",
1024 [
'undeletepagetitle',
wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) ]
1029 $this->
getHookRunner()->onUndeleteForm__showHistory( $archive, $this->mTargetObj );
1031 $out->addHTML( Html::openElement(
'div', [
'class' =>
'mw-undelete-history' ] ) );
1032 if ( $this->mAllowed ) {
1033 $out->addWikiMsg(
'undeletehistory' );
1034 $out->addWikiMsg(
'undeleterevdel' );
1036 $out->addWikiMsg(
'undeletehistorynoadmin' );
1038 $out->addHTML( Html::closeElement(
'div' ) );
1040 # List all stored revisions
1041 $revisions = $this->archivedRevisionLookup->listRevisions(
1044 self::REVISION_HISTORY_LIMIT + 1
1046 $files = $archive->listFiles();
1047 $numRevisions = $revisions->numRows();
1048 $showLoadMore = $numRevisions > self::REVISION_HISTORY_LIMIT;
1049 $haveRevisions = $numRevisions > 0;
1050 $haveFiles = $files && $files->numRows() > 0;
1052 # Batch existence check on user and talk pages
1053 if ( $haveRevisions || $haveFiles ) {
1054 $batch = $this->linkBatchFactory->newLinkBatch();
1055 $this->addRevisionsToBatch( $batch, $revisions );
1058 $this->addFilesToBatch( $batch, $files );
1063 if ( $this->mAllowed ) {
1066 $action = $this->
getPageTitle()->getLocalURL( [
'action' =>
'submit' ] );
1067 # Start the form here
1068 $form =
new FormLayout( [
1070 'action' => $action,
1075 # Show relevant lines from the deletion log:
1076 $deleteLogPage =
new LogPage(
'delete' );
1077 $out->addHTML( Xml::element(
'h2',
null, $deleteLogPage->getName()->text() ) .
"\n" );
1078 LogEventsList::showLogExtract( $out,
'delete', $this->mTargetObj );
1079 # Show relevant lines from the suppression log:
1080 $suppressLogPage =
new LogPage(
'suppress' );
1081 if ( $this->permissionManager->userHasRight( $this->getUser(),
'suppressionlog' ) ) {
1082 $out->addHTML( Xml::element(
'h2',
null, $suppressLogPage->getName()->text() ) .
"\n" );
1083 LogEventsList::showLogExtract( $out,
'suppress', $this->mTargetObj );
1086 if ( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
1087 $unsuppressAllowed = $this->permissionManager->userHasRight( $this->
getUser(),
'suppressrevision' );
1089 $fields[] =
new Layout( [
1090 'content' =>
new HtmlSnippet( $this->
msg(
'undeleteextrahelp' )->parseAsBlock() )
1093 $dropdownComment = $this->
msg(
'undelete-comment-dropdown' )
1094 ->page( $this->mTargetObj )->inContentLanguage()->text();
1096 if ( $unsuppressAllowed ) {
1097 $dropdownComment .=
"\n" . $this->
msg(
'undelete-comment-dropdown-unsuppress' )
1098 ->page( $this->mTargetObj )->inContentLanguage()->text();
1100 $options = Xml::listDropdownOptions(
1102 [
'other' => $this->
msg(
'undeletecommentotherlist' )->text() ]
1104 $options = Xml::listDropdownOptionsOoui( $options );
1106 $fields[] =
new FieldLayout(
1107 new DropdownInputWidget( [
1108 'name' =>
'wpCommentList',
1109 'inputId' =>
'wpCommentList',
1110 'infusable' =>
true,
1111 'value' => $this->
getRequest()->getText(
'wpCommentList',
'other' ),
1112 'options' => $options,
1115 'label' => $this->
msg(
'undeletecomment' )->text(),
1120 $fields[] =
new FieldLayout(
1121 new TextInputWidget( [
1122 'name' =>
'wpComment',
1123 'inputId' =>
'wpComment',
1124 'infusable' =>
true,
1125 'value' => $this->
getRequest()->getText(
'wpComment' ),
1126 'autofocus' =>
true,
1130 'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT,
1133 'label' => $this->
msg(
'undeleteothercomment' )->text(),
1138 if ( $unsuppressAllowed ) {
1139 $fields[] =
new FieldLayout(
1140 new CheckboxInputWidget( [
1141 'name' =>
'wpUnsuppress',
1142 'inputId' =>
'mw-undelete-unsuppress',
1146 'label' => $this->
msg(
'revdelete-unsuppress' )->text(),
1147 'align' =>
'inline',
1152 $undelPage = $this->undeletePageFactory->newUndeletePage(
1153 $this->wikiPageFactory->newFromTitle( $this->mTargetObj ),
1154 $this->getContext()->getAuthority()
1156 if ( $undelPage->canProbablyUndeleteAssociatedTalk()->isGood() ) {
1157 $fields[] =
new FieldLayout(
1158 new CheckboxInputWidget( [
1159 'name' =>
'undeletetalk',
1160 'inputId' =>
'mw-undelete-undeletetalk',
1161 'selected' =>
false,
1164 'label' => $this->
msg(
'undelete-undeletetalk' )->text(),
1165 'align' =>
'inline',
1170 $fields[] =
new FieldLayout(
1172 'content' =>
new HorizontalLayout( [
1174 new ButtonInputWidget( [
1175 'name' =>
'restore',
1176 'inputId' =>
'mw-undelete-submit',
1178 'label' => $this->
msg(
'undeletebtn' )->text(),
1179 'flags' => [
'primary',
'progressive' ],
1182 new ButtonInputWidget( [
1184 'inputId' =>
'mw-undelete-invert',
1186 'label' => $this->
msg(
'undeleteinvert' )->text()
1193 $fieldset =
new FieldsetLayout( [
1194 'label' => $this->
msg(
'undelete-fieldset-title' )->text(),
1195 'id' =>
'mw-undelete-table',
1201 if ( $unsuppressAllowed ) {
1203 $this->
msg(
'undelete-comment-dropdown-unsuppress' )->inContentLanguage()->
getTitle(),
1204 $this->
msg(
'undelete-edit-commentlist-unsuppress' )->text(),
1206 [
'action' =>
'edit' ]
1208 $link .= $this->
msg(
'pipe-separator' )->escaped();
1211 $this->
msg(
'undelete-comment-dropdown' )->inContentLanguage()->
getTitle(),
1212 $this->
msg(
'undelete-edit-commentlist' )->text(),
1214 [
'action' =>
'edit' ]
1217 $link = Html::rawElement(
'p', [
'class' =>
'mw-undelete-editcomments' ], $link );
1221 $form->appendContent(
1223 'expanded' =>
false,
1226 'content' => $fieldset,
1230 Html::hidden(
'target', $this->mTarget ) .
1231 Html::hidden(
'wpEditToken', $this->
getUser()->getEditToken() )
1237 $history .= Xml::element(
'h2',
null, $this->
msg(
'history' )->text() ) .
"\n";
1239 if ( $haveRevisions ) {
1240 # Show the page's stored (deleted) history
1242 if ( $this->permissionManager->userHasRight( $this->getUser(),
'deleterevision' ) ) {
1248 'class' =>
'deleterevision-log-submit mw-log-deleterevision-button'
1250 $this->
msg(
'showhideselectedversions' )->text()
1256 if ( $showLoadMore ) {
1258 Html::openElement(
'div' ) .
1261 [
'id' =>
'mw-load-more-revisions' ],
1262 $this->
msg(
'undelete-load-more-revisions' )->text()
1264 Html::closeElement(
'div' ) .
1268 $out->addWikiMsg(
'nohistory' );
1272 $history .= Xml::element(
'h2',
null, $this->
msg(
'filehist' )->text() ) .
"\n";
1273 $history .= Html::openElement(
'ul', [
'class' =>
'mw-undelete-revlist' ] );
1274 foreach ( $files as $row ) {
1275 $history .= $this->formatFileRow( $row );
1278 $history .= Html::closeElement(
'ul' );
1281 if ( $this->mAllowed ) {
1282 # Slip in the hidden controls here
1283 $misc = Html::hidden(
'target', $this->mTarget );
1284 $misc .= Html::hidden(
'wpEditToken', $this->
getUser()->getEditToken() );
1288 $form->appendContent(
new HtmlSnippet( $history ) );
1290 $out->addHTML( (
string)$form );
1292 $out->addHTML( $history );
1299 $revRecord = $this->revisionStore->newRevisionFromArchiveRow(
1301 IDBAccessObject::READ_NORMAL,
1308 if ( $this->mAllowed ) {
1309 if ( $this->mInvert ) {
1310 if ( in_array( $ts, $this->mTargetTimestamp ) ) {
1311 $checkBox = Xml::check(
"ts$ts" );
1313 $checkBox = Xml::check(
"ts$ts",
true );
1316 $checkBox = Xml::check(
"ts$ts" );
1324 if ( $this->mCanView ) {
1327 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1328 $pageLink = htmlspecialchars( $this->
getLanguage()->userTimeAndDate( $ts, $user ) );
1329 $last = $this->
msg(
'diff' )->escaped();
1330 } elseif ( $remaining > 0 || ( $earliestLiveTime && $ts > $earliestLiveTime ) ) {
1331 $pageLink = $this->getPageLink( $revRecord, $titleObj, $ts );
1334 $this->
msg(
'diff' )->text(),
1337 'target' => $this->mTargetObj->getPrefixedText(),
1343 $pageLink = $this->getPageLink( $revRecord, $titleObj, $ts );
1344 $last = $this->
msg(
'diff' )->escaped();
1347 $pageLink = htmlspecialchars( $this->
getLanguage()->userTimeAndDate( $ts, $user ) );
1348 $last = $this->
msg(
'diff' )->escaped();
1352 $userLink = Linker::revUserTools( $revRecord );
1355 $minor = $revRecord->isMinor() ? ChangesList::flag(
'minor' ) :
'';
1358 $size = $row->ar_len;
1359 if ( $size !==
null ) {
1360 $revTextSize = Linker::formatRevisionSize( $size );
1364 $comment = $this->commentFormatter->formatRevision( $revRecord, $user );
1374 $attribs[
'class'] = implode(
' ', $classes );
1377 $revisionRow = $this->
msg(
'undelete-revision-row2' )
1390 return Xml::tags(
'li', $attribs, $revisionRow ) .
"\n";
1393 private function formatFileRow( $row ) {
1399 if ( $this->mCanView && $row->fa_storage_key ) {
1400 if ( $this->mAllowed ) {
1401 $checkBox = Xml::check(
'fileid' . $row->fa_id );
1403 $key = urlencode( $row->fa_storage_key );
1404 $pageLink = $this->getFileLink( $file, $this->
getPageTitle(), $ts, $key );
1406 $pageLink = htmlspecialchars( $this->
getLanguage()->userTimeAndDate( $ts, $user ) );
1408 $userLink = $this->getFileUser( $file );
1409 $data = $this->
msg(
'widthheight' )->numParams( $row->fa_width, $row->fa_height )->text();
1410 $bytes = $this->
msg(
'parentheses' )
1411 ->plaintextParams( $this->
msg(
'nbytes' )->numParams( $row->fa_size )->text() )
1413 $data = htmlspecialchars( $data .
' ' . $bytes );
1414 $comment = $this->getFileComment( $file );
1417 $canHide = $this->
isAllowed(
'deleterevision' );
1418 if ( $canHide || ( $file->getVisibility() && $this->isAllowed(
'deletedhistory' ) ) ) {
1419 if ( !$file->userCan( File::DELETED_RESTRICTED, $user ) ) {
1421 $revdlink = Linker::revDeleteLinkDisabled( $canHide );
1424 'type' =>
'filearchive',
1425 'target' => $this->mTargetObj->getPrefixedDBkey(),
1426 'ids' => $row->fa_id
1428 $revdlink = Linker::revDeleteLink( $query,
1429 $file->isDeleted( File::DELETED_RESTRICTED ), $canHide );
1435 return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
1446 private function getPageLink( RevisionRecord $revRecord, $titleObj, $ts ) {
1448 $time = $this->
getLanguage()->userTimeAndDate( $ts, $user );
1450 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1454 [
'class' =>
'history-deleted' ],
1464 'target' => $this->mTargetObj->getPrefixedText(),
1469 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1470 $class = Linker::getRevisionDeletedClass( $revRecord );
1471 $link =
'<span class="' . $class .
'">' . $link .
'</span>';
1487 private function getFileLink( $file, $titleObj, $ts, $key ) {
1489 $time = $this->
getLanguage()->userTimeAndDate( $ts, $user );
1491 if ( !$file->userCan( File::DELETED_FILE, $user ) ) {
1494 [
'class' =>
'history-deleted' ],
1499 if ( $file->exists() ) {
1505 'target' => $this->mTargetObj->getPrefixedText(),
1507 'token' => $user->getEditToken( $key )
1511 $link = htmlspecialchars( $time );
1514 if ( $file->isDeleted( File::DELETED_FILE ) ) {
1515 $link =
'<span class="history-deleted">' . $link .
'</span>';
1527 private function getFileUser( $file ) {
1528 $uploader = $file->getUploader( File::FOR_THIS_USER, $this->
getAuthority() );
1530 return Html::rawElement(
1532 [
'class' =>
'history-deleted' ],
1533 $this->
msg(
'rev-deleted-user' )->escaped()
1537 $link = Linker::userLink( $uploader->getId(), $uploader->getName() ) .
1538 Linker::userToolLinks( $uploader->getId(), $uploader->getName() );
1540 if ( $file->isDeleted( File::DELETED_USER ) ) {
1541 $link = Html::rawElement(
1543 [
'class' =>
'history-deleted' ],
1557 private function getFileComment( $file ) {
1558 if ( !$file->userCan( File::DELETED_COMMENT, $this->getAuthority() ) ) {
1559 return Html::rawElement(
1561 [
'class' =>
'history-deleted' ],
1564 [
'class' =>
'comment' ],
1565 $this->
msg(
'rev-deleted-comment' )->escaped()
1570 $comment = $file->getDescription( File::FOR_THIS_USER, $this->
getAuthority() );
1571 $link = $this->commentFormatter->formatBlock( $comment );
1573 if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
1574 $link = Html::rawElement(
1576 [
'class' =>
'history-deleted' ],
1584 private function undelete() {
1586 && $this->mTargetObj->getNamespace() ===
NS_FILE
1588 throw new ErrorPageError(
'undelete-error',
'filedelete-maintenance' );
1594 $undeletePage = $this->undeletePageFactory->newUndeletePage(
1595 $this->wikiPageFactory->newFromTitle( $this->mTargetObj ),
1596 $this->getAuthority()
1598 if ( $this->mUndeleteTalk && $undeletePage->canProbablyUndeleteAssociatedTalk()->isGood() ) {
1599 $undeletePage->setUndeleteAssociatedTalk(
true );
1601 $status = $undeletePage
1602 ->setUndeleteOnlyTimestamps( $this->mTargetTimestamp )
1603 ->setUndeleteOnlyFileVersions( $this->mFileVersions )
1604 ->setUnsuppress( $this->mUnsuppress )
1606 ->undeleteIfAllowed( $this->mComment );
1608 if ( !$status->isGood() ) {
1609 $out->setPageTitleMsg( $this->
msg(
'undelete-error' ) );
1610 $out->wrapWikiTextAsInterface(
1612 Status::wrap( $status )->getWikiText(
1621 $restoredRevs = $status->getValue()[UndeletePage::REVISIONS_RESTORED];
1622 $restoredFiles = $status->getValue()[UndeletePage::FILES_RESTORED];
1624 if ( $restoredRevs === 0 && $restoredFiles === 0 ) {
1626 $out->setPageTitleMsg( $this->
msg(
'undelete-error' ) );
1628 if ( $status->getValue()[UndeletePage::FILES_RESTORED] !== 0 ) {
1630 $this->mTargetObj, $this->mFileVersions, $this->
getUser(), $this->mComment );
1647 return $this->
prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );