98 private const REVISION_HISTORY_LIMIT = 500;
108 private $mTargetTimestamp = [];
112 private $mComment =
'';
121 private $mUnsuppress;
123 private $mFileVersions = [];
125 private $mUndeleteTalk;
127 private $mHistoryOffset;
134 private $mSearchPrefix;
186 parent::__construct(
'Undelete',
'deletedhistory' );
187 $this->permissionManager = $permissionManager;
188 $this->revisionStore = $revisionStore;
189 $this->revisionRenderer = $revisionRenderer;
190 $this->contentHandlerFactory = $contentHandlerFactory;
191 $this->changeTagDefStore = $changeTagDefStore;
192 $this->linkBatchFactory = $linkBatchFactory;
194 $this->dbProvider = $dbProvider;
195 $this->userOptionsLookup = $userOptionsLookup;
196 $this->wikiPageFactory = $wikiPageFactory;
197 $this->searchEngineFactory = $searchEngineFactory;
198 $this->undeletePageFactory = $undeletePageFactory;
199 $this->archivedRevisionLookup = $archivedRevisionLookup;
200 $this->commentFormatter = $commentFormatter;
201 $this->watchlistManager = $watchlistManager;
208 private function loadRequest( $par ) {
212 $this->mAction = $request->getRawVal(
'action' );
213 if ( $par !==
null && $par !==
'' ) {
214 $this->mTarget = $par;
216 $this->mTarget = $request->getVal(
'target' );
219 $this->mTargetObj =
null;
221 if ( $this->mTarget !==
null && $this->mTarget !==
'' ) {
222 $this->mTargetObj = Title::newFromText( $this->mTarget );
225 $this->mSearchPrefix = $request->getText(
'prefix' );
226 $time = $request->getVal(
'timestamp' );
227 $this->mTimestamp = $time ?
wfTimestamp( TS_MW, $time ) :
'';
228 $this->mFilename = $request->getVal(
'file' );
230 $posted = $request->wasPosted() &&
231 $user->matchEditToken( $request->getVal(
'wpEditToken' ) );
232 $this->mRestore = $request->getCheck(
'restore' ) && $posted;
233 $this->mRevdel = $request->getCheck(
'revdel' ) && $posted;
234 $this->mInvert = $request->getCheck(
'invert' ) && $posted;
235 $this->mPreview = $request->getCheck(
'preview' ) && $posted;
236 $this->mDiff = $request->getCheck(
'diff' );
237 $this->mDiffOnly = $request->getBool(
'diffonly',
238 $this->userOptionsLookup->getOption( $this->getUser(),
'diffonly' ) );
239 $commentList = $request->getText(
'wpCommentList',
'other' );
240 $comment = $request->getText(
'wpComment' );
241 if ( $commentList ===
'other' ) {
242 $this->mComment = $comment;
243 } elseif ( $comment !==
'' ) {
244 $this->mComment = $commentList . $this->
msg(
'colon-separator' )->inContentLanguage()->text() . $comment;
246 $this->mComment = $commentList;
248 $this->mUnsuppress = $request->getVal(
'wpUnsuppress' ) &&
249 $this->permissionManager->userHasRight( $user,
'suppressrevision' );
250 $this->mToken = $request->getVal(
'token' );
251 $this->mUndeleteTalk = $request->getCheck(
'undeletetalk' );
252 $this->mHistoryOffset = $request->getVal(
'historyoffset' );
255 $this->mAllowed =
true;
256 $this->mCanView =
true;
257 } elseif ( $this->
isAllowed(
'deletedtext' ) ) {
258 $this->mAllowed =
false;
259 $this->mCanView =
true;
260 $this->mRestore =
false;
262 $this->mAllowed =
false;
263 $this->mCanView =
false;
264 $this->mTimestamp =
'';
265 $this->mRestore =
false;
268 if ( $this->mRestore || $this->mInvert ) {
270 $this->mFileVersions = [];
271 foreach ( $request->getValues() as $key => $val ) {
273 if ( preg_match(
'/^ts(\d{14})$/', $key,
$matches ) ) {
277 if ( preg_match(
'/^fileid(\d+)$/', $key,
$matches ) ) {
278 $this->mFileVersions[] = intval(
$matches[1] );
281 rsort( $timestamps );
282 $this->mTargetTimestamp = $timestamps;
295 $user = $user ?: $this->
getUser();
296 $block = $user->getBlock();
298 if ( $this->mTargetObj !==
null ) {
299 return $this->permissionManager->userCan( $permission, $user, $this->mTargetObj );
301 $hasRight = $this->permissionManager->userHasRight( $user, $permission );
302 $sitewideBlock = $block && $block->isSitewide();
303 return $permission ===
'undelete' ? ( $hasRight && !$sitewideBlock ) : $hasRight;
308 return $this->
isAllowed( $this->mRestriction, $user );
319 if ( !parent::userCanExecute( $user ) ) {
326 $this->mTargetObj && $this->permissionManager->isBlockedFrom( $user, $this->mTargetObj ) ) {
344 $this->
addHelpLink(
'Help:Deletion_and_undeletion' );
346 $this->loadRequest( $par );
351 if ( $this->mTargetObj ===
null ) {
352 $out->addWikiMsg(
'undelete-header' );
354 # Not all users can just browse every deleted page from the list
355 if ( $this->permissionManager->userHasRight( $user,
'browsearchive' ) ) {
356 $this->showSearchForm();
363 if ( $this->mAllowed ) {
364 $out->setPageTitleMsg( $this->
msg(
'undeletepage' ) );
366 $out->setPageTitleMsg( $this->
msg(
'viewdeletedpage' ) );
369 $this->
getSkin()->setRelevantTitle( $this->mTargetObj );
371 if ( $this->mTimestamp !==
'' ) {
372 $this->showRevision( $this->mTimestamp );
373 } elseif ( $this->mFilename !==
null && $this->mTargetObj->inNamespace(
NS_FILE ) ) {
374 $file =
new ArchivedFile( $this->mTargetObj, 0, $this->mFilename );
376 if ( !$file->exists() ) {
377 $out->addWikiMsg(
'filedelete-nofile', $this->mFilename );
378 } elseif ( !$file->userCan( File::DELETED_FILE, $user ) ) {
379 if ( $file->isDeleted( File::DELETED_RESTRICTED ) ) {
384 } elseif ( !$user->matchEditToken( $this->mToken, $this->mFilename ) ) {
385 $this->showFileConfirmationForm( $this->mFilename );
387 $this->showFile( $this->mFilename );
389 } elseif ( $this->mAction ===
'submit' ) {
390 if ( $this->mRestore ) {
392 } elseif ( $this->mRevdel ) {
393 $this->redirectToRevDel();
395 } elseif ( $this->mAction ===
'render' ) {
406 private function redirectToRevDel() {
409 foreach ( $this->
getRequest()->getValues() as $key => $val ) {
411 if ( preg_match(
"/^ts(\d{14})$/", $key,
$matches ) ) {
412 $revisionRecord = $this->archivedRevisionLookup
413 ->getRevisionRecordByTimestamp( $this->mTargetObj,
$matches[1] );
414 if ( $revisionRecord ) {
416 $revisions[ $revisionRecord->getId() ] = 1;
422 'type' =>
'revision',
424 'target' => $this->mTargetObj->getPrefixedText()
430 private function showSearchForm() {
432 $out->setPageTitleMsg( $this->
msg(
'undelete-search-title' ) );
433 $fuzzySearch = $this->
getRequest()->getVal(
'fuzzy',
'1' );
438 $fields[] =
new ActionFieldLayout(
439 new TextInputWidget( [
441 'inputId' =>
'prefix',
443 'value' => $this->mSearchPrefix,
446 new ButtonInputWidget( [
447 'label' => $this->
msg(
'undelete-search-submit' )->text(),
448 'flags' => [
'primary',
'progressive' ],
449 'inputId' =>
'searchUndelete',
453 'label' =>
new HtmlSnippet(
455 $fuzzySearch ?
'undelete-search-full' :
'undelete-search-prefix'
462 $fieldset =
new FieldsetLayout( [
463 'label' => $this->
msg(
'undelete-search-box' )->text(),
467 $form =
new FormLayout( [
472 $form->appendContent(
475 Html::hidden(
'title', $this->
getPageTitle()->getPrefixedDBkey() ) .
476 Html::hidden(
'fuzzy', $fuzzySearch )
489 # List undeletable articles
490 if ( $this->mSearchPrefix ) {
493 if ( $fuzzySearch ) {
498 $this->showList( $result );
508 private function showList( $result ) {
511 if ( $result->numRows() == 0 ) {
512 $out->addWikiMsg(
'undelete-no-results' );
517 $out->addWikiMsg(
'undeletepagetext', $this->
getLanguage()->formatNum( $result->numRows() ) );
521 $out->addHTML(
"<ul id='undeleteResultsList'>\n" );
522 foreach ( $result as $row ) {
523 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
524 if ( $title !==
null ) {
525 $item = $linkRenderer->makeKnownLink(
527 $title->getPrefixedText(),
529 [
'target' => $title->getPrefixedText() ]
535 [
'class' =>
'mw-invalidtitle' ],
536 Linker::getInvalidTitleDescription(
543 $revs = $this->
msg(
'undeleterevisions' )->numParams( $row->count )->parse();
547 [
'class' =>
'undeleteResult' ],
548 $item . $this->
msg(
'word-separator' )->escaped() .
549 $this->
msg(
'parentheses' )->rawParams( $revs )->escaped()
554 $out->addHTML(
"</ul>\n" );
559 private function showRevision( $timestamp ) {
560 if ( !preg_match(
'/[0-9]{14}/', $timestamp ) ) {
564 $out->addModuleStyles(
'mediawiki.interface.helpers.styles' );
570 $this->
msg(
'undelete-back-to-list' )->text(),
572 [
'target' => $this->mTargetObj->getPrefixedText() ]
575 $subtitle =
"< $listLink";
576 $out->setSubtitle( $subtitle );
581 $archive, $this->mTargetObj )
585 $revRecord = $this->archivedRevisionLookup->getRevisionRecordByTimestamp( $this->mTargetObj, $timestamp );
590 $out->addWikiMsg(
'undeleterevision-missing' );
594 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
596 $titleText = $this->mTargetObj->getPrefixedDBkey();
597 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
598 $msg = $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
599 ? [
'rev-suppressed-text-permission', $titleText ]
600 : [
'rev-deleted-text-permission', $titleText ];
603 $this->
msg( $msg[0], $msg[1] )->parse(),
610 $msg = $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
611 ? [
'rev-suppressed-text-view', $titleText ]
612 : [
'rev-deleted-text-view', $titleText ];
615 $this->
msg( $msg[0], $msg[1] )->parse(),
622 if ( $this->mDiff ) {
623 $previousRevRecord = $this->archivedRevisionLookup
624 ->getPreviousRevisionRecord( $this->mTargetObj, $timestamp );
625 if ( $previousRevRecord ) {
626 $this->showDiff( $previousRevRecord, $revRecord );
627 if ( $this->mDiffOnly ) {
631 $out->addHTML(
'<hr />' );
633 $out->addWikiMsg(
'undelete-nodiff' );
638 $this->
getPageTitle( $this->mTargetObj->getPrefixedDBkey() ),
639 $this->mTargetObj->getPrefixedText()
646 $time = $lang->userTimeAndDate( $timestamp, $user );
647 $d = $lang->userDate( $timestamp, $user );
648 $t = $lang->userTime( $timestamp, $user );
649 $userLink = Linker::revUserTools( $revRecord );
651 $content = $revRecord->getContent(
653 RevisionRecord::FOR_THIS_USER,
660 $undeleteRevisionContent =
'';
662 if ( !$this->mDiff ) {
663 $revdel = Linker::getRevDeleteLink(
669 $undeleteRevisionContent = $revdel .
' ';
673 $undeleteRevisionContent .= $out->msg(
682 if ( $this->mPreview || $isText ) {
685 $undeleteRevisionContent,
686 'mw-undelete-revision'
693 [
'class' =>
'mw-undelete-revision', ],
694 $undeleteRevisionContent
699 if ( $this->mPreview || !$isText ) {
702 $popts = $out->parserOptions();
704 $rendered = $this->revisionRenderer->getRenderedRevision(
708 [
'audience' => RevisionRecord::FOR_THIS_USER,
'causeAction' =>
'undelete-preview' ]
713 $pout = $rendered->getRevisionParserOutput();
715 $out->addParserOutput( $pout, [
716 'enableSectionEditLinks' =>
false,
724 '@phan-var TextContent $content';
728 'readonly' =>
'readonly',
731 ], $content->getText() .
"\n" );
733 $buttonFields[] =
new ButtonInputWidget( [
736 'label' => $this->
msg(
'showpreview' )->text()
742 $buttonFields[] =
new ButtonInputWidget( [
745 'label' => $this->
msg(
'showdiff' )->text()
750 Xml::openElement(
'div', [
751 'style' =>
'clear: both' ] ) .
752 Xml::openElement(
'form', [
754 'action' => $this->
getPageTitle()->getLocalURL( [
'action' =>
'submit' ] ) ] ) .
755 Xml::element(
'input', [
758 'value' => $this->mTargetObj->getPrefixedDBkey() ] ) .
759 Xml::element(
'input', [
761 'name' =>
'timestamp',
762 'value' => $timestamp ] ) .
763 Xml::element(
'input', [
765 'name' =>
'wpEditToken',
766 'value' => $user->getEditToken() ] ) .
769 'content' =>
new HorizontalLayout( [
770 'items' => $buttonFields
774 Xml::closeElement(
'form' ) .
775 Xml::closeElement(
'div' )
786 private function showDiff(
787 RevisionRecord $previousRevRecord,
788 RevisionRecord $currentRevRecord
790 $currentTitle = Title::newFromLinkTarget( $currentRevRecord->getPageAsLinkTarget() );
792 $diffContext =
new DerivativeContext( $this->
getContext() );
793 $diffContext->setTitle( $currentTitle );
794 $diffContext->setWikiPage( $this->wikiPageFactory->newFromTitle( $currentTitle ) );
796 $contentModel = $currentRevRecord->getSlot(
801 $diffEngine = $this->contentHandlerFactory->getContentHandler( $contentModel )
802 ->createDifferenceEngine( $diffContext );
804 $diffEngine->setRevisions( $previousRevRecord, $currentRevRecord );
805 $diffEngine->showDiffStyle();
806 $formattedDiff = $diffEngine->getDiff(
807 $this->diffHeader( $previousRevRecord,
'o' ),
808 $this->diffHeader( $currentRevRecord,
'n' )
811 $this->
getOutput()->addHTML(
"<div>$formattedDiff</div>\n" );
819 private function diffHeader( RevisionRecord $revRecord, $prefix ) {
820 if ( $revRecord instanceof RevisionArchiveRecord ) {
824 'target' => $this->mTargetObj->getPrefixedText(),
825 'timestamp' =>
wfTimestamp( TS_MW, $revRecord->getTimestamp() )
829 $targetPage = $revRecord->getPageAsLinkTarget();
830 $targetQuery = [
'oldid' => $revRecord->getId() ];
836 $rdel = Linker::getRevDeleteLink( $user, $revRecord, $this->mTargetObj );
844 $dbr = $this->dbProvider->getReplicaDatabase();
845 $tagIds = $dbr->newSelectQueryBuilder()
846 ->select(
'ct_tag_id' )
847 ->from(
'change_tag' )
848 ->where( [
'ct_rev_id' => $revRecord->getId() ] )
849 ->caller( __METHOD__ )->fetchFieldValues();
851 foreach ( $tagIds as $tagId ) {
853 $tags[] = $this->changeTagDefStore->getName( (
int)$tagId );
854 }
catch ( NameTableAccessException $exception ) {
858 $tags = implode(
',', $tags );
864 $lang->userTimeAndDate( $revRecord->getTimestamp(), $user ),
865 $lang->userDate( $revRecord->getTimestamp(), $user ),
866 $lang->userTime( $revRecord->getTimestamp(), $user )
871 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
872 $asof = Html::rawElement(
874 [
'class' => Linker::getRevisionDeletedClass( $revRecord ) ],
881 return '<div id="mw-diff-' . $prefix .
'title1"><strong>' .
884 '<div id="mw-diff-' . $prefix .
'title2">' .
885 Linker::revUserTools( $revRecord ) .
'<br />' .
887 '<div id="mw-diff-' . $prefix .
'title3">' .
888 $minor . $this->commentFormatter->formatRevision( $revRecord, $user ) . $rdel .
'<br />' .
890 '<div id="mw-diff-' . $prefix .
'title5">' .
891 $tagSummary[0] .
'<br />' .
899 private function showFileConfirmationForm( $key ) {
903 $file =
new ArchivedFile( $this->mTargetObj, 0, $this->mFilename );
904 $out->addWikiMsg(
'undelete-show-file-confirm',
905 $this->mTargetObj->getText(),
906 $lang->userDate( $file->getTimestamp(), $user ),
907 $lang->userTime( $file->getTimestamp(), $user ) );
909 Xml::openElement(
'form', [
912 'target' => $this->mTarget,
914 'token' => $user->getEditToken( $key ),
918 Xml::submitButton( $this->msg(
'undelete-show-file-submit' )->text() ) .
927 private function showFile( $key ) {
930 # We mustn't allow the output to be CDN cached, otherwise
931 # if an admin previews a deleted image, and it's cached, then
932 # a user without appropriate permissions can toddle off and
933 # nab the image, and CDN will serve it
935 $response->header(
'Expires: ' . gmdate(
'D, d M Y H:i:s', 0 ) .
' GMT' );
936 $response->header(
'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
938 $path = $this->localRepo->getZonePath(
'deleted' ) .
'/' . $this->localRepo->getDeletedHashPath( $key ) . $key;
939 $this->localRepo->streamFileWithStatus(
$path );
946 private function addRevisionsToBatch( LinkBatch $batch, IResultWrapper $revisions ) {
947 foreach ( $revisions as $row ) {
948 $batch->add(
NS_USER, $row->ar_user_text );
957 private function addFilesToBatch( LinkBatch $batch, IResultWrapper $files ) {
958 foreach ( $files as $row ) {
959 $batch->add(
NS_USER, $row->fa_user_text );
969 $out->setArticleBodyOnly(
true );
970 $dbr = $this->dbProvider->getReplicaDatabase();
971 if ( $this->mHistoryOffset ) {
972 $extraConds = [ $dbr->expr(
'ar_timestamp',
'<', $dbr->timestamp( $this->mHistoryOffset ) ) ];
976 $revisions = $this->archivedRevisionLookup->listRevisions(
979 self::REVISION_HISTORY_LIMIT + 1
981 $batch = $this->linkBatchFactory->newLinkBatch();
982 $this->addRevisionsToBatch( $batch, $revisions );
986 if ( $revisions->numRows() > self::REVISION_HISTORY_LIMIT ) {
988 $out->setStatusCode( 206 );
999 $history = Html::openElement(
'ul', [
'class' =>
'mw-undelete-revlist' ] );
1002 $numRevisions = $revisions->
numRows();
1003 $displayCount = min( $numRevisions, self::REVISION_HISTORY_LIMIT );
1004 $firstRev = $this->revisionStore->getFirstRevision( $this->mTargetObj );
1005 $earliestLiveTime = $firstRev ? $firstRev->getTimestamp() :
null;
1007 $revisions->rewind();
1008 for ( $i = 0; $i < $displayCount; $i++ ) {
1012 $history .= $this->
formatRevisionRow( $row, $earliestLiveTime, $numRevisions - $i );
1014 $history .= Html::closeElement(
'ul' );
1022 if ( $this->mAllowed ) {
1023 $out->addModules(
'mediawiki.misc-authed-ooui' );
1024 $out->addModuleStyles(
'mediawiki.special' );
1026 $out->addModuleStyles(
'mediawiki.interface.helpers.styles' );
1028 "<div class='mw-undelete-pagetitle'>\n$1\n</div>\n",
1029 [
'undeletepagetitle',
wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) ]
1034 $this->
getHookRunner()->onUndeleteForm__showHistory( $archive, $this->mTargetObj );
1036 $out->addHTML( Html::openElement(
'div', [
'class' =>
'mw-undelete-history' ] ) );
1037 if ( $this->mAllowed ) {
1038 $out->addWikiMsg(
'undeletehistory' );
1039 $out->addWikiMsg(
'undeleterevdel' );
1041 $out->addWikiMsg(
'undeletehistorynoadmin' );
1043 $out->addHTML( Html::closeElement(
'div' ) );
1045 # List all stored revisions
1046 $revisions = $this->archivedRevisionLookup->listRevisions(
1049 self::REVISION_HISTORY_LIMIT + 1
1051 $files = $archive->listFiles();
1052 $numRevisions = $revisions->numRows();
1053 $showLoadMore = $numRevisions > self::REVISION_HISTORY_LIMIT;
1054 $haveRevisions = $numRevisions > 0;
1055 $haveFiles = $files && $files->numRows() > 0;
1057 # Batch existence check on user and talk pages
1058 if ( $haveRevisions || $haveFiles ) {
1059 $batch = $this->linkBatchFactory->newLinkBatch();
1060 $this->addRevisionsToBatch( $batch, $revisions );
1063 $this->addFilesToBatch( $batch, $files );
1068 if ( $this->mAllowed ) {
1071 $action = $this->
getPageTitle()->getLocalURL( [
'action' =>
'submit' ] );
1072 # Start the form here
1073 $form =
new FormLayout( [
1075 'action' => $action,
1080 # Show relevant lines from the deletion log:
1081 $deleteLogPage =
new LogPage(
'delete' );
1082 $out->addHTML( Xml::element(
'h2',
null, $deleteLogPage->getName()->text() ) .
"\n" );
1083 LogEventsList::showLogExtract( $out,
'delete', $this->mTargetObj );
1084 # Show relevant lines from the suppression log:
1085 $suppressLogPage =
new LogPage(
'suppress' );
1086 if ( $this->permissionManager->userHasRight( $this->getUser(),
'suppressionlog' ) ) {
1087 $out->addHTML( Xml::element(
'h2',
null, $suppressLogPage->getName()->text() ) .
"\n" );
1088 LogEventsList::showLogExtract( $out,
'suppress', $this->mTargetObj );
1091 if ( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
1092 $unsuppressAllowed = $this->permissionManager->userHasRight( $this->
getUser(),
'suppressrevision' );
1094 $fields[] =
new Layout( [
1095 'content' =>
new HtmlSnippet( $this->
msg(
'undeleteextrahelp' )->parseAsBlock() )
1098 $dropdownComment = $this->
msg(
'undelete-comment-dropdown' )
1099 ->page( $this->mTargetObj )->inContentLanguage()->text();
1101 if ( $unsuppressAllowed ) {
1102 $dropdownComment .=
"\n" . $this->
msg(
'undelete-comment-dropdown-unsuppress' )
1103 ->page( $this->mTargetObj )->inContentLanguage()->text();
1105 $options = Xml::listDropdownOptions(
1107 [
'other' => $this->
msg(
'undeletecommentotherlist' )->text() ]
1109 $options = Xml::listDropdownOptionsOoui( $options );
1111 $fields[] =
new FieldLayout(
1112 new DropdownInputWidget( [
1113 'name' =>
'wpCommentList',
1114 'inputId' =>
'wpCommentList',
1115 'infusable' =>
true,
1116 'value' => $this->
getRequest()->getText(
'wpCommentList',
'other' ),
1117 'options' => $options,
1120 'label' => $this->
msg(
'undeletecomment' )->text(),
1125 $fields[] =
new FieldLayout(
1126 new TextInputWidget( [
1127 'name' =>
'wpComment',
1128 'inputId' =>
'wpComment',
1129 'infusable' =>
true,
1130 'value' => $this->
getRequest()->getText(
'wpComment' ),
1131 'autofocus' =>
true,
1135 'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT,
1138 'label' => $this->
msg(
'undeleteothercomment' )->text(),
1143 if ( $this->
getUser()->isRegistered() ) {
1144 $checkWatch = $this->watchlistManager->isWatched( $this->
getUser(), $this->mTargetObj )
1145 || $this->
getRequest()->getText(
'wpWatch' );
1146 $fields[] =
new FieldLayout(
1147 new CheckboxInputWidget( [
1148 'name' =>
'wpWatch',
1149 'inputId' =>
'mw-undelete-watch',
1151 'selected' => $checkWatch,
1154 'label' => $this->
msg(
'watchthis' )->text(),
1155 'align' =>
'inline',
1160 if ( $unsuppressAllowed ) {
1161 $fields[] =
new FieldLayout(
1162 new CheckboxInputWidget( [
1163 'name' =>
'wpUnsuppress',
1164 'inputId' =>
'mw-undelete-unsuppress',
1168 'label' => $this->
msg(
'revdelete-unsuppress' )->text(),
1169 'align' =>
'inline',
1174 $undelPage = $this->undeletePageFactory->newUndeletePage(
1175 $this->wikiPageFactory->newFromTitle( $this->mTargetObj ),
1176 $this->getContext()->getAuthority()
1178 if ( $undelPage->canProbablyUndeleteAssociatedTalk()->isGood() ) {
1179 $fields[] =
new FieldLayout(
1180 new CheckboxInputWidget( [
1181 'name' =>
'undeletetalk',
1182 'inputId' =>
'mw-undelete-undeletetalk',
1183 'selected' =>
false,
1186 'label' => $this->
msg(
'undelete-undeletetalk' )->text(),
1187 'align' =>
'inline',
1192 $fields[] =
new FieldLayout(
1194 'content' =>
new HorizontalLayout( [
1196 new ButtonInputWidget( [
1197 'name' =>
'restore',
1198 'inputId' =>
'mw-undelete-submit',
1200 'label' => $this->
msg(
'undeletebtn' )->text(),
1201 'flags' => [
'primary',
'progressive' ],
1204 new ButtonInputWidget( [
1206 'inputId' =>
'mw-undelete-invert',
1208 'label' => $this->
msg(
'undeleteinvert' )->text()
1215 $fieldset =
new FieldsetLayout( [
1216 'label' => $this->
msg(
'undelete-fieldset-title' )->text(),
1217 'id' =>
'mw-undelete-table',
1223 if ( $unsuppressAllowed ) {
1225 $this->
msg(
'undelete-comment-dropdown-unsuppress' )->inContentLanguage()->
getTitle(),
1226 $this->
msg(
'undelete-edit-commentlist-unsuppress' )->text(),
1228 [
'action' =>
'edit' ]
1230 $link .= $this->
msg(
'pipe-separator' )->escaped();
1233 $this->
msg(
'undelete-comment-dropdown' )->inContentLanguage()->
getTitle(),
1234 $this->
msg(
'undelete-edit-commentlist' )->text(),
1236 [
'action' =>
'edit' ]
1239 $link = Html::rawElement(
'p', [
'class' =>
'mw-undelete-editcomments' ], $link );
1243 $form->appendContent(
1245 'expanded' =>
false,
1248 'content' => $fieldset,
1252 Html::hidden(
'target', $this->mTarget ) .
1253 Html::hidden(
'wpEditToken', $this->
getUser()->getEditToken() )
1259 $history .= Xml::element(
'h2',
null, $this->
msg(
'history' )->text() ) .
"\n";
1261 if ( $haveRevisions ) {
1262 # Show the page's stored (deleted) history
1264 if ( $this->permissionManager->userHasRight( $this->getUser(),
'deleterevision' ) ) {
1270 'class' =>
'deleterevision-log-submit mw-log-deleterevision-button'
1272 $this->
msg(
'showhideselectedversions' )->text()
1278 if ( $showLoadMore ) {
1280 Html::openElement(
'div' ) .
1283 [
'id' =>
'mw-load-more-revisions' ],
1284 $this->
msg(
'undelete-load-more-revisions' )->text()
1286 Html::closeElement(
'div' ) .
1290 $out->addWikiMsg(
'nohistory' );
1294 $history .= Xml::element(
'h2',
null, $this->
msg(
'filehist' )->text() ) .
"\n";
1295 $history .= Html::openElement(
'ul', [
'class' =>
'mw-undelete-revlist' ] );
1296 foreach ( $files as $row ) {
1297 $history .= $this->formatFileRow( $row );
1300 $history .= Html::closeElement(
'ul' );
1303 if ( $this->mAllowed ) {
1304 # Slip in the hidden controls here
1305 $misc = Html::hidden(
'target', $this->mTarget );
1306 $misc .= Html::hidden(
'wpEditToken', $this->
getUser()->getEditToken() );
1310 $form->appendContent(
new HtmlSnippet( $history ) );
1312 $out->addHTML( (
string)$form );
1314 $out->addHTML( $history );
1321 $revRecord = $this->revisionStore->newRevisionFromArchiveRow(
1323 IDBAccessObject::READ_NORMAL,
1330 if ( $this->mAllowed ) {
1331 if ( $this->mInvert ) {
1332 if ( in_array( $ts, $this->mTargetTimestamp ) ) {
1333 $checkBox = Xml::check(
"ts$ts" );
1335 $checkBox = Xml::check(
"ts$ts",
true );
1338 $checkBox = Xml::check(
"ts$ts" );
1346 if ( $this->mCanView ) {
1349 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1350 $pageLink = htmlspecialchars( $this->
getLanguage()->userTimeAndDate( $ts, $user ) );
1351 $last = $this->
msg(
'diff' )->escaped();
1352 } elseif ( $remaining > 0 || ( $earliestLiveTime && $ts > $earliestLiveTime ) ) {
1353 $pageLink = $this->getPageLink( $revRecord, $titleObj, $ts );
1356 $this->
msg(
'diff' )->text(),
1359 'target' => $this->mTargetObj->getPrefixedText(),
1365 $pageLink = $this->getPageLink( $revRecord, $titleObj, $ts );
1366 $last = $this->
msg(
'diff' )->escaped();
1369 $pageLink = htmlspecialchars( $this->
getLanguage()->userTimeAndDate( $ts, $user ) );
1370 $last = $this->
msg(
'diff' )->escaped();
1374 $userLink = Linker::revUserTools( $revRecord );
1377 $minor = $revRecord->isMinor() ? ChangesList::flag(
'minor' ) :
'';
1380 $size = $row->ar_len;
1381 if ( $size !==
null ) {
1382 $revTextSize = Linker::formatRevisionSize( $size );
1386 $comment = $this->commentFormatter->formatRevision( $revRecord, $user );
1396 $attribs[
'class'] = implode(
' ', $classes );
1399 $revisionRow = $this->
msg(
'undelete-revision-row2' )
1412 return Xml::tags(
'li', $attribs, $revisionRow ) .
"\n";
1415 private function formatFileRow( $row ) {
1421 if ( $this->mCanView && $row->fa_storage_key ) {
1422 if ( $this->mAllowed ) {
1423 $checkBox = Xml::check(
'fileid' . $row->fa_id );
1425 $key = urlencode( $row->fa_storage_key );
1426 $pageLink = $this->getFileLink( $file, $this->
getPageTitle(), $ts, $key );
1428 $pageLink = htmlspecialchars( $this->
getLanguage()->userTimeAndDate( $ts, $user ) );
1430 $userLink = $this->getFileUser( $file );
1431 $data = $this->
msg(
'widthheight' )->numParams( $row->fa_width, $row->fa_height )->text();
1432 $bytes = $this->
msg(
'parentheses' )
1433 ->plaintextParams( $this->
msg(
'nbytes' )->numParams( $row->fa_size )->text() )
1435 $data = htmlspecialchars( $data .
' ' . $bytes );
1436 $comment = $this->getFileComment( $file );
1439 $canHide = $this->
isAllowed(
'deleterevision' );
1440 if ( $canHide || ( $file->getVisibility() && $this->isAllowed(
'deletedhistory' ) ) ) {
1441 if ( !$file->userCan( File::DELETED_RESTRICTED, $user ) ) {
1443 $revdlink = Linker::revDeleteLinkDisabled( $canHide );
1446 'type' =>
'filearchive',
1447 'target' => $this->mTargetObj->getPrefixedDBkey(),
1448 'ids' => $row->fa_id
1450 $revdlink = Linker::revDeleteLink( $query,
1451 $file->isDeleted( File::DELETED_RESTRICTED ), $canHide );
1457 return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
1468 private function getPageLink( RevisionRecord $revRecord, $titleObj, $ts ) {
1470 $time = $this->
getLanguage()->userTimeAndDate( $ts, $user );
1472 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1476 [
'class' =>
'history-deleted' ],
1486 'target' => $this->mTargetObj->getPrefixedText(),
1491 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1492 $class = Linker::getRevisionDeletedClass( $revRecord );
1493 $link =
'<span class="' . $class .
'">' . $link .
'</span>';
1509 private function getFileLink( $file, $titleObj, $ts, $key ) {
1511 $time = $this->
getLanguage()->userTimeAndDate( $ts, $user );
1513 if ( !$file->userCan( File::DELETED_FILE, $user ) ) {
1516 [
'class' =>
'history-deleted' ],
1521 if ( $file->exists() ) {
1527 'target' => $this->mTargetObj->getPrefixedText(),
1529 'token' => $user->getEditToken( $key )
1533 $link = htmlspecialchars( $time );
1536 if ( $file->isDeleted( File::DELETED_FILE ) ) {
1537 $link =
'<span class="history-deleted">' . $link .
'</span>';
1549 private function getFileUser( $file ) {
1550 $uploader = $file->getUploader( File::FOR_THIS_USER, $this->
getAuthority() );
1552 return Html::rawElement(
1554 [
'class' =>
'history-deleted' ],
1555 $this->
msg(
'rev-deleted-user' )->escaped()
1559 $link = Linker::userLink( $uploader->getId(), $uploader->getName() ) .
1560 Linker::userToolLinks( $uploader->getId(), $uploader->getName() );
1562 if ( $file->isDeleted( File::DELETED_USER ) ) {
1563 $link = Html::rawElement(
1565 [
'class' =>
'history-deleted' ],
1579 private function getFileComment( $file ) {
1580 if ( !$file->userCan( File::DELETED_COMMENT, $this->getAuthority() ) ) {
1581 return Html::rawElement(
1583 [
'class' =>
'history-deleted' ],
1586 [
'class' =>
'comment' ],
1587 $this->
msg(
'rev-deleted-comment' )->escaped()
1592 $comment = $file->getDescription( File::FOR_THIS_USER, $this->
getAuthority() );
1593 $link = $this->commentFormatter->formatBlock( $comment );
1595 if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
1596 $link = Html::rawElement(
1598 [
'class' =>
'history-deleted' ],
1606 private function undelete() {
1608 && $this->mTargetObj->getNamespace() ===
NS_FILE
1610 throw new ErrorPageError(
'undelete-error',
'filedelete-maintenance' );
1616 $undeletePage = $this->undeletePageFactory->newUndeletePage(
1617 $this->wikiPageFactory->newFromTitle( $this->mTargetObj ),
1618 $this->getAuthority()
1620 if ( $this->mUndeleteTalk && $undeletePage->canProbablyUndeleteAssociatedTalk()->isGood() ) {
1621 $undeletePage->setUndeleteAssociatedTalk(
true );
1623 $status = $undeletePage
1624 ->setUndeleteOnlyTimestamps( $this->mTargetTimestamp )
1625 ->setUndeleteOnlyFileVersions( $this->mFileVersions )
1626 ->setUnsuppress( $this->mUnsuppress )
1628 ->undeleteIfAllowed( $this->mComment );
1630 if ( !$status->isGood() ) {
1631 $out->setPageTitleMsg( $this->
msg(
'undelete-error' ) );
1632 $out->wrapWikiTextAsInterface(
1634 Status::wrap( $status )->getWikiText(
1643 $restoredRevs = $status->getValue()[UndeletePage::REVISIONS_RESTORED];
1644 $restoredFiles = $status->getValue()[UndeletePage::FILES_RESTORED];
1646 if ( $restoredRevs === 0 && $restoredFiles === 0 ) {
1648 $out->setPageTitleMsg( $this->
msg(
'undelete-error' ) );
1650 if ( $status->getValue()[UndeletePage::FILES_RESTORED] !== 0 ) {
1652 $this->mTargetObj, $this->mFileVersions, $this->
getUser(), $this->mComment );
1658 $this->watchlistManager->setWatch(
1675 return $this->
prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );