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