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