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