87 parent::__construct(
'Version' );
88 $this->parserFactory = $parserFactory;
89 $this->urlUtils = $urlUtils;
90 $this->dbProvider = $dbProvider;
103 $credits[$credit[
'type']][] = $credit;
112 $config = $this->getConfig();
113 $credits = self::getCredits( ExtensionRegistry::getInstance(), $config );
116 $this->outputHeader();
117 $out = $this->getOutput();
118 $out->getMetadata()->setPreventClickjacking(
false );
121 $parts = explode(
'/', (
string)$par );
123 if ( isset( $parts[1] ) ) {
124 $extName = str_replace(
'_',
' ', $parts[1] );
126 foreach ( $credits as $extensions ) {
127 foreach ( $extensions as $ext ) {
128 if ( isset( $ext[
'name'] ) && ( $ext[
'name'] === $extName ) ) {
135 $out->setStatusCode( 404 );
138 $extName =
'MediaWiki';
142 switch ( strtolower( $parts[0] ) ) {
144 $out->addModuleStyles(
'mediawiki.special' );
146 $wikiText =
'{{int:version-credits-not-found}}';
147 if ( $extName ===
'MediaWiki' ) {
148 $wikiText = file_get_contents( MW_INSTALL_PATH .
'/CREDITS' );
150 $wikiText = str_replace(
151 [
'<!-- BEGIN CONTRIBUTOR LIST -->',
'<!-- END CONTRIBUTOR LIST -->' ],
152 [
'<div class="mw-version-credits">',
'</div>' ],
155 } elseif ( ( $extNode !==
null ) && isset( $extNode[
'path'] ) ) {
156 $file = ExtensionInfo::getAuthorsFileName( dirname( $extNode[
'path'] ) );
158 $wikiText = file_get_contents( $file );
159 if ( str_ends_with( $file,
'.txt' ) ) {
172 $out->setPageTitleMsg( $this->msg(
'version-credits-title' )->plaintextParams( $extName ) );
173 $out->addWikiTextAsInterface( $wikiText );
177 $out->setPageTitleMsg( $this->msg(
'version-license-title' )->plaintextParams( $extName ) );
179 $licenseFound =
false;
181 if ( $extName ===
'MediaWiki' ) {
182 $out->addWikiTextAsInterface(
183 file_get_contents( MW_INSTALL_PATH .
'/COPYING' )
185 $licenseFound =
true;
186 } elseif ( ( $extNode !==
null ) && isset( $extNode[
'path'] ) ) {
187 $files = ExtensionInfo::getLicenseFileNames( dirname( $extNode[
'path'] ) );
189 $licenseFound =
true;
190 foreach ( $files as $file ) {
191 $out->addWikiTextAsInterface(
198 file_get_contents( $file )
204 if ( !$licenseFound ) {
205 $out->addWikiTextAsInterface(
'{{int:version-license-not-found}}' );
210 $out->addModuleStyles(
'mediawiki.special' );
212 $out->addHTML( $this->getMediaWikiCredits() );
214 $this->tocData =
new TOCData();
216 $this->tocSection = 0;
217 $this->tocSubSection = 0;
221 $this->softwareInformation(),
222 $this->getEntryPointInfo(),
223 $this->getSkinCredits( $credits ),
224 $this->getExtensionCredits( $credits ),
225 $this->getLibraries( $credits ),
226 $this->getParserTags(),
227 $this->getParserFunctionHooks(),
228 $this->getParsoidModules(),
234 $out->addTOCPlaceholder( $this->tocData );
237 foreach ( $sections as $content ) {
238 $out->addHTML( $content );
252 private function addTocSection( $labelMsg, $id ) {
255 $this->tocSubSection = 0;
256 $this->tocData->addSection(
new SectionMetadata(
259 $this->msg( $labelMsg )->escaped(),
260 $this->getLanguage()->formatNum( $this->tocSection ),
261 (
string)$this->tocIndex,
276 private function addTocSubSection( $label, $id ) {
278 $this->tocSubSection++;
279 $this->tocData->addSection(
new SectionMetadata(
282 htmlspecialchars( $label ),
284 $this->getLanguage()->formatNum( $this->tocSection ) .
'.' .
285 $this->getLanguage()->formatNum( $this->tocSubSection ),
286 (
string)$this->tocIndex,
299 private function getMediaWikiCredits() {
304 [
'id' =>
'mw-version-license' ],
305 $this->msg(
'version-license' )->text()
308 $ret .= Html::rawElement(
'div', [
'class' =>
'plainlinks' ],
309 $this->msg(
new RawMessage( self::getCopyrightAndAuthorList() ) )->parseAsBlock() .
310 Html::rawElement(
'div', [
'class' =>
'mw-version-license-info' ],
311 $this->msg(
'version-license-info' )->parseAsBlock()
325 if ( defined(
'MEDIAWIKI_INSTALL' ) ) {
326 $othersLink =
'[https://www.mediawiki.org/wiki/Special:Version/Credits ' .
327 wfMessage(
'version-poweredby-others' )->plain() .
']';
329 $othersLink =
'[[Special:Version/Credits|' .
330 wfMessage(
'version-poweredby-others' )->plain() .
']]';
333 $translatorsLink =
'[https://translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
334 wfMessage(
'version-poweredby-translators' )->plain() .
']';
337 'Magnus Manske',
'Brooke Vibber',
'Lee Daniel Crocker',
338 'Tim Starling',
'Erik Möller',
'Gabriel Wicke',
'Ævar Arnfjörð Bjarmason',
339 'Niklas Laxström',
'Domas Mituzas',
'Rob Church',
'Yuri Astrakhan',
340 'Aryeh Gregor',
'Aaron Schulz',
'Andrew Garrett',
'Raimond Spekking',
341 'Alexandre Emsenhuber',
'Siebrand Mazeland',
'Chad Horohoe',
342 'Roan Kattouw',
'Trevor Parscal',
'Bryan Tong Minh',
'Sam Reed',
343 'Victor Vasiliev',
'Rotem Liss',
'Platonides',
'Antoine Musso',
344 'Timo Tijhof',
'Daniel Kinzler',
'Jeroen De Dauw',
'Brad Jorsch',
345 'Bartosz Dziewoński',
'Ed Sanders',
'Moriel Schottlender',
346 'Kunal Mehta',
'James D. Forrester',
'Brian Wolff',
'Adam Shorland',
347 'DannyS712',
'Ori Livneh',
'Max Semenik',
'Amir Sarabadani',
348 'Derk-Jan Hartman',
'Petr Pchelko',
'Umherirrender',
'C. Scott Ananian',
349 'fomafix',
'Thiemo Kreuz',
'Gergő Tisza',
'Volker E.',
350 'Jack Phoenix',
'Isarra Yos',
351 $othersLink, $translatorsLink
354 return wfMessage(
'version-poweredby-credits', MWTimestamp::getLocalInstance()->format(
'Y' ),
363 private function getSoftwareInformation() {
364 $dbr = $this->dbProvider->getReplicaDatabase();
370 '[https://www.mediawiki.org/ MediaWiki]' => self::getVersionLinked(),
371 '[https://php.net/ PHP]' => PHP_VERSION .
" (" . PHP_SAPI .
")",
372 '[https://icu.unicode.org/ ICU]' => INTL_ICU_VERSION,
373 $dbr->getSoftwareLink() => $dbr->getServerInfo(),
377 if ( phpversion(
"wikidiff2" ) ) {
378 $software[
'[https://www.mediawiki.org/wiki/Wikidiff2 wikidiff2]' ] = phpversion(
"wikidiff2" );
382 $this->getHookRunner()->onSoftwareInfo( $software );
392 private function softwareInformation() {
393 $this->addTocSection(
'version-software',
'mw-version-software' );
397 [
'id' =>
'mw-version-software' ],
398 $this->msg(
'version-software' )->text()
401 $out .= Html::openElement(
'table', [
'class' =>
'wikitable plainlinks',
'id' =>
'sv-software' ] );
403 $out .= $this->getTableHeaderHtml( [
404 $this->msg(
'version-software-product' )->text(),
405 $this->msg(
'version-software-version' )->text()
408 foreach ( $this->getSoftwareInformation() as $name => $version ) {
409 $out .= Html::rawElement(
412 Html::rawElement(
'td', [], $this->msg(
new RawMessage( $name ) )->parse() ) .
413 Html::rawElement(
'td', [
'dir' =>
'ltr' ], $this->msg(
new RawMessage( $version ) )->parse() )
417 $out .= Html::closeElement(
'table' );
431 public static function getVersion( $flags =
'', $lang =
null ) {
432 $gitInfo = GitInfo::repo()->getHeadSHA1();
435 } elseif ( $flags ===
'nodb' ) {
436 $shortSha1 = substr( $gitInfo, 0, 7 );
439 $shortSha1 = substr( $gitInfo, 0, 7 );
441 if ( $lang !==
null ) {
442 $msg->inLanguage( $lang );
444 $shortSha1 = $msg->params( $shortSha1 )->text();
459 return self::getVersionLinkedGit() ?:
MW_VERSION;
465 private static function getMWVersionLinked() {
468 if ( $hookRunner->onSpecialVersionVersionUrl(
MW_VERSION, $versionUrl ) ) {
470 preg_match(
"/^(\d+\.\d+)/",
MW_VERSION, $versionParts );
471 $versionUrl =
"https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
474 return '[' . $versionUrl .
' ' .
MW_VERSION .
']';
482 private static function getVersionLinkedGit() {
485 $gitInfo =
new GitInfo( MW_INSTALL_PATH );
486 $headSHA1 = $gitInfo->getHeadSHA1();
491 $shortSHA1 =
'(' . substr( $headSHA1, 0, 7 ) .
')';
493 $gitHeadUrl = $gitInfo->getHeadViewUrl();
494 if ( $gitHeadUrl !==
false ) {
495 $shortSHA1 =
"[$gitHeadUrl $shortSHA1]";
498 $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
499 if ( $gitHeadCommitDate ) {
503 return self::getMWVersionLinked() .
" $shortSHA1";
516 if ( self::$extensionTypes === false ) {
517 self::$extensionTypes = [
518 'specialpage' =>
wfMessage(
'version-specialpages' )->text(),
519 'editor' =>
wfMessage(
'version-editors' )->text(),
520 'parserhook' =>
wfMessage(
'version-parserhooks' )->text(),
521 'variable' =>
wfMessage(
'version-variables' )->text(),
522 'media' =>
wfMessage(
'version-mediahandlers' )->text(),
523 'antispam' =>
wfMessage(
'version-antispam' )->text(),
524 'skin' =>
wfMessage(
'version-skins' )->text(),
525 'api' =>
wfMessage(
'version-api' )->text(),
526 'other' =>
wfMessage(
'version-other' )->text(),
530 ->onExtensionTypes( self::$extensionTypes );
533 return self::$extensionTypes;
546 $types = self::getExtensionTypes();
548 return $types[$type] ?? $types[
'other'];
557 private function getExtensionCredits( array $credits ) {
558 $extensionTypes = self::getExtensionTypes();
560 $this->addTocSection(
'version-extensions',
'mw-version-ext' );
562 $out = Html::element(
564 [
'id' =>
'mw-version-ext' ],
565 $this->msg(
'version-extensions' )->text()
571 ( count( $credits ) === 1 && isset( $credits[
'skin'] ) )
573 $out .= Html::element(
576 $this->msg(
'version-extensions-no-ext' )->text()
583 $credits[
'other'] ??= [];
584 foreach ( $credits as $type => $extensions ) {
585 if ( !array_key_exists( $type, $extensionTypes ) ) {
586 $credits[
'other'] = array_merge( $credits[
'other'], $extensions );
591 foreach ( $extensionTypes as $type => $text ) {
593 if ( $type !==
'other' && $type !==
'skin' ) {
594 $out .= $this->getExtensionCategory( $type, $text, $credits[$type] ?? [] );
599 $out .= $this->getExtensionCategory(
'other', $extensionTypes[
'other'], $credits[
'other'] );
610 private function getSkinCredits( array $credits ) {
611 $this->addTocSection(
'version-skins',
'mw-version-skin' );
613 $out = Html::element(
615 [
'id' =>
'mw-version-skin' ],
616 $this->msg(
'version-skins' )->text()
619 if ( !isset( $credits[
'skin'] ) || !$credits[
'skin'] ) {
620 $out .= Html::element(
623 $this->msg(
'version-skins-no-skin' )->text()
628 $out .= $this->getExtensionCategory(
'skin',
null, $credits[
'skin'] );
640 $this->addTocSection(
'version-libraries',
'mw-version-libraries' );
642 $out = Html::element(
644 [
'id' =>
'mw-version-libraries' ],
645 $this->msg(
'version-libraries' )->text()
649 . $this->getExternalLibraries( $credits )
650 . $this->getClientSideLibraries();
661 MW_INSTALL_PATH .
'/vendor/composer/installed.json'
664 $extensionTypes = self::getExtensionTypes();
665 foreach ( $extensionTypes as $type => $message ) {
666 if ( !isset( $credits[$type] ) || $credits[$type] === [] ) {
669 foreach ( $credits[$type] as $extension ) {
670 if ( !isset( $extension[
'path'] ) ) {
673 $paths[] = dirname( $extension[
'path'] ) .
'/vendor/composer/installed.json';
679 foreach ( $paths as
$path ) {
680 if ( !file_exists(
$path ) ) {
686 $dependencies += $installed->getInstalledDependencies();
689 ksort( $dependencies );
690 return $dependencies;
700 $dependencies = self::parseComposerInstalled( $credits );
701 if ( $dependencies === [] ) {
705 $this->addTocSubSection( $this->msg(
'version-libraries-server' )->text(),
'mw-version-libraries-server' );
707 $out = Html::element(
709 [
'id' =>
'mw-version-libraries-server' ],
710 $this->msg(
'version-libraries-server' )->text()
712 $out .= Html::openElement(
714 [
'class' =>
'wikitable plainlinks mw-installed-software',
'id' =>
'sv-libraries' ]
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-description' )->text(),
722 $this->msg(
'version-libraries-authors' )->text(),
725 foreach ( $dependencies as $name => $info ) {
726 if ( !is_array( $info ) || str_starts_with( $info[
'type'],
'mediawiki-' ) ) {
731 $authors = array_map(
static function ( $arr ) {
732 return new HtmlArmor( isset( $arr[
'homepage'] ) ?
733 Html::element(
'a', [
'href' => $arr[
'homepage'] ], $arr[
'name'] ) :
734 htmlspecialchars( $arr[
'name'] )
736 }, $info[
'authors'] );
737 $authors = $this->listAuthors( $authors,
false, MW_INSTALL_PATH .
"/vendor/$name" );
742 $out .= Html::openElement(
'tr', [
745 'id' => Sanitizer::escapeIdForAttribute(
746 "mw-version-library-$name"
751 $this->getLinkRenderer()->makeExternalLink(
752 "https://packagist.org/packages/$name",
754 $this->getFullTitle(),
756 [
'class' =>
'mw-version-library-name' ]
759 . Html::element(
'td', [
'dir' =>
'auto' ], $info[
'version'] )
761 . Html::element(
'td', [
'dir' =>
'auto' ], $this->listToText( $info[
'licenses'] ) )
762 . Html::element(
'td', [
'lang' =>
'en',
'dir' =>
'ltr' ], $info[
'description'] )
763 . Html::rawElement(
'td', [], $authors )
764 . Html::closeElement(
'tr' );
766 $out .= Html::closeElement(
'table' );
777 $registryDirs = [
'MediaWiki' => MW_INSTALL_PATH .
'/resources/lib' ]
778 + ExtensionRegistry::getInstance()->getAttribute(
'ForeignResourcesDir' );
781 foreach ( $registryDirs as
$source => $registryDir ) {
782 $foreignResources = Yaml::parseFile(
"$registryDir/foreign-resources.yaml" );
783 foreach ( $foreignResources as $name => $module ) {
784 $key = $name . $module[
'version'];
785 if ( isset( $modules[$key] ) ) {
786 $modules[$key][
'source'][] =
$source;
789 $modules[$key] = $module + [
'name' => $name,
'source' => [
$source ] ];
801 private function getClientSideLibraries() {
802 $this->addTocSubSection( $this->msg(
'version-libraries-client' )->text(),
'mw-version-libraries-client' );
804 $out = Html::element(
806 [
'id' =>
'mw-version-libraries-client' ],
807 $this->msg(
'version-libraries-client' )->text()
809 $out .= Html::openElement(
811 [
'class' =>
'wikitable plainlinks mw-installed-software',
'id' =>
'sv-libraries-client' ]
814 $out .= $this->getTableHeaderHtml( [
815 $this->msg(
'version-libraries-library' )->text(),
816 $this->msg(
'version-libraries-version' )->text(),
817 $this->msg(
'version-libraries-license' )->text(),
818 $this->msg(
'version-libraries-authors' )->text(),
819 $this->msg(
'version-libraries-source' )->text()
822 foreach ( self::parseForeignResources() as $name => $info ) {
826 $out .= Html::openElement(
'tr', [
829 'id' => Sanitizer::escapeIdForAttribute(
830 "mw-version-library-$name"
835 $this->getLinkRenderer()->makeExternalLink(
838 $this->getFullTitle(),
840 [
'class' =>
'mw-version-library-name' ]
843 . Html::element(
'td', [
'dir' =>
'auto' ], $info[
'version'] )
844 . Html::element(
'td', [
'dir' =>
'auto' ], $info[
'license'] )
845 . Html::element(
'td', [
'dir' =>
'auto' ], $info[
'authors'] ??
'—' )
847 . Html::element(
'td', [
'dir' =>
'auto' ], $this->listToText( $info[
'source'] ) )
848 . Html::closeElement(
'tr' );
850 $out .= Html::closeElement(
'table' );
861 $tags = $this->parserFactory->getMainInstance()->getTags();
866 $this->addTocSection(
'version-parser-extensiontags',
'mw-version-parser-extensiontags' );
868 $out = Html::rawElement(
870 [
'id' =>
'mw-version-parser-extensiontags' ],
873 [
'class' =>
'plainlinks' ],
874 $this->getLinkRenderer()->makeExternalLink(
875 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
876 $this->msg(
'version-parser-extensiontags' ),
877 $this->getFullTitle()
882 array_walk( $tags,
static function ( &$value ) {
884 $value = Html::rawElement(
888 'style' =>
'white-space: nowrap;',
890 Html::element(
'code', [],
"<$value>" )
894 $out .= $this->listToText( $tags );
905 $funcHooks = $this->parserFactory->getMainInstance()->getFunctionHooks();
910 $this->addTocSection(
'version-parser-function-hooks',
'mw-version-parser-function-hooks' );
912 $out = Html::rawElement(
914 [
'id' =>
'mw-version-parser-function-hooks' ],
917 [
'class' =>
'plainlinks' ],
918 $this->getLinkRenderer()->makeExternalLink(
919 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
920 $this->msg(
'version-parser-function-hooks' ),
921 $this->getFullTitle()
926 $funcSynonyms = $this->parserFactory->getMainInstance()->getFunctionSynonyms();
933 $preferredSynonyms = array_flip( array_reverse( $funcSynonyms[1] + $funcSynonyms[0] ) );
934 array_walk( $funcHooks,
static function ( &$value ) use ( $preferredSynonyms ) {
935 $value = $preferredSynonyms[$value];
939 usort( $funcHooks,
static function ( $a, $b ) {
940 return strcasecmp( ltrim( $a,
'#' ), ltrim( $b,
'#' ) );
943 array_walk( $funcHooks,
static function ( &$value ) {
945 $value = Html::rawElement(
948 Html::element(
'code', [],
'{{' . $value .
'}}' )
952 $out .= $this->getLanguage()->listToText( $funcHooks );
963 $siteConfig = MediaWikiServices::getInstance()->getParsoidSiteConfig();
964 $modules = $siteConfig->getExtensionModules();
970 $this->addTocSection(
'version-parsoid-modules',
'mw-version-parsoid-modules' );
972 $out = Html::rawElement(
974 [
'id' =>
'mw-version-parsoid-modules' ],
977 [
'class' =>
'plainlinks' ],
978 $this->getLinkRenderer()->makeExternalLink(
979 'https://www.mediawiki.org/wiki/Special:MyLanguage/Parsoid',
980 $this->msg(
'version-parsoid-modules' ),
981 $this->getFullTitle()
986 $moduleNames = array_map(
987 static fn ( $m )=>Html::element(
'code', [], $m->getConfig()[
'name'] ),
991 $out .= $this->getLanguage()->listToText( $moduleNames );
1008 if ( $creditsGroup ) {
1009 $out .= $this->openExtType( $text,
'credits-' . $type );
1011 usort( $creditsGroup, [ $this,
'compare' ] );
1013 foreach ( $creditsGroup as $extension ) {
1014 $out .= $this->getCreditsForExtension( $type, $extension );
1017 $out .= Html::closeElement(
'table' );
1030 return $this->getLanguage()->lc( $a[
'name'] ) <=> $this->getLanguage()->lc( $b[
'name'] );
1052 $out = $this->getOutput();
1056 if ( isset( $extension[
'namemsg'] ) ) {
1058 $extensionName = $this->msg( $extension[
'namemsg'] );
1059 } elseif ( isset( $extension[
'name'] ) ) {
1061 $extensionName = $extension[
'name'];
1063 $extensionName = $this->msg(
'version-no-ext-name' );
1066 if ( isset( $extension[
'url'] ) ) {
1067 $extensionNameLink = $this->getLinkRenderer()->makeExternalLink(
1070 $this->getFullTitle(),
1072 [
'class' =>
'mw-version-ext-name' ]
1075 $extensionNameLink = htmlspecialchars( $extensionName );
1081 $canonicalVersion =
'–';
1082 $extensionPath =
null;
1087 if ( isset( $extension[
'version'] ) ) {
1088 $canonicalVersion = $out->parseInlineAsInterface( $extension[
'version'] );
1091 if ( isset( $extension[
'path'] ) ) {
1092 $extensionPath = dirname( $extension[
'path'] );
1093 if ( $this->coreId ==
'' ) {
1094 wfDebug(
'Looking up core head id' );
1095 $coreHeadSHA1 = GitInfo::repo()->getHeadSHA1();
1096 if ( $coreHeadSHA1 ) {
1097 $this->coreId = $coreHeadSHA1;
1100 $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getInstance(
CACHE_ANYTHING );
1101 $memcKey = $cache->makeKey(
1102 'specialversion-ext-version-text', $extension[
'path'], $this->coreId
1104 [ $vcsVersion, $vcsLink, $vcsDate ] = $cache->get( $memcKey );
1106 if ( !$vcsVersion ) {
1107 wfDebug(
"Getting VCS info for extension {$extension['name']}" );
1108 $gitInfo =
new GitInfo( $extensionPath );
1109 $vcsVersion = $gitInfo->getHeadSHA1();
1110 if ( $vcsVersion !==
false ) {
1111 $vcsVersion = substr( $vcsVersion, 0, 7 );
1112 $vcsLink = $gitInfo->getHeadViewUrl();
1113 $vcsDate = $gitInfo->getHeadCommitDate();
1115 $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
1117 wfDebug(
"Pulled VCS info for extension {$extension['name']} from cache" );
1121 $versionString = Html::rawElement(
1123 [
'class' =>
'mw-version-ext-version' ],
1127 if ( $vcsVersion ) {
1129 $vcsVerString = $this->getLinkRenderer()->makeExternalLink(
1131 $this->msg(
'version-version', $vcsVersion ),
1132 $this->getFullTitle(),
1134 [
'class' =>
'mw-version-ext-vcs-version' ]
1137 $vcsVerString = Html::element(
'span',
1138 [
'class' =>
'mw-version-ext-vcs-version' ],
1142 $versionString .=
" {$vcsVerString}";
1145 $versionString .=
' ' . Html::element(
'span', [
1146 'class' =>
'mw-version-ext-vcs-timestamp',
1147 'dir' => $this->getLanguage()->getDir(),
1148 ], $this->getLanguage()->timeanddate( $vcsDate,
true ) );
1150 $versionString = Html::rawElement(
'span',
1151 [
'class' =>
'mw-version-ext-meta-version' ],
1159 if ( isset( $extension[
'name'] ) ) {
1160 $licenseName =
null;
1161 if ( isset( $extension[
'license-name'] ) ) {
1162 $licenseName =
new HtmlArmor( $out->parseInlineAsInterface( $extension[
'license-name'] ) );
1163 } elseif ( $extensionPath !==
null && ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
1164 $licenseName = $this->msg(
'version-ext-license' )->text();
1166 if ( $licenseName !==
null ) {
1167 $licenseLink = $this->getLinkRenderer()->makeLink(
1168 $this->getPageTitle(
'License/' . $extension[
'name'] ),
1171 'class' =>
'mw-version-ext-license',
1181 if ( isset( $extension[
'descriptionmsg'] ) ) {
1183 $descriptionMsg = $extension[
'descriptionmsg'];
1185 if ( is_array( $descriptionMsg ) ) {
1186 $descriptionMsgKey = array_shift( $descriptionMsg );
1187 $descriptionMsg = array_map(
'htmlspecialchars', $descriptionMsg );
1188 $description = $this->msg( $descriptionMsgKey, ...$descriptionMsg )->text();
1190 $description = $this->msg( $descriptionMsg )->text();
1192 } elseif ( isset( $extension[
'description'] ) ) {
1194 $description = $extension[
'description'];
1198 $description = $out->parseInlineAsInterface( $description );
1201 $authors = $extension[
'author'] ?? [];
1203 $authors = $this->listAuthors( $authors, $extension[
'name'], $extensionPath );
1206 $html = Html::openElement(
'tr', [
1207 'class' =>
'mw-version-ext',
1208 'id' => Sanitizer::escapeIdForAttribute(
'mw-version-ext-' . $type .
'-' . $extension[
'name'] )
1212 $html .= Html::rawElement(
'td', [], $extensionNameLink );
1213 $html .= Html::rawElement(
'td', [], $versionString );
1214 $html .= Html::rawElement(
'td', [], $licenseLink );
1215 $html .= Html::rawElement(
'td', [
'class' =>
'mw-version-ext-description' ], $description );
1216 $html .= Html::rawElement(
'td', [
'class' =>
'mw-version-ext-authors' ], $authors );
1218 $html .= Html::closeElement(
'tr' );
1228 private function getHooks() {
1229 if ( !$this->getConfig()->
get( MainConfigNames::SpecialVersionShowHooks ) ) {
1233 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1234 $hookNames = $hookContainer->getHookNames();
1236 if ( !$hookNames ) {
1243 $this->addTocSection(
'version-hooks',
'mw-version-hooks' );
1244 $ret[] = Html::element(
1246 [
'id' =>
'mw-version-hooks' ],
1247 $this->msg(
'version-hooks' )->text()
1249 $ret[] = Html::openElement(
'table', [
'class' =>
'wikitable',
'id' =>
'sv-hooks' ] );
1250 $ret[] = Html::openElement(
'tr' );
1251 $ret[] = Html::element(
'th', [], $this->msg(
'version-hook-name' )->text() );
1252 $ret[] = Html::element(
'th', [], $this->msg(
'version-hook-subscribedby' )->text() );
1253 $ret[] = Html::closeElement(
'tr' );
1255 foreach ( $hookNames as $name ) {
1256 $handlers = $hookContainer->getHandlerDescriptions( $name );
1258 $ret[] = Html::openElement(
'tr' );
1259 $ret[] = Html::element(
'td', [], $name );
1261 $ret[] = Html::element(
'td', [], $this->listToText( $handlers ) );
1262 $ret[] = Html::closeElement(
'tr' );
1265 $ret[] = Html::closeElement(
'table' );
1267 return implode(
"\n", $ret );
1270 private function openExtType( ?
string $text =
null, ?
string $name =
null ): string {
1273 $opt = [
'class' =>
'wikitable plainlinks mw-installed-software' ];
1276 $opt[
'id'] =
"sv-$name";
1279 $out .= Html::openElement(
'table', $opt );
1281 if ( $text !==
null ) {
1282 $out .= Html::element(
'caption', [], $text );
1285 if ( $name && $text !==
null ) {
1286 $this->addTocSubSection( $text,
"sv-$name" );
1289 $firstHeadingMsg = ( $name ===
'credits-skin' )
1290 ?
'version-skin-colheader-name'
1291 :
'version-ext-colheader-name';
1293 $out .= $this->getTableHeaderHtml( [
1294 $this->msg( $firstHeadingMsg )->text(),
1295 $this->msg(
'version-ext-colheader-version' )->text(),
1296 $this->msg(
'version-ext-colheader-license' )->text(),
1297 $this->msg(
'version-ext-colheader-description' )->text(),
1298 $this->msg(
'version-ext-colheader-credits' )->text()
1312 private function getTableHeaderHtml( $headers ): string {
1314 $out .= Html::openElement(
'thead' );
1315 $out .= Html::openElement(
'tr' );
1316 foreach ( $headers as
$header ) {
1317 $out .= Html::element(
'th', [
'scope' =>
'col' ],
$header );
1319 $out .= Html::closeElement(
'tr' );
1320 $out .= Html::closeElement(
'thead' );
1329 private function IPInfo() {
1330 $ip = str_replace(
'--',
' - ', htmlspecialchars( $this->
getRequest()->getIP() ) );
1332 return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
1357 $linkRenderer = $this->getLinkRenderer();
1360 $authors = (array)$authors;
1365 if ( count( $authors ) === 1 && $authors[0] ===
'...' ) {
1368 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1369 return $linkRenderer->makeLink(
1370 $this->getPageTitle(
"Credits/$extName" ),
1371 $this->msg(
'version-poweredby-various' )->text()
1374 return $this->msg(
'version-poweredby-various' )->escaped();
1380 foreach ( $authors as $item ) {
1382 $list[] = HtmlArmor::getHtml( $item );
1383 } elseif ( $item ===
'...' ) {
1386 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1387 $text = $linkRenderer->makeLink(
1388 $this->getPageTitle(
"Credits/$extName" ),
1389 $this->msg(
'version-poweredby-others' )->text()
1392 $text = $this->msg(
'version-poweredby-others' )->escaped();
1395 } elseif ( str_ends_with( $item,
' ...]' ) ) {
1397 $list[] = $this->getOutput()->parseInlineAsInterface(
1398 substr( $item, 0, -4 ) . $this->msg(
'version-poweredby-others' )->text() .
"]"
1401 $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1405 if ( $extName && !$hasOthers && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1406 $list[] = $linkRenderer->makeLink(
1407 $this->getPageTitle(
"Credits/$extName" ),
1408 $this->msg(
'version-poweredby-others' )->text()
1412 return $this->listToText( $list,
false );
1424 private function listToText( array $list,
bool $sort =
true ): string {
1432 return $this->getLanguage()
1433 ->listToText( array_map( [ __CLASS__,
'arrayToString' ], $list ) );
1446 if ( is_array( $list ) && count( $list ) == 1 ) {
1449 if ( $list instanceof Closure ) {
1452 } elseif ( is_object( $list ) ) {
1453 return wfMessage(
'parentheses' )->params( get_class( $list ) )->escaped();
1454 } elseif ( !is_array( $list ) ) {
1457 if ( is_object( $list[0] ) ) {
1458 $class = get_class( $list[0] );
1463 return wfMessage(
'parentheses' )->params(
"$class, {$list[1]}" )->escaped();
1474 return (
new GitInfo( $dir ) )->getHeadSHA1();
1482 $config = $this->getConfig();
1483 $scriptPath = $config->get( MainConfigNames::ScriptPath ) ?:
'/';
1486 'version-entrypoints-articlepath' => $config->get( MainConfigNames::ArticlePath ),
1487 'version-entrypoints-scriptpath' => $scriptPath,
1488 'version-entrypoints-index-php' =>
wfScript(
'index' ),
1489 'version-entrypoints-api-php' =>
wfScript(
'api' ),
1490 'version-entrypoints-rest-php' =>
wfScript(
'rest' ),
1493 $language = $this->getLanguage();
1495 'dir' => $language->getDir(),
1496 'lang' => $language->getHtmlCode(),
1500 $this->addTocSection(
'version-entrypoints',
'mw-version-entrypoints' );
1502 $out = Html::element(
1504 [
'id' =>
'mw-version-entrypoints' ],
1505 $this->msg(
'version-entrypoints' )->text()
1507 Html::openElement(
'table',
1509 'class' =>
'wikitable plainlinks',
1510 'id' =>
'mw-version-entrypoints-table',
1515 Html::openElement(
'thead' ) .
1516 Html::openElement(
'tr' ) .
1520 $this->msg(
'version-entrypoints-header-entrypoint' )->text()
1525 $this->msg(
'version-entrypoints-header-url' )->text()
1527 Html::closeElement(
'tr' ) .
1528 Html::closeElement(
'thead' );
1530 foreach ( $entryPoints as $message => $value ) {
1532 $out .= Html::openElement(
'tr' ) .
1533 Html::rawElement(
'td', [], $this->msg( $message )->parse() ) .
1534 Html::rawElement(
'td', [],
1538 $this->msg(
new RawMessage(
"[$url $value]" ) )->parse()
1541 Html::closeElement(
'tr' );
1544 $out .= Html::closeElement(
'table' );