MediaWiki  master
InfoAction.php
Go to the documentation of this file.
1 <?php
36 
42 class InfoAction extends FormlessAction {
43  private const VERSION = 1;
44 
47 
49  private $hookRunner;
50 
53 
56 
58  private $linkRenderer;
59 
61  private $loadBalancer;
62 
65 
67  private $namespaceInfo;
68 
70  private $pageProps;
71 
73  private $repoGroup;
74 
76  private $revisionLookup;
77 
79  private $wanObjectCache;
80 
83 
101  public function __construct(
102  Page $page,
105  HookContainer $hookContainer,
117  ) {
118  parent::__construct( $page, $context );
119  $this->contentLanguage = $contentLanguage;
120  $this->hookRunner = new HookRunner( $hookContainer );
121  $this->languageNameUtils = $languageNameUtils;
122  $this->linkBatchFactory = $linkBatchFactory;
123  $this->linkRenderer = $linkRenderer;
124  $this->loadBalancer = $loadBalancer;
125  $this->magicWordFactory = $magicWordFactory;
126  $this->namespaceInfo = $namespaceInfo;
127  $this->pageProps = $pageProps;
128  $this->repoGroup = $repoGroup;
129  $this->revisionLookup = $revisionLookup;
130  $this->wanObjectCache = $wanObjectCache;
131  $this->watchedItemStore = $watchedItemStore;
132  }
133 
139  public function getName() {
140  return 'info';
141  }
142 
148  public function requiresUnblock() {
149  return false;
150  }
151 
157  public function requiresWrite() {
158  return false;
159  }
160 
168  public static function invalidateCache( PageIdentity $page, $revid = null ) {
169  $services = MediaWikiServices::getInstance();
170  if ( !$revid ) {
171  $revision = $services->getRevisionLookup()
172  ->getRevisionByTitle( $page, 0, IDBAccessObject::READ_LATEST );
173  $revid = $revision ? $revision->getId() : null;
174  }
175  if ( $revid !== null ) {
176  $cache = $services->getMainWANObjectCache();
177  $key = self::getCacheKey( $cache, $page, $revid );
178  $cache->delete( $key );
179  }
180  }
181 
187  public function onView() {
188  $this->getOutput()->addModuleStyles( 'mediawiki.interface.helpers.styles' );
189 
190  // "Help" button
191  $this->addHelpLink( 'Page information' );
192 
193  // Validate revision
194  $oldid = $this->getArticle()->getOldID();
195  if ( $oldid ) {
196  $revRecord = $this->getArticle()->fetchRevisionRecord();
197 
198  // Revision is missing
199  if ( $revRecord === null ) {
200  return $this->msg( 'missing-revision', $oldid )->parse();
201  }
202 
203  // Revision is not current
204  if ( !$revRecord->isCurrent() ) {
205  return $this->msg( 'pageinfo-not-current' )->plain();
206  }
207  }
208 
209  $content = '';
210 
211  // Page header
212  if ( !$this->msg( 'pageinfo-header' )->isDisabled() ) {
213  $content .= $this->msg( 'pageinfo-header' )->parse();
214  }
215 
216  // TODO we shouldn't be adding styles manually like thes
217  // Hide "This page is a member of # hidden categories" explanation
219  'style',
220  [],
221  '.mw-hiddenCategoriesExplanation { display: none; }'
222  ) . "\n";
223 
224  // Hide "Templates used on this page" explanation
226  'style',
227  [],
228  '.mw-templatesUsedExplanation { display: none; }'
229  ) . "\n";
230 
231  // Get page information
232  $pageInfo = $this->pageInfo();
233 
234  // Allow extensions to add additional information
235  $this->hookRunner->onInfoAction( $this->getContext(), $pageInfo );
236 
237  // Render page information
238  foreach ( $pageInfo as $header => $infoTable ) {
239  // Messages:
240  // pageinfo-header-basic, pageinfo-header-edits, pageinfo-header-restrictions,
241  // pageinfo-header-properties, pageinfo-category-info
242  $content .= $this->makeHeader(
243  $this->msg( "pageinfo-$header" )->text(),
244  "mw-pageinfo-$header"
245  ) . "\n";
246  $table = "\n";
247  $below = "";
248  foreach ( $infoTable as $infoRow ) {
249  if ( $infoRow[0] == "below" ) {
250  $below = $infoRow[1] . "\n";
251  continue;
252  }
253  $name = ( $infoRow[0] instanceof Message ) ? $infoRow[0]->escaped() : $infoRow[0];
254  $value = ( $infoRow[1] instanceof Message ) ? $infoRow[1]->escaped() : $infoRow[1];
255  $id = ( $infoRow[0] instanceof Message ) ? $infoRow[0]->getKey() : null;
256  $table = $this->addRow( $table, $name, $value, $id ) . "\n";
257  }
258  if ( $table === "\n" ) {
259  // Don't add tables with no rows
260  $content .= "\n" . $below;
261  } else {
262  $content = $this->addTable( $content, $table ) . "\n" . $below;
263  }
264  }
265 
266  // Page footer
267  if ( !$this->msg( 'pageinfo-footer' )->isDisabled() ) {
268  $content .= $this->msg( 'pageinfo-footer' )->parse();
269  }
270 
271  return $content;
272  }
273 
281  protected function makeHeader( $header, $canonicalId ) {
282  return Html::rawElement(
283  'h2',
284  [ 'id' => Sanitizer::escapeIdForAttribute( $canonicalId ) ],
286  'span',
287  [
288  'class' => 'mw-headline',
290  ],
291  $header
292  )
293  );
294  }
295 
305  protected function addRow( $table, $name, $value, $id ) {
306  return $table .
308  'tr',
309  $id === null ? [] : [ 'id' => 'mw-' . $id ],
310  Html::rawElement( 'td', [ 'style' => 'vertical-align: top;' ], $name ) .
311  Html::rawElement( 'td', [], $value )
312  );
313  }
314 
322  protected function addTable( $content, $table ) {
323  return $content .
325  'table',
326  [ 'class' => 'wikitable mw-page-info' ],
327  $table
328  );
329  }
330 
343  protected function pageInfo() {
344  $user = $this->getUser();
345  $lang = $this->getLanguage();
346  $title = $this->getTitle();
347  $id = $title->getArticleID();
348  $config = $this->context->getConfig();
350 
351  $pageCounts = $this->pageCounts();
352 
353  $props = $this->pageProps->getAllProperties( $title );
354  $pageProperties = $props[$id] ?? [];
355 
356  // Basic information
357  $pageInfo = [];
358  $pageInfo['header-basic'] = [];
359 
360  // Display title
361  $displayTitle = $pageProperties['displaytitle'] ?? $title->getPrefixedText();
362 
363  $pageInfo['header-basic'][] = [
364  $this->msg( 'pageinfo-display-title' ),
365  $displayTitle
366  ];
367 
368  // Is it a redirect? If so, where to?
369  $redirectTarget = $this->getWikiPage()->getRedirectTarget();
370  if ( $redirectTarget !== null ) {
371  $pageInfo['header-basic'][] = [
372  $this->msg( 'pageinfo-redirectsto' ),
373  $linkRenderer->makeLink( $redirectTarget ) .
374  $this->msg( 'word-separator' )->escaped() .
375  $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
376  $redirectTarget,
377  $this->msg( 'pageinfo-redirectsto-info' )->text(),
378  [],
379  [ 'action' => 'info' ]
380  ) )->escaped()
381  ];
382  }
383 
384  // Default sort key
385  $sortKey = $pageProperties['defaultsort'] ?? $title->getCategorySortkey();
386 
387  $sortKey = htmlspecialchars( $sortKey );
388  $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-default-sort' ), $sortKey ];
389 
390  // Page length (in bytes)
391  $pageInfo['header-basic'][] = [
392  $this->msg( 'pageinfo-length' ),
393  $lang->formatNum( $title->getLength() )
394  ];
395 
396  // Page namespace
397  $pageNamespace = $title->getNsText();
398  if ( $pageNamespace ) {
399  $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-namespace' ), $pageNamespace ];
400  }
401 
402  // Page ID (number not localised, as it's a database ID)
403  $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-article-id' ), $id ];
404 
405  // Language in which the page content is (supposed to be) written
406  $pageLang = $title->getPageLanguage()->getCode();
407 
408  $pageLangHtml = $pageLang . ' - ' .
409  $this->languageNameUtils->getLanguageName( $pageLang, $lang->getCode() );
410  // Link to Special:PageLanguage with pre-filled page title if user has permissions
411  if ( $config->get( 'PageLanguageUseDB' )
412  && $this->getContext()->getAuthority()->probablyCan( 'pagelang', $title )
413  ) {
414  $pageLangHtml .= ' ' . $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
415  SpecialPage::getTitleValueFor( 'PageLanguage', $title->getPrefixedText() ),
416  $this->msg( 'pageinfo-language-change' )->text()
417  ) )->escaped();
418  }
419 
420  $pageInfo['header-basic'][] = [
421  $this->msg( 'pageinfo-language' )->escaped(),
422  $pageLangHtml
423  ];
424 
425  // Content model of the page
426  $modelHtml = htmlspecialchars( ContentHandler::getLocalizedName( $title->getContentModel() ) );
427  // If the user can change it, add a link to Special:ChangeContentModel
428  if ( $this->getContext()->getAuthority()->probablyCan( 'editcontentmodel', $title ) ) {
429  $modelHtml .= ' ' . $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
430  SpecialPage::getTitleValueFor( 'ChangeContentModel', $title->getPrefixedText() ),
431  $this->msg( 'pageinfo-content-model-change' )->text()
432  ) )->escaped();
433  }
434 
435  $pageInfo['header-basic'][] = [
436  $this->msg( 'pageinfo-content-model' ),
437  $modelHtml
438  ];
439 
440  if ( $title->inNamespace( NS_USER ) ) {
441  $pageUser = User::newFromName( $title->getRootText() );
442  if ( $pageUser && $pageUser->getId() && !$pageUser->isHidden() ) {
443  $pageInfo['header-basic'][] = [
444  $this->msg( 'pageinfo-user-id' ),
445  $pageUser->getId()
446  ];
447  }
448  }
449 
450  // Search engine status
451  $pOutput = new ParserOutput();
452  if ( isset( $pageProperties['noindex'] ) ) {
453  $pOutput->setIndexPolicy( 'noindex' );
454  }
455  if ( isset( $pageProperties['index'] ) ) {
456  $pOutput->setIndexPolicy( 'index' );
457  }
458 
459  // Use robot policy logic
460  $policy = $this->getArticle()->getRobotPolicy( 'view', $pOutput );
461  $pageInfo['header-basic'][] = [
462  // Messages: pageinfo-robot-index, pageinfo-robot-noindex
463  $this->msg( 'pageinfo-robot-policy' ),
464  $this->msg( "pageinfo-robot-${policy['index']}" )
465  ];
466 
467  $unwatchedPageThreshold = $config->get( 'UnwatchedPageThreshold' );
468  if ( $this->getContext()->getAuthority()->isAllowed( 'unwatchedpages' ) ||
469  ( $unwatchedPageThreshold !== false &&
470  $pageCounts['watchers'] >= $unwatchedPageThreshold )
471  ) {
472  // Number of page watchers
473  $pageInfo['header-basic'][] = [
474  $this->msg( 'pageinfo-watchers' ),
475  $lang->formatNum( $pageCounts['watchers'] )
476  ];
477  if (
478  $config->get( 'ShowUpdatedMarker' ) &&
479  isset( $pageCounts['visitingWatchers'] )
480  ) {
481  $minToDisclose = $config->get( 'UnwatchedPageSecret' );
482  if ( $pageCounts['visitingWatchers'] > $minToDisclose ||
483  $this->getContext()->getAuthority()->isAllowed( 'unwatchedpages' ) ) {
484  $pageInfo['header-basic'][] = [
485  $this->msg( 'pageinfo-visiting-watchers' ),
486  $lang->formatNum( $pageCounts['visitingWatchers'] )
487  ];
488  } else {
489  $pageInfo['header-basic'][] = [
490  $this->msg( 'pageinfo-visiting-watchers' ),
491  $this->msg( 'pageinfo-few-visiting-watchers' )
492  ];
493  }
494  }
495  } elseif ( $unwatchedPageThreshold !== false ) {
496  $pageInfo['header-basic'][] = [
497  $this->msg( 'pageinfo-watchers' ),
498  $this->msg( 'pageinfo-few-watchers' )->numParams( $unwatchedPageThreshold )
499  ];
500  }
501 
502  // Redirects to this page
503  $whatLinksHere = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
504  $pageInfo['header-basic'][] = [
506  $whatLinksHere,
507  $this->msg( 'pageinfo-redirects-name' )->text(),
508  [],
509  [
510  'hidelinks' => 1,
511  'hidetrans' => 1,
512  'hideimages' => $title->getNamespace() === NS_FILE
513  ]
514  ),
515  $this->msg( 'pageinfo-redirects-value' )
516  ->numParams( count( $title->getRedirectsHere() ) )
517  ];
518 
519  // Is it counted as a content page?
520  if ( $this->getWikiPage()->isCountable() ) {
521  $pageInfo['header-basic'][] = [
522  $this->msg( 'pageinfo-contentpage' ),
523  $this->msg( 'pageinfo-contentpage-yes' )
524  ];
525  }
526 
527  // Subpages of this page, if subpages are enabled for the current NS
528  if ( $this->namespaceInfo->hasSubpages( $title->getNamespace() ) ) {
529  $prefixIndex = SpecialPage::getTitleFor(
530  'Prefixindex',
531  $title->getPrefixedText() . '/'
532  );
533  $pageInfo['header-basic'][] = [
535  $prefixIndex,
536  $this->msg( 'pageinfo-subpages-name' )->text()
537  ),
538  $this->msg( 'pageinfo-subpages-value' )
539  ->numParams(
540  $pageCounts['subpages']['total'],
541  $pageCounts['subpages']['redirects'],
542  $pageCounts['subpages']['nonredirects']
543  )
544  ];
545  }
546 
547  if ( $title->inNamespace( NS_CATEGORY ) ) {
548  $category = Category::newFromTitle( $title );
549 
550  // $allCount is the total number of cat members,
551  // not the count of how many members are normal pages.
552  $allCount = (int)$category->getPageCount();
553  $subcatCount = (int)$category->getSubcatCount();
554  $fileCount = (int)$category->getFileCount();
555  $pagesCount = $allCount - $subcatCount - $fileCount;
556 
557  $pageInfo['category-info'] = [
558  [
559  $this->msg( 'pageinfo-category-total' ),
560  $lang->formatNum( $allCount )
561  ],
562  [
563  $this->msg( 'pageinfo-category-pages' ),
564  $lang->formatNum( $pagesCount )
565  ],
566  [
567  $this->msg( 'pageinfo-category-subcats' ),
568  $lang->formatNum( $subcatCount )
569  ],
570  [
571  $this->msg( 'pageinfo-category-files' ),
572  $lang->formatNum( $fileCount )
573  ]
574  ];
575  }
576 
577  // Display image SHA-1 value
578  if ( $title->inNamespace( NS_FILE ) ) {
579  $fileObj = $this->repoGroup->findFile( $title );
580  if ( $fileObj !== false ) {
581  // Convert the base-36 sha1 value obtained from database to base-16
582  $output = Wikimedia\base_convert( $fileObj->getSha1(), 36, 16, 40 );
583  $pageInfo['header-basic'][] = [
584  $this->msg( 'pageinfo-file-hash' ),
585  $output
586  ];
587  }
588  }
589 
590  // Page protection
591  $pageInfo['header-restrictions'] = [];
592 
593  // Is this page affected by the cascading protection of something which includes it?
594  if ( $title->isCascadeProtected() ) {
595  $cascadingFrom = '';
596  $sources = $title->getCascadeProtectionSources()[0];
597 
598  foreach ( $sources as $sourceTitle ) {
599  $cascadingFrom .= Html::rawElement(
600  'li',
601  [],
602  $linkRenderer->makeKnownLink( $sourceTitle )
603  );
604  }
605 
606  $cascadingFrom = Html::rawElement( 'ul', [], $cascadingFrom );
607  $pageInfo['header-restrictions'][] = [
608  $this->msg( 'pageinfo-protect-cascading-from' ),
609  $cascadingFrom
610  ];
611  }
612 
613  // Is out protection set to cascade to other pages?
614  if ( $title->areRestrictionsCascading() ) {
615  $pageInfo['header-restrictions'][] = [
616  $this->msg( 'pageinfo-protect-cascading' ),
617  $this->msg( 'pageinfo-protect-cascading-yes' )
618  ];
619  }
620 
621  // Page protection
622  foreach ( $title->getRestrictionTypes() as $restrictionType ) {
623  $protections = $title->getRestrictions( $restrictionType );
624 
625  switch ( count( $protections ) ) {
626  case 0:
627  $message = $this->getNamespaceProtectionMessage( $title );
628  if ( $message === null ) {
629  // Allow all users
630  $message = $this->msg( 'protect-default' )->escaped();
631  }
632  break;
633 
634  case 1:
635  // Messages: protect-level-autoconfirmed, protect-level-sysop
636  $message = $this->msg( 'protect-level-' . $protections[0] );
637  if ( !$message->isDisabled() ) {
638  $message = $message->escaped();
639  break;
640  }
641  // Intentional fall-through if message is disabled (or non-existent)
642 
643  default:
644  // Require "$1" permission
645  $message = $this->msg( "protect-fallback", $lang->commaList( $protections ) )->parse();
646  break;
647  }
648  $expiry = $title->getRestrictionExpiry( $restrictionType );
649  $formattedexpiry = $this->msg(
650  'parentheses',
651  $lang->formatExpiry( $expiry, true, 'infinity', $user )
652  )->escaped();
653  $message .= $this->msg( 'word-separator' )->escaped() . $formattedexpiry;
654 
655  // Messages: restriction-edit, restriction-move, restriction-create,
656  // restriction-upload
657  $pageInfo['header-restrictions'][] = [
658  $this->msg( "restriction-$restrictionType" ), $message
659  ];
660  }
661  $protectLog = SpecialPage::getTitleFor( 'Log' );
662  $pageInfo['header-restrictions'][] = [
663  'below',
665  $protectLog,
666  $this->msg( 'pageinfo-view-protect-log' )->text(),
667  [],
668  [ 'type' => 'protect', 'page' => $title->getPrefixedText() ]
669  ),
670  ];
671 
672  if ( !$this->getWikiPage()->exists() ) {
673  return $pageInfo;
674  }
675 
676  // Edit history
677  $pageInfo['header-edits'] = [];
678 
679  $firstRev = $this->revisionLookup->getFirstRevision( $this->getTitle() );
680  $lastRev = $this->getWikiPage()->getRevisionRecord();
681  $batch = $this->linkBatchFactory->newLinkBatch();
682  if ( $firstRev ) {
683  $firstRevUser = $firstRev->getUser( RevisionRecord::FOR_THIS_USER, $user );
684  if ( $firstRevUser ) {
685  $batch->add( NS_USER, $firstRevUser->getName() );
686  $batch->add( NS_USER_TALK, $firstRevUser->getName() );
687  }
688  }
689 
690  if ( $lastRev ) {
691  $lastRevUser = $lastRev->getUser( RevisionRecord::FOR_THIS_USER, $user );
692  if ( $lastRevUser ) {
693  $batch->add( NS_USER, $lastRevUser->getName() );
694  $batch->add( NS_USER_TALK, $lastRevUser->getName() );
695  }
696  }
697 
698  $batch->execute();
699 
700  if ( $firstRev ) {
701  // Page creator
702  $pageInfo['header-edits'][] = [
703  $this->msg( 'pageinfo-firstuser' ),
704  Linker::revUserTools( $firstRev )
705  ];
706 
707  // Date of page creation
708  $pageInfo['header-edits'][] = [
709  $this->msg( 'pageinfo-firsttime' ),
711  $title,
712  $lang->userTimeAndDate( $firstRev->getTimestamp(), $user ),
713  [],
714  [ 'oldid' => $firstRev->getId() ]
715  )
716  ];
717  }
718 
719  if ( $lastRev ) {
720  // Latest editor
721  $pageInfo['header-edits'][] = [
722  $this->msg( 'pageinfo-lastuser' ),
723  Linker::revUserTools( $lastRev )
724  ];
725 
726  // Date of latest edit
727  $pageInfo['header-edits'][] = [
728  $this->msg( 'pageinfo-lasttime' ),
730  $title,
731  $lang->userTimeAndDate( $this->getWikiPage()->getTimestamp(), $user ),
732  [],
733  [ 'oldid' => $this->getWikiPage()->getLatest() ]
734  )
735  ];
736  }
737 
738  // Total number of edits
739  $pageInfo['header-edits'][] = [
740  $this->msg( 'pageinfo-edits' ),
741  $lang->formatNum( $pageCounts['edits'] )
742  ];
743 
744  // Total number of distinct authors
745  if ( $pageCounts['authors'] > 0 ) {
746  $pageInfo['header-edits'][] = [
747  $this->msg( 'pageinfo-authors' ),
748  $lang->formatNum( $pageCounts['authors'] )
749  ];
750  }
751 
752  // Recent number of edits (within past 30 days)
753  $pageInfo['header-edits'][] = [
754  $this->msg(
755  'pageinfo-recent-edits',
756  $lang->formatDuration( $config->get( 'RCMaxAge' ) )
757  ),
758  $lang->formatNum( $pageCounts['recent_edits'] )
759  ];
760 
761  // Recent number of distinct authors
762  $pageInfo['header-edits'][] = [
763  $this->msg( 'pageinfo-recent-authors' ),
764  $lang->formatNum( $pageCounts['recent_authors'] )
765  ];
766 
767  // Array of MagicWord objects
768  $magicWords = $this->magicWordFactory->getDoubleUnderscoreArray();
769 
770  // Array of magic word IDs
771  $wordIDs = $magicWords->names;
772 
773  // Array of IDs => localized magic words
774  $localizedWords = $this->contentLanguage->getMagicWords();
775 
776  $listItems = [];
777  foreach ( $pageProperties as $property => $value ) {
778  if ( in_array( $property, $wordIDs ) ) {
779  $listItems[] = Html::element( 'li', [], $localizedWords[$property][1] );
780  }
781  }
782 
783  $localizedList = Html::rawElement( 'ul', [], implode( '', $listItems ) );
784  $hiddenCategories = $this->getWikiPage()->getHiddenCategories();
785 
786  if (
787  count( $listItems ) > 0 ||
788  count( $hiddenCategories ) > 0 ||
789  $pageCounts['transclusion']['from'] > 0 ||
790  $pageCounts['transclusion']['to'] > 0
791  ) {
792  $options = [ 'LIMIT' => $config->get( 'PageInfoTransclusionLimit' ) ];
793  $transcludedTemplates = $title->getTemplateLinksFrom( $options );
794  if ( $config->get( 'MiserMode' ) ) {
795  $transcludedTargets = [];
796  } else {
797  $transcludedTargets = $title->getTemplateLinksTo( $options );
798  }
799 
800  // Page properties
801  $pageInfo['header-properties'] = [];
802 
803  // Magic words
804  if ( count( $listItems ) > 0 ) {
805  $pageInfo['header-properties'][] = [
806  $this->msg( 'pageinfo-magic-words' )->numParams( count( $listItems ) ),
807  $localizedList
808  ];
809  }
810 
811  // Hidden categories
812  if ( count( $hiddenCategories ) > 0 ) {
813  $pageInfo['header-properties'][] = [
814  $this->msg( 'pageinfo-hidden-categories' )
815  ->numParams( count( $hiddenCategories ) ),
816  Linker::formatHiddenCategories( $hiddenCategories )
817  ];
818  }
819 
820  // Transcluded templates
821  if ( $pageCounts['transclusion']['from'] > 0 ) {
822  if ( $pageCounts['transclusion']['from'] > count( $transcludedTemplates ) ) {
823  $more = $this->msg( 'morenotlisted' )->escaped();
824  } else {
825  $more = null;
826  }
827 
828  $templateListFormatter = new TemplatesOnThisPageFormatter(
829  $this->getContext(),
831  );
832 
833  $pageInfo['header-properties'][] = [
834  $this->msg( 'pageinfo-templates' )
835  ->numParams( $pageCounts['transclusion']['from'] ),
836  $templateListFormatter->format( $transcludedTemplates, false, $more )
837  ];
838  }
839 
840  if ( !$config->get( 'MiserMode' ) && $pageCounts['transclusion']['to'] > 0 ) {
841  if ( $pageCounts['transclusion']['to'] > count( $transcludedTargets ) ) {
842  $more = $linkRenderer->makeLink(
843  $whatLinksHere,
844  $this->msg( 'moredotdotdot' )->text(),
845  [],
846  [ 'hidelinks' => 1, 'hideredirs' => 1 ]
847  );
848  } else {
849  $more = null;
850  }
851 
852  $templateListFormatter = new TemplatesOnThisPageFormatter(
853  $this->getContext(),
855  );
856 
857  $pageInfo['header-properties'][] = [
858  $this->msg( 'pageinfo-transclusions' )
859  ->numParams( $pageCounts['transclusion']['to'] ),
860  $templateListFormatter->format( $transcludedTargets, false, $more )
861  ];
862  }
863  }
864 
865  return $pageInfo;
866  }
867 
875  protected function getNamespaceProtectionMessage( Title $title ): ?string {
876  $rights = [];
877  if ( $title->isRawHtmlMessage() ) {
878  $rights[] = 'editsitecss';
879  $rights[] = 'editsitejs';
880  } elseif ( $title->isSiteCssConfigPage() ) {
881  $rights[] = 'editsitecss';
882  } elseif ( $title->isSiteJsConfigPage() ) {
883  $rights[] = 'editsitejs';
884  } elseif ( $title->isSiteJsonConfigPage() ) {
885  $rights[] = 'editsitejson';
886  } elseif ( $title->isUserCssConfigPage() ) {
887  $rights[] = 'editusercss';
888  } elseif ( $title->isUserJsConfigPage() ) {
889  $rights[] = 'edituserjs';
890  } elseif ( $title->isUserJsonConfigPage() ) {
891  $rights[] = 'edituserjson';
892  } else {
893  $namespaceProtection = $this->context->getConfig()->get( 'NamespaceProtection' );
894  $right = $namespaceProtection[$title->getNamespace()] ?? null;
895  if ( $right ) {
896  // a single string as the value is allowed as well as an array
897  $rights = (array)$right;
898  }
899  }
900  if ( $rights ) {
901  return $this->msg( 'protect-fallback', $this->getLanguage()->commaList( $rights ) )->parse();
902  } else {
903  return null;
904  }
905  }
906 
912  private function pageCounts() {
913  $page = $this->getWikiPage();
914  $fname = __METHOD__;
915  $config = $this->context->getConfig();
917 
918  return $cache->getWithSetCallback(
919  self::getCacheKey( $cache, $page->getTitle(), $page->getLatest() ),
920  WANObjectCache::TTL_WEEK,
921  function ( $oldValue, &$ttl, &$setOpts ) use ( $page, $config, $fname ) {
923 
924  $title = $page->getTitle();
925  $id = $title->getArticleID();
926 
927  $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
928  $dbrWatchlist = $this->loadBalancer->getConnectionRef(
929  DB_REPLICA,
930  [ 'watchlist' ]
931  );
932  $setOpts += Database::getCacheSetOptions( $dbr, $dbrWatchlist );
933 
935  $tables = [ 'revision' ];
936  $field = 'rev_actor';
937  $pageField = 'rev_page';
938  $tsField = 'rev_timestamp';
939  } else /* SCHEMA_COMPAT_READ_TEMP */ {
940  $tables = [ 'revision_actor_temp' ];
941  $field = 'revactor_actor';
942  $pageField = 'revactor_page';
943  $tsField = 'revactor_timestamp';
944  }
945  $joins = [];
946 
948 
949  $result = [];
950  $result['watchers'] = $watchedItemStore->countWatchers( $title );
951 
952  if ( $config->get( 'ShowUpdatedMarker' ) ) {
953  $updated = (int)wfTimestamp( TS_UNIX, $page->getTimestamp() );
954  $result['visitingWatchers'] = $watchedItemStore->countVisitingWatchers(
955  $title,
956  $updated - $config->get( 'WatchersMaxAge' )
957  );
958  }
959 
960  // Total number of edits
961  $edits = (int)$dbr->selectField(
962  'revision',
963  'COUNT(*)',
964  [ 'rev_page' => $id ],
965  $fname
966  );
967  $result['edits'] = $edits;
968 
969  // Total number of distinct authors
970  if ( $config->get( 'MiserMode' ) ) {
971  $result['authors'] = 0;
972  } else {
973  $result['authors'] = (int)$dbr->selectField(
974  $tables,
975  "COUNT(DISTINCT $field)",
976  [ $pageField => $id ],
977  $fname,
978  [],
979  $joins
980  );
981  }
982 
983  // "Recent" threshold defined by RCMaxAge setting
984  $threshold = $dbr->timestamp( time() - $config->get( 'RCMaxAge' ) );
985 
986  // Recent number of edits
987  $edits = (int)$dbr->selectField(
988  'revision',
989  'COUNT(rev_page)',
990  [
991  'rev_page' => $id,
992  "rev_timestamp >= " . $dbr->addQuotes( $threshold )
993  ],
994  $fname
995  );
996  $result['recent_edits'] = $edits;
997 
998  // Recent number of distinct authors
999  $result['recent_authors'] = (int)$dbr->selectField(
1000  $tables,
1001  "COUNT(DISTINCT $field)",
1002  [
1003  $pageField => $id,
1004  "$tsField >= " . $dbr->addQuotes( $threshold )
1005  ],
1006  $fname,
1007  [],
1008  $joins
1009  );
1010 
1011  // Subpages (if enabled)
1012  if ( $this->namespaceInfo->hasSubpages( $title->getNamespace() ) ) {
1013  $conds = [ 'page_namespace' => $title->getNamespace() ];
1014  $conds[] = 'page_title ' .
1015  $dbr->buildLike( $title->getDBkey() . '/', $dbr->anyString() );
1016 
1017  // Subpages of this page (redirects)
1018  $conds['page_is_redirect'] = 1;
1019  $result['subpages']['redirects'] = (int)$dbr->selectField(
1020  'page',
1021  'COUNT(page_id)',
1022  $conds,
1023  $fname
1024  );
1025 
1026  // Subpages of this page (non-redirects)
1027  $conds['page_is_redirect'] = 0;
1028  $result['subpages']['nonredirects'] = (int)$dbr->selectField(
1029  'page',
1030  'COUNT(page_id)',
1031  $conds,
1032  $fname
1033  );
1034 
1035  // Subpages of this page (total)
1036  $result['subpages']['total'] = $result['subpages']['redirects']
1037  + $result['subpages']['nonredirects'];
1038  }
1039 
1040  // Counts for the number of transclusion links (to/from)
1041  if ( $config->get( 'MiserMode' ) ) {
1042  $result['transclusion']['to'] = 0;
1043  } else {
1044  $result['transclusion']['to'] = (int)$dbr->selectField(
1045  'templatelinks',
1046  'COUNT(tl_from)',
1047  [
1048  'tl_namespace' => $title->getNamespace(),
1049  'tl_title' => $title->getDBkey()
1050  ],
1051  $fname
1052  );
1053  }
1054 
1055  $result['transclusion']['from'] = (int)$dbr->selectField(
1056  'templatelinks',
1057  'COUNT(*)',
1058  [ 'tl_from' => $title->getArticleID() ],
1059  $fname
1060  );
1061 
1062  return $result;
1063  }
1064  );
1065  }
1066 
1072  protected function getPageTitle() {
1073  return $this->msg( 'pageinfo-title', $this->getTitle()->getPrefixedText() )->text();
1074  }
1075 
1081  protected function getDescription() {
1082  return '';
1083  }
1084 
1091  protected static function getCacheKey( WANObjectCache $cache, PageIdentity $page, $revId ) {
1092  return $cache->makeKey( 'infoaction', md5( (string)$page ), $revId, self::VERSION );
1093  }
1094 }
Page\PageIdentity
Interface for objects (potentially) representing an editable wiki page.
Definition: PageIdentity.php:64
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:52
Page
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition: Page.php:29
InfoAction\$magicWordFactory
MagicWordFactory $magicWordFactory
Definition: InfoAction.php:64
WatchedItemStoreInterface\countWatchers
countWatchers( $target)
MediaWiki\Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
ParserOutput
Definition: ParserOutput.php:32
MagicWordFactory
A factory that stores information about MagicWords, and creates them on demand with caching.
Definition: MagicWordFactory.php:37
Linker\revUserTools
static revUserTools(RevisionRecord $revRecord, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition: Linker.php:1319
FormlessAction
An action which just does something, without showing a form first.
Definition: FormlessAction.php:30
Category\newFromTitle
static newFromTitle(PageIdentity $page)
Factory function.
Definition: Category.php:159
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:193
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
InfoAction\getCacheKey
static getCacheKey(WANObjectCache $cache, PageIdentity $page, $revId)
Definition: InfoAction.php:1091
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:812
MediaWiki\Linker\LinkRenderer
Class that generates HTML links for pages.
Definition: LinkRenderer.php:43
InfoAction\getDescription
getDescription()
Returns the description that goes below the "<h1>" tag.
Definition: InfoAction.php:1081
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1668
InfoAction\requiresWrite
requiresWrite()
Whether this action requires the wiki not to be locked.
Definition: InfoAction.php:157
getAuthority
getAuthority()
Action\exists
static exists(string $name)
Check if a given action is recognised, even if it's disabled.
Definition: Action.php:121
User\newFromName
static newFromName( $name, $validate='valid')
Definition: User.php:606
InfoAction\$linkBatchFactory
LinkBatchFactory $linkBatchFactory
Definition: InfoAction.php:55
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\$loadBalancer
ILoadBalancer $loadBalancer
Definition: InfoAction.php:61
InfoAction\getName
getName()
Returns the name of the action this object responds to.
Definition: InfoAction.php:139
MediaWiki\Revision\RevisionLookup
Service for looking up page revisions.
Definition: RevisionLookup.php:38
Linker\formatHiddenCategories
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition: Linker.php:2241
InfoAction\$pageProps
PageProps $pageProps
Definition: InfoAction.php:70
MediaWiki\Languages\LanguageNameUtils
A service that provides utilities to do with language names and codes.
Definition: LanguageNameUtils.php:42
$dbr
$dbr
Definition: testCompression.php:54
Action\getContext
getContext()
Get the IContextSource in use here.
Definition: Action.php:132
InfoAction
Displays information about a page.
Definition: InfoAction.php:42
InfoAction\makeHeader
makeHeader( $header, $canonicalId)
Creates a header that can be added to the output.
Definition: InfoAction.php:281
InfoAction\$revisionLookup
RevisionLookup $revisionLookup
Definition: InfoAction.php:76
InfoAction\getPageTitle
getPageTitle()
Returns the name that goes in the "<h1>" page title.
Definition: InfoAction.php:1072
Action\getArticle
getArticle()
Get a Article object.
Definition: Action.php:206
SpecialPage\getTitleValueFor
static getTitleValueFor( $name, $subpage=false, $fragment='')
Get a localised TitleValue object for a specified special page name.
Definition: SpecialPage.php:122
MediaWiki\Cache\LinkBatchFactory
Definition: LinkBatchFactory.php:39
WikiPage\getTitle
getTitle()
Get the title object of the article.
Definition: WikiPage.php:311
$title
$title
Definition: testCompression.php:38
WatchedItemStoreInterface\countVisitingWatchers
countVisitingWatchers( $target, $threshold)
Number of page watchers who also visited a "recent" edit.
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
InfoAction\VERSION
const VERSION
Definition: InfoAction.php:43
InfoAction\$watchedItemStore
WatchedItemStoreInterface $watchedItemStore
Definition: InfoAction.php:82
InfoAction\addRow
addRow( $table, $name, $value, $id)
Adds a row to a table that will be added to the content.
Definition: InfoAction.php:305
Action\getWikiPage
getWikiPage()
Get a WikiPage object.
Definition: Action.php:195
InfoAction\pageCounts
pageCounts()
Returns page counts that would be too "expensive" to retrieve by normal means.
Definition: InfoAction.php:912
MediaWiki\Linker\LinkRenderer\makeKnownLink
makeKnownLink( $target, $text=null, array $extraAttribs=[], array $query=[])
Definition: LinkRenderer.php:227
InfoAction\$linkRenderer
LinkRenderer $linkRenderer
Definition: InfoAction.php:58
Action\getUser
getUser()
Shortcut to get the User being used for this instance.
Definition: Action.php:166
Action\$context
IContextSource $context
IContextSource if specified; otherwise we'll use the Context from the Page.
Definition: Action.php:66
$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:343
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:322
ContentHandler\getLocalizedName
static getLocalizedName( $name, Language $lang=null)
Returns the localized name for a given content model.
Definition: ContentHandler.php:307
InfoAction\$contentLanguage
Language $contentLanguage
Definition: InfoAction.php:46
$header
$header
Definition: updateCredits.php:37
Action\addHelpLink
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition: Action.php:431
InfoAction\__construct
__construct(Page $page, IContextSource $context, Language $contentLanguage, HookContainer $hookContainer, LanguageNameUtils $languageNameUtils, LinkBatchFactory $linkBatchFactory, LinkRenderer $linkRenderer, ILoadBalancer $loadBalancer, MagicWordFactory $magicWordFactory, NamespaceInfo $namespaceInfo, PageProps $pageProps, RepoGroup $repoGroup, RevisionLookup $revisionLookup, WANObjectCache $wanObjectCache, WatchedItemStoreInterface $watchedItemStore)
Definition: InfoAction.php:101
InfoAction\$hookRunner
HookRunner $hookRunner
Definition: InfoAction.php:49
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:128
InfoAction\getNamespaceProtectionMessage
getNamespaceProtectionMessage(Title $title)
Get namespace protection message for title or null if no namespace protection has been applied.
Definition: InfoAction.php:875
Action\getTitle
getTitle()
Shortcut to get the Title object from the page.
Definition: Action.php:216
InfoAction\requiresUnblock
requiresUnblock()
Whether this action can still be executed by a blocked user.
Definition: InfoAction.php:148
NS_USER
const NS_USER
Definition: Defines.php:66
InfoAction\$languageNameUtils
LanguageNameUtils $languageNameUtils
Definition: InfoAction.php:52
InfoAction\$wanObjectCache
WANObjectCache $wanObjectCache
Definition: InfoAction.php:79
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:58
InfoAction\onView
onView()
Shows page information on GET request.
Definition: InfoAction.php:187
Title
Represents a title within MediaWiki.
Definition: Title.php:47
$magicWords
$magicWords
@phpcs-require-sorted-array
Definition: MessagesAb.php:73
SCHEMA_COMPAT_READ_NEW
const SCHEMA_COMPAT_READ_NEW
Definition: Defines.php:267
InfoAction\invalidateCache
static invalidateCache(PageIdentity $page, $revid=null)
Clear the info cache for a given Title.
Definition: InfoAction.php:168
$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
PageProps
Gives access to properties of a page.
Definition: PageProps.php:33
Action\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: Action.php:228
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:210
Action\getLanguage
getLanguage()
Shortcut to get the user Language being used for this instance.
Definition: Action.php:185
Message
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition: Message.php:138
RepoGroup
Prioritized list of file repositories.
Definition: RepoGroup.php:32
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:78
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:67
WikiPage\getLatest
getLatest( $wikiId=self::LOCAL)
Get the page_latest field.
Definition: WikiPage.php:752
Action\getOutput
getOutput()
Get the OutputPage being used for this instance.
Definition: Action.php:156
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
NamespaceInfo
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Definition: NamespaceInfo.php:35
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:556
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:232
NS_FILE
const NS_FILE
Definition: Defines.php:70
WikiPage\getTimestamp
getTimestamp()
Definition: WikiPage.php:848
WatchedItemStoreInterface
Definition: WatchedItemStoreInterface.php:31
InfoAction\$repoGroup
RepoGroup $repoGroup
Definition: InfoAction.php:73
MediaWiki\Linker\LinkRenderer\makeLink
makeLink( $target, $text=null, array $extraAttribs=[], array $query=[])
Definition: LinkRenderer.php:160
Language
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition: Language.php:42
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
InfoAction\$namespaceInfo
NamespaceInfo $namespaceInfo
Definition: InfoAction.php:67
$wgActorTableSchemaMigrationStage
int $wgActorTableSchemaMigrationStage
Actor table schema migration stage, for migration from the temporary table revision_actor_temp to the...
Definition: DefaultSettings.php:2411