MediaWiki master
SpecialVersion.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Specials;
24
25use Closure;
27use HtmlArmor;
28use Language;
46use ObjectCache;
48use Symfony\Component\Yaml\Yaml;
50use Wikimedia\Parsoid\Core\SectionMetadata;
51use Wikimedia\Parsoid\Core\TOCData;
53
60
64 protected $firstExtOpened = false;
65
69 protected $coreId = '';
70
74 protected static $extensionTypes = false;
75
77 protected $tocData;
78
80 protected $tocIndex;
81
83 protected $tocSection;
84
86 protected $tocSubSection;
87
88 private ParserFactory $parserFactory;
89 private UrlUtils $urlUtils;
90 private IConnectionProvider $dbProvider;
91
97 public function __construct(
98 ParserFactory $parserFactory,
99 UrlUtils $urlUtils,
100 IConnectionProvider $dbProvider
101 ) {
102 parent::__construct( 'Version' );
103 $this->parserFactory = $parserFactory;
104 $this->urlUtils = $urlUtils;
105 $this->dbProvider = $dbProvider;
106 }
107
115 public static function getCredits( ExtensionRegistry $reg, Config $conf ): array {
116 $credits = $conf->get( MainConfigNames::ExtensionCredits );
117 foreach ( $reg->getAllThings() as $credit ) {
118 $credits[$credit['type']][] = $credit;
119 }
120 return $credits;
121 }
122
126 public function execute( $par ) {
127 $config = $this->getConfig();
128 $credits = self::getCredits( ExtensionRegistry::getInstance(), $config );
129
130 $this->setHeaders();
131 $this->outputHeader();
132 $out = $this->getOutput();
133 $out->setPreventClickjacking( false );
134
135 // Explode the sub page information into useful bits
136 $parts = explode( '/', (string)$par );
137 $extNode = null;
138 if ( isset( $parts[1] ) ) {
139 $extName = str_replace( '_', ' ', $parts[1] );
140 // Find it!
141 foreach ( $credits as $extensions ) {
142 foreach ( $extensions as $ext ) {
143 if ( isset( $ext['name'] ) && ( $ext['name'] === $extName ) ) {
144 $extNode = &$ext;
145 break 2;
146 }
147 }
148 }
149 if ( !$extNode ) {
150 $out->setStatusCode( 404 );
151 }
152 } else {
153 $extName = 'MediaWiki';
154 }
155
156 // Now figure out what to do
157 switch ( strtolower( $parts[0] ) ) {
158 case 'credits':
159 $out->addModuleStyles( 'mediawiki.special' );
160
161 $wikiText = '{{int:version-credits-not-found}}';
162 if ( $extName === 'MediaWiki' ) {
163 $wikiText = file_get_contents( MW_INSTALL_PATH . '/CREDITS' );
164 // Put the contributor list into columns
165 $wikiText = str_replace(
166 [ '<!-- BEGIN CONTRIBUTOR LIST -->', '<!-- END CONTRIBUTOR LIST -->' ],
167 [ '<div class="mw-version-credits">', '</div>' ],
168 $wikiText
169 );
170 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
171 $file = ExtensionInfo::getAuthorsFileName( dirname( $extNode['path'] ) );
172 if ( $file ) {
173 $wikiText = file_get_contents( $file );
174 if ( str_ends_with( $file, '.txt' ) ) {
175 $wikiText = Html::element(
176 'pre',
177 [
178 'lang' => 'en',
179 'dir' => 'ltr',
180 ],
181 $wikiText
182 );
183 }
184 }
185 }
186
187 $out->setPageTitleMsg( $this->msg( 'version-credits-title' )->plaintextParams( $extName ) );
188 $out->addWikiTextAsInterface( $wikiText );
189 break;
190
191 case 'license':
192 $out->setPageTitleMsg( $this->msg( 'version-license-title' )->plaintextParams( $extName ) );
193
194 $licenseFound = false;
195
196 if ( $extName === 'MediaWiki' ) {
197 $out->addWikiTextAsInterface(
198 file_get_contents( MW_INSTALL_PATH . '/COPYING' )
199 );
200 $licenseFound = true;
201 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
202 $files = ExtensionInfo::getLicenseFileNames( dirname( $extNode['path'] ) );
203 if ( $files ) {
204 $licenseFound = true;
205 foreach ( $files as $file ) {
206 $out->addWikiTextAsInterface(
208 'pre',
209 [
210 'lang' => 'en',
211 'dir' => 'ltr',
212 ],
213 file_get_contents( $file )
214 )
215 );
216 }
217 }
218 }
219 if ( !$licenseFound ) {
220 $out->addWikiTextAsInterface( '{{int:version-license-not-found}}' );
221 }
222 break;
223
224 default:
225 $out->addModuleStyles( 'mediawiki.special' );
226
227 $out->addHTML( $this->getMediaWikiCredits() );
228
229 $this->tocData = new TOCData();
230 $this->tocIndex = 0;
231 $this->tocSection = 0;
232 $this->tocSubSection = 0;
233
234 // Build the page contents (this also fills in TOCData)
235 $sections = [
236 $this->softwareInformation(),
237 $this->getEntryPointInfo(),
238 $this->getSkinCredits( $credits ),
239 $this->getExtensionCredits( $credits ),
240 $this->getExternalLibraries( $credits ),
241 $this->getClientSideLibraries(),
242 $this->getParserTags(),
243 $this->getParserFunctionHooks(),
244 $this->getHooks(),
245 $this->IPInfo(),
246 ];
247
248 // Insert TOC first
249 $pout = new ParserOutput;
250 $pout->setTOCData( $this->tocData );
251 $pout->setOutputFlag( ParserOutputFlags::SHOW_TOC );
252 $pout->setRawText( Parser::TOC_PLACEHOLDER );
253 $out->addParserOutput( $pout );
254
255 // Insert contents
256 foreach ( $sections as $content ) {
257 $out->addHTML( $content );
258 }
259
260 break;
261 }
262 }
263
271 private function addTocSection( $labelMsg, $id ) {
272 $this->tocIndex++;
273 $this->tocSection++;
274 $this->tocSubSection = 0;
275 $this->tocData->addSection( new SectionMetadata(
276 1,
277 2,
278 $this->msg( $labelMsg )->escaped(),
279 $this->getLanguage()->formatNum( $this->tocSection ),
280 (string)$this->tocIndex,
281 null,
282 null,
283 $id,
284 $id
285 ) );
286 }
287
295 private function addTocSubSection( $label, $id ) {
296 $this->tocIndex++;
297 $this->tocSubSection++;
298 $this->tocData->addSection( new SectionMetadata(
299 2,
300 3,
301 htmlspecialchars( $label ),
302 // See Parser::localizeTOC
303 $this->getLanguage()->formatNum( $this->tocSection ) . '.' .
304 $this->getLanguage()->formatNum( $this->tocSubSection ),
305 (string)$this->tocIndex,
306 null,
307 null,
308 $id,
309 $id
310 ) );
311 }
312
318 private function getMediaWikiCredits() {
319 // No TOC entry for this heading, we treat it like the lede section
320
321 $ret = Html::element(
322 'h2',
323 [ 'id' => 'mw-version-license' ],
324 $this->msg( 'version-license' )->text()
325 );
326
327 $ret .= Html::rawElement( 'div', [ 'class' => 'plainlinks' ],
328 $this->msg( new RawMessage( self::getCopyrightAndAuthorList() ) )->parseAsBlock() .
329 Html::rawElement( 'div', [ 'class' => 'mw-version-license-info' ],
330 $this->msg( 'version-license-info' )->parseAsBlock()
331 )
332 );
333
334 return $ret;
335 }
336
343 public static function getCopyrightAndAuthorList() {
344 if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
345 $othersLink = '[https://www.mediawiki.org/wiki/Special:Version/Credits ' .
346 wfMessage( 'version-poweredby-others' )->plain() . ']';
347 } else {
348 $othersLink = '[[Special:Version/Credits|' .
349 wfMessage( 'version-poweredby-others' )->plain() . ']]';
350 }
351
352 $translatorsLink = '[https://translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
353 wfMessage( 'version-poweredby-translators' )->plain() . ']';
354
355 $authorList = [
356 'Magnus Manske', 'Brooke Vibber', 'Lee Daniel Crocker',
357 'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
358 'Niklas Laxström', 'Domas Mituzas', 'Rob Church', 'Yuri Astrakhan',
359 'Aryeh Gregor', 'Aaron Schulz', 'Andrew Garrett', 'Raimond Spekking',
360 'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
361 'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
362 'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
363 'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', 'Brad Jorsch',
364 'Bartosz Dziewoński', 'Ed Sanders', 'Moriel Schottlender',
365 'Kunal Mehta', 'James D. Forrester', 'Brian Wolff', 'Adam Shorland',
366 'DannyS712', 'Ori Livneh', 'Max Semenik', 'Amir Sarabadani',
367 'Derk-Jan Hartman', 'Petr Pchelko',
368 $othersLink, $translatorsLink
369 ];
370
371 return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
372 Message::listParam( $authorList ) )->plain();
373 }
374
380 private function getSoftwareInformation() {
381 $dbr = $this->dbProvider->getReplicaDatabase();
382
383 // Put the software in an array of form 'name' => 'version'. All messages should
384 // be loaded here, so feel free to use wfMessage in the 'name'. Wikitext
385 // can be used both in the name and value.
386 $software = [
387 '[https://www.mediawiki.org/ MediaWiki]' => self::getVersionLinked(),
388 '[https://php.net/ PHP]' => PHP_VERSION . " (" . PHP_SAPI . ")",
389 '[https://icu.unicode.org/ ICU]' => INTL_ICU_VERSION,
390 $dbr->getSoftwareLink() => $dbr->getServerInfo(),
391 ];
392
393 // T339915: If wikidiff2 is installed, show version
394 if ( phpversion( "wikidiff2" ) ) {
395 $software[ '[https://www.mediawiki.org/wiki/Wikidiff2 wikidiff2]' ] = phpversion( "wikidiff2" );
396 }
397
398 // Allow a hook to add/remove items.
399 $this->getHookRunner()->onSoftwareInfo( $software );
400
401 return $software;
402 }
403
409 private function softwareInformation() {
410 $this->addTocSection( 'version-software', 'mw-version-software' );
411
412 $out = Html::element(
413 'h2',
414 [ 'id' => 'mw-version-software' ],
415 $this->msg( 'version-software' )->text()
416 );
417
418 $out .= Html::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ] );
419
420 $out .= Html::rawElement( 'tr', [],
421 Html::element( 'th', [], $this->msg( 'version-software-product' )->text() ) .
422 Html::element( 'th', [], $this->msg( 'version-software-version' )->text() )
423 );
424
425 foreach ( $this->getSoftwareInformation() as $name => $version ) {
426 $out .= Html::rawElement(
427 'tr',
428 [],
429 Html::rawElement( 'td', [], $this->msg( new RawMessage( $name ) )->parse() ) .
430 Html::rawElement( 'td', [ 'dir' => 'ltr' ], $this->msg( new RawMessage( $version ) )->parse() )
431 );
432 }
433
434 $out .= Html::closeElement( 'table' );
435
436 return $out;
437 }
438
448 public static function getVersion( $flags = '', $lang = null ) {
449 $gitInfo = GitInfo::repo()->getHeadSHA1();
450 if ( !$gitInfo ) {
451 $version = MW_VERSION;
452 } elseif ( $flags === 'nodb' ) {
453 $shortSha1 = substr( $gitInfo, 0, 7 );
454 $version = MW_VERSION . " ($shortSha1)";
455 } else {
456 $shortSha1 = substr( $gitInfo, 0, 7 );
457 $msg = wfMessage( 'parentheses' );
458 if ( $lang !== null ) {
459 $msg->inLanguage( $lang );
460 }
461 $shortSha1 = $msg->params( $shortSha1 )->text();
462 $version = MW_VERSION . ' ' . $shortSha1;
463 }
464
465 return $version;
466 }
467
475 public static function getVersionLinked() {
476 return self::getVersionLinkedGit() ?: MW_VERSION;
477 }
478
482 private static function getMWVersionLinked() {
483 $versionUrl = "";
484 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
485 if ( $hookRunner->onSpecialVersionVersionUrl( MW_VERSION, $versionUrl ) ) {
486 $versionParts = [];
487 preg_match( "/^(\d+\.\d+)/", MW_VERSION, $versionParts );
488 $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
489 }
490
491 return '[' . $versionUrl . ' ' . MW_VERSION . ']';
492 }
493
499 private static function getVersionLinkedGit() {
500 global $wgLang;
501
502 $gitInfo = new GitInfo( MW_INSTALL_PATH );
503 $headSHA1 = $gitInfo->getHeadSHA1();
504 if ( !$headSHA1 ) {
505 return false;
506 }
507
508 $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
509
510 $gitHeadUrl = $gitInfo->getHeadViewUrl();
511 if ( $gitHeadUrl !== false ) {
512 $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
513 }
514
515 $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
516 if ( $gitHeadCommitDate ) {
517 $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( (string)$gitHeadCommitDate, true );
518 }
519
520 return self::getMWVersionLinked() . " $shortSHA1";
521 }
522
532 public static function getExtensionTypes(): array {
533 if ( self::$extensionTypes === false ) {
534 self::$extensionTypes = [
535 'specialpage' => wfMessage( 'version-specialpages' )->text(),
536 'editor' => wfMessage( 'version-editors' )->text(),
537 'parserhook' => wfMessage( 'version-parserhooks' )->text(),
538 'variable' => wfMessage( 'version-variables' )->text(),
539 'media' => wfMessage( 'version-mediahandlers' )->text(),
540 'antispam' => wfMessage( 'version-antispam' )->text(),
541 'skin' => wfMessage( 'version-skins' )->text(),
542 'api' => wfMessage( 'version-api' )->text(),
543 'other' => wfMessage( 'version-other' )->text(),
544 ];
545
546 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
547 ->onExtensionTypes( self::$extensionTypes );
548 }
549
550 return self::$extensionTypes;
551 }
552
562 public static function getExtensionTypeName( $type ) {
563 $types = self::getExtensionTypes();
564
565 return $types[$type] ?? $types['other'];
566 }
567
574 private function getExtensionCredits( array $credits ) {
575 $extensionTypes = self::getExtensionTypes();
576
577 $this->addTocSection( 'version-extensions', 'mw-version-ext' );
578
579 $out = Html::element(
580 'h2',
581 [ 'id' => 'mw-version-ext' ],
582 $this->msg( 'version-extensions' )->text()
583 );
584
585 if (
586 !$credits ||
587 // Skins are displayed separately, see getSkinCredits()
588 ( count( $credits ) === 1 && isset( $credits['skin'] ) )
589 ) {
590 $out .= Html::element(
591 'p',
592 [],
593 $this->msg( 'version-extensions-no-ext' )->text()
594 );
595
596 return $out;
597 }
598
599 $out .= Html::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-ext' ] );
600
601 // Find all extensions that do not have a valid type and give them the type 'other'.
602 $credits['other'] ??= [];
603 foreach ( $credits as $type => $extensions ) {
604 if ( !array_key_exists( $type, $extensionTypes ) ) {
605 $credits['other'] = array_merge( $credits['other'], $extensions );
606 }
607 }
608
609 $this->firstExtOpened = false;
610 // Loop through the extension categories to display their extensions in the list.
611 foreach ( $extensionTypes as $type => $text ) {
612 // Skins have a separate section
613 if ( $type !== 'other' && $type !== 'skin' ) {
614 $out .= $this->getExtensionCategory( $type, $text, $credits[$type] ?? [] );
615 }
616 }
617
618 // We want the 'other' type to be last in the list.
619 $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'], $credits['other'] );
620
621 $out .= Html::closeElement( 'table' );
622
623 return $out;
624 }
625
632 private function getSkinCredits( array $credits ) {
633 $this->addTocSection( 'version-skins', 'mw-version-skin' );
634
635 $out = Html::element(
636 'h2',
637 [ 'id' => 'mw-version-skin' ],
638 $this->msg( 'version-skins' )->text()
639 );
640
641 if ( !isset( $credits['skin'] ) || !$credits['skin'] ) {
642 $out .= Html::element(
643 'p',
644 [],
645 $this->msg( 'version-skins-no-skin' )->text()
646 );
647
648 return $out;
649 }
650 $out .= Html::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-skin' ] );
651
652 $this->firstExtOpened = false;
653 $out .= $this->getExtensionCategory( 'skin', null, $credits['skin'] );
654
655 $out .= Html::closeElement( 'table' );
656
657 return $out;
658 }
659
666 protected function getExternalLibraries( array $credits ) {
667 $paths = [
668 MW_INSTALL_PATH . '/vendor/composer/installed.json'
669 ];
670
671 $extensionTypes = self::getExtensionTypes();
672 foreach ( $extensionTypes as $type => $message ) {
673 if ( !isset( $credits[$type] ) || $credits[$type] === [] ) {
674 continue;
675 }
676 foreach ( $credits[$type] as $extension ) {
677 if ( !isset( $extension['path'] ) ) {
678 continue;
679 }
680 $paths[] = dirname( $extension['path'] ) . '/vendor/composer/installed.json';
681 }
682 }
683
684 $dependencies = [];
685
686 foreach ( $paths as $path ) {
687 if ( !file_exists( $path ) ) {
688 continue;
689 }
690
691 $installed = new ComposerInstalled( $path );
692
693 $dependencies += $installed->getInstalledDependencies();
694 }
695
696 if ( $dependencies === [] ) {
697 return '';
698 }
699
700 ksort( $dependencies );
701
702 $this->addTocSection( 'version-libraries', 'mw-version-libraries' );
703
704 $out = Html::element(
705 'h2',
706 [ 'id' => 'mw-version-libraries' ],
707 $this->msg( 'version-libraries' )->text()
708 );
709 $out .= Html::openElement(
710 'table',
711 [ 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries' ]
712 );
713 $out .= Html::openElement( 'tr' )
714 . Html::element( 'th', [], $this->msg( 'version-libraries-library' )->text() )
715 . Html::element( 'th', [], $this->msg( 'version-libraries-version' )->text() )
716 . Html::element( 'th', [], $this->msg( 'version-libraries-license' )->text() )
717 . Html::element( 'th', [], $this->msg( 'version-libraries-description' )->text() )
718 . Html::element( 'th', [], $this->msg( 'version-libraries-authors' )->text() )
719 . Html::closeElement( 'tr' );
720
721 foreach ( $dependencies as $name => $info ) {
722 if ( !is_array( $info ) || str_starts_with( $info['type'], 'mediawiki-' ) ) {
723 // Skip any extensions or skins since they'll be listed
724 // in their proper section
725 continue;
726 }
727 $authors = array_map( static function ( $arr ) {
728 return new HtmlArmor( isset( $arr['homepage'] ) ?
729 Html::element( 'a', [ 'href' => $arr['homepage'] ], $arr['name'] ) :
730 htmlspecialchars( $arr['name'] )
731 );
732 }, $info['authors'] );
733 $authors = $this->listAuthors( $authors, false, MW_INSTALL_PATH . "/vendor/$name" );
734
735 // We can safely assume that the libraries' names and descriptions
736 // are written in English and aren't going to be translated,
737 // so set appropriate lang and dir attributes
738 $out .= Html::openElement( 'tr', [
739 // Add an anchor so docs can link easily to the version of
740 // this specific library
741 'id' => Sanitizer::escapeIdForAttribute(
742 "mw-version-library-$name"
743 ) ] )
744 . Html::rawElement(
745 'td',
746 [],
747 Linker::makeExternalLink(
748 "https://packagist.org/packages/$name", $name,
749 true, '',
750 [ 'class' => 'mw-version-library-name' ]
751 )
752 )
753 . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
754 // @phan-suppress-next-line SecurityCheck-DoubleEscaped See FIXME in listToText
755 . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['licenses'] ) )
756 . Html::element( 'td', [ 'lang' => 'en', 'dir' => 'ltr' ], $info['description'] )
757 . Html::rawElement( 'td', [], $authors )
758 . Html::closeElement( 'tr' );
759 }
760 $out .= Html::closeElement( 'table' );
761
762 return $out;
763 }
764
770 public static function parseForeignResources() {
771 $registryDirs = [ 'MediaWiki' => MW_INSTALL_PATH . '/resources/lib' ]
772 + ExtensionRegistry::getInstance()->getAttribute( 'ForeignResourcesDir' );
773
774 $modules = [];
775 foreach ( $registryDirs as $source => $registryDir ) {
776 $foreignResources = Yaml::parseFile( "$registryDir/foreign-resources.yaml" );
777 foreach ( $foreignResources as $name => $module ) {
778 $key = $name . $module['version'];
779 if ( isset( $modules[$key] ) ) {
780 $modules[$key]['source'][] = $source;
781 continue;
782 }
783 $modules[$key] = $module + [ 'name' => $name, 'source' => [ $source ] ];
784 }
785 }
786 ksort( $modules );
787 return $modules;
788 }
789
795 private function getClientSideLibraries() {
796 $this->addTocSection( 'version-libraries-client', 'mw-version-libraries-client' );
797
798 $out = Html::element(
799 'h2',
800 [ 'id' => 'mw-version-libraries-client' ],
801 $this->msg( 'version-libraries-client' )->text()
802 );
803 $out .= Html::openElement(
804 'table',
805 [ 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries-client' ]
806 );
807 $out .= Html::openElement( 'tr' )
808 . Html::element( 'th', [], $this->msg( 'version-libraries-library' )->text() )
809 . Html::element( 'th', [], $this->msg( 'version-libraries-version' )->text() )
810 . Html::element( 'th', [], $this->msg( 'version-libraries-license' )->text() )
811 . Html::element( 'th', [], $this->msg( 'version-libraries-authors' )->text() )
812 . Html::element( 'th', [], $this->msg( 'version-libraries-source' )->text() )
813 . Html::closeElement( 'tr' );
814
815 foreach ( self::parseForeignResources() as $name => $info ) {
816 // We can safely assume that the libraries' names and descriptions
817 // are written in English and aren't going to be translated,
818 // so set appropriate lang and dir attributes
819 $out .= Html::openElement( 'tr', [
820 // Add an anchor so docs can link easily to the version of
821 // this specific library
822 'id' => Sanitizer::escapeIdForAttribute(
823 "mw-version-library-$name"
824 ) ] )
825 . Html::rawElement(
826 'td',
827 [],
828 Linker::makeExternalLink(
829 $info['homepage'], $info['name'],
830 true, '',
831 [ 'class' => 'mw-version-library-name' ]
832 )
833 )
834 . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
835 . Html::element( 'td', [ 'dir' => 'auto' ], $info['license'] )
836 . Html::element( 'td', [ 'dir' => 'auto' ], $info['authors'] ?? '—' )
837 // @phan-suppress-next-line SecurityCheck-DoubleEscaped See FIXME in listToText
838 . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['source'] ) )
839 . Html::closeElement( 'tr' );
840 }
841 $out .= Html::closeElement( 'table' );
842
843 return $out;
844 }
845
851 protected function getParserTags() {
852 $tags = $this->parserFactory->getMainInstance()->getTags();
853 if ( !$tags ) {
854 return '';
855 }
856
857 $this->addTocSection( 'version-parser-extensiontags', 'mw-version-parser-extensiontags' );
858
859 $out = Html::rawElement(
860 'h2',
861 [ 'id' => 'mw-version-parser-extensiontags' ],
862 Html::rawElement(
863 'span',
864 [ 'class' => 'plainlinks' ],
865 Linker::makeExternalLink(
866 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
867 $this->msg( 'version-parser-extensiontags' )->text()
868 )
869 )
870 );
871
872 array_walk( $tags, static function ( &$value ) {
873 // Bidirectional isolation improves readability in RTL wikis
874 $value = Html::element(
875 'bdi',
876 // Prevent < and > from slipping to another line
877 [
878 'style' => 'white-space: nowrap;',
879 ],
880 "<$value>"
881 );
882 } );
883
884 $out .= $this->listToText( $tags );
885
886 return $out;
887 }
888
894 protected function getParserFunctionHooks() {
895 $funcHooks = $this->parserFactory->getMainInstance()->getFunctionHooks();
896 if ( !$funcHooks ) {
897 return '';
898 }
899
900 $this->addTocSection( 'version-parser-function-hooks', 'mw-version-parser-function-hooks' );
901
902 $out = Html::rawElement(
903 'h2',
904 [ 'id' => 'mw-version-parser-function-hooks' ],
905 Html::rawElement(
906 'span',
907 [ 'class' => 'plainlinks' ],
908 Linker::makeExternalLink(
909 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
910 $this->msg( 'version-parser-function-hooks' )->text()
911 )
912 )
913 );
914
915 $out .= $this->listToText( $funcHooks );
916
917 return $out;
918 }
919
929 protected function getExtensionCategory( $type, ?string $text, array $creditsGroup ) {
930 $out = '';
931
932 if ( $creditsGroup ) {
933 $out .= $this->openExtType( $text, 'credits-' . $type );
934
935 usort( $creditsGroup, [ $this, 'compare' ] );
936
937 foreach ( $creditsGroup as $extension ) {
938 $out .= $this->getCreditsForExtension( $type, $extension );
939 }
940 }
941
942 return $out;
943 }
944
951 public function compare( $a, $b ) {
952 return $this->getLanguage()->lc( $a['name'] ) <=> $this->getLanguage()->lc( $b['name'] );
953 }
954
973 public function getCreditsForExtension( $type, array $extension ) {
974 $out = $this->getOutput();
975
976 // We must obtain the information for all the bits and pieces!
977 // ... such as extension names and links
978 if ( isset( $extension['namemsg'] ) ) {
979 // Localized name of extension
980 $extensionName = $this->msg( $extension['namemsg'] )->text();
981 } elseif ( isset( $extension['name'] ) ) {
982 // Non localized version
983 $extensionName = $extension['name'];
984 } else {
985 $extensionName = $this->msg( 'version-no-ext-name' )->text();
986 }
987
988 if ( isset( $extension['url'] ) ) {
989 $extensionNameLink = Linker::makeExternalLink(
990 $extension['url'],
991 $extensionName,
992 true,
993 '',
994 [ 'class' => 'mw-version-ext-name' ]
995 );
996 } else {
997 $extensionNameLink = htmlspecialchars( $extensionName );
998 }
999
1000 // ... and the version information
1001 // If the extension path is set we will check that directory for GIT
1002 // metadata in an attempt to extract date and vcs commit metadata.
1003 $canonicalVersion = '&ndash;';
1004 $extensionPath = null;
1005 $vcsVersion = null;
1006 $vcsLink = null;
1007 $vcsDate = null;
1008
1009 if ( isset( $extension['version'] ) ) {
1010 $canonicalVersion = $out->parseInlineAsInterface( $extension['version'] );
1011 }
1012
1013 if ( isset( $extension['path'] ) ) {
1014 $extensionPath = dirname( $extension['path'] );
1015 if ( $this->coreId == '' ) {
1016 wfDebug( 'Looking up core head id' );
1017 $coreHeadSHA1 = GitInfo::repo()->getHeadSHA1();
1018 if ( $coreHeadSHA1 ) {
1019 $this->coreId = $coreHeadSHA1;
1020 }
1021 }
1022 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
1023 $memcKey = $cache->makeKey(
1024 'specialversion-ext-version-text', $extension['path'], $this->coreId
1025 );
1026 [ $vcsVersion, $vcsLink, $vcsDate ] = $cache->get( $memcKey );
1027
1028 if ( !$vcsVersion ) {
1029 wfDebug( "Getting VCS info for extension {$extension['name']}" );
1030 $gitInfo = new GitInfo( $extensionPath );
1031 $vcsVersion = $gitInfo->getHeadSHA1();
1032 if ( $vcsVersion !== false ) {
1033 $vcsVersion = substr( $vcsVersion, 0, 7 );
1034 $vcsLink = $gitInfo->getHeadViewUrl();
1035 $vcsDate = $gitInfo->getHeadCommitDate();
1036 }
1037 $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
1038 } else {
1039 wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" );
1040 }
1041 }
1042
1043 $versionString = Html::rawElement(
1044 'span',
1045 [ 'class' => 'mw-version-ext-version' ],
1046 $canonicalVersion
1047 );
1048
1049 if ( $vcsVersion ) {
1050 if ( $vcsLink ) {
1051 $vcsVerString = Linker::makeExternalLink(
1052 $vcsLink,
1053 $this->msg( 'version-version', $vcsVersion )->text(),
1054 true,
1055 '',
1056 [ 'class' => 'mw-version-ext-vcs-version' ]
1057 );
1058 } else {
1059 $vcsVerString = Html::element( 'span',
1060 [ 'class' => 'mw-version-ext-vcs-version' ],
1061 "({$vcsVersion})"
1062 );
1063 }
1064 $versionString .= " {$vcsVerString}";
1065
1066 if ( $vcsDate ) {
1067 $versionString .= ' ' . Html::element( 'span', [
1068 'class' => 'mw-version-ext-vcs-timestamp',
1069 'dir' => $this->getLanguage()->getDir(),
1070 ], $this->getLanguage()->timeanddate( $vcsDate, true ) );
1071 }
1072 $versionString = Html::rawElement( 'span',
1073 [ 'class' => 'mw-version-ext-meta-version' ],
1074 $versionString
1075 );
1076 }
1077
1078 // ... and license information; if a license file exists we
1079 // will link to it
1080 $licenseLink = '';
1081 if ( isset( $extension['name'] ) ) {
1082 $licenseName = null;
1083 if ( isset( $extension['license-name'] ) ) {
1084 $licenseName = new HtmlArmor( $out->parseInlineAsInterface( $extension['license-name'] ) );
1085 } elseif ( $extensionPath !== null && ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
1086 $licenseName = $this->msg( 'version-ext-license' )->text();
1087 }
1088 if ( $licenseName !== null ) {
1089 $licenseLink = $this->getLinkRenderer()->makeLink(
1090 $this->getPageTitle( 'License/' . $extension['name'] ),
1091 $licenseName,
1092 [
1093 'class' => 'mw-version-ext-license',
1094 'dir' => 'auto',
1095 ]
1096 );
1097 }
1098 }
1099
1100 // ... and generate the description; which can be a parameterized l10n message
1101 // in the form [ <msgname>, <parameter>, <parameter>... ] or just a straight
1102 // up string
1103 if ( isset( $extension['descriptionmsg'] ) ) {
1104 // Localized description of extension
1105 $descriptionMsg = $extension['descriptionmsg'];
1106
1107 if ( is_array( $descriptionMsg ) ) {
1108 $descriptionMsgKey = array_shift( $descriptionMsg );
1109 $descriptionMsg = array_map( 'htmlspecialchars', $descriptionMsg );
1110 $description = $this->msg( $descriptionMsgKey, ...$descriptionMsg )->text();
1111 } else {
1112 $description = $this->msg( $descriptionMsg )->text();
1113 }
1114 } elseif ( isset( $extension['description'] ) ) {
1115 // Non localized version
1116 $description = $extension['description'];
1117 } else {
1118 $description = '';
1119 }
1120 $description = $out->parseInlineAsInterface( $description );
1121
1122 // ... now get the authors for this extension
1123 $authors = $extension['author'] ?? [];
1124 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable path is set when there is a name
1125 $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
1126
1127 // Finally! Create the table
1128 $html = Html::openElement( 'tr', [
1129 'class' => 'mw-version-ext',
1130 'id' => Sanitizer::escapeIdForAttribute( 'mw-version-ext-' . $type . '-' . $extension['name'] )
1131 ]
1132 );
1133
1134 $html .= Html::rawElement( 'td', [], $extensionNameLink );
1135 $html .= Html::rawElement( 'td', [], $versionString );
1136 $html .= Html::rawElement( 'td', [], $licenseLink );
1137 $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-description' ], $description );
1138 $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-authors' ], $authors );
1139
1140 $html .= Html::closeElement( 'tr' );
1141
1142 return $html;
1143 }
1144
1150 private function getHooks() {
1151 if ( !$this->getConfig()->get( MainConfigNames::SpecialVersionShowHooks ) ) {
1152 return '';
1153 }
1154
1155 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1156 $hookNames = $hookContainer->getHookNames();
1157
1158 if ( !$hookNames ) {
1159 return '';
1160 }
1161
1162 sort( $hookNames );
1163
1164 $ret = [];
1165 $this->addTocSection( 'version-hooks', 'mw-version-hooks' );
1166 $ret[] = Html::element(
1167 'h2',
1168 [ 'id' => 'mw-version-hooks' ],
1169 $this->msg( 'version-hooks' )->text()
1170 );
1171 $ret[] = Html::openElement( 'table', [ 'class' => 'wikitable', 'id' => 'sv-hooks' ] );
1172 $ret[] = Html::openElement( 'tr' );
1173 $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-name' )->text() );
1174 $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-subscribedby' )->text() );
1175 $ret[] = Html::closeElement( 'tr' );
1176
1177 foreach ( $hookNames as $name ) {
1178 $handlers = $hookContainer->getHandlerDescriptions( $name );
1179
1180 $ret[] = Html::openElement( 'tr' );
1181 $ret[] = Html::element( 'td', [], $name );
1182 // @phan-suppress-next-line SecurityCheck-DoubleEscaped See FIXME in listToText
1183 $ret[] = Html::element( 'td', [], $this->listToText( $handlers ) );
1184 $ret[] = Html::closeElement( 'tr' );
1185 }
1186
1187 $ret[] = Html::closeElement( 'table' );
1188
1189 return implode( "\n", $ret );
1190 }
1191
1192 private function openExtType( string $text = null, string $name = null ) {
1193 $out = '';
1194
1195 $opt = [ 'colspan' => 5 ];
1196 if ( $this->firstExtOpened ) {
1197 // Insert a spacing line
1198 $out .= Html::rawElement( 'tr', [ 'class' => 'sv-space' ],
1199 Html::element( 'td', $opt )
1200 );
1201 }
1202 $this->firstExtOpened = true;
1203
1204 if ( $name ) {
1205 $opt['id'] = "sv-$name";
1206 }
1207
1208 if ( $text !== null ) {
1209 $out .= Html::rawElement( 'tr', [],
1210 Html::element( 'th', $opt, $text )
1211 );
1212 }
1213
1214 if ( $name && $text !== null ) {
1215 $this->addTocSubSection( $text, "sv-$name" );
1216 }
1217
1218 $firstHeadingMsg = ( $name === 'credits-skin' )
1219 ? 'version-skin-colheader-name'
1220 : 'version-ext-colheader-name';
1221 $out .= Html::openElement( 'tr' );
1222 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1223 $this->msg( $firstHeadingMsg )->text() );
1224 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1225 $this->msg( 'version-ext-colheader-version' )->text() );
1226 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1227 $this->msg( 'version-ext-colheader-license' )->text() );
1228 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1229 $this->msg( 'version-ext-colheader-description' )->text() );
1230 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1231 $this->msg( 'version-ext-colheader-credits' )->text() );
1232 $out .= Html::closeElement( 'tr' );
1233
1234 return $out;
1235 }
1236
1242 private function IPInfo() {
1243 $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
1244
1245 return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
1246 }
1247
1268 public function listAuthors( $authors, $extName, $extDir ): string {
1269 $hasOthers = false;
1270 $linkRenderer = $this->getLinkRenderer();
1271
1272 $list = [];
1273 $authors = (array)$authors;
1274
1275 // Special case: if the authors array has only one item and it is "...",
1276 // it should not be rendered as the "version-poweredby-others" i18n msg,
1277 // but rather as "version-poweredby-various" i18n msg instead.
1278 if ( count( $authors ) === 1 && $authors[0] === '...' ) {
1279 // Link to the extension's or skin's AUTHORS or CREDITS file, if there is
1280 // such a file; otherwise just return the i18n msg as-is
1281 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1282 return $linkRenderer->makeLink(
1283 $this->getPageTitle( "Credits/$extName" ),
1284 $this->msg( 'version-poweredby-various' )->text()
1285 );
1286 } else {
1287 return $this->msg( 'version-poweredby-various' )->escaped();
1288 }
1289 }
1290
1291 // Otherwise, if we have an actual array that has more than one item,
1292 // process each array item as usual
1293 foreach ( $authors as $item ) {
1294 if ( $item instanceof HtmlArmor ) {
1295 $list[] = HtmlArmor::getHtml( $item );
1296 } elseif ( $item === '...' ) {
1297 $hasOthers = true;
1298
1299 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1300 $text = $linkRenderer->makeLink(
1301 $this->getPageTitle( "Credits/$extName" ),
1302 $this->msg( 'version-poweredby-others' )->text()
1303 );
1304 } else {
1305 $text = $this->msg( 'version-poweredby-others' )->escaped();
1306 }
1307 $list[] = $text;
1308 } elseif ( str_ends_with( $item, ' ...]' ) ) {
1309 $hasOthers = true;
1310 $list[] = $this->getOutput()->parseInlineAsInterface(
1311 substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
1312 );
1313 } else {
1314 $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1315 }
1316 }
1317
1318 if ( $extName && !$hasOthers && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1319 $list[] = $linkRenderer->makeLink(
1320 $this->getPageTitle( "Credits/$extName" ),
1321 $this->msg( 'version-poweredby-others' )->text()
1322 );
1323 }
1324
1325 return $this->listToText( $list, false );
1326 }
1327
1337 private function listToText( array $list, bool $sort = true ): string {
1338 if ( !$list ) {
1339 return '';
1340 }
1341 if ( $sort ) {
1342 sort( $list );
1343 }
1344
1345 return $this->getLanguage()
1346 ->listToText( array_map( [ __CLASS__, 'arrayToString' ], $list ) );
1347 }
1348
1358 public static function arrayToString( $list ) {
1359 if ( is_array( $list ) && count( $list ) == 1 ) {
1360 $list = $list[0];
1361 }
1362 if ( $list instanceof Closure ) {
1363 // Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$"
1364 return 'Closure';
1365 } elseif ( is_object( $list ) ) {
1366 return wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1367 } elseif ( !is_array( $list ) ) {
1368 return $list;
1369 } else {
1370 if ( is_object( $list[0] ) ) {
1371 $class = get_class( $list[0] );
1372 } else {
1373 $class = $list[0];
1374 }
1375
1376 return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1377 }
1378 }
1379
1385 public static function getGitHeadSha1( $dir ) {
1386 wfDeprecated( __METHOD__, '1.41' );
1387 return ( new GitInfo( $dir ) )->getHeadSHA1();
1388 }
1389
1394 public function getEntryPointInfo() {
1395 $config = $this->getConfig();
1396 $scriptPath = $config->get( MainConfigNames::ScriptPath ) ?: '/';
1397
1398 $entryPoints = [
1399 'version-entrypoints-articlepath' => $config->get( MainConfigNames::ArticlePath ),
1400 'version-entrypoints-scriptpath' => $scriptPath,
1401 'version-entrypoints-index-php' => wfScript( 'index' ),
1402 'version-entrypoints-api-php' => wfScript( 'api' ),
1403 'version-entrypoints-rest-php' => wfScript( 'rest' ),
1404 ];
1405
1406 $language = $this->getLanguage();
1407 $thAttributes = [
1408 'dir' => $language->getDir(),
1409 'lang' => $language->getHtmlCode()
1410 ];
1411
1412 $this->addTocSection( 'version-entrypoints', 'mw-version-entrypoints' );
1413
1414 $out = Html::element(
1415 'h2',
1416 [ 'id' => 'mw-version-entrypoints' ],
1417 $this->msg( 'version-entrypoints' )->text()
1418 ) .
1419 Html::openElement( 'table',
1420 [
1421 'class' => 'wikitable plainlinks',
1422 'id' => 'mw-version-entrypoints-table',
1423 'dir' => 'ltr',
1424 'lang' => 'en'
1425 ]
1426 ) .
1427 Html::openElement( 'tr' ) .
1428 Html::element(
1429 'th',
1430 $thAttributes,
1431 $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1432 ) .
1433 Html::element(
1434 'th',
1435 $thAttributes,
1436 $this->msg( 'version-entrypoints-header-url' )->text()
1437 ) .
1438 Html::closeElement( 'tr' );
1439
1440 foreach ( $entryPoints as $message => $value ) {
1441 $url = $this->urlUtils->expand( $value, PROTO_RELATIVE );
1442 $out .= Html::openElement( 'tr' ) .
1443 Html::rawElement( 'td', [], $this->msg( $message )->parse() ) .
1444 Html::rawElement( 'td', [],
1445 Html::rawElement(
1446 'code',
1447 [],
1448 $this->msg( new RawMessage( "[$url $value]" ) )->parse()
1449 )
1450 ) .
1451 Html::closeElement( 'tr' );
1452 }
1453
1454 $out .= Html::closeElement( 'table' );
1455
1456 return $out;
1457 }
1458
1459 protected function getGroupName() {
1460 return 'wiki';
1461 }
1462}
1463
1468class_alias( SpecialVersion::class, 'SpecialVersion' );
getRequest()
const CACHE_ANYTHING
Definition Defines.php:85
const MW_VERSION
The running version of MediaWiki.
Definition Defines.php:36
const PROTO_RELATIVE
Definition Defines.php:204
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfScript( $script='index')
Get the URL path to a MediaWiki entry point.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgLang
Definition Setup.php:536
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Load JSON files, and uses a Processor to extract information.
getAllThings()
Get credits information about all installed extensions and skins.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
Base class for language-specific code.
Definition Language.php:63
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
Variant of the Message class.
Some internal bits split of from Skin.php.
Definition Linker.php:65
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:158
static listParam(array $list, $type='text')
Definition Message.php:1338
ParserOutput is a rendering of a Content object or a message.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:156
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:46
Parent class for all special pages.
Version information about MediaWiki (core, extensions, libs), PHP, and the database.
getParserFunctionHooks()
Obtains a list of installed parser function hooks and the associated H2 header.
string $coreId
The current rev id/SHA hash of MediaWiki core.
static arrayToString( $list)
Convert an array or object to a string for display.
static getVersion( $flags='', $lang=null)
Return a string of the MediaWiki version with Git revision if available.
listAuthors( $authors, $extName, $extDir)
Return a formatted unsorted list of authors.
static getExtensionTypeName( $type)
Returns the internationalized name for an extension type.
getCreditsForExtension( $type, array $extension)
Creates and formats a version line for a single extension.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
getExtensionCategory( $type, ?string $text, array $creditsGroup)
Creates and returns the HTML for a single extension category.
static getVersionLinked()
Return a wikitext-formatted string of the MediaWiki version with a link to the Git SHA1 of head if av...
static getExtensionTypes()
Returns an array with the base extension types.
compare( $a, $b)
Callback to sort extensions by type.
__construct(ParserFactory $parserFactory, UrlUtils $urlUtils, IConnectionProvider $dbProvider)
static getCopyrightAndAuthorList()
Get the "MediaWiki is copyright 2001-20xx by lots of cool folks" text.
getEntryPointInfo()
Get the list of entry points and their URLs.
static getCredits(ExtensionRegistry $reg, Config $conf)
static string[] false $extensionTypes
Lazy initialized key/value with message content.
getParserTags()
Obtains a list of installed parser tags and the associated H2 header.
getExternalLibraries(array $credits)
Generate an HTML table for external libraries that are installed.
Fetch status information from a local git repository.
Definition GitInfo.php:47
Library for creating and parsing MW-style timestamps.
A service to expand, parse, and otherwise manipulate URLs.
Definition UrlUtils.php:16
Reads an installed.json file and provides accessors to get what is installed.
Interface for configuration instances.
Definition Config.php:32
Provide primary and replica IDatabase connections.
$source
element(SerializerNode $parent, SerializerNode $node, $contents)
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...