MediaWiki  master
SpecialUndelete.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Specials;
25 
26 use ArchivedFile;
27 use ChangesList;
28 use ChangeTags;
30 use ErrorPageError;
31 use File;
32 use LinkBatch;
33 use LocalRepo;
34 use LogEventsList;
35 use LogPage;
60 use Message;
61 use PageArchive;
63 use RepoGroup;
65 use TextContent;
69 use Xml;
70 
78 
83  private const REVISION_HISTORY_LIMIT = 500;
84 
85  private $mAction;
86  private $mTarget;
87  private $mTimestamp;
88  private $mRestore;
89  private $mRevdel;
90  private $mInvert;
91  private $mFilename;
93  private $mTargetTimestamp = [];
94  private $mAllowed;
95  private $mCanView;
97  private $mComment = '';
98  private $mToken;
100  private $mPreview;
102  private $mDiff;
104  private $mDiffOnly;
106  private $mUnsuppress;
108  private $mFileVersions = [];
110  private $mUndeleteTalk;
112  private $mHistoryOffset;
113 
115  private $mTargetObj;
119  private $mSearchPrefix;
120 
121  private PermissionManager $permissionManager;
122  private RevisionStore $revisionStore;
123  private RevisionRenderer $revisionRenderer;
124  private IContentHandlerFactory $contentHandlerFactory;
125  private NameTableStore $changeTagDefStore;
126  private LinkBatchFactory $linkBatchFactory;
127  private LocalRepo $localRepo;
128  private IConnectionProvider $dbProvider;
129  private UserOptionsLookup $userOptionsLookup;
130  private WikiPageFactory $wikiPageFactory;
131  private SearchEngineFactory $searchEngineFactory;
132  private UndeletePageFactory $undeletePageFactory;
133  private ArchivedRevisionLookup $archivedRevisionLookup;
134  private CommentFormatter $commentFormatter;
135 
152  public function __construct(
153  PermissionManager $permissionManager,
154  RevisionStore $revisionStore,
155  RevisionRenderer $revisionRenderer,
156  IContentHandlerFactory $contentHandlerFactory,
157  NameTableStore $changeTagDefStore,
158  LinkBatchFactory $linkBatchFactory,
159  RepoGroup $repoGroup,
160  IConnectionProvider $dbProvider,
161  UserOptionsLookup $userOptionsLookup,
162  WikiPageFactory $wikiPageFactory,
163  SearchEngineFactory $searchEngineFactory,
164  UndeletePageFactory $undeletePageFactory,
165  ArchivedRevisionLookup $archivedRevisionLookup,
166  CommentFormatter $commentFormatter
167  ) {
168  parent::__construct( 'Undelete', 'deletedhistory' );
169  $this->permissionManager = $permissionManager;
170  $this->revisionStore = $revisionStore;
171  $this->revisionRenderer = $revisionRenderer;
172  $this->contentHandlerFactory = $contentHandlerFactory;
173  $this->changeTagDefStore = $changeTagDefStore;
174  $this->linkBatchFactory = $linkBatchFactory;
175  $this->localRepo = $repoGroup->getLocalRepo();
176  $this->dbProvider = $dbProvider;
177  $this->userOptionsLookup = $userOptionsLookup;
178  $this->wikiPageFactory = $wikiPageFactory;
179  $this->searchEngineFactory = $searchEngineFactory;
180  $this->undeletePageFactory = $undeletePageFactory;
181  $this->archivedRevisionLookup = $archivedRevisionLookup;
182  $this->commentFormatter = $commentFormatter;
183  }
184 
185  public function doesWrites() {
186  return true;
187  }
188 
189  private function loadRequest( $par ) {
190  $request = $this->getRequest();
191  $user = $this->getUser();
192 
193  $this->mAction = $request->getRawVal( 'action' );
194  if ( $par !== null && $par !== '' ) {
195  $this->mTarget = $par;
196  } else {
197  $this->mTarget = $request->getVal( 'target' );
198  }
199 
200  $this->mTargetObj = null;
201 
202  if ( $this->mTarget !== null && $this->mTarget !== '' ) {
203  $this->mTargetObj = Title::newFromText( $this->mTarget );
204  }
205 
206  $this->mSearchPrefix = $request->getText( 'prefix' );
207  $time = $request->getVal( 'timestamp' );
208  $this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : '';
209  $this->mFilename = $request->getVal( 'file' );
210 
211  $posted = $request->wasPosted() &&
212  $user->matchEditToken( $request->getVal( 'wpEditToken' ) );
213  $this->mRestore = $request->getCheck( 'restore' ) && $posted;
214  $this->mRevdel = $request->getCheck( 'revdel' ) && $posted;
215  $this->mInvert = $request->getCheck( 'invert' ) && $posted;
216  $this->mPreview = $request->getCheck( 'preview' ) && $posted;
217  $this->mDiff = $request->getCheck( 'diff' );
218  $this->mDiffOnly = $request->getBool( 'diffonly',
219  $this->userOptionsLookup->getOption( $this->getUser(), 'diffonly' ) );
220  $this->mComment = $request->getText( 'wpComment' );
221  $this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) &&
222  $this->permissionManager->userHasRight( $user, 'suppressrevision' );
223  $this->mToken = $request->getVal( 'token' );
224  $this->mUndeleteTalk = $request->getCheck( 'undeletetalk' );
225  $this->mHistoryOffset = $request->getVal( 'historyoffset' );
226 
227  if ( $this->isAllowed( 'undelete' ) ) {
228  $this->mAllowed = true; // user can restore
229  $this->mCanView = true; // user can view content
230  } elseif ( $this->isAllowed( 'deletedtext' ) ) {
231  $this->mAllowed = false; // user cannot restore
232  $this->mCanView = true; // user can view content
233  $this->mRestore = false;
234  } else { // user can only view the list of revisions
235  $this->mAllowed = false;
236  $this->mCanView = false;
237  $this->mTimestamp = '';
238  $this->mRestore = false;
239  }
240 
241  if ( $this->mRestore || $this->mInvert ) {
242  $timestamps = [];
243  $this->mFileVersions = [];
244  foreach ( $request->getValues() as $key => $val ) {
245  $matches = [];
246  if ( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) {
247  $timestamps[] = $matches[1];
248  }
249 
250  if ( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) {
251  $this->mFileVersions[] = intval( $matches[1] );
252  }
253  }
254  rsort( $timestamps );
255  $this->mTargetTimestamp = $timestamps;
256  }
257  }
258 
267  protected function isAllowed( $permission, User $user = null ) {
268  $user = $user ?: $this->getUser();
269  $block = $user->getBlock();
270 
271  if ( $this->mTargetObj !== null ) {
272  return $this->permissionManager->userCan( $permission, $user, $this->mTargetObj );
273  } else {
274  $hasRight = $this->permissionManager->userHasRight( $user, $permission );
275  $sitewideBlock = $block && $block->isSitewide();
276  return $permission === 'undelete' ? ( $hasRight && !$sitewideBlock ) : $hasRight;
277  }
278  }
279 
280  public function userCanExecute( User $user ) {
281  return $this->isAllowed( $this->mRestriction, $user );
282  }
283 
287  public function checkPermissions() {
288  $user = $this->getUser();
289 
290  // First check if user has the right to use this page. If not,
291  // show a permissions error whether they are blocked or not.
292  if ( !parent::userCanExecute( $user ) ) {
293  $this->displayRestrictionError();
294  }
295 
296  // If a user has the right to use this page, but is blocked from
297  // the target, show a block error.
298  if (
299  $this->mTargetObj && $this->permissionManager->isBlockedFrom( $user, $this->mTargetObj ) ) {
300  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
301  throw new UserBlockedError( $user->getBlock() );
302  }
303 
304  // Finally, do the comprehensive permission check via isAllowed.
305  if ( !$this->userCanExecute( $user ) ) {
306  $this->displayRestrictionError();
307  }
308  }
309 
310  public function execute( $par ) {
311  $this->useTransactionalTimeLimit();
312 
313  $user = $this->getUser();
314 
315  $this->setHeaders();
316  $this->outputHeader();
317  $this->addHelpLink( 'Help:Deletion_and_undeletion' );
318 
319  $this->loadRequest( $par );
320  $this->checkPermissions(); // Needs to be after mTargetObj is set
321 
322  $out = $this->getOutput();
323 
324  if ( $this->mTargetObj === null ) {
325  $out->addWikiMsg( 'undelete-header' );
326 
327  # Not all users can just browse every deleted page from the list
328  if ( $this->permissionManager->userHasRight( $user, 'browsearchive' ) ) {
329  $this->showSearchForm();
330  }
331 
332  return;
333  }
334 
335  $this->addHelpLink( 'Help:Undelete' );
336  if ( $this->mAllowed ) {
337  $out->setPageTitleMsg( $this->msg( 'undeletepage' ) );
338  } else {
339  $out->setPageTitleMsg( $this->msg( 'viewdeletedpage' ) );
340  }
341 
342  $this->getSkin()->setRelevantTitle( $this->mTargetObj );
343 
344  if ( $this->mTimestamp !== '' ) {
345  $this->showRevision( $this->mTimestamp );
346  } elseif ( $this->mFilename !== null && $this->mTargetObj->inNamespace( NS_FILE ) ) {
347  $file = new ArchivedFile( $this->mTargetObj, 0, $this->mFilename );
348  // Check if user is allowed to see this file
349  if ( !$file->exists() ) {
350  $out->addWikiMsg( 'filedelete-nofile', $this->mFilename );
351  } elseif ( !$file->userCan( File::DELETED_FILE, $user ) ) {
352  if ( $file->isDeleted( File::DELETED_RESTRICTED ) ) {
353  throw new PermissionsError( 'suppressrevision' );
354  } else {
355  throw new PermissionsError( 'deletedtext' );
356  }
357  } elseif ( !$user->matchEditToken( $this->mToken, $this->mFilename ) ) {
358  $this->showFileConfirmationForm( $this->mFilename );
359  } else {
360  $this->showFile( $this->mFilename );
361  }
362  } elseif ( $this->mAction === 'submit' ) {
363  if ( $this->mRestore ) {
364  $this->undelete();
365  } elseif ( $this->mRevdel ) {
366  $this->redirectToRevDel();
367  }
368  } elseif ( $this->mAction === 'render' ) {
369  $this->showMoreHistory();
370  } else {
371  $this->showHistory();
372  }
373  }
374 
379  private function redirectToRevDel() {
380  $revisions = [];
381 
382  foreach ( $this->getRequest()->getValues() as $key => $val ) {
383  $matches = [];
384  if ( preg_match( "/^ts(\d{14})$/", $key, $matches ) ) {
385  $revisionRecord = $this->archivedRevisionLookup
386  ->getRevisionRecordByTimestamp( $this->mTargetObj, $matches[1] );
387  if ( $revisionRecord ) {
388  // Can return null
389  $revisions[ $revisionRecord->getId() ] = 1;
390  }
391  }
392  }
393 
394  $query = [
395  'type' => 'revision',
396  'ids' => $revisions,
397  'target' => $this->mTargetObj->getPrefixedText()
398  ];
399  $url = SpecialPage::getTitleFor( 'Revisiondelete' )->getFullURL( $query );
400  $this->getOutput()->redirect( $url );
401  }
402 
403  private function showSearchForm() {
404  $out = $this->getOutput();
405  $out->setPageTitleMsg( $this->msg( 'undelete-search-title' ) );
406  $fuzzySearch = $this->getRequest()->getVal( 'fuzzy', '1' );
407 
408  $out->enableOOUI();
409 
410  $fields = [];
411  $fields[] = new \OOUI\ActionFieldLayout(
412  new \OOUI\TextInputWidget( [
413  'name' => 'prefix',
414  'inputId' => 'prefix',
415  'infusable' => true,
416  'value' => $this->mSearchPrefix,
417  'autofocus' => true,
418  ] ),
419  new \OOUI\ButtonInputWidget( [
420  'label' => $this->msg( 'undelete-search-submit' )->text(),
421  'flags' => [ 'primary', 'progressive' ],
422  'inputId' => 'searchUndelete',
423  'type' => 'submit',
424  ] ),
425  [
426  'label' => new \OOUI\HtmlSnippet(
427  $this->msg(
428  $fuzzySearch ? 'undelete-search-full' : 'undelete-search-prefix'
429  )->parse()
430  ),
431  'align' => 'left',
432  ]
433  );
434 
435  $fieldset = new \OOUI\FieldsetLayout( [
436  'label' => $this->msg( 'undelete-search-box' )->text(),
437  'items' => $fields,
438  ] );
439 
440  $form = new \OOUI\FormLayout( [
441  'method' => 'get',
442  'action' => wfScript(),
443  ] );
444 
445  $form->appendContent(
446  $fieldset,
447  new \OOUI\HtmlSnippet(
448  Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) .
449  Html::hidden( 'fuzzy', $fuzzySearch )
450  )
451  );
452 
453  $out->addHTML(
454  new \OOUI\PanelLayout( [
455  'expanded' => false,
456  'padded' => true,
457  'framed' => true,
458  'content' => $form,
459  ] )
460  );
461 
462  # List undeletable articles
463  if ( $this->mSearchPrefix ) {
464  // For now, we enable search engine match only when specifically asked to
465  // by using fuzzy=1 parameter.
466  if ( $fuzzySearch ) {
467  $result = PageArchive::listPagesBySearch( $this->mSearchPrefix );
468  } else {
469  $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix );
470  }
471  $this->showList( $result );
472  }
473  }
474 
481  private function showList( $result ) {
482  $out = $this->getOutput();
483 
484  if ( $result->numRows() == 0 ) {
485  $out->addWikiMsg( 'undelete-no-results' );
486 
487  return false;
488  }
489 
490  $out->addWikiMsg( 'undeletepagetext', $this->getLanguage()->formatNum( $result->numRows() ) );
491 
492  $linkRenderer = $this->getLinkRenderer();
493  $undelete = $this->getPageTitle();
494  $out->addHTML( "<ul id='undeleteResultsList'>\n" );
495  foreach ( $result as $row ) {
496  $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
497  if ( $title !== null ) {
498  $item = $linkRenderer->makeKnownLink(
499  $undelete,
500  $title->getPrefixedText(),
501  [],
502  [ 'target' => $title->getPrefixedText() ]
503  );
504  } else {
505  // The title is no longer valid, show as text
506  $item = Html::element(
507  'span',
508  [ 'class' => 'mw-invalidtitle' ],
510  $this->getContext(),
511  $row->ar_namespace,
512  $row->ar_title
513  )
514  );
515  }
516  $revs = $this->msg( 'undeleterevisions' )->numParams( $row->count )->parse();
517  $out->addHTML(
519  'li',
520  [ 'class' => 'undeleteResult' ],
521  $item . $this->msg( 'word-separator' )->escaped() .
522  $this->msg( 'parentheses' )->rawParams( $revs )->escaped()
523  )
524  );
525  }
526  $result->free();
527  $out->addHTML( "</ul>\n" );
528 
529  return true;
530  }
531 
532  private function showRevision( $timestamp ) {
533  if ( !preg_match( '/[0-9]{14}/', $timestamp ) ) {
534  return;
535  }
536  $out = $this->getOutput();
537  $out->addModuleStyles( 'mediawiki.interface.helpers.styles' );
538 
539  // When viewing a specific revision, add a subtitle link back to the overall
540  // history, see T284114
541  $listLink = $this->getLinkRenderer()->makeKnownLink(
542  $this->getPageTitle(),
543  $this->msg( 'undelete-back-to-list' )->text(),
544  [],
545  [ 'target' => $this->mTargetObj->getPrefixedText() ]
546  );
547  // same < arrow as with subpages
548  $subtitle = "&lt; $listLink";
549  $out->setSubtitle( $subtitle );
550 
551  $archive = new PageArchive( $this->mTargetObj );
552  // FIXME: This hook must be deprecated, passing PageArchive by ref is awful.
553  if ( !$this->getHookRunner()->onUndeleteForm__showRevision(
554  $archive, $this->mTargetObj )
555  ) {
556  return;
557  }
558  $revRecord = $this->archivedRevisionLookup->getRevisionRecordByTimestamp( $this->mTargetObj, $timestamp );
559 
560  $user = $this->getUser();
561 
562  if ( !$revRecord ) {
563  $out->addWikiMsg( 'undeleterevision-missing' );
564  return;
565  }
566 
567  if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
568  // Used in wikilinks, should not contain whitespaces
569  $titleText = $this->mTargetObj->getPrefixedDBkey();
570  if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
571  $msg = $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
572  ? [ 'rev-suppressed-text-permission', $titleText ]
573  : [ 'rev-deleted-text-permission', $titleText ];
574  $out->addHTML(
576  $this->msg( $msg[0], $msg[1] )->parse(),
577  'plainlinks'
578  )
579  );
580  return;
581  }
582 
583  $msg = $revRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
584  ? [ 'rev-suppressed-text-view', $titleText ]
585  : [ 'rev-deleted-text-view', $titleText ];
586  $out->addHTML(
588  $this->msg( $msg[0], $msg[1] )->parse(),
589  'plainlinks'
590  )
591  );
592  // and we are allowed to see...
593  }
594 
595  if ( $this->mDiff ) {
596  $previousRevRecord = $this->archivedRevisionLookup
597  ->getPreviousRevisionRecord( $this->mTargetObj, $timestamp );
598  if ( $previousRevRecord ) {
599  $this->showDiff( $previousRevRecord, $revRecord );
600  if ( $this->mDiffOnly ) {
601  return;
602  }
603 
604  $out->addHTML( '<hr />' );
605  } else {
606  $out->addWikiMsg( 'undelete-nodiff' );
607  }
608  }
609 
610  $link = $this->getLinkRenderer()->makeKnownLink(
611  $this->getPageTitle( $this->mTargetObj->getPrefixedDBkey() ),
612  $this->mTargetObj->getPrefixedText()
613  );
614 
615  $lang = $this->getLanguage();
616 
617  // date and time are separate parameters to facilitate localisation.
618  // $time is kept for backward compat reasons.
619  $time = $lang->userTimeAndDate( $timestamp, $user );
620  $d = $lang->userDate( $timestamp, $user );
621  $t = $lang->userTime( $timestamp, $user );
622  $userLink = Linker::revUserTools( $revRecord );
623 
624  $content = $revRecord->getContent(
627  $user
628  );
629 
630  // TODO: MCR: this will have to become something like $hasTextSlots and $hasNonTextSlots
631  $isText = ( $content instanceof TextContent );
632 
633  $undeleteRevisionContent = '';
634  // Revision delete links
635  if ( !$this->mDiff ) {
636  $revdel = Linker::getRevDeleteLink(
637  $user,
638  $revRecord,
639  $this->mTargetObj
640  );
641  if ( $revdel ) {
642  $undeleteRevisionContent = $revdel . ' ';
643  }
644  }
645 
646  $undeleteRevisionContent .= $out->msg(
647  'undelete-revision',
648  Message::rawParam( $link ),
649  $time,
650  Message::rawParam( $userLink ),
651  $d,
652  $t
653  )->parseAsBlock();
654 
655  if ( $this->mPreview || $isText ) {
656  $out->addHTML(
658  $undeleteRevisionContent,
659  'mw-undelete-revision'
660  )
661  );
662  } else {
663  $out->addHTML(
665  'div',
666  [ 'class' => 'mw-undelete-revision', ],
667  $undeleteRevisionContent
668  )
669  );
670  }
671 
672  if ( $this->mPreview || !$isText ) {
673  // NOTE: non-text content has no source view, so always use rendered preview
674 
675  $popts = $out->parserOptions();
676 
677  $rendered = $this->revisionRenderer->getRenderedRevision(
678  $revRecord,
679  $popts,
680  $user,
681  [ 'audience' => RevisionRecord::FOR_THIS_USER, 'causeAction' => 'undelete-preview' ]
682  );
683 
684  // Fail hard if the audience check fails, since we already checked
685  // at the beginning of this method.
686  $pout = $rendered->getRevisionParserOutput();
687 
688  $out->addParserOutput( $pout, [
689  'enableSectionEditLinks' => false,
690  ] );
691  }
692 
693  $out->enableOOUI();
694  $buttonFields = [];
695 
696  if ( $isText ) {
697  '@phan-var TextContent $content';
698  // TODO: MCR: make this work for multiple slots
699  // source view for textual content
700  $sourceView = Xml::element( 'textarea', [
701  'readonly' => 'readonly',
702  'cols' => 80,
703  'rows' => 25
704  ], $content->getText() . "\n" );
705 
706  $buttonFields[] = new \OOUI\ButtonInputWidget( [
707  'type' => 'submit',
708  'name' => 'preview',
709  'label' => $this->msg( 'showpreview' )->text()
710  ] );
711  } else {
712  $sourceView = '';
713  }
714 
715  $buttonFields[] = new \OOUI\ButtonInputWidget( [
716  'name' => 'diff',
717  'type' => 'submit',
718  'label' => $this->msg( 'showdiff' )->text()
719  ] );
720 
721  $out->addHTML(
722  $sourceView .
723  Xml::openElement( 'div', [
724  'style' => 'clear: both' ] ) .
725  Xml::openElement( 'form', [
726  'method' => 'post',
727  'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ) ] ) .
728  Xml::element( 'input', [
729  'type' => 'hidden',
730  'name' => 'target',
731  'value' => $this->mTargetObj->getPrefixedDBkey() ] ) .
732  Xml::element( 'input', [
733  'type' => 'hidden',
734  'name' => 'timestamp',
735  'value' => $timestamp ] ) .
736  Xml::element( 'input', [
737  'type' => 'hidden',
738  'name' => 'wpEditToken',
739  'value' => $user->getEditToken() ] ) .
740  new \OOUI\FieldLayout(
741  new \OOUI\Widget( [
742  'content' => new \OOUI\HorizontalLayout( [
743  'items' => $buttonFields
744  ] )
745  ] )
746  ) .
747  Xml::closeElement( 'form' ) .
748  Xml::closeElement( 'div' )
749  );
750  }
751 
759  private function showDiff(
760  RevisionRecord $previousRevRecord,
761  RevisionRecord $currentRevRecord
762  ) {
763  $currentTitle = Title::newFromLinkTarget( $currentRevRecord->getPageAsLinkTarget() );
764 
765  $diffContext = new DerivativeContext( $this->getContext() );
766  $diffContext->setTitle( $currentTitle );
767  $diffContext->setWikiPage( $this->wikiPageFactory->newFromTitle( $currentTitle ) );
768 
769  $contentModel = $currentRevRecord->getSlot(
772  )->getModel();
773 
774  $diffEngine = $this->contentHandlerFactory->getContentHandler( $contentModel )
775  ->createDifferenceEngine( $diffContext );
776 
777  $diffEngine->setRevisions( $previousRevRecord, $currentRevRecord );
778  $diffEngine->showDiffStyle();
779  $formattedDiff = $diffEngine->getDiff(
780  $this->diffHeader( $previousRevRecord, 'o' ),
781  $this->diffHeader( $currentRevRecord, 'n' )
782  );
783 
784  $this->getOutput()->addHTML( "<div>$formattedDiff</div>\n" );
785  }
786 
792  private function diffHeader( RevisionRecord $revRecord, $prefix ) {
793  if ( $revRecord instanceof RevisionArchiveRecord ) {
794  // Revision in the archive table, only viewable via this special page
795  $targetPage = $this->getPageTitle();
796  $targetQuery = [
797  'target' => $this->mTargetObj->getPrefixedText(),
798  'timestamp' => wfTimestamp( TS_MW, $revRecord->getTimestamp() )
799  ];
800  } else {
801  // Revision in the revision table, viewable by oldid
802  $targetPage = $revRecord->getPageAsLinkTarget();
803  $targetQuery = [ 'oldid' => $revRecord->getId() ];
804  }
805 
806  // Add show/hide deletion links if available
807  $user = $this->getUser();
808  $lang = $this->getLanguage();
809  $rdel = Linker::getRevDeleteLink( $user, $revRecord, $this->mTargetObj );
810 
811  if ( $rdel ) {
812  $rdel = " $rdel";
813  }
814 
815  $minor = $revRecord->isMinor() ? ChangesList::flag( 'minor' ) : '';
816 
817  $dbr = $this->dbProvider->getReplicaDatabase();
818  $tagIds = $dbr->newSelectQueryBuilder()
819  ->select( 'ct_tag_id' )
820  ->from( 'change_tag' )
821  ->where( [ 'ct_rev_id' => $revRecord->getId() ] )
822  ->caller( __METHOD__ )->fetchFieldValues();
823  $tags = [];
824  foreach ( $tagIds as $tagId ) {
825  try {
826  $tags[] = $this->changeTagDefStore->getName( (int)$tagId );
827  } catch ( NameTableAccessException $exception ) {
828  continue;
829  }
830  }
831  $tags = implode( ',', $tags );
832  $tagSummary = ChangeTags::formatSummaryRow( $tags, 'deleteddiff', $this->getContext() );
833  $asof = $this->getLinkRenderer()->makeLink(
834  $targetPage,
835  $this->msg(
836  'revisionasof',
837  $lang->userTimeAndDate( $revRecord->getTimestamp(), $user ),
838  $lang->userDate( $revRecord->getTimestamp(), $user ),
839  $lang->userTime( $revRecord->getTimestamp(), $user )
840  )->text(),
841  [],
842  $targetQuery
843  );
844  if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
845  $asof = Html::rawElement(
846  'span',
847  [ 'class' => Linker::getRevisionDeletedClass( $revRecord ) ],
848  $asof
849  );
850  }
851 
852  // FIXME This is reimplementing DifferenceEngine#getRevisionHeader
853  // and partially #showDiffPage, but worse
854  return '<div id="mw-diff-' . $prefix . 'title1"><strong>' .
855  $asof .
856  '</strong></div>' .
857  '<div id="mw-diff-' . $prefix . 'title2">' .
858  Linker::revUserTools( $revRecord ) . '<br />' .
859  '</div>' .
860  '<div id="mw-diff-' . $prefix . 'title3">' .
861  $minor . $this->commentFormatter->formatRevision( $revRecord, $user ) . $rdel . '<br />' .
862  '</div>' .
863  '<div id="mw-diff-' . $prefix . 'title5">' .
864  $tagSummary[0] . '<br />' .
865  '</div>';
866  }
867 
872  private function showFileConfirmationForm( $key ) {
873  $out = $this->getOutput();
874  $lang = $this->getLanguage();
875  $user = $this->getUser();
876  $file = new ArchivedFile( $this->mTargetObj, 0, $this->mFilename );
877  $out->addWikiMsg( 'undelete-show-file-confirm',
878  $this->mTargetObj->getText(),
879  $lang->userDate( $file->getTimestamp(), $user ),
880  $lang->userTime( $file->getTimestamp(), $user ) );
881  $out->addHTML(
882  Xml::openElement( 'form', [
883  'method' => 'POST',
884  'action' => $this->getPageTitle()->getLocalURL( [
885  'target' => $this->mTarget,
886  'file' => $key,
887  'token' => $user->getEditToken( $key ),
888  ] ),
889  ]
890  ) .
891  Xml::submitButton( $this->msg( 'undelete-show-file-submit' )->text() ) .
892  '</form>'
893  );
894  }
895 
900  private function showFile( $key ) {
901  $this->getOutput()->disable();
902 
903  # We mustn't allow the output to be CDN cached, otherwise
904  # if an admin previews a deleted image, and it's cached, then
905  # a user without appropriate permissions can toddle off and
906  # nab the image, and CDN will serve it
907  $response = $this->getRequest()->response();
908  $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
909  $response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
910 
911  $path = $this->localRepo->getZonePath( 'deleted' ) . '/' . $this->localRepo->getDeletedHashPath( $key ) . $key;
912  $this->localRepo->streamFileWithStatus( $path );
913  }
914 
919  private function addRevisionsToBatch( LinkBatch $batch, IResultWrapper $revisions ) {
920  foreach ( $revisions as $row ) {
921  $batch->add( NS_USER, $row->ar_user_text );
922  $batch->add( NS_USER_TALK, $row->ar_user_text );
923  }
924  }
925 
930  private function addFilesToBatch( LinkBatch $batch, IResultWrapper $files ) {
931  foreach ( $files as $row ) {
932  $batch->add( NS_USER, $row->fa_user_text );
933  $batch->add( NS_USER_TALK, $row->fa_user_text );
934  }
935  }
936 
940  protected function showMoreHistory() {
941  $out = $this->getOutput();
942  $out->setArticleBodyOnly( true );
943  $dbr = $this->dbProvider->getReplicaDatabase();
944  if ( $this->mHistoryOffset ) {
945  $encOffset = $dbr->addQuotes( $dbr->timestamp( $this->mHistoryOffset ) );
946  $extraConds = [ "ar_timestamp < $encOffset" ];
947  } else {
948  $extraConds = [];
949  }
950  $revisions = $this->archivedRevisionLookup->listRevisions(
951  $this->mTargetObj,
952  $extraConds,
953  self::REVISION_HISTORY_LIMIT + 1
954  );
955  $batch = $this->linkBatchFactory->newLinkBatch();
956  $this->addRevisionsToBatch( $batch, $revisions );
957  $batch->execute();
958  $out->addHTML( $this->formatRevisionHistory( $revisions ) );
959 
960  if ( $revisions->numRows() > self::REVISION_HISTORY_LIMIT ) {
961  // Indicate to JS that the "show more" button should remain active
962  $out->setStatusCode( 206 );
963  }
964  }
965 
972  protected function formatRevisionHistory( IResultWrapper $revisions ) {
973  $history = Html::openElement( 'ul', [ 'class' => 'mw-undelete-revlist' ] );
974 
975  // Exclude the last data row if there is more data than history limit amount
976  $numRevisions = $revisions->numRows();
977  $displayCount = min( $numRevisions, self::REVISION_HISTORY_LIMIT );
978  $firstRev = $this->revisionStore->getFirstRevision( $this->mTargetObj );
979  $earliestLiveTime = $firstRev ? $firstRev->getTimestamp() : null;
980 
981  $revisions->rewind();
982  for ( $i = 0; $i < $displayCount; $i++ ) {
983  $row = $revisions->fetchObject();
984  // The $remaining parameter controls diff links and so must
985  // include the undisplayed row beyond the display limit.
986  $history .= $this->formatRevisionRow( $row, $earliestLiveTime, $numRevisions - $i );
987  }
988  $history .= Html::closeElement( 'ul' );
989  return $history;
990  }
991 
992  protected function showHistory() {
993  $this->checkReadOnly();
994 
995  $out = $this->getOutput();
996  if ( $this->mAllowed ) {
997  $out->addModules( 'mediawiki.misc-authed-ooui' );
998  }
999  $out->addModuleStyles( 'mediawiki.interface.helpers.styles' );
1000  $out->wrapWikiMsg(
1001  "<div class='mw-undelete-pagetitle'>\n$1\n</div>\n",
1002  [ 'undeletepagetitle', wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) ]
1003  );
1004 
1005  $archive = new PageArchive( $this->mTargetObj );
1006  // FIXME: This hook must be deprecated, passing PageArchive by ref is awful.
1007  $this->getHookRunner()->onUndeleteForm__showHistory( $archive, $this->mTargetObj );
1008 
1009  $out->addHTML( Html::openElement( 'div', [ 'class' => 'mw-undelete-history' ] ) );
1010  if ( $this->mAllowed ) {
1011  $out->addWikiMsg( 'undeletehistory' );
1012  $out->addWikiMsg( 'undeleterevdel' );
1013  } else {
1014  $out->addWikiMsg( 'undeletehistorynoadmin' );
1015  }
1016  $out->addHTML( Html::closeElement( 'div' ) );
1017 
1018  # List all stored revisions
1019  $revisions = $this->archivedRevisionLookup->listRevisions(
1020  $this->mTargetObj,
1021  [],
1022  self::REVISION_HISTORY_LIMIT + 1
1023  );
1024  $files = $archive->listFiles();
1025  $numRevisions = $revisions->numRows();
1026  $showLoadMore = $numRevisions > self::REVISION_HISTORY_LIMIT;
1027  $haveRevisions = $numRevisions > 0;
1028  $haveFiles = $files && $files->numRows() > 0;
1029 
1030  # Batch existence check on user and talk pages
1031  if ( $haveRevisions || $haveFiles ) {
1032  $batch = $this->linkBatchFactory->newLinkBatch();
1033  $this->addRevisionsToBatch( $batch, $revisions );
1034  if ( $haveFiles ) {
1035  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable -- $files is non-null
1036  $this->addFilesToBatch( $batch, $files );
1037  }
1038  $batch->execute();
1039  }
1040 
1041  if ( $this->mAllowed ) {
1042  $out->enableOOUI();
1043 
1044  $action = $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] );
1045  # Start the form here
1046  $form = new \OOUI\FormLayout( [
1047  'method' => 'post',
1048  'action' => $action,
1049  'id' => 'undelete',
1050  ] );
1051  }
1052 
1053  # Show relevant lines from the deletion log:
1054  $deleteLogPage = new LogPage( 'delete' );
1055  $out->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) . "\n" );
1056  LogEventsList::showLogExtract( $out, 'delete', $this->mTargetObj );
1057  # Show relevant lines from the suppression log:
1058  $suppressLogPage = new LogPage( 'suppress' );
1059  if ( $this->permissionManager->userHasRight( $this->getUser(), 'suppressionlog' ) ) {
1060  $out->addHTML( Xml::element( 'h2', null, $suppressLogPage->getName()->text() ) . "\n" );
1061  LogEventsList::showLogExtract( $out, 'suppress', $this->mTargetObj );
1062  }
1063 
1064  if ( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
1065  $fields = [];
1066  $fields[] = new \OOUI\Layout( [
1067  'content' => new \OOUI\HtmlSnippet( $this->msg( 'undeleteextrahelp' )->parseAsBlock() )
1068  ] );
1069 
1070  $fields[] = new \OOUI\FieldLayout(
1071  new \OOUI\TextInputWidget( [
1072  'name' => 'wpComment',
1073  'inputId' => 'wpComment',
1074  'infusable' => true,
1075  'value' => $this->mComment,
1076  'autofocus' => true,
1077  // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
1078  // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
1079  // Unicode codepoints.
1081  ] ),
1082  [
1083  'label' => $this->msg( 'undeletecomment' )->text(),
1084  'align' => 'top',
1085  ]
1086  );
1087 
1088  if ( $this->permissionManager->userHasRight( $this->getUser(), 'suppressrevision' ) ) {
1089  $fields[] = new \OOUI\FieldLayout(
1090  new \OOUI\CheckboxInputWidget( [
1091  'name' => 'wpUnsuppress',
1092  'inputId' => 'mw-undelete-unsuppress',
1093  'value' => '1',
1094  ] ),
1095  [
1096  'label' => $this->msg( 'revdelete-unsuppress' )->text(),
1097  'align' => 'inline',
1098  ]
1099  );
1100  }
1101 
1102  $undelPage = $this->undeletePageFactory->newUndeletePage(
1103  $this->wikiPageFactory->newFromTitle( $this->mTargetObj ),
1104  $this->getContext()->getAuthority()
1105  );
1106  if ( $undelPage->canProbablyUndeleteAssociatedTalk()->isGood() ) {
1107  $fields[] = new \OOUI\FieldLayout(
1108  new \OOUI\CheckboxInputWidget( [
1109  'name' => 'undeletetalk',
1110  'inputId' => 'mw-undelete-undeletetalk',
1111  'selected' => false,
1112  ] ),
1113  [
1114  'label' => $this->msg( 'undelete-undeletetalk' )->text(),
1115  'align' => 'inline',
1116  ]
1117  );
1118  }
1119 
1120  $fields[] = new \OOUI\FieldLayout(
1121  new \OOUI\Widget( [
1122  'content' => new \OOUI\HorizontalLayout( [
1123  'items' => [
1124  new \OOUI\ButtonInputWidget( [
1125  'name' => 'restore',
1126  'inputId' => 'mw-undelete-submit',
1127  'value' => '1',
1128  'label' => $this->msg( 'undeletebtn' )->text(),
1129  'flags' => [ 'primary', 'progressive' ],
1130  'type' => 'submit',
1131  ] ),
1132  new \OOUI\ButtonInputWidget( [
1133  'name' => 'invert',
1134  'inputId' => 'mw-undelete-invert',
1135  'value' => '1',
1136  'label' => $this->msg( 'undeleteinvert' )->text()
1137  ] ),
1138  ]
1139  ] )
1140  ] )
1141  );
1142 
1143  $fieldset = new \OOUI\FieldsetLayout( [
1144  'label' => $this->msg( 'undelete-fieldset-title' )->text(),
1145  'id' => 'mw-undelete-table',
1146  'items' => $fields,
1147  ] );
1148 
1149  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable form is set, when used here
1150  $form->appendContent(
1151  new \OOUI\PanelLayout( [
1152  'expanded' => false,
1153  'padded' => true,
1154  'framed' => true,
1155  'content' => $fieldset,
1156  ] ),
1157  new \OOUI\HtmlSnippet(
1158  Html::hidden( 'target', $this->mTarget ) .
1159  Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() )
1160  )
1161  );
1162  }
1163 
1164  $history = '';
1165  $history .= Xml::element( 'h2', null, $this->msg( 'history' )->text() ) . "\n";
1166 
1167  if ( $haveRevisions ) {
1168  # Show the page's stored (deleted) history
1169 
1170  if ( $this->permissionManager->userHasRight( $this->getUser(), 'deleterevision' ) ) {
1171  $history .= Html::element(
1172  'button',
1173  [
1174  'name' => 'revdel',
1175  'type' => 'submit',
1176  'class' => 'deleterevision-log-submit mw-log-deleterevision-button'
1177  ],
1178  $this->msg( 'showhideselectedversions' )->text()
1179  ) . "\n";
1180  }
1181 
1182  $history .= $this->formatRevisionHistory( $revisions );
1183 
1184  if ( $showLoadMore ) {
1185  $history .=
1186  Html::openElement( 'div' ) .
1187  Html::element(
1188  'span',
1189  [ 'id' => 'mw-load-more-revisions' ],
1190  $this->msg( 'undelete-load-more-revisions' )->text()
1191  ) .
1192  Html::closeElement( 'div' ) .
1193  "\n";
1194  }
1195  } else {
1196  $out->addWikiMsg( 'nohistory' );
1197  }
1198 
1199  if ( $haveFiles ) {
1200  $history .= Xml::element( 'h2', null, $this->msg( 'filehist' )->text() ) . "\n";
1201  $history .= Html::openElement( 'ul', [ 'class' => 'mw-undelete-revlist' ] );
1202  foreach ( $files as $row ) {
1203  $history .= $this->formatFileRow( $row );
1204  }
1205  $files->free();
1206  $history .= Html::closeElement( 'ul' );
1207  }
1208 
1209  if ( $this->mAllowed ) {
1210  # Slip in the hidden controls here
1211  $misc = Html::hidden( 'target', $this->mTarget );
1212  $misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
1213  $history .= $misc;
1214 
1215  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable form is set, when used here
1216  $form->appendContent( new \OOUI\HtmlSnippet( $history ) );
1217  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable form is set, when used here
1218  $out->addHTML( (string)$form );
1219  } else {
1220  $out->addHTML( $history );
1221  }
1222 
1223  return true;
1224  }
1225 
1226  protected function formatRevisionRow( $row, $earliestLiveTime, $remaining ) {
1227  $revRecord = $this->revisionStore->newRevisionFromArchiveRow(
1228  $row,
1229  RevisionStore::READ_NORMAL,
1230  $this->mTargetObj
1231  );
1232 
1233  $revTextSize = '';
1234  $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
1235  // Build checkboxen...
1236  if ( $this->mAllowed ) {
1237  if ( $this->mInvert ) {
1238  if ( in_array( $ts, $this->mTargetTimestamp ) ) {
1239  $checkBox = Xml::check( "ts$ts" );
1240  } else {
1241  $checkBox = Xml::check( "ts$ts", true );
1242  }
1243  } else {
1244  $checkBox = Xml::check( "ts$ts" );
1245  }
1246  } else {
1247  $checkBox = '';
1248  }
1249 
1250  // Build page & diff links...
1251  $user = $this->getUser();
1252  if ( $this->mCanView ) {
1253  $titleObj = $this->getPageTitle();
1254  # Last link
1255  if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1256  $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1257  $last = $this->msg( 'diff' )->escaped();
1258  } elseif ( $remaining > 0 || ( $earliestLiveTime && $ts > $earliestLiveTime ) ) {
1259  $pageLink = $this->getPageLink( $revRecord, $titleObj, $ts );
1260  $last = $this->getLinkRenderer()->makeKnownLink(
1261  $titleObj,
1262  $this->msg( 'diff' )->text(),
1263  [],
1264  [
1265  'target' => $this->mTargetObj->getPrefixedText(),
1266  'timestamp' => $ts,
1267  'diff' => 'prev'
1268  ]
1269  );
1270  } else {
1271  $pageLink = $this->getPageLink( $revRecord, $titleObj, $ts );
1272  $last = $this->msg( 'diff' )->escaped();
1273  }
1274  } else {
1275  $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1276  $last = $this->msg( 'diff' )->escaped();
1277  }
1278 
1279  // User links
1280  $userLink = Linker::revUserTools( $revRecord );
1281 
1282  // Minor edit
1283  $minor = $revRecord->isMinor() ? ChangesList::flag( 'minor' ) : '';
1284 
1285  // Revision text size
1286  $size = $row->ar_len;
1287  if ( $size !== null ) {
1288  $revTextSize = Linker::formatRevisionSize( $size );
1289  }
1290 
1291  // Edit summary
1292  $comment = $this->commentFormatter->formatRevision( $revRecord, $user );
1293 
1294  // Tags
1295  $attribs = [];
1296  [ $tagSummary, $classes ] = ChangeTags::formatSummaryRow(
1297  $row->ts_tags,
1298  'deletedhistory',
1299  $this->getContext()
1300  );
1301  if ( $classes ) {
1302  $attribs['class'] = implode( ' ', $classes );
1303  }
1304 
1305  $revisionRow = $this->msg( 'undelete-revision-row2' )
1306  ->rawParams(
1307  $checkBox,
1308  $last,
1309  $pageLink,
1310  $userLink,
1311  $minor,
1312  $revTextSize,
1313  $comment,
1314  $tagSummary
1315  )
1316  ->escaped();
1317 
1318  return Xml::tags( 'li', $attribs, $revisionRow ) . "\n";
1319  }
1320 
1321  private function formatFileRow( $row ) {
1322  $file = ArchivedFile::newFromRow( $row );
1323  $ts = wfTimestamp( TS_MW, $row->fa_timestamp );
1324  $user = $this->getUser();
1325 
1326  $checkBox = '';
1327  if ( $this->mCanView && $row->fa_storage_key ) {
1328  if ( $this->mAllowed ) {
1329  $checkBox = Xml::check( 'fileid' . $row->fa_id );
1330  }
1331  $key = urlencode( $row->fa_storage_key );
1332  $pageLink = $this->getFileLink( $file, $this->getPageTitle(), $ts, $key );
1333  } else {
1334  $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) );
1335  }
1336  $userLink = $this->getFileUser( $file );
1337  $data = $this->msg( 'widthheight' )->numParams( $row->fa_width, $row->fa_height )->text();
1338  $bytes = $this->msg( 'parentheses' )
1339  ->plaintextParams( $this->msg( 'nbytes' )->numParams( $row->fa_size )->text() )
1340  ->plain();
1341  $data = htmlspecialchars( $data . ' ' . $bytes );
1342  $comment = $this->getFileComment( $file );
1343 
1344  // Add show/hide deletion links if available
1345  $canHide = $this->isAllowed( 'deleterevision' );
1346  if ( $canHide || ( $file->getVisibility() && $this->isAllowed( 'deletedhistory' ) ) ) {
1347  if ( !$file->userCan( File::DELETED_RESTRICTED, $user ) ) {
1348  // Revision was hidden from sysops
1349  $revdlink = Linker::revDeleteLinkDisabled( $canHide );
1350  } else {
1351  $query = [
1352  'type' => 'filearchive',
1353  'target' => $this->mTargetObj->getPrefixedDBkey(),
1354  'ids' => $row->fa_id
1355  ];
1356  $revdlink = Linker::revDeleteLink( $query,
1357  $file->isDeleted( File::DELETED_RESTRICTED ), $canHide );
1358  }
1359  } else {
1360  $revdlink = '';
1361  }
1362 
1363  return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
1364  }
1365 
1374  private function getPageLink( RevisionRecord $revRecord, $titleObj, $ts ) {
1375  $user = $this->getUser();
1376  $time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1377 
1378  if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1379  // TODO The condition cannot be true when the function is called
1380  return Html::element(
1381  'span',
1382  [ 'class' => 'history-deleted' ],
1383  $time
1384  );
1385  }
1386 
1387  $link = $this->getLinkRenderer()->makeKnownLink(
1388  $titleObj,
1389  $time,
1390  [],
1391  [
1392  'target' => $this->mTargetObj->getPrefixedText(),
1393  'timestamp' => $ts
1394  ]
1395  );
1396 
1397  if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1398  $class = Linker::getRevisionDeletedClass( $revRecord );
1399  $link = '<span class="' . $class . '">' . $link . '</span>';
1400  }
1401 
1402  return $link;
1403  }
1404 
1415  private function getFileLink( $file, $titleObj, $ts, $key ) {
1416  $user = $this->getUser();
1417  $time = $this->getLanguage()->userTimeAndDate( $ts, $user );
1418 
1419  if ( !$file->userCan( File::DELETED_FILE, $user ) ) {
1420  return Html::element(
1421  'span',
1422  [ 'class' => 'history-deleted' ],
1423  $time
1424  );
1425  }
1426 
1427  if ( $file->exists() ) {
1428  $link = $this->getLinkRenderer()->makeKnownLink(
1429  $titleObj,
1430  $time,
1431  [],
1432  [
1433  'target' => $this->mTargetObj->getPrefixedText(),
1434  'file' => $key,
1435  'token' => $user->getEditToken( $key )
1436  ]
1437  );
1438  } else {
1439  $link = htmlspecialchars( $time );
1440  }
1441 
1442  if ( $file->isDeleted( File::DELETED_FILE ) ) {
1443  $link = '<span class="history-deleted">' . $link . '</span>';
1444  }
1445 
1446  return $link;
1447  }
1448 
1455  private function getFileUser( $file ) {
1456  $uploader = $file->getUploader( File::FOR_THIS_USER, $this->getAuthority() );
1457  if ( !$uploader ) {
1458  return Html::rawElement(
1459  'span',
1460  [ 'class' => 'history-deleted' ],
1461  $this->msg( 'rev-deleted-user' )->escaped()
1462  );
1463  }
1464 
1465  $link = Linker::userLink( $uploader->getId(), $uploader->getName() ) .
1466  Linker::userToolLinks( $uploader->getId(), $uploader->getName() );
1467 
1468  if ( $file->isDeleted( File::DELETED_USER ) ) {
1469  $link = Html::rawElement(
1470  'span',
1471  [ 'class' => 'history-deleted' ],
1472  $link
1473  );
1474  }
1475 
1476  return $link;
1477  }
1478 
1485  private function getFileComment( $file ) {
1486  if ( !$file->userCan( File::DELETED_COMMENT, $this->getAuthority() ) ) {
1487  return Html::rawElement(
1488  'span',
1489  [ 'class' => 'history-deleted' ],
1491  'span',
1492  [ 'class' => 'comment' ],
1493  $this->msg( 'rev-deleted-comment' )->escaped()
1494  )
1495  );
1496  }
1497 
1498  $comment = $file->getDescription( File::FOR_THIS_USER, $this->getAuthority() );
1499  $link = $this->commentFormatter->formatBlock( $comment );
1500 
1501  if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
1502  $link = Html::rawElement(
1503  'span',
1504  [ 'class' => 'history-deleted' ],
1505  $link
1506  );
1507  }
1508 
1509  return $link;
1510  }
1511 
1512  private function undelete() {
1513  if ( $this->getConfig()->get( MainConfigNames::UploadMaintenance )
1514  && $this->mTargetObj->getNamespace() === NS_FILE
1515  ) {
1516  throw new ErrorPageError( 'undelete-error', 'filedelete-maintenance' );
1517  }
1518 
1519  $this->checkReadOnly();
1520 
1521  $out = $this->getOutput();
1522  $undeletePage = $this->undeletePageFactory->newUndeletePage(
1523  $this->wikiPageFactory->newFromTitle( $this->mTargetObj ),
1524  $this->getAuthority()
1525  );
1526  if ( $this->mUndeleteTalk && $undeletePage->canProbablyUndeleteAssociatedTalk()->isGood() ) {
1527  $undeletePage->setUndeleteAssociatedTalk( true );
1528  }
1529  $status = $undeletePage
1530  ->setUndeleteOnlyTimestamps( $this->mTargetTimestamp )
1531  ->setUndeleteOnlyFileVersions( $this->mFileVersions )
1532  ->setUnsuppress( $this->mUnsuppress )
1533  // TODO This is currently duplicating some permission checks, but we do need it (T305680)
1534  ->undeleteIfAllowed( $this->mComment );
1535 
1536  if ( !$status->isGood() ) {
1537  $out->setPageTitleMsg( $this->msg( 'undelete-error' ) );
1538  $out->wrapWikiTextAsInterface(
1539  'error',
1540  Status::wrap( $status )->getWikiText(
1541  'cannotundelete',
1542  'cannotundelete',
1543  $this->getLanguage()
1544  )
1545  );
1546  return;
1547  }
1548 
1549  $restoredRevs = $status->getValue()[UndeletePage::REVISIONS_RESTORED];
1550  $restoredFiles = $status->getValue()[UndeletePage::FILES_RESTORED];
1551 
1552  if ( $restoredRevs === 0 && $restoredFiles === 0 ) {
1553  // TODO Should use a different message here
1554  $out->setPageTitleMsg( $this->msg( 'undelete-error' ) );
1555  } else {
1556  if ( $status->getValue()[UndeletePage::FILES_RESTORED] !== 0 ) {
1557  $this->getHookRunner()->onFileUndeleteComplete(
1558  $this->mTargetObj, $this->mFileVersions, $this->getUser(), $this->mComment );
1559  }
1560 
1561  $link = $this->getLinkRenderer()->makeKnownLink( $this->mTargetObj );
1562  $out->addWikiMsg( 'undeletedpage', Message::rawParam( $link ) );
1563  }
1564  }
1565 
1574  public function prefixSearchSubpages( $search, $limit, $offset ) {
1575  return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );
1576  }
1577 
1578  protected function getGroupName() {
1579  return 'pagetools';
1580  }
1581 }
1582 
1587 class_alias( SpecialUndelete::class, 'SpecialUndelete' );
const NS_USER
Definition: Defines.php:66
const NS_FILE
Definition: Defines.php:70
const NS_USER_TALK
Definition: Defines.php:67
wfScript( $script='index')
Get the URL path to a MediaWiki entry point.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
$matches
Deleted file in the 'filearchive' table.
static newFromRow( $row)
Loads a file object from the filearchive table.
static formatSummaryRow( $tags, $unused, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:147
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
An IContextSource implementation which will inherit context from another source but allow individual ...
An error page which can definitely be safely rendered using the OutputPage.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition: File.php:70
const DELETED_COMMENT
Definition: File.php:75
const DELETED_RESTRICTED
Definition: File.php:77
const DELETED_FILE
Definition: File.php:74
const DELETED_USER
Definition: File.php:76
const FOR_THIS_USER
Definition: File.php:91
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition: LinkBatch.php:44
add( $ns, $dbkey)
Definition: LinkBatch.php:170
execute()
Do the query and add the results to the LinkCache object.
Definition: LinkBatch.php:211
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition: LocalRepo.php:45
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Class to simplify the use of log pages.
Definition: LogPage.php:43
This is the main service interface for converting single-line comments from various DB comment fields...
Handle database storage of comments such as edit summaries and log reasons.
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
This class is a collection of static functions that serve two purposes:
Definition: Html.php:57
static warningBox( $html, $className='')
Return a warning box.
Definition: Html.php:815
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:288
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:239
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:897
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:352
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:264
Some internal bits split of from Skin.php.
Definition: Linker.php:65
static revDeleteLink( $query=[], $restricted=false, $delete=true)
Creates a (show/hide) link for deleting revisions/log entries.
Definition: Linker.php:2329
static userToolLinks( $userId, $userText, $redContribsWhenNoEdits=false, $flags=0, $edits=null, $useParentheses=true)
Generate standard user tool links (talk, contributions, block link, etc.)
Definition: Linker.php:1248
static getInvalidTitleDescription(IContextSource $context, $namespace, $title)
Get a message saying that an invalid title was encountered.
Definition: Linker.php:229
static getRevDeleteLink(Authority $performer, RevisionRecord $revRecord, LinkTarget $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
Definition: Linker.php:2277
static revDeleteLinkDisabled( $delete=true)
Creates a dead (show/hide) link for deleting revisions/log entries.
Definition: Linker.php:2353
static formatRevisionSize( $size)
Definition: Linker.php:1721
static userLink( $userId, $userName, $altUserName=false, $attributes=[])
Make user link (or user contributions for unregistered users)
Definition: Linker.php:1190
static getRevisionDeletedClass(RevisionRecord $revisionRecord)
Returns css class of a deleted revision.
Definition: Linker.php:1441
static revUserTools(RevisionRecord $revRecord, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition: Linker.php:1461
A class containing constants representing the names of configuration variables.
const UploadMaintenance
Name constant for the UploadMaintenance setting, for use with Config::get()
Backend logic for performing a page undelete action.
Service for creating WikiPage objects.
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
A RevisionRecord representing a revision of a deleted page persisted in the archive table.
Page revision base class.
The RevisionRenderer service provides access to rendered output for revisions.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
Parent class for all special pages.
Definition: SpecialPage.php:66
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getSkin()
Shortcut to get the skin being used for this instance.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
displayRestrictionError()
Output an error message telling the user what access level they have to have.
getUser()
Shortcut to get the User executing this instance.
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
prefixSearchString( $search, $limit, $offset, SearchEngineFactory $searchEngineFactory=null)
Perform a regular substring search for prefixSearchSubpages.
getPageTitle( $subpage=false)
Get a self-referential title object.
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
getConfig()
Shortcut to get main config object.
getContext()
Gets the context this SpecialPage is executed in.
getRequest()
Get the WebRequest being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
getAuthority()
Shortcut to get the Authority executing this instance.
getLanguage()
Shortcut to get user's language.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Special page allowing users with the appropriate permissions to view and restore deleted content.
__construct(PermissionManager $permissionManager, RevisionStore $revisionStore, RevisionRenderer $revisionRenderer, IContentHandlerFactory $contentHandlerFactory, NameTableStore $changeTagDefStore, LinkBatchFactory $linkBatchFactory, RepoGroup $repoGroup, IConnectionProvider $dbProvider, UserOptionsLookup $userOptionsLookup, WikiPageFactory $wikiPageFactory, SearchEngineFactory $searchEngineFactory, UndeletePageFactory $undeletePageFactory, ArchivedRevisionLookup $archivedRevisionLookup, CommentFormatter $commentFormatter)
doesWrites()
Indicates whether this special page may perform database writes.
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.Stability: stableto override 1....
formatRevisionRow( $row, $earliestLiveTime, $remaining)
isAllowed( $permission, User $user=null)
Checks whether a user is allowed the permission for the specific title if one is set.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
formatRevisionHistory(IResultWrapper $revisions)
Generate the.
execute( $par)
Default execute method Checks user permissions.
userCanExecute(User $user)
Checks if the given user (identified by an object) can execute this special page (as defined by $mRes...
showMoreHistory()
Handle XHR "show more history" requests (T249977)
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:58
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:76
Exception representing a failure to look up a row from a name table.
Represents a title within MediaWiki.
Definition: Title.php:76
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:400
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:650
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:290
Provides access to user options.
internal since 1.36
Definition: User.php:98
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition: Message.php:144
static rawParam( $raw)
Definition: Message.php:1143
Used to show archived pages and eventually restore them.
Definition: PageArchive.php:35
static listPagesBySearch( $term)
List deleted pages recorded in the archive matching the given term, using search engine archive.
Definition: PageArchive.php:61
static listPagesByPrefix( $prefix)
List deleted pages recorded in the archive table matching the given title prefix.
Show an error when a user tries to do something they do not have the necessary permissions for.
Prioritized list of file repositories.
Definition: RepoGroup.php:30
getLocalRepo()
Get the local repository, i.e.
Definition: RepoGroup.php:343
Factory class for SearchEngine.
Content object implementation for representing flat text.
Definition: TextContent.php:41
Show an error when the user tries to do something whilst blocked.
Module of static functions for generating XML.
Definition: Xml.php:33
static closeElement( $element)
Shortcut to close an XML element.
Definition: Xml.php:124
static check( $name, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox.
Definition: Xml.php:336
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:115
static submitButton( $value, $attribs=[])
Convenience function to build an HTML submit button When $wgUseMediaWikiUIEverywhere is true it will ...
Definition: Xml.php:473
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:141
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:50
Service for page undelete actions.
Provide primary and replica IDatabase connections.
Result wrapper for grabbing data queried from an IDatabase object.
fetchObject()
Fetch the next row from the given result object, in object form.
numRows()
Get the number of rows in a result object.
$content
Definition: router.php:76
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42