MediaWiki  master
SpecialVersion.php
Go to the documentation of this file.
1 <?php
30 
36 class SpecialVersion extends SpecialPage {
37 
41  protected $firstExtOpened = false;
42 
46  protected $coreId = '';
47 
51  protected static $extensionTypes = false;
52 
54  private $parser;
55 
57  private $urlUtils;
58 
63  public function __construct(
64  Parser $parser,
65  UrlUtils $urlUtils
66  ) {
67  parent::__construct( 'Version' );
68  $this->parser = $parser;
69  $this->urlUtils = $urlUtils;
70  }
71 
79  public static function getCredits( ExtensionRegistry $reg, Config $conf ): array {
80  $credits = $conf->get( MainConfigNames::ExtensionCredits );
81  foreach ( $reg->getAllThings() as $credit ) {
82  $credits[$credit['type']][] = $credit;
83  }
84  return $credits;
85  }
86 
91  public function execute( $par ) {
92  global $IP;
93  $config = $this->getConfig();
94  $credits = self::getCredits( ExtensionRegistry::getInstance(), $config );
95 
96  $this->setHeaders();
97  $this->outputHeader();
98  $out = $this->getOutput();
99  $out->setPreventClickjacking( false );
100 
101  // Explode the sub page information into useful bits
102  $parts = explode( '/', (string)$par );
103  $extNode = null;
104  if ( isset( $parts[1] ) ) {
105  $extName = str_replace( '_', ' ', $parts[1] );
106  // Find it!
107  foreach ( $credits as $extensions ) {
108  foreach ( $extensions as $ext ) {
109  if ( isset( $ext['name'] ) && ( $ext['name'] === $extName ) ) {
110  $extNode = &$ext;
111  break 2;
112  }
113  }
114  }
115  if ( !$extNode ) {
116  $out->setStatusCode( 404 );
117  }
118  } else {
119  $extName = 'MediaWiki';
120  }
121 
122  // Now figure out what to do
123  switch ( strtolower( $parts[0] ) ) {
124  case 'credits':
125  $out->addModuleStyles( 'mediawiki.special' );
126 
127  $wikiText = '{{int:version-credits-not-found}}';
128  if ( $extName === 'MediaWiki' ) {
129  $wikiText = file_get_contents( $IP . '/CREDITS' );
130  // Put the contributor list into columns
131  $wikiText = str_replace(
132  [ '<!-- BEGIN CONTRIBUTOR LIST -->', '<!-- END CONTRIBUTOR LIST -->' ],
133  [ '<div class="mw-version-credits">', '</div>' ],
134  $wikiText );
135  } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
136  $file = ExtensionInfo::getAuthorsFileName( dirname( $extNode['path'] ) );
137  if ( $file ) {
138  $wikiText = file_get_contents( $file );
139  if ( str_ends_with( $file, '.txt' ) ) {
140  $wikiText = Html::element(
141  'pre',
142  [
143  'lang' => 'en',
144  'dir' => 'ltr',
145  ],
146  $wikiText
147  );
148  }
149  }
150  }
151 
152  $out->setPageTitle( $this->msg( 'version-credits-title', $extName ) );
153  $out->addWikiTextAsInterface( $wikiText );
154  break;
155 
156  case 'license':
157  $out->setPageTitle( $this->msg( 'version-license-title', $extName ) );
158 
159  $licenseFound = false;
160 
161  if ( $extName === 'MediaWiki' ) {
162  $out->addWikiTextAsInterface(
163  file_get_contents( $IP . '/COPYING' )
164  );
165  $licenseFound = true;
166  } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
167  $files = ExtensionInfo::getLicenseFileNames( dirname( $extNode['path'] ) );
168  if ( $files ) {
169  $licenseFound = true;
170  foreach ( $files as $file ) {
171  $out->addWikiTextAsInterface(
173  'pre',
174  [
175  'lang' => 'en',
176  'dir' => 'ltr',
177  ],
178  file_get_contents( $file )
179  )
180  );
181  }
182  }
183  }
184  if ( !$licenseFound ) {
185  $out->addWikiTextAsInterface( '{{int:version-license-not-found}}' );
186  }
187  break;
188 
189  default:
190  $out->addModuleStyles( 'mediawiki.special' );
191  $out->addWikiTextAsInterface(
192  self::getMediaWikiCredits() .
193  self::softwareInformation() .
194  $this->getEntryPointInfo()
195  );
196  $out->addHTML(
197  $this->getSkinCredits( $credits ) .
198  $this->getExtensionCredits( $credits ) .
199  $this->getExternalLibraries( $credits ) .
200  $this->getParserTags() .
201  $this->getParserFunctionHooks()
202  );
203  $out->addWikiTextAsInterface( $this->getHooks() );
204  $out->addHTML( $this->IPInfo() );
205 
206  break;
207  }
208  }
209 
215  private static function getMediaWikiCredits() {
216  $ret = Xml::element(
217  'h2',
218  [ 'id' => 'mw-version-license' ],
219  wfMessage( 'version-license' )->text()
220  );
221 
222  // This text is always left-to-right.
223  $ret .= '<div class="plainlinks">';
224  $ret .= "__NOTOC__
225  " . self::getCopyrightAndAuthorList() . "\n
226  " . '<div class="mw-version-license-info">' .
227  wfMessage( 'version-license-info' )->text() .
228  '</div>';
229  $ret .= '</div>';
230 
231  return str_replace( "\t\t", '', $ret ) . "\n";
232  }
233 
239  public static function getCopyrightAndAuthorList() {
240  if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
241  $othersLink = '[https://www.mediawiki.org/wiki/Special:Version/Credits ' .
242  wfMessage( 'version-poweredby-others' )->text() . ']';
243  } else {
244  $othersLink = '[[Special:Version/Credits|' .
245  wfMessage( 'version-poweredby-others' )->text() . ']]';
246  }
247 
248  $translatorsLink = '[https://translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
249  wfMessage( 'version-poweredby-translators' )->text() . ']';
250 
251  $authorList = [
252  'Magnus Manske', 'Brion Vibber', 'Lee Daniel Crocker',
253  'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
254  'Niklas Laxström', 'Domas Mituzas', 'Rob Church', 'Yuri Astrakhan',
255  'Aryeh Gregor', 'Aaron Schulz', 'Andrew Garrett', 'Raimond Spekking',
256  'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
257  'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
258  'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
259  'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', 'Brad Jorsch',
260  'Bartosz Dziewoński', 'Ed Sanders', 'Moriel Schottlender',
261  'Kunal Mehta', 'James D. Forrester', 'Brian Wolff', 'Adam Shorland',
262  'DannyS712', 'Ori Livneh',
263  $othersLink, $translatorsLink
264  ];
265 
266  return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
267  Message::listParam( $authorList ) )->text();
268  }
269 
275  private static function getSoftwareInformation() {
276  $dbr = wfGetDB( DB_REPLICA );
277 
278  // Put the software in an array of form 'name' => 'version'. All messages should
279  // be loaded here, so feel free to use wfMessage in the 'name'. Wikitext
280  // can be used both in the name and value.
281  $software = [
282  '[https://www.mediawiki.org/ MediaWiki]' => self::getVersionLinked(),
283  '[https://php.net/ PHP]' => PHP_VERSION . " (" . PHP_SAPI . ")",
284  '[https://icu.unicode.org/ ICU]' => INTL_ICU_VERSION,
285  $dbr->getSoftwareLink() => $dbr->getServerInfo(),
286  ];
287 
288  // Allow a hook to add/remove items.
289  Hooks::runner()->onSoftwareInfo( $software );
290 
291  return $software;
292  }
293 
299  private static function softwareInformation() {
300  $out = Xml::element(
301  'h2',
302  [ 'id' => 'mw-version-software' ],
303  wfMessage( 'version-software' )->text()
304  ) .
305  Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ] ) .
306  "<tr>
307  <th>" . wfMessage( 'version-software-product' )->text() . "</th>
308  <th>" . wfMessage( 'version-software-version' )->text() . "</th>
309  </tr>\n";
310 
311  foreach ( self::getSoftwareInformation() as $name => $version ) {
312  $out .= "<tr>
313  <td>" . $name . "</td>
314  <td dir=\"ltr\">" . $version . "</td>
315  </tr>\n";
316  }
317 
318  return $out . Xml::closeElement( 'table' );
319  }
320 
329  public static function getVersion( $flags = '', $lang = null ) {
330  global $IP;
331 
332  $gitInfo = self::getGitHeadSha1( $IP );
333  if ( !$gitInfo ) {
334  $version = MW_VERSION;
335  } elseif ( $flags === 'nodb' ) {
336  $shortSha1 = substr( $gitInfo, 0, 7 );
337  $version = MW_VERSION . " ($shortSha1)";
338  } else {
339  $shortSha1 = substr( $gitInfo, 0, 7 );
340  $msg = wfMessage( 'parentheses' );
341  if ( $lang !== null ) {
342  $msg->inLanguage( $lang );
343  }
344  $shortSha1 = $msg->params( $shortSha1 )->escaped();
345  $version = MW_VERSION . ' ' . $shortSha1;
346  }
347 
348  return $version;
349  }
350 
358  public static function getVersionLinked() {
359  $gitVersion = self::getVersionLinkedGit();
360  if ( $gitVersion ) {
361  $v = $gitVersion;
362  } else {
363  $v = MW_VERSION; // fallback
364  }
365 
366  return $v;
367  }
368 
372  private static function getMWVersionLinked() {
373  $versionUrl = "";
374  if ( Hooks::runner()->onSpecialVersionVersionUrl( MW_VERSION, $versionUrl ) ) {
375  $versionParts = [];
376  preg_match( "/^(\d+\.\d+)/", MW_VERSION, $versionParts );
377  $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
378  }
379 
380  return '[' . $versionUrl . ' ' . MW_VERSION . ']';
381  }
382 
388  private static function getVersionLinkedGit() {
389  global $IP, $wgLang;
390 
391  $gitInfo = new GitInfo( $IP );
392  $headSHA1 = $gitInfo->getHeadSHA1();
393  if ( !$headSHA1 ) {
394  return false;
395  }
396 
397  $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
398 
399  $gitHeadUrl = $gitInfo->getHeadViewUrl();
400  if ( $gitHeadUrl !== false ) {
401  $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
402  }
403 
404  $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
405  if ( $gitHeadCommitDate ) {
406  $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( (string)$gitHeadCommitDate, true );
407  }
408 
409  return self::getMWVersionLinked() . " $shortSHA1";
410  }
411 
421  public static function getExtensionTypes(): array {
422  if ( self::$extensionTypes === false ) {
423  self::$extensionTypes = [
424  'specialpage' => wfMessage( 'version-specialpages' )->text(),
425  'editor' => wfMessage( 'version-editors' )->text(),
426  'parserhook' => wfMessage( 'version-parserhooks' )->text(),
427  'variable' => wfMessage( 'version-variables' )->text(),
428  'media' => wfMessage( 'version-mediahandlers' )->text(),
429  'antispam' => wfMessage( 'version-antispam' )->text(),
430  'skin' => wfMessage( 'version-skins' )->text(),
431  'api' => wfMessage( 'version-api' )->text(),
432  'other' => wfMessage( 'version-other' )->text(),
433  ];
434 
435  Hooks::runner()->onExtensionTypes( self::$extensionTypes );
436  }
437 
438  return self::$extensionTypes;
439  }
440 
450  public static function getExtensionTypeName( $type ) {
451  $types = self::getExtensionTypes();
452 
453  return $types[$type] ?? $types['other'];
454  }
455 
462  private function getExtensionCredits( array $credits ) {
463  if (
464  !$credits ||
465  // Skins are displayed separately, see getSkinCredits()
466  ( count( $credits ) === 1 && isset( $credits['skin'] ) )
467  ) {
468  return '';
469  }
470 
471  $extensionTypes = self::getExtensionTypes();
472 
473  $out = Xml::element(
474  'h2',
475  [ 'id' => 'mw-version-ext' ],
476  $this->msg( 'version-extensions' )->text()
477  ) .
478  Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-ext' ] );
479 
480  // Make sure the 'other' type is set to an array.
481  if ( !array_key_exists( 'other', $credits ) ) {
482  $credits['other'] = [];
483  }
484 
485  // Find all extensions that do not have a valid type and give them the type 'other'.
486  foreach ( $credits as $type => $extensions ) {
487  if ( !array_key_exists( $type, $extensionTypes ) ) {
488  $credits['other'] = array_merge( $credits['other'], $extensions );
489  }
490  }
491 
492  $this->firstExtOpened = false;
493  // Loop through the extension categories to display their extensions in the list.
494  foreach ( $extensionTypes as $type => $text ) {
495  // Skins have a separate section
496  if ( $type !== 'other' && $type !== 'skin' ) {
497  $out .= $this->getExtensionCategory( $type, $text, $credits[$type] ?? [] );
498  }
499  }
500 
501  // We want the 'other' type to be last in the list.
502  $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'], $credits['other'] );
503 
504  $out .= Xml::closeElement( 'table' );
505 
506  return $out;
507  }
508 
515  private function getSkinCredits( array $credits ) {
516  if ( !isset( $credits['skin'] ) || !$credits['skin'] ) {
517  return '';
518  }
519 
520  $out = Html::element(
521  'h2',
522  [ 'id' => 'mw-version-skin' ],
523  $this->msg( 'version-skins' )->text()
524  ) .
525  Html::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-skin' ] );
526 
527  $this->firstExtOpened = false;
528  $out .= $this->getExtensionCategory( 'skin', null, $credits['skin'] );
529 
530  $out .= Html::closeElement( 'table' );
531 
532  return $out;
533  }
534 
541  protected function getExternalLibraries( array $credits ) {
542  global $IP;
543  $paths = [
544  "$IP/vendor/composer/installed.json"
545  ];
546 
547  $extensionTypes = self::getExtensionTypes();
548  foreach ( $extensionTypes as $type => $message ) {
549  if ( !isset( $credits[$type] ) || $credits[$type] === [] ) {
550  continue;
551  }
552  foreach ( $credits[$type] as $extension ) {
553  if ( !isset( $extension['path'] ) ) {
554  continue;
555  }
556  $paths[] = dirname( $extension['path'] ) . '/vendor/composer/installed.json';
557  }
558  }
559 
560  $dependencies = [];
561 
562  foreach ( $paths as $path ) {
563  if ( !file_exists( $path ) ) {
564  continue;
565  }
566 
567  $installed = new ComposerInstalled( $path );
568 
569  $dependencies += $installed->getInstalledDependencies();
570  }
571 
572  if ( $dependencies === [] ) {
573  return '';
574  }
575 
576  ksort( $dependencies );
577 
578  $out = Html::element(
579  'h2',
580  [ 'id' => 'mw-version-libraries' ],
581  $this->msg( 'version-libraries' )->text()
582  );
583  $out .= Html::openElement(
584  'table',
585  [ 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries' ]
586  );
587  $out .= Html::openElement( 'tr' )
588  . Html::element( 'th', [], $this->msg( 'version-libraries-library' )->text() )
589  . Html::element( 'th', [], $this->msg( 'version-libraries-version' )->text() )
590  . Html::element( 'th', [], $this->msg( 'version-libraries-license' )->text() )
591  . Html::element( 'th', [], $this->msg( 'version-libraries-description' )->text() )
592  . Html::element( 'th', [], $this->msg( 'version-libraries-authors' )->text() )
593  . Html::closeElement( 'tr' );
594 
595  foreach ( $dependencies as $name => $info ) {
596  if ( !is_array( $info ) || str_starts_with( $info['type'], 'mediawiki-' ) ) {
597  // Skip any extensions or skins since they'll be listed
598  // in their proper section
599  continue;
600  }
601  $authors = array_map( static function ( $arr ) {
602  // If a homepage is set, link to it
603  if ( isset( $arr['homepage'] ) ) {
604  return "[{$arr['homepage']} {$arr['name']}]";
605  }
606  return $arr['name'];
607  }, $info['authors'] );
608  $authors = $this->listAuthors( $authors, false, "$IP/vendor/$name" );
609 
610  // We can safely assume that the libraries' names and descriptions
611  // are written in English and aren't going to be translated,
612  // so set appropriate lang and dir attributes
613  $out .= Html::openElement( 'tr', [
614  // Add an anchor so docs can link easily to the version of
615  // this specific library
617  "mw-version-library-$name"
618  ) ] )
620  'td',
621  [],
622  Linker::makeExternalLink(
623  "https://packagist.org/packages/$name", $name,
624  true, '',
625  [ 'class' => 'mw-version-library-name' ]
626  )
627  )
628  . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
629  // @phan-suppress-next-line SecurityCheck-DoubleEscaped false positive
630  . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['licenses'] ) )
631  . Html::element( 'td', [ 'lang' => 'en', 'dir' => 'ltr' ], $info['description'] )
632  . Html::rawElement( 'td', [], $authors )
633  . Html::closeElement( 'tr' );
634  }
635  $out .= Html::closeElement( 'table' );
636 
637  return $out;
638  }
639 
645  protected function getParserTags() {
646  $tags = $this->parser->getTags();
647  if ( !$tags ) {
648  return '';
649  }
650 
651  $out = Html::rawElement(
652  'h2',
653  [ 'id' => 'mw-version-parser-extensiontags' ],
655  'span',
656  [ 'class' => 'plainlinks' ],
657  Linker::makeExternalLink(
658  'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
659  $this->msg( 'version-parser-extensiontags' )->text()
660  )
661  )
662  );
663 
664  array_walk( $tags, static function ( &$value ) {
665  // Bidirectional isolation improves readability in RTL wikis
666  $value = Html::element(
667  'bdi',
668  // Prevent < and > from slipping to another line
669  [
670  'style' => 'white-space: nowrap;',
671  ],
672  "<$value>"
673  );
674  } );
675 
676  $out .= $this->listToText( $tags );
677 
678  return $out;
679  }
680 
686  protected function getParserFunctionHooks() {
687  $funcHooks = $this->parser->getFunctionHooks();
688  if ( !$funcHooks ) {
689  return '';
690  }
691 
692  $out = Html::rawElement(
693  'h2',
694  [ 'id' => 'mw-version-parser-function-hooks' ],
696  'span',
697  [ 'class' => 'plainlinks' ],
698  Linker::makeExternalLink(
699  'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
700  $this->msg( 'version-parser-function-hooks' )->text()
701  )
702  )
703  );
704 
705  $out .= $this->listToText( $funcHooks );
706 
707  return $out;
708  }
709 
719  protected function getExtensionCategory( $type, ?string $text, array $creditsGroup ) {
720  $out = '';
721 
722  if ( $creditsGroup ) {
723  $out .= $this->openExtType( $text, '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  }
814  $memcKey = $cache->makeKey(
815  'specialversion-ext-version-text', $extension['path'], $this->coreId
816  );
817  [ $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 ( $extensionPath !== null && 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 );
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  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable path is set when there is a name
916  $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
917 
918  // Finally! Create the table
919  $html = Html::openElement( 'tr', [
920  'class' => 'mw-version-ext',
921  'id' => Sanitizer::escapeIdForAttribute( 'mw-version-ext-' . $type . '-' . $extension['name'] )
922  ]
923  );
924 
925  $html .= Html::rawElement( 'td', [], $extensionNameLink );
926  $html .= Html::rawElement( 'td', [], $versionString );
927  $html .= Html::rawElement( 'td', [], $licenseLink );
928  $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-description' ], $description );
929  $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-authors' ], $authors );
930 
931  $html .= Html::closeElement( 'tr' );
932 
933  return $html;
934  }
935 
941  private function getHooks() {
942  if ( $this->getConfig()->get( MainConfigNames::SpecialVersionShowHooks ) &&
943  $this->getConfig()->get( MainConfigNames::Hooks )
944  ) {
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 
1044  public function listAuthors( $authors, $extName, $extDir ): string {
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 ( str_ends_with( $item, ' ...]' ) ) {
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 
1109  private function listToText( array $list, bool $sort = true ): string {
1110  if ( !$list ) {
1111  return '';
1112  }
1113  if ( $sort ) {
1114  sort( $list );
1115  }
1116 
1117  return $this->getLanguage()
1118  ->listToText( array_map( [ __CLASS__, 'arrayToString' ], $list ) );
1119  }
1120 
1129  public static function arrayToString( $list ) {
1130  if ( is_array( $list ) && count( $list ) == 1 ) {
1131  $list = $list[0];
1132  }
1133  if ( $list instanceof Closure ) {
1134  // Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$"
1135  return 'Closure';
1136  } elseif ( is_object( $list ) ) {
1137  return wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1138  } elseif ( !is_array( $list ) ) {
1139  return $list;
1140  } else {
1141  if ( is_object( $list[0] ) ) {
1142  $class = get_class( $list[0] );
1143  } else {
1144  $class = $list[0];
1145  }
1146 
1147  return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1148  }
1149  }
1150 
1155  public static function getGitHeadSha1( $dir ) {
1156  return ( new GitInfo( $dir ) )->getHeadSHA1();
1157  }
1158 
1163  public static function getGitCurrentBranch( $dir ) {
1164  return ( new GitInfo( $dir ) )->getCurrentBranch();
1165  }
1166 
1171  public function getEntryPointInfo() {
1172  $config = $this->getConfig();
1173  $scriptPath = $config->get( MainConfigNames::ScriptPath ) ?: '/';
1174 
1175  $entryPoints = [
1176  'version-entrypoints-articlepath' => $config->get( MainConfigNames::ArticlePath ),
1177  'version-entrypoints-scriptpath' => $scriptPath,
1178  'version-entrypoints-index-php' => wfScript( 'index' ),
1179  'version-entrypoints-api-php' => wfScript( 'api' ),
1180  'version-entrypoints-rest-php' => wfScript( 'rest' ),
1181  ];
1182 
1183  $language = $this->getLanguage();
1184  $thAttributes = [
1185  'dir' => $language->getDir(),
1186  'lang' => $language->getHtmlCode()
1187  ];
1188  $out = Html::element(
1189  'h2',
1190  [ 'id' => 'mw-version-entrypoints' ],
1191  $this->msg( 'version-entrypoints' )->text()
1192  ) .
1193  Html::openElement( 'table',
1194  [
1195  'class' => 'wikitable plainlinks',
1196  'id' => 'mw-version-entrypoints-table',
1197  'dir' => 'ltr',
1198  'lang' => 'en'
1199  ]
1200  ) .
1201  Html::openElement( 'tr' ) .
1202  Html::element(
1203  'th',
1204  $thAttributes,
1205  $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1206  ) .
1207  Html::element(
1208  'th',
1209  $thAttributes,
1210  $this->msg( 'version-entrypoints-header-url' )->text()
1211  ) .
1212  Html::closeElement( 'tr' );
1213 
1214  foreach ( $entryPoints as $message => $value ) {
1215  $url = $this->urlUtils->expand( $value, PROTO_RELATIVE );
1216  $out .= Html::openElement( 'tr' ) .
1217  // ->plain() looks like it should be ->parse(), but this function
1218  // returns wikitext, not HTML, boo
1219  Html::rawElement( 'td', [], $this->msg( $message )->plain() ) .
1220  Html::rawElement( 'td', [], Html::rawElement( 'code', [], "[$url $value]" ) ) .
1221  Html::closeElement( 'tr' );
1222  }
1223 
1224  $out .= Html::closeElement( 'table' );
1225 
1226  return $out;
1227  }
1228 
1229  protected function getGroupName() {
1230  return 'wiki';
1231  }
1232 }
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.
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:508
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 getLocalInstance( $ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
Some internal bits split of from Skin.php.
Definition: Linker.php:65
A class containing constants representing the names of configuration variables.
A service to expand, parse, and otherwise manipulate URLs.
Definition: UrlUtils.php:17
static listParam(array $list, $type='text')
Definition: Message.php:1277
static getInstance( $id)
Get a cached instance of the specified type of cache object.
Definition: ObjectCache.php:75
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition: Parser.php:104
static escapeIdForAttribute( $id, $mode=self::ID_PRIMARY)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid HTM...
Definition: Sanitizer.php:946
Parent class for all special pages.
Definition: SpecialPage.php:44
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)
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.
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.
__construct(Parser $parser, UrlUtils $urlUtils)
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
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