MediaWiki REL1_37
SpecialVersion.php
Go to the documentation of this file.
1<?php
27
34
38 protected $firstExtOpened = false;
39
43 protected $coreId = '';
44
48 protected static $extensionTypes = false;
49
51 private $parser;
52
56 public function __construct( Parser $parser ) {
57 parent::__construct( 'Version' );
58 $this->parser = $parser;
59 }
60
68 public static function getCredits( ExtensionRegistry $reg, Config $conf ): array {
69 $credits = $conf->get( 'ExtensionCredits' );
70 foreach ( $reg->getAllThings() as $name => $credit ) {
71 $credits[$credit['type']][] = $credit;
72 }
73 return $credits;
74 }
75
80 public function execute( $par ) {
81 global $IP;
82 $config = $this->getConfig();
83 $credits = self::getCredits( ExtensionRegistry::getInstance(), $config );
84
85 $this->setHeaders();
86 $this->outputHeader();
87 $out = $this->getOutput();
88 $out->allowClickjacking();
89
90 // Explode the sub page information into useful bits
91 $parts = explode( '/', (string)$par );
92 $extNode = null;
93 if ( isset( $parts[1] ) ) {
94 $extName = str_replace( '_', ' ', $parts[1] );
95 // Find it!
96 foreach ( $credits as $group => $extensions ) {
97 foreach ( $extensions as $ext ) {
98 if ( isset( $ext['name'] ) && ( $ext['name'] === $extName ) ) {
99 $extNode = &$ext;
100 break 2;
101 }
102 }
103 }
104 if ( !$extNode ) {
105 $out->setStatusCode( 404 );
106 }
107 } else {
108 $extName = 'MediaWiki';
109 }
110
111 // Now figure out what to do
112 switch ( strtolower( $parts[0] ) ) {
113 case 'credits':
114 $out->addModuleStyles( 'mediawiki.special.version' );
115
116 $wikiText = '{{int:version-credits-not-found}}';
117 if ( $extName === 'MediaWiki' ) {
118 $wikiText = file_get_contents( $IP . '/CREDITS' );
119 // Put the contributor list into columns
120 $wikiText = str_replace(
121 [ '<!-- BEGIN CONTRIBUTOR LIST -->', '<!-- END CONTRIBUTOR LIST -->' ],
122 [ '<div class="mw-version-credits">', '</div>' ],
123 $wikiText );
124 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
125 $file = ExtensionInfo::getAuthorsFileName( dirname( $extNode['path'] ) );
126 if ( $file ) {
127 $wikiText = file_get_contents( $file );
128 if ( substr( $file, -4 ) === '.txt' ) {
129 $wikiText = Html::element(
130 'pre',
131 [
132 'lang' => 'en',
133 'dir' => 'ltr',
134 ],
135 $wikiText
136 );
137 }
138 }
139 }
140
141 $out->setPageTitle( $this->msg( 'version-credits-title', $extName ) );
142 $out->addWikiTextAsInterface( $wikiText );
143 break;
144
145 case 'license':
146 $out->setPageTitle( $this->msg( 'version-license-title', $extName ) );
147
148 $licenseFound = false;
149
150 if ( $extName === 'MediaWiki' ) {
151 $out->addWikiTextAsInterface(
152 file_get_contents( $IP . '/COPYING' )
153 );
154 $licenseFound = true;
155 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
156 $files = ExtensionInfo::getLicenseFileNames( dirname( $extNode['path'] ) );
157
158 if ( count( $files ) ) {
159 $licenseFound = true;
160 foreach ( $files as $file ) {
161 $out->addWikiTextAsInterface(
162 Html::element(
163 'pre',
164 [
165 'lang' => 'en',
166 'dir' => 'ltr',
167 ],
168 file_get_contents( $file )
169 )
170 );
171 }
172 }
173 }
174 if ( !$licenseFound ) {
175 $out->addWikiTextAsInterface( '{{int:version-license-not-found}}' );
176 }
177 break;
178 default:
179 $out->addModuleStyles( 'mediawiki.special.version' );
180 $out->addWikiTextAsInterface(
181 self::getMediaWikiCredits() .
182 self::softwareInformation() .
183 $this->getEntryPointInfo()
184 );
185 $out->addHTML(
186 $this->getSkinCredits( $credits ) .
187 $this->getExtensionCredits( $credits ) .
188 $this->getExternalLibraries( $credits ) .
189 $this->getParserTags() .
190 $this->getParserFunctionHooks()
191 );
192 $out->addWikiTextAsInterface( $this->getWgHooks() );
193 $out->addHTML( $this->IPInfo() );
194
195 break;
196 }
197 }
198
204 private static function getMediaWikiCredits() {
205 $ret = Xml::element(
206 'h2',
207 [ 'id' => 'mw-version-license' ],
208 wfMessage( 'version-license' )->text()
209 );
210
211 // This text is always left-to-right.
212 $ret .= '<div class="plainlinks">';
213 $ret .= "__NOTOC__
214 " . self::getCopyrightAndAuthorList() . "\n
215 " . '<div class="mw-version-license-info">' .
216 wfMessage( 'version-license-info' )->text() .
217 '</div>';
218 $ret .= '</div>';
219
220 return str_replace( "\t\t", '', $ret ) . "\n";
221 }
222
228 public static function getCopyrightAndAuthorList() {
229 global $wgLang;
230
231 if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
232 $othersLink = '[https://www.mediawiki.org/wiki/Special:Version/Credits ' .
233 wfMessage( 'version-poweredby-others' )->text() . ']';
234 } else {
235 $othersLink = '[[Special:Version/Credits|' .
236 wfMessage( 'version-poweredby-others' )->text() . ']]';
237 }
238
239 $translatorsLink = '[https://translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
240 wfMessage( 'version-poweredby-translators' )->text() . ']';
241
242 $authorList = [
243 'Magnus Manske', 'Brion Vibber', 'Lee Daniel Crocker',
244 'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
245 'Niklas Laxström', 'Domas Mituzas', 'Rob Church', 'Yuri Astrakhan',
246 'Aryeh Gregor', 'Aaron Schulz', 'Andrew Garrett', 'Raimond Spekking',
247 'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
248 'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
249 'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
250 'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', 'Brad Jorsch',
251 'Bartosz Dziewoński', 'Ed Sanders', 'Moriel Schottlender',
252 'Kunal Mehta', 'James D. Forrester', 'Brian Wolff', 'Adam Shorland',
253 'DannyS712', 'Ori Livneh',
254 $othersLink, $translatorsLink
255 ];
256
257 return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
258 $wgLang->listToText( $authorList ) )->text();
259 }
260
266 private static function getSoftwareInformation() {
268
269 // Put the software in an array of form 'name' => 'version'. All messages should
270 // be loaded here, so feel free to use wfMessage in the 'name'. Wikitext
271 // can be used both in the name and value.
272 $software = [
273 '[https://www.mediawiki.org/ MediaWiki]' => self::getVersionLinked(),
274 '[https://php.net/ PHP]' => PHP_VERSION . " (" . PHP_SAPI . ")",
275 $dbr->getSoftwareLink() => $dbr->getServerInfo(),
276 ];
277
278 if ( defined( 'INTL_ICU_VERSION' ) ) {
279 $software['[http://site.icu-project.org/ ICU]'] = INTL_ICU_VERSION;
280 }
281
282 // Allow a hook to add/remove items.
283 Hooks::runner()->onSoftwareInfo( $software );
284
285 return $software;
286 }
287
293 private static function softwareInformation() {
294 $out = Xml::element(
295 'h2',
296 [ 'id' => 'mw-version-software' ],
297 wfMessage( 'version-software' )->text()
298 ) .
299 Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ] ) .
300 "<tr>
301 <th>" . wfMessage( 'version-software-product' )->text() . "</th>
302 <th>" . wfMessage( 'version-software-version' )->text() . "</th>
303 </tr>\n";
304
305 foreach ( self::getSoftwareInformation() as $name => $version ) {
306 $out .= "<tr>
307 <td>" . $name . "</td>
308 <td dir=\"ltr\">" . $version . "</td>
309 </tr>\n";
310 }
311
312 return $out . Xml::closeElement( 'table' );
313 }
314
322 public static function getVersion( $flags = '', $lang = null ) {
323 global $IP;
324
325 $gitInfo = self::getGitHeadSha1( $IP );
326 if ( !$gitInfo ) {
327 $version = MW_VERSION;
328 } elseif ( $flags === 'nodb' ) {
329 $shortSha1 = substr( $gitInfo, 0, 7 );
330 $version = MW_VERSION . " ($shortSha1)";
331 } else {
332 $shortSha1 = substr( $gitInfo, 0, 7 );
333 $msg = wfMessage( 'parentheses' );
334 if ( $lang !== null ) {
335 $msg->inLanguage( $lang );
336 }
337 $shortSha1 = $msg->params( $shortSha1 )->escaped();
338 $version = MW_VERSION . ' ' . $shortSha1;
339 }
340
341 return $version;
342 }
343
351 public static function getVersionLinked() {
352 $gitVersion = self::getVersionLinkedGit();
353 if ( $gitVersion ) {
354 $v = $gitVersion;
355 } else {
356 $v = MW_VERSION; // fallback
357 }
358
359 return $v;
360 }
361
365 private static function getMWVersionLinked() {
366 $versionUrl = "";
367 if ( Hooks::runner()->onSpecialVersionVersionUrl( MW_VERSION, $versionUrl ) ) {
368 $versionParts = [];
369 preg_match( "/^(\d+\.\d+)/", MW_VERSION, $versionParts );
370 $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
371 }
372
373 return '[' . $versionUrl . ' ' . MW_VERSION . ']';
374 }
375
381 private static function getVersionLinkedGit() {
382 global $IP, $wgLang;
383
384 $gitInfo = new GitInfo( $IP );
385 $headSHA1 = $gitInfo->getHeadSHA1();
386 if ( !$headSHA1 ) {
387 return false;
388 }
389
390 $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
391
392 $gitHeadUrl = $gitInfo->getHeadViewUrl();
393 if ( $gitHeadUrl !== false ) {
394 $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
395 }
396
397 $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
398 if ( $gitHeadCommitDate ) {
399 $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( $gitHeadCommitDate, true );
400 }
401
402 return self::getMWVersionLinked() . " $shortSHA1";
403 }
404
415 public static function getExtensionTypes() {
416 if ( self::$extensionTypes === false ) {
417 self::$extensionTypes = [
418 'specialpage' => wfMessage( 'version-specialpages' )->text(),
419 'editor' => wfMessage( 'version-editors' )->text(),
420 'parserhook' => wfMessage( 'version-parserhooks' )->text(),
421 'variable' => wfMessage( 'version-variables' )->text(),
422 'media' => wfMessage( 'version-mediahandlers' )->text(),
423 'antispam' => wfMessage( 'version-antispam' )->text(),
424 'skin' => wfMessage( 'version-skins' )->text(),
425 'api' => wfMessage( 'version-api' )->text(),
426 'other' => wfMessage( 'version-other' )->text(),
427 ];
428
429 Hooks::runner()->onExtensionTypes( self::$extensionTypes );
430 }
431
432 return self::$extensionTypes;
433 }
434
444 public static function getExtensionTypeName( $type ) {
445 $types = self::getExtensionTypes();
446
447 return $types[$type] ?? $types['other'];
448 }
449
456 private function getExtensionCredits( array $credits ) {
457 if (
458 !$credits ||
459 // Skins are displayed separately, see getSkinCredits()
460 ( count( $credits ) === 1 && isset( $credits['skin'] ) )
461 ) {
462 return '';
463 }
464
465 $extensionTypes = self::getExtensionTypes();
466
467 $out = Xml::element(
468 'h2',
469 [ 'id' => 'mw-version-ext' ],
470 $this->msg( 'version-extensions' )->text()
471 ) .
472 Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-ext' ] );
473
474 // Make sure the 'other' type is set to an array.
475 if ( !array_key_exists( 'other', $credits ) ) {
476 $credits['other'] = [];
477 }
478
479 // Find all extensions that do not have a valid type and give them the type 'other'.
480 foreach ( $credits as $type => $extensions ) {
481 if ( !array_key_exists( $type, $extensionTypes ) ) {
482 $credits['other'] = array_merge( $credits['other'], $extensions );
483 }
484 }
485
486 $this->firstExtOpened = false;
487 // Loop through the extension categories to display their extensions in the list.
488 foreach ( $extensionTypes as $type => $message ) {
489 // Skins have a separate section
490 if ( $type !== 'other' && $type !== 'skin' ) {
491 $out .= $this->getExtensionCategory( $type, $message, $credits[$type] ?? [] );
492 }
493 }
494
495 // We want the 'other' type to be last in the list.
496 $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'], $credits['other'] );
497
498 $out .= Xml::closeElement( 'table' );
499
500 return $out;
501 }
502
509 private function getSkinCredits( array $credits ) {
510 if ( !isset( $credits['skin'] ) || count( $credits['skin'] ) === 0 ) {
511 return '';
512 }
513
514 $out = Xml::element(
515 'h2',
516 [ 'id' => 'mw-version-skin' ],
517 $this->msg( 'version-skins' )->text()
518 ) .
519 Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-skin' ] );
520
521 $this->firstExtOpened = false;
522 $out .= $this->getExtensionCategory( 'skin', null, $credits['skin'] );
523
524 $out .= Xml::closeElement( 'table' );
525
526 return $out;
527 }
528
535 protected function getExternalLibraries( array $credits ) {
536 global $IP;
537 $paths = [
538 "$IP/vendor/composer/installed.json"
539 ];
540
541 $extensionTypes = self::getExtensionTypes();
542 foreach ( $extensionTypes as $type => $message ) {
543 if ( !isset( $credits[$type] ) || $credits[$type] === [] ) {
544 continue;
545 }
546 foreach ( $credits[$type] as $extension ) {
547 if ( !isset( $extension['path'] ) ) {
548 continue;
549 }
550 $paths[] = dirname( $extension['path'] ) . '/vendor/composer/installed.json';
551 }
552 }
553
554 $dependencies = [];
555
556 foreach ( $paths as $path ) {
557 if ( !file_exists( $path ) ) {
558 continue;
559 }
560
561 $installed = new ComposerInstalled( $path );
562
563 $dependencies += $installed->getInstalledDependencies();
564 }
565
566 if ( $dependencies === [] ) {
567 return '';
568 }
569
570 ksort( $dependencies );
571
572 $out = Html::element(
573 'h2',
574 [ 'id' => 'mw-version-libraries' ],
575 $this->msg( 'version-libraries' )->text()
576 );
577 $out .= Html::openElement(
578 'table',
579 [ 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries' ]
580 );
581 $out .= Html::openElement( 'tr' )
582 . Html::element( 'th', [], $this->msg( 'version-libraries-library' )->text() )
583 . Html::element( 'th', [], $this->msg( 'version-libraries-version' )->text() )
584 . Html::element( 'th', [], $this->msg( 'version-libraries-license' )->text() )
585 . Html::element( 'th', [], $this->msg( 'version-libraries-description' )->text() )
586 . Html::element( 'th', [], $this->msg( 'version-libraries-authors' )->text() )
587 . Html::closeElement( 'tr' );
588
589 foreach ( $dependencies as $name => $info ) {
590 if ( !is_array( $info ) || strpos( $info['type'], 'mediawiki-' ) === 0 ) {
591 // Skip any extensions or skins since they'll be listed
592 // in their proper section
593 continue;
594 }
595 $authors = array_map( static function ( $arr ) {
596 // If a homepage is set, link to it
597 if ( isset( $arr['homepage'] ) ) {
598 return "[{$arr['homepage']} {$arr['name']}]";
599 }
600 return $arr['name'];
601 }, $info['authors'] );
602 $authors = $this->listAuthors( $authors, false, "$IP/vendor/$name" );
603
604 // We can safely assume that the libraries' names and descriptions
605 // are written in English and aren't going to be translated,
606 // so set appropriate lang and dir attributes
607 $out .= Html::openElement( 'tr', [
608 // Add an anchor so docs can link easily to the version of
609 // this specific library
610 'id' => Sanitizer::escapeIdForAttribute(
611 "mw-version-library-$name"
612 ) ] )
613 . Html::rawElement(
614 'td',
615 [],
617 "https://packagist.org/packages/$name", $name,
618 true, '',
619 [ 'class' => 'mw-version-library-name' ]
620 )
621 )
622 . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
623 // @phan-suppress-next-line SecurityCheck-DoubleEscaped false positive
624 . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['licenses'] ) )
625 . Html::element( 'td', [ 'lang' => 'en', 'dir' => 'ltr' ], $info['description'] )
626 . Html::rawElement( 'td', [], $authors )
627 . Html::closeElement( 'tr' );
628 }
629 $out .= Html::closeElement( 'table' );
630
631 return $out;
632 }
633
639 protected function getParserTags() {
640 $tags = $this->parser->getTags();
641
642 if ( count( $tags ) ) {
643 $out = Html::rawElement(
644 'h2',
645 [
646 'class' => 'mw-headline plainlinks',
647 'id' => 'mw-version-parser-extensiontags',
648 ],
649 // @phan-suppress-next-line SecurityCheck-DoubleEscaped Using false for escape is safe
651 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
652 $this->msg( 'version-parser-extensiontags' )->parse(),
653 false /* msg()->parse() already escapes */
654 )
655 );
656
657 array_walk( $tags, static function ( &$value ) {
658 // Bidirectional isolation improves readability in RTL wikis
659 $value = Html::element(
660 'bdi',
661 // Prevent < and > from slipping to another line
662 [
663 'style' => 'white-space: nowrap;',
664 ],
665 "<$value>"
666 );
667 } );
668
669 $out .= $this->listToText( $tags );
670 } else {
671 $out = '';
672 }
673
674 return $out;
675 }
676
682 protected function getParserFunctionHooks() {
683 $fhooks = $this->parser->getFunctionHooks();
684 if ( count( $fhooks ) ) {
685 $out = Html::rawElement(
686 'h2',
687 [
688 'class' => 'mw-headline plainlinks',
689 'id' => 'mw-version-parser-function-hooks',
690 ],
691 // @phan-suppress-next-line SecurityCheck-DoubleEscaped Using false for escape is safe
693 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
694 $this->msg( 'version-parser-function-hooks' )->parse(),
695 false /* msg()->parse() already escapes */
696 )
697 );
698
699 $out .= $this->listToText( $fhooks );
700 } else {
701 $out = '';
702 }
703
704 return $out;
705 }
706
716 protected function getExtensionCategory( $type, $message, array $creditsGroup ) {
717 $config = $this->getConfig();
718 $credits = $config->get( 'ExtensionCredits' );
719
720 $out = '';
721
722 if ( $creditsGroup ) {
723 $out .= $this->openExtType( $message, 'credits-' . $type );
724
725 usort( $creditsGroup, [ $this, 'compare' ] );
726
727 foreach ( $creditsGroup as $extension ) {
728 $out .= $this->getCreditsForExtension( $type, $extension );
729 }
730 }
731
732 return $out;
733 }
734
741 public function compare( $a, $b ) {
742 return $this->getLanguage()->lc( $a['name'] ) <=> $this->getLanguage()->lc( $b['name'] );
743 }
744
763 public function getCreditsForExtension( $type, array $extension ) {
764 $out = $this->getOutput();
765
766 // We must obtain the information for all the bits and pieces!
767 // ... such as extension names and links
768 if ( isset( $extension['namemsg'] ) ) {
769 // Localized name of extension
770 $extensionName = $this->msg( $extension['namemsg'] )->text();
771 } elseif ( isset( $extension['name'] ) ) {
772 // Non localized version
773 $extensionName = $extension['name'];
774 } else {
775 $extensionName = $this->msg( 'version-no-ext-name' )->text();
776 }
777
778 if ( isset( $extension['url'] ) ) {
779 $extensionNameLink = Linker::makeExternalLink(
780 $extension['url'],
781 $extensionName,
782 true,
783 '',
784 [ 'class' => 'mw-version-ext-name' ]
785 );
786 } else {
787 $extensionNameLink = htmlspecialchars( $extensionName );
788 }
789
790 // ... and the version information
791 // If the extension path is set we will check that directory for GIT
792 // metadata in an attempt to extract date and vcs commit metadata.
793 $canonicalVersion = '&ndash;';
794 $extensionPath = null;
795 $vcsVersion = null;
796 $vcsLink = null;
797 $vcsDate = null;
798
799 if ( isset( $extension['version'] ) ) {
800 $canonicalVersion = $out->parseInlineAsInterface( $extension['version'] );
801 }
802
803 if ( isset( $extension['path'] ) ) {
804 global $IP;
805 $extensionPath = dirname( $extension['path'] );
806 if ( $this->coreId == '' ) {
807 wfDebug( 'Looking up core head id' );
808 $coreHeadSHA1 = self::getGitHeadSha1( $IP );
809 if ( $coreHeadSHA1 ) {
810 $this->coreId = $coreHeadSHA1;
811 }
812 }
813 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
814 $memcKey = $cache->makeKey(
815 'specialversion-ext-version-text', $extension['path'], $this->coreId
816 );
817 list( $vcsVersion, $vcsLink, $vcsDate ) = $cache->get( $memcKey );
818
819 if ( !$vcsVersion ) {
820 wfDebug( "Getting VCS info for extension {$extension['name']}" );
821 $gitInfo = new GitInfo( $extensionPath );
822 $vcsVersion = $gitInfo->getHeadSHA1();
823 if ( $vcsVersion !== false ) {
824 $vcsVersion = substr( $vcsVersion, 0, 7 );
825 $vcsLink = $gitInfo->getHeadViewUrl();
826 $vcsDate = $gitInfo->getHeadCommitDate();
827 }
828 $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
829 } else {
830 wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" );
831 }
832 }
833
834 $versionString = Html::rawElement(
835 'span',
836 [ 'class' => 'mw-version-ext-version' ],
837 $canonicalVersion
838 );
839
840 if ( $vcsVersion ) {
841 if ( $vcsLink ) {
842 $vcsVerString = Linker::makeExternalLink(
843 $vcsLink,
844 $this->msg( 'version-version', $vcsVersion )->text(),
845 true,
846 '',
847 [ 'class' => 'mw-version-ext-vcs-version' ]
848 );
849 } else {
850 $vcsVerString = Html::element( 'span',
851 [ 'class' => 'mw-version-ext-vcs-version' ],
852 "({$vcsVersion})"
853 );
854 }
855 $versionString .= " {$vcsVerString}";
856
857 if ( $vcsDate ) {
858 $versionString .= ' ' . Html::element( 'span', [
859 'class' => 'mw-version-ext-vcs-timestamp',
860 'dir' => $this->getLanguage()->getDir(),
861 ], $this->getLanguage()->timeanddate( $vcsDate, true ) );
862 }
863 $versionString = Html::rawElement( 'span',
864 [ 'class' => 'mw-version-ext-meta-version' ],
865 $versionString
866 );
867 }
868
869 // ... and license information; if a license file exists we
870 // will link to it
871 $licenseLink = '';
872 if ( isset( $extension['name'] ) ) {
873 $licenseName = null;
874 if ( isset( $extension['license-name'] ) ) {
875 $licenseName = new HtmlArmor( $out->parseInlineAsInterface( $extension['license-name'] ) );
876 } elseif ( ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
877 $licenseName = $this->msg( 'version-ext-license' )->text();
878 }
879 if ( $licenseName !== null ) {
880 $licenseLink = $this->getLinkRenderer()->makeLink(
881 $this->getPageTitle( 'License/' . $extension['name'] ),
882 $licenseName,
883 [
884 'class' => 'mw-version-ext-license',
885 'dir' => 'auto',
886 ]
887 );
888 }
889 }
890
891 // ... and generate the description; which can be a parameterized l10n message
892 // in the form [ <msgname>, <parameter>, <parameter>... ] or just a straight
893 // up string
894 if ( isset( $extension['descriptionmsg'] ) ) {
895 // Localized description of extension
896 $descriptionMsg = $extension['descriptionmsg'];
897
898 if ( is_array( $descriptionMsg ) ) {
899 $descriptionMsgKey = array_shift( $descriptionMsg );
900 $descriptionMsg = array_map( 'htmlspecialchars', $descriptionMsg ); // For sanity
901 $description = $this->msg( $descriptionMsgKey, ...$descriptionMsg )->text();
902 } else {
903 $description = $this->msg( $descriptionMsg )->text();
904 }
905 } elseif ( isset( $extension['description'] ) ) {
906 // Non localized version
907 $description = $extension['description'];
908 } else {
909 $description = '';
910 }
911 $description = $out->parseInlineAsInterface( $description );
912
913 // ... now get the authors for this extension
914 $authors = $extension['author'] ?? [];
915 $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
916
917 // Finally! Create the table
918 $html = Html::openElement( 'tr', [
919 'class' => 'mw-version-ext',
920 'id' => Sanitizer::escapeIdForAttribute( 'mw-version-ext-' . $type . '-' . $extension['name'] )
921 ]
922 );
923
924 $html .= Html::rawElement( 'td', [], $extensionNameLink );
925 $html .= Html::rawElement( 'td', [], $versionString );
926 $html .= Html::rawElement( 'td', [], $licenseLink );
927 $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-description' ], $description );
928 $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-authors' ], $authors );
929
930 $html .= Html::closeElement( 'tr' );
931
932 return $html;
933 }
934
940 private function getWgHooks() {
942
943 if ( $wgSpecialVersionShowHooks && count( $wgHooks ) ) {
944 $myWgHooks = $wgHooks;
945 ksort( $myWgHooks );
946
947 $ret = [];
948 $ret[] = '== {{int:version-hooks}} ==';
949 $ret[] = Html::openElement( 'table', [ 'class' => 'wikitable', 'id' => 'sv-hooks' ] );
950 $ret[] = Html::openElement( 'tr' );
951 $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-name' )->text() );
952 $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-subscribedby' )->text() );
953 $ret[] = Html::closeElement( 'tr' );
954
955 foreach ( $myWgHooks as $hook => $hooks ) {
956 $ret[] = Html::openElement( 'tr' );
957 $ret[] = Html::element( 'td', [], $hook );
958 // @phan-suppress-next-line SecurityCheck-DoubleEscaped false positive
959 $ret[] = Html::element( 'td', [], $this->listToText( $hooks ) );
960 $ret[] = Html::closeElement( 'tr' );
961 }
962
963 $ret[] = Html::closeElement( 'table' );
964
965 return implode( "\n", $ret );
966 }
967
968 return '';
969 }
970
971 private function openExtType( $text = null, $name = null ) {
972 $out = '';
973
974 $opt = [ 'colspan' => 5 ];
975 if ( $this->firstExtOpened ) {
976 // Insert a spacing line
977 $out .= Html::rawElement( 'tr', [ 'class' => 'sv-space' ],
978 Html::element( 'td', $opt )
979 );
980 }
981 $this->firstExtOpened = true;
982
983 if ( $name ) {
984 $opt['id'] = "sv-$name";
985 }
986
987 if ( $text !== null ) {
988 $out .= Html::rawElement( 'tr', [],
989 Html::element( 'th', $opt, $text )
990 );
991 }
992
993 $firstHeadingMsg = ( $name === 'credits-skin' )
994 ? 'version-skin-colheader-name'
995 : 'version-ext-colheader-name';
996 $out .= Html::openElement( 'tr' );
997 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
998 $this->msg( $firstHeadingMsg )->text() );
999 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1000 $this->msg( 'version-ext-colheader-version' )->text() );
1001 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1002 $this->msg( 'version-ext-colheader-license' )->text() );
1003 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1004 $this->msg( 'version-ext-colheader-description' )->text() );
1005 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1006 $this->msg( 'version-ext-colheader-credits' )->text() );
1007 $out .= Html::closeElement( 'tr' );
1008
1009 return $out;
1010 }
1011
1017 private function IPInfo() {
1018 $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
1019
1020 return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
1021 }
1022
1044 public function listAuthors( $authors, $extName, $extDir ) {
1045 $hasOthers = false;
1046 $linkRenderer = $this->getLinkRenderer();
1047
1048 $list = [];
1049 $authors = (array)$authors;
1050
1051 // Special case: if the authors array has only one item and it is "...",
1052 // it should not be rendered as the "version-poweredby-others" i18n msg,
1053 // but rather as "version-poweredby-various" i18n msg instead.
1054 if ( count( $authors ) === 1 && $authors[0] === '...' ) {
1055 // Link to the extension's or skin's AUTHORS or CREDITS file, if there is
1056 // such a file; otherwise just return the i18n msg as-is
1057 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1058 return $linkRenderer->makeLink(
1059 $this->getPageTitle( "Credits/$extName" ),
1060 $this->msg( 'version-poweredby-various' )->text()
1061 );
1062 } else {
1063 return $this->msg( 'version-poweredby-various' )->escaped();
1064 }
1065 }
1066
1067 // Otherwise, if we have an actual array that has more than one item,
1068 // process each array item as usual
1069 foreach ( $authors as $item ) {
1070 if ( $item == '...' ) {
1071 $hasOthers = true;
1072
1073 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1074 $text = $linkRenderer->makeLink(
1075 $this->getPageTitle( "Credits/$extName" ),
1076 $this->msg( 'version-poweredby-others' )->text()
1077 );
1078 } else {
1079 $text = $this->msg( 'version-poweredby-others' )->escaped();
1080 }
1081 $list[] = $text;
1082 } elseif ( substr( $item, -5 ) == ' ...]' ) {
1083 $hasOthers = true;
1084 $list[] = $this->getOutput()->parseInlineAsInterface(
1085 substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
1086 );
1087 } else {
1088 $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1089 }
1090 }
1091
1092 if ( $extName && !$hasOthers && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1093 $list[] = $linkRenderer->makeLink(
1094 $this->getPageTitle( "Credits/$extName" ),
1095 $this->msg( 'version-poweredby-others' )->text()
1096 );
1097 }
1098
1099 return $this->listToText( $list, false );
1100 }
1101
1114 public static function getExtAuthorsFileName( $extDir ) {
1115 wfDeprecated( __METHOD__, '1.35' );
1116 return ExtensionInfo::getAuthorsFileName( $extDir );
1117 }
1118
1131 public static function getExtLicenseFileName( $extDir ) {
1132 wfDeprecated( __METHOD__, '1.35' );
1133 $licenses = ExtensionInfo::getLicenseFileNames( $extDir );
1134 if ( count( $licenses ) === 0 ) {
1135 return false;
1136 }
1137 return $licenses[0];
1138 }
1139
1148 public function listToText( $list, $sort = true ) {
1149 if ( !count( $list ) ) {
1150 return '';
1151 }
1152 if ( $sort ) {
1153 sort( $list );
1154 }
1155
1156 return $this->getLanguage()
1157 ->listToText( array_map( [ __CLASS__, 'arrayToString' ], $list ) );
1158 }
1159
1168 public static function arrayToString( $list ) {
1169 if ( is_array( $list ) && count( $list ) == 1 ) {
1170 $list = $list[0];
1171 }
1172 if ( $list instanceof Closure ) {
1173 // Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$"
1174 return 'Closure';
1175 } elseif ( is_object( $list ) ) {
1176 $class = wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1177
1178 return $class;
1179 } elseif ( !is_array( $list ) ) {
1180 return $list;
1181 } else {
1182 if ( is_object( $list[0] ) ) {
1183 $class = get_class( $list[0] );
1184 } else {
1185 $class = $list[0];
1186 }
1187
1188 return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1189 }
1190 }
1191
1196 public static function getGitHeadSha1( $dir ) {
1197 $repo = new GitInfo( $dir );
1198
1199 return $repo->getHeadSHA1();
1200 }
1201
1206 public static function getGitCurrentBranch( $dir ) {
1207 $repo = new GitInfo( $dir );
1208 return $repo->getCurrentBranch();
1209 }
1210
1215 public function getEntryPointInfo() {
1216 $config = $this->getConfig();
1217 $scriptPath = $config->get( 'ScriptPath' ) ?: '/';
1218
1219 $entryPoints = [
1220 'version-entrypoints-articlepath' => $config->get( 'ArticlePath' ),
1221 'version-entrypoints-scriptpath' => $scriptPath,
1222 'version-entrypoints-index-php' => wfScript( 'index' ),
1223 'version-entrypoints-api-php' => wfScript( 'api' ),
1224 'version-entrypoints-rest-php' => wfScript( 'rest' ),
1225 ];
1226
1227 $language = $this->getLanguage();
1228 $thAttribures = [
1229 'dir' => $language->getDir(),
1230 'lang' => $language->getHtmlCode()
1231 ];
1232 $out = Html::element(
1233 'h2',
1234 [ 'id' => 'mw-version-entrypoints' ],
1235 $this->msg( 'version-entrypoints' )->text()
1236 ) .
1237 Html::openElement( 'table',
1238 [
1239 'class' => 'wikitable plainlinks',
1240 'id' => 'mw-version-entrypoints-table',
1241 'dir' => 'ltr',
1242 'lang' => 'en'
1243 ]
1244 ) .
1245 Html::openElement( 'tr' ) .
1246 Html::element(
1247 'th',
1248 $thAttribures,
1249 $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1250 ) .
1251 Html::element(
1252 'th',
1253 $thAttribures,
1254 $this->msg( 'version-entrypoints-header-url' )->text()
1255 ) .
1256 Html::closeElement( 'tr' );
1257
1258 foreach ( $entryPoints as $message => $value ) {
1259 $url = wfExpandUrl( $value, PROTO_RELATIVE );
1260 $out .= Html::openElement( 'tr' ) .
1261 // ->plain() looks like it should be ->parse(), but this function
1262 // returns wikitext, not HTML, boo
1263 Html::rawElement( 'td', [], $this->msg( $message )->plain() ) .
1264 Html::rawElement( 'td', [], Html::rawElement( 'code', [], "[$url $value]" ) ) .
1265 Html::closeElement( 'tr' );
1266 }
1267
1268 $out .= Html::closeElement( 'table' );
1269
1270 return $out;
1271 }
1272
1273 protected function getGroupName() {
1274 return 'wiki';
1275 }
1276}
$wgHooks
Global list of hooks.
$wgSpecialVersionShowHooks
Show the contents of $wgHooks in Special:Version.
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:194
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
$wgLang
Definition Setup.php:831
$IP
Definition WebStart.php:49
Reads an installed.json file and provides accessors to get what is installed.
The Registry loads JSON files, and uses a Processor to extract information from them.
getAllThings()
Get credits information about all installed extensions and skins.
@newable
Definition GitInfo.php:34
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition Linker.php:1011
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:91
Parent class for all special pages.
Give information about the version of MediaWiki, PHP, the DB and extensions.
static getVersionLinked()
Return a wikitext-formatted string of the MediaWiki version with a link to the Git SHA1 of head if av...
static getExtensionTypeName( $type)
Returns the internationalized name for an extension type.
IPInfo()
Get information about client's IP address.
static getGitHeadSha1( $dir)
__construct(Parser $parser)
getParserFunctionHooks()
Obtains a list of installed parser function hooks and the associated H2 header.
execute( $par)
main()
getExternalLibraries(array $credits)
Generate an HTML table for external libraries that are installed.
listToText( $list, $sort=true)
Convert an array of items into a list for display.
getEntryPointInfo()
Get the list of entry points and their URLs.
getWgHooks()
Generate wikitext showing hooks in $wgHooks.
static getCredits(ExtensionRegistry $reg, Config $conf)
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
getExtensionCategory( $type, $message, array $creditsGroup)
Creates and returns the HTML for a single extension category.
getExtensionCredits(array $credits)
Generate wikitext showing the name, URL, author and description of each extension.
listAuthors( $authors, $extName, $extDir)
Return a formatted unsorted list of authors.
static getSoftwareInformation()
Helper for self::softwareInformation().
string $coreId
The current rev id/SHA hash of MediaWiki core.
static softwareInformation()
Returns HTML showing the third party software versions (apache, php, mysql).
static getVersionLinkedGit()
static getExtensionTypes()
Returns an array with the base extension types.
openExtType( $text=null, $name=null)
getCreditsForExtension( $type, array $extension)
Creates and formats a version line for a single extension.
static getExtLicenseFileName( $extDir)
Obtains the full path of an extensions COPYING or LICENSE file if one exists.
static getMediaWikiCredits()
Returns wiki text showing the license information.
getParserTags()
Obtains a list of installed parser tags and the associated H2 header.
static getExtAuthorsFileName( $extDir)
Obtains the full path of an extensions AUTHORS or CREDITS file if one exists.
compare( $a, $b)
Callback to sort extensions by type.
static getVersion( $flags='', $lang=null)
Return a string of the MediaWiki version with Git revision if available.
static getCopyrightAndAuthorList()
Get the "MediaWiki is copyright 2001-20xx by lots of cool folks" text.
static getGitCurrentBranch( $dir)
static string[] false $extensionTypes
Lazy initialized key/value with message content.
static getMWVersionLinked()
static arrayToString( $list)
Convert an array or object to a string for display.
getSkinCredits(array $credits)
Generate wikitext showing the name, URL, author and description of each skin.
Interface for configuration instances.
Definition Config.php:30
$cache
Definition mcc.php:33
const DB_REPLICA
Definition defines.php:25
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42
if(!is_readable( $file)) $ext
Definition router.php:48
if(!isset( $args[0])) $lang