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