3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
21 namespace MediaWiki\Specials
;
31 use MediaWiki\Cache\LinkBatch
;
32 use MediaWiki\Cache\LinkBatchFactory
;
33 use MediaWiki\CommentFormatter\CommentFormatter
;
34 use MediaWiki\CommentStore\CommentStore
;
35 use MediaWiki\Content\IContentHandlerFactory
;
36 use MediaWiki\Content\TextContent
;
37 use MediaWiki\Context\DerivativeContext
;
38 use MediaWiki\Html\Html
;
39 use MediaWiki\Linker\Linker
;
40 use MediaWiki\Linker\LinkTarget
;
41 use MediaWiki\MainConfigNames
;
42 use MediaWiki\Message\Message
;
43 use MediaWiki\Page\UndeletePage
;
44 use MediaWiki\Page\UndeletePageFactory
;
45 use MediaWiki\Page\WikiPageFactory
;
46 use MediaWiki\Permissions\PermissionManager
;
47 use MediaWiki\Revision\ArchivedRevisionLookup
;
48 use MediaWiki\Revision\RevisionAccessException
;
49 use MediaWiki\Revision\RevisionArchiveRecord
;
50 use MediaWiki\Revision\RevisionRecord
;
51 use MediaWiki\Revision\RevisionRenderer
;
52 use MediaWiki\Revision\RevisionStore
;
53 use MediaWiki\Revision\SlotRecord
;
54 use MediaWiki\SpecialPage\SpecialPage
;
55 use MediaWiki\Storage\NameTableAccessException
;
56 use MediaWiki\Storage\NameTableStore
;
57 use MediaWiki\Title\Title
;
58 use MediaWiki\User\Options\UserOptionsLookup
;
59 use MediaWiki\User\User
;
60 use MediaWiki\Watchlist\WatchlistManager
;
61 use MediaWiki\Xml\Xml
;
62 use OOUI\ActionFieldLayout
;
63 use OOUI\ButtonInputWidget
;
64 use OOUI\CheckboxInputWidget
;
65 use OOUI\DropdownInputWidget
;
67 use OOUI\FieldsetLayout
;
69 use OOUI\HorizontalLayout
;
73 use OOUI\TextInputWidget
;
78 use SearchEngineFactory
;
80 use Wikimedia\Rdbms\IConnectionProvider
;
81 use Wikimedia\Rdbms\IDBAccessObject
;
82 use Wikimedia\Rdbms\IResultWrapper
;
85 * Special page allowing users with the appropriate permissions to view
86 * and restore deleted content.
88 * @ingroup SpecialPage
90 class SpecialUndelete
extends SpecialPage
{
93 * Limit of revisions (Page history) to display.
94 * (If there are more items to display - "Load more" button will appear).
96 private const REVISION_HISTORY_LIMIT
= 500;
98 /** @var string|null */
113 private $mTargetTimestamp = [];
119 private $mComment = '';
122 /** @var bool|null */
124 /** @var bool|null */
126 /** @var bool|null */
128 /** @var bool|null */
129 private $mUnsuppress;
131 private $mFileVersions = [];
132 /** @var bool|null */
133 private $mUndeleteTalk;
134 /** @var string|null Timestamp at which to start a "load more" request (open interval) */
135 private $mHistoryOffset;
137 /** @var Title|null */
140 * @var string Search prefix
142 private $mSearchPrefix;
144 private PermissionManager
$permissionManager;
145 private RevisionStore
$revisionStore;
146 private RevisionRenderer
$revisionRenderer;
147 private IContentHandlerFactory
$contentHandlerFactory;
148 private NameTableStore
$changeTagDefStore;
149 private LinkBatchFactory
$linkBatchFactory;
150 private LocalRepo
$localRepo;
151 private IConnectionProvider
$dbProvider;
152 private UserOptionsLookup
$userOptionsLookup;
153 private WikiPageFactory
$wikiPageFactory;
154 private SearchEngineFactory
$searchEngineFactory;
155 private UndeletePageFactory
$undeletePageFactory;
156 private ArchivedRevisionLookup
$archivedRevisionLookup;
157 private CommentFormatter
$commentFormatter;
158 private WatchlistManager
$watchlistManager;
161 * @param PermissionManager $permissionManager
162 * @param RevisionStore $revisionStore
163 * @param RevisionRenderer $revisionRenderer
164 * @param IContentHandlerFactory $contentHandlerFactory
165 * @param NameTableStore $changeTagDefStore
166 * @param LinkBatchFactory $linkBatchFactory
167 * @param RepoGroup $repoGroup
168 * @param IConnectionProvider $dbProvider
169 * @param UserOptionsLookup $userOptionsLookup
170 * @param WikiPageFactory $wikiPageFactory
171 * @param SearchEngineFactory $searchEngineFactory
172 * @param UndeletePageFactory $undeletePageFactory
173 * @param ArchivedRevisionLookup $archivedRevisionLookup
174 * @param CommentFormatter $commentFormatter
175 * @param WatchlistManager $watchlistManager
177 public function __construct(
178 PermissionManager
$permissionManager,
179 RevisionStore
$revisionStore,
180 RevisionRenderer
$revisionRenderer,
181 IContentHandlerFactory
$contentHandlerFactory,
182 NameTableStore
$changeTagDefStore,
183 LinkBatchFactory
$linkBatchFactory,
184 RepoGroup
$repoGroup,
185 IConnectionProvider
$dbProvider,
186 UserOptionsLookup
$userOptionsLookup,
187 WikiPageFactory
$wikiPageFactory,
188 SearchEngineFactory
$searchEngineFactory,
189 UndeletePageFactory
$undeletePageFactory,
190 ArchivedRevisionLookup
$archivedRevisionLookup,
191 CommentFormatter
$commentFormatter,
192 WatchlistManager
$watchlistManager
194 parent
::__construct( 'Undelete', 'deletedhistory' );
195 $this->permissionManager
= $permissionManager;
196 $this->revisionStore
= $revisionStore;
197 $this->revisionRenderer
= $revisionRenderer;
198 $this->contentHandlerFactory
= $contentHandlerFactory;
199 $this->changeTagDefStore
= $changeTagDefStore;
200 $this->linkBatchFactory
= $linkBatchFactory;
201 $this->localRepo
= $repoGroup->getLocalRepo();
202 $this->dbProvider
= $dbProvider;
203 $this->userOptionsLookup
= $userOptionsLookup;
204 $this->wikiPageFactory
= $wikiPageFactory;
205 $this->searchEngineFactory
= $searchEngineFactory;
206 $this->undeletePageFactory
= $undeletePageFactory;
207 $this->archivedRevisionLookup
= $archivedRevisionLookup;
208 $this->commentFormatter
= $commentFormatter;
209 $this->watchlistManager
= $watchlistManager;
212 public function doesWrites() {
216 private function loadRequest( $par ) {
217 $request = $this->getRequest();
218 $user = $this->getUser();
220 $this->mAction
= $request->getRawVal( 'action' );
221 if ( $par !== null && $par !== '' ) {
222 $this->mTarget
= $par;
224 $this->mTarget
= $request->getVal( 'target' );
227 $this->mTargetObj
= null;
229 if ( $this->mTarget
!== null && $this->mTarget
!== '' ) {
230 $this->mTargetObj
= Title
::newFromText( $this->mTarget
);
233 $this->mSearchPrefix
= $request->getText( 'prefix' );
234 $time = $request->getVal( 'timestamp' );
235 $this->mTimestamp
= $time ?
wfTimestamp( TS_MW
, $time ) : '';
236 $this->mFilename
= $request->getVal( 'file' );
238 $posted = $request->wasPosted() &&
239 $user->matchEditToken( $request->getVal( 'wpEditToken' ) );
240 $this->mRestore
= $request->getCheck( 'restore' ) && $posted;
241 $this->mRevdel
= $request->getCheck( 'revdel' ) && $posted;
242 $this->mInvert
= $request->getCheck( 'invert' ) && $posted;
243 $this->mPreview
= $request->getCheck( 'preview' ) && $posted;
244 $this->mDiff
= $request->getCheck( 'diff' );
245 $this->mDiffOnly
= $request->getBool( 'diffonly',
246 $this->userOptionsLookup
->getOption( $this->getUser(), 'diffonly' ) );
247 $commentList = $request->getText( 'wpCommentList', 'other' );
248 $comment = $request->getText( 'wpComment' );
249 if ( $commentList === 'other' ) {
250 $this->mComment
= $comment;
251 } elseif ( $comment !== '' ) {
252 $this->mComment
= $commentList . $this->msg( 'colon-separator' )->inContentLanguage()->text() . $comment;
254 $this->mComment
= $commentList;
256 $this->mUnsuppress
= $request->getVal( 'wpUnsuppress' ) &&
257 $this->permissionManager
->userHasRight( $user, 'suppressrevision' );
258 $this->mToken
= $request->getVal( 'token' );
259 $this->mUndeleteTalk
= $request->getCheck( 'undeletetalk' );
260 $this->mHistoryOffset
= $request->getVal( 'historyoffset' );
262 if ( $this->isAllowed( 'undelete' ) ) {
263 $this->mAllowed
= true; // user can restore
264 $this->mCanView
= true; // user can view content
265 } elseif ( $this->isAllowed( 'deletedtext' ) ) {
266 $this->mAllowed
= false; // user cannot restore
267 $this->mCanView
= true; // user can view content
268 $this->mRestore
= false;
269 } else { // user can only view the list of revisions
270 $this->mAllowed
= false;
271 $this->mCanView
= false;
272 $this->mTimestamp
= '';
273 $this->mRestore
= false;
276 if ( $this->mRestore ||
$this->mInvert
) {
278 $this->mFileVersions
= [];
279 foreach ( $request->getValues() as $key => $val ) {
281 if ( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) {
282 $timestamps[] = $matches[1];
285 if ( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) {
286 $this->mFileVersions
[] = intval( $matches[1] );
289 rsort( $timestamps );
290 $this->mTargetTimestamp
= $timestamps;
295 * Checks whether a user is allowed the permission for the
296 * specific title if one is set.
298 * @param string $permission
299 * @param User|null $user
302 protected function isAllowed( $permission, ?User
$user = null ) {
303 $user ??
= $this->getUser();
304 $block = $user->getBlock();
306 if ( $this->mTargetObj
!== null ) {
307 return $this->permissionManager
->userCan( $permission, $user, $this->mTargetObj
);
309 $hasRight = $this->permissionManager
->userHasRight( $user, $permission );
310 $sitewideBlock = $block && $block->isSitewide();
311 return $permission === 'undelete' ?
( $hasRight && !$sitewideBlock ) : $hasRight;
315 public function userCanExecute( User
$user ) {
316 return $this->isAllowed( $this->mRestriction
, $user );
322 public function checkPermissions() {
323 $user = $this->getUser();
325 // First check if user has the right to use this page. If not,
326 // show a permissions error whether they are blocked or not.
327 if ( !parent
::userCanExecute( $user ) ) {
328 $this->displayRestrictionError();
331 // If a user has the right to use this page, but is blocked from
332 // the target, show a block error.
334 $this->mTargetObj
&& $this->permissionManager
->isBlockedFrom( $user, $this->mTargetObj
) ) {
335 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
336 throw new UserBlockedError( $user->getBlock() );
339 // Finally, do the comprehensive permission check via isAllowed.
340 if ( !$this->userCanExecute( $user ) ) {
341 $this->displayRestrictionError();
345 public function execute( $par ) {
346 $this->useTransactionalTimeLimit();
348 $user = $this->getUser();
351 $this->outputHeader();
352 $this->addHelpLink( 'Help:Deletion_and_undeletion' );
354 $this->loadRequest( $par );
355 $this->checkPermissions(); // Needs to be after mTargetObj is set
357 $out = $this->getOutput();
359 if ( $this->mTargetObj
=== null ) {
360 $out->addWikiMsg( 'undelete-header' );
362 # Not all users can just browse every deleted page from the list
363 if ( $this->permissionManager
->userHasRight( $user, 'browsearchive' ) ) {
364 $this->showSearchForm();
370 $this->addHelpLink( 'Help:Undelete' );
371 if ( $this->mAllowed
) {
372 $out->setPageTitleMsg( $this->msg( 'undeletepage' ) );
374 $out->setPageTitleMsg( $this->msg( 'viewdeletedpage' ) );
377 $this->getSkin()->setRelevantTitle( $this->mTargetObj
);
379 if ( $this->mTimestamp
!== '' ) {
380 $this->showRevision( $this->mTimestamp
);
381 } elseif ( $this->mFilename
!== null && $this->mTargetObj
->inNamespace( NS_FILE
) ) {
382 $file = new ArchivedFile( $this->mTargetObj
, 0, $this->mFilename
);
383 // Check if user is allowed to see this file
384 if ( !$file->exists() ) {
385 $out->addWikiMsg( 'filedelete-nofile', $this->mFilename
);
386 } elseif ( !$file->userCan( File
::DELETED_FILE
, $user ) ) {
387 if ( $file->isDeleted( File
::DELETED_RESTRICTED
) ) {
388 throw new PermissionsError( 'suppressrevision' );
390 throw new PermissionsError( 'deletedtext' );
392 } elseif ( !$user->matchEditToken( $this->mToken
, $this->mFilename
) ) {
393 $this->showFileConfirmationForm( $this->mFilename
);
395 $this->showFile( $this->mFilename
);
397 } elseif ( $this->mAction
=== 'submit' ) {
398 if ( $this->mRestore
) {
400 } elseif ( $this->mRevdel
) {
401 $this->redirectToRevDel();
403 } elseif ( $this->mAction
=== 'render' ) {
404 $this->showMoreHistory();
406 $this->showHistory();
411 * Convert submitted form data to format expected by RevisionDelete and
412 * redirect the request
414 private function redirectToRevDel() {
417 foreach ( $this->getRequest()->getValues() as $key => $val ) {
419 if ( preg_match( "/^ts(\d{14})$/", $key, $matches ) ) {
420 $revisionRecord = $this->archivedRevisionLookup
421 ->getRevisionRecordByTimestamp( $this->mTargetObj
, $matches[1] );
422 if ( $revisionRecord ) {
424 $revisions[ $revisionRecord->getId() ] = 1;
430 'type' => 'revision',
432 'target' => $this->mTargetObj
->getPrefixedText()
434 $url = SpecialPage
::getTitleFor( 'Revisiondelete' )->getFullURL( $query );
435 $this->getOutput()->redirect( $url );
438 private function showSearchForm() {
439 $out = $this->getOutput();
440 $out->setPageTitleMsg( $this->msg( 'undelete-search-title' ) );
441 $fuzzySearch = $this->getRequest()->getVal( 'fuzzy', '1' );
446 $fields[] = new ActionFieldLayout(
447 new TextInputWidget( [
449 'inputId' => 'prefix',
451 'value' => $this->mSearchPrefix
,
454 new ButtonInputWidget( [
455 'label' => $this->msg( 'undelete-search-submit' )->text(),
456 'flags' => [ 'primary', 'progressive' ],
457 'inputId' => 'searchUndelete',
461 'label' => new HtmlSnippet(
463 $fuzzySearch ?
'undelete-search-full' : 'undelete-search-prefix'
470 $fieldset = new FieldsetLayout( [
471 'label' => $this->msg( 'undelete-search-box' )->text(),
475 $form = new FormLayout( [
477 'action' => wfScript(),
480 $form->appendContent(
483 Html
::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) .
484 Html
::hidden( 'fuzzy', $fuzzySearch )
497 # List undeletable articles
498 if ( $this->mSearchPrefix
) {
499 // For now, we enable search engine match only when specifically asked to
500 // by using fuzzy=1 parameter.
501 if ( $fuzzySearch ) {
502 $result = PageArchive
::listPagesBySearch( $this->mSearchPrefix
);
504 $result = PageArchive
::listPagesByPrefix( $this->mSearchPrefix
);
506 $this->showList( $result );
511 * Generic list of deleted pages
513 * @param IResultWrapper $result
516 private function showList( $result ) {
517 $out = $this->getOutput();
519 if ( $result->numRows() == 0 ) {
520 $out->addWikiMsg( 'undelete-no-results' );
525 $out->addWikiMsg( 'undeletepagetext', $this->getLanguage()->formatNum( $result->numRows() ) );
527 $linkRenderer = $this->getLinkRenderer();
528 $undelete = $this->getPageTitle();
529 $out->addHTML( "<ul id='undeleteResultsList'>\n" );
530 foreach ( $result as $row ) {
531 $title = Title
::makeTitleSafe( $row->ar_namespace
, $row->ar_title
);
532 if ( $title !== null ) {
533 $item = $linkRenderer->makeKnownLink(
535 $title->getPrefixedText(),
537 [ 'target' => $title->getPrefixedText() ]
540 // The title is no longer valid, show as text
541 $item = Html
::element(
543 [ 'class' => 'mw-invalidtitle' ],
544 Linker
::getInvalidTitleDescription(
551 $revs = $this->msg( 'undeleterevisions' )->numParams( $row->count
)->parse();
555 [ 'class' => 'undeleteResult' ],
556 $item . $this->msg( 'word-separator' )->escaped() .
557 $this->msg( 'parentheses' )->rawParams( $revs )->escaped()
562 $out->addHTML( "</ul>\n" );
567 private function showRevision( $timestamp ) {
568 if ( !preg_match( '/[0-9]{14}/', $timestamp ) ) {
571 $out = $this->getOutput();
572 $out->addModuleStyles( 'mediawiki.interface.helpers.styles' );
574 // When viewing a specific revision, add a subtitle link back to the overall
575 // history, see T284114
576 $listLink = $this->getLinkRenderer()->makeKnownLink(
577 $this->getPageTitle(),
578 $this->msg( 'undelete-back-to-list' )->text(),
580 [ 'target' => $this->mTargetObj
->getPrefixedText() ]
582 // same < arrow as with subpages
583 $subtitle = "< $listLink";
584 $out->setSubtitle( $subtitle );
586 $archive = new PageArchive( $this->mTargetObj
);
587 // FIXME: This hook must be deprecated, passing PageArchive by ref is awful.
588 if ( !$this->getHookRunner()->onUndeleteForm__showRevision(
589 $archive, $this->mTargetObj
)
593 $revRecord = $this->archivedRevisionLookup
->getRevisionRecordByTimestamp( $this->mTargetObj
, $timestamp );
595 $user = $this->getUser();
598 $out->addWikiMsg( 'undeleterevision-missing' );
602 if ( $revRecord->isDeleted( RevisionRecord
::DELETED_TEXT
) ) {
603 // Used in wikilinks, should not contain whitespaces
604 $titleText = $this->mTargetObj
->getPrefixedDBkey();
605 if ( !$revRecord->userCan( RevisionRecord
::DELETED_TEXT
, $this->getAuthority() ) ) {
606 $msg = $revRecord->isDeleted( RevisionRecord
::DELETED_RESTRICTED
)
607 ?
[ 'rev-suppressed-text-permission', $titleText ]
608 : [ 'rev-deleted-text-permission', $titleText ];
611 $this->msg( $msg[0], $msg[1] )->parse(),
618 $msg = $revRecord->isDeleted( RevisionRecord
::DELETED_RESTRICTED
)
619 ?
[ 'rev-suppressed-text-view', $titleText ]
620 : [ 'rev-deleted-text-view', $titleText ];
623 $this->msg( $msg[0], $msg[1] )->parse(),
627 // and we are allowed to see...
630 if ( $this->mDiff
) {
631 $previousRevRecord = $this->archivedRevisionLookup
632 ->getPreviousRevisionRecord( $this->mTargetObj
, $timestamp );
633 if ( $previousRevRecord ) {
634 $this->showDiff( $previousRevRecord, $revRecord );
635 if ( $this->mDiffOnly
) {
639 $out->addHTML( '<hr />' );
641 $out->addWikiMsg( 'undelete-nodiff' );
645 $link = $this->getLinkRenderer()->makeKnownLink(
646 $this->getPageTitle( $this->mTargetObj
->getPrefixedDBkey() ),
647 $this->mTargetObj
->getPrefixedText()
650 $lang = $this->getLanguage();
652 // date and time are separate parameters to facilitate localisation.
653 // $time is kept for backward compat reasons.
654 $time = $lang->userTimeAndDate( $timestamp, $user );
655 $d = $lang->userDate( $timestamp, $user );
656 $t = $lang->userTime( $timestamp, $user );
657 $userLink = Linker
::revUserTools( $revRecord );
660 $content = $revRecord->getContent(
662 RevisionRecord
::FOR_THIS_USER
,
665 } catch ( RevisionAccessException
$e ) {
669 // TODO: MCR: this will have to become something like $hasTextSlots and $hasNonTextSlots
670 $isText = ( $content instanceof TextContent
);
672 $undeleteRevisionContent = '';
673 // Revision delete links
674 if ( !$this->mDiff
) {
675 $revdel = Linker
::getRevDeleteLink(
681 $undeleteRevisionContent = $revdel . ' ';
685 $undeleteRevisionContent .= $out->msg(
687 Message
::rawParam( $link ),
689 Message
::rawParam( $userLink ),
694 if ( $this->mPreview ||
$isText ) {
697 $undeleteRevisionContent,
698 'mw-undelete-revision'
705 [ 'class' => 'mw-undelete-revision', ],
706 $undeleteRevisionContent
711 if ( $this->mPreview ||
!$isText ) {
712 // NOTE: non-text content has no source view, so always use rendered preview
714 $popts = $out->parserOptions();
717 $rendered = $this->revisionRenderer
->getRenderedRevision(
721 [ 'audience' => RevisionRecord
::FOR_THIS_USER
, 'causeAction' => 'undelete-preview' ]
724 // Fail hard if the audience check fails, since we already checked
725 // at the beginning of this method.
726 $pout = $rendered->getRevisionParserOutput();
728 $out->addParserOutput( $pout, [
729 'enableSectionEditLinks' => false,
731 } catch ( RevisionAccessException
$e ) {
739 '@phan-var TextContent $content';
740 // TODO: MCR: make this work for multiple slots
741 // source view for textual content
742 $sourceView = Xml
::element( 'textarea', [
743 'readonly' => 'readonly',
746 ], $content->getText() . "\n" );
748 $buttonFields[] = new ButtonInputWidget( [
751 'label' => $this->msg( 'showpreview' )->text()
757 $buttonFields[] = new ButtonInputWidget( [
760 'label' => $this->msg( 'showdiff' )->text()
765 Xml
::openElement( 'div', [
766 'style' => 'clear: both' ] ) .
767 Xml
::openElement( 'form', [
769 'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ) ] ) .
770 Xml
::element( 'input', [
773 'value' => $this->mTargetObj
->getPrefixedDBkey() ] ) .
774 Xml
::element( 'input', [
776 'name' => 'timestamp',
777 'value' => $timestamp ] ) .
778 Xml
::element( 'input', [
780 'name' => 'wpEditToken',
781 'value' => $user->getEditToken() ] ) .
784 'content' => new HorizontalLayout( [
785 'items' => $buttonFields
789 Xml
::closeElement( 'form' ) .
790 Xml
::closeElement( 'div' )
795 * Build a diff display between this and the previous either deleted
796 * or non-deleted edit.
798 * @param RevisionRecord $previousRevRecord
799 * @param RevisionRecord $currentRevRecord
801 private function showDiff(
802 RevisionRecord
$previousRevRecord,
803 RevisionRecord
$currentRevRecord
805 $currentTitle = Title
::newFromLinkTarget( $currentRevRecord->getPageAsLinkTarget() );
807 $diffContext = new DerivativeContext( $this->getContext() );
808 $diffContext->setTitle( $currentTitle );
809 $diffContext->setWikiPage( $this->wikiPageFactory
->newFromTitle( $currentTitle ) );
811 $contentModel = $currentRevRecord->getSlot(
816 $diffEngine = $this->contentHandlerFactory
->getContentHandler( $contentModel )
817 ->createDifferenceEngine( $diffContext );
819 $diffEngine->setRevisions( $previousRevRecord, $currentRevRecord );
820 $diffEngine->showDiffStyle();
821 $formattedDiff = $diffEngine->getDiff(
822 $this->diffHeader( $previousRevRecord, 'o' ),
823 $this->diffHeader( $currentRevRecord, 'n' )
826 $this->getOutput()->addHTML( "<div>$formattedDiff</div>\n" );
830 * @param RevisionRecord $revRecord
831 * @param string $prefix
834 private function diffHeader( RevisionRecord
$revRecord, $prefix ) {
835 if ( $revRecord instanceof RevisionArchiveRecord
) {
836 // Revision in the archive table, only viewable via this special page
837 $targetPage = $this->getPageTitle();
839 'target' => $this->mTargetObj
->getPrefixedText(),
840 'timestamp' => wfTimestamp( TS_MW
, $revRecord->getTimestamp() )
843 // Revision in the revision table, viewable by oldid
844 $targetPage = $revRecord->getPageAsLinkTarget();
845 $targetQuery = [ 'oldid' => $revRecord->getId() ];
848 // Add show/hide deletion links if available
849 $user = $this->getUser();
850 $lang = $this->getLanguage();
851 $rdel = Linker
::getRevDeleteLink( $user, $revRecord, $this->mTargetObj
);
857 $minor = $revRecord->isMinor() ? ChangesList
::flag( 'minor' ) : '';
859 $dbr = $this->dbProvider
->getReplicaDatabase();
860 $tagIds = $dbr->newSelectQueryBuilder()
861 ->select( 'ct_tag_id' )
862 ->from( 'change_tag' )
863 ->where( [ 'ct_rev_id' => $revRecord->getId() ] )
864 ->caller( __METHOD__
)->fetchFieldValues();
866 foreach ( $tagIds as $tagId ) {
868 $tags[] = $this->changeTagDefStore
->getName( (int)$tagId );
869 } catch ( NameTableAccessException
$exception ) {
873 $tags = implode( ',', $tags );
874 $tagSummary = ChangeTags
::formatSummaryRow( $tags, 'deleteddiff', $this->getContext() );
875 $asof = $this->getLinkRenderer()->makeLink(
879 $lang->userTimeAndDate( $revRecord->getTimestamp(), $user ),
880 $lang->userDate( $revRecord->getTimestamp(), $user ),
881 $lang->userTime( $revRecord->getTimestamp(), $user )
886 if ( $revRecord->isDeleted( RevisionRecord
::DELETED_TEXT
) ) {
887 $asof = Html
::rawElement(
889 [ 'class' => Linker
::getRevisionDeletedClass( $revRecord ) ],
894 // FIXME This is reimplementing DifferenceEngine#getRevisionHeader
895 // and partially #showDiffPage, but worse
896 return '<div id="mw-diff-' . $prefix . 'title1"><strong>' .
899 '<div id="mw-diff-' . $prefix . 'title2">' .
900 Linker
::revUserTools( $revRecord ) . '<br />' .
902 '<div id="mw-diff-' . $prefix . 'title3">' .
903 $minor . $this->commentFormatter
->formatRevision( $revRecord, $user ) . $rdel . '<br />' .
905 '<div id="mw-diff-' . $prefix . 'title5">' .
906 $tagSummary[0] . '<br />' .
911 * Show a form confirming whether a tokenless user really wants to see a file
914 private function showFileConfirmationForm( $key ) {
915 $out = $this->getOutput();
916 $lang = $this->getLanguage();
917 $user = $this->getUser();
918 $file = new ArchivedFile( $this->mTargetObj
, 0, $this->mFilename
);
919 $out->addWikiMsg( 'undelete-show-file-confirm',
920 $this->mTargetObj
->getText(),
921 $lang->userDate( $file->getTimestamp(), $user ),
922 $lang->userTime( $file->getTimestamp(), $user ) );
924 Html
::rawElement( 'form', [
926 'action' => $this->getPageTitle()->getLocalURL( [
927 'target' => $this->mTarget
,
929 'token' => $user->getEditToken( $key ),
932 Xml
::submitButton( $this->msg( 'undelete-show-file-submit' )->text() )
938 * Show a deleted file version requested by the visitor.
941 private function showFile( $key ) {
942 $this->getOutput()->disable();
944 # We mustn't allow the output to be CDN cached, otherwise
945 # if an admin previews a deleted image, and it's cached, then
946 # a user without appropriate permissions can toddle off and
947 # nab the image, and CDN will serve it
948 $response = $this->getRequest()->response();
949 $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
950 $response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
952 $path = $this->localRepo
->getZonePath( 'deleted' ) . '/' . $this->localRepo
->getDeletedHashPath( $key ) . $key;
953 $this->localRepo
->streamFileWithStatus( $path );
957 * @param LinkBatch $batch
958 * @param IResultWrapper $revisions
960 private function addRevisionsToBatch( LinkBatch
$batch, IResultWrapper
$revisions ) {
961 foreach ( $revisions as $row ) {
962 $batch->add( NS_USER
, $row->ar_user_text
);
963 $batch->add( NS_USER_TALK
, $row->ar_user_text
);
968 * @param LinkBatch $batch
969 * @param IResultWrapper $files
971 private function addFilesToBatch( LinkBatch
$batch, IResultWrapper
$files ) {
972 foreach ( $files as $row ) {
973 $batch->add( NS_USER
, $row->fa_user_text
);
974 $batch->add( NS_USER_TALK
, $row->fa_user_text
);
979 * Handle XHR "show more history" requests (T249977)
981 protected function showMoreHistory() {
982 $out = $this->getOutput();
983 $out->setArticleBodyOnly( true );
984 $dbr = $this->dbProvider
->getReplicaDatabase();
985 if ( $this->mHistoryOffset
) {
986 $extraConds = [ $dbr->expr( 'ar_timestamp', '<', $dbr->timestamp( $this->mHistoryOffset
) ) ];
990 $revisions = $this->archivedRevisionLookup
->listRevisions(
993 self
::REVISION_HISTORY_LIMIT +
1
995 $batch = $this->linkBatchFactory
->newLinkBatch();
996 $this->addRevisionsToBatch( $batch, $revisions );
998 $out->addHTML( $this->formatRevisionHistory( $revisions ) );
1000 if ( $revisions->numRows() > self
::REVISION_HISTORY_LIMIT
) {
1001 // Indicate to JS that the "show more" button should remain active
1002 $out->setStatusCode( 206 );
1007 * Generate the <ul> element representing a list of deleted revisions
1009 * @param IResultWrapper $revisions
1012 protected function formatRevisionHistory( IResultWrapper
$revisions ) {
1013 $history = Html
::openElement( 'ul', [ 'class' => 'mw-undelete-revlist' ] );
1015 // Exclude the last data row if there is more data than history limit amount
1016 $numRevisions = $revisions->numRows();
1017 $displayCount = min( $numRevisions, self
::REVISION_HISTORY_LIMIT
);
1018 $firstRev = $this->revisionStore
->getFirstRevision( $this->mTargetObj
);
1019 $earliestLiveTime = $firstRev ?
$firstRev->getTimestamp() : null;
1021 $revisions->rewind();
1022 for ( $i = 0; $i < $displayCount; $i++
) {
1023 $row = $revisions->fetchObject();
1024 // The $remaining parameter controls diff links and so must
1025 // include the undisplayed row beyond the display limit.
1026 $history .= $this->formatRevisionRow( $row, $earliestLiveTime, $numRevisions - $i );
1028 $history .= Html
::closeElement( 'ul' );
1032 protected function showHistory() {
1033 $this->checkReadOnly();
1035 $out = $this->getOutput();
1036 if ( $this->mAllowed
) {
1037 $out->addModules( 'mediawiki.misc-authed-ooui' );
1038 $out->addModuleStyles( 'mediawiki.special' );
1040 $out->addModuleStyles( 'mediawiki.interface.helpers.styles' );
1042 "<div class='mw-undelete-pagetitle'>\n$1\n</div>\n",
1043 [ 'undeletepagetitle', wfEscapeWikiText( $this->mTargetObj
->getPrefixedText() ) ]
1046 $archive = new PageArchive( $this->mTargetObj
);
1047 // FIXME: This hook must be deprecated, passing PageArchive by ref is awful.
1048 $this->getHookRunner()->onUndeleteForm__showHistory( $archive, $this->mTargetObj
);
1050 $out->addHTML( Html
::openElement( 'div', [ 'class' => 'mw-undelete-history' ] ) );
1051 if ( $this->mAllowed
) {
1052 $out->addWikiMsg( 'undeletehistory' );
1053 $out->addWikiMsg( 'undeleterevdel' );
1055 $out->addWikiMsg( 'undeletehistorynoadmin' );
1057 $out->addHTML( Html
::closeElement( 'div' ) );
1059 # List all stored revisions
1060 $revisions = $this->archivedRevisionLookup
->listRevisions(
1063 self
::REVISION_HISTORY_LIMIT +
1
1065 $files = $archive->listFiles();
1066 $numRevisions = $revisions->numRows();
1067 $showLoadMore = $numRevisions > self
::REVISION_HISTORY_LIMIT
;
1068 $haveRevisions = $numRevisions > 0;
1069 $haveFiles = $files && $files->numRows() > 0;
1071 # Batch existence check on user and talk pages
1072 if ( $haveRevisions ||
$haveFiles ) {
1073 $batch = $this->linkBatchFactory
->newLinkBatch();
1074 $this->addRevisionsToBatch( $batch, $revisions );
1076 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable -- $files is non-null
1077 $this->addFilesToBatch( $batch, $files );
1082 if ( $this->mAllowed
) {
1085 $action = $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] );
1086 # Start the form here
1087 $form = new FormLayout( [
1089 'action' => $action,
1094 # Show relevant lines from the deletion log:
1095 $deleteLogPage = new LogPage( 'delete' );
1096 $out->addHTML( Xml
::element( 'h2', null, $deleteLogPage->getName()->text() ) . "\n" );
1097 LogEventsList
::showLogExtract( $out, 'delete', $this->mTargetObj
);
1098 # Show relevant lines from the suppression log:
1099 $suppressLogPage = new LogPage( 'suppress' );
1100 if ( $this->permissionManager
->userHasRight( $this->getUser(), 'suppressionlog' ) ) {
1101 $out->addHTML( Xml
::element( 'h2', null, $suppressLogPage->getName()->text() ) . "\n" );
1102 LogEventsList
::showLogExtract( $out, 'suppress', $this->mTargetObj
);
1105 if ( $this->mAllowed
&& ( $haveRevisions ||
$haveFiles ) ) {
1106 $unsuppressAllowed = $this->permissionManager
->userHasRight( $this->getUser(), 'suppressrevision' );
1108 $fields[] = new Layout( [
1109 'content' => new HtmlSnippet( $this->msg( 'undeleteextrahelp' )->parseAsBlock() )
1112 $dropdownComment = $this->msg( 'undelete-comment-dropdown' )
1113 ->page( $this->mTargetObj
)->inContentLanguage()->text();
1114 // Add additional specific reasons for unsuppress
1115 if ( $unsuppressAllowed ) {
1116 $dropdownComment .= "\n" . $this->msg( 'undelete-comment-dropdown-unsuppress' )
1117 ->page( $this->mTargetObj
)->inContentLanguage()->text();
1119 $options = Xml
::listDropdownOptions(
1121 [ 'other' => $this->msg( 'undeletecommentotherlist' )->text() ]
1123 $options = Xml
::listDropdownOptionsOoui( $options );
1125 $fields[] = new FieldLayout(
1126 new DropdownInputWidget( [
1127 'name' => 'wpCommentList',
1128 'inputId' => 'wpCommentList',
1129 'infusable' => true,
1130 'value' => $this->getRequest()->getText( 'wpCommentList', 'other' ),
1131 'options' => $options,
1134 'label' => $this->msg( 'undeletecomment' )->text(),
1139 $fields[] = new FieldLayout(
1140 new TextInputWidget( [
1141 'name' => 'wpComment',
1142 'inputId' => 'wpComment',
1143 'infusable' => true,
1144 'value' => $this->getRequest()->getText( 'wpComment' ),
1145 'autofocus' => true,
1146 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
1147 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
1148 // Unicode codepoints.
1149 'maxLength' => CommentStore
::COMMENT_CHARACTER_LIMIT
,
1152 'label' => $this->msg( 'undeleteothercomment' )->text(),
1157 if ( $this->getUser()->isRegistered() ) {
1158 $checkWatch = $this->watchlistManager
->isWatched( $this->getUser(), $this->mTargetObj
)
1159 ||
$this->getRequest()->getText( 'wpWatch' );
1160 $fields[] = new FieldLayout(
1161 new CheckboxInputWidget( [
1162 'name' => 'wpWatch',
1163 'inputId' => 'mw-undelete-watch',
1165 'selected' => $checkWatch,
1168 'label' => $this->msg( 'watchthis' )->text(),
1169 'align' => 'inline',
1174 if ( $unsuppressAllowed ) {
1175 $fields[] = new FieldLayout(
1176 new CheckboxInputWidget( [
1177 'name' => 'wpUnsuppress',
1178 'inputId' => 'mw-undelete-unsuppress',
1182 'label' => $this->msg( 'revdelete-unsuppress' )->text(),
1183 'align' => 'inline',
1188 $undelPage = $this->undeletePageFactory
->newUndeletePage(
1189 $this->wikiPageFactory
->newFromTitle( $this->mTargetObj
),
1190 $this->getContext()->getAuthority()
1192 if ( $undelPage->canProbablyUndeleteAssociatedTalk()->isGood() ) {
1193 $fields[] = new FieldLayout(
1194 new CheckboxInputWidget( [
1195 'name' => 'undeletetalk',
1196 'inputId' => 'mw-undelete-undeletetalk',
1197 'selected' => false,
1200 'label' => $this->msg( 'undelete-undeletetalk' )->text(),
1201 'align' => 'inline',
1206 $fields[] = new FieldLayout(
1208 'content' => new HorizontalLayout( [
1210 new ButtonInputWidget( [
1211 'name' => 'restore',
1212 'inputId' => 'mw-undelete-submit',
1214 'label' => $this->msg( 'undeletebtn' )->text(),
1215 'flags' => [ 'primary', 'progressive' ],
1218 new ButtonInputWidget( [
1220 'inputId' => 'mw-undelete-invert',
1222 'label' => $this->msg( 'undeleteinvert' )->text()
1229 $fieldset = new FieldsetLayout( [
1230 'label' => $this->msg( 'undelete-fieldset-title' )->text(),
1231 'id' => 'mw-undelete-table',
1236 if ( $this->getAuthority()->isAllowed( 'editinterface' ) ) {
1237 if ( $unsuppressAllowed ) {
1238 $link .= $this->getLinkRenderer()->makeKnownLink(
1239 $this->msg( 'undelete-comment-dropdown-unsuppress' )->inContentLanguage()->getTitle(),
1240 $this->msg( 'undelete-edit-commentlist-unsuppress' )->text(),
1242 [ 'action' => 'edit' ]
1244 $link .= $this->msg( 'pipe-separator' )->escaped();
1246 $link .= $this->getLinkRenderer()->makeKnownLink(
1247 $this->msg( 'undelete-comment-dropdown' )->inContentLanguage()->getTitle(),
1248 $this->msg( 'undelete-edit-commentlist' )->text(),
1250 [ 'action' => 'edit' ]
1253 $link = Html
::rawElement( 'p', [ 'class' => 'mw-undelete-editcomments' ], $link );
1256 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable form is set, when used here
1257 $form->appendContent(
1259 'expanded' => false,
1262 'content' => $fieldset,
1266 Html
::hidden( 'target', $this->mTarget
) .
1267 Html
::hidden( 'wpEditToken', $this->getUser()->getEditToken() )
1273 $history .= Xml
::element( 'h2', null, $this->msg( 'history' )->text() ) . "\n";
1275 if ( $haveRevisions ) {
1276 # Show the page's stored (deleted) history
1278 if ( $this->permissionManager
->userHasRight( $this->getUser(), 'deleterevision' ) ) {
1279 $history .= Html
::element(
1284 'class' => 'deleterevision-log-submit mw-log-deleterevision-button'
1286 $this->msg( 'showhideselectedversions' )->text()
1290 $history .= $this->formatRevisionHistory( $revisions );
1292 if ( $showLoadMore ) {
1294 Html
::openElement( 'div' ) .
1297 [ 'id' => 'mw-load-more-revisions' ],
1298 $this->msg( 'undelete-load-more-revisions' )->text()
1300 Html
::closeElement( 'div' ) .
1304 $out->addWikiMsg( 'nohistory' );
1308 $history .= Xml
::element( 'h2', null, $this->msg( 'filehist' )->text() ) . "\n";
1309 $history .= Html
::openElement( 'ul', [ 'class' => 'mw-undelete-revlist' ] );
1310 foreach ( $files as $row ) {
1311 $history .= $this->formatFileRow( $row );
1314 $history .= Html
::closeElement( 'ul' );
1317 if ( $this->mAllowed
) {
1318 # Slip in the hidden controls here
1319 $misc = Html
::hidden( 'target', $this->mTarget
);
1320 $misc .= Html
::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
1323 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable form is set, when used here
1324 $form->appendContent( new HtmlSnippet( $history ) );
1325 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable form is set, when used here
1326 $out->addHTML( (string)$form );
1328 $out->addHTML( $history );
1334 protected function formatRevisionRow( $row, $earliestLiveTime, $remaining ) {
1335 $revRecord = $this->revisionStore
->newRevisionFromArchiveRow(
1337 IDBAccessObject
::READ_NORMAL
,
1342 $ts = wfTimestamp( TS_MW
, $row->ar_timestamp
);
1343 // Build checkboxen...
1344 if ( $this->mAllowed
) {
1345 if ( $this->mInvert
) {
1346 if ( in_array( $ts, $this->mTargetTimestamp
) ) {
1347 $checkBox = Xml
::check( "ts$ts" );
1349 $checkBox = Xml
::check( "ts$ts", true );
1352 $checkBox = Xml
::check( "ts$ts" );
1358 // Build page & diff links...
1359 $user = $this->getUser();
1360 if ( $this->mCanView
) {
1361 $titleObj = $this->getPageTitle();
1363 if ( !$revRecord->userCan( RevisionRecord
::DELETED_TEXT
, $this->getAuthority() ) ) {
1364 $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1365 $last = $this->msg( 'diff' )->escaped();
1366 } elseif ( $remaining > 0 ||
( $earliestLiveTime && $ts > $earliestLiveTime ) ) {
1367 $pageLink = $this->getPageLink( $revRecord, $titleObj, $ts );
1368 $last = $this->getLinkRenderer()->makeKnownLink(
1370 $this->msg( 'diff' )->text(),
1373 'target' => $this->mTargetObj
->getPrefixedText(),
1379 $pageLink = $this->getPageLink( $revRecord, $titleObj, $ts );
1380 $last = $this->msg( 'diff' )->escaped();
1383 $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1384 $last = $this->msg( 'diff' )->escaped();
1388 $userLink = Linker
::revUserTools( $revRecord );
1391 $minor = $revRecord->isMinor() ? ChangesList
::flag( 'minor' ) : '';
1393 // Revision text size
1394 $size = $row->ar_len
;
1395 if ( $size !== null ) {
1396 $revTextSize = Linker
::formatRevisionSize( $size );
1400 $comment = $this->commentFormatter
->formatRevision( $revRecord, $user );
1404 [ $tagSummary, $classes ] = ChangeTags
::formatSummaryRow(
1410 $attribs['class'] = implode( ' ', $classes );
1413 $revisionRow = $this->msg( 'undelete-revision-row2' )
1426 return Xml
::tags( 'li', $attribs, $revisionRow ) . "\n";
1429 private function formatFileRow( $row ) {
1430 $file = ArchivedFile
::newFromRow( $row );
1431 $ts = wfTimestamp( TS_MW
, $row->fa_timestamp
);
1432 $user = $this->getUser();
1435 if ( $this->mCanView
&& $row->fa_storage_key
) {
1436 if ( $this->mAllowed
) {
1437 $checkBox = Xml
::check( 'fileid' . $row->fa_id
);
1439 $key = urlencode( $row->fa_storage_key
);
1440 $pageLink = $this->getFileLink( $file, $this->getPageTitle(), $ts, $key );
1442 $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1444 $userLink = $this->getFileUser( $file );
1445 $data = $this->msg( 'widthheight' )->numParams( $row->fa_width
, $row->fa_height
)->text();
1446 $bytes = $this->msg( 'parentheses' )
1447 ->plaintextParams( $this->msg( 'nbytes' )->numParams( $row->fa_size
)->text() )
1449 $data = htmlspecialchars( $data . ' ' . $bytes );
1450 $comment = $this->getFileComment( $file );
1452 // Add show/hide deletion links if available
1453 $canHide = $this->isAllowed( 'deleterevision' );
1454 if ( $canHide ||
( $file->getVisibility() && $this->isAllowed( 'deletedhistory' ) ) ) {
1455 if ( !$file->userCan( File
::DELETED_RESTRICTED
, $user ) ) {
1456 // Revision was hidden from sysops
1457 $revdlink = Linker
::revDeleteLinkDisabled( $canHide );
1460 'type' => 'filearchive',
1461 'target' => $this->mTargetObj
->getPrefixedDBkey(),
1462 'ids' => $row->fa_id
1464 $revdlink = Linker
::revDeleteLink( $query,
1465 $file->isDeleted( File
::DELETED_RESTRICTED
), $canHide );
1471 return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
1475 * Fetch revision text link if it's available to all users
1477 * @param RevisionRecord $revRecord
1478 * @param LinkTarget $target
1479 * @param string $ts Timestamp
1482 private function getPageLink( RevisionRecord
$revRecord, LinkTarget
$target, $ts ) {
1483 $user = $this->getUser();
1484 $time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1486 if ( !$revRecord->userCan( RevisionRecord
::DELETED_TEXT
, $this->getAuthority() ) ) {
1487 // TODO The condition cannot be true when the function is called
1488 return Html
::element(
1490 [ 'class' => 'history-deleted' ],
1495 $link = $this->getLinkRenderer()->makeKnownLink(
1500 'target' => $this->mTargetObj
->getPrefixedText(),
1505 if ( $revRecord->isDeleted( RevisionRecord
::DELETED_TEXT
) ) {
1506 $class = Linker
::getRevisionDeletedClass( $revRecord );
1507 $link = '<span class="' . $class . '">' . $link . '</span>';
1514 * Fetch image view link if it's available to all users
1516 * @param File|ArchivedFile $file
1517 * @param LinkTarget $target
1518 * @param string $ts A timestamp
1519 * @param string $key A storage key
1521 * @return string HTML fragment
1523 private function getFileLink( $file, LinkTarget
$target, $ts, $key ) {
1524 $user = $this->getUser();
1525 $time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1527 if ( !$file->userCan( File
::DELETED_FILE
, $user ) ) {
1528 return Html
::element(
1530 [ 'class' => 'history-deleted' ],
1535 if ( $file->exists() ) {
1536 $link = $this->getLinkRenderer()->makeKnownLink(
1541 'target' => $this->mTargetObj
->getPrefixedText(),
1543 'token' => $user->getEditToken( $key )
1547 $link = htmlspecialchars( $time );
1550 if ( $file->isDeleted( File
::DELETED_FILE
) ) {
1551 $link = '<span class="history-deleted">' . $link . '</span>';
1558 * Fetch file's user id if it's available to this user
1560 * @param File|ArchivedFile $file
1561 * @return string HTML fragment
1563 private function getFileUser( $file ) {
1564 $uploader = $file->getUploader( File
::FOR_THIS_USER
, $this->getAuthority() );
1566 return Html
::rawElement(
1568 [ 'class' => 'history-deleted' ],
1569 $this->msg( 'rev-deleted-user' )->escaped()
1573 $link = Linker
::userLink( $uploader->getId(), $uploader->getName() ) .
1574 Linker
::userToolLinks( $uploader->getId(), $uploader->getName() );
1576 if ( $file->isDeleted( File
::DELETED_USER
) ) {
1577 $link = Html
::rawElement(
1579 [ 'class' => 'history-deleted' ],
1588 * Fetch file upload comment if it's available to this user
1590 * @param File|ArchivedFile $file
1591 * @return string HTML fragment
1593 private function getFileComment( $file ) {
1594 if ( !$file->userCan( File
::DELETED_COMMENT
, $this->getAuthority() ) ) {
1595 return Html
::rawElement(
1597 [ 'class' => 'history-deleted' ],
1600 [ 'class' => 'comment' ],
1601 $this->msg( 'rev-deleted-comment' )->escaped()
1606 $comment = $file->getDescription( File
::FOR_THIS_USER
, $this->getAuthority() );
1607 $link = $this->commentFormatter
->formatBlock( $comment );
1609 if ( $file->isDeleted( File
::DELETED_COMMENT
) ) {
1610 $link = Html
::rawElement(
1612 [ 'class' => 'history-deleted' ],
1620 private function undelete() {
1621 if ( $this->getConfig()->get( MainConfigNames
::UploadMaintenance
)
1622 && $this->mTargetObj
->getNamespace() === NS_FILE
1624 throw new ErrorPageError( 'undelete-error', 'filedelete-maintenance' );
1627 $this->checkReadOnly();
1629 $out = $this->getOutput();
1630 $undeletePage = $this->undeletePageFactory
->newUndeletePage(
1631 $this->wikiPageFactory
->newFromTitle( $this->mTargetObj
),
1632 $this->getAuthority()
1634 if ( $this->mUndeleteTalk
&& $undeletePage->canProbablyUndeleteAssociatedTalk()->isGood() ) {
1635 $undeletePage->setUndeleteAssociatedTalk( true );
1637 $status = $undeletePage
1638 ->setUndeleteOnlyTimestamps( $this->mTargetTimestamp
)
1639 ->setUndeleteOnlyFileVersions( $this->mFileVersions
)
1640 ->setUnsuppress( $this->mUnsuppress
)
1641 // TODO This is currently duplicating some permission checks, but we do need it (T305680)
1642 ->undeleteIfAllowed( $this->mComment
);
1644 if ( !$status->isGood() ) {
1645 $out->setPageTitleMsg( $this->msg( 'undelete-error' ) );
1646 $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
1647 foreach ( $status->getMessages() as $msg ) {
1648 $out->addHTML( Html
::errorBox(
1649 $this->msg( $msg )->parse()
1655 $restoredRevs = $status->getValue()[UndeletePage
::REVISIONS_RESTORED
];
1656 $restoredFiles = $status->getValue()[UndeletePage
::FILES_RESTORED
];
1658 if ( $restoredRevs === 0 && $restoredFiles === 0 ) {
1659 // TODO Should use a different message here
1660 $out->setPageTitleMsg( $this->msg( 'undelete-error' ) );
1662 if ( $status->getValue()[UndeletePage
::FILES_RESTORED
] !== 0 ) {
1663 $this->getHookRunner()->onFileUndeleteComplete(
1664 $this->mTargetObj
, $this->mFileVersions
, $this->getUser(), $this->mComment
);
1667 $link = $this->getLinkRenderer()->makeKnownLink( $this->mTargetObj
);
1668 $out->addWikiMsg( 'undeletedpage', Message
::rawParam( $link ) );
1670 $this->watchlistManager
->setWatch(
1671 $this->getRequest()->getCheck( 'wpWatch' ),
1672 $this->getAuthority(),
1679 * Return an array of subpages beginning with $search that this special page will accept.
1681 * @param string $search Prefix to search for
1682 * @param int $limit Maximum number of results to return (usually 10)
1683 * @param int $offset Number of results to skip (usually 0)
1684 * @return string[] Matching subpages
1686 public function prefixSearchSubpages( $search, $limit, $offset ) {
1687 return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory
);
1690 protected function getGroupName() {
1696 * Retain the old class name for backwards compatibility.
1697 * @deprecated since 1.41
1699 class_alias( SpecialUndelete
::class, 'SpecialUndelete' );