73 parent::__construct(
'Version' );
74 $this->parserFactory = $parserFactory;
75 $this->urlUtils = $urlUtils;
76 $this->dbProvider = $dbProvider;
89 $credits[$credit[
'type']][] = $credit;
98 $config = $this->getConfig();
99 $credits = self::getCredits( ExtensionRegistry::getInstance(), $config );
102 $this->outputHeader();
103 $out = $this->getOutput();
104 $out->getMetadata()->setPreventClickjacking(
false );
107 $parts = explode(
'/', (
string)$par );
109 if ( isset( $parts[1] ) ) {
110 $extName = str_replace(
'_',
' ', $parts[1] );
112 foreach ( $credits as $extensions ) {
113 foreach ( $extensions as $ext ) {
114 if ( isset( $ext[
'name'] ) && ( $ext[
'name'] === $extName ) ) {
121 $out->setStatusCode( 404 );
124 $extName =
'MediaWiki';
128 switch ( strtolower( $parts[0] ) ) {
130 $out->addModuleStyles(
'mediawiki.special' );
132 $wikiText =
'{{int:version-credits-not-found}}';
133 if ( $extName ===
'MediaWiki' ) {
134 $wikiText = file_get_contents( MW_INSTALL_PATH .
'/CREDITS' );
136 $wikiText = str_replace(
137 [
'<!-- BEGIN CONTRIBUTOR LIST -->',
'<!-- END CONTRIBUTOR LIST -->' ],
138 [
'<div class="mw-version-credits">',
'</div>' ],
141 } elseif ( ( $extNode !==
null ) && isset( $extNode[
'path'] ) ) {
142 $file = ExtensionInfo::getAuthorsFileName( dirname( $extNode[
'path'] ) );
144 $wikiText = file_get_contents( $file );
145 if ( str_ends_with( $file,
'.txt' ) ) {
158 $out->setPageTitleMsg( $this->msg(
'version-credits-title' )->plaintextParams( $extName ) );
159 $out->addWikiTextAsInterface( $wikiText );
163 $out->setPageTitleMsg( $this->msg(
'version-license-title' )->plaintextParams( $extName ) );
165 $licenseFound =
false;
167 if ( $extName ===
'MediaWiki' ) {
168 $out->addWikiTextAsInterface(
169 file_get_contents( MW_INSTALL_PATH .
'/COPYING' )
171 $licenseFound =
true;
172 } elseif ( ( $extNode !==
null ) && isset( $extNode[
'path'] ) ) {
173 $files = ExtensionInfo::getLicenseFileNames( dirname( $extNode[
'path'] ) );
175 $licenseFound =
true;
176 foreach ( $files as $file ) {
177 $out->addWikiTextAsInterface(
184 file_get_contents( $file )
190 if ( !$licenseFound ) {
191 $out->addWikiTextAsInterface(
'{{int:version-license-not-found}}' );
196 $out->addModuleStyles(
'mediawiki.special' );
198 $out->addHTML( $this->getMediaWikiCredits() );
200 $this->tocData =
new TOCData();
202 $this->tocSection = 0;
203 $this->tocSubSection = 0;
207 $this->softwareInformation(),
208 $this->getEntryPointInfo(),
209 $this->getSkinCredits( $credits ),
210 $this->getExtensionCredits( $credits ),
211 $this->getLibraries( $credits ),
212 $this->getParserTags(),
213 $this->getParserFunctionHooks(),
214 $this->getParsoidModules(),
220 $out->addTOCPlaceholder( $this->tocData );
223 foreach ( $sections as $content ) {
224 $out->addHTML( $content );
238 private function addTocSection( $labelMsg, $id ) {
241 $this->tocSubSection = 0;
242 $this->tocData->addSection(
new SectionMetadata(
245 $this->msg( $labelMsg )->escaped(),
246 $this->getLanguage()->formatNum( $this->tocSection ),
247 (
string)$this->tocIndex,
262 private function addTocSubSection( $label, $id ) {
264 $this->tocSubSection++;
265 $this->tocData->addSection(
new SectionMetadata(
268 htmlspecialchars( $label ),
270 $this->getLanguage()->formatNum( $this->tocSection ) .
'.' .
271 $this->getLanguage()->formatNum( $this->tocSubSection ),
272 (
string)$this->tocIndex,
285 private function getMediaWikiCredits() {
290 [
'id' =>
'mw-version-license' ],
291 $this->msg(
'version-license' )->text()
294 $ret .= Html::rawElement(
'div', [
'class' =>
'plainlinks' ],
295 $this->msg(
new RawMessage( self::getCopyrightAndAuthorList() ) )->parseAsBlock() .
296 Html::rawElement(
'div', [
'class' =>
'mw-version-license-info' ],
297 $this->msg(
'version-license-info' )->parseAsBlock()
311 if ( defined(
'MEDIAWIKI_INSTALL' ) ) {
312 $othersLink =
'[https://www.mediawiki.org/wiki/Special:Version/Credits ' .
313 wfMessage(
'version-poweredby-others' )->plain() .
']';
315 $othersLink =
'[[Special:Version/Credits|' .
316 wfMessage(
'version-poweredby-others' )->plain() .
']]';
319 $translatorsLink =
'[https://translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
320 wfMessage(
'version-poweredby-translators' )->plain() .
']';
323 'Magnus Manske',
'Brooke Vibber',
'Lee Daniel Crocker',
324 'Tim Starling',
'Erik Möller',
'Gabriel Wicke',
'Ævar Arnfjörð Bjarmason',
325 'Niklas Laxström',
'Domas Mituzas',
'Rob Church',
'Yuri Astrakhan',
326 'Aryeh Gregor',
'Aaron Schulz',
'Andrew Garrett',
'Raimond Spekking',
327 'Alexandre Emsenhuber',
'Siebrand Mazeland',
'Chad Horohoe',
328 'Roan Kattouw',
'Trevor Parscal',
'Bryan Tong Minh',
'Sam Reed',
329 'Victor Vasiliev',
'Rotem Liss',
'Platonides',
'Antoine Musso',
330 'Timo Tijhof',
'Daniel Kinzler',
'Jeroen De Dauw',
'Brad Jorsch',
331 'Bartosz Dziewoński',
'Ed Sanders',
'Moriel Schottlender',
332 'Kunal Mehta',
'James D. Forrester',
'Brian Wolff',
'Adam Shorland',
333 'DannyS712',
'Ori Livneh',
'Max Semenik',
'Amir Sarabadani',
334 'Derk-Jan Hartman',
'Petr Pchelko',
'Umherirrender',
'C. Scott Ananian',
335 'fomafix',
'Thiemo Kreuz',
'Gergő Tisza',
'Volker E.',
336 'Jack Phoenix',
'Isarra Yos',
337 $othersLink, $translatorsLink
340 return wfMessage(
'version-poweredby-credits', MWTimestamp::getLocalInstance()->format(
'Y' ),
349 private function getSoftwareInformation() {
350 $dbr = $this->dbProvider->getReplicaDatabase();
356 '[https://www.mediawiki.org/ MediaWiki]' => self::getVersionLinked(),
357 '[https://php.net/ PHP]' => PHP_VERSION .
" (" . PHP_SAPI .
")",
358 '[https://icu.unicode.org/ ICU]' => INTL_ICU_VERSION,
359 $dbr->getSoftwareLink() => $dbr->getServerInfo(),
363 if ( phpversion(
"wikidiff2" ) ) {
364 $software[
'[https://www.mediawiki.org/wiki/Wikidiff2 wikidiff2]' ] = phpversion(
"wikidiff2" );
368 $this->getHookRunner()->onSoftwareInfo( $software );
378 private function softwareInformation() {
379 $this->addTocSection(
'version-software',
'mw-version-software' );
383 [
'id' =>
'mw-version-software' ],
384 $this->msg(
'version-software' )->text()
387 $out .= Html::openElement(
'table', [
'class' =>
'wikitable plainlinks',
'id' =>
'sv-software' ] );
389 $out .= $this->getTableHeaderHtml( [
390 $this->msg(
'version-software-product' )->text(),
391 $this->msg(
'version-software-version' )->text()
394 foreach ( $this->getSoftwareInformation() as $name => $version ) {
395 $out .= Html::rawElement(
398 Html::rawElement(
'td', [], $this->msg(
new RawMessage( $name ) )->parse() ) .
399 Html::rawElement(
'td', [
'dir' =>
'ltr' ], $this->msg(
new RawMessage( $version ) )->parse() )
403 $out .= Html::closeElement(
'table' );
417 public static function getVersion( $flags =
'', $lang =
null ) {
418 $gitInfo = GitInfo::repo()->getHeadSHA1();
421 } elseif ( $flags ===
'nodb' ) {
422 $shortSha1 = substr( $gitInfo, 0, 7 );
425 $shortSha1 = substr( $gitInfo, 0, 7 );
427 if ( $lang !==
null ) {
428 $msg->inLanguage( $lang );
430 $shortSha1 = $msg->params( $shortSha1 )->text();
445 return self::getVersionLinkedGit() ?:
MW_VERSION;
451 private static function getMWVersionLinked() {
454 if ( $hookRunner->onSpecialVersionVersionUrl(
MW_VERSION, $versionUrl ) ) {
456 preg_match(
"/^(\d+\.\d+)/",
MW_VERSION, $versionParts );
457 $versionUrl =
"https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
460 return '[' . $versionUrl .
' ' .
MW_VERSION .
']';
468 private static function getVersionLinkedGit() {
471 $gitInfo =
new GitInfo( MW_INSTALL_PATH );
472 $headSHA1 = $gitInfo->getHeadSHA1();
477 $shortSHA1 =
'(' . substr( $headSHA1, 0, 7 ) .
')';
479 $gitHeadUrl = $gitInfo->getHeadViewUrl();
480 if ( $gitHeadUrl !==
false ) {
481 $shortSHA1 =
"[$gitHeadUrl $shortSHA1]";
484 $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
485 if ( $gitHeadCommitDate ) {
489 return self::getMWVersionLinked() .
" $shortSHA1";
502 if ( self::$extensionTypes === false ) {
503 self::$extensionTypes = [
504 'specialpage' =>
wfMessage(
'version-specialpages' )->text(),
505 'editor' =>
wfMessage(
'version-editors' )->text(),
506 'parserhook' =>
wfMessage(
'version-parserhooks' )->text(),
507 'variable' =>
wfMessage(
'version-variables' )->text(),
508 'media' =>
wfMessage(
'version-mediahandlers' )->text(),
509 'antispam' =>
wfMessage(
'version-antispam' )->text(),
510 'skin' =>
wfMessage(
'version-skins' )->text(),
511 'api' =>
wfMessage(
'version-api' )->text(),
512 'other' =>
wfMessage(
'version-other' )->text(),
516 ->onExtensionTypes( self::$extensionTypes );
519 return self::$extensionTypes;
532 $types = self::getExtensionTypes();
534 return $types[$type] ?? $types[
'other'];
543 private function getExtensionCredits( array $credits ) {
544 $extensionTypes = self::getExtensionTypes();
546 $this->addTocSection(
'version-extensions',
'mw-version-ext' );
548 $out = Html::element(
550 [
'id' =>
'mw-version-ext' ],
551 $this->msg(
'version-extensions' )->text()
557 ( count( $credits ) === 1 && isset( $credits[
'skin'] ) )
559 $out .= Html::element(
562 $this->msg(
'version-extensions-no-ext' )->text()
569 $credits[
'other'] ??= [];
570 foreach ( $credits as $type => $extensions ) {
571 if ( !array_key_exists( $type, $extensionTypes ) ) {
572 $credits[
'other'] = array_merge( $credits[
'other'], $extensions );
577 foreach ( $extensionTypes as $type => $text ) {
579 if ( $type !==
'other' && $type !==
'skin' ) {
580 $out .= $this->getExtensionCategory( $type, $text, $credits[$type] ?? [] );
585 $out .= $this->getExtensionCategory(
'other', $extensionTypes[
'other'], $credits[
'other'] );
596 private function getSkinCredits( array $credits ) {
597 $this->addTocSection(
'version-skins',
'mw-version-skin' );
599 $out = Html::element(
601 [
'id' =>
'mw-version-skin' ],
602 $this->
msg(
'version-skins' )->text()
605 if ( !isset( $credits[
'skin'] ) || !$credits[
'skin'] ) {
606 $out .= Html::element(
609 $this->
msg(
'version-skins-no-skin' )->text()
614 $out .= $this->getExtensionCategory(
'skin',
null, $credits[
'skin'] );
626 $this->addTocSection(
'version-libraries',
'mw-version-libraries' );
628 $out = Html::element(
630 [
'id' =>
'mw-version-libraries' ],
631 $this->msg(
'version-libraries' )->text()
635 . $this->getExternalLibraries( $credits )
636 . $this->getClientSideLibraries();
647 MW_INSTALL_PATH .
'/vendor/composer/installed.json'
650 $extensionTypes = self::getExtensionTypes();
651 foreach ( $extensionTypes as $type => $message ) {
652 if ( !isset( $credits[$type] ) || $credits[$type] === [] ) {
655 foreach ( $credits[$type] as $extension ) {
656 if ( !isset( $extension[
'path'] ) ) {
659 $paths[] = dirname( $extension[
'path'] ) .
'/vendor/composer/installed.json';
665 foreach ( $paths as
$path ) {
666 if ( !file_exists(
$path ) ) {
672 $dependencies += $installed->getInstalledDependencies();
675 ksort( $dependencies );
676 return $dependencies;
686 $dependencies = self::parseComposerInstalled( $credits );
687 if ( $dependencies === [] ) {
691 $this->addTocSubSection( $this->msg(
'version-libraries-server' )->text(),
'mw-version-libraries-server' );
693 $out = Html::element(
695 [
'id' =>
'mw-version-libraries-server' ],
696 $this->msg(
'version-libraries-server' )->text()
698 $out .= Html::openElement(
700 [
'class' =>
'wikitable plainlinks mw-installed-software',
'id' =>
'sv-libraries' ]
703 $out .= $this->getTableHeaderHtml( [
704 $this->msg(
'version-libraries-library' )->text(),
705 $this->msg(
'version-libraries-version' )->text(),
706 $this->msg(
'version-libraries-license' )->text(),
707 $this->msg(
'version-libraries-description' )->text(),
708 $this->msg(
'version-libraries-authors' )->text(),
711 foreach ( $dependencies as $name => $info ) {
712 if ( !is_array( $info ) || str_starts_with( $info[
'type'],
'mediawiki-' ) ) {
717 $authors = array_map(
static function ( $arr ) {
718 return new HtmlArmor( isset( $arr[
'homepage'] ) ?
719 Html::element(
'a', [
'href' => $arr[
'homepage'] ], $arr[
'name'] ) :
720 htmlspecialchars( $arr[
'name'] )
722 }, $info[
'authors'] );
723 $authors = $this->listAuthors( $authors,
false, MW_INSTALL_PATH .
"/vendor/$name" );
728 $out .= Html::openElement(
'tr', [
731 'id' => Sanitizer::escapeIdForAttribute(
732 "mw-version-library-$name"
737 $this->getLinkRenderer()->makeExternalLink(
738 "https://packagist.org/packages/$name",
740 $this->getFullTitle(),
742 [
'class' =>
'mw-version-library-name' ]
745 . Html::element(
'td', [
'dir' =>
'auto' ], $info[
'version'] )
747 . Html::element(
'td', [
'dir' =>
'auto' ], $this->listToText( $info[
'licenses'] ) )
748 . Html::element(
'td', [
'lang' =>
'en',
'dir' =>
'ltr' ], $info[
'description'] )
749 . Html::rawElement(
'td', [], $authors )
750 . Html::closeElement(
'tr' );
752 $out .= Html::closeElement(
'table' );
763 $registryDirs = [
'MediaWiki' => MW_INSTALL_PATH .
'/resources/lib' ]
764 + ExtensionRegistry::getInstance()->getAttribute(
'ForeignResourcesDir' );
767 foreach ( $registryDirs as
$source => $registryDir ) {
768 $foreignResources = Yaml::parseFile(
"$registryDir/foreign-resources.yaml" );
769 foreach ( $foreignResources as $name => $module ) {
770 $key = $name . $module[
'version'];
771 if ( isset( $modules[$key] ) ) {
772 $modules[$key][
'source'][] =
$source;
775 $modules[$key] = $module + [
'name' => $name,
'source' => [
$source ] ];
787 private function getClientSideLibraries() {
788 $this->addTocSubSection( $this->msg(
'version-libraries-client' )->text(),
'mw-version-libraries-client' );
790 $out = Html::element(
792 [
'id' =>
'mw-version-libraries-client' ],
793 $this->msg(
'version-libraries-client' )->text()
795 $out .= Html::openElement(
797 [
'class' =>
'wikitable plainlinks mw-installed-software',
'id' =>
'sv-libraries-client' ]
800 $out .= $this->getTableHeaderHtml( [
801 $this->msg(
'version-libraries-library' )->text(),
802 $this->msg(
'version-libraries-version' )->text(),
803 $this->msg(
'version-libraries-license' )->text(),
804 $this->msg(
'version-libraries-authors' )->text(),
805 $this->msg(
'version-libraries-source' )->text()
808 foreach ( self::parseForeignResources() as $name => $info ) {
812 $out .= Html::openElement(
'tr', [
815 'id' => Sanitizer::escapeIdForAttribute(
816 "mw-version-library-$name"
821 $this->getLinkRenderer()->makeExternalLink(
824 $this->getFullTitle(),
826 [
'class' =>
'mw-version-library-name' ]
829 . Html::element(
'td', [
'dir' =>
'auto' ], $info[
'version'] )
830 . Html::element(
'td', [
'dir' =>
'auto' ], $info[
'license'] )
831 . Html::element(
'td', [
'dir' =>
'auto' ], $info[
'authors'] ??
'—' )
833 . Html::element(
'td', [
'dir' =>
'auto' ], $this->listToText( $info[
'source'] ) )
834 . Html::closeElement(
'tr' );
836 $out .= Html::closeElement(
'table' );
847 $tags = $this->parserFactory->getMainInstance()->getTags();
852 $this->addTocSection(
'version-parser-extensiontags',
'mw-version-parser-extensiontags' );
854 $out = Html::rawElement(
856 [
'id' =>
'mw-version-parser-extensiontags' ],
859 [
'class' =>
'plainlinks' ],
860 $this->getLinkRenderer()->makeExternalLink(
861 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
862 $this->msg(
'version-parser-extensiontags' ),
863 $this->getFullTitle()
868 array_walk( $tags,
static function ( &$value ) {
870 $value = Html::rawElement(
874 'style' =>
'white-space: nowrap;',
876 Html::element(
'code', [],
"<$value>" )
880 $out .= $this->listToText( $tags );
891 $funcHooks = $this->parserFactory->getMainInstance()->getFunctionHooks();
896 $this->addTocSection(
'version-parser-function-hooks',
'mw-version-parser-function-hooks' );
898 $out = Html::rawElement(
900 [
'id' =>
'mw-version-parser-function-hooks' ],
903 [
'class' =>
'plainlinks' ],
904 $this->getLinkRenderer()->makeExternalLink(
905 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
906 $this->msg(
'version-parser-function-hooks' ),
907 $this->getFullTitle()
912 $funcSynonyms = $this->parserFactory->getMainInstance()->getFunctionSynonyms();
919 $preferredSynonyms = array_flip( array_reverse( $funcSynonyms[1] + $funcSynonyms[0] ) );
920 array_walk( $funcHooks,
static function ( &$value ) use ( $preferredSynonyms ) {
921 $value = $preferredSynonyms[$value];
923 $legacyHooks = array_flip( $funcHooks );
926 $cmpHooks =
static function ( $a, $b ) {
927 return strcasecmp( ltrim( $a,
'#' ), ltrim( $b,
'#' ) );
929 usort( $funcHooks, $cmpHooks );
931 $formatHooks =
static function ( &$value ) {
933 $value = Html::rawElement(
936 Html::element(
'code', [],
'{{' . $value .
'}}' )
939 array_walk( $funcHooks, $formatHooks );
941 $out .= $this->getLanguage()->listToText( $funcHooks );
943 # Get a list of parser functions from Parsoid as well.
945 $services = MediaWikiServices::getInstance();
946 $siteConfig = $services->getParsoidSiteConfig();
947 $magicWordFactory = $services->getMagicWordFactory();
948 foreach ( $siteConfig->getPFragmentHandlerKeys() as $key ) {
949 $config = $siteConfig->getPFragmentHandlerConfig( $key );
950 if ( !( $config[
'options'][
'parserFunction'] ??
false ) ) {
953 $mw = $magicWordFactory->get( $key );
954 foreach ( $mw->getSynonyms() as $local ) {
955 if ( !( $config[
'options'][
'nohash'] ??
false ) ) {
956 $local =
'#' . $local;
960 if ( isset( $legacyHooks[$local] ) ) {
963 $parsoidHooks[] = $local;
966 if ( $parsoidHooks ) {
967 $out .= Html::element(
969 [
'id' =>
'mw-version-parser-function-hooks-parsoid' ],
970 $this->msg(
'version-parser-function-hooks-parsoid' )->text()
972 usort( $parsoidHooks, $cmpHooks );
973 array_walk( $parsoidHooks, $formatHooks );
974 $out .= $this->getLanguage()->listToText( $parsoidHooks );
986 $siteConfig = MediaWikiServices::getInstance()->getParsoidSiteConfig();
987 $modules = $siteConfig->getExtensionModules();
993 $this->addTocSection(
'version-parsoid-modules',
'mw-version-parsoid-modules' );
995 $out = Html::rawElement(
997 [
'id' =>
'mw-version-parsoid-modules' ],
1000 [
'class' =>
'plainlinks' ],
1001 $this->getLinkRenderer()->makeExternalLink(
1002 'https://www.mediawiki.org/wiki/Special:MyLanguage/Parsoid',
1003 $this->msg(
'version-parsoid-modules' ),
1004 $this->getFullTitle()
1009 $moduleNames = array_map(
1010 static fn ( $m )=>Html::element(
'code', [
1011 'title' => $m->getConfig()[
'extension-name'] ??
null,
1012 ], $m->getConfig()[
'name'] ),
1016 $out .= $this->getLanguage()->listToText( $moduleNames );
1033 if ( $creditsGroup ) {
1034 $out .= $this->openExtType( $text,
'credits-' . $type );
1036 usort( $creditsGroup, $this->compare( ... ) );
1038 foreach ( $creditsGroup as $extension ) {
1039 $out .= $this->getCreditsForExtension( $type, $extension );
1042 $out .= Html::closeElement(
'table' );
1054 private function compare( $a, $b ) {
1055 return $this->getLanguage()->lc( $a[
'name'] ) <=> $this->getLanguage()->lc( $b[
'name'] );
1077 $out = $this->getOutput();
1081 if ( isset( $extension[
'namemsg'] ) ) {
1083 $extensionName = $this->msg( $extension[
'namemsg'] )->text();
1084 } elseif ( isset( $extension[
'name'] ) ) {
1086 $extensionName = $extension[
'name'];
1088 $extensionName = $this->msg(
'version-no-ext-name' )->text();
1091 if ( isset( $extension[
'url'] ) ) {
1092 $extensionNameLink = $this->getLinkRenderer()->makeExternalLink(
1095 $this->getFullTitle(),
1097 [
'class' =>
'mw-version-ext-name' ]
1100 $extensionNameLink = htmlspecialchars( $extensionName );
1106 $canonicalVersion =
'–';
1107 $extensionPath =
null;
1112 if ( isset( $extension[
'version'] ) ) {
1113 $canonicalVersion = $out->parseInlineAsInterface( $extension[
'version'] );
1116 if ( isset( $extension[
'path'] ) ) {
1117 $extensionPath = dirname( $extension[
'path'] );
1118 if ( $this->coreId ==
'' ) {
1119 wfDebug(
'Looking up core head id' );
1120 $coreHeadSHA1 = GitInfo::repo()->getHeadSHA1();
1121 if ( $coreHeadSHA1 ) {
1122 $this->coreId = $coreHeadSHA1;
1125 $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getInstance(
CACHE_ANYTHING );
1126 $memcKey = $cache->makeKey(
1127 'specialversion-ext-version-text', $extension[
'path'], $this->coreId
1129 [ $vcsVersion, $vcsLink, $vcsDate ] = $cache->get( $memcKey );
1131 if ( !$vcsVersion ) {
1132 wfDebug(
"Getting VCS info for extension {$extension['name']}" );
1133 $gitInfo =
new GitInfo( $extensionPath );
1134 $vcsVersion = $gitInfo->getHeadSHA1();
1135 if ( $vcsVersion !==
false ) {
1136 $vcsVersion = substr( $vcsVersion, 0, 7 );
1137 $vcsLink = $gitInfo->getHeadViewUrl();
1138 $vcsDate = $gitInfo->getHeadCommitDate();
1140 $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
1142 wfDebug(
"Pulled VCS info for extension {$extension['name']} from cache" );
1146 $versionString = Html::rawElement(
1148 [
'class' =>
'mw-version-ext-version' ],
1152 if ( $vcsVersion ) {
1154 $vcsVerString = $this->getLinkRenderer()->makeExternalLink(
1156 $this->msg(
'version-version', $vcsVersion ),
1157 $this->getFullTitle(),
1159 [
'class' =>
'mw-version-ext-vcs-version' ]
1162 $vcsVerString = Html::element(
'span',
1163 [
'class' =>
'mw-version-ext-vcs-version' ],
1167 $versionString .=
" {$vcsVerString}";
1170 $versionString .=
' ' . Html::element(
'span', [
1171 'class' =>
'mw-version-ext-vcs-timestamp',
1172 'dir' => $this->getLanguage()->getDir(),
1173 ], $this->getLanguage()->timeanddate( $vcsDate,
true ) );
1175 $versionString = Html::rawElement(
'span',
1176 [
'class' =>
'mw-version-ext-meta-version' ],
1184 if ( isset( $extension[
'name'] ) ) {
1185 $licenseName =
null;
1186 if ( isset( $extension[
'license-name'] ) ) {
1187 $licenseName =
new HtmlArmor( $out->parseInlineAsInterface( $extension[
'license-name'] ) );
1188 } elseif ( $extensionPath !==
null && ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
1189 $licenseName = $this->msg(
'version-ext-license' )->text();
1191 if ( $licenseName !==
null ) {
1192 $licenseLink = $this->getLinkRenderer()->makeLink(
1193 $this->getPageTitle(
'License/' . $extension[
'name'] ),
1196 'class' =>
'mw-version-ext-license',
1206 if ( isset( $extension[
'descriptionmsg'] ) ) {
1208 $descriptionMsg = $extension[
'descriptionmsg'];
1210 if ( is_array( $descriptionMsg ) ) {
1211 $descriptionMsgKey = array_shift( $descriptionMsg );
1212 $descriptionMsg = array_map(
'htmlspecialchars', $descriptionMsg );
1213 $description = $this->msg( $descriptionMsgKey, ...$descriptionMsg )->text();
1215 $description = $this->msg( $descriptionMsg )->text();
1217 } elseif ( isset( $extension[
'description'] ) ) {
1219 $description = $extension[
'description'];
1223 $description = $out->parseInlineAsInterface( $description );
1226 $authors = $extension[
'author'] ?? [];
1228 $authors = $this->listAuthors( $authors, $extension[
'name'], $extensionPath );
1231 $html = Html::openElement(
'tr', [
1232 'class' =>
'mw-version-ext',
1233 'id' => Sanitizer::escapeIdForAttribute(
'mw-version-ext-' . $type .
'-' . $extension[
'name'] )
1237 $html .= Html::rawElement(
'td', [], $extensionNameLink );
1238 $html .= Html::rawElement(
'td', [], $versionString );
1239 $html .= Html::rawElement(
'td', [], $licenseLink );
1240 $html .= Html::rawElement(
'td', [
'class' =>
'mw-version-ext-description' ], $description );
1241 $html .= Html::rawElement(
'td', [
'class' =>
'mw-version-ext-authors' ], $authors );
1243 $html .= Html::closeElement(
'tr' );
1253 private function getHooks() {
1254 if ( !$this->getConfig()->
get( MainConfigNames::SpecialVersionShowHooks ) ) {
1258 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1259 $hookNames = $hookContainer->getHookNames();
1261 if ( !$hookNames ) {
1268 $this->addTocSection(
'version-hooks',
'mw-version-hooks' );
1269 $ret[] = Html::element(
1271 [
'id' =>
'mw-version-hooks' ],
1272 $this->msg(
'version-hooks' )->text()
1274 $ret[] = Html::openElement(
'table', [
'class' =>
'wikitable',
'id' =>
'sv-hooks' ] );
1275 $ret[] = Html::openElement(
'tr' );
1276 $ret[] = Html::element(
'th', [], $this->msg(
'version-hook-name' )->text() );
1277 $ret[] = Html::element(
'th', [], $this->msg(
'version-hook-subscribedby' )->text() );
1278 $ret[] = Html::closeElement(
'tr' );
1280 foreach ( $hookNames as $name ) {
1281 $handlers = $hookContainer->getHandlerDescriptions( $name );
1283 $ret[] = Html::openElement(
'tr' );
1284 $ret[] = Html::element(
'td', [], $name );
1286 $ret[] = Html::element(
'td', [], $this->listToText( $handlers ) );
1287 $ret[] = Html::closeElement(
'tr' );
1290 $ret[] = Html::closeElement(
'table' );
1292 return implode(
"\n", $ret );
1295 private function openExtType( ?
string $text =
null, ?
string $name =
null ): string {
1298 $opt = [
'class' =>
'wikitable plainlinks mw-installed-software' ];
1301 $opt[
'id'] =
"sv-$name";
1304 $out .= Html::openElement(
'table', $opt );
1306 if ( $text !==
null ) {
1307 $out .= Html::element(
'caption', [], $text );
1310 if ( $name && $text !==
null ) {
1311 $this->addTocSubSection( $text,
"sv-$name" );
1314 $firstHeadingMsg = ( $name ===
'credits-skin' )
1315 ?
'version-skin-colheader-name'
1316 :
'version-ext-colheader-name';
1318 $out .= $this->getTableHeaderHtml( [
1319 $this->
msg( $firstHeadingMsg )->text(),
1320 $this->
msg(
'version-ext-colheader-version' )->text(),
1321 $this->
msg(
'version-ext-colheader-license' )->text(),
1322 $this->
msg(
'version-ext-colheader-description' )->text(),
1323 $this->
msg(
'version-ext-colheader-credits' )->text()
1337 private function getTableHeaderHtml( $headers ): string {
1339 $out .= Html::openElement(
'thead' );
1340 $out .= Html::openElement(
'tr' );
1341 foreach ( $headers as $header ) {
1342 $out .= Html::element(
'th', [
'scope' =>
'col' ], $header );
1344 $out .= Html::closeElement(
'tr' );
1345 $out .= Html::closeElement(
'thead' );
1354 private function IPInfo() {
1355 $ip = str_replace(
'--',
' - ', htmlspecialchars( $this->
getRequest()->getIP() ) );
1357 return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
1382 $linkRenderer = $this->getLinkRenderer();
1385 $authors = (array)$authors;
1390 if ( count( $authors ) === 1 && $authors[0] ===
'...' ) {
1393 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1394 return $linkRenderer->makeLink(
1395 $this->getPageTitle(
"Credits/$extName" ),
1396 $this->msg(
'version-poweredby-various' )->text()
1399 return $this->msg(
'version-poweredby-various' )->escaped();
1405 foreach ( $authors as $item ) {
1406 if ( $item instanceof HtmlArmor ) {
1407 $list[] = HtmlArmor::getHtml( $item );
1408 } elseif ( $item ===
'...' ) {
1411 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1412 $text = $linkRenderer->makeLink(
1413 $this->getPageTitle(
"Credits/$extName" ),
1414 $this->
msg(
'version-poweredby-others' )->text()
1417 $text = $this->
msg(
'version-poweredby-others' )->escaped();
1420 } elseif ( str_ends_with( $item,
' ...]' ) ) {
1422 $list[] = $this->getOutput()->parseInlineAsInterface(
1423 substr( $item, 0, -4 ) . $this->
msg(
'version-poweredby-others' )->text() .
"]"
1426 $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1430 if ( $extName && !$hasOthers && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1431 $list[] = $linkRenderer->makeLink(
1432 $this->getPageTitle(
"Credits/$extName" ),
1433 $this->
msg(
'version-poweredby-others' )->text()
1437 return $this->listToText( $list,
false );
1449 private function listToText( array $list,
bool $sort =
true ): string {
1457 return $this->getLanguage()
1458 ->listToText( array_map( [ self::class,
'arrayToString' ], $list ) );
1471 if ( is_array( $list ) && count( $list ) == 1 ) {
1474 if ( $list instanceof Closure ) {
1477 } elseif ( is_object( $list ) ) {
1478 return wfMessage(
'parentheses' )->params( get_class( $list ) )->escaped();
1479 } elseif ( !is_array( $list ) ) {
1482 if ( is_object( $list[0] ) ) {
1483 $class = get_class( $list[0] );
1488 return wfMessage(
'parentheses' )->params(
"$class, {$list[1]}" )->escaped();
1499 return (
new GitInfo( $dir ) )->getHeadSHA1();
1507 $config = $this->getConfig();
1508 $scriptPath = $config->get( MainConfigNames::ScriptPath ) ?:
'/';
1511 'version-entrypoints-articlepath' => $config->get( MainConfigNames::ArticlePath ),
1512 'version-entrypoints-scriptpath' => $scriptPath,
1513 'version-entrypoints-index-php' =>
wfScript(
'index' ),
1514 'version-entrypoints-api-php' =>
wfScript(
'api' ),
1515 'version-entrypoints-rest-php' =>
wfScript(
'rest' ),
1518 $language = $this->getLanguage();
1520 'dir' => $language->getDir(),
1521 'lang' => $language->getHtmlCode(),
1525 $this->addTocSection(
'version-entrypoints',
'mw-version-entrypoints' );
1527 $out = Html::element(
1529 [
'id' =>
'mw-version-entrypoints' ],
1530 $this->msg(
'version-entrypoints' )->text()
1532 Html::openElement(
'table',
1534 'class' =>
'wikitable plainlinks',
1535 'id' =>
'mw-version-entrypoints-table',
1540 Html::openElement(
'thead' ) .
1541 Html::openElement(
'tr' ) .
1545 $this->msg(
'version-entrypoints-header-entrypoint' )->text()
1550 $this->msg(
'version-entrypoints-header-url' )->text()
1552 Html::closeElement(
'tr' ) .
1553 Html::closeElement(
'thead' );
1555 foreach ( $entryPoints as $message => $value ) {
1557 $out .= Html::openElement(
'tr' ) .
1558 Html::rawElement(
'td', [], $this->msg( $message )->parse() ) .
1559 Html::rawElement(
'td', [],
1563 $this->msg(
new RawMessage(
"[$url $value]" ) )->parse()
1566 Html::closeElement(
'tr' );
1569 $out .= Html::closeElement(
'table' );