74 $revid = $revision ? $revision->getId() :
null;
76 if ( $revid !==
null ) {
77 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
92 $oldid = $this->page->getOldID();
94 $revision = $this->page->getRevisionFetched();
97 if ( $revision ===
null ) {
98 return $this->
msg(
'missing-revision', $oldid )->parse();
102 if ( !$revision->isCurrent() ) {
103 return $this->
msg(
'pageinfo-not-current' )->plain();
111 if ( !$this->
msg(
'pageinfo-header' )->isDisabled() ) {
112 $content .= $this->
msg(
'pageinfo-header' )->parse();
116 $content .= Html::element(
'style', [],
117 '.mw-hiddenCategoriesExplanation { display: none; }' ) .
"\n";
120 $content .= Html::element(
'style', [],
121 '.mw-templatesUsedExplanation { display: none; }' ) .
"\n";
130 foreach ( $pageInfo as
$header => $infoTable ) {
135 $this->
msg(
"pageinfo-${header}" )->text(),
136 "mw-pageinfo-${header}"
140 foreach ( $infoTable as $infoRow ) {
141 if ( $infoRow[0] ==
"below" ) {
142 $below = $infoRow[1] .
"\n";
145 $name = ( $infoRow[0] instanceof
Message ) ? $infoRow[0]->escaped() : $infoRow[0];
146 $value = ( $infoRow[1] instanceof
Message ) ? $infoRow[1]->escaped() : $infoRow[1];
147 $id = ( $infoRow[0] instanceof
Message ) ? $infoRow[0]->getKey() :
null;
148 $table = $this->
addRow( $table, $name, $value, $id ) .
"\n";
154 if ( !$this->
msg(
'pageinfo-footer' )->isDisabled() ) {
155 $content .= $this->
msg(
'pageinfo-footer' )->parse();
169 $spanAttribs = [
'class' =>
'mw-headline',
'id' => Sanitizer::escapeIdForAttribute(
$header ) ];
170 $h2Attribs = [
'id' => Sanitizer::escapeIdForAttribute( $canonicalId ) ];
172 return Html::rawElement(
'h2', $h2Attribs, Html::element(
'span', $spanAttribs,
$header ) );
184 protected function addRow( $table, $name, $value, $id ) {
188 $id ===
null ? [] : [
'id' =>
'mw-' . $id ],
189 Html::rawElement(
'td', [
'style' =>
'vertical-align: top;' ], $name ) .
190 Html::rawElement(
'td', [], $value )
202 return $content . Html::rawElement(
'table', [
'class' =>
'wikitable mw-page-info' ],
219 $services = MediaWikiServices::getInstance();
224 $id =
$title->getArticleID();
225 $config = $this->context->getConfig();
226 $linkRenderer = $services->getLinkRenderer();
228 $pageCounts = $this->
pageCounts( $this->page );
231 $pageProperties = $props[$id] ?? [];
235 $pageInfo[
'header-basic'] = [];
238 $displayTitle = $pageProperties[
'displaytitle'] ??
$title->getPrefixedText();
240 $pageInfo[
'header-basic'][] = [
241 $this->
msg(
'pageinfo-display-title' ), $displayTitle
245 $redirectTarget = $this->page->getRedirectTarget();
246 if ( $redirectTarget !==
null ) {
247 $pageInfo[
'header-basic'][] = [
248 $this->
msg(
'pageinfo-redirectsto' ),
249 $linkRenderer->makeLink( $redirectTarget ) .
250 $this->
msg(
'word-separator' )->escaped() .
251 $this->
msg(
'parentheses' )->rawParams( $linkRenderer->makeLink(
253 $this->msg(
'pageinfo-redirectsto-info' )->text(),
255 [
'action' =>
'info' ]
261 $sortKey = $pageProperties[
'defaultsort'] ??
$title->getCategorySortkey();
263 $sortKey = htmlspecialchars( $sortKey );
264 $pageInfo[
'header-basic'][] = [ $this->
msg(
'pageinfo-default-sort' ), $sortKey ];
267 $pageInfo[
'header-basic'][] = [
268 $this->
msg(
'pageinfo-length' ),
$lang->formatNum(
$title->getLength() )
272 $pageNamespace =
$title->getNsText();
273 if ( $pageNamespace ) {
274 $pageInfo[
'header-basic'][] = [ $this->
msg(
'pageinfo-namespace' ), $pageNamespace ];
278 $pageInfo[
'header-basic'][] = [ $this->
msg(
'pageinfo-article-id' ), $id ];
281 $pageLang =
$title->getPageLanguage()->getCode();
283 $pageLangHtml = $pageLang .
' - ' .
286 $permissionManager = $services->getPermissionManager();
287 if ( $config->get(
'PageLanguageUseDB' )
288 && $permissionManager->userCan(
'pagelang', $user,
$title )
290 $pageLangHtml .=
' ' . $this->
msg(
'parentheses' )->rawParams( $linkRenderer->makeLink(
292 $this->
msg(
'pageinfo-language-change' )->text()
296 $pageInfo[
'header-basic'][] = [
297 $this->
msg(
'pageinfo-language' )->escaped(),
304 if ( $config->get(
'ContentHandlerUseDB' )
305 && $permissionManager->userCan(
'editcontentmodel', $user,
$title )
307 $modelHtml .=
' ' . $this->
msg(
'parentheses' )->rawParams( $linkRenderer->makeLink(
309 $this->
msg(
'pageinfo-content-model-change' )->text()
313 $pageInfo[
'header-basic'][] = [
314 $this->
msg(
'pageinfo-content-model' ),
320 if ( $pageUser && $pageUser->getId() && !$pageUser->isHidden() ) {
321 $pageInfo[
'header-basic'][] = [
322 $this->
msg(
'pageinfo-user-id' ),
330 if ( isset( $pageProperties[
'noindex'] ) ) {
331 $pOutput->setIndexPolicy(
'noindex' );
333 if ( isset( $pageProperties[
'index'] ) ) {
334 $pOutput->setIndexPolicy(
'index' );
338 $policy = $this->page->getRobotPolicy(
'view', $pOutput );
339 $pageInfo[
'header-basic'][] = [
341 $this->
msg(
'pageinfo-robot-policy' ),
342 $this->
msg(
"pageinfo-robot-${policy['index']}" )
345 $unwatchedPageThreshold = $config->get(
'UnwatchedPageThreshold' );
346 if ( $permissionManager->userHasRight( $user,
'unwatchedpages' ) ||
347 ( $unwatchedPageThreshold !==
false &&
348 $pageCounts[
'watchers'] >= $unwatchedPageThreshold )
351 $pageInfo[
'header-basic'][] = [
352 $this->
msg(
'pageinfo-watchers' ),
353 $lang->formatNum( $pageCounts[
'watchers'] )
356 $config->get(
'ShowUpdatedMarker' ) &&
357 isset( $pageCounts[
'visitingWatchers'] )
359 $minToDisclose = $config->get(
'UnwatchedPageSecret' );
360 if ( $pageCounts[
'visitingWatchers'] > $minToDisclose ||
361 $permissionManager->userHasRight( $user,
'unwatchedpages' ) ) {
362 $pageInfo[
'header-basic'][] = [
363 $this->
msg(
'pageinfo-visiting-watchers' ),
364 $lang->formatNum( $pageCounts[
'visitingWatchers'] )
367 $pageInfo[
'header-basic'][] = [
368 $this->
msg(
'pageinfo-visiting-watchers' ),
369 $this->
msg(
'pageinfo-few-visiting-watchers' )
373 } elseif ( $unwatchedPageThreshold !==
false ) {
374 $pageInfo[
'header-basic'][] = [
375 $this->
msg(
'pageinfo-watchers' ),
376 $this->
msg(
'pageinfo-few-watchers' )->numParams( $unwatchedPageThreshold )
382 $pageInfo[
'header-basic'][] = [
383 $linkRenderer->makeLink(
385 $this->
msg(
'pageinfo-redirects-name' )->text(),
393 $this->
msg(
'pageinfo-redirects-value' )
394 ->numParams( count(
$title->getRedirectsHere() ) )
398 if ( $this->page->isCountable() ) {
399 $pageInfo[
'header-basic'][] = [
400 $this->
msg(
'pageinfo-contentpage' ),
401 $this->
msg(
'pageinfo-contentpage-yes' )
406 if ( $services->getNamespaceInfo()->hasSubpages(
$title->getNamespace() ) ) {
408 'Prefixindex',
$title->getPrefixedText() .
'/' );
409 $pageInfo[
'header-basic'][] = [
410 $linkRenderer->makeLink(
412 $this->
msg(
'pageinfo-subpages-name' )->text()
414 $this->
msg(
'pageinfo-subpages-value' )
416 $pageCounts[
'subpages'][
'total'],
417 $pageCounts[
'subpages'][
'redirects'],
418 $pageCounts[
'subpages'][
'nonredirects'] )
427 $allCount = (int)$category->getPageCount();
428 $subcatCount = (int)$category->getSubcatCount();
429 $fileCount = (int)$category->getFileCount();
430 $pagesCount = $allCount - $subcatCount - $fileCount;
432 $pageInfo[
'category-info'] = [
434 $this->
msg(
'pageinfo-category-total' ),
435 $lang->formatNum( $allCount )
438 $this->
msg(
'pageinfo-category-pages' ),
439 $lang->formatNum( $pagesCount )
442 $this->
msg(
'pageinfo-category-subcats' ),
443 $lang->formatNum( $subcatCount )
446 $this->
msg(
'pageinfo-category-files' ),
447 $lang->formatNum( $fileCount )
454 $fileObj = $services->getRepoGroup()->findFile(
$title );
455 if ( $fileObj !==
false ) {
457 $output = Wikimedia\base_convert( $fileObj->getSha1(), 36, 16, 40 );
458 $pageInfo[
'header-basic'][] = [
459 $this->
msg(
'pageinfo-file-hash' ),
466 $pageInfo[
'header-restrictions'] = [];
469 if (
$title->isCascadeProtected() ) {
471 $sources =
$title->getCascadeProtectionSources()[0];
473 foreach ( $sources as $sourceTitle ) {
474 $cascadingFrom .= Html::rawElement(
475 'li', [], $linkRenderer->makeKnownLink( $sourceTitle ) );
478 $cascadingFrom = Html::rawElement(
'ul', [], $cascadingFrom );
479 $pageInfo[
'header-restrictions'][] = [
480 $this->
msg(
'pageinfo-protect-cascading-from' ),
486 if (
$title->areRestrictionsCascading() ) {
487 $pageInfo[
'header-restrictions'][] = [
488 $this->
msg(
'pageinfo-protect-cascading' ),
489 $this->
msg(
'pageinfo-protect-cascading-yes' )
494 foreach (
$title->getRestrictionTypes() as $restrictionType ) {
495 $protectionLevel = implode(
', ',
$title->getRestrictions( $restrictionType ) );
497 if ( $protectionLevel ==
'' ) {
499 $message = $this->
msg(
'protect-default' )->escaped();
503 $message = $this->
msg(
"protect-level-$protectionLevel" );
504 if ( $message->isDisabled() ) {
506 $message = $this->
msg(
"protect-fallback", $protectionLevel )->parse();
508 $message = $message->escaped();
511 $expiry =
$title->getRestrictionExpiry( $restrictionType );
512 $formattedexpiry = $this->
msg(
'parentheses',
513 $lang->formatExpiry( $expiry ) )->escaped();
514 $message .= $this->
msg(
'word-separator' )->escaped() . $formattedexpiry;
518 $pageInfo[
'header-restrictions'][] = [
519 $this->
msg(
"restriction-$restrictionType" ), $message
523 $pageInfo[
'header-restrictions'][] = [
525 $linkRenderer->makeKnownLink(
527 $this->
msg(
'pageinfo-view-protect-log' )->text(),
529 [
'type' =>
'protect',
'page' =>
$title->getPrefixedText() ]
533 if ( !$this->page->exists() ) {
538 $pageInfo[
'header-edits'] = [];
540 $firstRev = $this->page->getOldestRevision();
541 $lastRev = $this->page->getRevision();
545 $firstRevUser = $firstRev->getUserText( RevisionRecord::FOR_THIS_USER );
546 if ( $firstRevUser !==
'' ) {
548 $batch->addObj( $firstRevUserTitle );
549 $batch->addObj( $firstRevUserTitle->getTalkPage() );
554 $lastRevUser = $lastRev->getUserText( RevisionRecord::FOR_THIS_USER );
555 if ( $lastRevUser !==
'' ) {
557 $batch->addObj( $lastRevUserTitle );
558 $batch->addObj( $lastRevUserTitle->getTalkPage() );
566 $pageInfo[
'header-edits'][] = [
567 $this->
msg(
'pageinfo-firstuser' ),
572 $pageInfo[
'header-edits'][] = [
573 $this->
msg(
'pageinfo-firsttime' ),
574 $linkRenderer->makeKnownLink(
576 $lang->userTimeAndDate( $firstRev->getTimestamp(), $user ),
578 [
'oldid' => $firstRev->getId() ]
585 $pageInfo[
'header-edits'][] = [
586 $this->
msg(
'pageinfo-lastuser' ),
591 $pageInfo[
'header-edits'][] = [
592 $this->
msg(
'pageinfo-lasttime' ),
593 $linkRenderer->makeKnownLink(
595 $lang->userTimeAndDate( $this->page->getTimestamp(), $user ),
597 [
'oldid' => $this->page->getLatest() ]
603 $pageInfo[
'header-edits'][] = [
604 $this->
msg(
'pageinfo-edits' ),
$lang->formatNum( $pageCounts[
'edits'] )
608 if ( $pageCounts[
'authors'] > 0 ) {
609 $pageInfo[
'header-edits'][] = [
610 $this->
msg(
'pageinfo-authors' ),
$lang->formatNum( $pageCounts[
'authors'] )
615 $pageInfo[
'header-edits'][] = [
616 $this->
msg(
'pageinfo-recent-edits',
617 $lang->formatDuration( $config->get(
'RCMaxAge' ) ) ),
618 $lang->formatNum( $pageCounts[
'recent_edits'] )
622 $pageInfo[
'header-edits'][] = [
623 $this->
msg(
'pageinfo-recent-authors' ),
624 $lang->formatNum( $pageCounts[
'recent_authors'] )
628 $magicWords = $services->getMagicWordFactory()->getDoubleUnderscoreArray();
634 $localizedWords = $services->getContentLanguage()->getMagicWords();
637 foreach ( $pageProperties as $property => $value ) {
638 if ( in_array( $property, $wordIDs ) ) {
639 $listItems[] = Html::element(
'li', [], $localizedWords[$property][1] );
643 $localizedList = Html::rawElement(
'ul', [], implode(
'', $listItems ) );
644 $hiddenCategories = $this->page->getHiddenCategories();
647 count( $listItems ) > 0 ||
648 count( $hiddenCategories ) > 0 ||
649 $pageCounts[
'transclusion'][
'from'] > 0 ||
650 $pageCounts[
'transclusion'][
'to'] > 0
652 $options = [
'LIMIT' => $config->get(
'PageInfoTransclusionLimit' ) ];
653 $transcludedTemplates =
$title->getTemplateLinksFrom( $options );
654 if ( $config->get(
'MiserMode' ) ) {
655 $transcludedTargets = [];
657 $transcludedTargets =
$title->getTemplateLinksTo( $options );
661 $pageInfo[
'header-properties'] = [];
664 if ( count( $listItems ) > 0 ) {
665 $pageInfo[
'header-properties'][] = [
666 $this->
msg(
'pageinfo-magic-words' )->numParams( count( $listItems ) ),
672 if ( count( $hiddenCategories ) > 0 ) {
673 $pageInfo[
'header-properties'][] = [
674 $this->
msg(
'pageinfo-hidden-categories' )
675 ->numParams( count( $hiddenCategories ) ),
681 if ( $pageCounts[
'transclusion'][
'from'] > 0 ) {
682 if ( $pageCounts[
'transclusion'][
'from'] > count( $transcludedTemplates ) ) {
683 $more = $this->
msg(
'morenotlisted' )->escaped();
693 $pageInfo[
'header-properties'][] = [
694 $this->
msg(
'pageinfo-templates' )
695 ->numParams( $pageCounts[
'transclusion'][
'from'] ),
696 $templateListFormatter->format( $transcludedTemplates,
false, $more )
700 if ( !$config->get(
'MiserMode' ) && $pageCounts[
'transclusion'][
'to'] > 0 ) {
701 if ( $pageCounts[
'transclusion'][
'to'] > count( $transcludedTargets ) ) {
702 $more = $linkRenderer->makeLink(
704 $this->
msg(
'moredotdotdot' )->text(),
706 [
'hidelinks' => 1,
'hideredirs' => 1 ]
717 $pageInfo[
'header-properties'][] = [
718 $this->
msg(
'pageinfo-transclusions' )
719 ->numParams( $pageCounts[
'transclusion'][
'to'] ),
720 $templateListFormatter->format( $transcludedTargets,
false, $more )
736 $config = $this->context->getConfig();
737 $services = MediaWikiServices::getInstance();
738 $cache = $services->getMainWANObjectCache();
740 return $cache->getWithSetCallback(
743 function ( $oldValue, &$ttl, &$setOpts ) use (
$page, $config, $fname, $services ) {
745 $id =
$title->getArticleID();
749 $setOpts += Database::getCacheSetOptions(
$dbr, $dbrWatchlist );
751 $tables = [
'revision_actor_temp' ];
752 $field =
'revactor_actor';
753 $pageField =
'revactor_page';
754 $tsField =
'revactor_timestamp';
757 $watchedItemStore = $services->getWatchedItemStore();
760 $result[
'watchers'] = $watchedItemStore->countWatchers(
$title );
762 if ( $config->get(
'ShowUpdatedMarker' ) ) {
764 $result[
'visitingWatchers'] = $watchedItemStore->countVisitingWatchers(
766 $updated - $config->get(
'WatchersMaxAge' )
771 $edits = (int)
$dbr->selectField(
774 [
'rev_page' => $id ],
777 $result[
'edits'] = $edits;
780 if ( $config->get(
'MiserMode' ) ) {
781 $result[
'authors'] = 0;
783 $result[
'authors'] = (int)
$dbr->selectField(
785 "COUNT(DISTINCT $field)",
786 [ $pageField => $id ],
794 $threshold =
$dbr->timestamp( time() - $config->get(
'RCMaxAge' ) );
797 $edits = (int)
$dbr->selectField(
802 "rev_timestamp >= " .
$dbr->addQuotes( $threshold )
806 $result[
'recent_edits'] = $edits;
809 $result[
'recent_authors'] = (int)
$dbr->selectField(
811 "COUNT(DISTINCT $field)",
814 "$tsField >= " .
$dbr->addQuotes( $threshold )
822 if ( $services->getNamespaceInfo()->hasSubpages(
$title->getNamespace() ) ) {
823 $conds = [
'page_namespace' =>
$title->getNamespace() ];
824 $conds[] =
'page_title ' .
828 $conds[
'page_is_redirect'] = 1;
829 $result[
'subpages'][
'redirects'] = (int)
$dbr->selectField(
837 $conds[
'page_is_redirect'] = 0;
838 $result[
'subpages'][
'nonredirects'] = (int)
$dbr->selectField(
846 $result[
'subpages'][
'total'] = $result[
'subpages'][
'redirects']
847 + $result[
'subpages'][
'nonredirects'];
851 if ( $config->get(
'MiserMode' ) ) {
852 $result[
'transclusion'][
'to'] = 0;
854 $result[
'transclusion'][
'to'] = (int)
$dbr->selectField(
858 'tl_namespace' =>
$title->getNamespace(),
859 'tl_title' =>
$title->getDBkey()
865 $result[
'transclusion'][
'from'] = (int)
$dbr->selectField(
868 [
'tl_from' =>
$title->getArticleID() ],
883 return $this->
msg(
'pageinfo-title', $this->
getTitle()->getPrefixedText() )->text();
895 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
897 # Sift for real versus user names
900 $page = $user->isAnon()
902 : $user->getUserPage();
904 $hiddenPrefs = $this->context->getConfig()->get(
'HiddenPrefs' );
905 if ( $user->getId() == 0 ) {
906 $anon_ips[] = $linkRenderer->makeLink(
$page, $user->getName() );
907 } elseif ( !in_array(
'realname', $hiddenPrefs ) && $user->getRealName() ) {
908 $real_names[] = $linkRenderer->makeLink(
$page, $user->getRealName() );
910 $user_names[] = $linkRenderer->makeLink(
$page, $user->getName() );
916 $real =
$lang->listToText( $real_names );
918 # "ThisSite user(s) A, B and C"
919 if ( count( $user_names ) ) {
920 $user = $this->
msg(
'siteusers' )
921 ->rawParams(
$lang->listToText( $user_names ) )
922 ->params( count( $user_names ) )->escaped();
927 if ( count( $anon_ips ) ) {
928 $anon = $this->
msg(
'anonusers' )
929 ->rawParams(
$lang->listToText( $anon_ips ) )
930 ->params( count( $anon_ips ) )->escaped();
935 # This is the big list, all mooshed together. We sift for blank strings
937 foreach ( [ $real, $user, $anon ] as
$s ) {
939 array_push( $fulllist,
$s );
943 $count = count( $fulllist );
945 # "Based on work by ..."
947 ? $this->
msg(
'othercontribs' )->rawParams(
948 $lang->listToText( $fulllist ) )->params( $count )->escaped()
968 return $cache->makeKey(
'infoaction', md5(
$title->getPrefixedText() ), $revId, self::VERSION );