215 $services = MediaWikiServices::getInstance();
220 $id = $title->getArticleID();
221 $config = $this->context->getConfig();
224 $pageCounts = $this->
pageCounts( $this->page );
227 $pageProperties = $props[$id] ?? [];
231 $pageInfo[
'header-basic'] = [];
234 $displayTitle = $pageProperties[
'displaytitle'] ?? $title->getPrefixedText();
236 $pageInfo[
'header-basic'][] = [
237 $this->
msg(
'pageinfo-display-title' ), $displayTitle
241 $redirectTarget = $this->page->getRedirectTarget();
242 if ( $redirectTarget !==
null ) {
243 $pageInfo[
'header-basic'][] = [
244 $this->
msg(
'pageinfo-redirectsto' ),
246 $this->
msg(
'word-separator' )->escaped() .
249 $this->msg(
'pageinfo-redirectsto-info' )->text(),
251 [
'action' =>
'info' ]
257 $sortKey = $pageProperties[
'defaultsort'] ?? $title->getCategorySortkey();
259 $sortKey = htmlspecialchars( $sortKey );
260 $pageInfo[
'header-basic'][] = [ $this->
msg(
'pageinfo-default-sort' ), $sortKey ];
263 $pageInfo[
'header-basic'][] = [
264 $this->
msg(
'pageinfo-length' ),
$lang->formatNum( $title->getLength() )
268 $pageInfo[
'header-basic'][] = [ $this->
msg(
'pageinfo-article-id' ), $id ];
271 $pageLang = $title->getPageLanguage()->getCode();
273 $pageLangHtml = $pageLang .
' - ' .
274 Language::fetchLanguageName( $pageLang,
$lang->getCode() );
276 if ( $config->get(
'PageLanguageUseDB' )
277 && $title->userCan(
'pagelang', $user )
279 $pageLangHtml .=
' ' . $this->
msg(
'parentheses' )->rawParams(
$linkRenderer->makeLink(
280 SpecialPage::getTitleValueFor(
'PageLanguage', $title->getPrefixedText() ),
281 $this->msg(
'pageinfo-language-change' )->text()
285 $pageInfo[
'header-basic'][] = [
286 $this->
msg(
'pageinfo-language' )->escaped(),
291 $modelHtml = htmlspecialchars( ContentHandler::getLocalizedName( $title->getContentModel() ) );
293 if ( $config->get(
'ContentHandlerUseDB' )
294 && $title->userCan(
'editcontentmodel', $user )
296 $modelHtml .=
' ' . $this->
msg(
'parentheses' )->rawParams(
$linkRenderer->makeLink(
297 SpecialPage::getTitleValueFor(
'ChangeContentModel', $title->getPrefixedText() ),
298 $this->msg(
'pageinfo-content-model-change' )->text()
302 $pageInfo[
'header-basic'][] = [
303 $this->
msg(
'pageinfo-content-model' ),
307 if ( $title->inNamespace( NS_USER ) ) {
309 if ( $pageUser && $pageUser->getId() && !$pageUser->isHidden() ) {
310 $pageInfo[
'header-basic'][] = [
311 $this->
msg(
'pageinfo-user-id' ),
319 if ( isset( $pageProperties[
'noindex'] ) ) {
320 $pOutput->setIndexPolicy(
'noindex' );
322 if ( isset( $pageProperties[
'index'] ) ) {
323 $pOutput->setIndexPolicy(
'index' );
327 $policy = $this->page->getRobotPolicy(
'view', $pOutput );
328 $pageInfo[
'header-basic'][] = [
330 $this->
msg(
'pageinfo-robot-policy' ),
331 $this->
msg(
"pageinfo-robot-${policy['index']}" )
334 $unwatchedPageThreshold = $config->get(
'UnwatchedPageThreshold' );
336 $user->isAllowed(
'unwatchedpages' ) ||
337 ( $unwatchedPageThreshold !==
false &&
338 $pageCounts[
'watchers'] >= $unwatchedPageThreshold )
341 $pageInfo[
'header-basic'][] = [
342 $this->
msg(
'pageinfo-watchers' ),
343 $lang->formatNum( $pageCounts[
'watchers'] )
346 $config->get(
'ShowUpdatedMarker' ) &&
347 isset( $pageCounts[
'visitingWatchers'] )
349 $minToDisclose = $config->get(
'UnwatchedPageSecret' );
350 if ( $pageCounts[
'visitingWatchers'] > $minToDisclose ||
351 $user->isAllowed(
'unwatchedpages' ) ) {
352 $pageInfo[
'header-basic'][] = [
353 $this->
msg(
'pageinfo-visiting-watchers' ),
354 $lang->formatNum( $pageCounts[
'visitingWatchers'] )
357 $pageInfo[
'header-basic'][] = [
358 $this->
msg(
'pageinfo-visiting-watchers' ),
359 $this->
msg(
'pageinfo-few-visiting-watchers' )
363 } elseif ( $unwatchedPageThreshold !==
false ) {
364 $pageInfo[
'header-basic'][] = [
365 $this->
msg(
'pageinfo-watchers' ),
366 $this->
msg(
'pageinfo-few-watchers' )->numParams( $unwatchedPageThreshold )
371 $whatLinksHere = SpecialPage::getTitleFor(
'Whatlinkshere', $title->getPrefixedText() );
372 $pageInfo[
'header-basic'][] = [
375 $this->
msg(
'pageinfo-redirects-name' )->
text(),
380 'hideimages' => $title->getNamespace() ==
NS_FILE
383 $this->
msg(
'pageinfo-redirects-value' )
384 ->numParams( count( $title->getRedirectsHere() ) )
388 if ( $this->page->isCountable() ) {
389 $pageInfo[
'header-basic'][] = [
390 $this->
msg(
'pageinfo-contentpage' ),
391 $this->
msg(
'pageinfo-contentpage-yes' )
396 if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) {
397 $prefixIndex = SpecialPage::getTitleFor(
398 'Prefixindex', $title->getPrefixedText() .
'/' );
399 $pageInfo[
'header-basic'][] = [
402 $this->
msg(
'pageinfo-subpages-name' )->
text()
404 $this->
msg(
'pageinfo-subpages-value' )
406 $pageCounts[
'subpages'][
'total'],
407 $pageCounts[
'subpages'][
'redirects'],
408 $pageCounts[
'subpages'][
'nonredirects'] )
413 $category = Category::newFromTitle( $title );
417 $allCount = (int)$category->getPageCount();
418 $subcatCount = (int)$category->getSubcatCount();
419 $fileCount = (int)$category->getFileCount();
420 $pagesCount = $allCount - $subcatCount - $fileCount;
422 $pageInfo[
'category-info'] = [
424 $this->
msg(
'pageinfo-category-total' ),
425 $lang->formatNum( $allCount )
428 $this->
msg(
'pageinfo-category-pages' ),
429 $lang->formatNum( $pagesCount )
432 $this->
msg(
'pageinfo-category-subcats' ),
433 $lang->formatNum( $subcatCount )
436 $this->
msg(
'pageinfo-category-files' ),
437 $lang->formatNum( $fileCount )
443 if ( $title->inNamespace(
NS_FILE ) ) {
445 if ( $fileObj !==
false ) {
447 $output = Wikimedia\base_convert( $fileObj->getSha1(), 36, 16, 40 );
448 $pageInfo[
'header-basic'][] = [
449 $this->
msg(
'pageinfo-file-hash' ),
456 $pageInfo[
'header-restrictions'] = [];
459 if ( $title->isCascadeProtected() ) {
461 $sources = $title->getCascadeProtectionSources()[0];
463 foreach ( $sources as $sourceTitle ) {
464 $cascadingFrom .= Html::rawElement(
468 $cascadingFrom = Html::rawElement(
'ul', [], $cascadingFrom );
469 $pageInfo[
'header-restrictions'][] = [
470 $this->
msg(
'pageinfo-protect-cascading-from' ),
476 if ( $title->areRestrictionsCascading() ) {
477 $pageInfo[
'header-restrictions'][] = [
478 $this->
msg(
'pageinfo-protect-cascading' ),
479 $this->
msg(
'pageinfo-protect-cascading-yes' )
484 foreach ( $title->getRestrictionTypes() as $restrictionType ) {
485 $protectionLevel = implode(
', ', $title->getRestrictions( $restrictionType ) );
487 if ( $protectionLevel ==
'' ) {
489 $message = $this->
msg(
'protect-default' )->escaped();
493 $message = $this->
msg(
"protect-level-$protectionLevel" );
494 if ( $message->isDisabled() ) {
496 $message = $this->
msg(
"protect-fallback", $protectionLevel )->parse();
498 $message = $message->escaped();
501 $expiry = $title->getRestrictionExpiry( $restrictionType );
502 $formattedexpiry = $this->
msg(
'parentheses',
503 $lang->formatExpiry( $expiry ) )->escaped();
504 $message .= $this->
msg(
'word-separator' )->escaped() . $formattedexpiry;
508 $pageInfo[
'header-restrictions'][] = [
509 $this->
msg(
"restriction-$restrictionType" ), $message
512 $protectLog = SpecialPage::getTitleFor(
'Log' );
513 $pageInfo[
'header-restrictions'][] = [
517 $this->
msg(
'pageinfo-view-protect-log' )->
text(),
519 [
'type' =>
'protect',
'page' => $title->getPrefixedText() ]
523 if ( !$this->page->exists() ) {
528 $pageInfo[
'header-edits'] = [];
530 $firstRev = $this->page->getOldestRevision();
531 $lastRev = $this->page->getRevision();
536 if ( $firstRevUser !==
'' ) {
537 $firstRevUserTitle = Title::makeTitle( NS_USER, $firstRevUser );
538 $batch->addObj( $firstRevUserTitle );
539 $batch->addObj( $firstRevUserTitle->getTalkPage() );
545 if ( $lastRevUser !==
'' ) {
546 $lastRevUserTitle = Title::makeTitle( NS_USER, $lastRevUser );
547 $batch->addObj( $lastRevUserTitle );
548 $batch->addObj( $lastRevUserTitle->getTalkPage() );
556 $pageInfo[
'header-edits'][] = [
557 $this->
msg(
'pageinfo-firstuser' ),
562 $pageInfo[
'header-edits'][] = [
563 $this->
msg(
'pageinfo-firsttime' ),
566 $lang->userTimeAndDate( $firstRev->getTimestamp(), $user ),
568 [
'oldid' => $firstRev->getId() ]
575 $pageInfo[
'header-edits'][] = [
576 $this->
msg(
'pageinfo-lastuser' ),
581 $pageInfo[
'header-edits'][] = [
582 $this->
msg(
'pageinfo-lasttime' ),
585 $lang->userTimeAndDate( $this->page->getTimestamp(), $user ),
587 [
'oldid' => $this->page->getLatest() ]
593 $pageInfo[
'header-edits'][] = [
594 $this->
msg(
'pageinfo-edits' ),
$lang->formatNum( $pageCounts[
'edits'] )
598 if ( $pageCounts[
'authors'] > 0 ) {
599 $pageInfo[
'header-edits'][] = [
600 $this->
msg(
'pageinfo-authors' ),
$lang->formatNum( $pageCounts[
'authors'] )
605 $pageInfo[
'header-edits'][] = [
606 $this->
msg(
'pageinfo-recent-edits',
607 $lang->formatDuration( $config->get(
'RCMaxAge' ) ) ),
608 $lang->formatNum( $pageCounts[
'recent_edits'] )
612 $pageInfo[
'header-edits'][] = [
613 $this->
msg(
'pageinfo-recent-authors' ),
614 $lang->formatNum( $pageCounts[
'recent_authors'] )
624 $localizedWords =
$services->getContentLanguage()->getMagicWords();
629 $listItems[] = Html::element(
'li', [], $localizedWords[
$property][1] );
633 $localizedList = Html::rawElement(
'ul', [], implode(
'', $listItems ) );
634 $hiddenCategories = $this->page->getHiddenCategories();
637 count( $listItems ) > 0 ||
638 count( $hiddenCategories ) > 0 ||
639 $pageCounts[
'transclusion'][
'from'] > 0 ||
640 $pageCounts[
'transclusion'][
'to'] > 0
642 $options = [
'LIMIT' => $config->get(
'PageInfoTransclusionLimit' ) ];
643 $transcludedTemplates = $title->getTemplateLinksFrom(
$options );
644 if ( $config->get(
'MiserMode' ) ) {
645 $transcludedTargets = [];
647 $transcludedTargets = $title->getTemplateLinksTo(
$options );
651 $pageInfo[
'header-properties'] = [];
654 if ( count( $listItems ) > 0 ) {
655 $pageInfo[
'header-properties'][] = [
656 $this->
msg(
'pageinfo-magic-words' )->numParams( count( $listItems ) ),
662 if ( count( $hiddenCategories ) > 0 ) {
663 $pageInfo[
'header-properties'][] = [
664 $this->
msg(
'pageinfo-hidden-categories' )
665 ->numParams( count( $hiddenCategories ) ),
671 if ( $pageCounts[
'transclusion'][
'from'] > 0 ) {
672 if ( $pageCounts[
'transclusion'][
'from'] > count( $transcludedTemplates ) ) {
673 $more = $this->
msg(
'morenotlisted' )->escaped();
683 $pageInfo[
'header-properties'][] = [
684 $this->
msg(
'pageinfo-templates' )
685 ->numParams( $pageCounts[
'transclusion'][
'from'] ),
686 $templateListFormatter->format( $transcludedTemplates,
false, $more )
690 if ( !$config->get(
'MiserMode' ) && $pageCounts[
'transclusion'][
'to'] > 0 ) {
691 if ( $pageCounts[
'transclusion'][
'to'] > count( $transcludedTargets ) ) {
694 $this->
msg(
'moredotdotdot' )->
text(),
696 [
'hidelinks' => 1,
'hideredirs' => 1 ]
707 $pageInfo[
'header-properties'][] = [
708 $this->
msg(
'pageinfo-transclusions' )
709 ->numParams( $pageCounts[
'transclusion'][
'to'] ),
710 $templateListFormatter->format( $transcludedTargets,
false, $more )
726 $config = $this->context->getConfig();
727 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
729 return $cache->getWithSetCallback(
730 self::getCacheKey(
$cache, $page->getTitle(), $page->getLatest() ),
731 WANObjectCache::TTL_WEEK,
732 function ( $oldValue, &$ttl, &$setOpts ) use ( $page, $config,
$fname ) {
735 $title = $page->getTitle();
736 $id = $title->getArticleID();
740 $setOpts += Database::getCacheSetOptions(
$dbr, $dbrWatchlist );
743 $tables = [
'revision_actor_temp' ];
744 $field =
'revactor_actor';
745 $pageField =
'revactor_page';
746 $tsField =
'revactor_timestamp';
750 $field =
'rev_user_text';
751 $pageField =
'rev_page';
752 $tsField =
'rev_timestamp';
756 $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
759 $result[
'watchers'] = $watchedItemStore->countWatchers( $title );
761 if ( $config->get(
'ShowUpdatedMarker' ) ) {
762 $updated =
wfTimestamp( TS_UNIX, $page->getTimestamp() );
763 $result[
'visitingWatchers'] = $watchedItemStore->countVisitingWatchers(
765 $updated - $config->get(
'WatchersMaxAge' )
770 $edits = (int)
$dbr->selectField(
773 [
'rev_page' => $id ],
776 $result[
'edits'] = $edits;
779 if ( $config->get(
'MiserMode' ) ) {
780 $result[
'authors'] = 0;
782 $result[
'authors'] = (int)
$dbr->selectField(
784 "COUNT(DISTINCT $field)",
785 [ $pageField => $id ],
793 $threshold =
$dbr->timestamp( time() - $config->get(
'RCMaxAge' ) );
796 $edits = (int)
$dbr->selectField(
801 "rev_timestamp >= " .
$dbr->addQuotes( $threshold )
805 $result[
'recent_edits'] = $edits;
808 $result[
'recent_authors'] = (int)
$dbr->selectField(
810 "COUNT(DISTINCT $field)",
813 "$tsField >= " .
$dbr->addQuotes( $threshold )
821 if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) {
822 $conds = [
'page_namespace' => $title->getNamespace() ];
823 $conds[] =
'page_title ' .
824 $dbr->buildLike( $title->getDBkey() .
'/',
$dbr->anyString() );
827 $conds[
'page_is_redirect'] = 1;
828 $result[
'subpages'][
'redirects'] = (int)
$dbr->selectField(
836 $conds[
'page_is_redirect'] = 0;
837 $result[
'subpages'][
'nonredirects'] = (int)
$dbr->selectField(
845 $result[
'subpages'][
'total'] = $result[
'subpages'][
'redirects']
846 + $result[
'subpages'][
'nonredirects'];
850 if ( $config->get(
'MiserMode' ) ) {
851 $result[
'transclusion'][
'to'] = 0;
853 $result[
'transclusion'][
'to'] = (int)
$dbr->selectField(
857 'tl_namespace' => $title->getNamespace(),
858 'tl_title' => $title->getDBkey()
864 $result[
'transclusion'][
'from'] = (int)
$dbr->selectField(
867 [
'tl_from' => $title->getArticleID() ],