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