MediaWiki  master
SpecialVersion.php
Go to the documentation of this file.
1 <?php
28 
34 class SpecialVersion extends SpecialPage {
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(
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() {
268  $dbr = wfGetDB( DB_REPLICA );
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
612  "mw-version-library-$name"
613  ) ] )
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  }
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:1017
static getLocalInstance( $ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
A class containing constants representing the names of configuration variables.
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:95
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:945
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)
__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