MediaWiki master
InfoAction.php
Go to the documentation of this file.
1<?php
25namespace MediaWiki\Actions;
26
35use MediaWiki\Languages\LanguageNameUtils;
63
70 private const VERSION = 1;
71
72 private Language $contentLanguage;
73 private LanguageNameUtils $languageNameUtils;
74 private LinkBatchFactory $linkBatchFactory;
75 private LinkRenderer $linkRenderer;
76 private IConnectionProvider $dbProvider;
77 private MagicWordFactory $magicWordFactory;
78 private NamespaceInfo $namespaceInfo;
79 private PageProps $pageProps;
80 private RepoGroup $repoGroup;
81 private RevisionLookup $revisionLookup;
82 private WANObjectCache $wanObjectCache;
83 private WatchedItemStoreInterface $watchedItemStore;
84 private RedirectLookup $redirectLookup;
85 private RestrictionStore $restrictionStore;
86 private LinksMigration $linksMigration;
87 private UserFactory $userFactory;
88
89 public function __construct(
90 Article $article,
92 Language $contentLanguage,
93 LanguageNameUtils $languageNameUtils,
94 LinkBatchFactory $linkBatchFactory,
95 LinkRenderer $linkRenderer,
96 IConnectionProvider $dbProvider,
97 MagicWordFactory $magicWordFactory,
98 NamespaceInfo $namespaceInfo,
99 PageProps $pageProps,
100 RepoGroup $repoGroup,
101 RevisionLookup $revisionLookup,
102 WANObjectCache $wanObjectCache,
103 WatchedItemStoreInterface $watchedItemStore,
104 RedirectLookup $redirectLookup,
105 RestrictionStore $restrictionStore,
106 LinksMigration $linksMigration,
107 UserFactory $userFactory
108 ) {
109 parent::__construct( $article, $context );
110 $this->contentLanguage = $contentLanguage;
111 $this->languageNameUtils = $languageNameUtils;
112 $this->linkBatchFactory = $linkBatchFactory;
113 $this->linkRenderer = $linkRenderer;
114 $this->dbProvider = $dbProvider;
115 $this->magicWordFactory = $magicWordFactory;
116 $this->namespaceInfo = $namespaceInfo;
117 $this->pageProps = $pageProps;
118 $this->repoGroup = $repoGroup;
119 $this->revisionLookup = $revisionLookup;
120 $this->wanObjectCache = $wanObjectCache;
121 $this->watchedItemStore = $watchedItemStore;
122 $this->redirectLookup = $redirectLookup;
123 $this->restrictionStore = $restrictionStore;
124 $this->linksMigration = $linksMigration;
125 $this->userFactory = $userFactory;
126 }
127
129 public function getName() {
130 return 'info';
131 }
132
134 public function requiresUnblock() {
135 return false;
136 }
137
139 public function requiresWrite() {
140 return false;
141 }
142
150 public static function invalidateCache( PageIdentity $page, $revid = null ) {
151 $services = MediaWikiServices::getInstance();
152 if ( $revid === null ) {
153 $revision = $services->getRevisionLookup()
154 ->getRevisionByTitle( $page, 0, IDBAccessObject::READ_LATEST );
155 $revid = $revision ? $revision->getId() : 0;
156 }
157 $cache = $services->getMainWANObjectCache();
158 $key = self::getCacheKey( $cache, $page, $revid ?? 0 );
159 $cache->delete( $key );
160 }
161
167 public function onView() {
168 $this->getOutput()->addModuleStyles( [
169 'mediawiki.interface.helpers.styles',
170 'mediawiki.action.styles',
171 ] );
172
173 // "Help" button
174 $this->addHelpLink( 'Page information' );
175
176 // Validate revision
177 $oldid = $this->getArticle()->getOldID();
178 if ( $oldid ) {
179 $revRecord = $this->getArticle()->fetchRevisionRecord();
180
181 if ( !$revRecord ) {
182 return $this->msg( 'missing-revision', $oldid )->parse();
183 } elseif ( !$revRecord->isCurrent() ) {
184 return $this->msg( 'pageinfo-not-current' )->plain();
185 }
186 }
187
188 // Page header
189 $msg = $this->msg( 'pageinfo-header' );
190 $content = $msg->isDisabled() ? '' : $msg->parse();
191
192 // Get page information
193 $pageInfo = $this->pageInfo();
194
195 // Allow extensions to add additional information
196 $this->getHookRunner()->onInfoAction( $this->getContext(), $pageInfo );
197
198 // Render page information
199 foreach ( $pageInfo as $header => $infoTable ) {
200 // Messages:
201 // pageinfo-header-basic, pageinfo-header-edits, pageinfo-header-restrictions,
202 // pageinfo-header-properties, pageinfo-category-info
203 $content .= $this->makeHeader(
204 $this->msg( "pageinfo-$header" )->text(),
205 "mw-pageinfo-$header"
206 ) . "\n";
207 $rows = '';
208 $below = "";
209 foreach ( $infoTable as $infoRow ) {
210 if ( $infoRow[0] == "below" ) {
211 $below = $infoRow[1] . "\n";
212 continue;
213 }
214 $name = ( $infoRow[0] instanceof Message ) ? $infoRow[0]->escaped() : $infoRow[0];
215 $value = ( $infoRow[1] instanceof Message ) ? $infoRow[1]->escaped() : $infoRow[1];
216 $id = ( $infoRow[0] instanceof Message ) ? $infoRow[0]->getKey() : null;
217 $rows .= $this->getRow( $name, $value, $id ) . "\n";
218 }
219 if ( $rows !== '' ) {
220 $content .= Html::rawElement( 'table', [ 'class' => 'wikitable mw-page-info' ],
221 "\n" . $rows );
222 }
223 $content .= "\n" . $below;
224 }
225
226 // Page footer
227 if ( !$this->msg( 'pageinfo-footer' )->isDisabled() ) {
228 $content .= $this->msg( 'pageinfo-footer' )->parse();
229 }
230
231 return $content;
232 }
233
241 private function makeHeader( $header, $canonicalId ) {
242 return Html::rawElement(
243 'h2',
244 [ 'id' => Sanitizer::escapeIdForAttribute( $header ) ],
246 'span',
247 [ 'id' => Sanitizer::escapeIdForAttribute( $canonicalId ) ],
248 ''
249 ) .
250 htmlspecialchars( $header )
251 );
252 }
253
261 private function getRow( $name, $value, $id ) {
262 return Html::rawElement(
263 'tr',
264 [
265 'id' => $id === null ? null : 'mw-' . $id,
266 'style' => 'vertical-align: top;',
267 ],
268 Html::rawElement( 'td', [], $name ) .
269 Html::rawElement( 'td', [], $value )
270 );
271 }
272
286 private function pageInfo() {
287 $user = $this->getUser();
288 $lang = $this->getLanguage();
289 $title = $this->getTitle();
290 $id = $title->getArticleID();
291 $config = $this->context->getConfig();
292 $linkRenderer = $this->linkRenderer;
293
294 $pageCounts = $this->pageCounts();
295
296 $pageProperties = $this->pageProps->getAllProperties( $title )[$id] ?? [];
297
298 // Basic information
299 $pageInfo = [];
300 $pageInfo['header-basic'] = [];
301
302 // Display title
303 $displayTitle = $pageProperties['displaytitle'] ??
304 htmlspecialchars( $title->getPrefixedText(), ENT_NOQUOTES );
305
306 $pageInfo['header-basic'][] = [
307 $this->msg( 'pageinfo-display-title' ),
308 $displayTitle
309 ];
310
311 // Is it a redirect? If so, where to?
312 $redirectTarget = $this->redirectLookup->getRedirectTarget( $this->getWikiPage() );
313 if ( $redirectTarget !== null ) {
314 $pageInfo['header-basic'][] = [
315 $this->msg( 'pageinfo-redirectsto' ),
316 $linkRenderer->makeLink( $redirectTarget ) .
317 $this->msg( 'word-separator' )->escaped() .
318 $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
319 $redirectTarget,
320 $this->msg( 'pageinfo-redirectsto-info' )->text(),
321 [],
322 [ 'action' => 'info' ]
323 ) )->escaped()
324 ];
325 }
326
327 // Default sort key
328 $sortKey = $pageProperties['defaultsort'] ?? $title->getCategorySortkey();
329 $pageInfo['header-basic'][] = [
330 $this->msg( 'pageinfo-default-sort' ),
331 htmlspecialchars( $sortKey )
332 ];
333
334 // Page length (in bytes)
335 $pageInfo['header-basic'][] = [
336 $this->msg( 'pageinfo-length' ),
337 $lang->formatNum( $title->getLength() )
338 ];
339
340 // Page namespace
341 $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-namespace-id' ), $title->getNamespace() ];
342 $pageNamespace = $title->getNsText();
343 if ( $pageNamespace ) {
344 $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-namespace' ), $pageNamespace ];
345 }
346
347 // Page ID (number not localised, as it's a database ID)
348 $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-article-id' ), $id ];
349
350 // Language in which the page content is (supposed to be) written
351 $pageLang = $title->getPageLanguage()->getCode();
352
353 $pageLangHtml = $pageLang . ' - ' .
354 $this->languageNameUtils->getLanguageName( $pageLang, $lang->getCode() );
355 // Link to Special:PageLanguage with pre-filled page title if user has permissions
356 if ( $config->get( MainConfigNames::PageLanguageUseDB )
357 && $this->getAuthority()->probablyCan( 'pagelang', $title )
358 ) {
359 $pageLangHtml .= $this->msg( 'word-separator' )->escaped();
360 $pageLangHtml .= $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
361 SpecialPage::getTitleValueFor( 'PageLanguage', $title->getPrefixedText() ),
362 $this->msg( 'pageinfo-language-change' )->text()
363 ) )->escaped();
364 }
365
366 $pageInfo['header-basic'][] = [
367 $this->msg( 'pageinfo-language' )->escaped(),
368 $pageLangHtml
369 ];
370
371 // Content model of the page
372 $modelHtml = htmlspecialchars( ContentHandler::getLocalizedName( $title->getContentModel() ) );
373 // If the user can change it, add a link to Special:ChangeContentModel
374 if ( $this->getAuthority()->probablyCan( 'editcontentmodel', $title ) ) {
375 $modelHtml .= $this->msg( 'word-separator' )->escaped();
376 $modelHtml .= $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink(
377 SpecialPage::getTitleValueFor( 'ChangeContentModel', $title->getPrefixedText() ),
378 $this->msg( 'pageinfo-content-model-change' )->text()
379 ) )->escaped();
380 }
381
382 $pageInfo['header-basic'][] = [
383 $this->msg( 'pageinfo-content-model' ),
384 $modelHtml
385 ];
386
387 if ( $title->inNamespace( NS_USER ) ) {
388 $pageUser = $this->userFactory->newFromName( $title->getRootText() );
389 if ( $pageUser && $pageUser->getId() && !$pageUser->isHidden() ) {
390 $pageInfo['header-basic'][] = [
391 $this->msg( 'pageinfo-user-id' ),
392 $pageUser->getId()
393 ];
394 }
395 }
396
397 // Search engine status
398 $parserOutput = new ParserOutput();
399 if ( isset( $pageProperties['noindex'] ) ) {
400 $parserOutput->setIndexPolicy( 'noindex' );
401 }
402 if ( isset( $pageProperties['index'] ) ) {
403 $parserOutput->setIndexPolicy( 'index' );
404 }
405
406 // Use robot policy logic
407 $policy = $this->getArticle()->getRobotPolicy( 'view', $parserOutput );
408 $pageInfo['header-basic'][] = [
409 // Messages: pageinfo-robot-index, pageinfo-robot-noindex
410 $this->msg( 'pageinfo-robot-policy' ),
411 $this->msg( "pageinfo-robot-{$policy['index']}" )
412 ];
413
414 $unwatchedPageThreshold = $config->get( MainConfigNames::UnwatchedPageThreshold );
415 if ( $this->getAuthority()->isAllowed( 'unwatchedpages' ) ||
416 ( $unwatchedPageThreshold !== false &&
417 $pageCounts['watchers'] >= $unwatchedPageThreshold )
418 ) {
419 // Number of page watchers
420 $pageInfo['header-basic'][] = [
421 $this->msg( 'pageinfo-watchers' ),
422 $lang->formatNum( $pageCounts['watchers'] )
423 ];
424
425 $visiting = $pageCounts['visitingWatchers'] ?? null;
426 if ( $visiting !== null && $config->get( MainConfigNames::ShowUpdatedMarker ) ) {
427 if ( $visiting > $config->get( MainConfigNames::UnwatchedPageSecret ) ||
428 $this->getAuthority()->isAllowed( 'unwatchedpages' )
429 ) {
430 $value = $lang->formatNum( $visiting );
431 } else {
432 $value = $this->msg( 'pageinfo-few-visiting-watchers' );
433 }
434 $pageInfo['header-basic'][] = [
435 $this->msg( 'pageinfo-visiting-watchers' )
436 ->numParams( ceil( $config->get( MainConfigNames::WatchersMaxAge ) / 86400 ) ),
437 $value
438 ];
439 }
440 } elseif ( $unwatchedPageThreshold !== false ) {
441 $pageInfo['header-basic'][] = [
442 $this->msg( 'pageinfo-watchers' ),
443 $this->msg( 'pageinfo-few-watchers' )->numParams( $unwatchedPageThreshold )
444 ];
445 }
446
447 // Redirects to this page
448 $whatLinksHere = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
449 $pageInfo['header-basic'][] = [
450 $linkRenderer->makeLink(
451 $whatLinksHere,
452 $this->msg( 'pageinfo-redirects-name' )->text(),
453 [],
454 [
455 'hidelinks' => 1,
456 'hidetrans' => 1,
457 'hideimages' => $title->getNamespace() === NS_FILE
458 ]
459 ),
460 $this->msg( 'pageinfo-redirects-value' )
461 ->numParams( count( $title->getRedirectsHere() ) )
462 ];
463
464 // Is it counted as a content page?
465 if ( $this->getWikiPage()->isCountable() ) {
466 $pageInfo['header-basic'][] = [
467 $this->msg( 'pageinfo-contentpage' ),
468 $this->msg( 'pageinfo-contentpage-yes' )
469 ];
470 }
471
472 // Subpages of this page, if subpages are enabled for the current NS
473 if ( $this->namespaceInfo->hasSubpages( $title->getNamespace() ) ) {
474 $prefixIndex = SpecialPage::getTitleFor(
475 'Prefixindex',
476 $title->getPrefixedText() . '/'
477 );
478 $pageInfo['header-basic'][] = [
479 $linkRenderer->makeLink(
480 $prefixIndex,
481 $this->msg( 'pageinfo-subpages-name' )->text()
482 ),
483 // $wgNamespacesWithSubpages can be changed and this can be unset (T340749)
484 isset( $pageCounts['subpages'] )
485 ? $this->msg( 'pageinfo-subpages-value' )->numParams(
486 $pageCounts['subpages']['total'],
487 $pageCounts['subpages']['redirects'],
488 $pageCounts['subpages']['nonredirects']
489 ) : $this->msg( 'pageinfo-subpages-value-unknown' )->rawParams(
490 $linkRenderer->makeKnownLink(
491 $title, $this->msg( 'purge' )->text(), [], [ 'action' => 'purge' ] )
492 )
493 ];
494 }
495
496 if ( $title->inNamespace( NS_CATEGORY ) ) {
497 $category = Category::newFromTitle( $title );
498
499 $allCount = $category->getMemberCount();
500 $subcatCount = $category->getSubcatCount();
501 $fileCount = $category->getFileCount();
502 $pageCount = $category->getPageCount( Category::COUNT_CONTENT_PAGES );
503
504 $pageInfo['category-info'] = [
505 [
506 $this->msg( 'pageinfo-category-total' ),
507 $lang->formatNum( $allCount )
508 ],
509 [
510 $this->msg( 'pageinfo-category-pages' ),
511 $lang->formatNum( $pageCount )
512 ],
513 [
514 $this->msg( 'pageinfo-category-subcats' ),
515 $lang->formatNum( $subcatCount )
516 ],
517 [
518 $this->msg( 'pageinfo-category-files' ),
519 $lang->formatNum( $fileCount )
520 ]
521 ];
522 }
523
524 // Display image SHA-1 value
525 if ( $title->inNamespace( NS_FILE ) ) {
526 $fileObj = $this->repoGroup->findFile( $title );
527 if ( $fileObj !== false ) {
528 // Convert the base-36 sha1 value obtained from database to base-16
529 $output = \Wikimedia\base_convert( $fileObj->getSha1(), 36, 16, 40 );
530 $pageInfo['header-basic'][] = [
531 $this->msg( 'pageinfo-file-hash' ),
532 $output
533 ];
534 }
535 }
536
537 // Page protection
538 $pageInfo['header-restrictions'] = [];
539
540 // Is this page affected by the cascading protection of something which includes it?
541 if ( $this->restrictionStore->isCascadeProtected( $title ) ) {
542 $cascadingFrom = '';
543 $sources = $this->restrictionStore->getCascadeProtectionSources( $title )[0];
544
545 foreach ( $sources as $sourcePageIdentity ) {
546 $cascadingFrom .= Html::rawElement(
547 'li',
548 [],
549 $linkRenderer->makeKnownLink( $sourcePageIdentity )
550 );
551 }
552
553 $cascadingFrom = Html::rawElement( 'ul', [], $cascadingFrom );
554 $pageInfo['header-restrictions'][] = [
555 $this->msg( 'pageinfo-protect-cascading-from' ),
556 $cascadingFrom
557 ];
558 }
559
560 // Is out protection set to cascade to other pages?
561 if ( $this->restrictionStore->areRestrictionsCascading( $title ) ) {
562 $pageInfo['header-restrictions'][] = [
563 $this->msg( 'pageinfo-protect-cascading' ),
564 $this->msg( 'pageinfo-protect-cascading-yes' )
565 ];
566 }
567
568 // Page protection
569 foreach ( $this->restrictionStore->listApplicableRestrictionTypes( $title ) as $restrictionType ) {
570 $protections = $this->restrictionStore->getRestrictions( $title, $restrictionType );
571
572 switch ( count( $protections ) ) {
573 case 0:
574 $message = $this->getNamespaceProtectionMessage( $title ) ??
575 // Allow all users by default
576 $this->msg( 'protect-default' )->escaped();
577 break;
578
579 case 1:
580 // Messages: protect-level-autoconfirmed, protect-level-sysop
581 $message = $this->msg( 'protect-level-' . $protections[0] );
582 if ( !$message->isDisabled() ) {
583 $message = $message->escaped();
584 break;
585 }
586 // Intentional fall-through if message is disabled (or non-existent)
587
588 default:
589 // Require "$1" permission
590 $message = $this->msg( "protect-fallback", $lang->commaList( $protections ) )->parse();
591 break;
592 }
593 $expiry = $this->restrictionStore->getRestrictionExpiry( $title, $restrictionType );
594 $formattedexpiry = $expiry === null ? '' : $this->msg(
595 'parentheses',
596 $lang->formatExpiry( $expiry, true, 'infinity', $user )
597 )->escaped();
598 $message .= $this->msg( 'word-separator' )->escaped() . $formattedexpiry;
599
600 // Messages: restriction-edit, restriction-move, restriction-create,
601 // restriction-upload
602 $pageInfo['header-restrictions'][] = [
603 $this->msg( "restriction-$restrictionType" ), $message
604 ];
605 }
606 $protectLog = SpecialPage::getTitleFor( 'Log' );
607 $pageInfo['header-restrictions'][] = [
608 'below',
609 $linkRenderer->makeKnownLink(
610 $protectLog,
611 $this->msg( 'pageinfo-view-protect-log' )->text(),
612 [],
613 [ 'type' => 'protect', 'page' => $title->getPrefixedText() ]
614 ),
615 ];
616
617 if ( !$this->getWikiPage()->exists() ) {
618 return $pageInfo;
619 }
620
621 // Edit history
622 $pageInfo['header-edits'] = [];
623
624 $firstRev = $this->revisionLookup->getFirstRevision( $this->getTitle() );
625 $lastRev = $this->getWikiPage()->getRevisionRecord();
626 $batch = $this->linkBatchFactory->newLinkBatch()
627 ->setCaller( __METHOD__ );
628 if ( $firstRev ) {
629 $firstRevUser = $firstRev->getUser( RevisionRecord::FOR_THIS_USER, $user );
630 if ( $firstRevUser ) {
631 $batch->addUser( $firstRevUser );
632 }
633 }
634
635 if ( $lastRev ) {
636 $lastRevUser = $lastRev->getUser( RevisionRecord::FOR_THIS_USER, $user );
637 if ( $lastRevUser ) {
638 $batch->addUser( $lastRevUser );
639 }
640 }
641
642 $batch->execute();
643
644 if ( $firstRev ) {
645 // Page creator
646 $firstRevUser = $firstRev->getUser( RevisionRecord::FOR_THIS_USER, $user );
647 // Check if the username is available – it may have been suppressed, in
648 // which case use the invalid user name '[HIDDEN]' to get the wiki's
649 // default user gender.
650 $firstRevUserName = $firstRevUser ? $firstRevUser->getName() : '[HIDDEN]';
651 $pageInfo['header-edits'][] = [
652 $this->msg( 'pageinfo-firstuser', $firstRevUserName ),
653 Linker::revUserTools( $firstRev )
654 ];
655
656 // Date of page creation
657 $pageInfo['header-edits'][] = [
658 $this->msg( 'pageinfo-firsttime' ),
659 $linkRenderer->makeKnownLink(
660 $title,
661 $lang->userTimeAndDate( $firstRev->getTimestamp(), $user ),
662 [],
663 [ 'oldid' => $firstRev->getId() ]
664 )
665 ];
666 }
667
668 if ( $lastRev ) {
669 // Latest editor
670 $lastRevUser = $lastRev->getUser( RevisionRecord::FOR_THIS_USER, $user );
671 // Check if the username is available – it may have been suppressed, in
672 // which case use the invalid user name '[HIDDEN]' to get the wiki's
673 // default user gender.
674 $lastRevUserName = $lastRevUser ? $lastRevUser->getName() : '[HIDDEN]';
675 $pageInfo['header-edits'][] = [
676 $this->msg( 'pageinfo-lastuser', $lastRevUserName ),
677 Linker::revUserTools( $lastRev )
678 ];
679
680 // Date of latest edit
681 $pageInfo['header-edits'][] = [
682 $this->msg( 'pageinfo-lasttime' ),
683 $linkRenderer->makeKnownLink(
684 $title,
685 $lang->userTimeAndDate( $this->getWikiPage()->getTimestamp(), $user ),
686 [],
687 [ 'oldid' => $this->getWikiPage()->getLatest() ]
688 )
689 ];
690 }
691
692 // Total number of edits
693 $pageInfo['header-edits'][] = [
694 $this->msg( 'pageinfo-edits' ),
695 $lang->formatNum( $pageCounts['edits'] )
696 ];
697
698 // Total number of distinct authors
699 if ( $pageCounts['authors'] > 0 ) {
700 $pageInfo['header-edits'][] = [
701 $this->msg( 'pageinfo-authors' ),
702 $lang->formatNum( $pageCounts['authors'] )
703 ];
704 }
705
706 // Recent number of edits (within past 30 days)
707 $pageInfo['header-edits'][] = [
708 $this->msg(
709 'pageinfo-recent-edits',
710 $lang->formatDuration( $config->get( MainConfigNames::RCMaxAge ) )
711 ),
712 $lang->formatNum( $pageCounts['recent_edits'] )
713 ];
714
715 // Recent number of distinct authors
716 $pageInfo['header-edits'][] = [
717 $this->msg( 'pageinfo-recent-authors' ),
718 $lang->formatNum( $pageCounts['recent_authors'] )
719 ];
720
721 // Array of magic word IDs
722 $wordIDs = $this->magicWordFactory->getDoubleUnderscoreArray()->getNames();
723
724 // Array of IDs => localized magic words
725 $localizedWords = $this->contentLanguage->getMagicWords();
726
727 $listItems = [];
728 foreach ( $pageProperties as $property => $value ) {
729 if ( in_array( $property, $wordIDs ) ) {
730 $listItems[] = Html::element( 'li', [], $localizedWords[$property][1] );
731 }
732 }
733
734 $localizedList = Html::rawElement( 'ul', [], implode( '', $listItems ) );
735 $hiddenCategories = $this->getWikiPage()->getHiddenCategories();
736
737 if (
738 count( $listItems ) > 0 ||
739 count( $hiddenCategories ) > 0 ||
740 $pageCounts['transclusion']['from'] > 0 ||
741 $pageCounts['transclusion']['to'] > 0
742 ) {
743 $options = [ 'LIMIT' => $config->get( MainConfigNames::PageInfoTransclusionLimit ) ];
744 $transcludedTemplates = $title->getTemplateLinksFrom( $options );
745 if ( $config->get( MainConfigNames::MiserMode ) ) {
746 $transcludedTargets = [];
747 } else {
748 $transcludedTargets = $title->getTemplateLinksTo( $options );
749 }
750
751 // Page properties
752 $pageInfo['header-properties'] = [];
753
754 // Magic words
755 if ( count( $listItems ) > 0 ) {
756 $pageInfo['header-properties'][] = [
757 $this->msg( 'pageinfo-magic-words' )->numParams( count( $listItems ) ),
758 $localizedList
759 ];
760 }
761
762 // Hidden categories
763 if ( count( $hiddenCategories ) > 0 ) {
764 $pageInfo['header-properties'][] = [
765 $this->msg( 'pageinfo-hidden-categories' )
766 ->numParams( count( $hiddenCategories ) ),
767 Linker::formatHiddenCategories( $hiddenCategories )
768 ];
769 }
770
771 // Transcluded templates
772 if ( $pageCounts['transclusion']['from'] > 0 ) {
773 if ( $pageCounts['transclusion']['from'] > count( $transcludedTemplates ) ) {
774 $more = $this->msg( 'morenotlisted' )->escaped();
775 } else {
776 $more = null;
777 }
778
779 $templateListFormatter = new TemplatesOnThisPageFormatter(
780 $this->getContext(),
781 $linkRenderer,
782 $this->linkBatchFactory,
783 $this->restrictionStore
784 );
785
786 $pageInfo['header-properties'][] = [
787 $this->msg( 'pageinfo-templates' )
788 ->numParams( $pageCounts['transclusion']['from'] ),
789 $templateListFormatter->format( $transcludedTemplates, false, $more )
790 ];
791 }
792
793 if ( !$config->get( MainConfigNames::MiserMode ) && $pageCounts['transclusion']['to'] > 0 ) {
794 if ( $pageCounts['transclusion']['to'] > count( $transcludedTargets ) ) {
795 $more = $linkRenderer->makeLink(
796 $whatLinksHere,
797 $this->msg( 'moredotdotdot' )->text(),
798 [],
799 [ 'hidelinks' => 1, 'hideredirs' => 1 ]
800 );
801 } else {
802 $more = null;
803 }
804
805 $templateListFormatter = new TemplatesOnThisPageFormatter(
806 $this->getContext(),
807 $linkRenderer,
808 $this->linkBatchFactory,
809 $this->restrictionStore
810 );
811
812 $pageInfo['header-properties'][] = [
813 $this->msg( 'pageinfo-transclusions' )
814 ->numParams( $pageCounts['transclusion']['to'] ),
815 $templateListFormatter->format( $transcludedTargets, false, $more )
816 ];
817 }
818 }
819
820 return $pageInfo;
821 }
822
830 private function getNamespaceProtectionMessage( Title $title ): ?string {
831 $rights = [];
832 if ( $title->isRawHtmlMessage() ) {
833 $rights[] = 'editsitecss';
834 $rights[] = 'editsitejs';
835 } elseif ( $title->isSiteCssConfigPage() ) {
836 $rights[] = 'editsitecss';
837 } elseif ( $title->isSiteJsConfigPage() ) {
838 $rights[] = 'editsitejs';
839 } elseif ( $title->isSiteJsonConfigPage() ) {
840 $rights[] = 'editsitejson';
841 } elseif ( $title->isUserCssConfigPage() ) {
842 $rights[] = 'editusercss';
843 } elseif ( $title->isUserJsConfigPage() ) {
844 $rights[] = 'edituserjs';
845 } elseif ( $title->isUserJsonConfigPage() ) {
846 $rights[] = 'edituserjson';
847 } else {
848 $namespaceProtection = $this->context->getConfig()->get( MainConfigNames::NamespaceProtection );
849 $right = $namespaceProtection[$title->getNamespace()] ?? null;
850 if ( $right ) {
851 // a single string as the value is allowed as well as an array
852 $rights = (array)$right;
853 }
854 }
855 if ( $rights ) {
856 return $this->msg( 'protect-fallback', $this->getLanguage()->commaList( $rights ) )->parse();
857 } else {
858 return null;
859 }
860 }
861
867 private function pageCounts() {
868 $page = $this->getWikiPage();
869 $fname = __METHOD__;
870 $config = $this->context->getConfig();
871 $cache = $this->wanObjectCache;
872
873 return $cache->getWithSetCallback(
874 self::getCacheKey( $cache, $page->getTitle(), $page->getLatest() ),
875 WANObjectCache::TTL_WEEK,
876 function ( $oldValue, &$ttl, &$setOpts ) use ( $page, $config, $fname ) {
877 $title = $page->getTitle();
878 $id = $title->getArticleID();
879
880 $dbr = $this->dbProvider->getReplicaDatabase();
881 $setOpts += Database::getCacheSetOptions( $dbr );
882
883 $field = 'rev_actor';
884 $pageField = 'rev_page';
885
886 $watchedItemStore = $this->watchedItemStore;
887
888 $result = [];
889 $result['watchers'] = $watchedItemStore->countWatchers( $title );
890
891 if ( $config->get( MainConfigNames::ShowUpdatedMarker ) ) {
892 $updated = (int)wfTimestamp( TS_UNIX, $page->getTimestamp() );
893 $result['visitingWatchers'] = $watchedItemStore->countVisitingWatchers(
894 $title,
895 $updated - $config->get( MainConfigNames::WatchersMaxAge )
896 );
897 }
898
899 // Total number of edits
900 $edits = (int)$dbr->newSelectQueryBuilder()
901 ->select( 'COUNT(*)' )
902 ->from( 'revision' )
903 ->where( [ 'rev_page' => $id ] )
904 ->caller( $fname )
905 ->fetchField();
906 $result['edits'] = $edits;
907
908 // Total number of distinct authors
909 if ( $config->get( MainConfigNames::MiserMode ) ) {
910 $result['authors'] = 0;
911 } else {
912 $result['authors'] = (int)$dbr->newSelectQueryBuilder()
913 ->select( "COUNT(DISTINCT $field)" )
914 ->from( 'revision' )
915 ->where( [ $pageField => $id ] )
916 ->caller( $fname )
917 ->fetchField();
918 }
919
920 // "Recent" threshold defined by RCMaxAge setting
921 $threshold = $dbr->timestamp( time() - $config->get( MainConfigNames::RCMaxAge ) );
922
923 // Recent number of edits
924 $edits = (int)$dbr->newSelectQueryBuilder()
925 ->select( 'COUNT(rev_page)' )
926 ->from( 'revision' )
927 ->where( [ 'rev_page' => $id ] )
928 ->andWhere( $dbr->expr( 'rev_timestamp', '>=', $threshold ) )
929 ->caller( $fname )
930 ->fetchField();
931 $result['recent_edits'] = $edits;
932
933 // Recent number of distinct authors
934 $result['recent_authors'] = (int)$dbr->newSelectQueryBuilder()
935 ->select( "COUNT(DISTINCT $field)" )
936 ->from( 'revision' )
937 ->where( [ $pageField => $id ] )
938 ->andWhere( [ $dbr->expr( 'rev_timestamp', '>=', $threshold ) ] )
939 ->caller( $fname )
940 ->fetchField();
941
942 // Subpages (if enabled)
943 if ( $this->namespaceInfo->hasSubpages( $title->getNamespace() ) ) {
944 $conds = [ 'page_namespace' => $title->getNamespace() ];
945 $conds[] = $dbr->expr(
946 'page_title',
948 new LikeValue( $title->getDBkey() . '/', $dbr->anyString() )
949 );
950
951 // Subpages of this page (redirects)
952 $conds['page_is_redirect'] = 1;
953 $result['subpages']['redirects'] = (int)$dbr->newSelectQueryBuilder()
954 ->select( 'COUNT(page_id)' )
955 ->from( 'page' )
956 ->where( $conds )
957 ->caller( $fname )
958 ->fetchField();
959 // Subpages of this page (non-redirects)
960 $conds['page_is_redirect'] = 0;
961 $result['subpages']['nonredirects'] = (int)$dbr->newSelectQueryBuilder()
962 ->select( 'COUNT(page_id)' )
963 ->from( 'page' )
964 ->where( $conds )
965 ->caller( $fname )
966 ->fetchField();
967
968 // Subpages of this page (total)
969 $result['subpages']['total'] = $result['subpages']['redirects']
970 + $result['subpages']['nonredirects'];
971 }
972
973 // Counts for the number of transclusion links (to/from)
974 if ( $config->get( MainConfigNames::MiserMode ) ) {
975 $result['transclusion']['to'] = 0;
976 } else {
977 $result['transclusion']['to'] = (int)$dbr->newSelectQueryBuilder()
978 ->select( 'COUNT(tl_from)' )
979 ->from( 'templatelinks' )
980 ->where( $this->linksMigration->getLinksConditions( 'templatelinks', $title ) )
981 ->caller( $fname )
982 ->fetchField();
983 }
984
985 $result['transclusion']['from'] = (int)$dbr->newSelectQueryBuilder()
986 ->select( 'COUNT(*)' )
987 ->from( 'templatelinks' )
988 ->where( [ 'tl_from' => $title->getArticleID() ] )
989 ->caller( $fname )
990 ->fetchField();
991
992 return $result;
993 }
994 );
995 }
996
1002 protected function getPageTitle() {
1003 return $this->msg( 'pageinfo-title' )->plaintextParams( $this->getTitle()->getPrefixedText() );
1004 }
1005
1011 protected function getDescription() {
1012 return '';
1013 }
1014
1021 protected static function getCacheKey( WANObjectCache $cache, PageIdentity $page, $revId ) {
1022 return $cache->makeKey( 'infoaction', md5( (string)$page ), $revId, self::VERSION );
1023 }
1024}
1025
1027class_alias( InfoAction::class, 'InfoAction' );
const NS_USER
Definition Defines.php:67
const NS_FILE
Definition Defines.php:71
const NS_CATEGORY
Definition Defines.php:79
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
getContext()
Get the IContextSource in use here.
Definition Action.php:132
getWikiPage()
Get a WikiPage object.
Definition Action.php:205
IContextSource null $context
IContextSource if specified; otherwise we'll use the Context from the Page.
Definition Action.php:79
getUser()
Shortcut to get the User being used for this instance.
Definition Action.php:166
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition Action.php:240
getTitle()
Shortcut to get the Title object from the page.
Definition Action.php:226
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition Action.php:459
getLanguage()
Shortcut to get the user Language being used for this instance.
Definition Action.php:195
getArticle()
Get a Article object.
Definition Action.php:216
getOutput()
Get the OutputPage being used for this instance.
Definition Action.php:156
getAuthority()
Shortcut to get the Authority executing this instance.
Definition Action.php:176
An action which just does something, without showing a form first.
Displays information about a page.
onView()
Shows page information on GET request.
requiresUnblock()
Whether this action can still be executed by a blocked user.Implementations of this methods must alwa...
static invalidateCache(PageIdentity $page, $revid=null)
Clear the info cache for a given Title.
requiresWrite()
Indicates whether this action page write access to the wiki.Subclasses must override this method to r...
static getCacheKey(WANObjectCache $cache, PageIdentity $page, $revId)
getName()
Return the name of the action this object responds to.1.17string Lowercase name
__construct(Article $article, IContextSource $context, Language $contentLanguage, LanguageNameUtils $languageNameUtils, LinkBatchFactory $linkBatchFactory, LinkRenderer $linkRenderer, IConnectionProvider $dbProvider, MagicWordFactory $magicWordFactory, NamespaceInfo $namespaceInfo, PageProps $pageProps, RepoGroup $repoGroup, RevisionLookup $revisionLookup, WANObjectCache $wanObjectCache, WatchedItemStoreInterface $watchedItemStore, RedirectLookup $redirectLookup, RestrictionStore $restrictionStore, LinksMigration $linksMigration, UserFactory $userFactory)
getDescription()
Returns the description that goes below the "<h1>" tag.
getPageTitle()
Returns the name that goes in the "<h1>" page title.
Category objects are immutable, strictly speaking.
Definition Category.php:44
static newFromTitle(PageIdentity $page)
Factory function.
Definition Category.php:196
Base class for content handling.
static getLocalizedName( $name, ?Language $lang=null)
Returns the localized name for a given content model.
Handles formatting for the "templates used on this page" lists.
Prioritized list of file repositories.
Definition RepoGroup.php:38
This class is a collection of static functions that serve two purposes:
Definition Html.php:57
Base class for language-specific code.
Definition Language.php:81
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:61
Service for compat reading of links tables.
A class containing constants representing the names of configuration variables.
const UnwatchedPageSecret
Name constant for the UnwatchedPageSecret setting, for use with Config::get()
const RCMaxAge
Name constant for the RCMaxAge setting, for use with Config::get()
const NamespaceProtection
Name constant for the NamespaceProtection setting, for use with Config::get()
const WatchersMaxAge
Name constant for the WatchersMaxAge setting, for use with Config::get()
const PageInfoTransclusionLimit
Name constant for the PageInfoTransclusionLimit setting, for use with Config::get()
const UnwatchedPageThreshold
Name constant for the UnwatchedPageThreshold setting, for use with Config::get()
const MiserMode
Name constant for the MiserMode setting, for use with Config::get()
const ShowUpdatedMarker
Name constant for the ShowUpdatedMarker setting, for use with Config::get()
const PageLanguageUseDB
Name constant for the PageLanguageUseDB setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:157
Legacy class representing an editable page and handling UI for some page actions.
Definition Article.php:76
Gives access to properties of a page.
Definition PageProps.php:35
Store information about magic words, and create/cache MagicWord objects.
ParserOutput is a rendering of a Content object or a message.
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:46
Page revision base class.
Parent class for all special pages.
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.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Represents a title within MediaWiki.
Definition Title.php:78
Create User objects.
Multi-datacenter aware caching interface.
makeKey( $keygroup,... $components)
static getCacheSetOptions(?IReadableDatabase ... $dbs)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
Content of like value.
Definition LikeValue.php:14
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.
Provide primary and replica IDatabase connections.
Interface for database access objects.
element(SerializerNode $parent, SerializerNode $node, $contents)