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