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