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