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  // Validate revision
94  $oldid = $this->getArticle()->getOldID();
95  if ( $oldid ) {
96  $revRecord = $this->getArticle()->fetchRevisionRecord();
97 
98  // Revision is missing
99  if ( $revRecord === null ) {
100  return $this->msg( 'missing-revision', $oldid )->parse();
101  }
102 
103  // Revision is not current
104  if ( !$revRecord->isCurrent() ) {
105  return $this->msg( 'pageinfo-not-current' )->plain();
106  }
107  }
108 
109  // "Help" button
110  $this->addHelpLink( 'Page information' );
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  $content = $this->addTable( $content, $table ) . "\n" . $below;
153  }
154 
155  // Page footer
156  if ( !$this->msg( 'pageinfo-footer' )->isDisabled() ) {
157  $content .= $this->msg( 'pageinfo-footer' )->parse();
158  }
159 
160  return $content;
161  }
162 
170  protected function makeHeader( $header, $canonicalId ) {
171  $spanAttribs = [ 'class' => 'mw-headline', 'id' => Sanitizer::escapeIdForAttribute( $header ) ];
172  $h2Attribs = [ 'id' => Sanitizer::escapeIdForAttribute( $canonicalId ) ];
173 
174  return Html::rawElement( 'h2', $h2Attribs, Html::element( 'span', $spanAttribs, $header ) );
175  }
176 
186  protected function addRow( $table, $name, $value, $id ) {
187  return $table .
189  'tr',
190  $id === null ? [] : [ 'id' => 'mw-' . $id ],
191  Html::rawElement( 'td', [ 'style' => 'vertical-align: top;' ], $name ) .
192  Html::rawElement( 'td', [], $value )
193  );
194  }
195 
203  protected function addTable( $content, $table ) {
204  return $content . Html::rawElement( 'table', [ 'class' => 'wikitable mw-page-info' ],
205  $table );
206  }
207 
220  protected function pageInfo() {
221  $services = MediaWikiServices::getInstance();
222 
223  $user = $this->getUser();
224  $lang = $this->getLanguage();
225  $title = $this->getTitle();
226  $id = $title->getArticleID();
227  $config = $this->context->getConfig();
228  $linkRenderer = $services->getLinkRenderer();
229 
230  $pageCounts = $this->pageCounts();
231 
232  $props = PageProps::getInstance()->getAllProperties( $title );
233  $pageProperties = $props[$id] ?? [];
234 
235  // Basic information
236  $pageInfo = [];
237  $pageInfo['header-basic'] = [];
238 
239  // Display title
240  $displayTitle = $pageProperties['displaytitle'] ?? $title->getPrefixedText();
241 
242  $pageInfo['header-basic'][] = [
243  $this->msg( 'pageinfo-display-title' ), $displayTitle
244  ];
245 
246  // Is it a redirect? If so, where to?
247  $redirectTarget = $this->getWikiPage()->getRedirectTarget();
248  if ( $redirectTarget !== null ) {
249  $pageInfo['header-basic'][] = [
250  $this->msg( 'pageinfo-redirectsto' ),
251  $linkRenderer->makeLink( $redirectTarget ) .
252  $this->msg( 'word-separator' )->escaped() .
253  $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
254  $redirectTarget,
255  $this->msg( 'pageinfo-redirectsto-info' )->text(),
256  [],
257  [ 'action' => 'info' ]
258  ) )->escaped()
259  ];
260  }
261 
262  // Default sort key
263  $sortKey = $pageProperties['defaultsort'] ?? $title->getCategorySortkey();
264 
265  $sortKey = htmlspecialchars( $sortKey );
266  $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-default-sort' ), $sortKey ];
267 
268  // Page length (in bytes)
269  $pageInfo['header-basic'][] = [
270  $this->msg( 'pageinfo-length' ), $lang->formatNum( $title->getLength() )
271  ];
272 
273  // Page namespace
274  $pageNamespace = $title->getNsText();
275  if ( $pageNamespace ) {
276  $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-namespace' ), $pageNamespace ];
277  }
278 
279  // Page ID (number not localised, as it's a database ID)
280  $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-article-id' ), $id ];
281 
282  // Language in which the page content is (supposed to be) written
283  $pageLang = $title->getPageLanguage()->getCode();
284 
285  $pageLangHtml = $pageLang . ' - ' .
286  $services->getLanguageNameUtils()->getLanguageName( $pageLang, $lang->getCode() );
287  // Link to Special:PageLanguage with pre-filled page title if user has permissions
288  $permissionManager = $services->getPermissionManager();
289  if ( $config->get( 'PageLanguageUseDB' )
290  && $permissionManager->userCan( 'pagelang', $user, $title )
291  ) {
292  $pageLangHtml .= ' ' . $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
293  SpecialPage::getTitleValueFor( 'PageLanguage', $title->getPrefixedText() ),
294  $this->msg( 'pageinfo-language-change' )->text()
295  ) )->escaped();
296  }
297 
298  $pageInfo['header-basic'][] = [
299  $this->msg( 'pageinfo-language' )->escaped(),
300  $pageLangHtml
301  ];
302 
303  // Content model of the page
304  $modelHtml = htmlspecialchars( ContentHandler::getLocalizedName( $title->getContentModel() ) );
305  // If the user can change it, add a link to Special:ChangeContentModel
306  if ( $permissionManager->userCan( 'editcontentmodel', $user, $title ) ) {
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->getArticle()->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->getWikiPage()->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->getWikiPage()->exists() ) {
534  return $pageInfo;
535  }
536 
537  // Edit history
538  $pageInfo['header-edits'] = [];
539 
540  $firstRev = MediaWikiServices::getInstance()
541  ->getRevisionLookup()
542  ->getFirstRevision( $this->getTitle() );
543  $lastRev = $this->getWikiPage()->getRevisionRecord();
544  $linkBatchFactory = $services->getLinkBatchFactory();
545  $batch = $linkBatchFactory->newLinkBatch();
546  if ( $firstRev ) {
547  $firstRevUser = $firstRev->getUser( RevisionRecord::FOR_THIS_USER, $user );
548  if ( $firstRevUser ) {
549  $batch->add( NS_USER, $firstRevUser->getName() );
550  $batch->add( NS_USER_TALK, $firstRevUser->getName() );
551  }
552  }
553 
554  if ( $lastRev ) {
555  $lastRevUser = $lastRev->getUser( RevisionRecord::FOR_THIS_USER, $user );
556  if ( $lastRevUser ) {
557  $batch->add( NS_USER, $lastRevUser->getName() );
558  $batch->add( NS_USER_TALK, $lastRevUser->getName() );
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->getWikiPage()->getTimestamp(), $user ),
596  [],
597  [ 'oldid' => $this->getWikiPage()->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->getWikiPage()->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 
733  private function pageCounts() {
734  $page = $this->getWikiPage();
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() ),
742  WANObjectCache::TTL_WEEK,
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 = (int)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 
891  protected function getDescription() {
892  return '';
893  }
894 
901  protected static function getCacheKey( WANObjectCache $cache, Title $title, $revId ) {
902  return $cache->makeKey( 'infoaction', md5( $title->getPrefixedText() ), $revId, self::VERSION );
903  }
904 }
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:50
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
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:165
$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:815
InfoAction\getDescription
getDescription()
Returns the description that goes below the "<h1>" tag.
Definition: InfoAction.php:891
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1815
InfoAction\requiresWrite
requiresWrite()
Whether this action requires the wiki not to be locked.
Definition: InfoAction.php:60
Action\exists
static exists(string $name)
Check if a given action is recognised, even if it's disabled.
Definition: Action.php:206
NS_FILE
const NS_FILE
Definition: Defines.php:75
User\newFromName
static newFromName( $name, $validate='valid')
Definition: User.php:545
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:106
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:2075
$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:170
InfoAction\getPageTitle
getPageTitle()
Returns the name that goes in the "<h1>" page title.
Definition: InfoAction.php:882
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:121
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2448
PageProps\getInstance
static getInstance()
Definition: PageProps.php:82
WikiPage\getTitle
getTitle()
Get the title object of the article.
Definition: WikiPage.php:285
$title
$title
Definition: testCompression.php:38
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
InfoAction\VERSION
const VERSION
Definition: InfoAction.php:35
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:83
WikiPage\getLatest
getLatest()
Get the page_latest field.
Definition: WikiPage.php:668
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:1142
InfoAction\addRow
addRow( $table, $name, $value, $id)
Adds a row to a table that will be added to the content.
Definition: InfoAction.php:186
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:733
InfoAction\getCacheKey
static getCacheKey(WANObjectCache $cache, Title $title, $revId)
Definition: InfoAction.php:901
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
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:72
InfoAction\pageInfo
pageInfo()
Returns an array of info groups (will be rendered as tables), keyed by group ID.
Definition: InfoAction.php:220
TemplatesOnThisPageFormatter
Handles formatting for the "templates used on this page" lists.
Definition: TemplatesOnThisPageFormatter.php:31
InfoAction\addTable
addTable( $content, $table)
Adds a table to the content that will be added to the output.
Definition: InfoAction.php:203
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:41
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
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:41
$magicWords
$magicWords
Definition: MessagesAb.php:72
$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_USER
const NS_USER
Definition: Defines.php:71
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
WikiPage\getTimestamp
getTimestamp()
Definition: WikiPage.php:786