MediaWiki  master
InfoAction.php
Go to the documentation of this file.
1 <?php
28 
34 class InfoAction extends FormlessAction {
35  const VERSION = 1;
36 
42  public function getName() {
43  return 'info';
44  }
45 
51  public function requiresUnblock() {
52  return false;
53  }
54 
60  public function requiresWrite() {
61  return false;
62  }
63 
71  public static function invalidateCache( Title $title, $revid = null ) {
72  if ( !$revid ) {
73  $revision = Revision::newFromTitle( $title, 0, Revision::READ_LATEST );
74  $revid = $revision ? $revision->getId() : null;
75  }
76  if ( $revid !== null ) {
77  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
78  $key = self::getCacheKey( $cache, $title, $revid );
79  $cache->delete( $key );
80  }
81  }
82 
88  public function onView() {
89  $content = '';
90 
91  // Validate revision
92  $oldid = $this->page->getOldID();
93  if ( $oldid ) {
94  $revision = $this->page->getRevisionFetched();
95 
96  // Revision is missing
97  if ( $revision === null ) {
98  return $this->msg( 'missing-revision', $oldid )->parse();
99  }
100 
101  // Revision is not current
102  if ( !$revision->isCurrent() ) {
103  return $this->msg( 'pageinfo-not-current' )->plain();
104  }
105  }
106 
107  // "Help" button
108  $this->addHelpLink( 'Page information' );
109 
110  // Page header
111  if ( !$this->msg( 'pageinfo-header' )->isDisabled() ) {
112  $content .= $this->msg( 'pageinfo-header' )->parse();
113  }
114 
115  // Hide "This page is a member of # hidden categories" explanation
116  $content .= Html::element( 'style', [],
117  '.mw-hiddenCategoriesExplanation { display: none; }' ) . "\n";
118 
119  // Hide "Templates used on this page" explanation
120  $content .= Html::element( 'style', [],
121  '.mw-templatesUsedExplanation { display: none; }' ) . "\n";
122 
123  // Get page information
124  $pageInfo = $this->pageInfo();
125 
126  // Allow extensions to add additional information
127  Hooks::run( 'InfoAction', [ $this->getContext(), &$pageInfo ] );
128 
129  // Render page information
130  foreach ( $pageInfo as $header => $infoTable ) {
131  // Messages:
132  // pageinfo-header-basic, pageinfo-header-edits, pageinfo-header-restrictions,
133  // pageinfo-header-properties, pageinfo-category-info
134  $content .= $this->makeHeader(
135  $this->msg( "pageinfo-${header}" )->text(),
136  "mw-pageinfo-${header}"
137  ) . "\n";
138  $table = "\n";
139  $below = "";
140  foreach ( $infoTable as $infoRow ) {
141  if ( $infoRow[0] == "below" ) {
142  $below = $infoRow[1] . "\n";
143  continue;
144  }
145  $name = ( $infoRow[0] instanceof Message ) ? $infoRow[0]->escaped() : $infoRow[0];
146  $value = ( $infoRow[1] instanceof Message ) ? $infoRow[1]->escaped() : $infoRow[1];
147  $id = ( $infoRow[0] instanceof Message ) ? $infoRow[0]->getKey() : null;
148  $table = $this->addRow( $table, $name, $value, $id ) . "\n";
149  }
150  $content = $this->addTable( $content, $table ) . "\n" . $below;
151  }
152 
153  // Page footer
154  if ( !$this->msg( 'pageinfo-footer' )->isDisabled() ) {
155  $content .= $this->msg( 'pageinfo-footer' )->parse();
156  }
157 
158  return $content;
159  }
160 
168  protected function makeHeader( $header, $canonicalId ) {
169  $spanAttribs = [ 'class' => 'mw-headline', 'id' => Sanitizer::escapeIdForAttribute( $header ) ];
170  $h2Attribs = [ 'id' => Sanitizer::escapeIdForAttribute( $canonicalId ) ];
171 
172  return Html::rawElement( 'h2', $h2Attribs, Html::element( 'span', $spanAttribs, $header ) );
173  }
174 
184  protected function addRow( $table, $name, $value, $id ) {
185  return $table .
187  'tr',
188  $id === null ? [] : [ 'id' => 'mw-' . $id ],
189  Html::rawElement( 'td', [ 'style' => 'vertical-align: top;' ], $name ) .
190  Html::rawElement( 'td', [], $value )
191  );
192  }
193 
201  protected function addTable( $content, $table ) {
202  return $content . Html::rawElement( 'table', [ 'class' => 'wikitable mw-page-info' ],
203  $table );
204  }
205 
218  protected function pageInfo() {
219  $services = MediaWikiServices::getInstance();
220 
221  $user = $this->getUser();
222  $lang = $this->getLanguage();
223  $title = $this->getTitle();
224  $id = $title->getArticleID();
225  $config = $this->context->getConfig();
226  $linkRenderer = $services->getLinkRenderer();
227 
228  $pageCounts = $this->pageCounts( $this->page );
229 
230  $props = PageProps::getInstance()->getAllProperties( $title );
231  $pageProperties = $props[$id] ?? [];
232 
233  // Basic information
234  $pageInfo = [];
235  $pageInfo['header-basic'] = [];
236 
237  // Display title
238  $displayTitle = $pageProperties['displaytitle'] ?? $title->getPrefixedText();
239 
240  $pageInfo['header-basic'][] = [
241  $this->msg( 'pageinfo-display-title' ), $displayTitle
242  ];
243 
244  // Is it a redirect? If so, where to?
245  $redirectTarget = $this->page->getRedirectTarget();
246  if ( $redirectTarget !== null ) {
247  $pageInfo['header-basic'][] = [
248  $this->msg( 'pageinfo-redirectsto' ),
249  $linkRenderer->makeLink( $redirectTarget ) .
250  $this->msg( 'word-separator' )->escaped() .
251  $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
252  $redirectTarget,
253  $this->msg( 'pageinfo-redirectsto-info' )->text(),
254  [],
255  [ 'action' => 'info' ]
256  ) )->escaped()
257  ];
258  }
259 
260  // Default sort key
261  $sortKey = $pageProperties['defaultsort'] ?? $title->getCategorySortkey();
262 
263  $sortKey = htmlspecialchars( $sortKey );
264  $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-default-sort' ), $sortKey ];
265 
266  // Page length (in bytes)
267  $pageInfo['header-basic'][] = [
268  $this->msg( 'pageinfo-length' ), $lang->formatNum( $title->getLength() )
269  ];
270 
271  // Page namespace
272  $pageNamespace = $title->getNsText();
273  if ( $pageNamespace ) {
274  $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-namespace' ), $pageNamespace ];
275  }
276 
277  // Page ID (number not localised, as it's a database ID)
278  $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-article-id' ), $id ];
279 
280  // Language in which the page content is (supposed to be) written
281  $pageLang = $title->getPageLanguage()->getCode();
282 
283  $pageLangHtml = $pageLang . ' - ' .
284  Language::fetchLanguageName( $pageLang, $lang->getCode() );
285  // Link to Special:PageLanguage with pre-filled page title if user has permissions
286  $permissionManager = $services->getPermissionManager();
287  if ( $config->get( 'PageLanguageUseDB' )
288  && $permissionManager->userCan( 'pagelang', $user, $title )
289  ) {
290  $pageLangHtml .= ' ' . $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
291  SpecialPage::getTitleValueFor( 'PageLanguage', $title->getPrefixedText() ),
292  $this->msg( 'pageinfo-language-change' )->text()
293  ) )->escaped();
294  }
295 
296  $pageInfo['header-basic'][] = [
297  $this->msg( 'pageinfo-language' )->escaped(),
298  $pageLangHtml
299  ];
300 
301  // Content model of the page
302  $modelHtml = htmlspecialchars( ContentHandler::getLocalizedName( $title->getContentModel() ) );
303  // If the user can change it, add a link to Special:ChangeContentModel
304  if ( $config->get( 'ContentHandlerUseDB' )
305  && $permissionManager->userCan( 'editcontentmodel', $user, $title )
306  ) {
307  $modelHtml .= ' ' . $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
308  SpecialPage::getTitleValueFor( 'ChangeContentModel', $title->getPrefixedText() ),
309  $this->msg( 'pageinfo-content-model-change' )->text()
310  ) )->escaped();
311  }
312 
313  $pageInfo['header-basic'][] = [
314  $this->msg( 'pageinfo-content-model' ),
315  $modelHtml
316  ];
317 
318  if ( $title->inNamespace( NS_USER ) ) {
319  $pageUser = User::newFromName( $title->getRootText() );
320  if ( $pageUser && $pageUser->getId() && !$pageUser->isHidden() ) {
321  $pageInfo['header-basic'][] = [
322  $this->msg( 'pageinfo-user-id' ),
323  $pageUser->getId()
324  ];
325  }
326  }
327 
328  // Search engine status
329  $pOutput = new ParserOutput();
330  if ( isset( $pageProperties['noindex'] ) ) {
331  $pOutput->setIndexPolicy( 'noindex' );
332  }
333  if ( isset( $pageProperties['index'] ) ) {
334  $pOutput->setIndexPolicy( 'index' );
335  }
336 
337  // Use robot policy logic
338  $policy = $this->page->getRobotPolicy( 'view', $pOutput );
339  $pageInfo['header-basic'][] = [
340  // Messages: pageinfo-robot-index, pageinfo-robot-noindex
341  $this->msg( 'pageinfo-robot-policy' ),
342  $this->msg( "pageinfo-robot-${policy['index']}" )
343  ];
344 
345  $unwatchedPageThreshold = $config->get( 'UnwatchedPageThreshold' );
346  if ( $permissionManager->userHasRight( $user, 'unwatchedpages' ) ||
347  ( $unwatchedPageThreshold !== false &&
348  $pageCounts['watchers'] >= $unwatchedPageThreshold )
349  ) {
350  // Number of page watchers
351  $pageInfo['header-basic'][] = [
352  $this->msg( 'pageinfo-watchers' ),
353  $lang->formatNum( $pageCounts['watchers'] )
354  ];
355  if (
356  $config->get( 'ShowUpdatedMarker' ) &&
357  isset( $pageCounts['visitingWatchers'] )
358  ) {
359  $minToDisclose = $config->get( 'UnwatchedPageSecret' );
360  if ( $pageCounts['visitingWatchers'] > $minToDisclose ||
361  $permissionManager->userHasRight( $user, 'unwatchedpages' ) ) {
362  $pageInfo['header-basic'][] = [
363  $this->msg( 'pageinfo-visiting-watchers' ),
364  $lang->formatNum( $pageCounts['visitingWatchers'] )
365  ];
366  } else {
367  $pageInfo['header-basic'][] = [
368  $this->msg( 'pageinfo-visiting-watchers' ),
369  $this->msg( 'pageinfo-few-visiting-watchers' )
370  ];
371  }
372  }
373  } elseif ( $unwatchedPageThreshold !== false ) {
374  $pageInfo['header-basic'][] = [
375  $this->msg( 'pageinfo-watchers' ),
376  $this->msg( 'pageinfo-few-watchers' )->numParams( $unwatchedPageThreshold )
377  ];
378  }
379 
380  // Redirects to this page
381  $whatLinksHere = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
382  $pageInfo['header-basic'][] = [
383  $linkRenderer->makeLink(
384  $whatLinksHere,
385  $this->msg( 'pageinfo-redirects-name' )->text(),
386  [],
387  [
388  'hidelinks' => 1,
389  'hidetrans' => 1,
390  'hideimages' => $title->getNamespace() == NS_FILE
391  ]
392  ),
393  $this->msg( 'pageinfo-redirects-value' )
394  ->numParams( count( $title->getRedirectsHere() ) )
395  ];
396 
397  // Is it counted as a content page?
398  if ( $this->page->isCountable() ) {
399  $pageInfo['header-basic'][] = [
400  $this->msg( 'pageinfo-contentpage' ),
401  $this->msg( 'pageinfo-contentpage-yes' )
402  ];
403  }
404 
405  // Subpages of this page, if subpages are enabled for the current NS
406  if ( $services->getNamespaceInfo()->hasSubpages( $title->getNamespace() ) ) {
407  $prefixIndex = SpecialPage::getTitleFor(
408  'Prefixindex', $title->getPrefixedText() . '/' );
409  $pageInfo['header-basic'][] = [
410  $linkRenderer->makeLink(
411  $prefixIndex,
412  $this->msg( 'pageinfo-subpages-name' )->text()
413  ),
414  $this->msg( 'pageinfo-subpages-value' )
415  ->numParams(
416  $pageCounts['subpages']['total'],
417  $pageCounts['subpages']['redirects'],
418  $pageCounts['subpages']['nonredirects'] )
419  ];
420  }
421 
422  if ( $title->inNamespace( NS_CATEGORY ) ) {
423  $category = Category::newFromTitle( $title );
424 
425  // $allCount is the total number of cat members,
426  // not the count of how many members are normal pages.
427  $allCount = (int)$category->getPageCount();
428  $subcatCount = (int)$category->getSubcatCount();
429  $fileCount = (int)$category->getFileCount();
430  $pagesCount = $allCount - $subcatCount - $fileCount;
431 
432  $pageInfo['category-info'] = [
433  [
434  $this->msg( 'pageinfo-category-total' ),
435  $lang->formatNum( $allCount )
436  ],
437  [
438  $this->msg( 'pageinfo-category-pages' ),
439  $lang->formatNum( $pagesCount )
440  ],
441  [
442  $this->msg( 'pageinfo-category-subcats' ),
443  $lang->formatNum( $subcatCount )
444  ],
445  [
446  $this->msg( 'pageinfo-category-files' ),
447  $lang->formatNum( $fileCount )
448  ]
449  ];
450  }
451 
452  // Display image SHA-1 value
453  if ( $title->inNamespace( NS_FILE ) ) {
454  $fileObj = $services->getRepoGroup()->findFile( $title );
455  if ( $fileObj !== false ) {
456  // Convert the base-36 sha1 value obtained from database to base-16
457  $output = Wikimedia\base_convert( $fileObj->getSha1(), 36, 16, 40 );
458  $pageInfo['header-basic'][] = [
459  $this->msg( 'pageinfo-file-hash' ),
460  $output
461  ];
462  }
463  }
464 
465  // Page protection
466  $pageInfo['header-restrictions'] = [];
467 
468  // Is this page affected by the cascading protection of something which includes it?
469  if ( $title->isCascadeProtected() ) {
470  $cascadingFrom = '';
471  $sources = $title->getCascadeProtectionSources()[0];
472 
473  foreach ( $sources as $sourceTitle ) {
474  $cascadingFrom .= Html::rawElement(
475  'li', [], $linkRenderer->makeKnownLink( $sourceTitle ) );
476  }
477 
478  $cascadingFrom = Html::rawElement( 'ul', [], $cascadingFrom );
479  $pageInfo['header-restrictions'][] = [
480  $this->msg( 'pageinfo-protect-cascading-from' ),
481  $cascadingFrom
482  ];
483  }
484 
485  // Is out protection set to cascade to other pages?
486  if ( $title->areRestrictionsCascading() ) {
487  $pageInfo['header-restrictions'][] = [
488  $this->msg( 'pageinfo-protect-cascading' ),
489  $this->msg( 'pageinfo-protect-cascading-yes' )
490  ];
491  }
492 
493  // Page protection
494  foreach ( $title->getRestrictionTypes() as $restrictionType ) {
495  $protectionLevel = implode( ', ', $title->getRestrictions( $restrictionType ) );
496 
497  if ( $protectionLevel == '' ) {
498  // Allow all users
499  $message = $this->msg( 'protect-default' )->escaped();
500  } else {
501  // Administrators only
502  // Messages: protect-level-autoconfirmed, protect-level-sysop
503  $message = $this->msg( "protect-level-$protectionLevel" );
504  if ( $message->isDisabled() ) {
505  // Require "$1" permission
506  $message = $this->msg( "protect-fallback", $protectionLevel )->parse();
507  } else {
508  $message = $message->escaped();
509  }
510  }
511  $expiry = $title->getRestrictionExpiry( $restrictionType );
512  $formattedexpiry = $this->msg( 'parentheses',
513  $lang->formatExpiry( $expiry ) )->escaped();
514  $message .= $this->msg( 'word-separator' )->escaped() . $formattedexpiry;
515 
516  // Messages: restriction-edit, restriction-move, restriction-create,
517  // restriction-upload
518  $pageInfo['header-restrictions'][] = [
519  $this->msg( "restriction-$restrictionType" ), $message
520  ];
521  }
522  $protectLog = SpecialPage::getTitleFor( 'Log' );
523  $pageInfo['header-restrictions'][] = [
524  'below',
525  $linkRenderer->makeKnownLink(
526  $protectLog,
527  $this->msg( 'pageinfo-view-protect-log' )->text(),
528  [],
529  [ 'type' => 'protect', 'page' => $title->getPrefixedText() ]
530  ),
531  ];
532 
533  if ( !$this->page->exists() ) {
534  return $pageInfo;
535  }
536 
537  // Edit history
538  $pageInfo['header-edits'] = [];
539 
540  $firstRev = $this->page->getOldestRevision();
541  $lastRev = $this->page->getRevision();
542  $batch = new LinkBatch;
543 
544  if ( $firstRev ) {
545  $firstRevUser = $firstRev->getUserText( RevisionRecord::FOR_THIS_USER );
546  if ( $firstRevUser !== '' ) {
547  $firstRevUserTitle = Title::makeTitle( NS_USER, $firstRevUser );
548  $batch->addObj( $firstRevUserTitle );
549  $batch->addObj( $firstRevUserTitle->getTalkPage() );
550  }
551  }
552 
553  if ( $lastRev ) {
554  $lastRevUser = $lastRev->getUserText( RevisionRecord::FOR_THIS_USER );
555  if ( $lastRevUser !== '' ) {
556  $lastRevUserTitle = Title::makeTitle( NS_USER, $lastRevUser );
557  $batch->addObj( $lastRevUserTitle );
558  $batch->addObj( $lastRevUserTitle->getTalkPage() );
559  }
560  }
561 
562  $batch->execute();
563 
564  if ( $firstRev ) {
565  // Page creator
566  $pageInfo['header-edits'][] = [
567  $this->msg( 'pageinfo-firstuser' ),
568  Linker::revUserTools( $firstRev )
569  ];
570 
571  // Date of page creation
572  $pageInfo['header-edits'][] = [
573  $this->msg( 'pageinfo-firsttime' ),
574  $linkRenderer->makeKnownLink(
575  $title,
576  $lang->userTimeAndDate( $firstRev->getTimestamp(), $user ),
577  [],
578  [ 'oldid' => $firstRev->getId() ]
579  )
580  ];
581  }
582 
583  if ( $lastRev ) {
584  // Latest editor
585  $pageInfo['header-edits'][] = [
586  $this->msg( 'pageinfo-lastuser' ),
587  Linker::revUserTools( $lastRev )
588  ];
589 
590  // Date of latest edit
591  $pageInfo['header-edits'][] = [
592  $this->msg( 'pageinfo-lasttime' ),
593  $linkRenderer->makeKnownLink(
594  $title,
595  $lang->userTimeAndDate( $this->page->getTimestamp(), $user ),
596  [],
597  [ 'oldid' => $this->page->getLatest() ]
598  )
599  ];
600  }
601 
602  // Total number of edits
603  $pageInfo['header-edits'][] = [
604  $this->msg( 'pageinfo-edits' ), $lang->formatNum( $pageCounts['edits'] )
605  ];
606 
607  // Total number of distinct authors
608  if ( $pageCounts['authors'] > 0 ) {
609  $pageInfo['header-edits'][] = [
610  $this->msg( 'pageinfo-authors' ), $lang->formatNum( $pageCounts['authors'] )
611  ];
612  }
613 
614  // Recent number of edits (within past 30 days)
615  $pageInfo['header-edits'][] = [
616  $this->msg( 'pageinfo-recent-edits',
617  $lang->formatDuration( $config->get( 'RCMaxAge' ) ) ),
618  $lang->formatNum( $pageCounts['recent_edits'] )
619  ];
620 
621  // Recent number of distinct authors
622  $pageInfo['header-edits'][] = [
623  $this->msg( 'pageinfo-recent-authors' ),
624  $lang->formatNum( $pageCounts['recent_authors'] )
625  ];
626 
627  // Array of MagicWord objects
628  $magicWords = $services->getMagicWordFactory()->getDoubleUnderscoreArray();
629 
630  // Array of magic word IDs
631  $wordIDs = $magicWords->names;
632 
633  // Array of IDs => localized magic words
634  $localizedWords = $services->getContentLanguage()->getMagicWords();
635 
636  $listItems = [];
637  foreach ( $pageProperties as $property => $value ) {
638  if ( in_array( $property, $wordIDs ) ) {
639  $listItems[] = Html::element( 'li', [], $localizedWords[$property][1] );
640  }
641  }
642 
643  $localizedList = Html::rawElement( 'ul', [], implode( '', $listItems ) );
644  $hiddenCategories = $this->page->getHiddenCategories();
645 
646  if (
647  count( $listItems ) > 0 ||
648  count( $hiddenCategories ) > 0 ||
649  $pageCounts['transclusion']['from'] > 0 ||
650  $pageCounts['transclusion']['to'] > 0
651  ) {
652  $options = [ 'LIMIT' => $config->get( 'PageInfoTransclusionLimit' ) ];
653  $transcludedTemplates = $title->getTemplateLinksFrom( $options );
654  if ( $config->get( 'MiserMode' ) ) {
655  $transcludedTargets = [];
656  } else {
657  $transcludedTargets = $title->getTemplateLinksTo( $options );
658  }
659 
660  // Page properties
661  $pageInfo['header-properties'] = [];
662 
663  // Magic words
664  if ( count( $listItems ) > 0 ) {
665  $pageInfo['header-properties'][] = [
666  $this->msg( 'pageinfo-magic-words' )->numParams( count( $listItems ) ),
667  $localizedList
668  ];
669  }
670 
671  // Hidden categories
672  if ( count( $hiddenCategories ) > 0 ) {
673  $pageInfo['header-properties'][] = [
674  $this->msg( 'pageinfo-hidden-categories' )
675  ->numParams( count( $hiddenCategories ) ),
676  Linker::formatHiddenCategories( $hiddenCategories )
677  ];
678  }
679 
680  // Transcluded templates
681  if ( $pageCounts['transclusion']['from'] > 0 ) {
682  if ( $pageCounts['transclusion']['from'] > count( $transcludedTemplates ) ) {
683  $more = $this->msg( 'morenotlisted' )->escaped();
684  } else {
685  $more = null;
686  }
687 
688  $templateListFormatter = new TemplatesOnThisPageFormatter(
689  $this->getContext(),
690  $linkRenderer
691  );
692 
693  $pageInfo['header-properties'][] = [
694  $this->msg( 'pageinfo-templates' )
695  ->numParams( $pageCounts['transclusion']['from'] ),
696  $templateListFormatter->format( $transcludedTemplates, false, $more )
697  ];
698  }
699 
700  if ( !$config->get( 'MiserMode' ) && $pageCounts['transclusion']['to'] > 0 ) {
701  if ( $pageCounts['transclusion']['to'] > count( $transcludedTargets ) ) {
702  $more = $linkRenderer->makeLink(
703  $whatLinksHere,
704  $this->msg( 'moredotdotdot' )->text(),
705  [],
706  [ 'hidelinks' => 1, 'hideredirs' => 1 ]
707  );
708  } else {
709  $more = null;
710  }
711 
712  $templateListFormatter = new TemplatesOnThisPageFormatter(
713  $this->getContext(),
714  $linkRenderer
715  );
716 
717  $pageInfo['header-properties'][] = [
718  $this->msg( 'pageinfo-transclusions' )
719  ->numParams( $pageCounts['transclusion']['to'] ),
720  $templateListFormatter->format( $transcludedTargets, false, $more )
721  ];
722  }
723  }
724 
725  return $pageInfo;
726  }
727 
734  protected function pageCounts( Page $page ) {
735  $fname = __METHOD__;
736  $config = $this->context->getConfig();
737  $services = MediaWikiServices::getInstance();
738  $cache = $services->getMainWANObjectCache();
739 
740  return $cache->getWithSetCallback(
741  self::getCacheKey( $cache, $page->getTitle(), $page->getLatest() ),
743  function ( $oldValue, &$ttl, &$setOpts ) use ( $page, $config, $fname, $services ) {
744  $title = $page->getTitle();
745  $id = $title->getArticleID();
746 
747  $dbr = wfGetDB( DB_REPLICA );
748  $dbrWatchlist = wfGetDB( DB_REPLICA, 'watchlist' );
749  $setOpts += Database::getCacheSetOptions( $dbr, $dbrWatchlist );
750 
751  $tables = [ 'revision_actor_temp' ];
752  $field = 'revactor_actor';
753  $pageField = 'revactor_page';
754  $tsField = 'revactor_timestamp';
755  $joins = [];
756 
757  $watchedItemStore = $services->getWatchedItemStore();
758 
759  $result = [];
760  $result['watchers'] = $watchedItemStore->countWatchers( $title );
761 
762  if ( $config->get( 'ShowUpdatedMarker' ) ) {
763  $updated = wfTimestamp( TS_UNIX, $page->getTimestamp() );
764  $result['visitingWatchers'] = $watchedItemStore->countVisitingWatchers(
765  $title,
766  $updated - $config->get( 'WatchersMaxAge' )
767  );
768  }
769 
770  // Total number of edits
771  $edits = (int)$dbr->selectField(
772  'revision',
773  'COUNT(*)',
774  [ 'rev_page' => $id ],
775  $fname
776  );
777  $result['edits'] = $edits;
778 
779  // Total number of distinct authors
780  if ( $config->get( 'MiserMode' ) ) {
781  $result['authors'] = 0;
782  } else {
783  $result['authors'] = (int)$dbr->selectField(
784  $tables,
785  "COUNT(DISTINCT $field)",
786  [ $pageField => $id ],
787  $fname,
788  [],
789  $joins
790  );
791  }
792 
793  // "Recent" threshold defined by RCMaxAge setting
794  $threshold = $dbr->timestamp( time() - $config->get( 'RCMaxAge' ) );
795 
796  // Recent number of edits
797  $edits = (int)$dbr->selectField(
798  'revision',
799  'COUNT(rev_page)',
800  [
801  'rev_page' => $id,
802  "rev_timestamp >= " . $dbr->addQuotes( $threshold )
803  ],
804  $fname
805  );
806  $result['recent_edits'] = $edits;
807 
808  // Recent number of distinct authors
809  $result['recent_authors'] = (int)$dbr->selectField(
810  $tables,
811  "COUNT(DISTINCT $field)",
812  [
813  $pageField => $id,
814  "$tsField >= " . $dbr->addQuotes( $threshold )
815  ],
816  $fname,
817  [],
818  $joins
819  );
820 
821  // Subpages (if enabled)
822  if ( $services->getNamespaceInfo()->hasSubpages( $title->getNamespace() ) ) {
823  $conds = [ 'page_namespace' => $title->getNamespace() ];
824  $conds[] = 'page_title ' .
825  $dbr->buildLike( $title->getDBkey() . '/', $dbr->anyString() );
826 
827  // Subpages of this page (redirects)
828  $conds['page_is_redirect'] = 1;
829  $result['subpages']['redirects'] = (int)$dbr->selectField(
830  'page',
831  'COUNT(page_id)',
832  $conds,
833  $fname
834  );
835 
836  // Subpages of this page (non-redirects)
837  $conds['page_is_redirect'] = 0;
838  $result['subpages']['nonredirects'] = (int)$dbr->selectField(
839  'page',
840  'COUNT(page_id)',
841  $conds,
842  $fname
843  );
844 
845  // Subpages of this page (total)
846  $result['subpages']['total'] = $result['subpages']['redirects']
847  + $result['subpages']['nonredirects'];
848  }
849 
850  // Counts for the number of transclusion links (to/from)
851  if ( $config->get( 'MiserMode' ) ) {
852  $result['transclusion']['to'] = 0;
853  } else {
854  $result['transclusion']['to'] = (int)$dbr->selectField(
855  'templatelinks',
856  'COUNT(tl_from)',
857  [
858  'tl_namespace' => $title->getNamespace(),
859  'tl_title' => $title->getDBkey()
860  ],
861  $fname
862  );
863  }
864 
865  $result['transclusion']['from'] = (int)$dbr->selectField(
866  'templatelinks',
867  'COUNT(*)',
868  [ 'tl_from' => $title->getArticleID() ],
869  $fname
870  );
871 
872  return $result;
873  }
874  );
875  }
876 
882  protected function getPageTitle() {
883  return $this->msg( 'pageinfo-title', $this->getTitle()->getPrefixedText() )->text();
884  }
885 
890  protected function getContributors() {
891  $contributors = $this->page->getContributors();
892  $real_names = [];
893  $user_names = [];
894  $anon_ips = [];
895  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
896 
897  # Sift for real versus user names
898 
899  foreach ( $contributors as $user ) {
900  $page = $user->isAnon()
901  ? SpecialPage::getTitleFor( 'Contributions', $user->getName() )
902  : $user->getUserPage();
903 
904  $hiddenPrefs = $this->context->getConfig()->get( 'HiddenPrefs' );
905  if ( $user->getId() == 0 ) {
906  $anon_ips[] = $linkRenderer->makeLink( $page, $user->getName() );
907  } elseif ( !in_array( 'realname', $hiddenPrefs ) && $user->getRealName() ) {
908  $real_names[] = $linkRenderer->makeLink( $page, $user->getRealName() );
909  } else {
910  $user_names[] = $linkRenderer->makeLink( $page, $user->getName() );
911  }
912  }
913 
914  $lang = $this->getLanguage();
915 
916  $real = $lang->listToText( $real_names );
917 
918  # "ThisSite user(s) A, B and C"
919  if ( count( $user_names ) ) {
920  $user = $this->msg( 'siteusers' )
921  ->rawParams( $lang->listToText( $user_names ) )
922  ->params( count( $user_names ) )->escaped();
923  } else {
924  $user = false;
925  }
926 
927  if ( count( $anon_ips ) ) {
928  $anon = $this->msg( 'anonusers' )
929  ->rawParams( $lang->listToText( $anon_ips ) )
930  ->params( count( $anon_ips ) )->escaped();
931  } else {
932  $anon = false;
933  }
934 
935  # This is the big list, all mooshed together. We sift for blank strings
936  $fulllist = [];
937  foreach ( [ $real, $user, $anon ] as $s ) {
938  if ( $s !== '' ) {
939  array_push( $fulllist, $s );
940  }
941  }
942 
943  $count = count( $fulllist );
944 
945  # "Based on work by ..."
946  return $count
947  ? $this->msg( 'othercontribs' )->rawParams(
948  $lang->listToText( $fulllist ) )->params( $count )->escaped()
949  : '';
950  }
951 
957  protected function getDescription() {
958  return '';
959  }
960 
967  protected static function getCacheKey( WANObjectCache $cache, Title $title, $revId ) {
968  return $cache->makeKey( 'infoaction', md5( $title->getPrefixedText() ), $revId, self::VERSION );
969  }
970 }
static getTitleValueFor( $name, $subpage=false, $fragment='')
Get a localised TitleValue object for a specified special page name.
Definition: SpecialPage.php:98
static getCacheKey(WANObjectCache $cache, Title $title, $revId)
Definition: InfoAction.php:967
addTable( $content, $table)
Adds a table to the content that will be added to the output.
Definition: InfoAction.php:201
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:231
const VERSION
Definition: InfoAction.php:35
addRow( $table, $name, $value, $id)
Adds a row to a table that will be added to the content.
Definition: InfoAction.php:184
static newFromTitle( $title)
Factory function.
Definition: Category.php:146
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition: Action.php:400
getTitle()
Shortcut to get the Title object from the page.
Definition: Action.php:247
Handles formatting for the "templates used on this page" lists.
The Message class provides methods which fulfil two basic services:
Definition: Message.php:162
if(!isset( $args[0])) $lang
pageInfo()
Returns an array of info groups (will be rendered as tables), keyed by group ID.
Definition: InfoAction.php:218
getUser()
Shortcut to get the User being used for this instance.
Definition: Action.php:218
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
onView()
Shows page information on GET request.
Definition: InfoAction.php:88
requiresUnblock()
Whether this action can still be executed by a blocked user.
Definition: InfoAction.php:51
getPrefixedText()
Get the prefixed title with spaces.
Definition: Title.php:1853
getLanguage()
Shortcut to get the user Language being used for this instance.
Definition: Action.php:237
$contributors
static getLocalizedName( $name, Language $lang=null)
Returns the localized name for a given content model.
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that&#39;s attached to a given link target...
Definition: Revision.php:139
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition: Linker.php:1987
pageCounts(Page $page)
Returns page counts that would be too "expensive" to retrieve by normal means.
Definition: InfoAction.php:734
$magicWords
Definition: MessagesAb.php:71
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition: LinkBatch.php:34
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: Action.php:259
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
makeKey( $class,... $components)
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition: Page.php:29
static invalidateCache(Title $title, $revid=null)
Clear the info cache for a given Title.
Definition: InfoAction.php:71
static escapeIdForAttribute( $id, $mode=self::ID_PRIMARY)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid HTM...
Definition: Sanitizer.php:1257
$cache
Definition: mcc.php:33
const NS_CATEGORY
Definition: Defines.php:74
getContext()
Get the IContextSource in use here.
Definition: Action.php:179
const NS_FILE
Definition: Defines.php:66
Displays information about a page.
Definition: InfoAction.php:34
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don&#39;t need a full Title object...
Definition: SpecialPage.php:83
$header
WikiPage Article ImagePage CategoryPage Page $page
Page on which we&#39;re performing the action.
Definition: Action.php:46
requiresWrite()
Whether this action requires the wiki not to be locked.
Definition: InfoAction.php:60
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:584
An action which just does something, without showing a form first.
static fetchLanguageName( $code, $inLanguage=self::AS_AUTONYMS, $include=self::ALL)
Definition: Language.php:868
static getInstance()
Definition: PageProps.php:66
getDescription()
Returns the description that goes below the "<h1>" tag.
Definition: InfoAction.php:957
makeHeader( $header, $canonicalId)
Creates a header that can be added to the output.
Definition: InfoAction.php:168
getName()
Returns the name of the action this object responds to.
Definition: InfoAction.php:42
getPageTitle()
Returns the name that goes in the "<h1>" page title.
Definition: InfoAction.php:882
const DB_REPLICA
Definition: defines.php:25
$content
Definition: router.php:78
getContributors()
Get a list of contributors of $article.
Definition: InfoAction.php:890
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:536
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
static revUserTools( $rev, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition: Linker.php:1124