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