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