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