MediaWiki master
SpecialVersion.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Specials;
10
11use Closure;
15use MediaWiki\Html\TocGeneratorTrait;
29use Symfony\Component\Yaml\Yaml;
33
40 use TocGeneratorTrait;
41
45 protected $coreId = '';
46
50 protected static $extensionTypes = false;
51
52 public function __construct(
53 private readonly ParserFactory $parserFactory,
54 private readonly UrlUtils $urlUtils,
55 private readonly IConnectionProvider $dbProvider
56 ) {
57 parent::__construct( 'Version' );
58 }
59
67 public static function getCredits( ExtensionRegistry $reg, Config $conf ): array {
68 $credits = $conf->get( MainConfigNames::ExtensionCredits );
69 foreach ( $reg->getAllThings() as $credit ) {
70 $credits[$credit['type']][] = $credit;
71 }
72 return $credits;
73 }
74
78 public function execute( $par ) {
79 $config = $this->getConfig();
80 $credits = self::getCredits( ExtensionRegistry::getInstance(), $config );
81
82 $this->setHeaders();
83 $this->outputHeader();
84 $out = $this->getOutput();
85 $out->getMetadata()->setPreventClickjacking( false );
86
87 // Explode the subpage information into useful bits
88 $parts = explode( '/', (string)$par );
89 $extNode = null;
90 if ( isset( $parts[1] ) ) {
91 $extName = str_replace( '_', ' ', $parts[1] );
92 // Find it!
93 foreach ( $credits as $extensions ) {
94 foreach ( $extensions as $ext ) {
95 if ( isset( $ext['name'] ) && ( $ext['name'] === $extName ) ) {
96 $extNode = &$ext;
97 break 2;
98 }
99 }
100 }
101 if ( !$extNode ) {
102 $out->setStatusCode( 404 );
103 }
104 } else {
105 $extName = 'MediaWiki';
106 }
107
108 // Now figure out what to do
109 switch ( strtolower( $parts[0] ) ) {
110 case 'credits':
111 $out->addModuleStyles( 'mediawiki.special' );
112
113 $wikiText = '{{int:version-credits-not-found}}';
114 if ( $extName === 'MediaWiki' ) {
115 $wikiText = file_get_contents( MW_INSTALL_PATH . '/CREDITS' );
116 // Put the contributor list into columns
117 $wikiText = str_replace(
118 [ '<!-- BEGIN CONTRIBUTOR LIST -->', '<!-- END CONTRIBUTOR LIST -->' ],
119 [ '<div class="mw-version-credits">', '</div>' ],
120 $wikiText
121 );
122 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
123 $file = ExtensionInfo::getAuthorsFileName( dirname( $extNode['path'] ) );
124 if ( $file ) {
125 $wikiText = file_get_contents( $file );
126 if ( str_ends_with( $file, '.txt' ) ) {
127 $wikiText = Html::element(
128 'pre',
129 [
130 'lang' => 'en',
131 'dir' => 'ltr',
132 ],
133 $wikiText
134 );
135 }
136 }
137 }
138
139 $out->setPageTitleMsg( $this->msg( 'version-credits-title' )->plaintextParams( $extName ) );
140 $out->addWikiTextAsInterface( $wikiText );
141 break;
142
143 case 'license':
144 $out->setPageTitleMsg( $this->msg( 'version-license-title' )->plaintextParams( $extName ) );
145
146 $licenseFound = false;
147
148 if ( $extName === 'MediaWiki' ) {
149 $out->addWikiTextAsInterface(
150 file_get_contents( MW_INSTALL_PATH . '/COPYING' )
151 );
152 $licenseFound = true;
153 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
154 $files = ExtensionInfo::getLicenseFileNames( dirname( $extNode['path'] ) );
155 if ( $files ) {
156 $licenseFound = true;
157 foreach ( $files as $file ) {
158 $out->addWikiTextAsInterface(
160 'pre',
161 [
162 'lang' => 'en',
163 'dir' => 'ltr',
164 ],
165 file_get_contents( $file )
166 )
167 );
168 }
169 }
170 }
171 if ( !$licenseFound ) {
172 $out->addWikiTextAsInterface( '{{int:version-license-not-found}}' );
173 }
174 break;
175
176 default:
177 $out->addModuleStyles( 'mediawiki.special' );
178
179 $out->addHTML( $this->getMediaWikiCredits() );
180
181 // Build the page contents (this also fills in TOCData)
182 $sections = [
183 $this->softwareInformation(),
184 $this->getEntryPointInfo(),
185 $this->getSkinCredits( $credits ),
186 $this->getExtensionCredits( $credits ),
187 $this->getLibraries( $credits ),
188 $this->getParserTags(),
189 $this->getParserFunctionHooks(),
190 $this->getParsoidModules(),
191 $this->getHooks(),
192 $this->IPInfo(),
193 ];
194
195 // Insert TOC first
196 $out->addTOCPlaceholder( $this->getTocData() );
197
198 // Insert contents
199 foreach ( $sections as $content ) {
200 $out->addHTML( $content );
201 }
202
203 break;
204 }
205 }
206
212 private function getMediaWikiCredits() {
213 // No TOC entry for this heading, we treat it like the lede section
214
215 $ret = Html::element(
216 'h2',
217 [ 'id' => 'mw-version-license' ],
218 $this->msg( 'version-license' )->text()
219 );
220
221 $ret .= Html::rawElement( 'div', [ 'class' => 'plainlinks' ],
222 $this->msg( new RawMessage( self::getCopyrightAndAuthorList() ) )->parseAsBlock() .
223 Html::rawElement( 'div', [ 'class' => 'mw-version-license-info' ],
224 $this->msg( 'version-license-info' )->parseAsBlock()
225 )
226 );
227
228 return $ret;
229 }
230
237 public static function getCopyrightAndAuthorList() {
238 if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
239 $othersLink = '[https://www.mediawiki.org/wiki/Special:Version/Credits ' .
240 wfMessage( 'version-poweredby-others' )->plain() . ']';
241 } else {
242 $othersLink = '[[Special:Version/Credits|' .
243 wfMessage( 'version-poweredby-others' )->plain() . ']]';
244 }
245
246 $translatorsLink = '[https://translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
247 wfMessage( 'version-poweredby-translators' )->plain() . ']';
248
249 $authorList = [
250 'Magnus Manske', 'Brooke Vibber', 'Lee Daniel Crocker',
251 'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
252 'Niklas Laxström', 'Domas Mituzas', 'Rob Church', 'Yuri Astrakhan',
253 'Aryeh Gregor', 'Aaron Schulz', 'Andrew Garrett', 'Raimond Spekking',
254 'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
255 'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
256 'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
257 'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', 'Brad Jorsch',
258 'Bartosz Dziewoński', 'Ed Sanders', 'Moriel Schottlender',
259 'Kunal Mehta', 'James D. Forrester', 'Brian Wolff', 'Adam Shorland',
260 'DannyS712', 'Ori Livneh', 'Max Semenik', 'Amir Sarabadani',
261 'Derk-Jan Hartman', 'Petr Pchelko', 'Umherirrender', 'C. Scott Ananian',
262 'fomafix', 'Thiemo Kreuz', 'Gergő Tisza', 'Volker E.',
263 'Jack Phoenix', 'Isarra Yos',
264 $othersLink, $translatorsLink
265 ];
266
267 return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
268 Message::listParam( $authorList ) )->plain();
269 }
270
276 private function getSoftwareInformation() {
277 $dbr = $this->dbProvider->getReplicaDatabase();
278
279 // Put the software in an array of form 'name' => 'version'. All messages should
280 // be loaded here, so feel free to use wfMessage in the 'name'. Wikitext
281 // can be used both in the name and value.
282 $versionLink = self::getVersionLinkedGit( $this->getLanguage() ) ?: MW_VERSION;
283 $software = [
284 '[https://www.mediawiki.org/ MediaWiki]' => $versionLink,
285 '[https://php.net/ PHP]' => PHP_VERSION . " (" . PHP_SAPI . ")",
286 '[https://icu.unicode.org/ ICU]' => INTL_ICU_VERSION,
287 $dbr->getSoftwareLink() => $dbr->getServerInfo(),
288 ];
289
290 // T339915: If wikidiff2 is installed, show version
291 if ( phpversion( "wikidiff2" ) ) {
292 $software[ '[https://www.mediawiki.org/wiki/Wikidiff2 wikidiff2]' ] = phpversion( "wikidiff2" );
293 }
294
295 // Allow a hook to add/remove items.
296 $this->getHookRunner()->onSoftwareInfo( $software );
297
298 return $software;
299 }
300
306 private function softwareInformation() {
307 $this->addTocSection( id: 'mw-version-software', msg: 'version-software' );
308
309 $out = Html::element(
310 'h2',
311 [ 'id' => 'mw-version-software' ],
312 $this->msg( 'version-software' )->text()
313 );
314
315 $out .= Html::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ] );
316
317 $out .= $this->getTableHeaderHtml( [
318 $this->msg( 'version-software-product' )->text(),
319 $this->msg( 'version-software-version' )->text()
320 ] );
321
322 foreach ( $this->getSoftwareInformation() as $name => $version ) {
323 $out .= Html::rawElement(
324 'tr',
325 [],
326 Html::rawElement( 'td', [], $this->msg( new RawMessage( $name ) )->parse() ) .
327 Html::rawElement( 'td', [ 'dir' => 'ltr' ], $this->msg( new RawMessage( $version ) )->parse() )
328 );
329 }
330
331 $out .= Html::closeElement( 'table' );
332
333 return $out;
334 }
335
345 public static function getVersion( $flags = '', $lang = null ) {
346 $gitInfo = GitInfo::repo()->getHeadSHA1();
347 if ( !$gitInfo ) {
348 $version = MW_VERSION;
349 } elseif ( $flags === 'nodb' ) {
350 $shortSha1 = substr( $gitInfo, 0, 7 );
351 $version = MW_VERSION . " ($shortSha1)";
352 } else {
353 $shortSha1 = substr( $gitInfo, 0, 7 );
354 $msg = wfMessage( 'parentheses' );
355 if ( $lang !== null ) {
356 $msg->inLanguage( $lang );
357 }
358 $shortSha1 = $msg->params( $shortSha1 )->text();
359 $version = MW_VERSION . ' ' . $shortSha1;
360 }
361
362 return $version;
363 }
364
368 private static function getMWVersionLinked() {
369 $versionUrl = "";
370 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
371 if ( $hookRunner->onSpecialVersionVersionUrl( MW_VERSION, $versionUrl ) ) {
372 $versionParts = [];
373 preg_match( "/^(\d+\.\d+)/", MW_VERSION, $versionParts );
374 $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
375 }
376
377 return '[' . $versionUrl . ' ' . MW_VERSION . ']';
378 }
379
385 private static function getVersionLinkedGit( Language $lang ) {
386 // TODO make function non-static and replace param with $this->getLanguage() after dropping getVersionLinked
387 $gitInfo = new GitInfo( MW_INSTALL_PATH );
388 $headSHA1 = $gitInfo->getHeadSHA1();
389 if ( !$headSHA1 ) {
390 return false;
391 }
392
393 $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
394
395 $gitHeadUrl = $gitInfo->getHeadViewUrl();
396 if ( $gitHeadUrl !== false ) {
397 $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
398 }
399
400 $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
401 if ( $gitHeadCommitDate ) {
402 $shortSHA1 .= Html::element( 'br' );
403 $shortSHA1 .= $lang->timeanddate( (string)$gitHeadCommitDate, true );
404 }
405
406 return self::getMWVersionLinked() . " $shortSHA1";
407 }
408
418 public static function getExtensionTypes(): array {
419 if ( self::$extensionTypes === false ) {
420 self::$extensionTypes = [
421 'specialpage' => wfMessage( 'version-specialpages' )->text(),
422 'editor' => wfMessage( 'version-editors' )->text(),
423 'parserhook' => wfMessage( 'version-parserhooks' )->text(),
424 'variable' => wfMessage( 'version-variables' )->text(),
425 'media' => wfMessage( 'version-mediahandlers' )->text(),
426 'antispam' => wfMessage( 'version-antispam' )->text(),
427 'skin' => wfMessage( 'version-skins' )->text(),
428 'api' => wfMessage( 'version-api' )->text(),
429 'other' => wfMessage( 'version-other' )->text(),
430 ];
431
432 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
433 ->onExtensionTypes( self::$extensionTypes );
434 }
435
436 return self::$extensionTypes;
437 }
438
448 public static function getExtensionTypeName( $type ) {
449 $types = self::getExtensionTypes();
450
451 return $types[$type] ?? $types['other'];
452 }
453
460 private function getExtensionCredits( array $credits ) {
461 $extensionTypes = self::getExtensionTypes();
462
463 $this->addTocSection( id: 'mw-version-ext', msg: 'version-extensions' );
464
465 $out = Html::element(
466 'h2',
467 [ 'id' => 'mw-version-ext' ],
468 $this->msg( 'version-extensions' )->text()
469 );
470
471 if (
472 !$credits ||
473 // Skins are displayed separately, see getSkinCredits()
474 ( count( $credits ) === 1 && isset( $credits['skin'] ) )
475 ) {
476 $out .= Html::element(
477 'p',
478 [],
479 $this->msg( 'version-extensions-no-ext' )->text()
480 );
481
482 return $out;
483 }
484
485 // Find all extensions that do not have a valid type and give them the type 'other'.
486 $credits['other'] ??= [];
487 foreach ( $credits as $type => $extensions ) {
488 if ( !array_key_exists( $type, $extensionTypes ) ) {
489 $credits['other'] = array_merge( $credits['other'], $extensions );
490 }
491 }
492
493 // Loop through the extension categories to display their extensions in the list.
494 foreach ( $extensionTypes as $type => $text ) {
495 // Skins have a separate section
496 if ( $type !== 'other' && $type !== 'skin' ) {
497 $out .= $this->getExtensionCategory( $type, $text, $credits[$type] ?? [] );
498 }
499 }
500
501 // We want the 'other' type to be last in the list.
502 $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'], $credits['other'] );
503
504 return $out;
505 }
506
513 private function getSkinCredits( array $credits ) {
514 $this->addTocSection( id: 'mw-version-skin', msg: 'version-skins' );
515
516 $out = Html::element(
517 'h2',
518 [ 'id' => 'mw-version-skin' ],
519 $this->msg( 'version-skins' )->text()
520 );
521
522 if ( !isset( $credits['skin'] ) || !$credits['skin'] ) {
523 $out .= Html::element(
524 'p',
525 [],
526 $this->msg( 'version-skins-no-skin' )->text()
527 );
528
529 return $out;
530 }
531 $out .= $this->getExtensionCategory( 'skin', null, $credits['skin'] );
532
533 return $out;
534 }
535
542 protected function getLibraries( array $credits ) {
543 $this->addTocSection( id: 'mw-version-libraries', msg: 'version-libraries' );
544
545 $out = Html::element(
546 'h2',
547 [ 'id' => 'mw-version-libraries' ],
548 $this->msg( 'version-libraries' )->text()
549 );
550
551 return $out
552 . $this->getExternalLibraries( $credits )
553 . $this->getClientSideLibraries();
554 }
555
562 public static function parseComposerInstalled( array $credits ) {
563 $paths = [
564 MW_INSTALL_PATH . '/vendor/composer/installed.json'
565 ];
566
567 $extensionTypes = self::getExtensionTypes();
568 foreach ( $extensionTypes as $type => $message ) {
569 if ( !isset( $credits[$type] ) || $credits[$type] === [] ) {
570 continue;
571 }
572 foreach ( $credits[$type] as $extension ) {
573 if ( !isset( $extension['path'] ) ) {
574 continue;
575 }
576 $paths[] = dirname( $extension['path'] ) . '/vendor/composer/installed.json';
577 }
578 }
579
580 $dependencies = [];
581
582 foreach ( $paths as $path ) {
583 if ( !file_exists( $path ) ) {
584 continue;
585 }
586
587 $installed = new ComposerInstalled( $path );
588
589 $dependencies += $installed->getInstalledDependencies();
590 }
591
592 ksort( $dependencies );
593 return $dependencies;
594 }
595
602 protected function getExternalLibraries( array $credits ) {
603 $dependencies = self::parseComposerInstalled( $credits );
604 if ( $dependencies === [] ) {
605 return '';
606 }
607
608 $this->addTocSubSection( id: 'mw-version-libraries-server', msg: 'version-libraries-server' );
609
610 $out = Html::element(
611 'h3',
612 [ 'id' => 'mw-version-libraries-server' ],
613 $this->msg( 'version-libraries-server' )->text()
614 );
615 $out .= Html::openElement(
616 'table',
617 [ 'class' => 'wikitable plainlinks mw-installed-software', 'id' => 'sv-libraries' ]
618 );
619
620 $out .= $this->getTableHeaderHtml( [
621 $this->msg( 'version-libraries-library' )->text(),
622 $this->msg( 'version-libraries-version' )->text(),
623 $this->msg( 'version-libraries-license' )->text(),
624 $this->msg( 'version-libraries-description' )->text(),
625 $this->msg( 'version-libraries-authors' )->text(),
626 ] );
627
628 foreach ( $dependencies as $name => $info ) {
629 if ( !is_array( $info ) || str_starts_with( $info['type'], 'mediawiki-' ) ) {
630 // Skip any extensions or skins since they'll be listed
631 // in their proper section
632 continue;
633 }
634 $authors = array_map( static function ( $arr ) {
635 return new HtmlArmor( isset( $arr['homepage'] ) ?
636 Html::element( 'a', [ 'href' => $arr['homepage'] ], $arr['name'] ) :
637 htmlspecialchars( $arr['name'] )
638 );
639 }, $info['authors'] );
640 $authors = $this->listAuthors( $authors, false, MW_INSTALL_PATH . "/vendor/$name" );
641
642 // We can safely assume that the libraries' names and descriptions
643 // are written in English and aren't going to be translated,
644 // so set appropriate lang and dir attributes
645 $out .= Html::openElement( 'tr', [
646 // Add an anchor so docs can link easily to the version of
647 // this specific library
648 'id' => Sanitizer::escapeIdForAttribute(
649 "mw-version-library-$name"
650 ) ] )
651 . Html::rawElement(
652 'td',
653 [],
654 $this->getLinkRenderer()->makeExternalLink(
655 "https://packagist.org/packages/$name",
656 $name,
657 $this->getFullTitle(),
658 '',
659 [ 'class' => 'mw-version-library-name' ]
660 )
661 )
662 . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
663 // @phan-suppress-next-line SecurityCheck-DoubleEscaped See FIXME in listToText
664 . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['licenses'] ) )
665 . Html::element( 'td', [ 'lang' => 'en', 'dir' => 'ltr' ], $info['description'] )
666 . Html::rawElement( 'td', [], $authors )
667 . Html::closeElement( 'tr' );
668 }
669 $out .= Html::closeElement( 'table' );
670
671 return $out;
672 }
673
679 public static function parseForeignResources() {
680 $registryDirs = [ 'MediaWiki' => MW_INSTALL_PATH . '/resources/lib' ]
681 + ExtensionRegistry::getInstance()->getAttribute( 'ForeignResourcesDir' );
682
683 $modules = [];
684 foreach ( $registryDirs as $source => $registryDir ) {
685 $foreignResources = Yaml::parseFile( "$registryDir/foreign-resources.yaml" );
686 foreach ( $foreignResources as $name => $module ) {
687 $key = $name . $module['version'];
688 if ( isset( $modules[$key] ) ) {
689 $modules[$key]['source'][] = $source;
690 continue;
691 }
692 $modules[$key] = $module + [ 'name' => $name, 'source' => [ $source ] ];
693 }
694 }
695 ksort( $modules );
696 return $modules;
697 }
698
704 private function getClientSideLibraries() {
705 $this->addTocSubSection( id: 'mw-version-libraries-client', msg: 'version-libraries-client' );
706
707 $out = Html::element(
708 'h3',
709 [ 'id' => 'mw-version-libraries-client' ],
710 $this->msg( 'version-libraries-client' )->text()
711 );
712 $out .= Html::openElement(
713 'table',
714 [ 'class' => 'wikitable plainlinks mw-installed-software', 'id' => 'sv-libraries-client' ]
715 );
716
717 $out .= $this->getTableHeaderHtml( [
718 $this->msg( 'version-libraries-library' )->text(),
719 $this->msg( 'version-libraries-version' )->text(),
720 $this->msg( 'version-libraries-license' )->text(),
721 $this->msg( 'version-libraries-authors' )->text(),
722 $this->msg( 'version-libraries-source' )->text()
723 ] );
724
725 foreach ( self::parseForeignResources() as $name => $info ) {
726 // We can safely assume that the libraries' names and descriptions
727 // are written in English and aren't going to be translated,
728 // so set appropriate lang and dir attributes
729 $out .= Html::openElement( 'tr', [
730 // Add an anchor so docs can link easily to the version of
731 // this specific library
732 'id' => Sanitizer::escapeIdForAttribute(
733 "mw-version-library-$name"
734 ) ] )
735 . Html::rawElement(
736 'td',
737 [],
738 $this->getLinkRenderer()->makeExternalLink(
739 $info['homepage'],
740 $info['name'],
741 $this->getFullTitle(),
742 '',
743 [ 'class' => 'mw-version-library-name' ]
744 )
745 )
746 . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
747 . Html::element( 'td', [ 'dir' => 'auto' ], $info['license'] )
748 . Html::element( 'td', [ 'dir' => 'auto' ], $info['authors'] ?? '—' )
749 // @phan-suppress-next-line SecurityCheck-DoubleEscaped See FIXME in listToText
750 . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['source'] ) )
751 . Html::closeElement( 'tr' );
752 }
753 $out .= Html::closeElement( 'table' );
754
755 return $out;
756 }
757
763 protected function getParserTags() {
764 $tags = $this->parserFactory->getMainInstance()->getTags();
765 if ( !$tags ) {
766 return '';
767 }
768
769 $this->addTocSection( id: 'mw-version-parser-extensiontags', msg: 'version-parser-extensiontags' );
770
771 $out = Html::rawElement(
772 'h2',
773 [ 'id' => 'mw-version-parser-extensiontags' ],
774 Html::rawElement(
775 'span',
776 [ 'class' => 'plainlinks' ],
777 $this->getLinkRenderer()->makeExternalLink(
778 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
779 $this->msg( 'version-parser-extensiontags' ),
780 $this->getFullTitle()
781 )
782 )
783 );
784
785 array_walk( $tags, static function ( &$value ) {
786 // Bidirectional isolation improves readability in RTL wikis
787 $value = Html::rawElement(
788 'bdi',
789 // Prevent < and > from slipping to another line
790 [
791 'style' => 'white-space: nowrap;',
792 ],
793 Html::element( 'code', [], "<$value>" )
794 );
795 } );
796
797 $out .= $this->listToText( $tags );
798
799 return $out;
800 }
801
807 protected function getParserFunctionHooks() {
808 $funcHooks = $this->parserFactory->getMainInstance()->getFunctionHooks();
809 if ( !$funcHooks ) {
810 return '';
811 }
812
813 $this->addTocSection( id: 'mw-version-parser-function-hooks', msg: 'version-parser-function-hooks' );
814
815 $out = Html::rawElement(
816 'h2',
817 [ 'id' => 'mw-version-parser-function-hooks' ],
818 Html::rawElement(
819 'span',
820 [ 'class' => 'plainlinks' ],
821 $this->getLinkRenderer()->makeExternalLink(
822 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
823 $this->msg( 'version-parser-function-hooks' ),
824 $this->getFullTitle()
825 )
826 )
827 );
828
829 $funcSynonyms = $this->parserFactory->getMainInstance()->getFunctionSynonyms();
830 // This will give us the preferred synonyms in the content language, as if
831 // we used MagicWord::getSynonym( 0 ), because they appear first in the arrays.
832 // We can't use MagicWord directly, because only Parser knows whether a function
833 // uses the leading "#" or not. Case-sensitive functions ("1") win over
834 // case-insensitive ones ("0"), like in Parser::callParserFunction().
835 // There should probably be a better API for this.
836 $preferredSynonyms = array_flip( array_reverse( $funcSynonyms[1] + $funcSynonyms[0] ) );
837 array_walk( $funcHooks, static function ( &$value ) use ( $preferredSynonyms ) {
838 $value = $preferredSynonyms[$value];
839 } );
840 $legacyHooks = array_flip( $funcHooks );
841
842 // Sort case-insensitively, ignoring the leading '#' if present
843 $cmpHooks = static function ( $a, $b ) {
844 return strcasecmp( ltrim( $a, '#' ), ltrim( $b, '#' ) );
845 };
846 usort( $funcHooks, $cmpHooks );
847
848 $formatHooks = static function ( &$value ) {
849 // Bidirectional isolation ensures it displays as {{#ns}} and not {{ns#}} in RTL wikis
850 $value = Html::rawElement(
851 'bdi',
852 [],
853 Html::element( 'code', [], '{{' . $value . '}}' )
854 );
855 };
856 array_walk( $funcHooks, $formatHooks );
857
858 $out .= $this->getLanguage()->listToText( $funcHooks );
859
860 # Get a list of parser functions from Parsoid as well.
861 $parsoidHooks = [];
862 $services = MediaWikiServices::getInstance();
863 $siteConfig = $services->getParsoidSiteConfig();
864 $magicWordFactory = $services->getMagicWordFactory();
865 foreach ( $siteConfig->getPFragmentHandlerKeys() as $key ) {
866 $config = $siteConfig->getPFragmentHandlerConfig( $key );
867 if ( !( $config['options']['parserFunction'] ?? false ) ) {
868 continue;
869 }
870 $mw = $magicWordFactory->get( $key );
871 foreach ( $mw->getSynonyms() as $local ) {
872 if ( !( $config['options']['nohash'] ?? false ) ) {
873 $local = '#' . $local;
874 }
875 // Skip hooks already present in legacy hooks (they will
876 // also work in parsoid)
877 if ( isset( $legacyHooks[$local] ) ) {
878 continue;
879 }
880 $parsoidHooks[] = $local;
881 }
882 }
883 if ( $parsoidHooks ) {
884 $out .= Html::element(
885 'h3',
886 [ 'id' => 'mw-version-parser-function-hooks-parsoid' ],
887 $this->msg( 'version-parser-function-hooks-parsoid' )->text()
888 );
889 usort( $parsoidHooks, $cmpHooks );
890 array_walk( $parsoidHooks, $formatHooks );
891 $out .= $this->getLanguage()->listToText( $parsoidHooks );
892 }
893
894 return $out;
895 }
896
902 protected function getParsoidModules() {
903 $siteConfig = MediaWikiServices::getInstance()->getParsoidSiteConfig();
904 $modules = $siteConfig->getExtensionModules();
905
906 if ( !$modules ) {
907 return '';
908 }
909
910 $this->addTocSection( id: 'mw-version-parsoid-modules', msg: 'version-parsoid-modules' );
911
912 $out = Html::rawElement(
913 'h2',
914 [ 'id' => 'mw-version-parsoid-modules' ],
915 Html::rawElement(
916 'span',
917 [ 'class' => 'plainlinks' ],
918 $this->getLinkRenderer()->makeExternalLink(
919 'https://www.mediawiki.org/wiki/Special:MyLanguage/Parsoid',
920 $this->msg( 'version-parsoid-modules' ),
921 $this->getFullTitle()
922 )
923 )
924 );
925
926 $moduleNames = array_map(
927 static fn ( $m )=>Html::element( 'code', [
928 'title' => $m->getConfig()['extension-name'] ?? null,
929 ], $m->getConfig()['name'] ),
930 $modules
931 );
932
933 $out .= $this->getLanguage()->listToText( $moduleNames );
934
935 return $out;
936 }
937
947 protected function getExtensionCategory( $type, ?string $text, array $creditsGroup ) {
948 $out = '';
949
950 if ( $creditsGroup ) {
951 $out .= $this->openExtType( $text, 'credits-' . $type );
952
953 usort( $creditsGroup, $this->compare( ... ) );
954
955 foreach ( $creditsGroup as $extension ) {
956 $out .= $this->getCreditsForExtension( $type, $extension );
957 }
958
959 $out .= Html::closeElement( 'table' );
960 }
961
962 return $out;
963 }
964
971 private function compare( $a, $b ) {
972 return $this->getLanguage()->lc( $a['name'] ) <=> $this->getLanguage()->lc( $b['name'] );
973 }
974
993 public function getCreditsForExtension( $type, array $extension ) {
994 $out = $this->getOutput();
995
996 // We must obtain the information for all the bits and pieces!
997 // ... such as extension names and links
998 if ( isset( $extension['namemsg'] ) ) {
999 // Localized name of extension
1000 $extensionName = $this->msg( $extension['namemsg'] )->text();
1001 } elseif ( isset( $extension['name'] ) ) {
1002 // Non localized version
1003 $extensionName = $extension['name'];
1004 } else {
1005 $extensionName = $this->msg( 'version-no-ext-name' )->text();
1006 }
1007
1008 if ( isset( $extension['url'] ) ) {
1009 $extensionNameLink = $this->getLinkRenderer()->makeExternalLink(
1010 $extension['url'],
1011 $extensionName,
1012 $this->getFullTitle(),
1013 '',
1014 [ 'class' => 'mw-version-ext-name' ]
1015 );
1016 } else {
1017 $extensionNameLink = htmlspecialchars( $extensionName );
1018 }
1019
1020 // ... and the version information
1021 // If the extension path is set we will check that directory for GIT
1022 // metadata in an attempt to extract date and vcs commit metadata.
1023 $canonicalVersion = '&ndash;';
1024 $extensionPath = null;
1025 $vcsVersion = null;
1026 $vcsLink = null;
1027 $vcsDate = null;
1028
1029 if ( isset( $extension['version'] ) ) {
1030 $canonicalVersion = $out->parseInlineAsInterface( $extension['version'] );
1031 }
1032
1033 if ( isset( $extension['path'] ) ) {
1034 $extensionPath = dirname( $extension['path'] );
1035 if ( $this->coreId == '' ) {
1036 wfDebug( 'Looking up core head id' );
1037 $coreHeadSHA1 = GitInfo::repo()->getHeadSHA1();
1038 if ( $coreHeadSHA1 ) {
1039 $this->coreId = $coreHeadSHA1;
1040 }
1041 }
1042 $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getInstance( CACHE_ANYTHING );
1043 $memcKey = $cache->makeKey(
1044 'specialversion-ext-version-text', $extension['path'], $this->coreId
1045 );
1046 $res = $cache->get( $memcKey );
1047
1048 if ( $res === false ) {
1049 wfDebug( "Getting VCS info for extension {$extension['name']}" );
1050 $gitInfo = new GitInfo( $extensionPath );
1051 $vcsVersion = $gitInfo->getHeadSHA1();
1052 if ( $vcsVersion !== false ) {
1053 $vcsVersion = substr( $vcsVersion, 0, 7 );
1054 $vcsLink = $gitInfo->getHeadViewUrl();
1055 $vcsDate = $gitInfo->getHeadCommitDate();
1056 }
1057 $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
1058 } else {
1059 wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" );
1060 [ $vcsVersion, $vcsLink, $vcsDate ] = $res;
1061 }
1062 }
1063
1064 $versionString = Html::rawElement(
1065 'span',
1066 [ 'class' => 'mw-version-ext-version' ],
1067 $canonicalVersion
1068 );
1069
1070 if ( $vcsVersion ) {
1071 if ( $vcsLink ) {
1072 $vcsVerString = $this->getLinkRenderer()->makeExternalLink(
1073 $vcsLink,
1074 $this->msg( 'version-version', $vcsVersion ),
1075 $this->getFullTitle(),
1076 '',
1077 [ 'class' => 'mw-version-ext-vcs-version' ]
1078 );
1079 } else {
1080 $vcsVerString = Html::element( 'span',
1081 [ 'class' => 'mw-version-ext-vcs-version' ],
1082 "({$vcsVersion})"
1083 );
1084 }
1085 $versionString .= " {$vcsVerString}";
1086
1087 if ( $vcsDate ) {
1088 $versionString .= ' ' . Html::element( 'span', [
1089 'class' => 'mw-version-ext-vcs-timestamp',
1090 'dir' => $this->getLanguage()->getDir(),
1091 ], $this->getLanguage()->timeanddate( $vcsDate, true ) );
1092 }
1093 $versionString = Html::rawElement( 'span',
1094 [ 'class' => 'mw-version-ext-meta-version' ],
1095 $versionString
1096 );
1097 }
1098
1099 // ... and license information; if a license file exists we
1100 // will link to it
1101 $licenseLink = '';
1102 if ( isset( $extension['name'] ) ) {
1103 $licenseName = null;
1104 if ( isset( $extension['license-name'] ) ) {
1105 $licenseName = new HtmlArmor( $out->parseInlineAsInterface( $extension['license-name'] ) );
1106 } elseif ( $extensionPath !== null && ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
1107 $licenseName = $this->msg( 'version-ext-license' )->text();
1108 }
1109 if ( $licenseName !== null ) {
1110 $licenseLink = $this->getLinkRenderer()->makeLink(
1111 $this->getPageTitle( 'License/' . $extension['name'] ),
1112 $licenseName,
1113 [
1114 'class' => 'mw-version-ext-license',
1115 'dir' => 'auto',
1116 ]
1117 );
1118 }
1119 }
1120
1121 // ... and generate the description; which can be a parameterized l10n message
1122 // in the form [ <msgname>, <parameter>, <parameter>... ] or just a straight
1123 // up string
1124 if ( isset( $extension['descriptionmsg'] ) ) {
1125 // Localized description of extension
1126 $descriptionMsg = $extension['descriptionmsg'];
1127
1128 if ( is_array( $descriptionMsg ) ) {
1129 $descriptionMsgKey = array_shift( $descriptionMsg );
1130 $descriptionMsg = array_map( 'htmlspecialchars', $descriptionMsg );
1131 $description = $this->msg( $descriptionMsgKey, ...$descriptionMsg )->text();
1132 } else {
1133 $description = $this->msg( $descriptionMsg )->text();
1134 }
1135 } elseif ( isset( $extension['description'] ) ) {
1136 // Non localized version
1137 $description = $extension['description'];
1138 } else {
1139 $description = '';
1140 }
1141 $description = $out->parseInlineAsInterface( $description );
1142
1143 // ... now get the authors for this extension
1144 $authors = $extension['author'] ?? [];
1145 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable path is set when there is a name
1146 $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
1147
1148 // Finally! Create the table
1149 $html = Html::openElement( 'tr', [
1150 'class' => 'mw-version-ext',
1151 'id' => Sanitizer::escapeIdForAttribute( 'mw-version-ext-' . $type . '-' . $extension['name'] )
1152 ]
1153 );
1154
1155 $html .= Html::rawElement( 'td', [], $extensionNameLink );
1156 $html .= Html::rawElement( 'td', [], $versionString );
1157 $html .= Html::rawElement( 'td', [], $licenseLink );
1158 $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-description' ], $description );
1159 $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-authors' ], $authors );
1160
1161 $html .= Html::closeElement( 'tr' );
1162
1163 return $html;
1164 }
1165
1171 private function getHooks() {
1172 if ( !$this->getConfig()->get( MainConfigNames::SpecialVersionShowHooks ) ) {
1173 return '';
1174 }
1175
1176 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1177 $hookNames = $hookContainer->getHookNames();
1178
1179 if ( !$hookNames ) {
1180 return '';
1181 }
1182
1183 sort( $hookNames );
1184
1185 $ret = [];
1186 $this->addTocSection( id: 'mw-version-hooks', msg: 'version-hooks' );
1187 $ret[] = Html::element(
1188 'h2',
1189 [ 'id' => 'mw-version-hooks' ],
1190 $this->msg( 'version-hooks' )->text()
1191 );
1192 $ret[] = Html::openElement( 'table', [ 'class' => 'wikitable', 'id' => 'sv-hooks' ] );
1193 $ret[] = Html::openElement( 'tr' );
1194 $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-name' )->text() );
1195 $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-subscribedby' )->text() );
1196 $ret[] = Html::closeElement( 'tr' );
1197
1198 foreach ( $hookNames as $name ) {
1199 $handlers = $hookContainer->getHandlerDescriptions( $name );
1200
1201 $ret[] = Html::openElement( 'tr' );
1202 $ret[] = Html::element( 'td', [], $name );
1203 // @phan-suppress-next-line SecurityCheck-DoubleEscaped See FIXME in listToText
1204 $ret[] = Html::element( 'td', [], $this->listToText( $handlers ) );
1205 $ret[] = Html::closeElement( 'tr' );
1206 }
1207
1208 $ret[] = Html::closeElement( 'table' );
1209
1210 return implode( "\n", $ret );
1211 }
1212
1213 private function openExtType( ?string $text = null, ?string $name = null ): string {
1214 $out = '';
1215
1216 $opt = [ 'class' => 'wikitable plainlinks mw-installed-software' ];
1217
1218 if ( $name ) {
1219 $opt['id'] = "sv-$name";
1220 }
1221
1222 $out .= Html::openElement( 'table', $opt );
1223
1224 if ( $text !== null ) {
1225 $out .= Html::element( 'caption', [], $text );
1226 }
1227
1228 if ( $name && $text !== null ) {
1229 $this->addTocSubSection( "sv-$name", 'rawmessage', $text );
1230 }
1231
1232 $firstHeadingMsg = ( $name === 'credits-skin' )
1233 ? 'version-skin-colheader-name'
1234 : 'version-ext-colheader-name';
1235
1236 $out .= $this->getTableHeaderHtml( [
1237 $this->msg( $firstHeadingMsg )->text(),
1238 $this->msg( 'version-ext-colheader-version' )->text(),
1239 $this->msg( 'version-ext-colheader-license' )->text(),
1240 $this->msg( 'version-ext-colheader-description' )->text(),
1241 $this->msg( 'version-ext-colheader-credits' )->text()
1242 ] );
1243
1244 return $out;
1245 }
1246
1255 private function getTableHeaderHtml( $headers ): string {
1256 $out = '';
1257 foreach ( $headers as $header ) {
1258 $out .= Html::element( 'th', [ 'scope' => 'col' ], $header );
1259 }
1260 return Html::rawElement( 'thead', [],
1261 Html::rawElement( 'tr', [], $out )
1262 );
1263 }
1264
1270 private function IPInfo() {
1271 $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
1272
1273 return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
1274 }
1275
1296 public function listAuthors( $authors, $extName, $extDir ): string {
1297 $hasOthers = false;
1298 $linkRenderer = $this->getLinkRenderer();
1299
1300 $list = [];
1301 $authors = (array)$authors;
1302
1303 // Special case: if the authors array has only one item and it is "...",
1304 // it should not be rendered as the "version-poweredby-others" i18n msg,
1305 // but rather as "version-poweredby-various" i18n msg instead.
1306 if ( count( $authors ) === 1 && $authors[0] === '...' ) {
1307 // Link to the extension's or skin's AUTHORS or CREDITS file, if there is
1308 // such a file; otherwise just return the i18n msg as-is
1309 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1310 return $linkRenderer->makeLink(
1311 $this->getPageTitle( "Credits/$extName" ),
1312 $this->msg( 'version-poweredby-various' )->text()
1313 );
1314 } else {
1315 return $this->msg( 'version-poweredby-various' )->escaped();
1316 }
1317 }
1318
1319 // Otherwise, if we have an actual array that has more than one item,
1320 // process each array item as usual
1321 foreach ( $authors as $item ) {
1322 if ( $item instanceof HtmlArmor ) {
1323 $list[] = HtmlArmor::getHtml( $item );
1324 } elseif ( $item === '...' ) {
1325 $hasOthers = true;
1326
1327 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1328 $text = $linkRenderer->makeLink(
1329 $this->getPageTitle( "Credits/$extName" ),
1330 $this->msg( 'version-poweredby-others' )->text()
1331 );
1332 } else {
1333 $text = $this->msg( 'version-poweredby-others' )->escaped();
1334 }
1335 $list[] = $text;
1336 } elseif ( str_ends_with( $item, ' ...]' ) ) {
1337 $hasOthers = true;
1338 $list[] = $this->getOutput()->parseInlineAsInterface(
1339 substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
1340 );
1341 } else {
1342 $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1343 }
1344 }
1345
1346 if ( $extName && !$hasOthers && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1347 $list[] = $linkRenderer->makeLink(
1348 $this->getPageTitle( "Credits/$extName" ),
1349 $this->msg( 'version-poweredby-others' )->text()
1350 );
1351 }
1352
1353 return $this->listToText( $list, false );
1354 }
1355
1365 private function listToText( array $list, bool $sort = true ): string {
1366 if ( !$list ) {
1367 return '';
1368 }
1369 if ( $sort ) {
1370 sort( $list );
1371 }
1372
1373 return $this->getLanguage()
1374 ->listToText( array_map( self::arrayToString( ... ), $list ) );
1375 }
1376
1386 public static function arrayToString( $list ) {
1387 if ( is_array( $list ) && count( $list ) == 1 ) {
1388 $list = $list[0];
1389 }
1390 if ( $list instanceof Closure ) {
1391 // Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$"
1392 return 'Closure';
1393 } elseif ( is_object( $list ) ) {
1394 return wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1395 } elseif ( !is_array( $list ) ) {
1396 return $list;
1397 } else {
1398 if ( is_object( $list[0] ) ) {
1399 $class = get_class( $list[0] );
1400 } else {
1401 $class = $list[0];
1402 }
1403
1404 return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1405 }
1406 }
1407
1413 public static function getGitHeadSha1( $dir ) {
1414 wfDeprecated( __METHOD__, '1.41' );
1415 return ( new GitInfo( $dir ) )->getHeadSHA1();
1416 }
1417
1422 public function getEntryPointInfo() {
1423 $config = $this->getConfig();
1424 $scriptPath = $config->get( MainConfigNames::ScriptPath ) ?: '/';
1425
1426 $entryPoints = [
1427 'version-entrypoints-articlepath' => $config->get( MainConfigNames::ArticlePath ),
1428 'version-entrypoints-scriptpath' => $scriptPath,
1429 'version-entrypoints-index-php' => wfScript( 'index' ),
1430 'version-entrypoints-api-php' => wfScript( 'api' ),
1431 'version-entrypoints-rest-php' => wfScript( 'rest' ),
1432 ];
1433
1434 $language = $this->getLanguage();
1435 $thAttributes = [
1436 'dir' => $language->getDir(),
1437 'lang' => $language->getHtmlCode(),
1438 'scope' => 'col'
1439 ];
1440
1441 $this->addTocSection( id: 'mw-version-entrypoints', msg: 'version-entrypoints' );
1442
1443 $out = Html::element(
1444 'h2',
1445 [ 'id' => 'mw-version-entrypoints' ],
1446 $this->msg( 'version-entrypoints' )->text()
1447 ) .
1448 Html::openElement( 'table',
1449 [
1450 'class' => 'wikitable plainlinks',
1451 'id' => 'mw-version-entrypoints-table',
1452 'dir' => 'ltr',
1453 'lang' => 'en'
1454 ]
1455 ) .
1456 Html::openElement( 'thead' ) .
1457 Html::openElement( 'tr' ) .
1458 Html::element(
1459 'th',
1460 $thAttributes,
1461 $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1462 ) .
1463 Html::element(
1464 'th',
1465 $thAttributes,
1466 $this->msg( 'version-entrypoints-header-url' )->text()
1467 ) .
1468 Html::closeElement( 'tr' ) .
1469 Html::closeElement( 'thead' );
1470
1471 foreach ( $entryPoints as $message => $value ) {
1472 $url = $this->urlUtils->expand( $value, PROTO_RELATIVE );
1473 $out .= Html::openElement( 'tr' ) .
1474 Html::rawElement( 'td', [], $this->msg( $message )->parse() ) .
1475 Html::rawElement( 'td', [],
1476 Html::rawElement(
1477 'code',
1478 [],
1479 $this->msg( new RawMessage( "[$url $value]" ) )->parse()
1480 )
1481 ) .
1482 Html::closeElement( 'tr' );
1483 }
1484
1485 $out .= Html::closeElement( 'table' );
1486
1487 return $out;
1488 }
1489
1491 protected function getGroupName() {
1492 return 'wiki';
1493 }
1494}
1495
1500class_alias( SpecialVersion::class, 'SpecialVersion' );
const CACHE_ANYTHING
Definition Defines.php:72
const MW_VERSION
The running version of MediaWiki.
Definition Defines.php:23
const PROTO_RELATIVE
Definition Defines.php:219
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfScript( $script='index')
Get the URL path to a MediaWiki entry point.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:44
Base class for language-specific code.
Definition Language.php:65
Variant of the Message class.
A class containing constants representing the names of configuration variables.
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
static listParam(array $list, $type=ListType::AND)
Definition Message.php:1355
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:34
Load JSON files, and uses a Processor to extract information.
getAllThings()
Get credits information about all installed extensions and skins.
Parent class for all special pages.
Version information about MediaWiki (core, extensions, libs), PHP, and the database.
getParserFunctionHooks()
Obtains a list of installed parser function hooks and the associated H2 header.
getLibraries(array $credits)
Generate the section for installed external libraries.
getParsoidModules()
Obtains a list of installed Parsoid Modules and the associated H2 header.
static parseComposerInstalled(array $credits)
string $coreId
The current rev id/SHA hash of MediaWiki core.
static arrayToString( $list)
Convert an array or object to a string for display.
static getVersion( $flags='', $lang=null)
Return a string of the MediaWiki version with Git revision if available.
listAuthors( $authors, $extName, $extDir)
Return a formatted unsorted list of authors.
static getExtensionTypeName( $type)
Returns the internationalized name for an extension type.
getCreditsForExtension( $type, array $extension)
Creates and formats a version line for a single extension.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
getExtensionCategory( $type, ?string $text, array $creditsGroup)
Creates and returns the HTML for a single extension category.
static getExtensionTypes()
Returns an array with the base extension types.
static getCopyrightAndAuthorList()
Get the "MediaWiki is copyright 2001-20xx by lots of cool folks" text.
getEntryPointInfo()
Get the list of entry points and their URLs.
static getCredits(ExtensionRegistry $reg, Config $conf)
__construct(private readonly ParserFactory $parserFactory, private readonly UrlUtils $urlUtils, private readonly IConnectionProvider $dbProvider)
static string[] false $extensionTypes
Lazy initialized key/value with message content.
getParserTags()
Obtains a list of installed parser tags and the associated H2 header.
getExternalLibraries(array $credits)
Generate an HTML table for external libraries that are installed.
Fetch status information from a local git repository.
Definition GitInfo.php:33
Library for creating and parsing MW-style timestamps.
A service to expand, parse, and otherwise manipulate URLs.
Definition UrlUtils.php:16
Reads an installed.json file and provides accessors to get what is installed.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:18
Interface for configuration instances.
Definition Config.php:18
Provide primary and replica IDatabase connections.
$source
element(SerializerNode $parent, SerializerNode $node, $contents)
addTocSection(string $id, string $msg,... $params)
Add a section to the table of contents.
addTocSubSection(string $id, string $msg,... $params)
Add a sub-section to the table of contents.
msg( $key,... $params)