83 private const REVISION_HISTORY_LIMIT = 500;
100 private $mTargetTimestamp = [];
106 private $mComment =
'';
116 private $mUnsuppress;
118 private $mFileVersions = [];
120 private $mUndeleteTalk;
122 private $mHistoryOffset;
129 private $mSearchPrefix;
136 private LinkBatchFactory $linkBatchFactory;
153 LinkBatchFactory $linkBatchFactory,
164 parent::__construct(
'Undelete',
'deletedhistory' );
165 $this->permissionManager = $permissionManager;
166 $this->revisionStore = $revisionStore;
167 $this->revisionRenderer = $revisionRenderer;
168 $this->contentHandlerFactory = $contentHandlerFactory;
169 $this->changeTagDefStore = $changeTagDefStore;
170 $this->linkBatchFactory = $linkBatchFactory;
172 $this->dbProvider = $dbProvider;
173 $this->userOptionsLookup = $userOptionsLookup;
174 $this->wikiPageFactory = $wikiPageFactory;
175 $this->searchEngineFactory = $searchEngineFactory;
176 $this->undeletePageFactory = $undeletePageFactory;
177 $this->archivedRevisionLookup = $archivedRevisionLookup;
178 $this->commentFormatter = $commentFormatter;
179 $this->watchlistManager = $watchlistManager;
187 private function loadRequest( ?
string $par ) {
191 $this->mAction = $request->getRawVal(
'action' );
192 if ( $par !==
null && $par !==
'' ) {
193 $this->mTarget = $par;
195 $this->mTarget = $request->getVal(
'target' );
198 $this->mTargetObj =
null;
200 if ( $this->mTarget !==
null && $this->mTarget !==
'' ) {
201 $this->mTargetObj = Title::newFromText( $this->mTarget );
204 $this->mSearchPrefix = $request->getText(
'prefix' );
205 $time = $request->getVal(
'timestamp' );
206 $this->mTimestamp = $time ?
wfTimestamp( TS_MW, $time ) :
'';
207 $this->mFilename = $request->getVal(
'file' );
209 $posted = $request->wasPosted() &&
210 $user->matchEditToken( $request->getVal(
'wpEditToken' ) );
211 $this->mRestore = $request->getCheck(
'restore' ) && $posted;
212 $this->mRevdel = $request->getCheck(
'revdel' ) && $posted;
213 $this->mInvert = $request->getCheck(
'invert' ) && $posted;
214 $this->mPreview = $request->getCheck(
'preview' ) && $posted;
215 $this->mDiff = $request->getCheck(
'diff' );
216 $this->mDiffOnly = $request->getBool(
'diffonly',
217 $this->userOptionsLookup->getOption( $this->getUser(),
'diffonly' ) );
218 $commentList = $request->getText(
'wpCommentList',
'other' );
219 $comment = $request->getText(
'wpComment' );
220 if ( $commentList ===
'other' ) {
221 $this->mComment = $comment;
222 } elseif ( $comment !==
'' ) {
223 $this->mComment = $commentList . $this->
msg(
'colon-separator' )->inContentLanguage()->text() . $comment;
225 $this->mComment = $commentList;
227 $this->mUnsuppress = $request->getVal(
'wpUnsuppress' ) &&
228 $this->permissionManager->userHasRight( $user,
'suppressrevision' );
229 $this->mToken = $request->getVal(
'token' );
230 $this->mUndeleteTalk = $request->getCheck(
'undeletetalk' );
231 $this->mHistoryOffset = $request->getVal(
'historyoffset' );
234 $this->mAllowed =
true;
235 $this->mCanView =
true;
236 } elseif ( $this->
isAllowed(
'deletedtext' ) ) {
237 $this->mAllowed =
false;
238 $this->mCanView =
true;
239 $this->mRestore =
false;
241 $this->mAllowed =
false;
242 $this->mCanView =
false;
243 $this->mTimestamp =
'';
244 $this->mRestore =
false;
247 if ( $this->mRestore || $this->mInvert ) {
249 $this->mFileVersions = [];
250 foreach ( $request->getValues() as $key => $val ) {
252 if ( preg_match(
'/^ts(\d{14})$/', $key,
$matches ) ) {
256 if ( preg_match(
'/^fileid(\d+)$/', $key,
$matches ) ) {
257 $this->mFileVersions[] = intval(
$matches[1] );
260 rsort( $timestamps );
261 $this->mTargetTimestamp = $timestamps;
275 $block = $user->getBlock();
277 if ( $this->mTargetObj !==
null ) {
278 return $this->permissionManager->userCan( $permission, $user, $this->mTargetObj );
280 $hasRight = $this->permissionManager->userHasRight( $user, $permission );
281 $sitewideBlock = $block && $block->isSitewide();
282 return $permission ===
'undelete' ? ( $hasRight && !$sitewideBlock ) : $hasRight;
288 return $this->
isAllowed( $this->mRestriction, $user );
299 if ( !parent::userCanExecute( $user ) ) {
306 $this->mTargetObj && $this->permissionManager->isBlockedFrom( $user, $this->mTargetObj ) ) {
325 $this->
addHelpLink(
'Help:Deletion_and_undeletion' );
327 $this->loadRequest( $par );
332 $out->addModuleStyles(
'mediawiki.codex.messagebox.styles' );
334 if ( $this->mTargetObj ===
null ) {
335 $out->addWikiMsg(
'undelete-header' );
337 # Not all users can just browse every deleted page from the list
338 if ( $this->permissionManager->userHasRight( $user,
'browsearchive' ) ) {
339 $this->showSearchForm();
346 if ( $this->mAllowed ) {
347 $out->setPageTitleMsg( $this->
msg(
'undeletepage' ) );
349 $out->setPageTitleMsg( $this->
msg(
'viewdeletedpage' ) );
352 $this->
getSkin()->setRelevantTitle( $this->mTargetObj );
354 if ( $this->mTimestamp !==
'' ) {
355 $this->showRevision( $this->mTimestamp );
356 } elseif ( $this->mFilename !==
null && $this->mTargetObj->inNamespace(
NS_FILE ) ) {
357 $file =
new ArchivedFile( $this->mTargetObj, 0, $this->mFilename );
359 if ( !$file->exists() ) {
360 $out->addWikiMsg(
'filedelete-nofile', $this->mFilename );
361 } elseif ( !$file->userCan( File::DELETED_FILE, $user ) ) {
362 if ( $file->isDeleted( File::DELETED_RESTRICTED ) ) {
367 } elseif ( !$user->matchEditToken( $this->mToken, $this->mFilename ) ) {
368 $this->showFileConfirmationForm( $this->mFilename );
370 $this->showFile( $this->mFilename );
372 } elseif ( $this->mAction ===
'submit' ) {
373 if ( $this->mRestore ) {
375 } elseif ( $this->mRevdel ) {
376 $this->redirectToRevDel();
378 } elseif ( $this->mAction ===
'render' ) {
389 private function redirectToRevDel() {
391 $fileArchiveIds = [];
393 foreach ( $this->
getRequest()->getValues() as $key => $val ) {
395 if ( preg_match(
"/^ts(\d{14})$/", $key,
$matches ) ) {
396 $revisionRecord = $this->archivedRevisionLookup
397 ->getRevisionRecordByTimestamp( $this->mTargetObj,
$matches[1] );
398 if ( $revisionRecord ) {
399 $revisionIds[] = (int)$revisionRecord->getId();
401 } elseif ( preg_match(
'/^fileid(\d+)$/', $key,
$matches ) ) {
402 $fileArchiveIds[] = (int)
$matches[1];
406 $hasRevisions = count( $revisionIds ) > 0;
407 $hasFiles = count( $fileArchiveIds ) > 0;
410 if ( $hasRevisions && $hasFiles ) {
411 $this->renderUndeleteSelectionError(
'mixed' );
413 } elseif ( !$hasRevisions && !$hasFiles ) {
414 $this->renderUndeleteSelectionError(
'none' );
419 $idsForQuery = $hasFiles
420 ? array_fill_keys( $fileArchiveIds, 1 )
421 : array_fill_keys( $revisionIds, 1 );
424 'type' => $hasFiles ?
'filearchive' :
'revision',
425 'ids' => $idsForQuery,
426 'target' => $this->mTargetObj->getPrefixedText()
438 private function renderUndeleteSelectionError(
string $case ): void {
439 $msg = $case ===
'mixed'
440 ? $this->
msg(
'undelete-error-mixed' )
441 : $this->
msg(
'undelete-error-none' );
443 $this->
getOutput()->addHTML( Html::errorBox( $msg->parse() ) );
446 private function showSearchForm() {
447 $out = $this->getOutput();
448 $out->setPageTitleMsg( $this->
msg(
'undelete-search-title' ) );
449 $fuzzySearch = $this->
getRequest()->getVal(
'fuzzy',
'1' );
454 $fields[] =
new ActionFieldLayout(
455 new TextInputWidget( [
457 'inputId' =>
'prefix',
459 'value' => $this->mSearchPrefix,
462 new ButtonInputWidget( [
463 'label' => $this->
msg(
'undelete-search-submit' )->text(),
464 'flags' => [
'primary',
'progressive' ],
465 'inputId' =>
'searchUndelete',
469 'label' =>
new HtmlSnippet(
471 $fuzzySearch ?
'undelete-search-full' :
'undelete-search-prefix'
478 $fieldset =
new FieldsetLayout( [
479 'label' => $this->
msg(
'undelete-search-box' )->text(),
483 $form =
new FormLayout( [
488 $form->appendContent(
491 Html::hidden(
'title', $this->getPageTitle()->getPrefixedDBkey() ) .
492 Html::hidden(
'fuzzy', $fuzzySearch )
505 # List undeletable articles
506 if ( $this->mSearchPrefix ) {
509 if ( $fuzzySearch ) {
510 $result = PageArchive::listPagesBySearch( $this->mSearchPrefix );
512 $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix );
514 $this->showList( $result );
524 private function showList( $result ) {
525 $out = $this->getOutput();
527 if ( $result->numRows() == 0 ) {
528 $out->addWikiMsg(
'undelete-no-results' );
533 $out->addWikiMsg(
'undeletepagetext', $this->getLanguage()->formatNum( $result->numRows() ) );
535 $linkRenderer = $this->getLinkRenderer();
536 $undelete = $this->getPageTitle();
537 $out->addHTML(
"<ul id='undeleteResultsList'>\n" );
538 foreach ( $result as $row ) {
539 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
540 if ( $title !==
null ) {
541 $item = $linkRenderer->makeKnownLink(
543 $title->getPrefixedText(),
545 [
'target' => $title->getPrefixedText() ]
551 [
'class' =>
'mw-invalidtitle' ],
552 Linker::getInvalidTitleDescription(
559 $revs = $this->
msg(
'undeleterevisions' )->numParams( $row->count )->parse();
563 [
'class' =>
'undeleteResult' ],
564 $item . $this->
msg(
'word-separator' )->escaped() .
565 $this->
msg(
'parentheses' )->rawParams( $revs )->escaped()
570 $out->addHTML(
"</ul>\n" );
575 private function showRevision(
string $timestamp ) {
576 if ( !preg_match(
'/[0-9]{14}/', $timestamp ) ) {
579 $out = $this->getOutput();
580 $out->addModuleStyles(
'mediawiki.interface.helpers.styles' );
584 $listLink = $this->getLinkRenderer()->makeKnownLink(
585 $this->getPageTitle(),
586 $this->
msg(
'undelete-back-to-list' )->text(),
588 [
'target' => $this->mTargetObj->getPrefixedText() ]
591 $subtitle =
"< $listLink";
592 $out->setSubtitle( $subtitle );
594 $archive =
new PageArchive( $this->mTargetObj );
596 if ( !$this->getHookRunner()->onUndeleteForm__showRevision(
597 $archive, $this->mTargetObj )
601 $revRecord = $this->archivedRevisionLookup->getRevisionRecordByTimestamp( $this->mTargetObj, $timestamp );
606 $out->addWikiMsg(
'undeleterevision-missing' );
610 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
612 $titleText = $this->mTargetObj->getPrefixedURL();
613 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
614 $msg = $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
615 ? [
'rev-suppressed-text-permission', $titleText ]
616 : [
'rev-deleted-text-permission', $titleText ];
619 $this->
msg( $msg[0], $msg[1] )->parse(),
626 $msg = $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
627 ? [
'rev-suppressed-text-view', $titleText ]
628 : [
'rev-deleted-text-view', $titleText ];
631 $this->
msg( $msg[0], $msg[1] )->parse(),
638 if ( $this->mDiff ) {
639 $previousRevRecord = $this->archivedRevisionLookup
640 ->getPreviousRevisionRecord( $this->mTargetObj, $timestamp );
641 if ( $previousRevRecord ) {
642 $this->showDiff( $previousRevRecord, $revRecord );
643 if ( $this->mDiffOnly ) {
647 $out->addHTML(
'<hr />' );
649 $out->addWikiMsg(
'undelete-nodiff' );
653 $link = $this->getLinkRenderer()->makeKnownLink(
654 $this->getPageTitle( $this->mTargetObj->getPrefixedDBkey() ),
655 $this->mTargetObj->getPrefixedText()
658 $lang = $this->getLanguage();
662 $time = $lang->userTimeAndDate( $timestamp, $user );
663 $d = $lang->userDate( $timestamp, $user );
664 $t = $lang->userTime( $timestamp, $user );
665 $userLink = Linker::revUserTools( $revRecord );
668 $content = $revRecord->getContent(
670 RevisionRecord::FOR_THIS_USER,
673 }
catch ( RevisionAccessException ) {
678 $isText = ( $content instanceof TextContent );
680 $undeleteRevisionContent =
'';
682 if ( !$this->mDiff ) {
683 $revdel = Linker::getRevDeleteLink(
689 $undeleteRevisionContent = $revdel .
' ';
693 $undeleteRevisionContent .= $out->msg(
702 if ( $this->mPreview || $isText ) {
705 $undeleteRevisionContent,
706 'mw-undelete-revision'
713 [
'class' =>
'mw-undelete-revision', ],
714 $undeleteRevisionContent
719 if ( $this->mPreview || !$isText ) {
722 $popts = ParserOptions::newFromContext( $this->
getContext() );
725 $rendered = $this->revisionRenderer->getRenderedRevision(
729 [
'audience' => RevisionRecord::FOR_THIS_USER,
'causeAction' =>
'undelete-preview' ]
734 $pout = $rendered->getRevisionParserOutput();
736 $out->addParserOutput( $pout, $popts, [
737 'enableSectionEditLinks' =>
false,
739 }
catch ( RevisionAccessException ) {
747 '@phan-var TextContent $content';
751 'readonly' =>
'readonly',
754 ], $content->getText() .
"\n" );
756 $buttonFields[] =
new ButtonInputWidget( [
759 'label' => $this->
msg(
'showpreview' )->text()
765 $buttonFields[] =
new ButtonInputWidget( [
768 'label' => $this->
msg(
'showdiff' )->text()
773 Html::openElement(
'div', [
774 'style' =>
'clear: both' ] ) .
775 Html::openElement(
'form', [
777 'action' => $this->getPageTitle()->getLocalURL( [
'action' =>
'submit' ] ) ] ) .
781 'value' => $this->mTargetObj->getPrefixedDBkey() ] ) .
784 'name' =>
'timestamp',
785 'value' => $timestamp ] ) .
788 'name' =>
'wpEditToken',
789 'value' => $user->getEditToken() ] ) .
792 'content' =>
new HorizontalLayout( [
793 'items' => $buttonFields
797 Html::closeElement(
'form' ) .
798 Html::closeElement(
'div' )
809 private function showDiff(
810 RevisionRecord $previousRevRecord,
811 RevisionRecord $currentRevRecord
813 $currentTitle = Title::newFromPageIdentity( $currentRevRecord->getPage() );
815 $diffContext =
new DerivativeContext( $this->
getContext() );
816 $diffContext->setTitle( $currentTitle );
817 $diffContext->setWikiPage( $this->wikiPageFactory->newFromTitle( $currentTitle ) );
819 $contentModel = $currentRevRecord->getSlot(
824 $diffEngine = $this->contentHandlerFactory->getContentHandler( $contentModel )
825 ->createDifferenceEngine( $diffContext );
827 $diffEngine->setRevisions( $previousRevRecord, $currentRevRecord );
828 $diffEngine->showDiffStyle();
829 $formattedDiff = $diffEngine->getDiff(
830 $this->diffHeader( $previousRevRecord,
'o' ),
831 $this->diffHeader( $currentRevRecord,
'n' )
834 if ( $formattedDiff ===
false ) {
835 if ( $diffEngine->hasSuppressedRevision() ) {
836 $error =
'rev-suppressed-no-diff';
837 } elseif ( $diffEngine->hasDeletedRevision() ) {
838 $error =
'rev-deleted-no-diff';
841 $error =
'undelete-error-loading-diff';
843 $this->getOutput()->addHTML( $this->
msg( $error )->parse() );
845 $this->getOutput()->addHTML(
"<div>$formattedDiff</div>\n" );
854 private function diffHeader( RevisionRecord $revRecord, $prefix ) {
855 if ( $revRecord instanceof RevisionArchiveRecord ) {
857 $targetPage = $this->getPageTitle();
859 'target' => $this->mTargetObj->getPrefixedText(),
860 'timestamp' =>
wfTimestamp( TS_MW, $revRecord->getTimestamp() )
864 $targetPage = $revRecord->getPageAsLinkTarget();
865 $targetQuery = [
'oldid' => $revRecord->getId() ];
870 $lang = $this->getLanguage();
871 $rdel = Linker::getRevDeleteLink( $user, $revRecord, $this->mTargetObj );
877 $minor = $revRecord->isMinor() ? ChangesList::flag(
'minor' ) :
'';
879 $dbr = $this->dbProvider->getReplicaDatabase();
880 $tagIds = $dbr->newSelectQueryBuilder()
881 ->select(
'ct_tag_id' )
882 ->from(
'change_tag' )
883 ->where( [
'ct_rev_id' => $revRecord->getId() ] )
884 ->caller( __METHOD__ )->fetchFieldValues();
886 foreach ( $tagIds as $tagId ) {
888 $tags[] = $this->changeTagDefStore->getName( (
int)$tagId );
889 }
catch ( NameTableAccessException ) {
893 $tags = implode(
',', $tags );
894 $tagSummary = ChangeTags::formatSummaryRow( $tags,
'deleteddiff', $this->
getContext() );
895 $asof = $this->getLinkRenderer()->makeLink(
899 $lang->userTimeAndDate( $revRecord->getTimestamp(), $user ),
900 $lang->userDate( $revRecord->getTimestamp(), $user ),
901 $lang->userTime( $revRecord->getTimestamp(), $user )
906 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
907 $asof = Html::rawElement(
909 [
'class' => Linker::getRevisionDeletedClass( $revRecord ) ],
916 return '<div id="mw-diff-' . $prefix .
'title1"><strong>' .
919 '<div id="mw-diff-' . $prefix .
'title2">' .
920 Linker::revUserTools( $revRecord ) .
'<br />' .
922 '<div id="mw-diff-' . $prefix .
'title3">' .
923 $minor . $this->commentFormatter->formatRevision( $revRecord, $user ) . $rdel .
'<br />' .
925 '<div id="mw-diff-' . $prefix .
'title5">' .
926 $tagSummary[0] .
'<br />' .
934 private function showFileConfirmationForm( $key ) {
935 $out = $this->getOutput();
936 $lang = $this->getLanguage();
938 $file =
new ArchivedFile( $this->mTargetObj, 0, $this->mFilename );
939 $out->addWikiMsg(
'undelete-show-file-confirm',
940 $this->mTargetObj->getText(),
941 $lang->userDate( $file->getTimestamp(), $user ),
942 $lang->userTime( $file->getTimestamp(), $user ) );
944 Html::rawElement(
'form', [
946 'action' => $this->getPageTitle()->getLocalURL( [
947 'target' => $this->mTarget,
949 'token' => $user->getEditToken( $key ),
952 Html::submitButton( $this->msg(
'undelete-show-file-submit' )->text() )
961 private function showFile( $key ) {
962 $this->getOutput()->disable();
964 # We mustn't allow the output to be CDN cached, otherwise
965 # if an admin previews a deleted image, and it's cached, then
966 # a user without appropriate permissions can toddle off and
967 # nab the image, and CDN will serve it
969 $response->header(
'Expires: ' . gmdate(
'D, d M Y H:i:s', 0 ) .
' GMT' );
970 $response->header(
'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
972 $path = $this->localRepo->getZonePath(
'deleted' ) .
'/' . $this->localRepo->getDeletedHashPath( $key ) . $key;
973 $this->localRepo->streamFileWithStatus(
$path );
976 private function addRevisionsToBatch( LinkBatch $batch, IResultWrapper $revisions ) {
977 foreach ( $revisions as $row ) {
978 $batch->addUser(
new UserIdentityValue( (
int)$row->ar_user, $row->ar_user_text ) );
982 private function addFilesToBatch( LinkBatch $batch, IResultWrapper $files ) {
983 foreach ( $files as $row ) {
984 $batch->add(
NS_USER, $row->fa_user_text );
993 $out = $this->getOutput();
994 $out->setArticleBodyOnly(
true );
995 $dbr = $this->dbProvider->getReplicaDatabase();
996 if ( $this->mHistoryOffset ) {
997 $extraConds = [ $dbr->expr(
'ar_timestamp',
'<', $dbr->timestamp( $this->mHistoryOffset ) ) ];
1001 $revisions = $this->archivedRevisionLookup->listRevisions(
1004 self::REVISION_HISTORY_LIMIT + 1
1006 $batch = $this->linkBatchFactory->newLinkBatch()->setCaller( __METHOD__ );
1007 $this->addRevisionsToBatch( $batch, $revisions );
1009 $out->addHTML( $this->formatRevisionHistory( $revisions ) );
1011 if ( $revisions->numRows() > self::REVISION_HISTORY_LIMIT ) {
1013 $out->setStatusCode( 206 );
1024 $history = Html::openElement(
'ul', [
'class' =>
'mw-undelete-revlist' ] );
1027 $numRevisions = $revisions->
numRows();
1028 $displayCount = min( $numRevisions, self::REVISION_HISTORY_LIMIT );
1029 $firstRev = $this->revisionStore->getFirstRevision( $this->mTargetObj );
1030 $earliestLiveTime = $firstRev ? $firstRev->getTimestamp() :
null;
1032 $revisions->rewind();
1033 for ( $i = 0; $i < $displayCount; $i++ ) {
1037 $history .= $this->formatRevisionRow( $row, $earliestLiveTime, $numRevisions - $i );
1039 $history .= Html::closeElement(
'ul' );
1044 $this->checkReadOnly();
1046 $out = $this->getOutput();
1047 if ( $this->mAllowed ) {
1048 $out->addModules(
'mediawiki.misc-authed-ooui' );
1049 $out->addModuleStyles(
'mediawiki.special' );
1051 $out->addModuleStyles(
'mediawiki.interface.helpers.styles' );
1053 "<div class='mw-undelete-pagetitle'>\n$1\n</div>\n",
1054 [
'undeletepagetitle',
wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) ]
1059 $this->getHookRunner()->onUndeleteForm__showHistory( $archive, $this->mTargetObj );
1061 $out->addHTML( Html::openElement(
'div', [
'class' =>
'mw-undelete-history' ] ) );
1062 if ( $this->mAllowed ) {
1063 $out->addWikiMsg(
'undeletehistory' );
1064 $out->addWikiMsg(
'undeleterevdel' );
1066 $out->addWikiMsg(
'undeletehistorynoadmin' );
1068 $out->addHTML( Html::closeElement(
'div' ) );
1070 # List all stored revisions
1071 $revisions = $this->archivedRevisionLookup->listRevisions(
1074 self::REVISION_HISTORY_LIMIT + 1
1076 $files = $archive->listFiles();
1077 $numRevisions = $revisions->numRows();
1078 $showLoadMore = $numRevisions > self::REVISION_HISTORY_LIMIT;
1079 $haveRevisions = $numRevisions > 0;
1080 $haveFiles = $files && $files->numRows() > 0;
1082 # Batch existence check on user and talk pages
1083 if ( $haveRevisions || $haveFiles ) {
1084 $batch = $this->linkBatchFactory->newLinkBatch()->setCaller( __METHOD__ );
1085 $this->addRevisionsToBatch( $batch, $revisions );
1088 $this->addFilesToBatch( $batch, $files );
1093 if ( $this->mAllowed ) {
1096 $action = $this->getPageTitle()->getLocalURL( [
'action' =>
'submit' ] );
1097 # Start the form here
1098 $form =
new FormLayout( [
1100 'action' => $action,
1105 # Show relevant lines from the deletion log:
1106 $deleteLogPage =
new LogPage(
'delete' );
1107 $out->addHTML(
Html::element(
'h2', [], $deleteLogPage->getName()->text() ) .
"\n" );
1108 LogEventsList::showLogExtract( $out,
'delete', $this->mTargetObj );
1109 # Show relevant lines from the suppression log:
1110 $suppressLogPage =
new LogPage(
'suppress' );
1111 if ( $this->permissionManager->userHasRight( $this->getUser(),
'suppressionlog' ) ) {
1112 $out->addHTML(
Html::element(
'h2', [], $suppressLogPage->getName()->text() ) .
"\n" );
1113 LogEventsList::showLogExtract( $out,
'suppress', $this->mTargetObj );
1116 if ( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
1117 $unsuppressAllowed = $this->permissionManager->userHasRight( $this->getUser(),
'suppressrevision' );
1119 $fields[] =
new Layout( [
1120 'content' =>
new HtmlSnippet( $this->msg(
'undeleteextrahelp' )->parseAsBlock() )
1123 $dropdownComment = $this->msg(
'undelete-comment-dropdown' )
1124 ->page( $this->mTargetObj )->inContentLanguage()->text();
1126 if ( $unsuppressAllowed ) {
1127 $dropdownComment .=
"\n" . $this->msg(
'undelete-comment-dropdown-unsuppress' )
1128 ->page( $this->mTargetObj )->inContentLanguage()->text();
1130 $options = Html::listDropdownOptions(
1132 [
'other' => $this->msg(
'undeletecommentotherlist' )->text() ]
1134 $options = Html::listDropdownOptionsOoui( $options );
1136 $fields[] =
new FieldLayout(
1137 new DropdownInputWidget( [
1138 'name' =>
'wpCommentList',
1139 'inputId' =>
'wpCommentList',
1140 'infusable' =>
true,
1141 'value' => $this->getRequest()->getText(
'wpCommentList',
'other' ),
1142 'options' => $options,
1145 'label' => $this->msg(
'undeletecomment' )->text(),
1150 $fields[] =
new FieldLayout(
1151 new TextInputWidget( [
1152 'name' =>
'wpComment',
1153 'inputId' =>
'wpComment',
1154 'infusable' =>
true,
1155 'value' => $this->getRequest()->getText(
'wpComment' ),
1156 'autofocus' =>
true,
1160 'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT,
1163 'label' => $this->msg(
'undeleteothercomment' )->text(),
1168 if ( $this->getUser()->isRegistered() ) {
1169 $checkWatch = $this->watchlistManager->isWatched( $this->getUser(), $this->mTargetObj )
1170 || $this->getRequest()->getText(
'wpWatch' );
1171 $fields[] =
new FieldLayout(
1172 new CheckboxInputWidget( [
1173 'name' =>
'wpWatch',
1174 'inputId' =>
'mw-undelete-watch',
1176 'selected' => $checkWatch,
1179 'label' => $this->msg(
'watchthis' )->text(),
1180 'align' =>
'inline',
1185 if ( $unsuppressAllowed ) {
1186 $fields[] =
new FieldLayout(
1187 new CheckboxInputWidget( [
1188 'name' =>
'wpUnsuppress',
1189 'inputId' =>
'mw-undelete-unsuppress',
1193 'label' => $this->msg(
'revdelete-unsuppress' )->text(),
1194 'align' =>
'inline',
1199 $undelPage = $this->undeletePageFactory->newUndeletePage(
1200 $this->wikiPageFactory->newFromTitle( $this->mTargetObj ),
1201 $this->getContext()->getAuthority()
1203 if ( $undelPage->canProbablyUndeleteAssociatedTalk()->isGood() ) {
1204 $fields[] =
new FieldLayout(
1205 new CheckboxInputWidget( [
1206 'name' =>
'undeletetalk',
1207 'inputId' =>
'mw-undelete-undeletetalk',
1208 'selected' =>
false,
1211 'label' => $this->msg(
'undelete-undeletetalk' )->text(),
1212 'align' =>
'inline',
1217 $fields[] =
new FieldLayout(
1219 'content' =>
new HorizontalLayout( [
1221 new ButtonInputWidget( [
1222 'name' =>
'restore',
1223 'inputId' =>
'mw-undelete-submit',
1225 'label' => $this->msg(
'undeletebtn' )->text(),
1226 'flags' => [
'primary',
'progressive' ],
1229 new ButtonInputWidget( [
1231 'inputId' =>
'mw-undelete-invert',
1233 'label' => $this->msg(
'undeleteinvert' )->text()
1240 $fieldset =
new FieldsetLayout( [
1241 'label' => $this->msg(
'undelete-fieldset-title' )->text(),
1242 'id' =>
'mw-undelete-table',
1247 if ( $this->getAuthority()->isAllowed(
'editinterface' ) ) {
1248 if ( $unsuppressAllowed ) {
1249 $link .= $this->getLinkRenderer()->makeKnownLink(
1250 $this->msg(
'undelete-comment-dropdown-unsuppress' )->inContentLanguage()->getTitle(),
1251 $this->msg(
'undelete-edit-commentlist-unsuppress' )->text(),
1253 [
'action' =>
'edit' ]
1255 $link .= $this->msg(
'pipe-separator' )->escaped();
1257 $link .= $this->getLinkRenderer()->makeKnownLink(
1258 $this->msg(
'undelete-comment-dropdown' )->inContentLanguage()->getTitle(),
1259 $this->msg(
'undelete-edit-commentlist' )->text(),
1261 [
'action' =>
'edit' ]
1264 $link = Html::rawElement(
'p', [
'class' =>
'mw-undelete-editcomments' ], $link );
1268 $form->appendContent(
1270 'expanded' =>
false,
1273 'content' => $fieldset,
1277 Html::hidden(
'target', $this->mTarget ) .
1278 Html::hidden(
'wpEditToken', $this->getUser()->getEditToken() )
1284 $history .=
Html::element(
'h2', [], $this->msg(
'history' )->text() ) .
"\n";
1286 if ( $haveRevisions ) {
1287 # Show the page's stored (deleted) history
1289 if ( $this->mAllowed && $this->permissionManager->userHasRight( $this->getUser(),
'deleterevision' ) ) {
1295 'class' => [
'deleterevision-log-submit',
'mw-log-deleterevision-button' ]
1297 $this->msg(
'showhideselectedversions' )->text()
1301 $history .= $this->formatRevisionHistory( $revisions );
1303 if ( $showLoadMore ) {
1305 Html::openElement(
'div' ) .
1308 [
'id' =>
'mw-load-more-revisions' ],
1309 $this->msg(
'undelete-load-more-revisions' )->text()
1311 Html::closeElement(
'div' ) .
1315 $out->addWikiMsg(
'nohistory' );
1319 $history .=
Html::element(
'h2', [], $this->msg(
'filehist' )->text() ) .
"\n";
1320 $history .= Html::openElement(
'ul', [
'class' =>
'mw-undelete-revlist' ] );
1321 foreach ( $files as $row ) {
1322 $history .= $this->formatFileRow( $row );
1325 $history .= Html::closeElement(
'ul' );
1328 if ( $this->mAllowed ) {
1329 # Slip in the hidden controls here
1330 $misc = Html::hidden(
'target', $this->mTarget );
1331 $misc .= Html::hidden(
'wpEditToken', $this->getUser()->getEditToken() );
1335 $form->appendContent(
new HtmlSnippet( $history ) );
1337 $out->addHTML( (
string)$form );
1339 $out->addHTML( $history );
1350 $revRecord = $this->revisionStore->newRevisionFromArchiveRow(
1352 IDBAccessObject::READ_NORMAL,
1359 if ( $this->mAllowed ) {
1360 if ( $this->mInvert ) {
1361 if ( in_array( $ts, $this->mTargetTimestamp ) ) {
1362 $checkBox = Html::check(
"ts$ts" );
1364 $checkBox = Html::check(
"ts$ts",
true );
1367 $checkBox = Html::check(
"ts$ts" );
1374 $user = $this->getUser();
1375 if ( $this->mCanView ) {
1376 $titleObj = $this->getPageTitle();
1378 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1379 $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1380 $last = $this->msg(
'diff' )->escaped();
1381 } elseif ( $remaining > 0 || ( $earliestLiveTime && $ts > $earliestLiveTime ) ) {
1382 $pageLink = $this->getPageLink( $revRecord, $titleObj, $ts );
1383 $last = $this->getLinkRenderer()->makeKnownLink(
1385 $this->msg(
'diff' )->text(),
1388 'target' => $this->mTargetObj->getPrefixedText(),
1394 $pageLink = $this->getPageLink( $revRecord, $titleObj, $ts );
1395 $last = $this->msg(
'diff' )->escaped();
1398 $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1399 $last = $this->msg(
'diff' )->escaped();
1403 $userLink = Linker::revUserTools( $revRecord );
1406 $minor = $revRecord->isMinor() ? ChangesList::flag(
'minor' ) :
'';
1409 $size = $row->ar_len;
1410 if ( $size !==
null ) {
1411 $revTextSize = Linker::formatRevisionSize( $size );
1415 $comment = $this->commentFormatter->formatRevision( $revRecord, $user );
1419 [ $tagSummary, $classes ] = ChangeTags::formatSummaryRow(
1424 $attribs[
'class'] = $classes;
1426 $revisionRow = $this->msg(
'undelete-revision-row2' )
1439 return Html::rawElement(
'li', $attribs, $revisionRow ) .
"\n";
1442 private function formatFileRow( \stdClass $row ): string {
1445 $user = $this->getUser();
1448 if ( $this->mCanView && $row->fa_storage_key ) {
1449 if ( $this->mAllowed ) {
1450 $checkBox = Html::check(
'fileid' . $row->fa_id );
1452 $key = urlencode( $row->fa_storage_key );
1453 $pageLink = $this->getFileLink( $file, $this->getPageTitle(), $ts, $key );
1455 $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1457 $userLink = $this->getFileUser( $file );
1458 $data = $this->
msg(
'widthheight' )->numParams( $row->fa_width, $row->fa_height )->text();
1459 $bytes = $this->
msg(
'parentheses' )
1460 ->plaintextParams( $this->
msg(
'nbytes' )->numParams( $row->fa_size )->text() )
1462 $data = htmlspecialchars( $data .
' ' . $bytes );
1463 $comment = $this->getFileComment( $file );
1466 $canHide = $this->isAllowed(
'deleterevision' );
1467 if ( $canHide || ( $file->getVisibility() && $this->isAllowed(
'deletedhistory' ) ) ) {
1468 if ( !$file->userCan( File::DELETED_RESTRICTED, $user ) ) {
1470 $revdlink = Linker::revDeleteLinkDisabled( $canHide );
1473 'type' =>
'filearchive',
1474 'target' => $this->mTargetObj->getPrefixedDBkey(),
1475 'ids' => $row->fa_id
1477 $revdlink = Linker::revDeleteLink( $query,
1478 $file->isDeleted( File::DELETED_RESTRICTED ), $canHide );
1484 return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
1495 private function getPageLink( RevisionRecord $revRecord, LinkTarget $target, $ts ) {
1497 $time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1499 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1501 return Html::element(
1503 [
'class' =>
'history-deleted' ],
1508 $link = $this->getLinkRenderer()->makeKnownLink(
1513 'target' => $this->mTargetObj->getPrefixedText(),
1518 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1519 $class = Linker::getRevisionDeletedClass( $revRecord );
1520 $link =
'<span class="' . $class .
'">' . $link .
'</span>';
1536 private function getFileLink( $file, LinkTarget $target, $ts, $key ) {
1538 $time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1540 if ( !$file->userCan( File::DELETED_FILE, $user ) ) {
1541 return Html::element(
1543 [
'class' =>
'history-deleted' ],
1548 if ( $file->exists() ) {
1549 $link = $this->getLinkRenderer()->makeKnownLink(
1554 'target' => $this->mTargetObj->getPrefixedText(),
1556 'token' => $user->getEditToken( $key )
1560 $link = htmlspecialchars( $time );
1563 if ( $file->isDeleted( File::DELETED_FILE ) ) {
1564 $link =
'<span class="history-deleted">' . $link .
'</span>';
1576 private function getFileUser( $file ) {
1577 $uploader = $file->getUploader( File::FOR_THIS_USER, $this->
getAuthority() );
1579 return Html::rawElement(
1581 [
'class' =>
'history-deleted' ],
1582 $this->
msg(
'rev-deleted-user' )->escaped()
1586 $link = Linker::userLink( $uploader->getId(), $uploader->getName() ) .
1587 Linker::userToolLinks( $uploader->getId(), $uploader->getName() );
1589 if ( $file->isDeleted( File::DELETED_USER ) ) {
1590 $link = Html::rawElement(
1592 [
'class' =>
'history-deleted' ],
1606 private function getFileComment( $file ) {
1607 if ( !$file->userCan( File::DELETED_COMMENT, $this->getAuthority() ) ) {
1608 return Html::rawElement(
1610 [
'class' =>
'history-deleted' ],
1613 [
'class' =>
'comment' ],
1614 $this->
msg(
'rev-deleted-comment' )->escaped()
1619 $comment = $file->getDescription( File::FOR_THIS_USER, $this->
getAuthority() );
1620 $link = $this->commentFormatter->formatBlock( $comment );
1622 if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
1623 $link = Html::rawElement(
1625 [
'class' =>
'history-deleted' ],
1633 private function undelete() {
1634 if ( $this->getConfig()->
get( MainConfigNames::UploadMaintenance )
1635 && $this->mTargetObj->getNamespace() ===
NS_FILE
1637 throw new ErrorPageError(
'undelete-error',
'filedelete-maintenance' );
1640 $this->checkReadOnly();
1642 $out = $this->getOutput();
1643 $undeletePage = $this->undeletePageFactory->newUndeletePage(
1644 $this->wikiPageFactory->newFromTitle( $this->mTargetObj ),
1645 $this->getAuthority()
1647 if ( $this->mUndeleteTalk && $undeletePage->canProbablyUndeleteAssociatedTalk()->isGood() ) {
1648 $undeletePage->setUndeleteAssociatedTalk(
true );
1650 $status = $undeletePage
1651 ->setUndeleteOnlyTimestamps( $this->mTargetTimestamp )
1652 ->setUndeleteOnlyFileVersions( $this->mFileVersions )
1653 ->setUnsuppress( $this->mUnsuppress )
1655 ->undeleteIfAllowed( $this->mComment );
1657 if ( !$status->isGood() ) {
1658 $out->setPageTitleMsg( $this->
msg(
'undelete-error' ) );
1659 foreach ( $status->getMessages() as $msg ) {
1660 $out->addHTML( Html::errorBox(
1661 $this->
msg( $msg )->parse()
1667 $restoredRevs = $status->getValue()[UndeletePage::REVISIONS_RESTORED];
1668 $restoredFiles = $status->getValue()[UndeletePage::FILES_RESTORED];
1670 if ( $restoredRevs === 0 && $restoredFiles === 0 ) {
1672 $out->setPageTitleMsg( $this->
msg(
'undelete-error' ) );
1674 if ( $status->getValue()[UndeletePage::FILES_RESTORED] !== 0 ) {
1675 $this->getHookRunner()->onFileUndeleteComplete(
1676 $this->mTargetObj, $this->mFileVersions, $this->
getUser(), $this->mComment );
1679 $link = $this->getLinkRenderer()->makeKnownLink( $this->mTargetObj );
1680 $out->addWikiMsg(
'undeletedpage', Message::rawParam( $link ) );
1682 $this->watchlistManager->setWatch(
1699 return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );