MediaWiki master
SpecialVersion.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Specials;
24
25use Closure;
26use HtmlArmor;
43use Symfony\Component\Yaml\Yaml;
45use Wikimedia\Parsoid\Core\SectionMetadata;
46use Wikimedia\Parsoid\Core\TOCData;
48
55
59 protected $coreId = '';
60
64 protected static $extensionTypes = false;
65
67 protected $tocData;
68
70 protected $tocIndex;
71
73 protected $tocSection;
74
76 protected $tocSubSection;
77
78 private ParserFactory $parserFactory;
79 private UrlUtils $urlUtils;
80 private IConnectionProvider $dbProvider;
81
82 public function __construct(
83 ParserFactory $parserFactory,
84 UrlUtils $urlUtils,
85 IConnectionProvider $dbProvider
86 ) {
87 parent::__construct( 'Version' );
88 $this->parserFactory = $parserFactory;
89 $this->urlUtils = $urlUtils;
90 $this->dbProvider = $dbProvider;
91 }
92
100 public static function getCredits( ExtensionRegistry $reg, Config $conf ): array {
101 $credits = $conf->get( MainConfigNames::ExtensionCredits );
102 foreach ( $reg->getAllThings() as $credit ) {
103 $credits[$credit['type']][] = $credit;
104 }
105 return $credits;
106 }
107
111 public function execute( $par ) {
112 $config = $this->getConfig();
113 $credits = self::getCredits( ExtensionRegistry::getInstance(), $config );
114
115 $this->setHeaders();
116 $this->outputHeader();
117 $out = $this->getOutput();
118 $out->getMetadata()->setPreventClickjacking( false );
119
120 // Explode the subpage information into useful bits
121 $parts = explode( '/', (string)$par );
122 $extNode = null;
123 if ( isset( $parts[1] ) ) {
124 $extName = str_replace( '_', ' ', $parts[1] );
125 // Find it!
126 foreach ( $credits as $extensions ) {
127 foreach ( $extensions as $ext ) {
128 if ( isset( $ext['name'] ) && ( $ext['name'] === $extName ) ) {
129 $extNode = &$ext;
130 break 2;
131 }
132 }
133 }
134 if ( !$extNode ) {
135 $out->setStatusCode( 404 );
136 }
137 } else {
138 $extName = 'MediaWiki';
139 }
140
141 // Now figure out what to do
142 switch ( strtolower( $parts[0] ) ) {
143 case 'credits':
144 $out->addModuleStyles( 'mediawiki.special' );
145
146 $wikiText = '{{int:version-credits-not-found}}';
147 if ( $extName === 'MediaWiki' ) {
148 $wikiText = file_get_contents( MW_INSTALL_PATH . '/CREDITS' );
149 // Put the contributor list into columns
150 $wikiText = str_replace(
151 [ '<!-- BEGIN CONTRIBUTOR LIST -->', '<!-- END CONTRIBUTOR LIST -->' ],
152 [ '<div class="mw-version-credits">', '</div>' ],
153 $wikiText
154 );
155 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
156 $file = ExtensionInfo::getAuthorsFileName( dirname( $extNode['path'] ) );
157 if ( $file ) {
158 $wikiText = file_get_contents( $file );
159 if ( str_ends_with( $file, '.txt' ) ) {
160 $wikiText = Html::element(
161 'pre',
162 [
163 'lang' => 'en',
164 'dir' => 'ltr',
165 ],
166 $wikiText
167 );
168 }
169 }
170 }
171
172 $out->setPageTitleMsg( $this->msg( 'version-credits-title' )->plaintextParams( $extName ) );
173 $out->addWikiTextAsInterface( $wikiText );
174 break;
175
176 case 'license':
177 $out->setPageTitleMsg( $this->msg( 'version-license-title' )->plaintextParams( $extName ) );
178
179 $licenseFound = false;
180
181 if ( $extName === 'MediaWiki' ) {
182 $out->addWikiTextAsInterface(
183 file_get_contents( MW_INSTALL_PATH . '/COPYING' )
184 );
185 $licenseFound = true;
186 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
187 $files = ExtensionInfo::getLicenseFileNames( dirname( $extNode['path'] ) );
188 if ( $files ) {
189 $licenseFound = true;
190 foreach ( $files as $file ) {
191 $out->addWikiTextAsInterface(
193 'pre',
194 [
195 'lang' => 'en',
196 'dir' => 'ltr',
197 ],
198 file_get_contents( $file )
199 )
200 );
201 }
202 }
203 }
204 if ( !$licenseFound ) {
205 $out->addWikiTextAsInterface( '{{int:version-license-not-found}}' );
206 }
207 break;
208
209 default:
210 $out->addModuleStyles( 'mediawiki.special' );
211
212 $out->addHTML( $this->getMediaWikiCredits() );
213
214 $this->tocData = new TOCData();
215 $this->tocIndex = 0;
216 $this->tocSection = 0;
217 $this->tocSubSection = 0;
218
219 // Build the page contents (this also fills in TOCData)
220 $sections = [
221 $this->softwareInformation(),
222 $this->getEntryPointInfo(),
223 $this->getSkinCredits( $credits ),
224 $this->getExtensionCredits( $credits ),
225 $this->getLibraries( $credits ),
226 $this->getParserTags(),
227 $this->getParserFunctionHooks(),
228 $this->getParsoidModules(),
229 $this->getHooks(),
230 $this->IPInfo(),
231 ];
232
233 // Insert TOC first
234 $out->addTOCPlaceholder( $this->tocData );
235
236 // Insert contents
237 foreach ( $sections as $content ) {
238 $out->addHTML( $content );
239 }
240
241 break;
242 }
243 }
244
252 private function addTocSection( $labelMsg, $id ) {
253 $this->tocIndex++;
254 $this->tocSection++;
255 $this->tocSubSection = 0;
256 $this->tocData->addSection( new SectionMetadata(
257 1,
258 2,
259 $this->msg( $labelMsg )->escaped(),
260 $this->getLanguage()->formatNum( $this->tocSection ),
261 (string)$this->tocIndex,
262 null,
263 null,
264 $id,
265 $id
266 ) );
267 }
268
276 private function addTocSubSection( $label, $id ) {
277 $this->tocIndex++;
278 $this->tocSubSection++;
279 $this->tocData->addSection( new SectionMetadata(
280 2,
281 3,
282 htmlspecialchars( $label ),
283 // See Parser::localizeTOC
284 $this->getLanguage()->formatNum( $this->tocSection ) . '.' .
285 $this->getLanguage()->formatNum( $this->tocSubSection ),
286 (string)$this->tocIndex,
287 null,
288 null,
289 $id,
290 $id
291 ) );
292 }
293
299 private function getMediaWikiCredits() {
300 // No TOC entry for this heading, we treat it like the lede section
301
302 $ret = Html::element(
303 'h2',
304 [ 'id' => 'mw-version-license' ],
305 $this->msg( 'version-license' )->text()
306 );
307
308 $ret .= Html::rawElement( 'div', [ 'class' => 'plainlinks' ],
309 $this->msg( new RawMessage( self::getCopyrightAndAuthorList() ) )->parseAsBlock() .
310 Html::rawElement( 'div', [ 'class' => 'mw-version-license-info' ],
311 $this->msg( 'version-license-info' )->parseAsBlock()
312 )
313 );
314
315 return $ret;
316 }
317
324 public static function getCopyrightAndAuthorList() {
325 if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
326 $othersLink = '[https://www.mediawiki.org/wiki/Special:Version/Credits ' .
327 wfMessage( 'version-poweredby-others' )->plain() . ']';
328 } else {
329 $othersLink = '[[Special:Version/Credits|' .
330 wfMessage( 'version-poweredby-others' )->plain() . ']]';
331 }
332
333 $translatorsLink = '[https://translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
334 wfMessage( 'version-poweredby-translators' )->plain() . ']';
335
336 $authorList = [
337 'Magnus Manske', 'Brooke Vibber', 'Lee Daniel Crocker',
338 'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
339 'Niklas Laxström', 'Domas Mituzas', 'Rob Church', 'Yuri Astrakhan',
340 'Aryeh Gregor', 'Aaron Schulz', 'Andrew Garrett', 'Raimond Spekking',
341 'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
342 'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
343 'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
344 'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', 'Brad Jorsch',
345 'Bartosz Dziewoński', 'Ed Sanders', 'Moriel Schottlender',
346 'Kunal Mehta', 'James D. Forrester', 'Brian Wolff', 'Adam Shorland',
347 'DannyS712', 'Ori Livneh', 'Max Semenik', 'Amir Sarabadani',
348 'Derk-Jan Hartman', 'Petr Pchelko', 'Umherirrender', 'C. Scott Ananian',
349 'fomafix', 'Thiemo Kreuz', 'Gergő Tisza', 'Volker E.',
350 'Jack Phoenix', 'Isarra Yos',
351 $othersLink, $translatorsLink
352 ];
353
354 return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
355 Message::listParam( $authorList ) )->plain();
356 }
357
363 private function getSoftwareInformation() {
364 $dbr = $this->dbProvider->getReplicaDatabase();
365
366 // Put the software in an array of form 'name' => 'version'. All messages should
367 // be loaded here, so feel free to use wfMessage in the 'name'. Wikitext
368 // can be used both in the name and value.
369 $software = [
370 '[https://www.mediawiki.org/ MediaWiki]' => self::getVersionLinked(),
371 '[https://php.net/ PHP]' => PHP_VERSION . " (" . PHP_SAPI . ")",
372 '[https://icu.unicode.org/ ICU]' => INTL_ICU_VERSION,
373 $dbr->getSoftwareLink() => $dbr->getServerInfo(),
374 ];
375
376 // T339915: If wikidiff2 is installed, show version
377 if ( phpversion( "wikidiff2" ) ) {
378 $software[ '[https://www.mediawiki.org/wiki/Wikidiff2 wikidiff2]' ] = phpversion( "wikidiff2" );
379 }
380
381 // Allow a hook to add/remove items.
382 $this->getHookRunner()->onSoftwareInfo( $software );
383
384 return $software;
385 }
386
392 private function softwareInformation() {
393 $this->addTocSection( 'version-software', 'mw-version-software' );
394
395 $out = Html::element(
396 'h2',
397 [ 'id' => 'mw-version-software' ],
398 $this->msg( 'version-software' )->text()
399 );
400
401 $out .= Html::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ] );
402
403 $out .= $this->getTableHeaderHtml( [
404 $this->msg( 'version-software-product' )->text(),
405 $this->msg( 'version-software-version' )->text()
406 ] );
407
408 foreach ( $this->getSoftwareInformation() as $name => $version ) {
409 $out .= Html::rawElement(
410 'tr',
411 [],
412 Html::rawElement( 'td', [], $this->msg( new RawMessage( $name ) )->parse() ) .
413 Html::rawElement( 'td', [ 'dir' => 'ltr' ], $this->msg( new RawMessage( $version ) )->parse() )
414 );
415 }
416
417 $out .= Html::closeElement( 'table' );
418
419 return $out;
420 }
421
431 public static function getVersion( $flags = '', $lang = null ) {
432 $gitInfo = GitInfo::repo()->getHeadSHA1();
433 if ( !$gitInfo ) {
434 $version = MW_VERSION;
435 } elseif ( $flags === 'nodb' ) {
436 $shortSha1 = substr( $gitInfo, 0, 7 );
437 $version = MW_VERSION . " ($shortSha1)";
438 } else {
439 $shortSha1 = substr( $gitInfo, 0, 7 );
440 $msg = wfMessage( 'parentheses' );
441 if ( $lang !== null ) {
442 $msg->inLanguage( $lang );
443 }
444 $shortSha1 = $msg->params( $shortSha1 )->text();
445 $version = MW_VERSION . ' ' . $shortSha1;
446 }
447
448 return $version;
449 }
450
458 public static function getVersionLinked() {
459 return self::getVersionLinkedGit() ?: MW_VERSION;
460 }
461
465 private static function getMWVersionLinked() {
466 $versionUrl = "";
467 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
468 if ( $hookRunner->onSpecialVersionVersionUrl( MW_VERSION, $versionUrl ) ) {
469 $versionParts = [];
470 preg_match( "/^(\d+\.\d+)/", MW_VERSION, $versionParts );
471 $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
472 }
473
474 return '[' . $versionUrl . ' ' . MW_VERSION . ']';
475 }
476
482 private static function getVersionLinkedGit() {
483 global $wgLang;
484
485 $gitInfo = new GitInfo( MW_INSTALL_PATH );
486 $headSHA1 = $gitInfo->getHeadSHA1();
487 if ( !$headSHA1 ) {
488 return false;
489 }
490
491 $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
492
493 $gitHeadUrl = $gitInfo->getHeadViewUrl();
494 if ( $gitHeadUrl !== false ) {
495 $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
496 }
497
498 $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
499 if ( $gitHeadCommitDate ) {
500 $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( (string)$gitHeadCommitDate, true );
501 }
502
503 return self::getMWVersionLinked() . " $shortSHA1";
504 }
505
515 public static function getExtensionTypes(): array {
516 if ( self::$extensionTypes === false ) {
517 self::$extensionTypes = [
518 'specialpage' => wfMessage( 'version-specialpages' )->text(),
519 'editor' => wfMessage( 'version-editors' )->text(),
520 'parserhook' => wfMessage( 'version-parserhooks' )->text(),
521 'variable' => wfMessage( 'version-variables' )->text(),
522 'media' => wfMessage( 'version-mediahandlers' )->text(),
523 'antispam' => wfMessage( 'version-antispam' )->text(),
524 'skin' => wfMessage( 'version-skins' )->text(),
525 'api' => wfMessage( 'version-api' )->text(),
526 'other' => wfMessage( 'version-other' )->text(),
527 ];
528
529 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
530 ->onExtensionTypes( self::$extensionTypes );
531 }
532
533 return self::$extensionTypes;
534 }
535
545 public static function getExtensionTypeName( $type ) {
546 $types = self::getExtensionTypes();
547
548 return $types[$type] ?? $types['other'];
549 }
550
557 private function getExtensionCredits( array $credits ) {
558 $extensionTypes = self::getExtensionTypes();
559
560 $this->addTocSection( 'version-extensions', 'mw-version-ext' );
561
562 $out = Html::element(
563 'h2',
564 [ 'id' => 'mw-version-ext' ],
565 $this->msg( 'version-extensions' )->text()
566 );
567
568 if (
569 !$credits ||
570 // Skins are displayed separately, see getSkinCredits()
571 ( count( $credits ) === 1 && isset( $credits['skin'] ) )
572 ) {
573 $out .= Html::element(
574 'p',
575 [],
576 $this->msg( 'version-extensions-no-ext' )->text()
577 );
578
579 return $out;
580 }
581
582 // Find all extensions that do not have a valid type and give them the type 'other'.
583 $credits['other'] ??= [];
584 foreach ( $credits as $type => $extensions ) {
585 if ( !array_key_exists( $type, $extensionTypes ) ) {
586 $credits['other'] = array_merge( $credits['other'], $extensions );
587 }
588 }
589
590 // Loop through the extension categories to display their extensions in the list.
591 foreach ( $extensionTypes as $type => $text ) {
592 // Skins have a separate section
593 if ( $type !== 'other' && $type !== 'skin' ) {
594 $out .= $this->getExtensionCategory( $type, $text, $credits[$type] ?? [] );
595 }
596 }
597
598 // We want the 'other' type to be last in the list.
599 $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'], $credits['other'] );
600
601 return $out;
602 }
603
610 private function getSkinCredits( array $credits ) {
611 $this->addTocSection( 'version-skins', 'mw-version-skin' );
612
613 $out = Html::element(
614 'h2',
615 [ 'id' => 'mw-version-skin' ],
616 $this->msg( 'version-skins' )->text()
617 );
618
619 if ( !isset( $credits['skin'] ) || !$credits['skin'] ) {
620 $out .= Html::element(
621 'p',
622 [],
623 $this->msg( 'version-skins-no-skin' )->text()
624 );
625
626 return $out;
627 }
628 $out .= $this->getExtensionCategory( 'skin', null, $credits['skin'] );
629
630 return $out;
631 }
632
639 protected function getLibraries( array $credits ) {
640 $this->addTocSection( 'version-libraries', 'mw-version-libraries' );
641
642 $out = Html::element(
643 'h2',
644 [ 'id' => 'mw-version-libraries' ],
645 $this->msg( 'version-libraries' )->text()
646 );
647
648 return $out
649 . $this->getExternalLibraries( $credits )
650 . $this->getClientSideLibraries();
651 }
652
659 public static function parseComposerInstalled( array $credits ) {
660 $paths = [
661 MW_INSTALL_PATH . '/vendor/composer/installed.json'
662 ];
663
664 $extensionTypes = self::getExtensionTypes();
665 foreach ( $extensionTypes as $type => $message ) {
666 if ( !isset( $credits[$type] ) || $credits[$type] === [] ) {
667 continue;
668 }
669 foreach ( $credits[$type] as $extension ) {
670 if ( !isset( $extension['path'] ) ) {
671 continue;
672 }
673 $paths[] = dirname( $extension['path'] ) . '/vendor/composer/installed.json';
674 }
675 }
676
677 $dependencies = [];
678
679 foreach ( $paths as $path ) {
680 if ( !file_exists( $path ) ) {
681 continue;
682 }
683
684 $installed = new ComposerInstalled( $path );
685
686 $dependencies += $installed->getInstalledDependencies();
687 }
688
689 ksort( $dependencies );
690 return $dependencies;
691 }
692
699 protected function getExternalLibraries( array $credits ) {
700 $dependencies = self::parseComposerInstalled( $credits );
701 if ( $dependencies === [] ) {
702 return '';
703 }
704
705 $this->addTocSubSection( $this->msg( 'version-libraries-server' )->text(), 'mw-version-libraries-server' );
706
707 $out = Html::element(
708 'h3',
709 [ 'id' => 'mw-version-libraries-server' ],
710 $this->msg( 'version-libraries-server' )->text()
711 );
712 $out .= Html::openElement(
713 'table',
714 [ 'class' => 'wikitable plainlinks mw-installed-software', 'id' => 'sv-libraries' ]
715 );
716
717 $out .= $this->getTableHeaderHtml( [
718 $this->msg( 'version-libraries-library' )->text(),
719 $this->msg( 'version-libraries-version' )->text(),
720 $this->msg( 'version-libraries-license' )->text(),
721 $this->msg( 'version-libraries-description' )->text(),
722 $this->msg( 'version-libraries-authors' )->text(),
723 ] );
724
725 foreach ( $dependencies as $name => $info ) {
726 if ( !is_array( $info ) || str_starts_with( $info['type'], 'mediawiki-' ) ) {
727 // Skip any extensions or skins since they'll be listed
728 // in their proper section
729 continue;
730 }
731 $authors = array_map( static function ( $arr ) {
732 return new HtmlArmor( isset( $arr['homepage'] ) ?
733 Html::element( 'a', [ 'href' => $arr['homepage'] ], $arr['name'] ) :
734 htmlspecialchars( $arr['name'] )
735 );
736 }, $info['authors'] );
737 $authors = $this->listAuthors( $authors, false, MW_INSTALL_PATH . "/vendor/$name" );
738
739 // We can safely assume that the libraries' names and descriptions
740 // are written in English and aren't going to be translated,
741 // so set appropriate lang and dir attributes
742 $out .= Html::openElement( 'tr', [
743 // Add an anchor so docs can link easily to the version of
744 // this specific library
745 'id' => Sanitizer::escapeIdForAttribute(
746 "mw-version-library-$name"
747 ) ] )
748 . Html::rawElement(
749 'td',
750 [],
751 $this->getLinkRenderer()->makeExternalLink(
752 "https://packagist.org/packages/$name",
753 $name,
754 $this->getFullTitle(),
755 '',
756 [ 'class' => 'mw-version-library-name' ]
757 )
758 )
759 . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
760 // @phan-suppress-next-line SecurityCheck-DoubleEscaped See FIXME in listToText
761 . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['licenses'] ) )
762 . Html::element( 'td', [ 'lang' => 'en', 'dir' => 'ltr' ], $info['description'] )
763 . Html::rawElement( 'td', [], $authors )
764 . Html::closeElement( 'tr' );
765 }
766 $out .= Html::closeElement( 'table' );
767
768 return $out;
769 }
770
776 public static function parseForeignResources() {
777 $registryDirs = [ 'MediaWiki' => MW_INSTALL_PATH . '/resources/lib' ]
778 + ExtensionRegistry::getInstance()->getAttribute( 'ForeignResourcesDir' );
779
780 $modules = [];
781 foreach ( $registryDirs as $source => $registryDir ) {
782 $foreignResources = Yaml::parseFile( "$registryDir/foreign-resources.yaml" );
783 foreach ( $foreignResources as $name => $module ) {
784 $key = $name . $module['version'];
785 if ( isset( $modules[$key] ) ) {
786 $modules[$key]['source'][] = $source;
787 continue;
788 }
789 $modules[$key] = $module + [ 'name' => $name, 'source' => [ $source ] ];
790 }
791 }
792 ksort( $modules );
793 return $modules;
794 }
795
801 private function getClientSideLibraries() {
802 $this->addTocSubSection( $this->msg( 'version-libraries-client' )->text(), 'mw-version-libraries-client' );
803
804 $out = Html::element(
805 'h3',
806 [ 'id' => 'mw-version-libraries-client' ],
807 $this->msg( 'version-libraries-client' )->text()
808 );
809 $out .= Html::openElement(
810 'table',
811 [ 'class' => 'wikitable plainlinks mw-installed-software', 'id' => 'sv-libraries-client' ]
812 );
813
814 $out .= $this->getTableHeaderHtml( [
815 $this->msg( 'version-libraries-library' )->text(),
816 $this->msg( 'version-libraries-version' )->text(),
817 $this->msg( 'version-libraries-license' )->text(),
818 $this->msg( 'version-libraries-authors' )->text(),
819 $this->msg( 'version-libraries-source' )->text()
820 ] );
821
822 foreach ( self::parseForeignResources() as $name => $info ) {
823 // We can safely assume that the libraries' names and descriptions
824 // are written in English and aren't going to be translated,
825 // so set appropriate lang and dir attributes
826 $out .= Html::openElement( 'tr', [
827 // Add an anchor so docs can link easily to the version of
828 // this specific library
829 'id' => Sanitizer::escapeIdForAttribute(
830 "mw-version-library-$name"
831 ) ] )
832 . Html::rawElement(
833 'td',
834 [],
835 $this->getLinkRenderer()->makeExternalLink(
836 $info['homepage'],
837 $info['name'],
838 $this->getFullTitle(),
839 '',
840 [ 'class' => 'mw-version-library-name' ]
841 )
842 )
843 . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
844 . Html::element( 'td', [ 'dir' => 'auto' ], $info['license'] )
845 . Html::element( 'td', [ 'dir' => 'auto' ], $info['authors'] ?? '—' )
846 // @phan-suppress-next-line SecurityCheck-DoubleEscaped See FIXME in listToText
847 . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['source'] ) )
848 . Html::closeElement( 'tr' );
849 }
850 $out .= Html::closeElement( 'table' );
851
852 return $out;
853 }
854
860 protected function getParserTags() {
861 $tags = $this->parserFactory->getMainInstance()->getTags();
862 if ( !$tags ) {
863 return '';
864 }
865
866 $this->addTocSection( 'version-parser-extensiontags', 'mw-version-parser-extensiontags' );
867
868 $out = Html::rawElement(
869 'h2',
870 [ 'id' => 'mw-version-parser-extensiontags' ],
871 Html::rawElement(
872 'span',
873 [ 'class' => 'plainlinks' ],
874 $this->getLinkRenderer()->makeExternalLink(
875 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
876 $this->msg( 'version-parser-extensiontags' ),
877 $this->getFullTitle()
878 )
879 )
880 );
881
882 array_walk( $tags, static function ( &$value ) {
883 // Bidirectional isolation improves readability in RTL wikis
884 $value = Html::rawElement(
885 'bdi',
886 // Prevent < and > from slipping to another line
887 [
888 'style' => 'white-space: nowrap;',
889 ],
890 Html::element( 'code', [], "<$value>" )
891 );
892 } );
893
894 $out .= $this->listToText( $tags );
895
896 return $out;
897 }
898
904 protected function getParserFunctionHooks() {
905 $funcHooks = $this->parserFactory->getMainInstance()->getFunctionHooks();
906 if ( !$funcHooks ) {
907 return '';
908 }
909
910 $this->addTocSection( 'version-parser-function-hooks', 'mw-version-parser-function-hooks' );
911
912 $out = Html::rawElement(
913 'h2',
914 [ 'id' => 'mw-version-parser-function-hooks' ],
915 Html::rawElement(
916 'span',
917 [ 'class' => 'plainlinks' ],
918 $this->getLinkRenderer()->makeExternalLink(
919 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
920 $this->msg( 'version-parser-function-hooks' ),
921 $this->getFullTitle()
922 )
923 )
924 );
925
926 $funcSynonyms = $this->parserFactory->getMainInstance()->getFunctionSynonyms();
927 // This will give us the preferred synonyms in the content language, as if
928 // we used MagicWord::getSynonym( 0 ), because they appear first in the arrays.
929 // We can't use MagicWord directly, because only Parser knows whether a function
930 // uses the leading "#" or not. Case-sensitive functions ("1") win over
931 // case-insensitive ones ("0"), like in Parser::callParserFunction().
932 // There should probably be a better API for this.
933 $preferredSynonyms = array_flip( array_reverse( $funcSynonyms[1] + $funcSynonyms[0] ) );
934 array_walk( $funcHooks, static function ( &$value ) use ( $preferredSynonyms ) {
935 $value = $preferredSynonyms[$value];
936 } );
937
938 // Sort case-insensitively, ignoring the leading '#' if present
939 usort( $funcHooks, static function ( $a, $b ) {
940 return strcasecmp( ltrim( $a, '#' ), ltrim( $b, '#' ) );
941 } );
942
943 array_walk( $funcHooks, static function ( &$value ) {
944 // Bidirectional isolation ensures it displays as {{#ns}} and not {{ns#}} in RTL wikis
945 $value = Html::rawElement(
946 'bdi',
947 [],
948 Html::element( 'code', [], '{{' . $value . '}}' )
949 );
950 } );
951
952 $out .= $this->getLanguage()->listToText( $funcHooks );
953
954 return $out;
955 }
956
962 protected function getParsoidModules() {
963 $siteConfig = MediaWikiServices::getInstance()->getParsoidSiteConfig();
964 $modules = $siteConfig->getExtensionModules();
965
966 if ( !$modules ) {
967 return '';
968 }
969
970 $this->addTocSection( 'version-parsoid-modules', 'mw-version-parsoid-modules' );
971
972 $out = Html::rawElement(
973 'h2',
974 [ 'id' => 'mw-version-parsoid-modules' ],
975 Html::rawElement(
976 'span',
977 [ 'class' => 'plainlinks' ],
978 $this->getLinkRenderer()->makeExternalLink(
979 'https://www.mediawiki.org/wiki/Special:MyLanguage/Parsoid',
980 $this->msg( 'version-parsoid-modules' ),
981 $this->getFullTitle()
982 )
983 )
984 );
985
986 $moduleNames = array_map(
987 static fn ( $m )=>Html::element( 'code', [], $m->getConfig()['name'] ),
988 $modules
989 );
990
991 $out .= $this->getLanguage()->listToText( $moduleNames );
992
993 return $out;
994 }
995
1005 protected function getExtensionCategory( $type, ?string $text, array $creditsGroup ) {
1006 $out = '';
1007
1008 if ( $creditsGroup ) {
1009 $out .= $this->openExtType( $text, 'credits-' . $type );
1010
1011 usort( $creditsGroup, [ $this, 'compare' ] );
1012
1013 foreach ( $creditsGroup as $extension ) {
1014 $out .= $this->getCreditsForExtension( $type, $extension );
1015 }
1016
1017 $out .= Html::closeElement( 'table' );
1018 }
1019
1020 return $out;
1021 }
1022
1029 public function compare( $a, $b ) {
1030 return $this->getLanguage()->lc( $a['name'] ) <=> $this->getLanguage()->lc( $b['name'] );
1031 }
1032
1051 public function getCreditsForExtension( $type, array $extension ) {
1052 $out = $this->getOutput();
1053
1054 // We must obtain the information for all the bits and pieces!
1055 // ... such as extension names and links
1056 if ( isset( $extension['namemsg'] ) ) {
1057 // Localized name of extension
1058 $extensionName = $this->msg( $extension['namemsg'] );
1059 } elseif ( isset( $extension['name'] ) ) {
1060 // Non localized version
1061 $extensionName = $extension['name'];
1062 } else {
1063 $extensionName = $this->msg( 'version-no-ext-name' );
1064 }
1065
1066 if ( isset( $extension['url'] ) ) {
1067 $extensionNameLink = $this->getLinkRenderer()->makeExternalLink(
1068 $extension['url'],
1069 $extensionName,
1070 $this->getFullTitle(),
1071 '',
1072 [ 'class' => 'mw-version-ext-name' ]
1073 );
1074 } else {
1075 $extensionNameLink = htmlspecialchars( $extensionName );
1076 }
1077
1078 // ... and the version information
1079 // If the extension path is set we will check that directory for GIT
1080 // metadata in an attempt to extract date and vcs commit metadata.
1081 $canonicalVersion = '&ndash;';
1082 $extensionPath = null;
1083 $vcsVersion = null;
1084 $vcsLink = null;
1085 $vcsDate = null;
1086
1087 if ( isset( $extension['version'] ) ) {
1088 $canonicalVersion = $out->parseInlineAsInterface( $extension['version'] );
1089 }
1090
1091 if ( isset( $extension['path'] ) ) {
1092 $extensionPath = dirname( $extension['path'] );
1093 if ( $this->coreId == '' ) {
1094 wfDebug( 'Looking up core head id' );
1095 $coreHeadSHA1 = GitInfo::repo()->getHeadSHA1();
1096 if ( $coreHeadSHA1 ) {
1097 $this->coreId = $coreHeadSHA1;
1098 }
1099 }
1100 $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getInstance( CACHE_ANYTHING );
1101 $memcKey = $cache->makeKey(
1102 'specialversion-ext-version-text', $extension['path'], $this->coreId
1103 );
1104 [ $vcsVersion, $vcsLink, $vcsDate ] = $cache->get( $memcKey );
1105
1106 if ( !$vcsVersion ) {
1107 wfDebug( "Getting VCS info for extension {$extension['name']}" );
1108 $gitInfo = new GitInfo( $extensionPath );
1109 $vcsVersion = $gitInfo->getHeadSHA1();
1110 if ( $vcsVersion !== false ) {
1111 $vcsVersion = substr( $vcsVersion, 0, 7 );
1112 $vcsLink = $gitInfo->getHeadViewUrl();
1113 $vcsDate = $gitInfo->getHeadCommitDate();
1114 }
1115 $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
1116 } else {
1117 wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" );
1118 }
1119 }
1120
1121 $versionString = Html::rawElement(
1122 'span',
1123 [ 'class' => 'mw-version-ext-version' ],
1124 $canonicalVersion
1125 );
1126
1127 if ( $vcsVersion ) {
1128 if ( $vcsLink ) {
1129 $vcsVerString = $this->getLinkRenderer()->makeExternalLink(
1130 $vcsLink,
1131 $this->msg( 'version-version', $vcsVersion ),
1132 $this->getFullTitle(),
1133 '',
1134 [ 'class' => 'mw-version-ext-vcs-version' ]
1135 );
1136 } else {
1137 $vcsVerString = Html::element( 'span',
1138 [ 'class' => 'mw-version-ext-vcs-version' ],
1139 "({$vcsVersion})"
1140 );
1141 }
1142 $versionString .= " {$vcsVerString}";
1143
1144 if ( $vcsDate ) {
1145 $versionString .= ' ' . Html::element( 'span', [
1146 'class' => 'mw-version-ext-vcs-timestamp',
1147 'dir' => $this->getLanguage()->getDir(),
1148 ], $this->getLanguage()->timeanddate( $vcsDate, true ) );
1149 }
1150 $versionString = Html::rawElement( 'span',
1151 [ 'class' => 'mw-version-ext-meta-version' ],
1152 $versionString
1153 );
1154 }
1155
1156 // ... and license information; if a license file exists we
1157 // will link to it
1158 $licenseLink = '';
1159 if ( isset( $extension['name'] ) ) {
1160 $licenseName = null;
1161 if ( isset( $extension['license-name'] ) ) {
1162 $licenseName = new HtmlArmor( $out->parseInlineAsInterface( $extension['license-name'] ) );
1163 } elseif ( $extensionPath !== null && ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
1164 $licenseName = $this->msg( 'version-ext-license' )->text();
1165 }
1166 if ( $licenseName !== null ) {
1167 $licenseLink = $this->getLinkRenderer()->makeLink(
1168 $this->getPageTitle( 'License/' . $extension['name'] ),
1169 $licenseName,
1170 [
1171 'class' => 'mw-version-ext-license',
1172 'dir' => 'auto',
1173 ]
1174 );
1175 }
1176 }
1177
1178 // ... and generate the description; which can be a parameterized l10n message
1179 // in the form [ <msgname>, <parameter>, <parameter>... ] or just a straight
1180 // up string
1181 if ( isset( $extension['descriptionmsg'] ) ) {
1182 // Localized description of extension
1183 $descriptionMsg = $extension['descriptionmsg'];
1184
1185 if ( is_array( $descriptionMsg ) ) {
1186 $descriptionMsgKey = array_shift( $descriptionMsg );
1187 $descriptionMsg = array_map( 'htmlspecialchars', $descriptionMsg );
1188 $description = $this->msg( $descriptionMsgKey, ...$descriptionMsg )->text();
1189 } else {
1190 $description = $this->msg( $descriptionMsg )->text();
1191 }
1192 } elseif ( isset( $extension['description'] ) ) {
1193 // Non localized version
1194 $description = $extension['description'];
1195 } else {
1196 $description = '';
1197 }
1198 $description = $out->parseInlineAsInterface( $description );
1199
1200 // ... now get the authors for this extension
1201 $authors = $extension['author'] ?? [];
1202 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable path is set when there is a name
1203 $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
1204
1205 // Finally! Create the table
1206 $html = Html::openElement( 'tr', [
1207 'class' => 'mw-version-ext',
1208 'id' => Sanitizer::escapeIdForAttribute( 'mw-version-ext-' . $type . '-' . $extension['name'] )
1209 ]
1210 );
1211
1212 $html .= Html::rawElement( 'td', [], $extensionNameLink );
1213 $html .= Html::rawElement( 'td', [], $versionString );
1214 $html .= Html::rawElement( 'td', [], $licenseLink );
1215 $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-description' ], $description );
1216 $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-authors' ], $authors );
1217
1218 $html .= Html::closeElement( 'tr' );
1219
1220 return $html;
1221 }
1222
1228 private function getHooks() {
1229 if ( !$this->getConfig()->get( MainConfigNames::SpecialVersionShowHooks ) ) {
1230 return '';
1231 }
1232
1233 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1234 $hookNames = $hookContainer->getHookNames();
1235
1236 if ( !$hookNames ) {
1237 return '';
1238 }
1239
1240 sort( $hookNames );
1241
1242 $ret = [];
1243 $this->addTocSection( 'version-hooks', 'mw-version-hooks' );
1244 $ret[] = Html::element(
1245 'h2',
1246 [ 'id' => 'mw-version-hooks' ],
1247 $this->msg( 'version-hooks' )->text()
1248 );
1249 $ret[] = Html::openElement( 'table', [ 'class' => 'wikitable', 'id' => 'sv-hooks' ] );
1250 $ret[] = Html::openElement( 'tr' );
1251 $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-name' )->text() );
1252 $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-subscribedby' )->text() );
1253 $ret[] = Html::closeElement( 'tr' );
1254
1255 foreach ( $hookNames as $name ) {
1256 $handlers = $hookContainer->getHandlerDescriptions( $name );
1257
1258 $ret[] = Html::openElement( 'tr' );
1259 $ret[] = Html::element( 'td', [], $name );
1260 // @phan-suppress-next-line SecurityCheck-DoubleEscaped See FIXME in listToText
1261 $ret[] = Html::element( 'td', [], $this->listToText( $handlers ) );
1262 $ret[] = Html::closeElement( 'tr' );
1263 }
1264
1265 $ret[] = Html::closeElement( 'table' );
1266
1267 return implode( "\n", $ret );
1268 }
1269
1270 private function openExtType( ?string $text = null, ?string $name = null ): string {
1271 $out = '';
1272
1273 $opt = [ 'class' => 'wikitable plainlinks mw-installed-software' ];
1274
1275 if ( $name ) {
1276 $opt['id'] = "sv-$name";
1277 }
1278
1279 $out .= Html::openElement( 'table', $opt );
1280
1281 if ( $text !== null ) {
1282 $out .= Html::element( 'caption', [], $text );
1283 }
1284
1285 if ( $name && $text !== null ) {
1286 $this->addTocSubSection( $text, "sv-$name" );
1287 }
1288
1289 $firstHeadingMsg = ( $name === 'credits-skin' )
1290 ? 'version-skin-colheader-name'
1291 : 'version-ext-colheader-name';
1292
1293 $out .= $this->getTableHeaderHtml( [
1294 $this->msg( $firstHeadingMsg )->text(),
1295 $this->msg( 'version-ext-colheader-version' )->text(),
1296 $this->msg( 'version-ext-colheader-license' )->text(),
1297 $this->msg( 'version-ext-colheader-description' )->text(),
1298 $this->msg( 'version-ext-colheader-credits' )->text()
1299 ] );
1300
1301 return $out;
1302 }
1303
1312 private function getTableHeaderHtml( $headers ): string {
1313 $out = '';
1314 $out .= Html::openElement( 'thead' );
1315 $out .= Html::openElement( 'tr' );
1316 foreach ( $headers as $header ) {
1317 $out .= Html::element( 'th', [ 'scope' => 'col' ], $header );
1318 }
1319 $out .= Html::closeElement( 'tr' );
1320 $out .= Html::closeElement( 'thead' );
1321 return $out;
1322 }
1323
1329 private function IPInfo() {
1330 $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
1331
1332 return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
1333 }
1334
1355 public function listAuthors( $authors, $extName, $extDir ): string {
1356 $hasOthers = false;
1357 $linkRenderer = $this->getLinkRenderer();
1358
1359 $list = [];
1360 $authors = (array)$authors;
1361
1362 // Special case: if the authors array has only one item and it is "...",
1363 // it should not be rendered as the "version-poweredby-others" i18n msg,
1364 // but rather as "version-poweredby-various" i18n msg instead.
1365 if ( count( $authors ) === 1 && $authors[0] === '...' ) {
1366 // Link to the extension's or skin's AUTHORS or CREDITS file, if there is
1367 // such a file; otherwise just return the i18n msg as-is
1368 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1369 return $linkRenderer->makeLink(
1370 $this->getPageTitle( "Credits/$extName" ),
1371 $this->msg( 'version-poweredby-various' )->text()
1372 );
1373 } else {
1374 return $this->msg( 'version-poweredby-various' )->escaped();
1375 }
1376 }
1377
1378 // Otherwise, if we have an actual array that has more than one item,
1379 // process each array item as usual
1380 foreach ( $authors as $item ) {
1381 if ( $item instanceof HtmlArmor ) {
1382 $list[] = HtmlArmor::getHtml( $item );
1383 } elseif ( $item === '...' ) {
1384 $hasOthers = true;
1385
1386 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1387 $text = $linkRenderer->makeLink(
1388 $this->getPageTitle( "Credits/$extName" ),
1389 $this->msg( 'version-poweredby-others' )->text()
1390 );
1391 } else {
1392 $text = $this->msg( 'version-poweredby-others' )->escaped();
1393 }
1394 $list[] = $text;
1395 } elseif ( str_ends_with( $item, ' ...]' ) ) {
1396 $hasOthers = true;
1397 $list[] = $this->getOutput()->parseInlineAsInterface(
1398 substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
1399 );
1400 } else {
1401 $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1402 }
1403 }
1404
1405 if ( $extName && !$hasOthers && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1406 $list[] = $linkRenderer->makeLink(
1407 $this->getPageTitle( "Credits/$extName" ),
1408 $this->msg( 'version-poweredby-others' )->text()
1409 );
1410 }
1411
1412 return $this->listToText( $list, false );
1413 }
1414
1424 private function listToText( array $list, bool $sort = true ): string {
1425 if ( !$list ) {
1426 return '';
1427 }
1428 if ( $sort ) {
1429 sort( $list );
1430 }
1431
1432 return $this->getLanguage()
1433 ->listToText( array_map( [ __CLASS__, 'arrayToString' ], $list ) );
1434 }
1435
1445 public static function arrayToString( $list ) {
1446 if ( is_array( $list ) && count( $list ) == 1 ) {
1447 $list = $list[0];
1448 }
1449 if ( $list instanceof Closure ) {
1450 // Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$"
1451 return 'Closure';
1452 } elseif ( is_object( $list ) ) {
1453 return wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1454 } elseif ( !is_array( $list ) ) {
1455 return $list;
1456 } else {
1457 if ( is_object( $list[0] ) ) {
1458 $class = get_class( $list[0] );
1459 } else {
1460 $class = $list[0];
1461 }
1462
1463 return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1464 }
1465 }
1466
1472 public static function getGitHeadSha1( $dir ) {
1473 wfDeprecated( __METHOD__, '1.41' );
1474 return ( new GitInfo( $dir ) )->getHeadSHA1();
1475 }
1476
1481 public function getEntryPointInfo() {
1482 $config = $this->getConfig();
1483 $scriptPath = $config->get( MainConfigNames::ScriptPath ) ?: '/';
1484
1485 $entryPoints = [
1486 'version-entrypoints-articlepath' => $config->get( MainConfigNames::ArticlePath ),
1487 'version-entrypoints-scriptpath' => $scriptPath,
1488 'version-entrypoints-index-php' => wfScript( 'index' ),
1489 'version-entrypoints-api-php' => wfScript( 'api' ),
1490 'version-entrypoints-rest-php' => wfScript( 'rest' ),
1491 ];
1492
1493 $language = $this->getLanguage();
1494 $thAttributes = [
1495 'dir' => $language->getDir(),
1496 'lang' => $language->getHtmlCode(),
1497 'scope' => 'col'
1498 ];
1499
1500 $this->addTocSection( 'version-entrypoints', 'mw-version-entrypoints' );
1501
1502 $out = Html::element(
1503 'h2',
1504 [ 'id' => 'mw-version-entrypoints' ],
1505 $this->msg( 'version-entrypoints' )->text()
1506 ) .
1507 Html::openElement( 'table',
1508 [
1509 'class' => 'wikitable plainlinks',
1510 'id' => 'mw-version-entrypoints-table',
1511 'dir' => 'ltr',
1512 'lang' => 'en'
1513 ]
1514 ) .
1515 Html::openElement( 'thead' ) .
1516 Html::openElement( 'tr' ) .
1517 Html::element(
1518 'th',
1519 $thAttributes,
1520 $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1521 ) .
1522 Html::element(
1523 'th',
1524 $thAttributes,
1525 $this->msg( 'version-entrypoints-header-url' )->text()
1526 ) .
1527 Html::closeElement( 'tr' ) .
1528 Html::closeElement( 'thead' );
1529
1530 foreach ( $entryPoints as $message => $value ) {
1531 $url = $this->urlUtils->expand( $value, PROTO_RELATIVE );
1532 $out .= Html::openElement( 'tr' ) .
1533 Html::rawElement( 'td', [], $this->msg( $message )->parse() ) .
1534 Html::rawElement( 'td', [],
1535 Html::rawElement(
1536 'code',
1537 [],
1538 $this->msg( new RawMessage( "[$url $value]" ) )->parse()
1539 )
1540 ) .
1541 Html::closeElement( 'tr' );
1542 }
1543
1544 $out .= Html::closeElement( 'table' );
1545
1546 return $out;
1547 }
1548
1549 protected function getGroupName() {
1550 return 'wiki';
1551 }
1552}
1553
1558class_alias( SpecialVersion::class, 'SpecialVersion' );
const CACHE_ANYTHING
Definition Defines.php:86
const MW_VERSION
The running version of MediaWiki.
Definition Defines.php:37
const PROTO_RELATIVE
Definition Defines.php:233
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfScript( $script='index')
Get the URL path to a MediaWiki entry point.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgLang
Definition Setup.php:562
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:57
Base class for language-specific code.
Definition Language.php:81
Variant of the Message class.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:157
static listParam(array $list, $type=ListType::AND)
Definition Message.php:1367
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:46
Load JSON files, and uses a Processor to extract information.
getAllThings()
Get credits information about all installed extensions and skins.
Parent class for all special pages.
Version information about MediaWiki (core, extensions, libs), PHP, and the database.
getParserFunctionHooks()
Obtains a list of installed parser function hooks and the associated H2 header.
getLibraries(array $credits)
Generate the section for installed external libraries.
getParsoidModules()
Obtains a list of installed Parsoid Modules and the associated H2 header.
static parseComposerInstalled(array $credits)
string $coreId
The current rev id/SHA hash of MediaWiki core.
static arrayToString( $list)
Convert an array or object to a string for display.
static getVersion( $flags='', $lang=null)
Return a string of the MediaWiki version with Git revision if available.
listAuthors( $authors, $extName, $extDir)
Return a formatted unsorted list of authors.
static getExtensionTypeName( $type)
Returns the internationalized name for an extension type.
getCreditsForExtension( $type, array $extension)
Creates and formats a version line for a single extension.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
getExtensionCategory( $type, ?string $text, array $creditsGroup)
Creates and returns the HTML for a single extension category.
static getVersionLinked()
Return a wikitext-formatted string of the MediaWiki version with a link to the Git SHA1 of head if av...
static getExtensionTypes()
Returns an array with the base extension types.
compare( $a, $b)
Callback to sort extensions by type.
__construct(ParserFactory $parserFactory, UrlUtils $urlUtils, IConnectionProvider $dbProvider)
static getCopyrightAndAuthorList()
Get the "MediaWiki is copyright 2001-20xx by lots of cool folks" text.
getEntryPointInfo()
Get the list of entry points and their URLs.
static getCredits(ExtensionRegistry $reg, Config $conf)
static string[] false $extensionTypes
Lazy initialized key/value with message content.
getParserTags()
Obtains a list of installed parser tags and the associated H2 header.
getExternalLibraries(array $credits)
Generate an HTML table for external libraries that are installed.
Fetch status information from a local git repository.
Definition GitInfo.php:47
Library for creating and parsing MW-style timestamps.
A service to expand, parse, and otherwise manipulate URLs.
Definition UrlUtils.php:16
Reads an installed.json file and provides accessors to get what is installed.
Interface for configuration instances.
Definition Config.php:32
Provide primary and replica IDatabase connections.
$source
element(SerializerNode $parent, SerializerNode $node, $contents)
$header