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