95 parent::__construct(
'Version' );
96 $this->parserFactory = $parserFactory;
97 $this->urlUtils = $urlUtils;
98 $this->dbProvider = $dbProvider;
111 $credits[$credit[
'type']][] = $credit;
120 $config = $this->getConfig();
121 $credits = self::getCredits( ExtensionRegistry::getInstance(), $config );
124 $this->outputHeader();
125 $out = $this->getOutput();
126 $out->getMetadata()->setPreventClickjacking(
false );
129 $parts = explode(
'/', (
string)$par );
131 if ( isset( $parts[1] ) ) {
132 $extName = str_replace(
'_',
' ', $parts[1] );
134 foreach ( $credits as $extensions ) {
135 foreach ( $extensions as $ext ) {
136 if ( isset( $ext[
'name'] ) && ( $ext[
'name'] === $extName ) ) {
143 $out->setStatusCode( 404 );
146 $extName =
'MediaWiki';
150 switch ( strtolower( $parts[0] ) ) {
152 $out->addModuleStyles(
'mediawiki.special' );
154 $wikiText =
'{{int:version-credits-not-found}}';
155 if ( $extName ===
'MediaWiki' ) {
156 $wikiText = file_get_contents( MW_INSTALL_PATH .
'/CREDITS' );
158 $wikiText = str_replace(
159 [
'<!-- BEGIN CONTRIBUTOR LIST -->',
'<!-- END CONTRIBUTOR LIST -->' ],
160 [
'<div class="mw-version-credits">',
'</div>' ],
163 } elseif ( ( $extNode !==
null ) && isset( $extNode[
'path'] ) ) {
164 $file = ExtensionInfo::getAuthorsFileName( dirname( $extNode[
'path'] ) );
166 $wikiText = file_get_contents( $file );
167 if ( str_ends_with( $file,
'.txt' ) ) {
180 $out->setPageTitleMsg( $this->msg(
'version-credits-title' )->plaintextParams( $extName ) );
181 $out->addWikiTextAsInterface( $wikiText );
185 $out->setPageTitleMsg( $this->msg(
'version-license-title' )->plaintextParams( $extName ) );
187 $licenseFound =
false;
189 if ( $extName ===
'MediaWiki' ) {
190 $out->addWikiTextAsInterface(
191 file_get_contents( MW_INSTALL_PATH .
'/COPYING' )
193 $licenseFound =
true;
194 } elseif ( ( $extNode !==
null ) && isset( $extNode[
'path'] ) ) {
195 $files = ExtensionInfo::getLicenseFileNames( dirname( $extNode[
'path'] ) );
197 $licenseFound =
true;
198 foreach ( $files as $file ) {
199 $out->addWikiTextAsInterface(
206 file_get_contents( $file )
212 if ( !$licenseFound ) {
213 $out->addWikiTextAsInterface(
'{{int:version-license-not-found}}' );
218 $out->addModuleStyles(
'mediawiki.special' );
220 $out->addHTML( $this->getMediaWikiCredits() );
222 $this->tocData =
new TOCData();
224 $this->tocSection = 0;
225 $this->tocSubSection = 0;
229 $this->softwareInformation(),
230 $this->getEntryPointInfo(),
231 $this->getSkinCredits( $credits ),
232 $this->getExtensionCredits( $credits ),
233 $this->getLibraries( $credits ),
234 $this->getParserTags(),
235 $this->getParserFunctionHooks(),
243 $pout->setOutputFlag( ParserOutputFlags::SHOW_TOC );
244 $pout->setRawText( Parser::TOC_PLACEHOLDER );
245 $out->addParserOutput( $pout );
248 foreach ( $sections as $content ) {
249 $out->addHTML( $content );
263 private function addTocSection( $labelMsg, $id ) {
266 $this->tocSubSection = 0;
267 $this->tocData->addSection(
new SectionMetadata(
270 $this->msg( $labelMsg )->escaped(),
271 $this->getLanguage()->formatNum( $this->tocSection ),
272 (
string)$this->tocIndex,
287 private function addTocSubSection( $label, $id ) {
289 $this->tocSubSection++;
290 $this->tocData->addSection(
new SectionMetadata(
293 htmlspecialchars( $label ),
295 $this->getLanguage()->formatNum( $this->tocSection ) .
'.' .
296 $this->getLanguage()->formatNum( $this->tocSubSection ),
297 (
string)$this->tocIndex,
310 private function getMediaWikiCredits() {
315 [
'id' =>
'mw-version-license' ],
316 $this->msg(
'version-license' )->text()
319 $ret .= Html::rawElement(
'div', [
'class' =>
'plainlinks' ],
320 $this->msg(
new RawMessage( self::getCopyrightAndAuthorList() ) )->parseAsBlock() .
321 Html::rawElement(
'div', [
'class' =>
'mw-version-license-info' ],
322 $this->msg(
'version-license-info' )->parseAsBlock()
336 if ( defined(
'MEDIAWIKI_INSTALL' ) ) {
337 $othersLink =
'[https://www.mediawiki.org/wiki/Special:Version/Credits ' .
338 wfMessage(
'version-poweredby-others' )->plain() .
']';
340 $othersLink =
'[[Special:Version/Credits|' .
341 wfMessage(
'version-poweredby-others' )->plain() .
']]';
344 $translatorsLink =
'[https://translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
345 wfMessage(
'version-poweredby-translators' )->plain() .
']';
348 'Magnus Manske',
'Brooke Vibber',
'Lee Daniel Crocker',
349 'Tim Starling',
'Erik Möller',
'Gabriel Wicke',
'Ævar Arnfjörð Bjarmason',
350 'Niklas Laxström',
'Domas Mituzas',
'Rob Church',
'Yuri Astrakhan',
351 'Aryeh Gregor',
'Aaron Schulz',
'Andrew Garrett',
'Raimond Spekking',
352 'Alexandre Emsenhuber',
'Siebrand Mazeland',
'Chad Horohoe',
353 'Roan Kattouw',
'Trevor Parscal',
'Bryan Tong Minh',
'Sam Reed',
354 'Victor Vasiliev',
'Rotem Liss',
'Platonides',
'Antoine Musso',
355 'Timo Tijhof',
'Daniel Kinzler',
'Jeroen De Dauw',
'Brad Jorsch',
356 'Bartosz Dziewoński',
'Ed Sanders',
'Moriel Schottlender',
357 'Kunal Mehta',
'James D. Forrester',
'Brian Wolff',
'Adam Shorland',
358 'DannyS712',
'Ori Livneh',
'Max Semenik',
'Amir Sarabadani',
359 'Derk-Jan Hartman',
'Petr Pchelko',
360 $othersLink, $translatorsLink
363 return wfMessage(
'version-poweredby-credits', MWTimestamp::getLocalInstance()->format(
'Y' ),
372 private function getSoftwareInformation() {
373 $dbr = $this->dbProvider->getReplicaDatabase();
379 '[https://www.mediawiki.org/ MediaWiki]' => self::getVersionLinked(),
380 '[https://php.net/ PHP]' => PHP_VERSION .
" (" . PHP_SAPI .
")",
381 '[https://icu.unicode.org/ ICU]' => INTL_ICU_VERSION,
382 $dbr->getSoftwareLink() => $dbr->getServerInfo(),
386 if ( phpversion(
"wikidiff2" ) ) {
387 $software[
'[https://www.mediawiki.org/wiki/Wikidiff2 wikidiff2]' ] = phpversion(
"wikidiff2" );
391 $this->getHookRunner()->onSoftwareInfo( $software );
401 private function softwareInformation() {
402 $this->addTocSection(
'version-software',
'mw-version-software' );
406 [
'id' =>
'mw-version-software' ],
407 $this->msg(
'version-software' )->text()
410 $out .= Html::openElement(
'table', [
'class' =>
'wikitable plainlinks',
'id' =>
'sv-software' ] );
412 $out .= $this->getTableHeaderHtml( [
413 $this->msg(
'version-software-product' )->text(),
414 $this->msg(
'version-software-version' )->text()
417 foreach ( $this->getSoftwareInformation() as $name => $version ) {
418 $out .= Html::rawElement(
421 Html::rawElement(
'td', [], $this->msg(
new RawMessage( $name ) )->parse() ) .
422 Html::rawElement(
'td', [
'dir' =>
'ltr' ], $this->msg(
new RawMessage( $version ) )->parse() )
426 $out .= Html::closeElement(
'table' );
440 public static function getVersion( $flags =
'', $lang =
null ) {
441 $gitInfo = GitInfo::repo()->getHeadSHA1();
444 } elseif ( $flags ===
'nodb' ) {
445 $shortSha1 = substr( $gitInfo, 0, 7 );
448 $shortSha1 = substr( $gitInfo, 0, 7 );
450 if ( $lang !==
null ) {
451 $msg->inLanguage( $lang );
453 $shortSha1 = $msg->params( $shortSha1 )->text();
468 return self::getVersionLinkedGit() ?:
MW_VERSION;
474 private static function getMWVersionLinked() {
477 if ( $hookRunner->onSpecialVersionVersionUrl(
MW_VERSION, $versionUrl ) ) {
479 preg_match(
"/^(\d+\.\d+)/",
MW_VERSION, $versionParts );
480 $versionUrl =
"https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
483 return '[' . $versionUrl .
' ' .
MW_VERSION .
']';
491 private static function getVersionLinkedGit() {
494 $gitInfo =
new GitInfo( MW_INSTALL_PATH );
495 $headSHA1 = $gitInfo->getHeadSHA1();
500 $shortSHA1 =
'(' . substr( $headSHA1, 0, 7 ) .
')';
502 $gitHeadUrl = $gitInfo->getHeadViewUrl();
503 if ( $gitHeadUrl !==
false ) {
504 $shortSHA1 =
"[$gitHeadUrl $shortSHA1]";
507 $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
508 if ( $gitHeadCommitDate ) {
512 return self::getMWVersionLinked() .
" $shortSHA1";
525 if ( self::$extensionTypes === false ) {
526 self::$extensionTypes = [
527 'specialpage' =>
wfMessage(
'version-specialpages' )->text(),
528 'editor' =>
wfMessage(
'version-editors' )->text(),
529 'parserhook' =>
wfMessage(
'version-parserhooks' )->text(),
530 'variable' =>
wfMessage(
'version-variables' )->text(),
531 'media' =>
wfMessage(
'version-mediahandlers' )->text(),
532 'antispam' =>
wfMessage(
'version-antispam' )->text(),
533 'skin' =>
wfMessage(
'version-skins' )->text(),
534 'api' =>
wfMessage(
'version-api' )->text(),
535 'other' =>
wfMessage(
'version-other' )->text(),
539 ->onExtensionTypes( self::$extensionTypes );
542 return self::$extensionTypes;
555 $types = self::getExtensionTypes();
557 return $types[$type] ?? $types[
'other'];
566 private function getExtensionCredits( array $credits ) {
567 $extensionTypes = self::getExtensionTypes();
569 $this->addTocSection(
'version-extensions',
'mw-version-ext' );
571 $out = Html::element(
573 [
'id' =>
'mw-version-ext' ],
574 $this->msg(
'version-extensions' )->text()
580 ( count( $credits ) === 1 && isset( $credits[
'skin'] ) )
582 $out .= Html::element(
585 $this->msg(
'version-extensions-no-ext' )->text()
592 $credits[
'other'] ??= [];
593 foreach ( $credits as $type => $extensions ) {
594 if ( !array_key_exists( $type, $extensionTypes ) ) {
595 $credits[
'other'] = array_merge( $credits[
'other'], $extensions );
600 foreach ( $extensionTypes as $type => $text ) {
602 if ( $type !==
'other' && $type !==
'skin' ) {
603 $out .= $this->getExtensionCategory( $type, $text, $credits[$type] ?? [] );
608 $out .= $this->getExtensionCategory(
'other', $extensionTypes[
'other'], $credits[
'other'] );
619 private function getSkinCredits( array $credits ) {
620 $this->addTocSection(
'version-skins',
'mw-version-skin' );
622 $out = Html::element(
624 [
'id' =>
'mw-version-skin' ],
625 $this->msg(
'version-skins' )->text()
628 if ( !isset( $credits[
'skin'] ) || !$credits[
'skin'] ) {
629 $out .= Html::element(
632 $this->msg(
'version-skins-no-skin' )->text()
637 $out .= $this->getExtensionCategory(
'skin',
null, $credits[
'skin'] );
649 $this->addTocSection(
'version-libraries',
'mw-version-libraries' );
651 $out = Html::element(
653 [
'id' =>
'mw-version-libraries' ],
654 $this->msg(
'version-libraries' )->text()
658 . $this->getExternalLibraries( $credits )
659 . $this->getClientSideLibraries();
670 MW_INSTALL_PATH .
'/vendor/composer/installed.json'
673 $extensionTypes = self::getExtensionTypes();
674 foreach ( $extensionTypes as $type => $message ) {
675 if ( !isset( $credits[$type] ) || $credits[$type] === [] ) {
678 foreach ( $credits[$type] as $extension ) {
679 if ( !isset( $extension[
'path'] ) ) {
682 $paths[] = dirname( $extension[
'path'] ) .
'/vendor/composer/installed.json';
688 foreach ( $paths as
$path ) {
689 if ( !file_exists(
$path ) ) {
695 $dependencies += $installed->getInstalledDependencies();
698 if ( $dependencies === [] ) {
702 ksort( $dependencies );
704 $this->addTocSubSection( $this->msg(
'version-libraries-server' )->text(),
'mw-version-libraries-server' );
706 $out = Html::element(
708 [
'id' =>
'mw-version-libraries-server' ],
709 $this->msg(
'version-libraries-server' )->text()
711 $out .= Html::openElement(
713 [
'class' =>
'wikitable plainlinks mw-installed-software',
'id' =>
'sv-libraries' ]
716 $out .= $this->getTableHeaderHtml( [
717 $this->msg(
'version-libraries-library' )->text(),
718 $this->msg(
'version-libraries-version' )->text(),
719 $this->msg(
'version-libraries-license' )->text(),
720 $this->msg(
'version-libraries-description' )->text(),
721 $this->msg(
'version-libraries-authors' )->text(),
724 foreach ( $dependencies as $name => $info ) {
725 if ( !is_array( $info ) || str_starts_with( $info[
'type'],
'mediawiki-' ) ) {
730 $authors = array_map(
static function ( $arr ) {
731 return new HtmlArmor( isset( $arr[
'homepage'] ) ?
732 Html::element(
'a', [
'href' => $arr[
'homepage'] ], $arr[
'name'] ) :
733 htmlspecialchars( $arr[
'name'] )
735 }, $info[
'authors'] );
736 $authors = $this->listAuthors( $authors,
false, MW_INSTALL_PATH .
"/vendor/$name" );
741 $out .= Html::openElement(
'tr', [
744 'id' => Sanitizer::escapeIdForAttribute(
745 "mw-version-library-$name"
750 $this->getLinkRenderer()->makeExternalLink(
751 "https://packagist.org/packages/$name",
753 $this->getFullTitle(),
755 [
'class' =>
'mw-version-library-name' ]
758 . Html::element(
'td', [
'dir' =>
'auto' ], $info[
'version'] )
760 . Html::element(
'td', [
'dir' =>
'auto' ], $this->listToText( $info[
'licenses'] ) )
761 . Html::element(
'td', [
'lang' =>
'en',
'dir' =>
'ltr' ], $info[
'description'] )
762 . Html::rawElement(
'td', [], $authors )
763 . Html::closeElement(
'tr' );
765 $out .= Html::closeElement(
'table' );
776 $registryDirs = [
'MediaWiki' => MW_INSTALL_PATH .
'/resources/lib' ]
777 + ExtensionRegistry::getInstance()->getAttribute(
'ForeignResourcesDir' );
780 foreach ( $registryDirs as
$source => $registryDir ) {
781 $foreignResources = Yaml::parseFile(
"$registryDir/foreign-resources.yaml" );
782 foreach ( $foreignResources as $name => $module ) {
783 $key = $name . $module[
'version'];
784 if ( isset( $modules[$key] ) ) {
785 $modules[$key][
'source'][] =
$source;
788 $modules[$key] = $module + [
'name' => $name,
'source' => [
$source ] ];
800 private function getClientSideLibraries() {
801 $this->addTocSubSection( $this->msg(
'version-libraries-client' )->text(),
'mw-version-libraries-client' );
803 $out = Html::element(
805 [
'id' =>
'mw-version-libraries-client' ],
806 $this->msg(
'version-libraries-client' )->text()
808 $out .= Html::openElement(
810 [
'class' =>
'wikitable plainlinks mw-installed-software',
'id' =>
'sv-libraries-client' ]
813 $out .= $this->getTableHeaderHtml( [
814 $this->msg(
'version-libraries-library' )->text(),
815 $this->msg(
'version-libraries-version' )->text(),
816 $this->msg(
'version-libraries-license' )->text(),
817 $this->msg(
'version-libraries-authors' )->text(),
818 $this->msg(
'version-libraries-source' )->text()
821 foreach ( self::parseForeignResources() as $name => $info ) {
825 $out .= Html::openElement(
'tr', [
828 'id' => Sanitizer::escapeIdForAttribute(
829 "mw-version-library-$name"
834 $this->getLinkRenderer()->makeExternalLink(
837 $this->getFullTitle(),
839 [
'class' =>
'mw-version-library-name' ]
842 . Html::element(
'td', [
'dir' =>
'auto' ], $info[
'version'] )
843 . Html::element(
'td', [
'dir' =>
'auto' ], $info[
'license'] )
844 . Html::element(
'td', [
'dir' =>
'auto' ], $info[
'authors'] ??
'—' )
846 . Html::element(
'td', [
'dir' =>
'auto' ], $this->listToText( $info[
'source'] ) )
847 . Html::closeElement(
'tr' );
849 $out .= Html::closeElement(
'table' );
860 $tags = $this->parserFactory->getMainInstance()->getTags();
865 $this->addTocSection(
'version-parser-extensiontags',
'mw-version-parser-extensiontags' );
867 $out = Html::rawElement(
869 [
'id' =>
'mw-version-parser-extensiontags' ],
872 [
'class' =>
'plainlinks' ],
873 $this->getLinkRenderer()->makeExternalLink(
874 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
875 $this->msg(
'version-parser-extensiontags' ),
876 $this->getFullTitle()
881 array_walk( $tags,
static function ( &$value ) {
883 $value = Html::rawElement(
887 'style' =>
'white-space: nowrap;',
889 Html::element(
'code', [],
"<$value>" )
893 $out .= $this->listToText( $tags );
904 $funcHooks = $this->parserFactory->getMainInstance()->getFunctionHooks();
909 $this->addTocSection(
'version-parser-function-hooks',
'mw-version-parser-function-hooks' );
911 $out = Html::rawElement(
913 [
'id' =>
'mw-version-parser-function-hooks' ],
916 [
'class' =>
'plainlinks' ],
917 $this->getLinkRenderer()->makeExternalLink(
918 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
919 $this->msg(
'version-parser-function-hooks' ),
920 $this->getFullTitle()
925 $funcSynonyms = $this->parserFactory->getMainInstance()->getFunctionSynonyms();
932 $preferredSynonyms = array_flip( array_reverse( $funcSynonyms[1] + $funcSynonyms[0] ) );
933 array_walk( $funcHooks,
static function ( &$value ) use ( $preferredSynonyms ) {
934 $value = $preferredSynonyms[$value];
938 usort( $funcHooks,
static function ( $a, $b ) {
939 return strcasecmp( ltrim( $a,
'#' ), ltrim( $b,
'#' ) );
942 array_walk( $funcHooks,
static function ( &$value ) {
944 $value = Html::rawElement(
947 Html::element(
'code', [],
'{{' . $value .
'}}' )
951 $out .= $this->getLanguage()->listToText( $funcHooks );
968 if ( $creditsGroup ) {
969 $out .= $this->openExtType( $text,
'credits-' . $type );
971 usort( $creditsGroup, [ $this,
'compare' ] );
973 foreach ( $creditsGroup as $extension ) {
974 $out .= $this->getCreditsForExtension( $type, $extension );
977 $out .= Html::closeElement(
'table' );
990 return $this->getLanguage()->lc( $a[
'name'] ) <=> $this->getLanguage()->lc( $b[
'name'] );
1012 $out = $this->getOutput();
1016 if ( isset( $extension[
'namemsg'] ) ) {
1018 $extensionName = $this->msg( $extension[
'namemsg'] );
1019 } elseif ( isset( $extension[
'name'] ) ) {
1021 $extensionName = $extension[
'name'];
1023 $extensionName = $this->msg(
'version-no-ext-name' );
1026 if ( isset( $extension[
'url'] ) ) {
1027 $extensionNameLink = $this->getLinkRenderer()->makeExternalLink(
1030 $this->getFullTitle(),
1032 [
'class' =>
'mw-version-ext-name' ]
1035 $extensionNameLink = htmlspecialchars( $extensionName );
1041 $canonicalVersion =
'–';
1042 $extensionPath =
null;
1047 if ( isset( $extension[
'version'] ) ) {
1048 $canonicalVersion = $out->parseInlineAsInterface( $extension[
'version'] );
1051 if ( isset( $extension[
'path'] ) ) {
1052 $extensionPath = dirname( $extension[
'path'] );
1053 if ( $this->coreId ==
'' ) {
1054 wfDebug(
'Looking up core head id' );
1055 $coreHeadSHA1 = GitInfo::repo()->getHeadSHA1();
1056 if ( $coreHeadSHA1 ) {
1057 $this->coreId = $coreHeadSHA1;
1060 $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getInstance(
CACHE_ANYTHING );
1061 $memcKey = $cache->makeKey(
1062 'specialversion-ext-version-text', $extension[
'path'], $this->coreId
1064 [ $vcsVersion, $vcsLink, $vcsDate ] = $cache->get( $memcKey );
1066 if ( !$vcsVersion ) {
1067 wfDebug(
"Getting VCS info for extension {$extension['name']}" );
1068 $gitInfo =
new GitInfo( $extensionPath );
1069 $vcsVersion = $gitInfo->getHeadSHA1();
1070 if ( $vcsVersion !==
false ) {
1071 $vcsVersion = substr( $vcsVersion, 0, 7 );
1072 $vcsLink = $gitInfo->getHeadViewUrl();
1073 $vcsDate = $gitInfo->getHeadCommitDate();
1075 $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
1077 wfDebug(
"Pulled VCS info for extension {$extension['name']} from cache" );
1081 $versionString = Html::rawElement(
1083 [
'class' =>
'mw-version-ext-version' ],
1087 if ( $vcsVersion ) {
1089 $vcsVerString = $this->getLinkRenderer()->makeExternalLink(
1091 $this->msg(
'version-version', $vcsVersion ),
1092 $this->getFullTitle(),
1094 [
'class' =>
'mw-version-ext-vcs-version' ]
1097 $vcsVerString = Html::element(
'span',
1098 [
'class' =>
'mw-version-ext-vcs-version' ],
1102 $versionString .=
" {$vcsVerString}";
1105 $versionString .=
' ' . Html::element(
'span', [
1106 'class' =>
'mw-version-ext-vcs-timestamp',
1107 'dir' => $this->getLanguage()->getDir(),
1108 ], $this->getLanguage()->timeanddate( $vcsDate,
true ) );
1110 $versionString = Html::rawElement(
'span',
1111 [
'class' =>
'mw-version-ext-meta-version' ],
1119 if ( isset( $extension[
'name'] ) ) {
1120 $licenseName =
null;
1121 if ( isset( $extension[
'license-name'] ) ) {
1122 $licenseName =
new HtmlArmor( $out->parseInlineAsInterface( $extension[
'license-name'] ) );
1123 } elseif ( $extensionPath !==
null && ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
1124 $licenseName = $this->msg(
'version-ext-license' )->text();
1126 if ( $licenseName !==
null ) {
1127 $licenseLink = $this->getLinkRenderer()->makeLink(
1128 $this->getPageTitle(
'License/' . $extension[
'name'] ),
1131 'class' =>
'mw-version-ext-license',
1141 if ( isset( $extension[
'descriptionmsg'] ) ) {
1143 $descriptionMsg = $extension[
'descriptionmsg'];
1145 if ( is_array( $descriptionMsg ) ) {
1146 $descriptionMsgKey = array_shift( $descriptionMsg );
1147 $descriptionMsg = array_map(
'htmlspecialchars', $descriptionMsg );
1148 $description = $this->msg( $descriptionMsgKey, ...$descriptionMsg )->text();
1150 $description = $this->msg( $descriptionMsg )->text();
1152 } elseif ( isset( $extension[
'description'] ) ) {
1154 $description = $extension[
'description'];
1158 $description = $out->parseInlineAsInterface( $description );
1161 $authors = $extension[
'author'] ?? [];
1163 $authors = $this->listAuthors( $authors, $extension[
'name'], $extensionPath );
1166 $html = Html::openElement(
'tr', [
1167 'class' =>
'mw-version-ext',
1168 'id' => Sanitizer::escapeIdForAttribute(
'mw-version-ext-' . $type .
'-' . $extension[
'name'] )
1172 $html .= Html::rawElement(
'td', [], $extensionNameLink );
1173 $html .= Html::rawElement(
'td', [], $versionString );
1174 $html .= Html::rawElement(
'td', [], $licenseLink );
1175 $html .= Html::rawElement(
'td', [
'class' =>
'mw-version-ext-description' ], $description );
1176 $html .= Html::rawElement(
'td', [
'class' =>
'mw-version-ext-authors' ], $authors );
1178 $html .= Html::closeElement(
'tr' );
1188 private function getHooks() {
1189 if ( !$this->getConfig()->
get( MainConfigNames::SpecialVersionShowHooks ) ) {
1193 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1194 $hookNames = $hookContainer->getHookNames();
1196 if ( !$hookNames ) {
1203 $this->addTocSection(
'version-hooks',
'mw-version-hooks' );
1204 $ret[] = Html::element(
1206 [
'id' =>
'mw-version-hooks' ],
1207 $this->msg(
'version-hooks' )->text()
1209 $ret[] = Html::openElement(
'table', [
'class' =>
'wikitable',
'id' =>
'sv-hooks' ] );
1210 $ret[] = Html::openElement(
'tr' );
1211 $ret[] = Html::element(
'th', [], $this->msg(
'version-hook-name' )->text() );
1212 $ret[] = Html::element(
'th', [], $this->msg(
'version-hook-subscribedby' )->text() );
1213 $ret[] = Html::closeElement(
'tr' );
1215 foreach ( $hookNames as $name ) {
1216 $handlers = $hookContainer->getHandlerDescriptions( $name );
1218 $ret[] = Html::openElement(
'tr' );
1219 $ret[] = Html::element(
'td', [], $name );
1221 $ret[] = Html::element(
'td', [], $this->listToText( $handlers ) );
1222 $ret[] = Html::closeElement(
'tr' );
1225 $ret[] = Html::closeElement(
'table' );
1227 return implode(
"\n", $ret );
1230 private function openExtType( ?
string $text =
null, ?
string $name =
null ) {
1233 $opt = [
'class' =>
'wikitable plainlinks mw-installed-software' ];
1236 $opt[
'id'] =
"sv-$name";
1239 $out .= Html::openElement(
'table', $opt );
1241 if ( $text !==
null ) {
1242 $out .= Html::element(
'caption', [], $text );
1245 if ( $name && $text !==
null ) {
1246 $this->addTocSubSection( $text,
"sv-$name" );
1249 $firstHeadingMsg = ( $name ===
'credits-skin' )
1250 ?
'version-skin-colheader-name'
1251 :
'version-ext-colheader-name';
1253 $out .= $this->getTableHeaderHtml( [
1254 $this->msg( $firstHeadingMsg )->text(),
1255 $this->msg(
'version-ext-colheader-version' )->text(),
1256 $this->msg(
'version-ext-colheader-license' )->text(),
1257 $this->msg(
'version-ext-colheader-description' )->text(),
1258 $this->msg(
'version-ext-colheader-credits' )->text()
1272 private function getTableHeaderHtml( $headers ): string {
1274 $out .= Html::openElement(
'thead' );
1275 $out .= Html::openElement(
'tr' );
1276 foreach ( $headers as
$header ) {
1277 $out .= Html::element(
'th', [
'scope' =>
'col' ],
$header );
1279 $out .= Html::closeElement(
'tr' );
1280 $out .= Html::closeElement(
'thead' );
1289 private function IPInfo() {
1290 $ip = str_replace(
'--',
' - ', htmlspecialchars( $this->
getRequest()->getIP() ) );
1292 return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
1317 $linkRenderer = $this->getLinkRenderer();
1320 $authors = (array)$authors;
1325 if ( count( $authors ) === 1 && $authors[0] ===
'...' ) {
1328 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1329 return $linkRenderer->makeLink(
1330 $this->getPageTitle(
"Credits/$extName" ),
1331 $this->msg(
'version-poweredby-various' )->text()
1334 return $this->msg(
'version-poweredby-various' )->escaped();
1340 foreach ( $authors as $item ) {
1342 $list[] = HtmlArmor::getHtml( $item );
1343 } elseif ( $item ===
'...' ) {
1346 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1347 $text = $linkRenderer->makeLink(
1348 $this->getPageTitle(
"Credits/$extName" ),
1349 $this->msg(
'version-poweredby-others' )->text()
1352 $text = $this->msg(
'version-poweredby-others' )->escaped();
1355 } elseif ( str_ends_with( $item,
' ...]' ) ) {
1357 $list[] = $this->getOutput()->parseInlineAsInterface(
1358 substr( $item, 0, -4 ) . $this->msg(
'version-poweredby-others' )->text() .
"]"
1361 $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1365 if ( $extName && !$hasOthers && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1366 $list[] = $linkRenderer->makeLink(
1367 $this->getPageTitle(
"Credits/$extName" ),
1368 $this->msg(
'version-poweredby-others' )->text()
1372 return $this->listToText( $list,
false );
1384 private function listToText( array $list,
bool $sort =
true ): string {
1392 return $this->getLanguage()
1393 ->listToText( array_map( [ __CLASS__,
'arrayToString' ], $list ) );
1406 if ( is_array( $list ) && count( $list ) == 1 ) {
1409 if ( $list instanceof Closure ) {
1412 } elseif ( is_object( $list ) ) {
1413 return wfMessage(
'parentheses' )->params( get_class( $list ) )->escaped();
1414 } elseif ( !is_array( $list ) ) {
1417 if ( is_object( $list[0] ) ) {
1418 $class = get_class( $list[0] );
1423 return wfMessage(
'parentheses' )->params(
"$class, {$list[1]}" )->escaped();
1434 return (
new GitInfo( $dir ) )->getHeadSHA1();
1442 $config = $this->getConfig();
1443 $scriptPath = $config->get( MainConfigNames::ScriptPath ) ?:
'/';
1446 'version-entrypoints-articlepath' => $config->get( MainConfigNames::ArticlePath ),
1447 'version-entrypoints-scriptpath' => $scriptPath,
1448 'version-entrypoints-index-php' =>
wfScript(
'index' ),
1449 'version-entrypoints-api-php' =>
wfScript(
'api' ),
1450 'version-entrypoints-rest-php' =>
wfScript(
'rest' ),
1453 $language = $this->getLanguage();
1455 'dir' => $language->getDir(),
1456 'lang' => $language->getHtmlCode(),
1460 $this->addTocSection(
'version-entrypoints',
'mw-version-entrypoints' );
1462 $out = Html::element(
1464 [
'id' =>
'mw-version-entrypoints' ],
1465 $this->msg(
'version-entrypoints' )->text()
1467 Html::openElement(
'table',
1469 'class' =>
'wikitable plainlinks',
1470 'id' =>
'mw-version-entrypoints-table',
1475 Html::openElement(
'thead' ) .
1476 Html::openElement(
'tr' ) .
1480 $this->msg(
'version-entrypoints-header-entrypoint' )->text()
1485 $this->msg(
'version-entrypoints-header-url' )->text()
1487 Html::closeElement(
'tr' ) .
1488 Html::closeElement(
'thead' );
1490 foreach ( $entryPoints as $message => $value ) {
1492 $out .= Html::openElement(
'tr' ) .
1493 Html::rawElement(
'td', [], $this->msg( $message )->parse() ) .
1494 Html::rawElement(
'td', [],
1498 $this->msg(
new RawMessage(
"[$url $value]" ) )->parse()
1501 Html::closeElement(
'tr' );
1504 $out .= Html::closeElement(
'table' );