MediaWiki 1.40.4
SpecialVersion.php
Go to the documentation of this file.
1<?php
34use Symfony\Component\Yaml\Yaml;
35use Wikimedia\Parsoid\Core\SectionMetadata;
36use Wikimedia\Parsoid\Core\TOCData;
37
44
48 protected $firstExtOpened = false;
49
53 protected $coreId = '';
54
58 protected static $extensionTypes = false;
59
61 protected $tocData;
62
64 protected $tocLength;
65
67 private $parser;
68
70 private $urlUtils;
71
76 public function __construct(
77 Parser $parser,
78 UrlUtils $urlUtils
79 ) {
80 parent::__construct( 'Version' );
81 $this->parser = $parser;
82 $this->urlUtils = $urlUtils;
83 }
84
92 public static function getCredits( ExtensionRegistry $reg, Config $conf ): array {
93 $credits = $conf->get( MainConfigNames::ExtensionCredits );
94 foreach ( $reg->getAllThings() as $credit ) {
95 $credits[$credit['type']][] = $credit;
96 }
97 return $credits;
98 }
99
104 public function execute( $par ) {
105 global $IP;
106 $config = $this->getConfig();
107 $credits = self::getCredits( ExtensionRegistry::getInstance(), $config );
108
109 $this->setHeaders();
110 $this->outputHeader();
111 $out = $this->getOutput();
112 $out->setPreventClickjacking( false );
113
114 // Explode the sub page information into useful bits
115 $parts = explode( '/', (string)$par );
116 $extNode = null;
117 if ( isset( $parts[1] ) ) {
118 $extName = str_replace( '_', ' ', $parts[1] );
119 // Find it!
120 foreach ( $credits as $extensions ) {
121 foreach ( $extensions as $ext ) {
122 if ( isset( $ext['name'] ) && ( $ext['name'] === $extName ) ) {
123 $extNode = &$ext;
124 break 2;
125 }
126 }
127 }
128 if ( !$extNode ) {
129 $out->setStatusCode( 404 );
130 }
131 } else {
132 $extName = 'MediaWiki';
133 }
134
135 // Now figure out what to do
136 switch ( strtolower( $parts[0] ) ) {
137 case 'credits':
138 $out->addModuleStyles( 'mediawiki.special' );
139
140 $wikiText = '{{int:version-credits-not-found}}';
141 if ( $extName === 'MediaWiki' ) {
142 $wikiText = file_get_contents( $IP . '/CREDITS' );
143 // Put the contributor list into columns
144 $wikiText = str_replace(
145 [ '<!-- BEGIN CONTRIBUTOR LIST -->', '<!-- END CONTRIBUTOR LIST -->' ],
146 [ '<div class="mw-version-credits">', '</div>' ],
147 $wikiText );
148 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
149 $file = ExtensionInfo::getAuthorsFileName( dirname( $extNode['path'] ) );
150 if ( $file ) {
151 $wikiText = file_get_contents( $file );
152 if ( str_ends_with( $file, '.txt' ) ) {
153 $wikiText = Html::element(
154 'pre',
155 [
156 'lang' => 'en',
157 'dir' => 'ltr',
158 ],
159 $wikiText
160 );
161 }
162 }
163 }
164
165 $out->setPageTitle( $this->msg( 'version-credits-title', $extName ) );
166 $out->addWikiTextAsInterface( $wikiText );
167 break;
168
169 case 'license':
170 $out->setPageTitle( $this->msg( 'version-license-title', $extName ) );
171
172 $licenseFound = false;
173
174 if ( $extName === 'MediaWiki' ) {
175 $out->addWikiTextAsInterface(
176 file_get_contents( $IP . '/COPYING' )
177 );
178 $licenseFound = true;
179 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
180 $files = ExtensionInfo::getLicenseFileNames( dirname( $extNode['path'] ) );
181 if ( $files ) {
182 $licenseFound = true;
183 foreach ( $files as $file ) {
184 $out->addWikiTextAsInterface(
185 Html::element(
186 'pre',
187 [
188 'lang' => 'en',
189 'dir' => 'ltr',
190 ],
191 file_get_contents( $file )
192 )
193 );
194 }
195 }
196 }
197 if ( !$licenseFound ) {
198 $out->addWikiTextAsInterface( '{{int:version-license-not-found}}' );
199 }
200 break;
201
202 default:
203 $out->addModuleStyles( 'mediawiki.special' );
204
205 $out->addHTML( $this->getMediaWikiCredits() );
206
207 $this->tocData = new TOCData();
208 $this->tocLength = 0;
209
210 // Build the page contents (this also fills in TOCData)
211 $sections = [
212 [ 'html', $this->softwareInformation() ],
213 [ 'wikitext', $this->getEntryPointInfo() ],
214 [ 'html', $this->getSkinCredits( $credits ) ],
215 [ 'html', $this->getExtensionCredits( $credits ) ],
216 [ 'html', $this->getExternalLibraries( $credits ) ],
217 [ 'html', $this->getClientSideLibraries() ],
218 [ 'html', $this->getParserTags() ],
219 [ 'html', $this->getParserFunctionHooks() ],
220 [ 'wikitext', $this->getHooks() ],
221 [ 'html', $this->IPInfo() ],
222 ];
223
224 // Insert TOC first
225 $pout = new ParserOutput;
226 $pout->setTOCData( $this->tocData );
227 $pout->setOutputFlag( ParserOutputFlags::SHOW_TOC );
228 $pout->setText( Parser::TOC_PLACEHOLDER );
229 $out->addParserOutputText( $pout );
230
231 // Insert contents
232 foreach ( $sections as [ $mode, $content ] ) {
233 if ( $mode === 'wikitext' ) {
234 $out->addWikiTextAsInterface( $content );
235 } elseif ( $mode === 'html' ) {
236 // Yeah Phan, I get it, mixing HTML and wikitext like this is not a good practice
237 // @phan-suppress-next-line SecurityCheck-XSS
238 $out->addHTML( $content );
239 }
240 }
241
242 // Set TOC metadata at the end, because otherwise the addWikiTextAsInterface() calls override it
243 $out->addParserOutputMetadata( $pout );
244
245 break;
246 }
247 }
248
256 private function addTocSection( $labelMsg, $id ) {
257 $this->tocLength++;
258 $this->tocData->addSection( new SectionMetadata(
259 1,
260 2,
261 $this->msg( $labelMsg )->escaped(),
262 $this->getLanguage()->formatNum( $this->tocLength ),
263 (string)$this->tocLength,
264 null,
265 null,
266 $id,
267 $id
268 ) );
269 }
270
276 private function getMediaWikiCredits() {
277 // No TOC entry for this heading, we treat it like the lede section
278
279 $ret = Html::element(
280 'h2',
281 [ 'id' => 'mw-version-license' ],
282 $this->msg( 'version-license' )->text()
283 );
284
285 $ret .= Html::rawElement( 'div', [ 'class' => 'plainlinks' ],
286 $this->msg( new RawMessage( self::getCopyrightAndAuthorList() ) )->parseAsBlock() .
287 Html::rawElement( 'div', [ 'class' => 'mw-version-license-info' ],
288 $this->msg( 'version-license-info' )->parseAsBlock()
289 )
290 );
291
292 return $ret;
293 }
294
301 public static function getCopyrightAndAuthorList() {
302 if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
303 $othersLink = '[https://www.mediawiki.org/wiki/Special:Version/Credits ' .
304 wfMessage( 'version-poweredby-others' )->plain() . ']';
305 } else {
306 $othersLink = '[[Special:Version/Credits|' .
307 wfMessage( 'version-poweredby-others' )->plain() . ']]';
308 }
309
310 $translatorsLink = '[https://translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
311 wfMessage( 'version-poweredby-translators' )->plain() . ']';
312
313 $authorList = [
314 'Magnus Manske', 'Brion Vibber', 'Lee Daniel Crocker',
315 'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
316 'Niklas Laxström', 'Domas Mituzas', 'Rob Church', 'Yuri Astrakhan',
317 'Aryeh Gregor', 'Aaron Schulz', 'Andrew Garrett', 'Raimond Spekking',
318 'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
319 'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
320 'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
321 'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', 'Brad Jorsch',
322 'Bartosz Dziewoński', 'Ed Sanders', 'Moriel Schottlender',
323 'Kunal Mehta', 'James D. Forrester', 'Brian Wolff', 'Adam Shorland',
324 'DannyS712', 'Ori Livneh',
325 $othersLink, $translatorsLink
326 ];
327
328 return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
329 Message::listParam( $authorList ) )->plain();
330 }
331
337 private static function getSoftwareInformation() {
339
340 // Put the software in an array of form 'name' => 'version'. All messages should
341 // be loaded here, so feel free to use wfMessage in the 'name'. Wikitext
342 // can be used both in the name and value.
343 $software = [
344 '[https://www.mediawiki.org/ MediaWiki]' => self::getVersionLinked(),
345 '[https://php.net/ PHP]' => PHP_VERSION . " (" . PHP_SAPI . ")",
346 '[https://icu.unicode.org/ ICU]' => INTL_ICU_VERSION,
347 $dbr->getSoftwareLink() => $dbr->getServerInfo(),
348 ];
349
350 // Allow a hook to add/remove items.
351 Hooks::runner()->onSoftwareInfo( $software );
352
353 return $software;
354 }
355
361 private function softwareInformation() {
362 $this->addTocSection( 'version-software', 'mw-version-software' );
363
364 $out = Html::element(
365 'h2',
366 [ 'id' => 'mw-version-software' ],
367 $this->msg( 'version-software' )->text()
368 );
369
370 $out .= Html::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ] );
371
372 $out .= Html::rawElement( 'tr', [],
373 Html::element( 'th', [], $this->msg( 'version-software-product' )->text() ) .
374 Html::element( 'th', [], $this->msg( 'version-software-version' )->text() )
375 );
376
377 foreach ( self::getSoftwareInformation() as $name => $version ) {
378 $out .= Html::rawElement( 'tr', [],
379 Html::rawElement( 'td', [], $this->msg( new RawMessage( $name ) )->parse() ) .
380 Html::rawElement( 'td', [ 'dir' => 'ltr' ], $this->msg( new RawMessage( $version ) )->parse() )
381 );
382 }
383
384 $out .= Html::closeElement( 'table' );
385
386 return $out;
387 }
388
397 public static function getVersion( $flags = '', $lang = null ) {
398 global $IP;
399
400 $gitInfo = self::getGitHeadSha1( $IP );
401 if ( !$gitInfo ) {
402 $version = MW_VERSION;
403 } elseif ( $flags === 'nodb' ) {
404 $shortSha1 = substr( $gitInfo, 0, 7 );
405 $version = MW_VERSION . " ($shortSha1)";
406 } else {
407 $shortSha1 = substr( $gitInfo, 0, 7 );
408 $msg = wfMessage( 'parentheses' );
409 if ( $lang !== null ) {
410 $msg->inLanguage( $lang );
411 }
412 $shortSha1 = $msg->params( $shortSha1 )->escaped();
413 $version = MW_VERSION . ' ' . $shortSha1;
414 }
415
416 return $version;
417 }
418
426 public static function getVersionLinked() {
427 $gitVersion = self::getVersionLinkedGit();
428 if ( $gitVersion ) {
429 $v = $gitVersion;
430 } else {
431 $v = MW_VERSION; // fallback
432 }
433
434 return $v;
435 }
436
440 private static function getMWVersionLinked() {
441 $versionUrl = "";
442 if ( Hooks::runner()->onSpecialVersionVersionUrl( MW_VERSION, $versionUrl ) ) {
443 $versionParts = [];
444 preg_match( "/^(\d+\.\d+)/", MW_VERSION, $versionParts );
445 $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
446 }
447
448 return '[' . $versionUrl . ' ' . MW_VERSION . ']';
449 }
450
456 private static function getVersionLinkedGit() {
457 global $IP, $wgLang;
458
459 $gitInfo = new GitInfo( $IP );
460 $headSHA1 = $gitInfo->getHeadSHA1();
461 if ( !$headSHA1 ) {
462 return false;
463 }
464
465 $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
466
467 $gitHeadUrl = $gitInfo->getHeadViewUrl();
468 if ( $gitHeadUrl !== false ) {
469 $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
470 }
471
472 $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
473 if ( $gitHeadCommitDate ) {
474 $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( (string)$gitHeadCommitDate, true );
475 }
476
477 return self::getMWVersionLinked() . " $shortSHA1";
478 }
479
489 public static function getExtensionTypes(): array {
490 if ( self::$extensionTypes === false ) {
491 self::$extensionTypes = [
492 'specialpage' => wfMessage( 'version-specialpages' )->text(),
493 'editor' => wfMessage( 'version-editors' )->text(),
494 'parserhook' => wfMessage( 'version-parserhooks' )->text(),
495 'variable' => wfMessage( 'version-variables' )->text(),
496 'media' => wfMessage( 'version-mediahandlers' )->text(),
497 'antispam' => wfMessage( 'version-antispam' )->text(),
498 'skin' => wfMessage( 'version-skins' )->text(),
499 'api' => wfMessage( 'version-api' )->text(),
500 'other' => wfMessage( 'version-other' )->text(),
501 ];
502
503 Hooks::runner()->onExtensionTypes( self::$extensionTypes );
504 }
505
506 return self::$extensionTypes;
507 }
508
518 public static function getExtensionTypeName( $type ) {
519 $types = self::getExtensionTypes();
520
521 return $types[$type] ?? $types['other'];
522 }
523
530 private function getExtensionCredits( array $credits ) {
531 if (
532 !$credits ||
533 // Skins are displayed separately, see getSkinCredits()
534 ( count( $credits ) === 1 && isset( $credits['skin'] ) )
535 ) {
536 return '';
537 }
538
539 $extensionTypes = self::getExtensionTypes();
540
541 $this->addTocSection( 'version-extensions', 'mw-version-ext' );
542
543 $out = Xml::element(
544 'h2',
545 [ 'id' => 'mw-version-ext' ],
546 $this->msg( 'version-extensions' )->text()
547 ) .
548 Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-ext' ] );
549
550 // Make sure the 'other' type is set to an array.
551 if ( !array_key_exists( 'other', $credits ) ) {
552 $credits['other'] = [];
553 }
554
555 // Find all extensions that do not have a valid type and give them the type 'other'.
556 foreach ( $credits as $type => $extensions ) {
557 if ( !array_key_exists( $type, $extensionTypes ) ) {
558 $credits['other'] = array_merge( $credits['other'], $extensions );
559 }
560 }
561
562 $this->firstExtOpened = false;
563 // Loop through the extension categories to display their extensions in the list.
564 foreach ( $extensionTypes as $type => $text ) {
565 // Skins have a separate section
566 if ( $type !== 'other' && $type !== 'skin' ) {
567 $out .= $this->getExtensionCategory( $type, $text, $credits[$type] ?? [] );
568 }
569 }
570
571 // We want the 'other' type to be last in the list.
572 $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'], $credits['other'] );
573
574 $out .= Xml::closeElement( 'table' );
575
576 return $out;
577 }
578
585 private function getSkinCredits( array $credits ) {
586 if ( !isset( $credits['skin'] ) || !$credits['skin'] ) {
587 return '';
588 }
589
590 $this->addTocSection( 'version-skins', 'mw-version-skin' );
591
592 $out = Html::element(
593 'h2',
594 [ 'id' => 'mw-version-skin' ],
595 $this->msg( 'version-skins' )->text()
596 ) .
597 Html::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-skin' ] );
598
599 $this->firstExtOpened = false;
600 $out .= $this->getExtensionCategory( 'skin', null, $credits['skin'] );
601
602 $out .= Html::closeElement( 'table' );
603
604 return $out;
605 }
606
613 protected function getExternalLibraries( array $credits ) {
614 global $IP;
615 $paths = [
616 "$IP/vendor/composer/installed.json"
617 ];
618
619 $extensionTypes = self::getExtensionTypes();
620 foreach ( $extensionTypes as $type => $message ) {
621 if ( !isset( $credits[$type] ) || $credits[$type] === [] ) {
622 continue;
623 }
624 foreach ( $credits[$type] as $extension ) {
625 if ( !isset( $extension['path'] ) ) {
626 continue;
627 }
628 $paths[] = dirname( $extension['path'] ) . '/vendor/composer/installed.json';
629 }
630 }
631
632 $dependencies = [];
633
634 foreach ( $paths as $path ) {
635 if ( !file_exists( $path ) ) {
636 continue;
637 }
638
639 $installed = new ComposerInstalled( $path );
640
641 $dependencies += $installed->getInstalledDependencies();
642 }
643
644 if ( $dependencies === [] ) {
645 return '';
646 }
647
648 ksort( $dependencies );
649
650 $this->addTocSection( 'version-libraries', 'mw-version-libraries' );
651
652 $out = Html::element(
653 'h2',
654 [ 'id' => 'mw-version-libraries' ],
655 $this->msg( 'version-libraries' )->text()
656 );
657 $out .= Html::openElement(
658 'table',
659 [ 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries' ]
660 );
661 $out .= Html::openElement( 'tr' )
662 . Html::element( 'th', [], $this->msg( 'version-libraries-library' )->text() )
663 . Html::element( 'th', [], $this->msg( 'version-libraries-version' )->text() )
664 . Html::element( 'th', [], $this->msg( 'version-libraries-license' )->text() )
665 . Html::element( 'th', [], $this->msg( 'version-libraries-description' )->text() )
666 . Html::element( 'th', [], $this->msg( 'version-libraries-authors' )->text() )
667 . Html::closeElement( 'tr' );
668
669 foreach ( $dependencies as $name => $info ) {
670 if ( !is_array( $info ) || str_starts_with( $info['type'], 'mediawiki-' ) ) {
671 // Skip any extensions or skins since they'll be listed
672 // in their proper section
673 continue;
674 }
675 $authors = array_map( static function ( $arr ) {
676 // If a homepage is set, link to it
677 if ( isset( $arr['homepage'] ) ) {
678 return "[{$arr['homepage']} {$arr['name']}]";
679 }
680 return $arr['name'];
681 }, $info['authors'] );
682 $authors = $this->listAuthors( $authors, false, "$IP/vendor/$name" );
683
684 // We can safely assume that the libraries' names and descriptions
685 // are written in English and aren't going to be translated,
686 // so set appropriate lang and dir attributes
687 $out .= Html::openElement( 'tr', [
688 // Add an anchor so docs can link easily to the version of
689 // this specific library
690 'id' => Sanitizer::escapeIdForAttribute(
691 "mw-version-library-$name"
692 ) ] )
693 . Html::rawElement(
694 'td',
695 [],
696 Linker::makeExternalLink(
697 "https://packagist.org/packages/$name", $name,
698 true, '',
699 [ 'class' => 'mw-version-library-name' ]
700 )
701 )
702 . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
703 . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['licenses'] ) )
704 . Html::element( 'td', [ 'lang' => 'en', 'dir' => 'ltr' ], $info['description'] )
705 . Html::rawElement( 'td', [], $authors )
706 . Html::closeElement( 'tr' );
707 }
708 $out .= Html::closeElement( 'table' );
709
710 return $out;
711 }
712
718 private function getClientSideLibraries() {
719 global $IP;
720 $registryFile = "{$IP}/resources/lib/foreign-resources.yaml";
721 $modules = Yaml::parseFile( $registryFile );
722 ksort( $modules );
723
724 $this->addTocSection( 'version-libraries-client', 'mw-version-libraries-client' );
725
726 $out = Html::element(
727 'h2',
728 [ 'id' => 'mw-version-libraries-client' ],
729 $this->msg( 'version-libraries-client' )->text()
730 );
731 $out .= Html::openElement(
732 'table',
733 [ 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries-client' ]
734 );
735 $out .= Html::openElement( 'tr' )
736 . Html::element( 'th', [], $this->msg( 'version-libraries-library' )->text() )
737 . Html::element( 'th', [], $this->msg( 'version-libraries-version' )->text() )
738 . Html::element( 'th', [], $this->msg( 'version-libraries-license' )->text() )
739 . Html::closeElement( 'tr' );
740
741 foreach ( $modules as $name => $info ) {
742 // We can safely assume that the libraries' names and descriptions
743 // are written in English and aren't going to be translated,
744 // so set appropriate lang and dir attributes
745 $out .= Html::openElement( 'tr', [
746 // Add an anchor so docs can link easily to the version of
747 // this specific library
748 'id' => Sanitizer::escapeIdForAttribute(
749 "mw-version-library-$name"
750 ) ] )
751 . Html::rawElement(
752 'td',
753 [],
754 Linker::makeExternalLink(
755 $info['homepage'], $name,
756 true, '',
757 [ 'class' => 'mw-version-library-name' ]
758 )
759 )
760 . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
761 . Html::element( 'td', [ 'dir' => 'auto' ], $info['license'] )
762 . Html::closeElement( 'tr' );
763 }
764 $out .= Html::closeElement( 'table' );
765
766 return $out;
767 }
768
774 protected function getParserTags() {
775 $tags = $this->parser->getTags();
776 if ( !$tags ) {
777 return '';
778 }
779
780 $this->addTocSection( 'version-parser-extensiontags', 'mw-version-parser-extensiontags' );
781
782 $out = Html::rawElement(
783 'h2',
784 [ 'id' => 'mw-version-parser-extensiontags' ],
785 Html::rawElement(
786 'span',
787 [ 'class' => 'plainlinks' ],
788 Linker::makeExternalLink(
789 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
790 $this->msg( 'version-parser-extensiontags' )->text()
791 )
792 )
793 );
794
795 array_walk( $tags, static function ( &$value ) {
796 // Bidirectional isolation improves readability in RTL wikis
797 $value = Html::element(
798 'bdi',
799 // Prevent < and > from slipping to another line
800 [
801 'style' => 'white-space: nowrap;',
802 ],
803 "<$value>"
804 );
805 } );
806
807 $out .= $this->listToText( $tags );
808
809 return $out;
810 }
811
817 protected function getParserFunctionHooks() {
818 $funcHooks = $this->parser->getFunctionHooks();
819 if ( !$funcHooks ) {
820 return '';
821 }
822
823 $this->addTocSection( 'version-parser-function-hooks', 'mw-version-parser-function-hooks' );
824
825 $out = Html::rawElement(
826 'h2',
827 [ 'id' => 'mw-version-parser-function-hooks' ],
828 Html::rawElement(
829 'span',
830 [ 'class' => 'plainlinks' ],
831 Linker::makeExternalLink(
832 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
833 $this->msg( 'version-parser-function-hooks' )->text()
834 )
835 )
836 );
837
838 $out .= $this->listToText( $funcHooks );
839
840 return $out;
841 }
842
852 protected function getExtensionCategory( $type, ?string $text, array $creditsGroup ) {
853 $out = '';
854
855 if ( $creditsGroup ) {
856 $out .= $this->openExtType( $text, 'credits-' . $type );
857
858 usort( $creditsGroup, [ $this, 'compare' ] );
859
860 foreach ( $creditsGroup as $extension ) {
861 $out .= $this->getCreditsForExtension( $type, $extension );
862 }
863 }
864
865 return $out;
866 }
867
874 public function compare( $a, $b ) {
875 return $this->getLanguage()->lc( $a['name'] ) <=> $this->getLanguage()->lc( $b['name'] );
876 }
877
896 public function getCreditsForExtension( $type, array $extension ) {
897 $out = $this->getOutput();
898
899 // We must obtain the information for all the bits and pieces!
900 // ... such as extension names and links
901 if ( isset( $extension['namemsg'] ) ) {
902 // Localized name of extension
903 $extensionName = $this->msg( $extension['namemsg'] )->text();
904 } elseif ( isset( $extension['name'] ) ) {
905 // Non localized version
906 $extensionName = $extension['name'];
907 } else {
908 $extensionName = $this->msg( 'version-no-ext-name' )->text();
909 }
910
911 if ( isset( $extension['url'] ) ) {
912 $extensionNameLink = Linker::makeExternalLink(
913 $extension['url'],
914 $extensionName,
915 true,
916 '',
917 [ 'class' => 'mw-version-ext-name' ]
918 );
919 } else {
920 $extensionNameLink = htmlspecialchars( $extensionName );
921 }
922
923 // ... and the version information
924 // If the extension path is set we will check that directory for GIT
925 // metadata in an attempt to extract date and vcs commit metadata.
926 $canonicalVersion = '&ndash;';
927 $extensionPath = null;
928 $vcsVersion = null;
929 $vcsLink = null;
930 $vcsDate = null;
931
932 if ( isset( $extension['version'] ) ) {
933 $canonicalVersion = $out->parseInlineAsInterface( $extension['version'] );
934 }
935
936 if ( isset( $extension['path'] ) ) {
937 global $IP;
938 $extensionPath = dirname( $extension['path'] );
939 if ( $this->coreId == '' ) {
940 wfDebug( 'Looking up core head id' );
941 $coreHeadSHA1 = self::getGitHeadSha1( $IP );
942 if ( $coreHeadSHA1 ) {
943 $this->coreId = $coreHeadSHA1;
944 }
945 }
946 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
947 $memcKey = $cache->makeKey(
948 'specialversion-ext-version-text', $extension['path'], $this->coreId
949 );
950 [ $vcsVersion, $vcsLink, $vcsDate ] = $cache->get( $memcKey );
951
952 if ( !$vcsVersion ) {
953 wfDebug( "Getting VCS info for extension {$extension['name']}" );
954 $gitInfo = new GitInfo( $extensionPath );
955 $vcsVersion = $gitInfo->getHeadSHA1();
956 if ( $vcsVersion !== false ) {
957 $vcsVersion = substr( $vcsVersion, 0, 7 );
958 $vcsLink = $gitInfo->getHeadViewUrl();
959 $vcsDate = $gitInfo->getHeadCommitDate();
960 }
961 $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
962 } else {
963 wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" );
964 }
965 }
966
967 $versionString = Html::rawElement(
968 'span',
969 [ 'class' => 'mw-version-ext-version' ],
970 $canonicalVersion
971 );
972
973 if ( $vcsVersion ) {
974 if ( $vcsLink ) {
975 $vcsVerString = Linker::makeExternalLink(
976 $vcsLink,
977 $this->msg( 'version-version', $vcsVersion )->text(),
978 true,
979 '',
980 [ 'class' => 'mw-version-ext-vcs-version' ]
981 );
982 } else {
983 $vcsVerString = Html::element( 'span',
984 [ 'class' => 'mw-version-ext-vcs-version' ],
985 "({$vcsVersion})"
986 );
987 }
988 $versionString .= " {$vcsVerString}";
989
990 if ( $vcsDate ) {
991 $versionString .= ' ' . Html::element( 'span', [
992 'class' => 'mw-version-ext-vcs-timestamp',
993 'dir' => $this->getLanguage()->getDir(),
994 ], $this->getLanguage()->timeanddate( $vcsDate, true ) );
995 }
996 $versionString = Html::rawElement( 'span',
997 [ 'class' => 'mw-version-ext-meta-version' ],
998 $versionString
999 );
1000 }
1001
1002 // ... and license information; if a license file exists we
1003 // will link to it
1004 $licenseLink = '';
1005 if ( isset( $extension['name'] ) ) {
1006 $licenseName = null;
1007 if ( isset( $extension['license-name'] ) ) {
1008 $licenseName = new HtmlArmor( $out->parseInlineAsInterface( $extension['license-name'] ) );
1009 } elseif ( $extensionPath !== null && ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
1010 $licenseName = $this->msg( 'version-ext-license' )->text();
1011 }
1012 if ( $licenseName !== null ) {
1013 $licenseLink = $this->getLinkRenderer()->makeLink(
1014 $this->getPageTitle( 'License/' . $extension['name'] ),
1015 $licenseName,
1016 [
1017 'class' => 'mw-version-ext-license',
1018 'dir' => 'auto',
1019 ]
1020 );
1021 }
1022 }
1023
1024 // ... and generate the description; which can be a parameterized l10n message
1025 // in the form [ <msgname>, <parameter>, <parameter>... ] or just a straight
1026 // up string
1027 if ( isset( $extension['descriptionmsg'] ) ) {
1028 // Localized description of extension
1029 $descriptionMsg = $extension['descriptionmsg'];
1030
1031 if ( is_array( $descriptionMsg ) ) {
1032 $descriptionMsgKey = array_shift( $descriptionMsg );
1033 $descriptionMsg = array_map( 'htmlspecialchars', $descriptionMsg );
1034 $description = $this->msg( $descriptionMsgKey, ...$descriptionMsg )->text();
1035 } else {
1036 $description = $this->msg( $descriptionMsg )->text();
1037 }
1038 } elseif ( isset( $extension['description'] ) ) {
1039 // Non localized version
1040 $description = $extension['description'];
1041 } else {
1042 $description = '';
1043 }
1044 $description = $out->parseInlineAsInterface( $description );
1045
1046 // ... now get the authors for this extension
1047 $authors = $extension['author'] ?? [];
1048 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable path is set when there is a name
1049 $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
1050
1051 // Finally! Create the table
1052 $html = Html::openElement( 'tr', [
1053 'class' => 'mw-version-ext',
1054 'id' => Sanitizer::escapeIdForAttribute( 'mw-version-ext-' . $type . '-' . $extension['name'] )
1055 ]
1056 );
1057
1058 $html .= Html::rawElement( 'td', [], $extensionNameLink );
1059 $html .= Html::rawElement( 'td', [], $versionString );
1060 $html .= Html::rawElement( 'td', [], $licenseLink );
1061 $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-description' ], $description );
1062 $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-authors' ], $authors );
1063
1064 $html .= Html::closeElement( 'tr' );
1065
1066 return $html;
1067 }
1068
1074 private function getHooks() {
1075 if ( $this->getConfig()->get( MainConfigNames::SpecialVersionShowHooks ) ) {
1076 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1077 $hookNames = $hookContainer->getHookNames();
1078 sort( $hookNames );
1079
1080 $ret = [];
1081 $ret[] = '== {{int:version-hooks}} ==';
1082 $ret[] = Html::openElement( 'table', [ 'class' => 'wikitable', 'id' => 'sv-hooks' ] );
1083 $ret[] = Html::openElement( 'tr' );
1084 $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-name' )->text() );
1085 $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-subscribedby' )->text() );
1086 $ret[] = Html::closeElement( 'tr' );
1087
1088 foreach ( $hookNames as $hook ) {
1089 $hooks = $hookContainer->getLegacyHandlers( $hook );
1090 if ( !$hooks ) {
1091 continue;
1092 }
1093 $ret[] = Html::openElement( 'tr' );
1094 $ret[] = Html::element( 'td', [], $hook );
1095 $ret[] = Html::element( 'td', [], $this->listToText( $hooks ) );
1096 $ret[] = Html::closeElement( 'tr' );
1097 }
1098
1099 $ret[] = Html::closeElement( 'table' );
1100
1101 return implode( "\n", $ret );
1102 }
1103
1104 return '';
1105 }
1106
1107 private function openExtType( string $text = null, string $name = null ) {
1108 $out = '';
1109
1110 $opt = [ 'colspan' => 5 ];
1111 if ( $this->firstExtOpened ) {
1112 // Insert a spacing line
1113 $out .= Html::rawElement( 'tr', [ 'class' => 'sv-space' ],
1114 Html::element( 'td', $opt )
1115 );
1116 }
1117 $this->firstExtOpened = true;
1118
1119 if ( $name ) {
1120 $opt['id'] = "sv-$name";
1121 }
1122
1123 if ( $text !== null ) {
1124 $out .= Html::rawElement( 'tr', [],
1125 Html::element( 'th', $opt, $text )
1126 );
1127 }
1128
1129 $firstHeadingMsg = ( $name === 'credits-skin' )
1130 ? 'version-skin-colheader-name'
1131 : 'version-ext-colheader-name';
1132 $out .= Html::openElement( 'tr' );
1133 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1134 $this->msg( $firstHeadingMsg )->text() );
1135 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1136 $this->msg( 'version-ext-colheader-version' )->text() );
1137 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1138 $this->msg( 'version-ext-colheader-license' )->text() );
1139 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1140 $this->msg( 'version-ext-colheader-description' )->text() );
1141 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1142 $this->msg( 'version-ext-colheader-credits' )->text() );
1143 $out .= Html::closeElement( 'tr' );
1144
1145 return $out;
1146 }
1147
1153 private function IPInfo() {
1154 $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
1155
1156 return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
1157 }
1158
1179 public function listAuthors( $authors, $extName, $extDir ): string {
1180 $hasOthers = false;
1181 $linkRenderer = $this->getLinkRenderer();
1182
1183 $list = [];
1184 $authors = (array)$authors;
1185
1186 // Special case: if the authors array has only one item and it is "...",
1187 // it should not be rendered as the "version-poweredby-others" i18n msg,
1188 // but rather as "version-poweredby-various" i18n msg instead.
1189 if ( count( $authors ) === 1 && $authors[0] === '...' ) {
1190 // Link to the extension's or skin's AUTHORS or CREDITS file, if there is
1191 // such a file; otherwise just return the i18n msg as-is
1192 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1193 return $linkRenderer->makeLink(
1194 $this->getPageTitle( "Credits/$extName" ),
1195 $this->msg( 'version-poweredby-various' )->text()
1196 );
1197 } else {
1198 return $this->msg( 'version-poweredby-various' )->escaped();
1199 }
1200 }
1201
1202 // Otherwise, if we have an actual array that has more than one item,
1203 // process each array item as usual
1204 foreach ( $authors as $item ) {
1205 if ( $item == '...' ) {
1206 $hasOthers = true;
1207
1208 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1209 $text = $linkRenderer->makeLink(
1210 $this->getPageTitle( "Credits/$extName" ),
1211 $this->msg( 'version-poweredby-others' )->text()
1212 );
1213 } else {
1214 $text = $this->msg( 'version-poweredby-others' )->escaped();
1215 }
1216 $list[] = $text;
1217 } elseif ( str_ends_with( $item, ' ...]' ) ) {
1218 $hasOthers = true;
1219 $list[] = $this->getOutput()->parseInlineAsInterface(
1220 substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
1221 );
1222 } else {
1223 $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1224 }
1225 }
1226
1227 if ( $extName && !$hasOthers && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1228 $list[] = $linkRenderer->makeLink(
1229 $this->getPageTitle( "Credits/$extName" ),
1230 $this->msg( 'version-poweredby-others' )->text()
1231 );
1232 }
1233
1234 return $this->listToText( $list, false );
1235 }
1236
1244 private function listToText( array $list, bool $sort = true ): string {
1245 if ( !$list ) {
1246 return '';
1247 }
1248 if ( $sort ) {
1249 sort( $list );
1250 }
1251
1252 return $this->getLanguage()
1253 ->listToText( array_map( [ __CLASS__, 'arrayToString' ], $list ) );
1254 }
1255
1264 public static function arrayToString( $list ) {
1265 if ( is_array( $list ) && count( $list ) == 1 ) {
1266 $list = $list[0];
1267 }
1268 if ( $list instanceof Closure ) {
1269 // Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$"
1270 return 'Closure';
1271 } elseif ( is_object( $list ) ) {
1272 return wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1273 } elseif ( !is_array( $list ) ) {
1274 return $list;
1275 } else {
1276 if ( is_object( $list[0] ) ) {
1277 $class = get_class( $list[0] );
1278 } else {
1279 $class = $list[0];
1280 }
1281
1282 return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1283 }
1284 }
1285
1290 public static function getGitHeadSha1( $dir ) {
1291 return ( new GitInfo( $dir ) )->getHeadSHA1();
1292 }
1293
1298 public static function getGitCurrentBranch( $dir ) {
1299 return ( new GitInfo( $dir ) )->getCurrentBranch();
1300 }
1301
1306 public function getEntryPointInfo() {
1307 $config = $this->getConfig();
1308 $scriptPath = $config->get( MainConfigNames::ScriptPath ) ?: '/';
1309
1310 $entryPoints = [
1311 'version-entrypoints-articlepath' => $config->get( MainConfigNames::ArticlePath ),
1312 'version-entrypoints-scriptpath' => $scriptPath,
1313 'version-entrypoints-index-php' => wfScript( 'index' ),
1314 'version-entrypoints-api-php' => wfScript( 'api' ),
1315 'version-entrypoints-rest-php' => wfScript( 'rest' ),
1316 ];
1317
1318 $language = $this->getLanguage();
1319 $thAttributes = [
1320 'dir' => $language->getDir(),
1321 'lang' => $language->getHtmlCode()
1322 ];
1323
1324 $this->addTocSection( 'version-entrypoints', 'mw-version-entrypoints' );
1325
1326 $out = Html::element(
1327 'h2',
1328 [ 'id' => 'mw-version-entrypoints' ],
1329 $this->msg( 'version-entrypoints' )->text()
1330 ) .
1331 Html::openElement( 'table',
1332 [
1333 'class' => 'wikitable plainlinks',
1334 'id' => 'mw-version-entrypoints-table',
1335 'dir' => 'ltr',
1336 'lang' => 'en'
1337 ]
1338 ) .
1339 Html::openElement( 'tr' ) .
1340 Html::element(
1341 'th',
1342 $thAttributes,
1343 $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1344 ) .
1345 Html::element(
1346 'th',
1347 $thAttributes,
1348 $this->msg( 'version-entrypoints-header-url' )->text()
1349 ) .
1350 Html::closeElement( 'tr' );
1351
1352 foreach ( $entryPoints as $message => $value ) {
1353 $url = $this->urlUtils->expand( $value, PROTO_RELATIVE );
1354 $out .= Html::openElement( 'tr' ) .
1355 // ->plain() looks like it should be ->parse(), but this function
1356 // returns wikitext, not HTML, boo
1357 Html::rawElement( 'td', [], $this->msg( $message )->plain() ) .
1358 Html::rawElement( 'td', [], Html::rawElement( 'code', [], "[$url $value]" ) ) .
1359 Html::closeElement( 'tr' );
1360 }
1361
1362 $out .= Html::closeElement( 'table' );
1363
1364 return $out;
1365 }
1366
1367 protected function getGroupName() {
1368 return 'wiki';
1369 }
1370}
const CACHE_ANYTHING
Definition Defines.php:85
const MW_VERSION
The running version of MediaWiki.
Definition Defines.php:36
const PROTO_RELATIVE
Definition Defines.php:195
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
if(!defined( 'MEDIAWIKI')) if(ini_get('mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Definition Setup.php:93
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode $wgLang
Definition Setup.php:527
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition WebStart.php:88
Reads an installed.json file and provides accessors to get what is installed.
The Registry loads JSON files, and uses a Processor to extract information from them.
getAllThings()
Get credits information about all installed extensions and skins.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
This class is a collection of static functions that serve two purposes:
Definition Html.php:55
Variant of the Message class.
Some internal bits split of from Skin.php.
Definition Linker.php:67
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
A service to expand, parse, and otherwise manipulate URLs.
Definition UrlUtils.php:17
static listParam(array $list, $type='text')
Definition Message.php:1278
setTOCData(TOCData $tocData)
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:107
Parent class for all special pages.
Give information about the version of MediaWiki, PHP, the DB and extensions.
static getVersionLinked()
Return a wikitext-formatted string of the MediaWiki version with a link to the Git SHA1 of head if av...
static getExtensionTypeName( $type)
Returns the internationalized name for an extension type.
static getGitHeadSha1( $dir)
getParserFunctionHooks()
Obtains a list of installed parser function hooks and the associated H2 header.
execute( $par)
main()
getExternalLibraries(array $credits)
Generate an HTML table for external libraries that are installed.
getEntryPointInfo()
Get the list of entry points and their URLs.
static getCredits(ExtensionRegistry $reg, Config $conf)
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
listAuthors( $authors, $extName, $extDir)
Return a formatted unsorted list of authors.
string $coreId
The current rev id/SHA hash of MediaWiki core.
static getExtensionTypes()
Returns an array with the base extension types.
__construct(Parser $parser, UrlUtils $urlUtils)
getCreditsForExtension( $type, array $extension)
Creates and formats a version line for a single extension.
getParserTags()
Obtains a list of installed parser tags and the associated H2 header.
compare( $a, $b)
Callback to sort extensions by type.
static getVersion( $flags='', $lang=null)
Return a string of the MediaWiki version with Git revision if available.
static getCopyrightAndAuthorList()
Get the "MediaWiki is copyright 2001-20xx by lots of cool folks" text.
static getGitCurrentBranch( $dir)
static string[] false $extensionTypes
Lazy initialized key/value with message content.
static arrayToString( $list)
Convert an array or object to a string for display.
getExtensionCategory( $type, ?string $text, array $creditsGroup)
Creates and returns the HTML for a single extension category.
static closeElement( $element)
Shortcut to close an XML element.
Definition Xml.php:122
static openElement( $element, $attribs=null)
This opens an XML element.
Definition Xml.php:113
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition Xml.php:44
Interface for configuration instances.
Definition Config.php:30
const DB_REPLICA
Definition defines.php:26
$content
Definition router.php:76
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42
if(!is_readable( $file)) $ext
Definition router.php:48
if(!isset( $args[0])) $lang