MediaWiki REL1_39
InfoAction.php
Go to the documentation of this file.
1<?php
40
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,
119 IContextSource $context,
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 ) ],
297 Html::element(
298 'span',
299 [
300 'class' => 'mw-headline',
301 'id' => Sanitizer::escapeIdForAttribute( $header ),
302 ],
303 $header
304 )
305 );
306 }
307
317 protected function addRow( $table, $name, $value, $id ) {
318 return $table .
319 Html::rawElement(
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 .
336 Html::rawElement(
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 $setOpts += Database::getCacheSetOptions( $dbr );
942
943 $field = 'rev_actor';
944 $pageField = 'rev_page';
945
946 $watchedItemStore = $this->watchedItemStore;
947
948 $result = [];
949 $result['watchers'] = $watchedItemStore->countWatchers( $title );
950
951 if ( $config->get( MainConfigNames::ShowUpdatedMarker ) ) {
952 $updated = (int)wfTimestamp( TS_UNIX, $page->getTimestamp() );
953 $result['visitingWatchers'] = $watchedItemStore->countVisitingWatchers(
954 $title,
955 $updated - $config->get( MainConfigNames::WatchersMaxAge )
956 );
957 }
958
959 // Total number of edits
960 $edits = (int)$dbr->newSelectQueryBuilder()
961 ->select( 'COUNT(*)' )
962 ->from( 'revision' )
963 ->where( [ 'rev_page' => $id ] )
964 ->caller( $fname )
965 ->fetchField();
966 $result['edits'] = $edits;
967
968 // Total number of distinct authors
969 if ( $config->get( MainConfigNames::MiserMode ) ) {
970 $result['authors'] = 0;
971 } else {
972 $result['authors'] = (int)$dbr->newSelectQueryBuilder()
973 ->select( "COUNT(DISTINCT $field)" )
974 ->from( 'revision' )
975 ->where( [ $pageField => $id ] )
976 ->caller( $fname )
977 ->fetchField();
978 }
979
980 // "Recent" threshold defined by RCMaxAge setting
981 $threshold = $dbr->timestamp( time() - $config->get( MainConfigNames::RCMaxAge ) );
982
983 // Recent number of edits
984 $edits = (int)$dbr->newSelectQueryBuilder()
985 ->select( 'COUNT(rev_page)' )
986 ->from( 'revision' )
987 ->where( [ 'rev_page' => $id ] )
988 ->andWhere( [ "rev_timestamp >= " . $dbr->addQuotes( $threshold ) ] )
989 ->caller( $fname )
990 ->fetchField();
991 $result['recent_edits'] = $edits;
992
993 // Recent number of distinct authors
994 $result['recent_authors'] = (int)$dbr->newSelectQueryBuilder()
995 ->select( "COUNT(DISTINCT $field)" )
996 ->from( 'revision' )
997 ->where( [ $pageField => $id ] )
998 ->andWhere( [ 'rev_timestamp >= ' . $dbr->addQuotes( $threshold ) ] )
999 ->caller( $fname )
1000 ->fetchField();
1001
1002 // Subpages (if enabled)
1003 if ( $this->namespaceInfo->hasSubpages( $title->getNamespace() ) ) {
1004 $conds = [ 'page_namespace' => $title->getNamespace() ];
1005 $conds[] = 'page_title ' .
1006 $dbr->buildLike( $title->getDBkey() . '/', $dbr->anyString() );
1007
1008 // Subpages of this page (redirects)
1009 $conds['page_is_redirect'] = 1;
1010 $result['subpages']['redirects'] = (int)$dbr->newSelectQueryBuilder()
1011 ->select( 'COUNT(page_id)' )
1012 ->from( 'page' )
1013 ->where( $conds )
1014 ->caller( $fname )
1015 ->fetchField();
1016 // Subpages of this page (non-redirects)
1017 $conds['page_is_redirect'] = 0;
1018 $result['subpages']['nonredirects'] = (int)$dbr->newSelectQueryBuilder()
1019 ->select( 'COUNT(page_id)' )
1020 ->from( 'page' )
1021 ->where( $conds )
1022 ->caller( $fname )
1023 ->fetchField();
1024
1025 // Subpages of this page (total)
1026 $result['subpages']['total'] = $result['subpages']['redirects']
1027 + $result['subpages']['nonredirects'];
1028 }
1029
1030 // Counts for the number of transclusion links (to/from)
1031 if ( $config->get( MainConfigNames::MiserMode ) ) {
1032 $result['transclusion']['to'] = 0;
1033 } else {
1034 $result['transclusion']['to'] = (int)$dbr->newSelectQueryBuilder()
1035 ->select( 'COUNT(tl_from)' )
1036 ->from( 'templatelinks' )
1037 ->where( $this->linksMigration->getLinksConditions( 'templatelinks', $title ) )
1038 ->caller( $fname )
1039 ->fetchField();
1040 }
1041
1042 $result['transclusion']['from'] = (int)$dbr->newSelectQueryBuilder()
1043 ->select( 'COUNT(*)' )
1044 ->from( 'templatelinks' )
1045 ->where( [ 'tl_from' => $title->getArticleID() ] )
1046 ->caller( $fname )
1047 ->fetchField();
1048
1049 return $result;
1050 }
1051 );
1052 }
1053
1059 protected function getPageTitle() {
1060 return $this->msg( 'pageinfo-title', $this->getTitle()->getPrefixedText() )->text();
1061 }
1062
1068 protected function getDescription() {
1069 return '';
1070 }
1071
1078 protected static function getCacheKey( WANObjectCache $cache, PageIdentity $page, $revId ) {
1079 return $cache->makeKey( 'infoaction', md5( (string)$page ), $revId, self::VERSION );
1080 }
1081}
getUser()
getAuthority()
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
getContext()
getWikiPage()
Get a WikiPage object.
Definition Action.php:209
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition Action.php:456
getOutput()
Get the OutputPage being used for this instance.
Definition Action.php:160
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
An action which just does something, without showing a form first.
Displays information about a page.
requiresWrite()
Whether this action requires the wiki not to be locked.
makeHeader( $header, $canonicalId)
Creates a header that can be added to the output.
__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)
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.
static getCacheKey(WANObjectCache $cache, PageIdentity $page, $revId)
onView()
Shows page information on GET request.
getNamespaceProtectionMessage(Title $title)
Get namespace protection message for title or null if no namespace protection has been applied.
getDescription()
Returns the description that goes below the "<h1>" tag.
requiresUnblock()
Whether this action can still be executed by a blocked user.
getName()
Returns the name of the action this object responds to.
addRow( $table, $name, $value, $id)
Adds a row to a table that will be added to the content.
addTable( $content, $table)
Adds a table to the content that will be added to the output.
static invalidateCache(PageIdentity $page, $revid=null)
Clear the info cache for a given Title.
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:2035
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:1371
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...
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:140
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 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
static newFromName( $name, $validate='valid')
Definition User.php:598
Multi-datacenter aware caching interface.
static getCacheSetOptions(?IDatabase ... $dbs)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
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