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];
937 $legacyHooks = array_flip( $funcHooks );
940 $cmpHooks =
static function ( $a, $b ) {
941 return strcasecmp( ltrim( $a,
'#' ), ltrim( $b,
'#' ) );
943 usort( $funcHooks, $cmpHooks );
945 $formatHooks =
static function ( &$value ) {
947 $value = Html::rawElement(
950 Html::element(
'code', [],
'{{' . $value .
'}}' )
953 array_walk( $funcHooks, $formatHooks );
955 $out .= $this->getLanguage()->listToText( $funcHooks );
957 # Get a list of parser functions from Parsoid as well.
959 $services = MediaWikiServices::getInstance();
960 $siteConfig = $services->getParsoidSiteConfig();
961 $magicWordFactory = $services->getMagicWordFactory();
962 foreach ( $siteConfig->getPFragmentHandlerKeys() as $key ) {
963 $config = $siteConfig->getPFragmentHandlerConfig( $key );
964 if ( !( $config[
'options'][
'parserFunction'] ??
false ) ) {
967 $mw = $magicWordFactory->get( $key );
968 foreach ( $mw->getSynonyms() as $local ) {
969 if ( !( $config[
'options'][
'nohash'] ??
false ) ) {
970 $local =
'#' . $local;
974 if ( isset( $legacyHooks[$local] ) ) {
977 $parsoidHooks[] = $local;
980 if ( $parsoidHooks ) {
981 $out .= Html::element(
983 [
'id' =>
'mw-version-parser-function-hooks-parsoid' ],
984 $this->msg(
'version-parser-function-hooks-parsoid' )->text()
986 usort( $parsoidHooks, $cmpHooks );
987 array_walk( $parsoidHooks, $formatHooks );
988 $out .= $this->getLanguage()->listToText( $parsoidHooks );
1000 $siteConfig = MediaWikiServices::getInstance()->getParsoidSiteConfig();
1001 $modules = $siteConfig->getExtensionModules();
1007 $this->addTocSection(
'version-parsoid-modules',
'mw-version-parsoid-modules' );
1009 $out = Html::rawElement(
1011 [
'id' =>
'mw-version-parsoid-modules' ],
1014 [
'class' =>
'plainlinks' ],
1015 $this->getLinkRenderer()->makeExternalLink(
1016 'https://www.mediawiki.org/wiki/Special:MyLanguage/Parsoid',
1017 $this->msg(
'version-parsoid-modules' ),
1018 $this->getFullTitle()
1023 $moduleNames = array_map(
1024 static fn ( $m )=>Html::element(
'code', [], $m->getConfig()[
'name'] ),
1028 $out .= $this->getLanguage()->listToText( $moduleNames );
1045 if ( $creditsGroup ) {
1046 $out .= $this->openExtType( $text,
'credits-' . $type );
1048 usort( $creditsGroup, [ $this,
'compare' ] );
1050 foreach ( $creditsGroup as $extension ) {
1051 $out .= $this->getCreditsForExtension( $type, $extension );
1054 $out .= Html::closeElement(
'table' );
1067 return $this->getLanguage()->lc( $a[
'name'] ) <=> $this->getLanguage()->lc( $b[
'name'] );
1089 $out = $this->getOutput();
1093 if ( isset( $extension[
'namemsg'] ) ) {
1095 $extensionName = $this->msg( $extension[
'namemsg'] );
1096 } elseif ( isset( $extension[
'name'] ) ) {
1098 $extensionName = $extension[
'name'];
1100 $extensionName = $this->msg(
'version-no-ext-name' );
1103 if ( isset( $extension[
'url'] ) ) {
1104 $extensionNameLink = $this->getLinkRenderer()->makeExternalLink(
1107 $this->getFullTitle(),
1109 [
'class' =>
'mw-version-ext-name' ]
1112 $extensionNameLink = htmlspecialchars( $extensionName );
1118 $canonicalVersion =
'–';
1119 $extensionPath =
null;
1124 if ( isset( $extension[
'version'] ) ) {
1125 $canonicalVersion = $out->parseInlineAsInterface( $extension[
'version'] );
1128 if ( isset( $extension[
'path'] ) ) {
1129 $extensionPath = dirname( $extension[
'path'] );
1130 if ( $this->coreId ==
'' ) {
1131 wfDebug(
'Looking up core head id' );
1132 $coreHeadSHA1 = GitInfo::repo()->getHeadSHA1();
1133 if ( $coreHeadSHA1 ) {
1134 $this->coreId = $coreHeadSHA1;
1137 $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getInstance(
CACHE_ANYTHING );
1138 $memcKey = $cache->makeKey(
1139 'specialversion-ext-version-text', $extension[
'path'], $this->coreId
1141 [ $vcsVersion, $vcsLink, $vcsDate ] = $cache->get( $memcKey );
1143 if ( !$vcsVersion ) {
1144 wfDebug(
"Getting VCS info for extension {$extension['name']}" );
1145 $gitInfo =
new GitInfo( $extensionPath );
1146 $vcsVersion = $gitInfo->getHeadSHA1();
1147 if ( $vcsVersion !==
false ) {
1148 $vcsVersion = substr( $vcsVersion, 0, 7 );
1149 $vcsLink = $gitInfo->getHeadViewUrl();
1150 $vcsDate = $gitInfo->getHeadCommitDate();
1152 $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
1154 wfDebug(
"Pulled VCS info for extension {$extension['name']} from cache" );
1158 $versionString = Html::rawElement(
1160 [
'class' =>
'mw-version-ext-version' ],
1164 if ( $vcsVersion ) {
1166 $vcsVerString = $this->getLinkRenderer()->makeExternalLink(
1168 $this->msg(
'version-version', $vcsVersion ),
1169 $this->getFullTitle(),
1171 [
'class' =>
'mw-version-ext-vcs-version' ]
1174 $vcsVerString = Html::element(
'span',
1175 [
'class' =>
'mw-version-ext-vcs-version' ],
1179 $versionString .=
" {$vcsVerString}";
1182 $versionString .=
' ' . Html::element(
'span', [
1183 'class' =>
'mw-version-ext-vcs-timestamp',
1184 'dir' => $this->getLanguage()->getDir(),
1185 ], $this->getLanguage()->timeanddate( $vcsDate,
true ) );
1187 $versionString = Html::rawElement(
'span',
1188 [
'class' =>
'mw-version-ext-meta-version' ],
1196 if ( isset( $extension[
'name'] ) ) {
1197 $licenseName =
null;
1198 if ( isset( $extension[
'license-name'] ) ) {
1199 $licenseName =
new HtmlArmor( $out->parseInlineAsInterface( $extension[
'license-name'] ) );
1200 } elseif ( $extensionPath !==
null && ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
1201 $licenseName = $this->msg(
'version-ext-license' )->text();
1203 if ( $licenseName !==
null ) {
1204 $licenseLink = $this->getLinkRenderer()->makeLink(
1205 $this->getPageTitle(
'License/' . $extension[
'name'] ),
1208 'class' =>
'mw-version-ext-license',
1218 if ( isset( $extension[
'descriptionmsg'] ) ) {
1220 $descriptionMsg = $extension[
'descriptionmsg'];
1222 if ( is_array( $descriptionMsg ) ) {
1223 $descriptionMsgKey = array_shift( $descriptionMsg );
1224 $descriptionMsg = array_map(
'htmlspecialchars', $descriptionMsg );
1225 $description = $this->msg( $descriptionMsgKey, ...$descriptionMsg )->text();
1227 $description = $this->msg( $descriptionMsg )->text();
1229 } elseif ( isset( $extension[
'description'] ) ) {
1231 $description = $extension[
'description'];
1235 $description = $out->parseInlineAsInterface( $description );
1238 $authors = $extension[
'author'] ?? [];
1240 $authors = $this->listAuthors( $authors, $extension[
'name'], $extensionPath );
1243 $html = Html::openElement(
'tr', [
1244 'class' =>
'mw-version-ext',
1245 'id' => Sanitizer::escapeIdForAttribute(
'mw-version-ext-' . $type .
'-' . $extension[
'name'] )
1249 $html .= Html::rawElement(
'td', [], $extensionNameLink );
1250 $html .= Html::rawElement(
'td', [], $versionString );
1251 $html .= Html::rawElement(
'td', [], $licenseLink );
1252 $html .= Html::rawElement(
'td', [
'class' =>
'mw-version-ext-description' ], $description );
1253 $html .= Html::rawElement(
'td', [
'class' =>
'mw-version-ext-authors' ], $authors );
1255 $html .= Html::closeElement(
'tr' );
1265 private function getHooks() {
1266 if ( !$this->getConfig()->
get( MainConfigNames::SpecialVersionShowHooks ) ) {
1270 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1271 $hookNames = $hookContainer->getHookNames();
1273 if ( !$hookNames ) {
1280 $this->addTocSection(
'version-hooks',
'mw-version-hooks' );
1281 $ret[] = Html::element(
1283 [
'id' =>
'mw-version-hooks' ],
1284 $this->msg(
'version-hooks' )->text()
1286 $ret[] = Html::openElement(
'table', [
'class' =>
'wikitable',
'id' =>
'sv-hooks' ] );
1287 $ret[] = Html::openElement(
'tr' );
1288 $ret[] = Html::element(
'th', [], $this->msg(
'version-hook-name' )->text() );
1289 $ret[] = Html::element(
'th', [], $this->msg(
'version-hook-subscribedby' )->text() );
1290 $ret[] = Html::closeElement(
'tr' );
1292 foreach ( $hookNames as $name ) {
1293 $handlers = $hookContainer->getHandlerDescriptions( $name );
1295 $ret[] = Html::openElement(
'tr' );
1296 $ret[] = Html::element(
'td', [], $name );
1298 $ret[] = Html::element(
'td', [], $this->listToText( $handlers ) );
1299 $ret[] = Html::closeElement(
'tr' );
1302 $ret[] = Html::closeElement(
'table' );
1304 return implode(
"\n", $ret );
1307 private function openExtType( ?
string $text =
null, ?
string $name =
null ): string {
1310 $opt = [
'class' =>
'wikitable plainlinks mw-installed-software' ];
1313 $opt[
'id'] =
"sv-$name";
1316 $out .= Html::openElement(
'table', $opt );
1318 if ( $text !==
null ) {
1319 $out .= Html::element(
'caption', [], $text );
1322 if ( $name && $text !==
null ) {
1323 $this->addTocSubSection( $text,
"sv-$name" );
1326 $firstHeadingMsg = ( $name ===
'credits-skin' )
1327 ?
'version-skin-colheader-name'
1328 :
'version-ext-colheader-name';
1330 $out .= $this->getTableHeaderHtml( [
1331 $this->msg( $firstHeadingMsg )->text(),
1332 $this->msg(
'version-ext-colheader-version' )->text(),
1333 $this->msg(
'version-ext-colheader-license' )->text(),
1334 $this->msg(
'version-ext-colheader-description' )->text(),
1335 $this->msg(
'version-ext-colheader-credits' )->text()
1349 private function getTableHeaderHtml( $headers ): string {
1351 $out .= Html::openElement(
'thead' );
1352 $out .= Html::openElement(
'tr' );
1353 foreach ( $headers as
$header ) {
1354 $out .= Html::element(
'th', [
'scope' =>
'col' ],
$header );
1356 $out .= Html::closeElement(
'tr' );
1357 $out .= Html::closeElement(
'thead' );
1366 private function IPInfo() {
1367 $ip = str_replace(
'--',
' - ', htmlspecialchars( $this->
getRequest()->getIP() ) );
1369 return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
1394 $linkRenderer = $this->getLinkRenderer();
1397 $authors = (array)$authors;
1402 if ( count( $authors ) === 1 && $authors[0] ===
'...' ) {
1405 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1406 return $linkRenderer->makeLink(
1407 $this->getPageTitle(
"Credits/$extName" ),
1408 $this->msg(
'version-poweredby-various' )->text()
1411 return $this->msg(
'version-poweredby-various' )->escaped();
1417 foreach ( $authors as $item ) {
1418 if ( $item instanceof HtmlArmor ) {
1419 $list[] = HtmlArmor::getHtml( $item );
1420 } elseif ( $item ===
'...' ) {
1423 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1424 $text = $linkRenderer->makeLink(
1425 $this->getPageTitle(
"Credits/$extName" ),
1426 $this->msg(
'version-poweredby-others' )->text()
1429 $text = $this->msg(
'version-poweredby-others' )->escaped();
1432 } elseif ( str_ends_with( $item,
' ...]' ) ) {
1434 $list[] = $this->getOutput()->parseInlineAsInterface(
1435 substr( $item, 0, -4 ) . $this->msg(
'version-poweredby-others' )->text() .
"]"
1438 $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1442 if ( $extName && !$hasOthers && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1443 $list[] = $linkRenderer->makeLink(
1444 $this->getPageTitle(
"Credits/$extName" ),
1445 $this->msg(
'version-poweredby-others' )->text()
1449 return $this->listToText( $list,
false );
1461 private function listToText( array $list,
bool $sort =
true ): string {
1469 return $this->getLanguage()
1470 ->listToText( array_map( [ __CLASS__,
'arrayToString' ], $list ) );
1483 if ( is_array( $list ) && count( $list ) == 1 ) {
1486 if ( $list instanceof Closure ) {
1489 } elseif ( is_object( $list ) ) {
1490 return wfMessage(
'parentheses' )->params( get_class( $list ) )->escaped();
1491 } elseif ( !is_array( $list ) ) {
1494 if ( is_object( $list[0] ) ) {
1495 $class = get_class( $list[0] );
1500 return wfMessage(
'parentheses' )->params(
"$class, {$list[1]}" )->escaped();
1511 return (
new GitInfo( $dir ) )->getHeadSHA1();
1519 $config = $this->getConfig();
1520 $scriptPath = $config->get( MainConfigNames::ScriptPath ) ?:
'/';
1523 'version-entrypoints-articlepath' => $config->get( MainConfigNames::ArticlePath ),
1524 'version-entrypoints-scriptpath' => $scriptPath,
1525 'version-entrypoints-index-php' =>
wfScript(
'index' ),
1526 'version-entrypoints-api-php' =>
wfScript(
'api' ),
1527 'version-entrypoints-rest-php' =>
wfScript(
'rest' ),
1530 $language = $this->getLanguage();
1532 'dir' => $language->getDir(),
1533 'lang' => $language->getHtmlCode(),
1537 $this->addTocSection(
'version-entrypoints',
'mw-version-entrypoints' );
1539 $out = Html::element(
1541 [
'id' =>
'mw-version-entrypoints' ],
1542 $this->msg(
'version-entrypoints' )->text()
1544 Html::openElement(
'table',
1546 'class' =>
'wikitable plainlinks',
1547 'id' =>
'mw-version-entrypoints-table',
1552 Html::openElement(
'thead' ) .
1553 Html::openElement(
'tr' ) .
1557 $this->msg(
'version-entrypoints-header-entrypoint' )->text()
1562 $this->msg(
'version-entrypoints-header-url' )->text()
1564 Html::closeElement(
'tr' ) .
1565 Html::closeElement(
'thead' );
1567 foreach ( $entryPoints as $message => $value ) {
1569 $out .= Html::openElement(
'tr' ) .
1570 Html::rawElement(
'td', [], $this->msg( $message )->parse() ) .
1571 Html::rawElement(
'td', [],
1575 $this->msg(
new RawMessage(
"[$url $value]" ) )->parse()
1578 Html::closeElement(
'tr' );
1581 $out .= Html::closeElement(
'table' );