51 parent::__construct(
'Version' );
61 $extensionCredits = $config->get(
'ExtensionCredits' );
66 $out->allowClickjacking();
69 $parts = explode(
'/', (
string)$par );
71 if ( isset( $parts[1] ) ) {
72 $extName = str_replace(
'_',
' ', $parts[1] );
74 foreach ( $extensionCredits as $group => $extensions ) {
75 foreach ( $extensions as
$ext ) {
76 if ( isset(
$ext[
'name'] ) && (
$ext[
'name'] === $extName ) ) {
83 $out->setStatusCode( 404 );
86 $extName =
'MediaWiki';
90 switch ( strtolower( $parts[0] ) ) {
92 $out->addModuleStyles(
'mediawiki.special.version' );
94 $wikiText =
'{{int:version-credits-not-found}}';
95 if ( $extName ===
'MediaWiki' ) {
96 $wikiText = file_get_contents(
$IP .
'/CREDITS' );
98 $wikiText = str_replace(
99 [
'<!-- BEGIN CONTRIBUTOR LIST -->',
'<!-- END CONTRIBUTOR LIST -->' ],
100 [
'<div class="mw-version-credits">',
'</div>' ],
102 } elseif ( ( $extNode !==
null ) && isset( $extNode[
'path'] ) ) {
105 $wikiText = file_get_contents(
$file );
106 if ( substr(
$file, -4 ) ===
'.txt' ) {
107 $wikiText = Html::element(
119 $out->setPageTitle( $this->
msg(
'version-credits-title', $extName ) );
120 $out->addWikiTextAsInterface( $wikiText );
124 $wikiText =
'{{int:version-license-not-found}}';
125 if ( $extName ===
'MediaWiki' ) {
126 $wikiText = file_get_contents(
$IP .
'/COPYING' );
127 } elseif ( ( $extNode !==
null ) && isset( $extNode[
'path'] ) ) {
130 $wikiText = file_get_contents(
$file );
131 $wikiText = Html::element(
142 $out->setPageTitle( $this->
msg(
'version-license-title', $extName ) );
143 $out->addWikiTextAsInterface( $wikiText );
147 $out->addModuleStyles(
'mediawiki.special.version' );
148 $out->addWikiTextAsInterface(
160 $out->addWikiTextAsInterface( $this->
getWgHooks() );
161 $out->addHTML( $this->
IPInfo() );
175 [
'id' =>
'mw-version-license' ],
180 $ret .=
'<div class="plainlinks">';
183 " .
'<div class="mw-version-license-info">' .
184 wfMessage(
'version-license-info' )->text() .
188 return str_replace(
"\t\t",
'', $ret ) .
"\n";
199 if ( defined(
'MEDIAWIKI_INSTALL' ) ) {
200 $othersLink =
'[https://www.mediawiki.org/wiki/Special:Version/Credits ' .
201 wfMessage(
'version-poweredby-others' )->text() .
']';
203 $othersLink =
'[[Special:Version/Credits|' .
204 wfMessage(
'version-poweredby-others' )->text() .
']]';
207 $translatorsLink =
'[https://translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
208 wfMessage(
'version-poweredby-translators' )->text() .
']';
211 'Magnus Manske',
'Brion Vibber',
'Lee Daniel Crocker',
212 'Tim Starling',
'Erik Möller',
'Gabriel Wicke',
'Ævar Arnfjörð Bjarmason',
213 'Niklas Laxström',
'Domas Mituzas',
'Rob Church',
'Yuri Astrakhan',
214 'Aryeh Gregor',
'Aaron Schulz',
'Andrew Garrett',
'Raimond Spekking',
215 'Alexandre Emsenhuber',
'Siebrand Mazeland',
'Chad Horohoe',
216 'Roan Kattouw',
'Trevor Parscal',
'Bryan Tong Minh',
'Sam Reed',
217 'Victor Vasiliev',
'Rotem Liss',
'Platonides',
'Antoine Musso',
218 'Timo Tijhof',
'Daniel Kinzler',
'Jeroen De Dauw',
'Brad Jorsch',
219 'Bartosz Dziewoński',
'Ed Sanders',
'Moriel Schottlender',
220 'Kunal Mehta',
'James D. Forrester',
'Brian Wolff',
'Adam Shorland',
221 $othersLink, $translatorsLink
225 $wgLang->listToText( $authorList ) )->text();
244 $software[
'[https://hhvm.com/ HHVM]'] = HHVM_VERSION .
" (" . PHP_SAPI .
")";
246 $software[
'[https://php.net/ PHP]'] = PHP_VERSION .
" (" . PHP_SAPI .
")";
249 $software[
$dbr->getSoftwareLink()] =
$dbr->getServerInfo();
251 if ( defined(
'INTL_ICU_VERSION' ) ) {
252 $software[
'[http://site.icu-project.org/ ICU]'] = INTL_ICU_VERSION;
269 [
'id' =>
'mw-version-software' ],
272 Xml::openElement(
'table', [
'class' =>
'wikitable plainlinks',
'id' =>
'sv-software' ] ) .
274 <th>" .
wfMessage(
'version-software-product' )->text() .
"</th>
275 <th>" .
wfMessage(
'version-software-version' )->text() .
"</th>
278 foreach ( self::getSoftwareInformation() as $name => $version ) {
280 <td>" . $name .
"</td>
281 <td dir=\"ltr\">" . $version .
"</td>
301 } elseif ( $flags ===
'nodb' ) {
302 $shortSha1 = substr( $gitInfo, 0, 7 );
303 $version =
"$wgVersion ($shortSha1)";
305 $shortSha1 = substr( $gitInfo, 0, 7 );
307 if (
$lang !==
null ) {
308 $msg->inLanguage(
$lang );
310 $shortSha1 = $msg->params( $shortSha1 )->escaped();
311 $version =
"$wgVersion $shortSha1";
345 preg_match(
"/^(\d+\.\d+)/",
$wgVersion, $versionParts );
346 $versionUrl =
"https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
349 return "[$versionUrl $wgVersion]";
361 $headSHA1 = $gitInfo->getHeadSHA1();
366 $shortSHA1 =
'(' . substr( $headSHA1, 0, 7 ) .
')';
368 $gitHeadUrl = $gitInfo->getHeadViewUrl();
369 if ( $gitHeadUrl !==
false ) {
370 $shortSHA1 =
"[$gitHeadUrl $shortSHA1]";
373 $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
374 if ( $gitHeadCommitDate ) {
375 $shortSHA1 .= Html::element(
'br' ) .
$wgLang->timeanddate( $gitHeadCommitDate,
true );
392 if ( self::$extensionTypes ===
false ) {
393 self::$extensionTypes = [
394 'specialpage' =>
wfMessage(
'version-specialpages' )->text(),
395 'editor' =>
wfMessage(
'version-editors' )->text(),
396 'parserhook' =>
wfMessage(
'version-parserhooks' )->text(),
397 'variable' =>
wfMessage(
'version-variables' )->text(),
398 'media' =>
wfMessage(
'version-mediahandlers' )->text(),
399 'antispam' =>
wfMessage(
'version-antispam' )->text(),
400 'skin' =>
wfMessage(
'version-skins' )->text(),
401 'api' =>
wfMessage(
'version-api' )->text(),
402 'other' =>
wfMessage(
'version-other' )->text(),
405 Hooks::run(
'ExtensionTypes', [ &self::$extensionTypes ] );
423 return $types[
$type] ?? $types[
'other'];
433 $extensionCredits = $config->get(
'ExtensionCredits' );
436 count( $extensionCredits ) === 0 ||
438 ( count( $extensionCredits ) === 1 && isset( $extensionCredits[
'skin'] ) )
447 [
'id' =>
'mw-version-ext' ],
448 $this->
msg(
'version-extensions' )->text()
450 Xml::openElement(
'table', [
'class' =>
'wikitable plainlinks',
'id' =>
'sv-ext' ] );
453 if ( !array_key_exists(
'other', $extensionCredits ) ) {
454 $extensionCredits[
'other'] = [];
458 foreach ( $extensionCredits as
$type => $extensions ) {
460 $extensionCredits[
'other'] = array_merge( $extensionCredits[
'other'], $extensions );
464 $this->firstExtOpened =
false;
468 if (
$type !==
'other' &&
$type !==
'skin' ) {
494 [
'id' =>
'mw-version-skin' ],
495 $this->
msg(
'version-skins' )->text()
497 Xml::openElement(
'table', [
'class' =>
'wikitable plainlinks',
'id' =>
'sv-skin' ] );
499 $this->firstExtOpened =
false;
514 $path =
"$IP/vendor/composer/installed.json";
515 if ( !file_exists(
$path ) ) {
520 $out = Html::element(
522 [
'id' =>
'mw-version-libraries' ],
523 $this->
msg(
'version-libraries' )->text()
525 $out .= Html::openElement(
527 [
'class' =>
'wikitable plainlinks',
'id' =>
'sv-libraries' ]
529 $out .= Html::openElement(
'tr' )
530 . Html::element(
'th', [], $this->
msg(
'version-libraries-library' )->text() )
531 . Html::element(
'th', [], $this->
msg(
'version-libraries-version' )->text() )
532 . Html::element(
'th', [], $this->
msg(
'version-libraries-license' )->text() )
533 . Html::element(
'th', [], $this->
msg(
'version-libraries-description' )->text() )
534 . Html::element(
'th', [], $this->
msg(
'version-libraries-authors' )->text() )
535 . Html::closeElement(
'tr' );
537 foreach ( $installed->getInstalledDependencies() as $name => $info ) {
538 if ( strpos( $info[
'type'],
'mediawiki-' ) === 0 ) {
543 $authors = array_map(
function ( $arr ) {
545 if ( isset( $arr[
'homepage'] ) ) {
546 return "[{$arr['homepage']} {$arr['name']}]";
549 }, $info[
'authors'] );
550 $authors = $this->
listAuthors( $authors,
false,
"$IP/vendor/$name" );
555 $out .= Html::openElement(
'tr' )
560 "https://packagist.org/packages/$name", $name,
562 [
'class' =>
'mw-version-library-name' ]
565 . Html::element(
'td', [
'dir' =>
'auto' ], $info[
'version'] )
566 . Html::element(
'td', [
'dir' =>
'auto' ], $this->
listToText( $info[
'licenses'] ) )
567 . Html::element(
'td', [
'lang' =>
'en',
'dir' =>
'ltr' ], $info[
'description'] )
568 . Html::rawElement(
'td', [], $authors )
569 . Html::closeElement(
'tr' );
571 $out .= Html::closeElement(
'table' );
582 $tags = MediaWikiServices::getInstance()->getParser()->getTags();
584 if ( count( $tags ) ) {
585 $out = Html::rawElement(
588 'class' =>
'mw-headline plainlinks',
589 'id' =>
'mw-version-parser-extensiontags',
592 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
593 $this->
msg(
'version-parser-extensiontags' )->parse(),
598 array_walk( $tags,
function ( &$value ) {
600 $value = Html::element(
604 'style' =>
'white-space: nowrap;',
624 $fhooks = MediaWikiServices::getInstance()->getParser()->getFunctionHooks();
625 if ( count( $fhooks ) ) {
626 $out = Html::rawElement(
629 'class' =>
'mw-headline plainlinks',
630 'id' =>
'mw-version-parser-function-hooks',
633 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
634 $this->
msg(
'version-parser-function-hooks' )->parse(),
659 $extensionCredits = $config->get(
'ExtensionCredits' );
663 if ( array_key_exists(
$type, $extensionCredits ) && count( $extensionCredits[
$type] ) > 0 ) {
666 usort( $extensionCredits[
$type], [ $this,
'compare' ] );
668 foreach ( $extensionCredits[
$type] as $extension ) {
709 if ( isset( $extension[
'namemsg'] ) ) {
711 $extensionName = $this->
msg( $extension[
'namemsg'] )->text();
712 } elseif ( isset( $extension[
'name'] ) ) {
714 $extensionName = $extension[
'name'];
716 $extensionName = $this->
msg(
'version-no-ext-name' )->text();
719 if ( isset( $extension[
'url'] ) ) {
725 [
'class' =>
'mw-version-ext-name' ]
728 $extensionNameLink = htmlspecialchars( $extensionName );
734 $canonicalVersion =
'–';
735 $extensionPath =
null;
740 if ( isset( $extension[
'version'] ) ) {
741 $canonicalVersion = $out->parseInlineAsInterface( $extension[
'version'] );
744 if ( isset( $extension[
'path'] ) ) {
746 $extensionPath = dirname( $extension[
'path'] );
747 if ( $this->coreId ==
'' ) {
748 wfDebug(
'Looking up core head id' );
750 if ( $coreHeadSHA1 ) {
751 $this->coreId = $coreHeadSHA1;
755 $memcKey =
$cache->makeKey(
756 'specialversion-ext-version-text', $extension[
'path'], $this->coreId
758 list( $vcsVersion, $vcsLink, $vcsDate ) =
$cache->get( $memcKey );
760 if ( !$vcsVersion ) {
761 wfDebug(
"Getting VCS info for extension {$extension['name']}" );
762 $gitInfo =
new GitInfo( $extensionPath );
763 $vcsVersion = $gitInfo->getHeadSHA1();
764 if ( $vcsVersion !==
false ) {
765 $vcsVersion = substr( $vcsVersion, 0, 7 );
766 $vcsLink = $gitInfo->getHeadViewUrl();
767 $vcsDate = $gitInfo->getHeadCommitDate();
769 $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
771 wfDebug(
"Pulled VCS info for extension {$extension['name']} from cache" );
775 $versionString = Html::rawElement(
777 [
'class' =>
'mw-version-ext-version' ],
785 $this->
msg(
'version-version', $vcsVersion ),
788 [
'class' =>
'mw-version-ext-vcs-version' ]
791 $vcsVerString = Html::element(
'span',
792 [
'class' =>
'mw-version-ext-vcs-version' ],
796 $versionString .=
" {$vcsVerString}";
799 $vcsTimeString = Html::element(
'span',
800 [
'class' =>
'mw-version-ext-vcs-timestamp' ],
801 $this->
getLanguage()->timeanddate( $vcsDate,
true )
803 $versionString .=
" {$vcsTimeString}";
805 $versionString = Html::rawElement(
'span',
806 [
'class' =>
'mw-version-ext-meta-version' ],
814 if ( isset( $extension[
'name'] ) ) {
816 if ( isset( $extension[
'license-name'] ) ) {
817 $licenseName =
new HtmlArmor( $out->parseInlineAsInterface( $extension[
'license-name'] ) );
819 $licenseName = $this->
msg(
'version-ext-license' )->text();
821 if ( $licenseName !==
null ) {
826 'class' =>
'mw-version-ext-license',
836 if ( isset( $extension[
'descriptionmsg'] ) ) {
838 $descriptionMsg = $extension[
'descriptionmsg'];
840 if ( is_array( $descriptionMsg ) ) {
841 $descriptionMsgKey = $descriptionMsg[0];
842 array_shift( $descriptionMsg );
843 array_map(
"htmlspecialchars", $descriptionMsg );
844 $description = $this->
msg( $descriptionMsgKey, $descriptionMsg )->text();
846 $description = $this->
msg( $descriptionMsg )->text();
848 } elseif ( isset( $extension[
'description'] ) ) {
850 $description = $extension[
'description'];
854 $description = $out->parseInlineAsInterface( $description );
857 $authors = $extension[
'author'] ?? [];
858 $authors = $this->
listAuthors( $authors, $extension[
'name'], $extensionPath );
861 $html = Html::openElement(
'tr', [
862 'class' =>
'mw-version-ext',
863 'id' => Sanitizer::escapeIdForAttribute(
'mw-version-ext-' .
$type .
'-' . $extension[
'name'] )
867 $html .= Html::rawElement(
'td', [], $extensionNameLink );
868 $html .= Html::rawElement(
'td', [], $versionString );
869 $html .= Html::rawElement(
'td', [], $licenseLink );
870 $html .= Html::rawElement(
'td', [
'class' =>
'mw-version-ext-description' ], $description );
871 $html .= Html::rawElement(
'td', [
'class' =>
'mw-version-ext-authors' ], $authors );
873 $html .= Html::closeElement(
'tr' );
891 $ret[] =
'== {{int:version-hooks}} ==';
892 $ret[] = Html::openElement(
'table', [
'class' =>
'wikitable',
'id' =>
'sv-hooks' ] );
893 $ret[] = Html::openElement(
'tr' );
894 $ret[] = Html::element(
'th', [], $this->
msg(
'version-hook-name' )->text() );
895 $ret[] = Html::element(
'th', [], $this->
msg(
'version-hook-subscribedby' )->text() );
896 $ret[] = Html::closeElement(
'tr' );
898 foreach ( $myWgHooks as $hook => $hooks ) {
899 $ret[] = Html::openElement(
'tr' );
900 $ret[] = Html::element(
'td', [], $hook );
901 $ret[] = Html::element(
'td', [], $this->
listToText( $hooks ) );
902 $ret[] = Html::closeElement(
'tr' );
905 $ret[] = Html::closeElement(
'table' );
907 return implode(
"\n", $ret );
916 $opt = [
'colspan' => 5 ];
917 if ( $this->firstExtOpened ) {
919 $out .= Html::rawElement(
'tr', [
'class' =>
'sv-space' ],
920 Html::element(
'td', $opt )
923 $this->firstExtOpened =
true;
926 $opt[
'id'] =
"sv-$name";
929 if ( $text !==
null ) {
930 $out .= Html::rawElement(
'tr', [],
931 Html::element(
'th', $opt, $text )
935 $firstHeadingMsg = ( $name ===
'credits-skin' )
936 ?
'version-skin-colheader-name'
937 :
'version-ext-colheader-name';
938 $out .= Html::openElement(
'tr' );
939 $out .= Html::element(
'th', [
'class' =>
'mw-version-ext-col-label' ],
940 $this->
msg( $firstHeadingMsg )->text() );
941 $out .= Html::element(
'th', [
'class' =>
'mw-version-ext-col-label' ],
942 $this->
msg(
'version-ext-colheader-version' )->text() );
943 $out .= Html::element(
'th', [
'class' =>
'mw-version-ext-col-label' ],
944 $this->
msg(
'version-ext-colheader-license' )->text() );
945 $out .= Html::element(
'th', [
'class' =>
'mw-version-ext-col-label' ],
946 $this->
msg(
'version-ext-colheader-description' )->text() );
947 $out .= Html::element(
'th', [
'class' =>
'mw-version-ext-col-label' ],
948 $this->
msg(
'version-ext-colheader-credits' )->text() );
949 $out .= Html::closeElement(
'tr' );
960 $ip = str_replace(
'--',
' - ', htmlspecialchars( $this->
getRequest()->getIP() ) );
962 return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
991 $authors = (array)$authors;
996 if ( count( $authors ) === 1 && $authors[0] ===
'...' ) {
1002 $this->
msg(
'version-poweredby-various' )->text()
1005 return $this->
msg(
'version-poweredby-various' )->escaped();
1011 foreach ( $authors as $item ) {
1012 if ( $item ==
'...' ) {
1018 $this->
msg(
'version-poweredby-others' )->text()
1021 $text = $this->
msg(
'version-poweredby-others' )->escaped();
1024 } elseif ( substr( $item, -5 ) ==
' ...]' ) {
1026 $list[] = $this->
getOutput()->parseInlineAsInterface(
1027 substr( $item, 0, -4 ) . $this->
msg(
'version-poweredby-others' )->text() .
"]"
1030 $list[] = $this->
getOutput()->parseInlineAsInterface( $item );
1037 $this->
msg(
'version-poweredby-others' )->text()
1060 foreach ( scandir( $extDir ) as
$file ) {
1061 $fullPath = $extDir . DIRECTORY_SEPARATOR .
$file;
1062 if ( preg_match(
'/^((AUTHORS)|(CREDITS))(\.txt|\.wiki|\.mediawiki)?$/',
$file ) &&
1063 is_readable( $fullPath ) &&
1064 is_file( $fullPath )
1089 foreach ( scandir( $extDir ) as
$file ) {
1090 $fullPath = $extDir . DIRECTORY_SEPARATOR .
$file;
1091 if ( preg_match(
'/^((COPYING)|(LICENSE))(\.txt)?$/',
$file ) &&
1092 is_readable( $fullPath ) &&
1093 is_file( $fullPath )
1111 if ( !count( $list ) ) {
1119 ->listToText( array_map( [ __CLASS__,
'arrayToString' ], $list ) );
1131 if ( is_array( $list ) && count( $list ) == 1 ) {
1134 if ( $list instanceof Closure ) {
1137 } elseif ( is_object( $list ) ) {
1138 $class =
wfMessage(
'parentheses' )->params( get_class( $list ) )->escaped();
1141 } elseif ( !is_array( $list ) ) {
1144 if ( is_object( $list[0] ) ) {
1145 $class = get_class( $list[0] );
1150 return wfMessage(
'parentheses' )->params(
"$class, {$list[1]}" )->escaped();
1161 return $repo->getHeadSHA1();
1170 return $repo->getCurrentBranch();
1179 $scriptPath = $config->get(
'ScriptPath' ) ?:
'/';
1182 'version-entrypoints-articlepath' => $config->get(
'ArticlePath' ),
1183 'version-entrypoints-scriptpath' => $scriptPath,
1184 'version-entrypoints-index-php' =>
wfScript(
'index' ),
1185 'version-entrypoints-api-php' =>
wfScript(
'api' ),
1186 'version-entrypoints-load-php' =>
wfScript(
'load' ),
1191 'dir' => $language->getDir(),
1192 'lang' => $language->getHtmlCode()
1194 $out = Html::element(
1196 [
'id' =>
'mw-version-entrypoints' ],
1197 $this->
msg(
'version-entrypoints' )->text()
1199 Html::openElement(
'table',
1201 'class' =>
'wikitable plainlinks',
1202 'id' =>
'mw-version-entrypoints-table',
1207 Html::openElement(
'tr' ) .
1211 $this->
msg(
'version-entrypoints-header-entrypoint' )->text()
1216 $this->
msg(
'version-entrypoints-header-url' )->text()
1218 Html::closeElement(
'tr' );
1220 foreach ( $entryPoints as $message => $value ) {
1222 $out .= Html::openElement(
'tr' ) .
1225 Html::rawElement(
'td', [], $this->
msg( $message )->plain() ) .
1226 Html::rawElement(
'td', [], Html::rawElement(
'code', [],
"[$url $value]" ) ) .
1227 Html::closeElement(
'tr' );
1230 $out .= Html::closeElement(
'table' );