MediaWiki REL1_37
InfoAction.php
Go to the documentation of this file.
1<?php
36
43 private const VERSION = 1;
44
47
49 private $hookRunner;
50
53
56
59
62
65
68
70 private $pageProps;
71
73 private $repoGroup;
74
77
80
83
101 public function __construct(
102 Page $page,
103 IContextSource $context,
104 Language $contentLanguage,
105 HookContainer $hookContainer,
106 LanguageNameUtils $languageNameUtils,
107 LinkBatchFactory $linkBatchFactory,
108 LinkRenderer $linkRenderer,
109 ILoadBalancer $loadBalancer,
110 MagicWordFactory $magicWordFactory,
111 NamespaceInfo $namespaceInfo,
112 PageProps $pageProps,
113 RepoGroup $repoGroup,
114 RevisionLookup $revisionLookup,
115 WANObjectCache $wanObjectCache,
116 WatchedItemStoreInterface $watchedItemStore
117 ) {
118 parent::__construct( $page, $context );
119 $this->contentLanguage = $contentLanguage;
120 $this->hookRunner = new HookRunner( $hookContainer );
121 $this->languageNameUtils = $languageNameUtils;
122 $this->linkBatchFactory = $linkBatchFactory;
123 $this->linkRenderer = $linkRenderer;
124 $this->loadBalancer = $loadBalancer;
125 $this->magicWordFactory = $magicWordFactory;
126 $this->namespaceInfo = $namespaceInfo;
127 $this->pageProps = $pageProps;
128 $this->repoGroup = $repoGroup;
129 $this->revisionLookup = $revisionLookup;
130 $this->wanObjectCache = $wanObjectCache;
131 $this->watchedItemStore = $watchedItemStore;
132 }
133
139 public function getName() {
140 return 'info';
141 }
142
148 public function requiresUnblock() {
149 return false;
150 }
151
157 public function requiresWrite() {
158 return false;
159 }
160
168 public static function invalidateCache( PageIdentity $page, $revid = null ) {
169 $services = MediaWikiServices::getInstance();
170 if ( !$revid ) {
171 $revision = $services->getRevisionLookup()
172 ->getRevisionByTitle( $page, 0, IDBAccessObject::READ_LATEST );
173 $revid = $revision ? $revision->getId() : null;
174 }
175 if ( $revid !== null ) {
176 $cache = $services->getMainWANObjectCache();
177 $key = self::getCacheKey( $cache, $page, $revid );
178 $cache->delete( $key );
179 }
180 }
181
187 public function onView() {
188 $this->getOutput()->addModuleStyles( 'mediawiki.interface.helpers.styles' );
189
190 // "Help" button
191 $this->addHelpLink( 'Page information' );
192
193 // Validate revision
194 $oldid = $this->getArticle()->getOldID();
195 if ( $oldid ) {
196 $revRecord = $this->getArticle()->fetchRevisionRecord();
197
198 // Revision is missing
199 if ( $revRecord === null ) {
200 return $this->msg( 'missing-revision', $oldid )->parse();
201 }
202
203 // Revision is not current
204 if ( !$revRecord->isCurrent() ) {
205 return $this->msg( 'pageinfo-not-current' )->plain();
206 }
207 }
208
209 $content = '';
210
211 // Page header
212 if ( !$this->msg( 'pageinfo-header' )->isDisabled() ) {
213 $content .= $this->msg( 'pageinfo-header' )->parse();
214 }
215
216 // TODO we shouldn't be adding styles manually like thes
217 // Hide "This page is a member of # hidden categories" explanation
218 $content .= Html::element(
219 'style',
220 [],
221 '.mw-hiddenCategoriesExplanation { display: none; }'
222 ) . "\n";
223
224 // Hide "Templates used on this page" explanation
225 $content .= Html::element(
226 'style',
227 [],
228 '.mw-templatesUsedExplanation { display: none; }'
229 ) . "\n";
230
231 // Get page information
232 $pageInfo = $this->pageInfo();
233
234 // Allow extensions to add additional information
235 $this->hookRunner->onInfoAction( $this->getContext(), $pageInfo );
236
237 // Render page information
238 foreach ( $pageInfo as $header => $infoTable ) {
239 // Messages:
240 // pageinfo-header-basic, pageinfo-header-edits, pageinfo-header-restrictions,
241 // pageinfo-header-properties, pageinfo-category-info
242 $content .= $this->makeHeader(
243 $this->msg( "pageinfo-$header" )->text(),
244 "mw-pageinfo-$header"
245 ) . "\n";
246 $table = "\n";
247 $below = "";
248 foreach ( $infoTable as $infoRow ) {
249 if ( $infoRow[0] == "below" ) {
250 $below = $infoRow[1] . "\n";
251 continue;
252 }
253 $name = ( $infoRow[0] instanceof Message ) ? $infoRow[0]->escaped() : $infoRow[0];
254 $value = ( $infoRow[1] instanceof Message ) ? $infoRow[1]->escaped() : $infoRow[1];
255 $id = ( $infoRow[0] instanceof Message ) ? $infoRow[0]->getKey() : null;
256 $table = $this->addRow( $table, $name, $value, $id ) . "\n";
257 }
258 if ( $table === "\n" ) {
259 // Don't add tables with no rows
260 $content .= "\n" . $below;
261 } else {
262 $content = $this->addTable( $content, $table ) . "\n" . $below;
263 }
264 }
265
266 // Page footer
267 if ( !$this->msg( 'pageinfo-footer' )->isDisabled() ) {
268 $content .= $this->msg( 'pageinfo-footer' )->parse();
269 }
270
271 return $content;
272 }
273
281 protected function makeHeader( $header, $canonicalId ) {
282 return Html::rawElement(
283 'h2',
284 [ 'id' => Sanitizer::escapeIdForAttribute( $canonicalId ) ],
285 Html::element(
286 'span',
287 [
288 'class' => 'mw-headline',
289 'id' => Sanitizer::escapeIdForAttribute( $header ),
290 ],
291 $header
292 )
293 );
294 }
295
305 protected function addRow( $table, $name, $value, $id ) {
306 return $table .
307 Html::rawElement(
308 'tr',
309 $id === null ? [] : [ 'id' => 'mw-' . $id ],
310 Html::rawElement( 'td', [ 'style' => 'vertical-align: top;' ], $name ) .
311 Html::rawElement( 'td', [], $value )
312 );
313 }
314
322 protected function addTable( $content, $table ) {
323 return $content .
324 Html::rawElement(
325 'table',
326 [ 'class' => 'wikitable mw-page-info' ],
327 $table
328 );
329 }
330
343 protected function pageInfo() {
344 $user = $this->getUser();
345 $lang = $this->getLanguage();
346 $title = $this->getTitle();
347 $id = $title->getArticleID();
348 $config = $this->context->getConfig();
349 $linkRenderer = $this->linkRenderer;
350
351 $pageCounts = $this->pageCounts();
352
353 $props = $this->pageProps->getAllProperties( $title );
354 $pageProperties = $props[$id] ?? [];
355
356 // Basic information
357 $pageInfo = [];
358 $pageInfo['header-basic'] = [];
359
360 // Display title
361 $displayTitle = $pageProperties['displaytitle'] ?? $title->getPrefixedText();
362
363 $pageInfo['header-basic'][] = [
364 $this->msg( 'pageinfo-display-title' ),
365 $displayTitle
366 ];
367
368 // Is it a redirect? If so, where to?
369 $redirectTarget = $this->getWikiPage()->getRedirectTarget();
370 if ( $redirectTarget !== null ) {
371 $pageInfo['header-basic'][] = [
372 $this->msg( 'pageinfo-redirectsto' ),
373 $linkRenderer->makeLink( $redirectTarget ) .
374 $this->msg( 'word-separator' )->escaped() .
375 $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
376 $redirectTarget,
377 $this->msg( 'pageinfo-redirectsto-info' )->text(),
378 [],
379 [ 'action' => 'info' ]
380 ) )->escaped()
381 ];
382 }
383
384 // Default sort key
385 $sortKey = $pageProperties['defaultsort'] ?? $title->getCategorySortkey();
386
387 $sortKey = htmlspecialchars( $sortKey );
388 $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-default-sort' ), $sortKey ];
389
390 // Page length (in bytes)
391 $pageInfo['header-basic'][] = [
392 $this->msg( 'pageinfo-length' ),
393 $lang->formatNum( $title->getLength() )
394 ];
395
396 // Page namespace
397 $pageNamespace = $title->getNsText();
398 if ( $pageNamespace ) {
399 $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-namespace' ), $pageNamespace ];
400 }
401
402 // Page ID (number not localised, as it's a database ID)
403 $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-article-id' ), $id ];
404
405 // Language in which the page content is (supposed to be) written
406 $pageLang = $title->getPageLanguage()->getCode();
407
408 $pageLangHtml = $pageLang . ' - ' .
409 $this->languageNameUtils->getLanguageName( $pageLang, $lang->getCode() );
410 // Link to Special:PageLanguage with pre-filled page title if user has permissions
411 if ( $config->get( 'PageLanguageUseDB' )
412 && $this->getContext()->getAuthority()->probablyCan( 'pagelang', $title )
413 ) {
414 $pageLangHtml .= ' ' . $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
415 SpecialPage::getTitleValueFor( 'PageLanguage', $title->getPrefixedText() ),
416 $this->msg( 'pageinfo-language-change' )->text()
417 ) )->escaped();
418 }
419
420 $pageInfo['header-basic'][] = [
421 $this->msg( 'pageinfo-language' )->escaped(),
422 $pageLangHtml
423 ];
424
425 // Content model of the page
426 $modelHtml = htmlspecialchars( ContentHandler::getLocalizedName( $title->getContentModel() ) );
427 // If the user can change it, add a link to Special:ChangeContentModel
428 if ( $this->getContext()->getAuthority()->probablyCan( 'editcontentmodel', $title ) ) {
429 $modelHtml .= ' ' . $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
430 SpecialPage::getTitleValueFor( 'ChangeContentModel', $title->getPrefixedText() ),
431 $this->msg( 'pageinfo-content-model-change' )->text()
432 ) )->escaped();
433 }
434
435 $pageInfo['header-basic'][] = [
436 $this->msg( 'pageinfo-content-model' ),
437 $modelHtml
438 ];
439
440 if ( $title->inNamespace( NS_USER ) ) {
441 $pageUser = User::newFromName( $title->getRootText() );
442 if ( $pageUser && $pageUser->getId() && !$pageUser->isHidden() ) {
443 $pageInfo['header-basic'][] = [
444 $this->msg( 'pageinfo-user-id' ),
445 $pageUser->getId()
446 ];
447 }
448 }
449
450 // Search engine status
451 $pOutput = new ParserOutput();
452 if ( isset( $pageProperties['noindex'] ) ) {
453 $pOutput->setIndexPolicy( 'noindex' );
454 }
455 if ( isset( $pageProperties['index'] ) ) {
456 $pOutput->setIndexPolicy( 'index' );
457 }
458
459 // Use robot policy logic
460 $policy = $this->getArticle()->getRobotPolicy( 'view', $pOutput );
461 $pageInfo['header-basic'][] = [
462 // Messages: pageinfo-robot-index, pageinfo-robot-noindex
463 $this->msg( 'pageinfo-robot-policy' ),
464 $this->msg( "pageinfo-robot-{$policy['index']}" )
465 ];
466
467 $unwatchedPageThreshold = $config->get( 'UnwatchedPageThreshold' );
468 if ( $this->getContext()->getAuthority()->isAllowed( 'unwatchedpages' ) ||
469 ( $unwatchedPageThreshold !== false &&
470 $pageCounts['watchers'] >= $unwatchedPageThreshold )
471 ) {
472 // Number of page watchers
473 $pageInfo['header-basic'][] = [
474 $this->msg( 'pageinfo-watchers' ),
475 $lang->formatNum( $pageCounts['watchers'] )
476 ];
477 if (
478 $config->get( 'ShowUpdatedMarker' ) &&
479 isset( $pageCounts['visitingWatchers'] )
480 ) {
481 $minToDisclose = $config->get( 'UnwatchedPageSecret' );
482 if ( $pageCounts['visitingWatchers'] > $minToDisclose ||
483 $this->getContext()->getAuthority()->isAllowed( 'unwatchedpages' ) ) {
484 $pageInfo['header-basic'][] = [
485 $this->msg( 'pageinfo-visiting-watchers' ),
486 $lang->formatNum( $pageCounts['visitingWatchers'] )
487 ];
488 } else {
489 $pageInfo['header-basic'][] = [
490 $this->msg( 'pageinfo-visiting-watchers' ),
491 $this->msg( 'pageinfo-few-visiting-watchers' )
492 ];
493 }
494 }
495 } elseif ( $unwatchedPageThreshold !== false ) {
496 $pageInfo['header-basic'][] = [
497 $this->msg( 'pageinfo-watchers' ),
498 $this->msg( 'pageinfo-few-watchers' )->numParams( $unwatchedPageThreshold )
499 ];
500 }
501
502 // Redirects to this page
503 $whatLinksHere = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
504 $pageInfo['header-basic'][] = [
505 $linkRenderer->makeLink(
506 $whatLinksHere,
507 $this->msg( 'pageinfo-redirects-name' )->text(),
508 [],
509 [
510 'hidelinks' => 1,
511 'hidetrans' => 1,
512 'hideimages' => $title->getNamespace() === NS_FILE
513 ]
514 ),
515 $this->msg( 'pageinfo-redirects-value' )
516 ->numParams( count( $title->getRedirectsHere() ) )
517 ];
518
519 // Is it counted as a content page?
520 if ( $this->getWikiPage()->isCountable() ) {
521 $pageInfo['header-basic'][] = [
522 $this->msg( 'pageinfo-contentpage' ),
523 $this->msg( 'pageinfo-contentpage-yes' )
524 ];
525 }
526
527 // Subpages of this page, if subpages are enabled for the current NS
528 if ( $this->namespaceInfo->hasSubpages( $title->getNamespace() ) ) {
529 $prefixIndex = SpecialPage::getTitleFor(
530 'Prefixindex',
531 $title->getPrefixedText() . '/'
532 );
533 $pageInfo['header-basic'][] = [
534 $linkRenderer->makeLink(
535 $prefixIndex,
536 $this->msg( 'pageinfo-subpages-name' )->text()
537 ),
538 $this->msg( 'pageinfo-subpages-value' )
539 ->numParams(
540 $pageCounts['subpages']['total'],
541 $pageCounts['subpages']['redirects'],
542 $pageCounts['subpages']['nonredirects']
543 )
544 ];
545 }
546
547 if ( $title->inNamespace( NS_CATEGORY ) ) {
548 $category = Category::newFromTitle( $title );
549
550 // $allCount is the total number of cat members,
551 // not the count of how many members are normal pages.
552 $allCount = (int)$category->getPageCount();
553 $subcatCount = (int)$category->getSubcatCount();
554 $fileCount = (int)$category->getFileCount();
555 $pagesCount = $allCount - $subcatCount - $fileCount;
556
557 $pageInfo['category-info'] = [
558 [
559 $this->msg( 'pageinfo-category-total' ),
560 $lang->formatNum( $allCount )
561 ],
562 [
563 $this->msg( 'pageinfo-category-pages' ),
564 $lang->formatNum( $pagesCount )
565 ],
566 [
567 $this->msg( 'pageinfo-category-subcats' ),
568 $lang->formatNum( $subcatCount )
569 ],
570 [
571 $this->msg( 'pageinfo-category-files' ),
572 $lang->formatNum( $fileCount )
573 ]
574 ];
575 }
576
577 // Display image SHA-1 value
578 if ( $title->inNamespace( NS_FILE ) ) {
579 $fileObj = $this->repoGroup->findFile( $title );
580 if ( $fileObj !== false ) {
581 // Convert the base-36 sha1 value obtained from database to base-16
582 $output = Wikimedia\base_convert( $fileObj->getSha1(), 36, 16, 40 );
583 $pageInfo['header-basic'][] = [
584 $this->msg( 'pageinfo-file-hash' ),
585 $output
586 ];
587 }
588 }
589
590 // Page protection
591 $pageInfo['header-restrictions'] = [];
592
593 // Is this page affected by the cascading protection of something which includes it?
594 if ( $title->isCascadeProtected() ) {
595 $cascadingFrom = '';
596 $sources = $title->getCascadeProtectionSources()[0];
597
598 foreach ( $sources as $sourceTitle ) {
599 $cascadingFrom .= Html::rawElement(
600 'li',
601 [],
602 $linkRenderer->makeKnownLink( $sourceTitle )
603 );
604 }
605
606 $cascadingFrom = Html::rawElement( 'ul', [], $cascadingFrom );
607 $pageInfo['header-restrictions'][] = [
608 $this->msg( 'pageinfo-protect-cascading-from' ),
609 $cascadingFrom
610 ];
611 }
612
613 // Is out protection set to cascade to other pages?
614 if ( $title->areRestrictionsCascading() ) {
615 $pageInfo['header-restrictions'][] = [
616 $this->msg( 'pageinfo-protect-cascading' ),
617 $this->msg( 'pageinfo-protect-cascading-yes' )
618 ];
619 }
620
621 // Page protection
622 foreach ( $title->getRestrictionTypes() as $restrictionType ) {
623 $protections = $title->getRestrictions( $restrictionType );
624
625 switch ( count( $protections ) ) {
626 case 0:
627 $message = $this->getNamespaceProtectionMessage( $title );
628 if ( $message === null ) {
629 // Allow all users
630 $message = $this->msg( 'protect-default' )->escaped();
631 }
632 break;
633
634 case 1:
635 // Messages: protect-level-autoconfirmed, protect-level-sysop
636 $message = $this->msg( 'protect-level-' . $protections[0] );
637 if ( !$message->isDisabled() ) {
638 $message = $message->escaped();
639 break;
640 }
641 // Intentional fall-through if message is disabled (or non-existent)
642
643 default:
644 // Require "$1" permission
645 $message = $this->msg( "protect-fallback", $lang->commaList( $protections ) )->parse();
646 break;
647 }
648 $expiry = $title->getRestrictionExpiry( $restrictionType );
649 $formattedexpiry = $this->msg(
650 'parentheses',
651 $lang->formatExpiry( $expiry, true, 'infinity', $user )
652 )->escaped();
653 $message .= $this->msg( 'word-separator' )->escaped() . $formattedexpiry;
654
655 // Messages: restriction-edit, restriction-move, restriction-create,
656 // restriction-upload
657 $pageInfo['header-restrictions'][] = [
658 $this->msg( "restriction-$restrictionType" ), $message
659 ];
660 }
661 $protectLog = SpecialPage::getTitleFor( 'Log' );
662 $pageInfo['header-restrictions'][] = [
663 'below',
664 $linkRenderer->makeKnownLink(
665 $protectLog,
666 $this->msg( 'pageinfo-view-protect-log' )->text(),
667 [],
668 [ 'type' => 'protect', 'page' => $title->getPrefixedText() ]
669 ),
670 ];
671
672 if ( !$this->getWikiPage()->exists() ) {
673 return $pageInfo;
674 }
675
676 // Edit history
677 $pageInfo['header-edits'] = [];
678
679 $firstRev = $this->revisionLookup->getFirstRevision( $this->getTitle() );
680 $lastRev = $this->getWikiPage()->getRevisionRecord();
681 $batch = $this->linkBatchFactory->newLinkBatch();
682 if ( $firstRev ) {
683 $firstRevUser = $firstRev->getUser( RevisionRecord::FOR_THIS_USER, $user );
684 if ( $firstRevUser ) {
685 $batch->add( NS_USER, $firstRevUser->getName() );
686 $batch->add( NS_USER_TALK, $firstRevUser->getName() );
687 }
688 }
689
690 if ( $lastRev ) {
691 $lastRevUser = $lastRev->getUser( RevisionRecord::FOR_THIS_USER, $user );
692 if ( $lastRevUser ) {
693 $batch->add( NS_USER, $lastRevUser->getName() );
694 $batch->add( NS_USER_TALK, $lastRevUser->getName() );
695 }
696 }
697
698 $batch->execute();
699
700 if ( $firstRev ) {
701 // Page creator
702 $pageInfo['header-edits'][] = [
703 $this->msg( 'pageinfo-firstuser' ),
704 Linker::revUserTools( $firstRev )
705 ];
706
707 // Date of page creation
708 $pageInfo['header-edits'][] = [
709 $this->msg( 'pageinfo-firsttime' ),
710 $linkRenderer->makeKnownLink(
711 $title,
712 $lang->userTimeAndDate( $firstRev->getTimestamp(), $user ),
713 [],
714 [ 'oldid' => $firstRev->getId() ]
715 )
716 ];
717 }
718
719 if ( $lastRev ) {
720 // Latest editor
721 $pageInfo['header-edits'][] = [
722 $this->msg( 'pageinfo-lastuser' ),
723 Linker::revUserTools( $lastRev )
724 ];
725
726 // Date of latest edit
727 $pageInfo['header-edits'][] = [
728 $this->msg( 'pageinfo-lasttime' ),
729 $linkRenderer->makeKnownLink(
730 $title,
731 $lang->userTimeAndDate( $this->getWikiPage()->getTimestamp(), $user ),
732 [],
733 [ 'oldid' => $this->getWikiPage()->getLatest() ]
734 )
735 ];
736 }
737
738 // Total number of edits
739 $pageInfo['header-edits'][] = [
740 $this->msg( 'pageinfo-edits' ),
741 $lang->formatNum( $pageCounts['edits'] )
742 ];
743
744 // Total number of distinct authors
745 if ( $pageCounts['authors'] > 0 ) {
746 $pageInfo['header-edits'][] = [
747 $this->msg( 'pageinfo-authors' ),
748 $lang->formatNum( $pageCounts['authors'] )
749 ];
750 }
751
752 // Recent number of edits (within past 30 days)
753 $pageInfo['header-edits'][] = [
754 $this->msg(
755 'pageinfo-recent-edits',
756 $lang->formatDuration( $config->get( 'RCMaxAge' ) )
757 ),
758 $lang->formatNum( $pageCounts['recent_edits'] )
759 ];
760
761 // Recent number of distinct authors
762 $pageInfo['header-edits'][] = [
763 $this->msg( 'pageinfo-recent-authors' ),
764 $lang->formatNum( $pageCounts['recent_authors'] )
765 ];
766
767 // Array of MagicWord objects
768 $magicWords = $this->magicWordFactory->getDoubleUnderscoreArray();
769
770 // Array of magic word IDs
771 $wordIDs = $magicWords->names;
772
773 // Array of IDs => localized magic words
774 $localizedWords = $this->contentLanguage->getMagicWords();
775
776 $listItems = [];
777 foreach ( $pageProperties as $property => $value ) {
778 if ( in_array( $property, $wordIDs ) ) {
779 $listItems[] = Html::element( 'li', [], $localizedWords[$property][1] );
780 }
781 }
782
783 $localizedList = Html::rawElement( 'ul', [], implode( '', $listItems ) );
784 $hiddenCategories = $this->getWikiPage()->getHiddenCategories();
785
786 if (
787 count( $listItems ) > 0 ||
788 count( $hiddenCategories ) > 0 ||
789 $pageCounts['transclusion']['from'] > 0 ||
790 $pageCounts['transclusion']['to'] > 0
791 ) {
792 $options = [ 'LIMIT' => $config->get( 'PageInfoTransclusionLimit' ) ];
793 $transcludedTemplates = $title->getTemplateLinksFrom( $options );
794 if ( $config->get( 'MiserMode' ) ) {
795 $transcludedTargets = [];
796 } else {
797 $transcludedTargets = $title->getTemplateLinksTo( $options );
798 }
799
800 // Page properties
801 $pageInfo['header-properties'] = [];
802
803 // Magic words
804 if ( count( $listItems ) > 0 ) {
805 $pageInfo['header-properties'][] = [
806 $this->msg( 'pageinfo-magic-words' )->numParams( count( $listItems ) ),
807 $localizedList
808 ];
809 }
810
811 // Hidden categories
812 if ( count( $hiddenCategories ) > 0 ) {
813 $pageInfo['header-properties'][] = [
814 $this->msg( 'pageinfo-hidden-categories' )
815 ->numParams( count( $hiddenCategories ) ),
816 Linker::formatHiddenCategories( $hiddenCategories )
817 ];
818 }
819
820 // Transcluded templates
821 if ( $pageCounts['transclusion']['from'] > 0 ) {
822 if ( $pageCounts['transclusion']['from'] > count( $transcludedTemplates ) ) {
823 $more = $this->msg( 'morenotlisted' )->escaped();
824 } else {
825 $more = null;
826 }
827
828 $templateListFormatter = new TemplatesOnThisPageFormatter(
829 $this->getContext(),
830 $linkRenderer
831 );
832
833 $pageInfo['header-properties'][] = [
834 $this->msg( 'pageinfo-templates' )
835 ->numParams( $pageCounts['transclusion']['from'] ),
836 $templateListFormatter->format( $transcludedTemplates, false, $more )
837 ];
838 }
839
840 if ( !$config->get( 'MiserMode' ) && $pageCounts['transclusion']['to'] > 0 ) {
841 if ( $pageCounts['transclusion']['to'] > count( $transcludedTargets ) ) {
842 $more = $linkRenderer->makeLink(
843 $whatLinksHere,
844 $this->msg( 'moredotdotdot' )->text(),
845 [],
846 [ 'hidelinks' => 1, 'hideredirs' => 1 ]
847 );
848 } else {
849 $more = null;
850 }
851
852 $templateListFormatter = new TemplatesOnThisPageFormatter(
853 $this->getContext(),
854 $linkRenderer
855 );
856
857 $pageInfo['header-properties'][] = [
858 $this->msg( 'pageinfo-transclusions' )
859 ->numParams( $pageCounts['transclusion']['to'] ),
860 $templateListFormatter->format( $transcludedTargets, false, $more )
861 ];
862 }
863 }
864
865 return $pageInfo;
866 }
867
875 protected function getNamespaceProtectionMessage( Title $title ): ?string {
876 $rights = [];
877 if ( $title->isRawHtmlMessage() ) {
878 $rights[] = 'editsitecss';
879 $rights[] = 'editsitejs';
880 } elseif ( $title->isSiteCssConfigPage() ) {
881 $rights[] = 'editsitecss';
882 } elseif ( $title->isSiteJsConfigPage() ) {
883 $rights[] = 'editsitejs';
884 } elseif ( $title->isSiteJsonConfigPage() ) {
885 $rights[] = 'editsitejson';
886 } elseif ( $title->isUserCssConfigPage() ) {
887 $rights[] = 'editusercss';
888 } elseif ( $title->isUserJsConfigPage() ) {
889 $rights[] = 'edituserjs';
890 } elseif ( $title->isUserJsonConfigPage() ) {
891 $rights[] = 'edituserjson';
892 } else {
893 $namespaceProtection = $this->context->getConfig()->get( 'NamespaceProtection' );
894 $right = $namespaceProtection[$title->getNamespace()] ?? null;
895 if ( $right ) {
896 // a single string as the value is allowed as well as an array
897 $rights = (array)$right;
898 }
899 }
900 if ( $rights ) {
901 return $this->msg( 'protect-fallback', $this->getLanguage()->commaList( $rights ) )->parse();
902 } else {
903 return null;
904 }
905 }
906
912 private function pageCounts() {
913 $page = $this->getWikiPage();
914 $fname = __METHOD__;
915 $config = $this->context->getConfig();
916 $cache = $this->wanObjectCache;
917
918 return $cache->getWithSetCallback(
919 self::getCacheKey( $cache, $page->getTitle(), $page->getLatest() ),
920 WANObjectCache::TTL_WEEK,
921 function ( $oldValue, &$ttl, &$setOpts ) use ( $page, $config, $fname ) {
923
924 $title = $page->getTitle();
925 $id = $title->getArticleID();
926
927 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
928 $dbrWatchlist = $this->loadBalancer->getConnectionRef(
930 [ 'watchlist' ]
931 );
932 $setOpts += Database::getCacheSetOptions( $dbr, $dbrWatchlist );
933
935 $tables = [ 'revision' ];
936 $field = 'rev_actor';
937 $pageField = 'rev_page';
938 $tsField = 'rev_timestamp';
939 } else /* SCHEMA_COMPAT_READ_TEMP */ {
940 $tables = [ 'revision_actor_temp' ];
941 $field = 'revactor_actor';
942 $pageField = 'revactor_page';
943 $tsField = 'revactor_timestamp';
944 }
945 $joins = [];
946
947 $watchedItemStore = $this->watchedItemStore;
948
949 $result = [];
950 $result['watchers'] = $watchedItemStore->countWatchers( $title );
951
952 if ( $config->get( 'ShowUpdatedMarker' ) ) {
953 $updated = (int)wfTimestamp( TS_UNIX, $page->getTimestamp() );
954 $result['visitingWatchers'] = $watchedItemStore->countVisitingWatchers(
955 $title,
956 $updated - $config->get( 'WatchersMaxAge' )
957 );
958 }
959
960 // Total number of edits
961 $edits = (int)$dbr->selectField(
962 'revision',
963 'COUNT(*)',
964 [ 'rev_page' => $id ],
965 $fname
966 );
967 $result['edits'] = $edits;
968
969 // Total number of distinct authors
970 if ( $config->get( 'MiserMode' ) ) {
971 $result['authors'] = 0;
972 } else {
973 $result['authors'] = (int)$dbr->selectField(
974 $tables,
975 "COUNT(DISTINCT $field)",
976 [ $pageField => $id ],
977 $fname,
978 [],
979 $joins
980 );
981 }
982
983 // "Recent" threshold defined by RCMaxAge setting
984 $threshold = $dbr->timestamp( time() - $config->get( 'RCMaxAge' ) );
985
986 // Recent number of edits
987 $edits = (int)$dbr->selectField(
988 'revision',
989 'COUNT(rev_page)',
990 [
991 'rev_page' => $id,
992 "rev_timestamp >= " . $dbr->addQuotes( $threshold )
993 ],
994 $fname
995 );
996 $result['recent_edits'] = $edits;
997
998 // Recent number of distinct authors
999 $result['recent_authors'] = (int)$dbr->selectField(
1000 $tables,
1001 "COUNT(DISTINCT $field)",
1002 [
1003 $pageField => $id,
1004 "$tsField >= " . $dbr->addQuotes( $threshold )
1005 ],
1006 $fname,
1007 [],
1008 $joins
1009 );
1010
1011 // Subpages (if enabled)
1012 if ( $this->namespaceInfo->hasSubpages( $title->getNamespace() ) ) {
1013 $conds = [ 'page_namespace' => $title->getNamespace() ];
1014 $conds[] = 'page_title ' .
1015 $dbr->buildLike( $title->getDBkey() . '/', $dbr->anyString() );
1016
1017 // Subpages of this page (redirects)
1018 $conds['page_is_redirect'] = 1;
1019 $result['subpages']['redirects'] = (int)$dbr->selectField(
1020 'page',
1021 'COUNT(page_id)',
1022 $conds,
1023 $fname
1024 );
1025
1026 // Subpages of this page (non-redirects)
1027 $conds['page_is_redirect'] = 0;
1028 $result['subpages']['nonredirects'] = (int)$dbr->selectField(
1029 'page',
1030 'COUNT(page_id)',
1031 $conds,
1032 $fname
1033 );
1034
1035 // Subpages of this page (total)
1036 $result['subpages']['total'] = $result['subpages']['redirects']
1037 + $result['subpages']['nonredirects'];
1038 }
1039
1040 // Counts for the number of transclusion links (to/from)
1041 if ( $config->get( 'MiserMode' ) ) {
1042 $result['transclusion']['to'] = 0;
1043 } else {
1044 $result['transclusion']['to'] = (int)$dbr->selectField(
1045 'templatelinks',
1046 'COUNT(tl_from)',
1047 [
1048 'tl_namespace' => $title->getNamespace(),
1049 'tl_title' => $title->getDBkey()
1050 ],
1051 $fname
1052 );
1053 }
1054
1055 $result['transclusion']['from'] = (int)$dbr->selectField(
1056 'templatelinks',
1057 'COUNT(*)',
1058 [ 'tl_from' => $title->getArticleID() ],
1059 $fname
1060 );
1061
1062 return $result;
1063 }
1064 );
1065 }
1066
1072 protected function getPageTitle() {
1073 return $this->msg( 'pageinfo-title', $this->getTitle()->getPrefixedText() )->text();
1074 }
1075
1081 protected function getDescription() {
1082 return '';
1083 }
1084
1091 protected static function getCacheKey( WANObjectCache $cache, PageIdentity $page, $revId ) {
1092 return $cache->makeKey( 'infoaction', md5( (string)$page ), $revId, self::VERSION );
1093 }
1094}
getAuthority()
int $wgActorTableSchemaMigrationStage
Actor table schema migration stage, for migration from the temporary table revision_actor_temp to the...
const SCHEMA_COMPAT_READ_NEW
Definition Defines.php:267
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:195
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition Action.php:441
getOutput()
Get the OutputPage being used for this instance.
Definition Action.php:156
getUser()
Shortcut to get the User being used for this instance.
Definition Action.php:166
static exists(string $name)
Check if a given action is recognised, even if it's disabled.
Definition Action.php:121
getArticle()
Get a Article object.
Definition Action.php:206
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition Action.php:228
getLanguage()
Shortcut to get the user Language being used for this instance.
Definition Action.php:185
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.
WANObjectCache $wanObjectCache
pageCounts()
Returns page counts that would be too "expensive" to retrieve by normal means.
RevisionLookup $revisionLookup
MagicWordFactory $magicWordFactory
getPageTitle()
Returns the name that goes in the "<h1>" page title.
__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)
pageInfo()
Returns an array of info groups (will be rendered as tables), keyed by group ID.
NamespaceInfo $namespaceInfo
HookRunner $hookRunner
PageProps $pageProps
WatchedItemStoreInterface $watchedItemStore
static getCacheKey(WANObjectCache $cache, PageIdentity $page, $revId)
LanguageNameUtils $languageNameUtils
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.
LinkRenderer $linkRenderer
ILoadBalancer $loadBalancer
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.
RepoGroup $repoGroup
LinkBatchFactory $linkBatchFactory
const VERSION
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.
Language $contentLanguage
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition Language.php:42
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition Linker.php:2241
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:1319
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 links for pages.
makeKnownLink( $target, $text=null, array $extraAttribs=[], array $query=[])
makeLink( $target, $text=null, array $extraAttribs=[], array $query=[])
MediaWikiServices is the service locator for the application scope of MediaWiki.
Page revision base class.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:138
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:33
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:48
static newFromName( $name, $validate='valid')
Definition User.php:607
Multi-datacenter aware caching interface.
Relational database abstraction object.
Definition Database.php:52
Interface for objects which can provide a MediaWiki context on request.
Interface for objects (potentially) representing an editable wiki page.
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.
Database cluster connection, tracking, load balancing, and transaction manager interface.
$cache
Definition mcc.php:33
const DB_REPLICA
Definition defines.php:25
$content
Definition router.php:76
if(!isset( $args[0])) $lang
$header