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 $software = [
283 '[https://www.mediawiki.org/ MediaWiki]' => self::getVersionLinked(),
284 '[https://php.net/ PHP]' => PHP_VERSION . " (" . PHP_SAPI . ")",
285 '[https://icu.unicode.org/ ICU]' => INTL_ICU_VERSION,
286 $dbr->getSoftwareLink() => $dbr->getServerInfo(),
287 ];
288
289 // T339915: If wikidiff2 is installed, show version
290 if ( phpversion( "wikidiff2" ) ) {
291 $software[ '[https://www.mediawiki.org/wiki/Wikidiff2 wikidiff2]' ] = phpversion( "wikidiff2" );
292 }
293
294 // Allow a hook to add/remove items.
295 $this->getHookRunner()->onSoftwareInfo( $software );
296
297 return $software;
298 }
299
305 private function softwareInformation() {
306 $this->addTocSection( id: 'mw-version-software', msg: 'version-software' );
307
308 $out = Html::element(
309 'h2',
310 [ 'id' => 'mw-version-software' ],
311 $this->msg( 'version-software' )->text()
312 );
313
314 $out .= Html::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ] );
315
316 $out .= $this->getTableHeaderHtml( [
317 $this->msg( 'version-software-product' )->text(),
318 $this->msg( 'version-software-version' )->text()
319 ] );
320
321 foreach ( $this->getSoftwareInformation() as $name => $version ) {
322 $out .= Html::rawElement(
323 'tr',
324 [],
325 Html::rawElement( 'td', [], $this->msg( new RawMessage( $name ) )->parse() ) .
326 Html::rawElement( 'td', [ 'dir' => 'ltr' ], $this->msg( new RawMessage( $version ) )->parse() )
327 );
328 }
329
330 $out .= Html::closeElement( 'table' );
331
332 return $out;
333 }
334
344 public static function getVersion( $flags = '', $lang = null ) {
345 $gitInfo = GitInfo::repo()->getHeadSHA1();
346 if ( !$gitInfo ) {
347 $version = MW_VERSION;
348 } elseif ( $flags === 'nodb' ) {
349 $shortSha1 = substr( $gitInfo, 0, 7 );
350 $version = MW_VERSION . " ($shortSha1)";
351 } else {
352 $shortSha1 = substr( $gitInfo, 0, 7 );
353 $msg = wfMessage( 'parentheses' );
354 if ( $lang !== null ) {
355 $msg->inLanguage( $lang );
356 }
357 $shortSha1 = $msg->params( $shortSha1 )->text();
358 $version = MW_VERSION . ' ' . $shortSha1;
359 }
360
361 return $version;
362 }
363
371 public static function getVersionLinked() {
372 return self::getVersionLinkedGit() ?: MW_VERSION;
373 }
374
378 private static function getMWVersionLinked() {
379 $versionUrl = "";
380 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
381 if ( $hookRunner->onSpecialVersionVersionUrl( MW_VERSION, $versionUrl ) ) {
382 $versionParts = [];
383 preg_match( "/^(\d+\.\d+)/", MW_VERSION, $versionParts );
384 $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
385 }
386
387 return '[' . $versionUrl . ' ' . MW_VERSION . ']';
388 }
389
395 private static function getVersionLinkedGit() {
396 global $wgLang;
397
398 $gitInfo = new GitInfo( MW_INSTALL_PATH );
399 $headSHA1 = $gitInfo->getHeadSHA1();
400 if ( !$headSHA1 ) {
401 return false;
402 }
403
404 $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
405
406 $gitHeadUrl = $gitInfo->getHeadViewUrl();
407 if ( $gitHeadUrl !== false ) {
408 $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
409 }
410
411 $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
412 if ( $gitHeadCommitDate ) {
413 $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( (string)$gitHeadCommitDate, true );
414 }
415
416 return self::getMWVersionLinked() . " $shortSHA1";
417 }
418
428 public static function getExtensionTypes(): array {
429 if ( self::$extensionTypes === false ) {
430 self::$extensionTypes = [
431 'specialpage' => wfMessage( 'version-specialpages' )->text(),
432 'editor' => wfMessage( 'version-editors' )->text(),
433 'parserhook' => wfMessage( 'version-parserhooks' )->text(),
434 'variable' => wfMessage( 'version-variables' )->text(),
435 'media' => wfMessage( 'version-mediahandlers' )->text(),
436 'antispam' => wfMessage( 'version-antispam' )->text(),
437 'skin' => wfMessage( 'version-skins' )->text(),
438 'api' => wfMessage( 'version-api' )->text(),
439 'other' => wfMessage( 'version-other' )->text(),
440 ];
441
442 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
443 ->onExtensionTypes( self::$extensionTypes );
444 }
445
446 return self::$extensionTypes;
447 }
448
458 public static function getExtensionTypeName( $type ) {
459 $types = self::getExtensionTypes();
460
461 return $types[$type] ?? $types['other'];
462 }
463
470 private function getExtensionCredits( array $credits ) {
471 $extensionTypes = self::getExtensionTypes();
472
473 $this->addTocSection( id: 'mw-version-ext', msg: 'version-extensions' );
474
475 $out = Html::element(
476 'h2',
477 [ 'id' => 'mw-version-ext' ],
478 $this->msg( 'version-extensions' )->text()
479 );
480
481 if (
482 !$credits ||
483 // Skins are displayed separately, see getSkinCredits()
484 ( count( $credits ) === 1 && isset( $credits['skin'] ) )
485 ) {
486 $out .= Html::element(
487 'p',
488 [],
489 $this->msg( 'version-extensions-no-ext' )->text()
490 );
491
492 return $out;
493 }
494
495 // Find all extensions that do not have a valid type and give them the type 'other'.
496 $credits['other'] ??= [];
497 foreach ( $credits as $type => $extensions ) {
498 if ( !array_key_exists( $type, $extensionTypes ) ) {
499 $credits['other'] = array_merge( $credits['other'], $extensions );
500 }
501 }
502
503 // Loop through the extension categories to display their extensions in the list.
504 foreach ( $extensionTypes as $type => $text ) {
505 // Skins have a separate section
506 if ( $type !== 'other' && $type !== 'skin' ) {
507 $out .= $this->getExtensionCategory( $type, $text, $credits[$type] ?? [] );
508 }
509 }
510
511 // We want the 'other' type to be last in the list.
512 $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'], $credits['other'] );
513
514 return $out;
515 }
516
523 private function getSkinCredits( array $credits ) {
524 $this->addTocSection( id: 'mw-version-skin', msg: 'version-skins' );
525
526 $out = Html::element(
527 'h2',
528 [ 'id' => 'mw-version-skin' ],
529 $this->msg( 'version-skins' )->text()
530 );
531
532 if ( !isset( $credits['skin'] ) || !$credits['skin'] ) {
533 $out .= Html::element(
534 'p',
535 [],
536 $this->msg( 'version-skins-no-skin' )->text()
537 );
538
539 return $out;
540 }
541 $out .= $this->getExtensionCategory( 'skin', null, $credits['skin'] );
542
543 return $out;
544 }
545
552 protected function getLibraries( array $credits ) {
553 $this->addTocSection( id: 'mw-version-libraries', msg: 'version-libraries' );
554
555 $out = Html::element(
556 'h2',
557 [ 'id' => 'mw-version-libraries' ],
558 $this->msg( 'version-libraries' )->text()
559 );
560
561 return $out
562 . $this->getExternalLibraries( $credits )
563 . $this->getClientSideLibraries();
564 }
565
572 public static function parseComposerInstalled( array $credits ) {
573 $paths = [
574 MW_INSTALL_PATH . '/vendor/composer/installed.json'
575 ];
576
577 $extensionTypes = self::getExtensionTypes();
578 foreach ( $extensionTypes as $type => $message ) {
579 if ( !isset( $credits[$type] ) || $credits[$type] === [] ) {
580 continue;
581 }
582 foreach ( $credits[$type] as $extension ) {
583 if ( !isset( $extension['path'] ) ) {
584 continue;
585 }
586 $paths[] = dirname( $extension['path'] ) . '/vendor/composer/installed.json';
587 }
588 }
589
590 $dependencies = [];
591
592 foreach ( $paths as $path ) {
593 if ( !file_exists( $path ) ) {
594 continue;
595 }
596
597 $installed = new ComposerInstalled( $path );
598
599 $dependencies += $installed->getInstalledDependencies();
600 }
601
602 ksort( $dependencies );
603 return $dependencies;
604 }
605
612 protected function getExternalLibraries( array $credits ) {
613 $dependencies = self::parseComposerInstalled( $credits );
614 if ( $dependencies === [] ) {
615 return '';
616 }
617
618 $this->addTocSubSection( id: 'mw-version-libraries-server', msg: 'version-libraries-server' );
619
620 $out = Html::element(
621 'h3',
622 [ 'id' => 'mw-version-libraries-server' ],
623 $this->msg( 'version-libraries-server' )->text()
624 );
625 $out .= Html::openElement(
626 'table',
627 [ 'class' => 'wikitable plainlinks mw-installed-software', 'id' => 'sv-libraries' ]
628 );
629
630 $out .= $this->getTableHeaderHtml( [
631 $this->msg( 'version-libraries-library' )->text(),
632 $this->msg( 'version-libraries-version' )->text(),
633 $this->msg( 'version-libraries-license' )->text(),
634 $this->msg( 'version-libraries-description' )->text(),
635 $this->msg( 'version-libraries-authors' )->text(),
636 ] );
637
638 foreach ( $dependencies as $name => $info ) {
639 if ( !is_array( $info ) || str_starts_with( $info['type'], 'mediawiki-' ) ) {
640 // Skip any extensions or skins since they'll be listed
641 // in their proper section
642 continue;
643 }
644 $authors = array_map( static function ( $arr ) {
645 return new HtmlArmor( isset( $arr['homepage'] ) ?
646 Html::element( 'a', [ 'href' => $arr['homepage'] ], $arr['name'] ) :
647 htmlspecialchars( $arr['name'] )
648 );
649 }, $info['authors'] );
650 $authors = $this->listAuthors( $authors, false, MW_INSTALL_PATH . "/vendor/$name" );
651
652 // We can safely assume that the libraries' names and descriptions
653 // are written in English and aren't going to be translated,
654 // so set appropriate lang and dir attributes
655 $out .= Html::openElement( 'tr', [
656 // Add an anchor so docs can link easily to the version of
657 // this specific library
658 'id' => Sanitizer::escapeIdForAttribute(
659 "mw-version-library-$name"
660 ) ] )
661 . Html::rawElement(
662 'td',
663 [],
664 $this->getLinkRenderer()->makeExternalLink(
665 "https://packagist.org/packages/$name",
666 $name,
667 $this->getFullTitle(),
668 '',
669 [ 'class' => 'mw-version-library-name' ]
670 )
671 )
672 . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
673 // @phan-suppress-next-line SecurityCheck-DoubleEscaped See FIXME in listToText
674 . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['licenses'] ) )
675 . Html::element( 'td', [ 'lang' => 'en', 'dir' => 'ltr' ], $info['description'] )
676 . Html::rawElement( 'td', [], $authors )
677 . Html::closeElement( 'tr' );
678 }
679 $out .= Html::closeElement( 'table' );
680
681 return $out;
682 }
683
689 public static function parseForeignResources() {
690 $registryDirs = [ 'MediaWiki' => MW_INSTALL_PATH . '/resources/lib' ]
691 + ExtensionRegistry::getInstance()->getAttribute( 'ForeignResourcesDir' );
692
693 $modules = [];
694 foreach ( $registryDirs as $source => $registryDir ) {
695 $foreignResources = Yaml::parseFile( "$registryDir/foreign-resources.yaml" );
696 foreach ( $foreignResources as $name => $module ) {
697 $key = $name . $module['version'];
698 if ( isset( $modules[$key] ) ) {
699 $modules[$key]['source'][] = $source;
700 continue;
701 }
702 $modules[$key] = $module + [ 'name' => $name, 'source' => [ $source ] ];
703 }
704 }
705 ksort( $modules );
706 return $modules;
707 }
708
714 private function getClientSideLibraries() {
715 $this->addTocSubSection( id: 'mw-version-libraries-client', msg: 'version-libraries-client' );
716
717 $out = Html::element(
718 'h3',
719 [ 'id' => 'mw-version-libraries-client' ],
720 $this->msg( 'version-libraries-client' )->text()
721 );
722 $out .= Html::openElement(
723 'table',
724 [ 'class' => 'wikitable plainlinks mw-installed-software', 'id' => 'sv-libraries-client' ]
725 );
726
727 $out .= $this->getTableHeaderHtml( [
728 $this->msg( 'version-libraries-library' )->text(),
729 $this->msg( 'version-libraries-version' )->text(),
730 $this->msg( 'version-libraries-license' )->text(),
731 $this->msg( 'version-libraries-authors' )->text(),
732 $this->msg( 'version-libraries-source' )->text()
733 ] );
734
735 foreach ( self::parseForeignResources() as $name => $info ) {
736 // We can safely assume that the libraries' names and descriptions
737 // are written in English and aren't going to be translated,
738 // so set appropriate lang and dir attributes
739 $out .= Html::openElement( 'tr', [
740 // Add an anchor so docs can link easily to the version of
741 // this specific library
742 'id' => Sanitizer::escapeIdForAttribute(
743 "mw-version-library-$name"
744 ) ] )
745 . Html::rawElement(
746 'td',
747 [],
748 $this->getLinkRenderer()->makeExternalLink(
749 $info['homepage'],
750 $info['name'],
751 $this->getFullTitle(),
752 '',
753 [ 'class' => 'mw-version-library-name' ]
754 )
755 )
756 . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
757 . Html::element( 'td', [ 'dir' => 'auto' ], $info['license'] )
758 . Html::element( 'td', [ 'dir' => 'auto' ], $info['authors'] ?? '—' )
759 // @phan-suppress-next-line SecurityCheck-DoubleEscaped See FIXME in listToText
760 . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['source'] ) )
761 . Html::closeElement( 'tr' );
762 }
763 $out .= Html::closeElement( 'table' );
764
765 return $out;
766 }
767
773 protected function getParserTags() {
774 $tags = $this->parserFactory->getMainInstance()->getTags();
775 if ( !$tags ) {
776 return '';
777 }
778
779 $this->addTocSection( id: 'mw-version-parser-extensiontags', msg: 'version-parser-extensiontags' );
780
781 $out = Html::rawElement(
782 'h2',
783 [ 'id' => 'mw-version-parser-extensiontags' ],
784 Html::rawElement(
785 'span',
786 [ 'class' => 'plainlinks' ],
787 $this->getLinkRenderer()->makeExternalLink(
788 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
789 $this->msg( 'version-parser-extensiontags' ),
790 $this->getFullTitle()
791 )
792 )
793 );
794
795 array_walk( $tags, static function ( &$value ) {
796 // Bidirectional isolation improves readability in RTL wikis
797 $value = Html::rawElement(
798 'bdi',
799 // Prevent < and > from slipping to another line
800 [
801 'style' => 'white-space: nowrap;',
802 ],
803 Html::element( 'code', [], "<$value>" )
804 );
805 } );
806
807 $out .= $this->listToText( $tags );
808
809 return $out;
810 }
811
817 protected function getParserFunctionHooks() {
818 $funcHooks = $this->parserFactory->getMainInstance()->getFunctionHooks();
819 if ( !$funcHooks ) {
820 return '';
821 }
822
823 $this->addTocSection( id: 'mw-version-parser-function-hooks', msg: 'version-parser-function-hooks' );
824
825 $out = Html::rawElement(
826 'h2',
827 [ 'id' => 'mw-version-parser-function-hooks' ],
828 Html::rawElement(
829 'span',
830 [ 'class' => 'plainlinks' ],
831 $this->getLinkRenderer()->makeExternalLink(
832 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
833 $this->msg( 'version-parser-function-hooks' ),
834 $this->getFullTitle()
835 )
836 )
837 );
838
839 $funcSynonyms = $this->parserFactory->getMainInstance()->getFunctionSynonyms();
840 // This will give us the preferred synonyms in the content language, as if
841 // we used MagicWord::getSynonym( 0 ), because they appear first in the arrays.
842 // We can't use MagicWord directly, because only Parser knows whether a function
843 // uses the leading "#" or not. Case-sensitive functions ("1") win over
844 // case-insensitive ones ("0"), like in Parser::callParserFunction().
845 // There should probably be a better API for this.
846 $preferredSynonyms = array_flip( array_reverse( $funcSynonyms[1] + $funcSynonyms[0] ) );
847 array_walk( $funcHooks, static function ( &$value ) use ( $preferredSynonyms ) {
848 $value = $preferredSynonyms[$value];
849 } );
850 $legacyHooks = array_flip( $funcHooks );
851
852 // Sort case-insensitively, ignoring the leading '#' if present
853 $cmpHooks = static function ( $a, $b ) {
854 return strcasecmp( ltrim( $a, '#' ), ltrim( $b, '#' ) );
855 };
856 usort( $funcHooks, $cmpHooks );
857
858 $formatHooks = static function ( &$value ) {
859 // Bidirectional isolation ensures it displays as {{#ns}} and not {{ns#}} in RTL wikis
860 $value = Html::rawElement(
861 'bdi',
862 [],
863 Html::element( 'code', [], '{{' . $value . '}}' )
864 );
865 };
866 array_walk( $funcHooks, $formatHooks );
867
868 $out .= $this->getLanguage()->listToText( $funcHooks );
869
870 # Get a list of parser functions from Parsoid as well.
871 $parsoidHooks = [];
872 $services = MediaWikiServices::getInstance();
873 $siteConfig = $services->getParsoidSiteConfig();
874 $magicWordFactory = $services->getMagicWordFactory();
875 foreach ( $siteConfig->getPFragmentHandlerKeys() as $key ) {
876 $config = $siteConfig->getPFragmentHandlerConfig( $key );
877 if ( !( $config['options']['parserFunction'] ?? false ) ) {
878 continue;
879 }
880 $mw = $magicWordFactory->get( $key );
881 foreach ( $mw->getSynonyms() as $local ) {
882 if ( !( $config['options']['nohash'] ?? false ) ) {
883 $local = '#' . $local;
884 }
885 // Skip hooks already present in legacy hooks (they will
886 // also work in parsoid)
887 if ( isset( $legacyHooks[$local] ) ) {
888 continue;
889 }
890 $parsoidHooks[] = $local;
891 }
892 }
893 if ( $parsoidHooks ) {
894 $out .= Html::element(
895 'h3',
896 [ 'id' => 'mw-version-parser-function-hooks-parsoid' ],
897 $this->msg( 'version-parser-function-hooks-parsoid' )->text()
898 );
899 usort( $parsoidHooks, $cmpHooks );
900 array_walk( $parsoidHooks, $formatHooks );
901 $out .= $this->getLanguage()->listToText( $parsoidHooks );
902 }
903
904 return $out;
905 }
906
912 protected function getParsoidModules() {
913 $siteConfig = MediaWikiServices::getInstance()->getParsoidSiteConfig();
914 $modules = $siteConfig->getExtensionModules();
915
916 if ( !$modules ) {
917 return '';
918 }
919
920 $this->addTocSection( id: 'mw-version-parsoid-modules', msg: 'version-parsoid-modules' );
921
922 $out = Html::rawElement(
923 'h2',
924 [ 'id' => 'mw-version-parsoid-modules' ],
925 Html::rawElement(
926 'span',
927 [ 'class' => 'plainlinks' ],
928 $this->getLinkRenderer()->makeExternalLink(
929 'https://www.mediawiki.org/wiki/Special:MyLanguage/Parsoid',
930 $this->msg( 'version-parsoid-modules' ),
931 $this->getFullTitle()
932 )
933 )
934 );
935
936 $moduleNames = array_map(
937 static fn ( $m )=>Html::element( 'code', [
938 'title' => $m->getConfig()['extension-name'] ?? null,
939 ], $m->getConfig()['name'] ),
940 $modules
941 );
942
943 $out .= $this->getLanguage()->listToText( $moduleNames );
944
945 return $out;
946 }
947
957 protected function getExtensionCategory( $type, ?string $text, array $creditsGroup ) {
958 $out = '';
959
960 if ( $creditsGroup ) {
961 $out .= $this->openExtType( $text, 'credits-' . $type );
962
963 usort( $creditsGroup, $this->compare( ... ) );
964
965 foreach ( $creditsGroup as $extension ) {
966 $out .= $this->getCreditsForExtension( $type, $extension );
967 }
968
969 $out .= Html::closeElement( 'table' );
970 }
971
972 return $out;
973 }
974
981 private function compare( $a, $b ) {
982 return $this->getLanguage()->lc( $a['name'] ) <=> $this->getLanguage()->lc( $b['name'] );
983 }
984
1003 public function getCreditsForExtension( $type, array $extension ) {
1004 $out = $this->getOutput();
1005
1006 // We must obtain the information for all the bits and pieces!
1007 // ... such as extension names and links
1008 if ( isset( $extension['namemsg'] ) ) {
1009 // Localized name of extension
1010 $extensionName = $this->msg( $extension['namemsg'] )->text();
1011 } elseif ( isset( $extension['name'] ) ) {
1012 // Non localized version
1013 $extensionName = $extension['name'];
1014 } else {
1015 $extensionName = $this->msg( 'version-no-ext-name' )->text();
1016 }
1017
1018 if ( isset( $extension['url'] ) ) {
1019 $extensionNameLink = $this->getLinkRenderer()->makeExternalLink(
1020 $extension['url'],
1021 $extensionName,
1022 $this->getFullTitle(),
1023 '',
1024 [ 'class' => 'mw-version-ext-name' ]
1025 );
1026 } else {
1027 $extensionNameLink = htmlspecialchars( $extensionName );
1028 }
1029
1030 // ... and the version information
1031 // If the extension path is set we will check that directory for GIT
1032 // metadata in an attempt to extract date and vcs commit metadata.
1033 $canonicalVersion = '&ndash;';
1034 $extensionPath = null;
1035 $vcsVersion = null;
1036 $vcsLink = null;
1037 $vcsDate = null;
1038
1039 if ( isset( $extension['version'] ) ) {
1040 $canonicalVersion = $out->parseInlineAsInterface( $extension['version'] );
1041 }
1042
1043 if ( isset( $extension['path'] ) ) {
1044 $extensionPath = dirname( $extension['path'] );
1045 if ( $this->coreId == '' ) {
1046 wfDebug( 'Looking up core head id' );
1047 $coreHeadSHA1 = GitInfo::repo()->getHeadSHA1();
1048 if ( $coreHeadSHA1 ) {
1049 $this->coreId = $coreHeadSHA1;
1050 }
1051 }
1052 $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getInstance( CACHE_ANYTHING );
1053 $memcKey = $cache->makeKey(
1054 'specialversion-ext-version-text', $extension['path'], $this->coreId
1055 );
1056 $res = $cache->get( $memcKey );
1057
1058 if ( $res === false ) {
1059 wfDebug( "Getting VCS info for extension {$extension['name']}" );
1060 $gitInfo = new GitInfo( $extensionPath );
1061 $vcsVersion = $gitInfo->getHeadSHA1();
1062 if ( $vcsVersion !== false ) {
1063 $vcsVersion = substr( $vcsVersion, 0, 7 );
1064 $vcsLink = $gitInfo->getHeadViewUrl();
1065 $vcsDate = $gitInfo->getHeadCommitDate();
1066 }
1067 $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
1068 } else {
1069 wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" );
1070 [ $vcsVersion, $vcsLink, $vcsDate ] = $res;
1071 }
1072 }
1073
1074 $versionString = Html::rawElement(
1075 'span',
1076 [ 'class' => 'mw-version-ext-version' ],
1077 $canonicalVersion
1078 );
1079
1080 if ( $vcsVersion ) {
1081 if ( $vcsLink ) {
1082 $vcsVerString = $this->getLinkRenderer()->makeExternalLink(
1083 $vcsLink,
1084 $this->msg( 'version-version', $vcsVersion ),
1085 $this->getFullTitle(),
1086 '',
1087 [ 'class' => 'mw-version-ext-vcs-version' ]
1088 );
1089 } else {
1090 $vcsVerString = Html::element( 'span',
1091 [ 'class' => 'mw-version-ext-vcs-version' ],
1092 "({$vcsVersion})"
1093 );
1094 }
1095 $versionString .= " {$vcsVerString}";
1096
1097 if ( $vcsDate ) {
1098 $versionString .= ' ' . Html::element( 'span', [
1099 'class' => 'mw-version-ext-vcs-timestamp',
1100 'dir' => $this->getLanguage()->getDir(),
1101 ], $this->getLanguage()->timeanddate( $vcsDate, true ) );
1102 }
1103 $versionString = Html::rawElement( 'span',
1104 [ 'class' => 'mw-version-ext-meta-version' ],
1105 $versionString
1106 );
1107 }
1108
1109 // ... and license information; if a license file exists we
1110 // will link to it
1111 $licenseLink = '';
1112 if ( isset( $extension['name'] ) ) {
1113 $licenseName = null;
1114 if ( isset( $extension['license-name'] ) ) {
1115 $licenseName = new HtmlArmor( $out->parseInlineAsInterface( $extension['license-name'] ) );
1116 } elseif ( $extensionPath !== null && ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
1117 $licenseName = $this->msg( 'version-ext-license' )->text();
1118 }
1119 if ( $licenseName !== null ) {
1120 $licenseLink = $this->getLinkRenderer()->makeLink(
1121 $this->getPageTitle( 'License/' . $extension['name'] ),
1122 $licenseName,
1123 [
1124 'class' => 'mw-version-ext-license',
1125 'dir' => 'auto',
1126 ]
1127 );
1128 }
1129 }
1130
1131 // ... and generate the description; which can be a parameterized l10n message
1132 // in the form [ <msgname>, <parameter>, <parameter>... ] or just a straight
1133 // up string
1134 if ( isset( $extension['descriptionmsg'] ) ) {
1135 // Localized description of extension
1136 $descriptionMsg = $extension['descriptionmsg'];
1137
1138 if ( is_array( $descriptionMsg ) ) {
1139 $descriptionMsgKey = array_shift( $descriptionMsg );
1140 $descriptionMsg = array_map( 'htmlspecialchars', $descriptionMsg );
1141 $description = $this->msg( $descriptionMsgKey, ...$descriptionMsg )->text();
1142 } else {
1143 $description = $this->msg( $descriptionMsg )->text();
1144 }
1145 } elseif ( isset( $extension['description'] ) ) {
1146 // Non localized version
1147 $description = $extension['description'];
1148 } else {
1149 $description = '';
1150 }
1151 $description = $out->parseInlineAsInterface( $description );
1152
1153 // ... now get the authors for this extension
1154 $authors = $extension['author'] ?? [];
1155 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable path is set when there is a name
1156 $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
1157
1158 // Finally! Create the table
1159 $html = Html::openElement( 'tr', [
1160 'class' => 'mw-version-ext',
1161 'id' => Sanitizer::escapeIdForAttribute( 'mw-version-ext-' . $type . '-' . $extension['name'] )
1162 ]
1163 );
1164
1165 $html .= Html::rawElement( 'td', [], $extensionNameLink );
1166 $html .= Html::rawElement( 'td', [], $versionString );
1167 $html .= Html::rawElement( 'td', [], $licenseLink );
1168 $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-description' ], $description );
1169 $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-authors' ], $authors );
1170
1171 $html .= Html::closeElement( 'tr' );
1172
1173 return $html;
1174 }
1175
1181 private function getHooks() {
1182 if ( !$this->getConfig()->get( MainConfigNames::SpecialVersionShowHooks ) ) {
1183 return '';
1184 }
1185
1186 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1187 $hookNames = $hookContainer->getHookNames();
1188
1189 if ( !$hookNames ) {
1190 return '';
1191 }
1192
1193 sort( $hookNames );
1194
1195 $ret = [];
1196 $this->addTocSection( id: 'mw-version-hooks', msg: 'version-hooks' );
1197 $ret[] = Html::element(
1198 'h2',
1199 [ 'id' => 'mw-version-hooks' ],
1200 $this->msg( 'version-hooks' )->text()
1201 );
1202 $ret[] = Html::openElement( 'table', [ 'class' => 'wikitable', 'id' => 'sv-hooks' ] );
1203 $ret[] = Html::openElement( 'tr' );
1204 $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-name' )->text() );
1205 $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-subscribedby' )->text() );
1206 $ret[] = Html::closeElement( 'tr' );
1207
1208 foreach ( $hookNames as $name ) {
1209 $handlers = $hookContainer->getHandlerDescriptions( $name );
1210
1211 $ret[] = Html::openElement( 'tr' );
1212 $ret[] = Html::element( 'td', [], $name );
1213 // @phan-suppress-next-line SecurityCheck-DoubleEscaped See FIXME in listToText
1214 $ret[] = Html::element( 'td', [], $this->listToText( $handlers ) );
1215 $ret[] = Html::closeElement( 'tr' );
1216 }
1217
1218 $ret[] = Html::closeElement( 'table' );
1219
1220 return implode( "\n", $ret );
1221 }
1222
1223 private function openExtType( ?string $text = null, ?string $name = null ): string {
1224 $out = '';
1225
1226 $opt = [ 'class' => 'wikitable plainlinks mw-installed-software' ];
1227
1228 if ( $name ) {
1229 $opt['id'] = "sv-$name";
1230 }
1231
1232 $out .= Html::openElement( 'table', $opt );
1233
1234 if ( $text !== null ) {
1235 $out .= Html::element( 'caption', [], $text );
1236 }
1237
1238 if ( $name && $text !== null ) {
1239 $this->addTocSubSection( "sv-$name", 'rawmessage', $text );
1240 }
1241
1242 $firstHeadingMsg = ( $name === 'credits-skin' )
1243 ? 'version-skin-colheader-name'
1244 : 'version-ext-colheader-name';
1245
1246 $out .= $this->getTableHeaderHtml( [
1247 $this->msg( $firstHeadingMsg )->text(),
1248 $this->msg( 'version-ext-colheader-version' )->text(),
1249 $this->msg( 'version-ext-colheader-license' )->text(),
1250 $this->msg( 'version-ext-colheader-description' )->text(),
1251 $this->msg( 'version-ext-colheader-credits' )->text()
1252 ] );
1253
1254 return $out;
1255 }
1256
1265 private function getTableHeaderHtml( $headers ): string {
1266 $out = '';
1267 foreach ( $headers as $header ) {
1268 $out .= Html::element( 'th', [ 'scope' => 'col' ], $header );
1269 }
1270 return Html::rawElement( 'thead', [],
1271 Html::rawElement( 'tr', [], $out )
1272 );
1273 }
1274
1280 private function IPInfo() {
1281 $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
1282
1283 return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
1284 }
1285
1306 public function listAuthors( $authors, $extName, $extDir ): string {
1307 $hasOthers = false;
1308 $linkRenderer = $this->getLinkRenderer();
1309
1310 $list = [];
1311 $authors = (array)$authors;
1312
1313 // Special case: if the authors array has only one item and it is "...",
1314 // it should not be rendered as the "version-poweredby-others" i18n msg,
1315 // but rather as "version-poweredby-various" i18n msg instead.
1316 if ( count( $authors ) === 1 && $authors[0] === '...' ) {
1317 // Link to the extension's or skin's AUTHORS or CREDITS file, if there is
1318 // such a file; otherwise just return the i18n msg as-is
1319 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1320 return $linkRenderer->makeLink(
1321 $this->getPageTitle( "Credits/$extName" ),
1322 $this->msg( 'version-poweredby-various' )->text()
1323 );
1324 } else {
1325 return $this->msg( 'version-poweredby-various' )->escaped();
1326 }
1327 }
1328
1329 // Otherwise, if we have an actual array that has more than one item,
1330 // process each array item as usual
1331 foreach ( $authors as $item ) {
1332 if ( $item instanceof HtmlArmor ) {
1333 $list[] = HtmlArmor::getHtml( $item );
1334 } elseif ( $item === '...' ) {
1335 $hasOthers = true;
1336
1337 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1338 $text = $linkRenderer->makeLink(
1339 $this->getPageTitle( "Credits/$extName" ),
1340 $this->msg( 'version-poweredby-others' )->text()
1341 );
1342 } else {
1343 $text = $this->msg( 'version-poweredby-others' )->escaped();
1344 }
1345 $list[] = $text;
1346 } elseif ( str_ends_with( $item, ' ...]' ) ) {
1347 $hasOthers = true;
1348 $list[] = $this->getOutput()->parseInlineAsInterface(
1349 substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
1350 );
1351 } else {
1352 $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1353 }
1354 }
1355
1356 if ( $extName && !$hasOthers && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1357 $list[] = $linkRenderer->makeLink(
1358 $this->getPageTitle( "Credits/$extName" ),
1359 $this->msg( 'version-poweredby-others' )->text()
1360 );
1361 }
1362
1363 return $this->listToText( $list, false );
1364 }
1365
1375 private function listToText( array $list, bool $sort = true ): string {
1376 if ( !$list ) {
1377 return '';
1378 }
1379 if ( $sort ) {
1380 sort( $list );
1381 }
1382
1383 return $this->getLanguage()
1384 ->listToText( array_map( self::arrayToString( ... ), $list ) );
1385 }
1386
1396 public static function arrayToString( $list ) {
1397 if ( is_array( $list ) && count( $list ) == 1 ) {
1398 $list = $list[0];
1399 }
1400 if ( $list instanceof Closure ) {
1401 // Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$"
1402 return 'Closure';
1403 } elseif ( is_object( $list ) ) {
1404 return wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1405 } elseif ( !is_array( $list ) ) {
1406 return $list;
1407 } else {
1408 if ( is_object( $list[0] ) ) {
1409 $class = get_class( $list[0] );
1410 } else {
1411 $class = $list[0];
1412 }
1413
1414 return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1415 }
1416 }
1417
1423 public static function getGitHeadSha1( $dir ) {
1424 wfDeprecated( __METHOD__, '1.41' );
1425 return ( new GitInfo( $dir ) )->getHeadSHA1();
1426 }
1427
1432 public function getEntryPointInfo() {
1433 $config = $this->getConfig();
1434 $scriptPath = $config->get( MainConfigNames::ScriptPath ) ?: '/';
1435
1436 $entryPoints = [
1437 'version-entrypoints-articlepath' => $config->get( MainConfigNames::ArticlePath ),
1438 'version-entrypoints-scriptpath' => $scriptPath,
1439 'version-entrypoints-index-php' => wfScript( 'index' ),
1440 'version-entrypoints-api-php' => wfScript( 'api' ),
1441 'version-entrypoints-rest-php' => wfScript( 'rest' ),
1442 ];
1443
1444 $language = $this->getLanguage();
1445 $thAttributes = [
1446 'dir' => $language->getDir(),
1447 'lang' => $language->getHtmlCode(),
1448 'scope' => 'col'
1449 ];
1450
1451 $this->addTocSection( id: 'mw-version-entrypoints', msg: 'version-entrypoints' );
1452
1453 $out = Html::element(
1454 'h2',
1455 [ 'id' => 'mw-version-entrypoints' ],
1456 $this->msg( 'version-entrypoints' )->text()
1457 ) .
1458 Html::openElement( 'table',
1459 [
1460 'class' => 'wikitable plainlinks',
1461 'id' => 'mw-version-entrypoints-table',
1462 'dir' => 'ltr',
1463 'lang' => 'en'
1464 ]
1465 ) .
1466 Html::openElement( 'thead' ) .
1467 Html::openElement( 'tr' ) .
1468 Html::element(
1469 'th',
1470 $thAttributes,
1471 $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1472 ) .
1473 Html::element(
1474 'th',
1475 $thAttributes,
1476 $this->msg( 'version-entrypoints-header-url' )->text()
1477 ) .
1478 Html::closeElement( 'tr' ) .
1479 Html::closeElement( 'thead' );
1480
1481 foreach ( $entryPoints as $message => $value ) {
1482 $url = $this->urlUtils->expand( $value, PROTO_RELATIVE );
1483 $out .= Html::openElement( 'tr' ) .
1484 Html::rawElement( 'td', [], $this->msg( $message )->parse() ) .
1485 Html::rawElement( 'td', [],
1486 Html::rawElement(
1487 'code',
1488 [],
1489 $this->msg( new RawMessage( "[$url $value]" ) )->parse()
1490 )
1491 ) .
1492 Html::closeElement( 'tr' );
1493 }
1494
1495 $out .= Html::closeElement( 'table' );
1496
1497 return $out;
1498 }
1499
1501 protected function getGroupName() {
1502 return 'wiki';
1503 }
1504}
1505
1510class_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(MW_ENTRY_POINT==='index') if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgLang
Definition Setup.php:551
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
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:43
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:32
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 getVersionLinked()
Return a wikitext-formatted string of the MediaWiki version with a link to the Git SHA1 of head if av...
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)