MediaWiki master
SpecialVersion.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Specials;
24
25use Closure;
27use HtmlArmor;
28use Language;
47use Symfony\Component\Yaml\Yaml;
49use Wikimedia\Parsoid\Core\SectionMetadata;
50use Wikimedia\Parsoid\Core\TOCData;
52
59
63 protected $coreId = '';
64
68 protected static $extensionTypes = false;
69
71 protected $tocData;
72
74 protected $tocIndex;
75
77 protected $tocSection;
78
80 protected $tocSubSection;
81
82 private ParserFactory $parserFactory;
83 private UrlUtils $urlUtils;
84 private IConnectionProvider $dbProvider;
85
91 public function __construct(
92 ParserFactory $parserFactory,
93 UrlUtils $urlUtils,
94 IConnectionProvider $dbProvider
95 ) {
96 parent::__construct( 'Version' );
97 $this->parserFactory = $parserFactory;
98 $this->urlUtils = $urlUtils;
99 $this->dbProvider = $dbProvider;
100 }
101
109 public static function getCredits( ExtensionRegistry $reg, Config $conf ): array {
110 $credits = $conf->get( MainConfigNames::ExtensionCredits );
111 foreach ( $reg->getAllThings() as $credit ) {
112 $credits[$credit['type']][] = $credit;
113 }
114 return $credits;
115 }
116
120 public function execute( $par ) {
121 $config = $this->getConfig();
122 $credits = self::getCredits( ExtensionRegistry::getInstance(), $config );
123
124 $this->setHeaders();
125 $this->outputHeader();
126 $out = $this->getOutput();
127 $out->setPreventClickjacking( false );
128
129 // Explode the sub page information into useful bits
130 $parts = explode( '/', (string)$par );
131 $extNode = null;
132 if ( isset( $parts[1] ) ) {
133 $extName = str_replace( '_', ' ', $parts[1] );
134 // Find it!
135 foreach ( $credits as $extensions ) {
136 foreach ( $extensions as $ext ) {
137 if ( isset( $ext['name'] ) && ( $ext['name'] === $extName ) ) {
138 $extNode = &$ext;
139 break 2;
140 }
141 }
142 }
143 if ( !$extNode ) {
144 $out->setStatusCode( 404 );
145 }
146 } else {
147 $extName = 'MediaWiki';
148 }
149
150 // Now figure out what to do
151 switch ( strtolower( $parts[0] ) ) {
152 case 'credits':
153 $out->addModuleStyles( 'mediawiki.special' );
154
155 $wikiText = '{{int:version-credits-not-found}}';
156 if ( $extName === 'MediaWiki' ) {
157 $wikiText = file_get_contents( MW_INSTALL_PATH . '/CREDITS' );
158 // Put the contributor list into columns
159 $wikiText = str_replace(
160 [ '<!-- BEGIN CONTRIBUTOR LIST -->', '<!-- END CONTRIBUTOR LIST -->' ],
161 [ '<div class="mw-version-credits">', '</div>' ],
162 $wikiText
163 );
164 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
165 $file = ExtensionInfo::getAuthorsFileName( dirname( $extNode['path'] ) );
166 if ( $file ) {
167 $wikiText = file_get_contents( $file );
168 if ( str_ends_with( $file, '.txt' ) ) {
169 $wikiText = Html::element(
170 'pre',
171 [
172 'lang' => 'en',
173 'dir' => 'ltr',
174 ],
175 $wikiText
176 );
177 }
178 }
179 }
180
181 $out->setPageTitleMsg( $this->msg( 'version-credits-title' )->plaintextParams( $extName ) );
182 $out->addWikiTextAsInterface( $wikiText );
183 break;
184
185 case 'license':
186 $out->setPageTitleMsg( $this->msg( 'version-license-title' )->plaintextParams( $extName ) );
187
188 $licenseFound = false;
189
190 if ( $extName === 'MediaWiki' ) {
191 $out->addWikiTextAsInterface(
192 file_get_contents( MW_INSTALL_PATH . '/COPYING' )
193 );
194 $licenseFound = true;
195 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
196 $files = ExtensionInfo::getLicenseFileNames( dirname( $extNode['path'] ) );
197 if ( $files ) {
198 $licenseFound = true;
199 foreach ( $files as $file ) {
200 $out->addWikiTextAsInterface(
202 'pre',
203 [
204 'lang' => 'en',
205 'dir' => 'ltr',
206 ],
207 file_get_contents( $file )
208 )
209 );
210 }
211 }
212 }
213 if ( !$licenseFound ) {
214 $out->addWikiTextAsInterface( '{{int:version-license-not-found}}' );
215 }
216 break;
217
218 default:
219 $out->addModuleStyles( 'mediawiki.special' );
220
221 $out->addHTML( $this->getMediaWikiCredits() );
222
223 $this->tocData = new TOCData();
224 $this->tocIndex = 0;
225 $this->tocSection = 0;
226 $this->tocSubSection = 0;
227
228 // Build the page contents (this also fills in TOCData)
229 $sections = [
230 $this->softwareInformation(),
231 $this->getEntryPointInfo(),
232 $this->getSkinCredits( $credits ),
233 $this->getExtensionCredits( $credits ),
234 $this->getLibraries( $credits ),
235 $this->getParserTags(),
236 $this->getParserFunctionHooks(),
237 $this->getHooks(),
238 $this->IPInfo(),
239 ];
240
241 // Insert TOC first
242 $pout = new ParserOutput;
243 $pout->setTOCData( $this->tocData );
244 $pout->setOutputFlag( ParserOutputFlags::SHOW_TOC );
245 $pout->setRawText( Parser::TOC_PLACEHOLDER );
246 $out->addParserOutput( $pout );
247
248 // Insert contents
249 foreach ( $sections as $content ) {
250 $out->addHTML( $content );
251 }
252
253 break;
254 }
255 }
256
264 private function addTocSection( $labelMsg, $id ) {
265 $this->tocIndex++;
266 $this->tocSection++;
267 $this->tocSubSection = 0;
268 $this->tocData->addSection( new SectionMetadata(
269 1,
270 2,
271 $this->msg( $labelMsg )->escaped(),
272 $this->getLanguage()->formatNum( $this->tocSection ),
273 (string)$this->tocIndex,
274 null,
275 null,
276 $id,
277 $id
278 ) );
279 }
280
288 private function addTocSubSection( $label, $id ) {
289 $this->tocIndex++;
290 $this->tocSubSection++;
291 $this->tocData->addSection( new SectionMetadata(
292 2,
293 3,
294 htmlspecialchars( $label ),
295 // See Parser::localizeTOC
296 $this->getLanguage()->formatNum( $this->tocSection ) . '.' .
297 $this->getLanguage()->formatNum( $this->tocSubSection ),
298 (string)$this->tocIndex,
299 null,
300 null,
301 $id,
302 $id
303 ) );
304 }
305
311 private function getMediaWikiCredits() {
312 // No TOC entry for this heading, we treat it like the lede section
313
314 $ret = Html::element(
315 'h2',
316 [ 'id' => 'mw-version-license' ],
317 $this->msg( 'version-license' )->text()
318 );
319
320 $ret .= Html::rawElement( 'div', [ 'class' => 'plainlinks' ],
321 $this->msg( new RawMessage( self::getCopyrightAndAuthorList() ) )->parseAsBlock() .
322 Html::rawElement( 'div', [ 'class' => 'mw-version-license-info' ],
323 $this->msg( 'version-license-info' )->parseAsBlock()
324 )
325 );
326
327 return $ret;
328 }
329
336 public static function getCopyrightAndAuthorList() {
337 if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
338 $othersLink = '[https://www.mediawiki.org/wiki/Special:Version/Credits ' .
339 wfMessage( 'version-poweredby-others' )->plain() . ']';
340 } else {
341 $othersLink = '[[Special:Version/Credits|' .
342 wfMessage( 'version-poweredby-others' )->plain() . ']]';
343 }
344
345 $translatorsLink = '[https://translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
346 wfMessage( 'version-poweredby-translators' )->plain() . ']';
347
348 $authorList = [
349 'Magnus Manske', 'Brooke Vibber', 'Lee Daniel Crocker',
350 'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
351 'Niklas Laxström', 'Domas Mituzas', 'Rob Church', 'Yuri Astrakhan',
352 'Aryeh Gregor', 'Aaron Schulz', 'Andrew Garrett', 'Raimond Spekking',
353 'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
354 'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
355 'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
356 'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', 'Brad Jorsch',
357 'Bartosz Dziewoński', 'Ed Sanders', 'Moriel Schottlender',
358 'Kunal Mehta', 'James D. Forrester', 'Brian Wolff', 'Adam Shorland',
359 'DannyS712', 'Ori Livneh', 'Max Semenik', 'Amir Sarabadani',
360 'Derk-Jan Hartman', 'Petr Pchelko',
361 $othersLink, $translatorsLink
362 ];
363
364 return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
365 Message::listParam( $authorList ) )->plain();
366 }
367
373 private function getSoftwareInformation() {
374 $dbr = $this->dbProvider->getReplicaDatabase();
375
376 // Put the software in an array of form 'name' => 'version'. All messages should
377 // be loaded here, so feel free to use wfMessage in the 'name'. Wikitext
378 // can be used both in the name and value.
379 $software = [
380 '[https://www.mediawiki.org/ MediaWiki]' => self::getVersionLinked(),
381 '[https://php.net/ PHP]' => PHP_VERSION . " (" . PHP_SAPI . ")",
382 '[https://icu.unicode.org/ ICU]' => INTL_ICU_VERSION,
383 $dbr->getSoftwareLink() => $dbr->getServerInfo(),
384 ];
385
386 // T339915: If wikidiff2 is installed, show version
387 if ( phpversion( "wikidiff2" ) ) {
388 $software[ '[https://www.mediawiki.org/wiki/Wikidiff2 wikidiff2]' ] = phpversion( "wikidiff2" );
389 }
390
391 // Allow a hook to add/remove items.
392 $this->getHookRunner()->onSoftwareInfo( $software );
393
394 return $software;
395 }
396
402 private function softwareInformation() {
403 $this->addTocSection( 'version-software', 'mw-version-software' );
404
405 $out = Html::element(
406 'h2',
407 [ 'id' => 'mw-version-software' ],
408 $this->msg( 'version-software' )->text()
409 );
410
411 $out .= Html::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ] );
412
413 $out .= $this->getTableHeaderHtml( [
414 $this->msg( 'version-software-product' )->text(),
415 $this->msg( 'version-software-version' )->text()
416 ] );
417
418 foreach ( $this->getSoftwareInformation() as $name => $version ) {
419 $out .= Html::rawElement(
420 'tr',
421 [],
422 Html::rawElement( 'td', [], $this->msg( new RawMessage( $name ) )->parse() ) .
423 Html::rawElement( 'td', [ 'dir' => 'ltr' ], $this->msg( new RawMessage( $version ) )->parse() )
424 );
425 }
426
427 $out .= Html::closeElement( 'table' );
428
429 return $out;
430 }
431
441 public static function getVersion( $flags = '', $lang = null ) {
442 $gitInfo = GitInfo::repo()->getHeadSHA1();
443 if ( !$gitInfo ) {
444 $version = MW_VERSION;
445 } elseif ( $flags === 'nodb' ) {
446 $shortSha1 = substr( $gitInfo, 0, 7 );
447 $version = MW_VERSION . " ($shortSha1)";
448 } else {
449 $shortSha1 = substr( $gitInfo, 0, 7 );
450 $msg = wfMessage( 'parentheses' );
451 if ( $lang !== null ) {
452 $msg->inLanguage( $lang );
453 }
454 $shortSha1 = $msg->params( $shortSha1 )->text();
455 $version = MW_VERSION . ' ' . $shortSha1;
456 }
457
458 return $version;
459 }
460
468 public static function getVersionLinked() {
469 return self::getVersionLinkedGit() ?: MW_VERSION;
470 }
471
475 private static function getMWVersionLinked() {
476 $versionUrl = "";
477 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
478 if ( $hookRunner->onSpecialVersionVersionUrl( MW_VERSION, $versionUrl ) ) {
479 $versionParts = [];
480 preg_match( "/^(\d+\.\d+)/", MW_VERSION, $versionParts );
481 $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
482 }
483
484 return '[' . $versionUrl . ' ' . MW_VERSION . ']';
485 }
486
492 private static function getVersionLinkedGit() {
493 global $wgLang;
494
495 $gitInfo = new GitInfo( MW_INSTALL_PATH );
496 $headSHA1 = $gitInfo->getHeadSHA1();
497 if ( !$headSHA1 ) {
498 return false;
499 }
500
501 $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
502
503 $gitHeadUrl = $gitInfo->getHeadViewUrl();
504 if ( $gitHeadUrl !== false ) {
505 $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
506 }
507
508 $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
509 if ( $gitHeadCommitDate ) {
510 $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( (string)$gitHeadCommitDate, true );
511 }
512
513 return self::getMWVersionLinked() . " $shortSHA1";
514 }
515
525 public static function getExtensionTypes(): array {
526 if ( self::$extensionTypes === false ) {
527 self::$extensionTypes = [
528 'specialpage' => wfMessage( 'version-specialpages' )->text(),
529 'editor' => wfMessage( 'version-editors' )->text(),
530 'parserhook' => wfMessage( 'version-parserhooks' )->text(),
531 'variable' => wfMessage( 'version-variables' )->text(),
532 'media' => wfMessage( 'version-mediahandlers' )->text(),
533 'antispam' => wfMessage( 'version-antispam' )->text(),
534 'skin' => wfMessage( 'version-skins' )->text(),
535 'api' => wfMessage( 'version-api' )->text(),
536 'other' => wfMessage( 'version-other' )->text(),
537 ];
538
539 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
540 ->onExtensionTypes( self::$extensionTypes );
541 }
542
543 return self::$extensionTypes;
544 }
545
555 public static function getExtensionTypeName( $type ) {
556 $types = self::getExtensionTypes();
557
558 return $types[$type] ?? $types['other'];
559 }
560
567 private function getExtensionCredits( array $credits ) {
568 $extensionTypes = self::getExtensionTypes();
569
570 $this->addTocSection( 'version-extensions', 'mw-version-ext' );
571
572 $out = Html::element(
573 'h2',
574 [ 'id' => 'mw-version-ext' ],
575 $this->msg( 'version-extensions' )->text()
576 );
577
578 if (
579 !$credits ||
580 // Skins are displayed separately, see getSkinCredits()
581 ( count( $credits ) === 1 && isset( $credits['skin'] ) )
582 ) {
583 $out .= Html::element(
584 'p',
585 [],
586 $this->msg( 'version-extensions-no-ext' )->text()
587 );
588
589 return $out;
590 }
591
592 // Find all extensions that do not have a valid type and give them the type 'other'.
593 $credits['other'] ??= [];
594 foreach ( $credits as $type => $extensions ) {
595 if ( !array_key_exists( $type, $extensionTypes ) ) {
596 $credits['other'] = array_merge( $credits['other'], $extensions );
597 }
598 }
599
600 // Loop through the extension categories to display their extensions in the list.
601 foreach ( $extensionTypes as $type => $text ) {
602 // Skins have a separate section
603 if ( $type !== 'other' && $type !== 'skin' ) {
604 $out .= $this->getExtensionCategory( $type, $text, $credits[$type] ?? [] );
605 }
606 }
607
608 // We want the 'other' type to be last in the list.
609 $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'], $credits['other'] );
610
611 return $out;
612 }
613
620 private function getSkinCredits( array $credits ) {
621 $this->addTocSection( 'version-skins', 'mw-version-skin' );
622
623 $out = Html::element(
624 'h2',
625 [ 'id' => 'mw-version-skin' ],
626 $this->msg( 'version-skins' )->text()
627 );
628
629 if ( !isset( $credits['skin'] ) || !$credits['skin'] ) {
630 $out .= Html::element(
631 'p',
632 [],
633 $this->msg( 'version-skins-no-skin' )->text()
634 );
635
636 return $out;
637 }
638 $out .= $this->getExtensionCategory( 'skin', null, $credits['skin'] );
639
640 return $out;
641 }
642
649 protected function getLibraries( array $credits ) {
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
658 return $out
659 . $this->getExternalLibraries( $credits )
660 . $this->getClientSideLibraries();
661 }
662
669 protected function getExternalLibraries( array $credits ) {
670 $paths = [
671 MW_INSTALL_PATH . '/vendor/composer/installed.json'
672 ];
673
674 $extensionTypes = self::getExtensionTypes();
675 foreach ( $extensionTypes as $type => $message ) {
676 if ( !isset( $credits[$type] ) || $credits[$type] === [] ) {
677 continue;
678 }
679 foreach ( $credits[$type] as $extension ) {
680 if ( !isset( $extension['path'] ) ) {
681 continue;
682 }
683 $paths[] = dirname( $extension['path'] ) . '/vendor/composer/installed.json';
684 }
685 }
686
687 $dependencies = [];
688
689 foreach ( $paths as $path ) {
690 if ( !file_exists( $path ) ) {
691 continue;
692 }
693
694 $installed = new ComposerInstalled( $path );
695
696 $dependencies += $installed->getInstalledDependencies();
697 }
698
699 if ( $dependencies === [] ) {
700 return '';
701 }
702
703 ksort( $dependencies );
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 Linker::makeExternalLink(
752 "https://packagist.org/packages/$name", $name,
753 true, '',
754 [ 'class' => 'mw-version-library-name' ]
755 )
756 )
757 . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
758 // @phan-suppress-next-line SecurityCheck-DoubleEscaped See FIXME in listToText
759 . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['licenses'] ) )
760 . Html::element( 'td', [ 'lang' => 'en', 'dir' => 'ltr' ], $info['description'] )
761 . Html::rawElement( 'td', [], $authors )
762 . Html::closeElement( 'tr' );
763 }
764 $out .= Html::closeElement( 'table' );
765
766 return $out;
767 }
768
774 public static function parseForeignResources() {
775 $registryDirs = [ 'MediaWiki' => MW_INSTALL_PATH . '/resources/lib' ]
776 + ExtensionRegistry::getInstance()->getAttribute( 'ForeignResourcesDir' );
777
778 $modules = [];
779 foreach ( $registryDirs as $source => $registryDir ) {
780 $foreignResources = Yaml::parseFile( "$registryDir/foreign-resources.yaml" );
781 foreach ( $foreignResources as $name => $module ) {
782 $key = $name . $module['version'];
783 if ( isset( $modules[$key] ) ) {
784 $modules[$key]['source'][] = $source;
785 continue;
786 }
787 $modules[$key] = $module + [ 'name' => $name, 'source' => [ $source ] ];
788 }
789 }
790 ksort( $modules );
791 return $modules;
792 }
793
799 private function getClientSideLibraries() {
800 $this->addTocSubSection( $this->msg( 'version-libraries-client' )->text(), 'mw-version-libraries-client' );
801
802 $out = Html::element(
803 'h3',
804 [ 'id' => 'mw-version-libraries-client' ],
805 $this->msg( 'version-libraries-client' )->text()
806 );
807 $out .= Html::openElement(
808 'table',
809 [ 'class' => 'wikitable plainlinks mw-installed-software', 'id' => 'sv-libraries-client' ]
810 );
811
812 $out .= $this->getTableHeaderHtml( [
813 $this->msg( 'version-libraries-library' )->text(),
814 $this->msg( 'version-libraries-version' )->text(),
815 $this->msg( 'version-libraries-license' )->text(),
816 $this->msg( 'version-libraries-authors' )->text(),
817 $this->msg( 'version-libraries-source' )->text()
818 ] );
819
820 foreach ( self::parseForeignResources() as $name => $info ) {
821 // We can safely assume that the libraries' names and descriptions
822 // are written in English and aren't going to be translated,
823 // so set appropriate lang and dir attributes
824 $out .= Html::openElement( 'tr', [
825 // Add an anchor so docs can link easily to the version of
826 // this specific library
827 'id' => Sanitizer::escapeIdForAttribute(
828 "mw-version-library-$name"
829 ) ] )
830 . Html::rawElement(
831 'td',
832 [],
833 Linker::makeExternalLink(
834 $info['homepage'], $info['name'],
835 true, '',
836 [ 'class' => 'mw-version-library-name' ]
837 )
838 )
839 . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
840 . Html::element( 'td', [ 'dir' => 'auto' ], $info['license'] )
841 . Html::element( 'td', [ 'dir' => 'auto' ], $info['authors'] ?? '—' )
842 // @phan-suppress-next-line SecurityCheck-DoubleEscaped See FIXME in listToText
843 . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['source'] ) )
844 . Html::closeElement( 'tr' );
845 }
846 $out .= Html::closeElement( 'table' );
847
848 return $out;
849 }
850
856 protected function getParserTags() {
857 $tags = $this->parserFactory->getMainInstance()->getTags();
858 if ( !$tags ) {
859 return '';
860 }
861
862 $this->addTocSection( 'version-parser-extensiontags', 'mw-version-parser-extensiontags' );
863
864 $out = Html::rawElement(
865 'h2',
866 [ 'id' => 'mw-version-parser-extensiontags' ],
867 Html::rawElement(
868 'span',
869 [ 'class' => 'plainlinks' ],
870 Linker::makeExternalLink(
871 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
872 $this->msg( 'version-parser-extensiontags' )->text()
873 )
874 )
875 );
876
877 array_walk( $tags, static function ( &$value ) {
878 // Bidirectional isolation improves readability in RTL wikis
879 $value = Html::rawElement(
880 'bdi',
881 // Prevent < and > from slipping to another line
882 [
883 'style' => 'white-space: nowrap;',
884 ],
885 Html::element( 'code', [], "<$value>" )
886 );
887 } );
888
889 $out .= $this->listToText( $tags );
890
891 return $out;
892 }
893
899 protected function getParserFunctionHooks() {
900 $funcHooks = $this->parserFactory->getMainInstance()->getFunctionHooks();
901 if ( !$funcHooks ) {
902 return '';
903 }
904
905 $this->addTocSection( 'version-parser-function-hooks', 'mw-version-parser-function-hooks' );
906
907 $out = Html::rawElement(
908 'h2',
909 [ 'id' => 'mw-version-parser-function-hooks' ],
910 Html::rawElement(
911 'span',
912 [ 'class' => 'plainlinks' ],
913 Linker::makeExternalLink(
914 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
915 $this->msg( 'version-parser-function-hooks' )->text()
916 )
917 )
918 );
919
920 $funcSynonyms = $this->parserFactory->getMainInstance()->getFunctionSynonyms();
921 // This will give us the preferred synonyms in the content language, as if
922 // we used MagicWord::getSynonym( 0 ), because they appear first in the arrays.
923 // We can't use MagicWord directly, because only Parser knows whether a function
924 // uses the leading "#" or not. Case-sensitive functions ("1") win over
925 // case-insensitive ones ("0"), like in Parser::callParserFunction().
926 // There should probably be a better API for this.
927 $preferredSynonyms = array_flip( array_reverse( $funcSynonyms[1] + $funcSynonyms[0] ) );
928 array_walk( $funcHooks, static function ( &$value ) use ( $preferredSynonyms ) {
929 $value = $preferredSynonyms[$value];
930 } );
931
932 // Sort case-insensitively, ignoring the leading '#' if present
933 usort( $funcHooks, static function ( $a, $b ) {
934 return strcasecmp( ltrim( $a, '#' ), ltrim( $b, '#' ) );
935 } );
936
937 array_walk( $funcHooks, static function ( &$value ) {
938 // Bidirectional isolation ensures it displays as {{#ns}} and not {{ns#}} in RTL wikis
939 $value = Html::rawElement(
940 'bdi',
941 [],
942 Html::element( 'code', [], '{{' . $value . '}}' )
943 );
944 } );
945
946 $out .= $this->getLanguage()->listToText( $funcHooks );
947
948 return $out;
949 }
950
960 protected function getExtensionCategory( $type, ?string $text, array $creditsGroup ) {
961 $out = '';
962
963 if ( $creditsGroup ) {
964 $out .= $this->openExtType( $text, 'credits-' . $type );
965
966 usort( $creditsGroup, [ $this, 'compare' ] );
967
968 foreach ( $creditsGroup as $extension ) {
969 $out .= $this->getCreditsForExtension( $type, $extension );
970 }
971
972 $out .= Html::closeElement( 'table' );
973 }
974
975 return $out;
976 }
977
984 public function compare( $a, $b ) {
985 return $this->getLanguage()->lc( $a['name'] ) <=> $this->getLanguage()->lc( $b['name'] );
986 }
987
1006 public function getCreditsForExtension( $type, array $extension ) {
1007 $out = $this->getOutput();
1008
1009 // We must obtain the information for all the bits and pieces!
1010 // ... such as extension names and links
1011 if ( isset( $extension['namemsg'] ) ) {
1012 // Localized name of extension
1013 $extensionName = $this->msg( $extension['namemsg'] )->text();
1014 } elseif ( isset( $extension['name'] ) ) {
1015 // Non localized version
1016 $extensionName = $extension['name'];
1017 } else {
1018 $extensionName = $this->msg( 'version-no-ext-name' )->text();
1019 }
1020
1021 if ( isset( $extension['url'] ) ) {
1022 $extensionNameLink = Linker::makeExternalLink(
1023 $extension['url'],
1024 $extensionName,
1025 true,
1026 '',
1027 [ 'class' => 'mw-version-ext-name' ]
1028 );
1029 } else {
1030 $extensionNameLink = htmlspecialchars( $extensionName );
1031 }
1032
1033 // ... and the version information
1034 // If the extension path is set we will check that directory for GIT
1035 // metadata in an attempt to extract date and vcs commit metadata.
1036 $canonicalVersion = '&ndash;';
1037 $extensionPath = null;
1038 $vcsVersion = null;
1039 $vcsLink = null;
1040 $vcsDate = null;
1041
1042 if ( isset( $extension['version'] ) ) {
1043 $canonicalVersion = $out->parseInlineAsInterface( $extension['version'] );
1044 }
1045
1046 if ( isset( $extension['path'] ) ) {
1047 $extensionPath = dirname( $extension['path'] );
1048 if ( $this->coreId == '' ) {
1049 wfDebug( 'Looking up core head id' );
1050 $coreHeadSHA1 = GitInfo::repo()->getHeadSHA1();
1051 if ( $coreHeadSHA1 ) {
1052 $this->coreId = $coreHeadSHA1;
1053 }
1054 }
1055 $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getInstance( CACHE_ANYTHING );
1056 $memcKey = $cache->makeKey(
1057 'specialversion-ext-version-text', $extension['path'], $this->coreId
1058 );
1059 [ $vcsVersion, $vcsLink, $vcsDate ] = $cache->get( $memcKey );
1060
1061 if ( !$vcsVersion ) {
1062 wfDebug( "Getting VCS info for extension {$extension['name']}" );
1063 $gitInfo = new GitInfo( $extensionPath );
1064 $vcsVersion = $gitInfo->getHeadSHA1();
1065 if ( $vcsVersion !== false ) {
1066 $vcsVersion = substr( $vcsVersion, 0, 7 );
1067 $vcsLink = $gitInfo->getHeadViewUrl();
1068 $vcsDate = $gitInfo->getHeadCommitDate();
1069 }
1070 $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
1071 } else {
1072 wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" );
1073 }
1074 }
1075
1076 $versionString = Html::rawElement(
1077 'span',
1078 [ 'class' => 'mw-version-ext-version' ],
1079 $canonicalVersion
1080 );
1081
1082 if ( $vcsVersion ) {
1083 if ( $vcsLink ) {
1084 $vcsVerString = Linker::makeExternalLink(
1085 $vcsLink,
1086 $this->msg( 'version-version', $vcsVersion )->text(),
1087 true,
1088 '',
1089 [ 'class' => 'mw-version-ext-vcs-version' ]
1090 );
1091 } else {
1092 $vcsVerString = Html::element( 'span',
1093 [ 'class' => 'mw-version-ext-vcs-version' ],
1094 "({$vcsVersion})"
1095 );
1096 }
1097 $versionString .= " {$vcsVerString}";
1098
1099 if ( $vcsDate ) {
1100 $versionString .= ' ' . Html::element( 'span', [
1101 'class' => 'mw-version-ext-vcs-timestamp',
1102 'dir' => $this->getLanguage()->getDir(),
1103 ], $this->getLanguage()->timeanddate( $vcsDate, true ) );
1104 }
1105 $versionString = Html::rawElement( 'span',
1106 [ 'class' => 'mw-version-ext-meta-version' ],
1107 $versionString
1108 );
1109 }
1110
1111 // ... and license information; if a license file exists we
1112 // will link to it
1113 $licenseLink = '';
1114 if ( isset( $extension['name'] ) ) {
1115 $licenseName = null;
1116 if ( isset( $extension['license-name'] ) ) {
1117 $licenseName = new HtmlArmor( $out->parseInlineAsInterface( $extension['license-name'] ) );
1118 } elseif ( $extensionPath !== null && ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
1119 $licenseName = $this->msg( 'version-ext-license' )->text();
1120 }
1121 if ( $licenseName !== null ) {
1122 $licenseLink = $this->getLinkRenderer()->makeLink(
1123 $this->getPageTitle( 'License/' . $extension['name'] ),
1124 $licenseName,
1125 [
1126 'class' => 'mw-version-ext-license',
1127 'dir' => 'auto',
1128 ]
1129 );
1130 }
1131 }
1132
1133 // ... and generate the description; which can be a parameterized l10n message
1134 // in the form [ <msgname>, <parameter>, <parameter>... ] or just a straight
1135 // up string
1136 if ( isset( $extension['descriptionmsg'] ) ) {
1137 // Localized description of extension
1138 $descriptionMsg = $extension['descriptionmsg'];
1139
1140 if ( is_array( $descriptionMsg ) ) {
1141 $descriptionMsgKey = array_shift( $descriptionMsg );
1142 $descriptionMsg = array_map( 'htmlspecialchars', $descriptionMsg );
1143 $description = $this->msg( $descriptionMsgKey, ...$descriptionMsg )->text();
1144 } else {
1145 $description = $this->msg( $descriptionMsg )->text();
1146 }
1147 } elseif ( isset( $extension['description'] ) ) {
1148 // Non localized version
1149 $description = $extension['description'];
1150 } else {
1151 $description = '';
1152 }
1153 $description = $out->parseInlineAsInterface( $description );
1154
1155 // ... now get the authors for this extension
1156 $authors = $extension['author'] ?? [];
1157 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable path is set when there is a name
1158 $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
1159
1160 // Finally! Create the table
1161 $html = Html::openElement( 'tr', [
1162 'class' => 'mw-version-ext',
1163 'id' => Sanitizer::escapeIdForAttribute( 'mw-version-ext-' . $type . '-' . $extension['name'] )
1164 ]
1165 );
1166
1167 $html .= Html::rawElement( 'td', [], $extensionNameLink );
1168 $html .= Html::rawElement( 'td', [], $versionString );
1169 $html .= Html::rawElement( 'td', [], $licenseLink );
1170 $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-description' ], $description );
1171 $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-authors' ], $authors );
1172
1173 $html .= Html::closeElement( 'tr' );
1174
1175 return $html;
1176 }
1177
1183 private function getHooks() {
1184 if ( !$this->getConfig()->get( MainConfigNames::SpecialVersionShowHooks ) ) {
1185 return '';
1186 }
1187
1188 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1189 $hookNames = $hookContainer->getHookNames();
1190
1191 if ( !$hookNames ) {
1192 return '';
1193 }
1194
1195 sort( $hookNames );
1196
1197 $ret = [];
1198 $this->addTocSection( 'version-hooks', 'mw-version-hooks' );
1199 $ret[] = Html::element(
1200 'h2',
1201 [ 'id' => 'mw-version-hooks' ],
1202 $this->msg( 'version-hooks' )->text()
1203 );
1204 $ret[] = Html::openElement( 'table', [ 'class' => 'wikitable', 'id' => 'sv-hooks' ] );
1205 $ret[] = Html::openElement( 'tr' );
1206 $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-name' )->text() );
1207 $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-subscribedby' )->text() );
1208 $ret[] = Html::closeElement( 'tr' );
1209
1210 foreach ( $hookNames as $name ) {
1211 $handlers = $hookContainer->getHandlerDescriptions( $name );
1212
1213 $ret[] = Html::openElement( 'tr' );
1214 $ret[] = Html::element( 'td', [], $name );
1215 // @phan-suppress-next-line SecurityCheck-DoubleEscaped See FIXME in listToText
1216 $ret[] = Html::element( 'td', [], $this->listToText( $handlers ) );
1217 $ret[] = Html::closeElement( 'tr' );
1218 }
1219
1220 $ret[] = Html::closeElement( 'table' );
1221
1222 return implode( "\n", $ret );
1223 }
1224
1225 private function openExtType( string $text = null, string $name = null ) {
1226 $out = '';
1227
1228 $opt = [ 'class' => 'wikitable plainlinks mw-installed-software' ];
1229
1230 if ( $name ) {
1231 $opt['id'] = "sv-$name";
1232 }
1233
1234 $out .= Html::openElement( 'table', $opt );
1235
1236 if ( $text !== null ) {
1237 $out .= Html::element( 'caption', [], $text );
1238 }
1239
1240 if ( $name && $text !== null ) {
1241 $this->addTocSubSection( $text, "sv-$name" );
1242 }
1243
1244 $firstHeadingMsg = ( $name === 'credits-skin' )
1245 ? 'version-skin-colheader-name'
1246 : 'version-ext-colheader-name';
1247
1248 $out .= $this->getTableHeaderHtml( [
1249 $this->msg( $firstHeadingMsg )->text(),
1250 $this->msg( 'version-ext-colheader-version' )->text(),
1251 $this->msg( 'version-ext-colheader-license' )->text(),
1252 $this->msg( 'version-ext-colheader-description' )->text(),
1253 $this->msg( 'version-ext-colheader-credits' )->text()
1254 ] );
1255
1256 return $out;
1257 }
1258
1267 private function getTableHeaderHtml( $headers ): string {
1268 $out = '';
1269 $out .= Html::openElement( 'thead' );
1270 $out .= Html::openElement( 'tr' );
1271 foreach ( $headers as $header ) {
1272 $out .= Html::element( 'th', [ 'scope' => 'col' ], $header );
1273 }
1274 $out .= Html::closeElement( 'tr' );
1275 $out .= Html::closeElement( 'thead' );
1276 return $out;
1277 }
1278
1284 private function IPInfo() {
1285 $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
1286
1287 return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
1288 }
1289
1310 public function listAuthors( $authors, $extName, $extDir ): string {
1311 $hasOthers = false;
1312 $linkRenderer = $this->getLinkRenderer();
1313
1314 $list = [];
1315 $authors = (array)$authors;
1316
1317 // Special case: if the authors array has only one item and it is "...",
1318 // it should not be rendered as the "version-poweredby-others" i18n msg,
1319 // but rather as "version-poweredby-various" i18n msg instead.
1320 if ( count( $authors ) === 1 && $authors[0] === '...' ) {
1321 // Link to the extension's or skin's AUTHORS or CREDITS file, if there is
1322 // such a file; otherwise just return the i18n msg as-is
1323 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1324 return $linkRenderer->makeLink(
1325 $this->getPageTitle( "Credits/$extName" ),
1326 $this->msg( 'version-poweredby-various' )->text()
1327 );
1328 } else {
1329 return $this->msg( 'version-poweredby-various' )->escaped();
1330 }
1331 }
1332
1333 // Otherwise, if we have an actual array that has more than one item,
1334 // process each array item as usual
1335 foreach ( $authors as $item ) {
1336 if ( $item instanceof HtmlArmor ) {
1337 $list[] = HtmlArmor::getHtml( $item );
1338 } elseif ( $item === '...' ) {
1339 $hasOthers = true;
1340
1341 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1342 $text = $linkRenderer->makeLink(
1343 $this->getPageTitle( "Credits/$extName" ),
1344 $this->msg( 'version-poweredby-others' )->text()
1345 );
1346 } else {
1347 $text = $this->msg( 'version-poweredby-others' )->escaped();
1348 }
1349 $list[] = $text;
1350 } elseif ( str_ends_with( $item, ' ...]' ) ) {
1351 $hasOthers = true;
1352 $list[] = $this->getOutput()->parseInlineAsInterface(
1353 substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
1354 );
1355 } else {
1356 $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1357 }
1358 }
1359
1360 if ( $extName && !$hasOthers && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1361 $list[] = $linkRenderer->makeLink(
1362 $this->getPageTitle( "Credits/$extName" ),
1363 $this->msg( 'version-poweredby-others' )->text()
1364 );
1365 }
1366
1367 return $this->listToText( $list, false );
1368 }
1369
1379 private function listToText( array $list, bool $sort = true ): string {
1380 if ( !$list ) {
1381 return '';
1382 }
1383 if ( $sort ) {
1384 sort( $list );
1385 }
1386
1387 return $this->getLanguage()
1388 ->listToText( array_map( [ __CLASS__, 'arrayToString' ], $list ) );
1389 }
1390
1400 public static function arrayToString( $list ) {
1401 if ( is_array( $list ) && count( $list ) == 1 ) {
1402 $list = $list[0];
1403 }
1404 if ( $list instanceof Closure ) {
1405 // Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$"
1406 return 'Closure';
1407 } elseif ( is_object( $list ) ) {
1408 return wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1409 } elseif ( !is_array( $list ) ) {
1410 return $list;
1411 } else {
1412 if ( is_object( $list[0] ) ) {
1413 $class = get_class( $list[0] );
1414 } else {
1415 $class = $list[0];
1416 }
1417
1418 return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1419 }
1420 }
1421
1427 public static function getGitHeadSha1( $dir ) {
1428 wfDeprecated( __METHOD__, '1.41' );
1429 return ( new GitInfo( $dir ) )->getHeadSHA1();
1430 }
1431
1436 public function getEntryPointInfo() {
1437 $config = $this->getConfig();
1438 $scriptPath = $config->get( MainConfigNames::ScriptPath ) ?: '/';
1439
1440 $entryPoints = [
1441 'version-entrypoints-articlepath' => $config->get( MainConfigNames::ArticlePath ),
1442 'version-entrypoints-scriptpath' => $scriptPath,
1443 'version-entrypoints-index-php' => wfScript( 'index' ),
1444 'version-entrypoints-api-php' => wfScript( 'api' ),
1445 'version-entrypoints-rest-php' => wfScript( 'rest' ),
1446 ];
1447
1448 $language = $this->getLanguage();
1449 $thAttributes = [
1450 'dir' => $language->getDir(),
1451 'lang' => $language->getHtmlCode(),
1452 'scope' => 'col'
1453 ];
1454
1455 $this->addTocSection( 'version-entrypoints', 'mw-version-entrypoints' );
1456
1457 $out = Html::element(
1458 'h2',
1459 [ 'id' => 'mw-version-entrypoints' ],
1460 $this->msg( 'version-entrypoints' )->text()
1461 ) .
1462 Html::openElement( 'table',
1463 [
1464 'class' => 'wikitable plainlinks',
1465 'id' => 'mw-version-entrypoints-table',
1466 'dir' => 'ltr',
1467 'lang' => 'en'
1468 ]
1469 ) .
1470 Html::openElement( 'thead' ) .
1471 Html::openElement( 'tr' ) .
1472 Html::element(
1473 'th',
1474 $thAttributes,
1475 $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1476 ) .
1477 Html::element(
1478 'th',
1479 $thAttributes,
1480 $this->msg( 'version-entrypoints-header-url' )->text()
1481 ) .
1482 Html::closeElement( 'tr' ) .
1483 Html::closeElement( 'thead' );
1484
1485 foreach ( $entryPoints as $message => $value ) {
1486 $url = $this->urlUtils->expand( $value, PROTO_RELATIVE );
1487 $out .= Html::openElement( 'tr' ) .
1488 Html::rawElement( 'td', [], $this->msg( $message )->parse() ) .
1489 Html::rawElement( 'td', [],
1490 Html::rawElement(
1491 'code',
1492 [],
1493 $this->msg( new RawMessage( "[$url $value]" ) )->parse()
1494 )
1495 ) .
1496 Html::closeElement( 'tr' );
1497 }
1498
1499 $out .= Html::closeElement( 'table' );
1500
1501 return $out;
1502 }
1503
1504 protected function getGroupName() {
1505 return 'wiki';
1506 }
1507}
1508
1513class_alias( SpecialVersion::class, 'SpecialVersion' );
getRequest()
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:204
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:537
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Load JSON files, and uses a Processor to extract information.
getAllThings()
Get credits information about all installed extensions and skins.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
Base class for language-specific code.
Definition Language.php:63
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:56
Variant of the Message class.
Some internal bits split of from Skin.php.
Definition Linker.php:65
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:158
static listParam(array $list, $type='text')
Definition Message.php:1348
ParserOutput is a rendering of a Content object or a message.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:156
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:46
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.
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 server-side 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)
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...
$header