MediaWiki  master
SpecialVersion.php
Go to the documentation of this file.
1 <?php
35 use Symfony\Component\Yaml\Yaml;
36 use Wikimedia\Parsoid\Core\SectionMetadata;
37 use Wikimedia\Parsoid\Core\TOCData;
38 
44 class SpecialVersion extends SpecialPage {
45 
49  protected $firstExtOpened = false;
50 
54  protected $coreId = '';
55 
59  protected static $extensionTypes = false;
60 
62  protected $tocData;
63 
65  protected $tocLength;
66 
68  private $parser;
69 
71  private $urlUtils;
72 
77  public function __construct(
78  Parser $parser,
79  UrlUtils $urlUtils
80  ) {
81  parent::__construct( 'Version' );
82  $this->parser = $parser;
83  $this->urlUtils = $urlUtils;
84  }
85 
93  public static function getCredits( ExtensionRegistry $reg, Config $conf ): array {
94  $credits = $conf->get( MainConfigNames::ExtensionCredits );
95  foreach ( $reg->getAllThings() as $credit ) {
96  $credits[$credit['type']][] = $credit;
97  }
98  return $credits;
99  }
100 
105  public function execute( $par ) {
106  global $IP;
107  $config = $this->getConfig();
108  $credits = self::getCredits( ExtensionRegistry::getInstance(), $config );
109 
110  $this->setHeaders();
111  $this->outputHeader();
112  $out = $this->getOutput();
113  $out->setPreventClickjacking( false );
114 
115  // Explode the sub page information into useful bits
116  $parts = explode( '/', (string)$par );
117  $extNode = null;
118  if ( isset( $parts[1] ) ) {
119  $extName = str_replace( '_', ' ', $parts[1] );
120  // Find it!
121  foreach ( $credits as $extensions ) {
122  foreach ( $extensions as $ext ) {
123  if ( isset( $ext['name'] ) && ( $ext['name'] === $extName ) ) {
124  $extNode = &$ext;
125  break 2;
126  }
127  }
128  }
129  if ( !$extNode ) {
130  $out->setStatusCode( 404 );
131  }
132  } else {
133  $extName = 'MediaWiki';
134  }
135 
136  // Now figure out what to do
137  switch ( strtolower( $parts[0] ) ) {
138  case 'credits':
139  $out->addModuleStyles( 'mediawiki.special' );
140 
141  $wikiText = '{{int:version-credits-not-found}}';
142  if ( $extName === 'MediaWiki' ) {
143  $wikiText = file_get_contents( $IP . '/CREDITS' );
144  // Put the contributor list into columns
145  $wikiText = str_replace(
146  [ '<!-- BEGIN CONTRIBUTOR LIST -->', '<!-- END CONTRIBUTOR LIST -->' ],
147  [ '<div class="mw-version-credits">', '</div>' ],
148  $wikiText );
149  } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
150  $file = ExtensionInfo::getAuthorsFileName( dirname( $extNode['path'] ) );
151  if ( $file ) {
152  $wikiText = file_get_contents( $file );
153  if ( str_ends_with( $file, '.txt' ) ) {
154  $wikiText = Html::element(
155  'pre',
156  [
157  'lang' => 'en',
158  'dir' => 'ltr',
159  ],
160  $wikiText
161  );
162  }
163  }
164  }
165 
166  $out->setPageTitle( $this->msg( 'version-credits-title', $extName ) );
167  $out->addWikiTextAsInterface( $wikiText );
168  break;
169 
170  case 'license':
171  $out->setPageTitle( $this->msg( 'version-license-title', $extName ) );
172 
173  $licenseFound = false;
174 
175  if ( $extName === 'MediaWiki' ) {
176  $out->addWikiTextAsInterface(
177  file_get_contents( $IP . '/COPYING' )
178  );
179  $licenseFound = true;
180  } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
181  $files = ExtensionInfo::getLicenseFileNames( dirname( $extNode['path'] ) );
182  if ( $files ) {
183  $licenseFound = true;
184  foreach ( $files as $file ) {
185  $out->addWikiTextAsInterface(
186  Html::element(
187  'pre',
188  [
189  'lang' => 'en',
190  'dir' => 'ltr',
191  ],
192  file_get_contents( $file )
193  )
194  );
195  }
196  }
197  }
198  if ( !$licenseFound ) {
199  $out->addWikiTextAsInterface( '{{int:version-license-not-found}}' );
200  }
201  break;
202 
203  default:
204  $out->addModuleStyles( 'mediawiki.special' );
205 
206  $out->addHTML( $this->getMediaWikiCredits() );
207 
208  $this->tocData = new TOCData();
209  $this->tocLength = 0;
210 
211  // Build the page contents (this also fills in TOCData)
212  $sections = [
213  $this->softwareInformation(),
214  $this->getEntryPointInfo(),
215  $this->getSkinCredits( $credits ),
216  $this->getExtensionCredits( $credits ),
217  $this->getExternalLibraries( $credits ),
218  $this->getClientSideLibraries(),
219  $this->getParserTags(),
220  $this->getParserFunctionHooks(),
221  $this->getHooks(),
222  $this->IPInfo(),
223  ];
224 
225  // Insert TOC first
226  $pout = new ParserOutput;
227  $pout->setTOCData( $this->tocData );
228  $pout->setOutputFlag( ParserOutputFlags::SHOW_TOC );
229  $pout->setText( Parser::TOC_PLACEHOLDER );
230  $out->addParserOutput( $pout );
231 
232  // Insert contents
233  foreach ( $sections as $content ) {
234  $out->addHTML( $content );
235  }
236 
237  break;
238  }
239  }
240 
248  private function addTocSection( $labelMsg, $id ) {
249  $this->tocLength++;
250  $this->tocData->addSection( new SectionMetadata(
251  1,
252  2,
253  $this->msg( $labelMsg )->escaped(),
254  $this->getLanguage()->formatNum( $this->tocLength ),
255  (string)$this->tocLength,
256  null,
257  null,
258  $id,
259  $id
260  ) );
261  }
262 
268  private function getMediaWikiCredits() {
269  // No TOC entry for this heading, we treat it like the lede section
270 
271  $ret = Html::element(
272  'h2',
273  [ 'id' => 'mw-version-license' ],
274  $this->msg( 'version-license' )->text()
275  );
276 
277  $ret .= Html::rawElement( 'div', [ 'class' => 'plainlinks' ],
278  $this->msg( new RawMessage( self::getCopyrightAndAuthorList() ) )->parseAsBlock() .
279  Html::rawElement( 'div', [ 'class' => 'mw-version-license-info' ],
280  $this->msg( 'version-license-info' )->parseAsBlock()
281  )
282  );
283 
284  return $ret;
285  }
286 
293  public static function getCopyrightAndAuthorList() {
294  if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
295  $othersLink = '[https://www.mediawiki.org/wiki/Special:Version/Credits ' .
296  wfMessage( 'version-poweredby-others' )->plain() . ']';
297  } else {
298  $othersLink = '[[Special:Version/Credits|' .
299  wfMessage( 'version-poweredby-others' )->plain() . ']]';
300  }
301 
302  $translatorsLink = '[https://translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
303  wfMessage( 'version-poweredby-translators' )->plain() . ']';
304 
305  $authorList = [
306  'Magnus Manske', 'Brion Vibber', 'Lee Daniel Crocker',
307  'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
308  'Niklas Laxström', 'Domas Mituzas', 'Rob Church', 'Yuri Astrakhan',
309  'Aryeh Gregor', 'Aaron Schulz', 'Andrew Garrett', 'Raimond Spekking',
310  'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
311  'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
312  'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
313  'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', 'Brad Jorsch',
314  'Bartosz Dziewoński', 'Ed Sanders', 'Moriel Schottlender',
315  'Kunal Mehta', 'James D. Forrester', 'Brian Wolff', 'Adam Shorland',
316  'DannyS712', 'Ori Livneh', 'Max Semenik', 'Amir Sarabadani',
317  'Derk-Jan Hartman', 'Petr Pchelko',
318  $othersLink, $translatorsLink
319  ];
320 
321  return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
322  Message::listParam( $authorList ) )->plain();
323  }
324 
330  private function getSoftwareInformation() {
331  $dbr = wfGetDB( DB_REPLICA );
332 
333  // Put the software in an array of form 'name' => 'version'. All messages should
334  // be loaded here, so feel free to use wfMessage in the 'name'. Wikitext
335  // can be used both in the name and value.
336  $software = [
337  '[https://www.mediawiki.org/ MediaWiki]' => self::getVersionLinked(),
338  '[https://php.net/ PHP]' => PHP_VERSION . " (" . PHP_SAPI . ")",
339  '[https://icu.unicode.org/ ICU]' => INTL_ICU_VERSION,
340  $dbr->getSoftwareLink() => $dbr->getServerInfo(),
341  ];
342 
343  // Allow a hook to add/remove items.
344  $this->getHookRunner()->onSoftwareInfo( $software );
345 
346  return $software;
347  }
348 
354  private function softwareInformation() {
355  $this->addTocSection( 'version-software', 'mw-version-software' );
356 
357  $out = Html::element(
358  'h2',
359  [ 'id' => 'mw-version-software' ],
360  $this->msg( 'version-software' )->text()
361  );
362 
363  $out .= Html::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ] );
364 
365  $out .= Html::rawElement( 'tr', [],
366  Html::element( 'th', [], $this->msg( 'version-software-product' )->text() ) .
367  Html::element( 'th', [], $this->msg( 'version-software-version' )->text() )
368  );
369 
370  foreach ( $this->getSoftwareInformation() as $name => $version ) {
371  $out .= Html::rawElement( 'tr', [],
372  Html::rawElement( 'td', [], $this->msg( new RawMessage( $name ) )->parse() ) .
373  Html::rawElement( 'td', [ 'dir' => 'ltr' ], $this->msg( new RawMessage( $version ) )->parse() )
374  );
375  }
376 
377  $out .= Html::closeElement( 'table' );
378 
379  return $out;
380  }
381 
390  public static function getVersion( $flags = '', $lang = null ) {
391  global $IP;
392 
393  $gitInfo = self::getGitHeadSha1( $IP );
394  if ( !$gitInfo ) {
395  $version = MW_VERSION;
396  } elseif ( $flags === 'nodb' ) {
397  $shortSha1 = substr( $gitInfo, 0, 7 );
398  $version = MW_VERSION . " ($shortSha1)";
399  } else {
400  $shortSha1 = substr( $gitInfo, 0, 7 );
401  $msg = wfMessage( 'parentheses' );
402  if ( $lang !== null ) {
403  $msg->inLanguage( $lang );
404  }
405  $shortSha1 = $msg->params( $shortSha1 )->escaped();
406  $version = MW_VERSION . ' ' . $shortSha1;
407  }
408 
409  return $version;
410  }
411 
419  public static function getVersionLinked() {
420  $gitVersion = self::getVersionLinkedGit();
421  if ( $gitVersion ) {
422  $v = $gitVersion;
423  } else {
424  $v = MW_VERSION; // fallback
425  }
426 
427  return $v;
428  }
429 
433  private static function getMWVersionLinked() {
434  $versionUrl = "";
435  $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
436  if ( $hookRunner->onSpecialVersionVersionUrl( MW_VERSION, $versionUrl ) ) {
437  $versionParts = [];
438  preg_match( "/^(\d+\.\d+)/", MW_VERSION, $versionParts );
439  $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
440  }
441 
442  return '[' . $versionUrl . ' ' . MW_VERSION . ']';
443  }
444 
450  private static function getVersionLinkedGit() {
451  global $IP, $wgLang;
452 
453  $gitInfo = new GitInfo( $IP );
454  $headSHA1 = $gitInfo->getHeadSHA1();
455  if ( !$headSHA1 ) {
456  return false;
457  }
458 
459  $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
460 
461  $gitHeadUrl = $gitInfo->getHeadViewUrl();
462  if ( $gitHeadUrl !== false ) {
463  $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
464  }
465 
466  $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
467  if ( $gitHeadCommitDate ) {
468  $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( (string)$gitHeadCommitDate, true );
469  }
470 
471  return self::getMWVersionLinked() . " $shortSHA1";
472  }
473 
483  public static function getExtensionTypes(): array {
484  if ( self::$extensionTypes === false ) {
485  self::$extensionTypes = [
486  'specialpage' => wfMessage( 'version-specialpages' )->text(),
487  'editor' => wfMessage( 'version-editors' )->text(),
488  'parserhook' => wfMessage( 'version-parserhooks' )->text(),
489  'variable' => wfMessage( 'version-variables' )->text(),
490  'media' => wfMessage( 'version-mediahandlers' )->text(),
491  'antispam' => wfMessage( 'version-antispam' )->text(),
492  'skin' => wfMessage( 'version-skins' )->text(),
493  'api' => wfMessage( 'version-api' )->text(),
494  'other' => wfMessage( 'version-other' )->text(),
495  ];
496 
497  ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
498  ->onExtensionTypes( self::$extensionTypes );
499  }
500 
501  return self::$extensionTypes;
502  }
503 
513  public static function getExtensionTypeName( $type ) {
514  $types = self::getExtensionTypes();
515 
516  return $types[$type] ?? $types['other'];
517  }
518 
525  private function getExtensionCredits( array $credits ) {
526  if (
527  !$credits ||
528  // Skins are displayed separately, see getSkinCredits()
529  ( count( $credits ) === 1 && isset( $credits['skin'] ) )
530  ) {
531  return '';
532  }
533 
534  $extensionTypes = self::getExtensionTypes();
535 
536  $this->addTocSection( 'version-extensions', 'mw-version-ext' );
537 
538  $out = Xml::element(
539  'h2',
540  [ 'id' => 'mw-version-ext' ],
541  $this->msg( 'version-extensions' )->text()
542  ) .
543  Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-ext' ] );
544 
545  // Make sure the 'other' type is set to an array.
546  if ( !array_key_exists( 'other', $credits ) ) {
547  $credits['other'] = [];
548  }
549 
550  // Find all extensions that do not have a valid type and give them the type 'other'.
551  foreach ( $credits as $type => $extensions ) {
552  if ( !array_key_exists( $type, $extensionTypes ) ) {
553  $credits['other'] = array_merge( $credits['other'], $extensions );
554  }
555  }
556 
557  $this->firstExtOpened = false;
558  // Loop through the extension categories to display their extensions in the list.
559  foreach ( $extensionTypes as $type => $text ) {
560  // Skins have a separate section
561  if ( $type !== 'other' && $type !== 'skin' ) {
562  $out .= $this->getExtensionCategory( $type, $text, $credits[$type] ?? [] );
563  }
564  }
565 
566  // We want the 'other' type to be last in the list.
567  $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'], $credits['other'] );
568 
569  $out .= Xml::closeElement( 'table' );
570 
571  return $out;
572  }
573 
580  private function getSkinCredits( array $credits ) {
581  if ( !isset( $credits['skin'] ) || !$credits['skin'] ) {
582  return '';
583  }
584 
585  $this->addTocSection( 'version-skins', 'mw-version-skin' );
586 
587  $out = Html::element(
588  'h2',
589  [ 'id' => 'mw-version-skin' ],
590  $this->msg( 'version-skins' )->text()
591  ) .
592  Html::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-skin' ] );
593 
594  $this->firstExtOpened = false;
595  $out .= $this->getExtensionCategory( 'skin', null, $credits['skin'] );
596 
597  $out .= Html::closeElement( 'table' );
598 
599  return $out;
600  }
601 
608  protected function getExternalLibraries( array $credits ) {
609  global $IP;
610  $paths = [
611  "$IP/vendor/composer/installed.json"
612  ];
613 
614  $extensionTypes = self::getExtensionTypes();
615  foreach ( $extensionTypes as $type => $message ) {
616  if ( !isset( $credits[$type] ) || $credits[$type] === [] ) {
617  continue;
618  }
619  foreach ( $credits[$type] as $extension ) {
620  if ( !isset( $extension['path'] ) ) {
621  continue;
622  }
623  $paths[] = dirname( $extension['path'] ) . '/vendor/composer/installed.json';
624  }
625  }
626 
627  $dependencies = [];
628 
629  foreach ( $paths as $path ) {
630  if ( !file_exists( $path ) ) {
631  continue;
632  }
633 
634  $installed = new ComposerInstalled( $path );
635 
636  $dependencies += $installed->getInstalledDependencies();
637  }
638 
639  if ( $dependencies === [] ) {
640  return '';
641  }
642 
643  ksort( $dependencies );
644 
645  $this->addTocSection( 'version-libraries', 'mw-version-libraries' );
646 
647  $out = Html::element(
648  'h2',
649  [ 'id' => 'mw-version-libraries' ],
650  $this->msg( 'version-libraries' )->text()
651  );
652  $out .= Html::openElement(
653  'table',
654  [ 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries' ]
655  );
656  $out .= Html::openElement( 'tr' )
657  . Html::element( 'th', [], $this->msg( 'version-libraries-library' )->text() )
658  . Html::element( 'th', [], $this->msg( 'version-libraries-version' )->text() )
659  . Html::element( 'th', [], $this->msg( 'version-libraries-license' )->text() )
660  . Html::element( 'th', [], $this->msg( 'version-libraries-description' )->text() )
661  . Html::element( 'th', [], $this->msg( 'version-libraries-authors' )->text() )
662  . Html::closeElement( 'tr' );
663 
664  foreach ( $dependencies as $name => $info ) {
665  if ( !is_array( $info ) || str_starts_with( $info['type'], 'mediawiki-' ) ) {
666  // Skip any extensions or skins since they'll be listed
667  // in their proper section
668  continue;
669  }
670  $authors = array_map( static function ( $arr ) {
671  // If a homepage is set, link to it
672  if ( isset( $arr['homepage'] ) ) {
673  return "[{$arr['homepage']} {$arr['name']}]";
674  }
675  return $arr['name'];
676  }, $info['authors'] );
677  $authors = $this->listAuthors( $authors, false, "$IP/vendor/$name" );
678 
679  // We can safely assume that the libraries' names and descriptions
680  // are written in English and aren't going to be translated,
681  // so set appropriate lang and dir attributes
682  $out .= Html::openElement( 'tr', [
683  // Add an anchor so docs can link easily to the version of
684  // this specific library
686  "mw-version-library-$name"
687  ) ] )
688  . Html::rawElement(
689  'td',
690  [],
691  Linker::makeExternalLink(
692  "https://packagist.org/packages/$name", $name,
693  true, '',
694  [ 'class' => 'mw-version-library-name' ]
695  )
696  )
697  . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
698  . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['licenses'] ) )
699  . Html::element( 'td', [ 'lang' => 'en', 'dir' => 'ltr' ], $info['description'] )
700  . Html::rawElement( 'td', [], $authors )
701  . Html::closeElement( 'tr' );
702  }
703  $out .= Html::closeElement( 'table' );
704 
705  return $out;
706  }
707 
713  private function getClientSideLibraries() {
714  global $IP;
715  $registryDirs = [ 'MediaWiki' => "{$IP}/resources/lib" ]
716  + ExtensionRegistry::getInstance()->getAttribute( 'ForeignResourcesDir' );
717 
718  $modules = [];
719  foreach ( $registryDirs as $source => $registryDir ) {
720  $foreignResources = Yaml::parseFile( "$registryDir/foreign-resources.yaml" );
721  foreach ( $foreignResources as $name => $module ) {
722  $key = $name . $module['version'];
723  if ( isset( $modules[$key] ) ) {
724  $modules[$key]['source'][] = $source;
725  continue;
726  }
727  $modules[$key] = $module + [ 'name' => $name, 'source' => [ $source ] ];
728  }
729  }
730  ksort( $modules );
731 
732  $this->addTocSection( 'version-libraries-client', 'mw-version-libraries-client' );
733 
734  $out = Html::element(
735  'h2',
736  [ 'id' => 'mw-version-libraries-client' ],
737  $this->msg( 'version-libraries-client' )->text()
738  );
739  $out .= Html::openElement(
740  'table',
741  [ 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries-client' ]
742  );
743  $out .= Html::openElement( 'tr' )
744  . Html::element( 'th', [], $this->msg( 'version-libraries-library' )->text() )
745  . Html::element( 'th', [], $this->msg( 'version-libraries-version' )->text() )
746  . Html::element( 'th', [], $this->msg( 'version-libraries-license' )->text() )
747  . Html::element( 'th', [], $this->msg( 'version-libraries-authors' )->text() )
748  . Html::element( 'th', [], $this->msg( 'version-libraries-source' )->text() )
749  . Html::closeElement( 'tr' );
750 
751  foreach ( $modules as $name => $info ) {
752  // We can safely assume that the libraries' names and descriptions
753  // are written in English and aren't going to be translated,
754  // so set appropriate lang and dir attributes
755  $out .= Html::openElement( 'tr', [
756  // Add an anchor so docs can link easily to the version of
757  // this specific library
759  "mw-version-library-$name"
760  ) ] )
761  . Html::rawElement(
762  'td',
763  [],
764  Linker::makeExternalLink(
765  $info['homepage'], $info['name'],
766  true, '',
767  [ 'class' => 'mw-version-library-name' ]
768  )
769  )
770  . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
771  . Html::element( 'td', [ 'dir' => 'auto' ], $info['license'] )
772  . Html::element( 'td', [ 'dir' => 'auto' ], $info['authors'] ?? '—' )
773  . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['source'] ) )
774  . Html::closeElement( 'tr' );
775  }
776  $out .= Html::closeElement( 'table' );
777 
778  return $out;
779  }
780 
786  protected function getParserTags() {
787  $tags = $this->parser->getTags();
788  if ( !$tags ) {
789  return '';
790  }
791 
792  $this->addTocSection( 'version-parser-extensiontags', 'mw-version-parser-extensiontags' );
793 
794  $out = Html::rawElement(
795  'h2',
796  [ 'id' => 'mw-version-parser-extensiontags' ],
797  Html::rawElement(
798  'span',
799  [ 'class' => 'plainlinks' ],
800  Linker::makeExternalLink(
801  'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
802  $this->msg( 'version-parser-extensiontags' )->text()
803  )
804  )
805  );
806 
807  array_walk( $tags, static function ( &$value ) {
808  // Bidirectional isolation improves readability in RTL wikis
809  $value = Html::element(
810  'bdi',
811  // Prevent < and > from slipping to another line
812  [
813  'style' => 'white-space: nowrap;',
814  ],
815  "<$value>"
816  );
817  } );
818 
819  $out .= $this->listToText( $tags );
820 
821  return $out;
822  }
823 
829  protected function getParserFunctionHooks() {
830  $funcHooks = $this->parser->getFunctionHooks();
831  if ( !$funcHooks ) {
832  return '';
833  }
834 
835  $this->addTocSection( 'version-parser-function-hooks', 'mw-version-parser-function-hooks' );
836 
837  $out = Html::rawElement(
838  'h2',
839  [ 'id' => 'mw-version-parser-function-hooks' ],
840  Html::rawElement(
841  'span',
842  [ 'class' => 'plainlinks' ],
843  Linker::makeExternalLink(
844  'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
845  $this->msg( 'version-parser-function-hooks' )->text()
846  )
847  )
848  );
849 
850  $out .= $this->listToText( $funcHooks );
851 
852  return $out;
853  }
854 
864  protected function getExtensionCategory( $type, ?string $text, array $creditsGroup ) {
865  $out = '';
866 
867  if ( $creditsGroup ) {
868  $out .= $this->openExtType( $text, 'credits-' . $type );
869 
870  usort( $creditsGroup, [ $this, 'compare' ] );
871 
872  foreach ( $creditsGroup as $extension ) {
873  $out .= $this->getCreditsForExtension( $type, $extension );
874  }
875  }
876 
877  return $out;
878  }
879 
886  public function compare( $a, $b ) {
887  return $this->getLanguage()->lc( $a['name'] ) <=> $this->getLanguage()->lc( $b['name'] );
888  }
889 
908  public function getCreditsForExtension( $type, array $extension ) {
909  $out = $this->getOutput();
910 
911  // We must obtain the information for all the bits and pieces!
912  // ... such as extension names and links
913  if ( isset( $extension['namemsg'] ) ) {
914  // Localized name of extension
915  $extensionName = $this->msg( $extension['namemsg'] )->text();
916  } elseif ( isset( $extension['name'] ) ) {
917  // Non localized version
918  $extensionName = $extension['name'];
919  } else {
920  $extensionName = $this->msg( 'version-no-ext-name' )->text();
921  }
922 
923  if ( isset( $extension['url'] ) ) {
924  $extensionNameLink = Linker::makeExternalLink(
925  $extension['url'],
926  $extensionName,
927  true,
928  '',
929  [ 'class' => 'mw-version-ext-name' ]
930  );
931  } else {
932  $extensionNameLink = htmlspecialchars( $extensionName );
933  }
934 
935  // ... and the version information
936  // If the extension path is set we will check that directory for GIT
937  // metadata in an attempt to extract date and vcs commit metadata.
938  $canonicalVersion = '&ndash;';
939  $extensionPath = null;
940  $vcsVersion = null;
941  $vcsLink = null;
942  $vcsDate = null;
943 
944  if ( isset( $extension['version'] ) ) {
945  $canonicalVersion = $out->parseInlineAsInterface( $extension['version'] );
946  }
947 
948  if ( isset( $extension['path'] ) ) {
949  global $IP;
950  $extensionPath = dirname( $extension['path'] );
951  if ( $this->coreId == '' ) {
952  wfDebug( 'Looking up core head id' );
953  $coreHeadSHA1 = self::getGitHeadSha1( $IP );
954  if ( $coreHeadSHA1 ) {
955  $this->coreId = $coreHeadSHA1;
956  }
957  }
959  $memcKey = $cache->makeKey(
960  'specialversion-ext-version-text', $extension['path'], $this->coreId
961  );
962  [ $vcsVersion, $vcsLink, $vcsDate ] = $cache->get( $memcKey );
963 
964  if ( !$vcsVersion ) {
965  wfDebug( "Getting VCS info for extension {$extension['name']}" );
966  $gitInfo = new GitInfo( $extensionPath );
967  $vcsVersion = $gitInfo->getHeadSHA1();
968  if ( $vcsVersion !== false ) {
969  $vcsVersion = substr( $vcsVersion, 0, 7 );
970  $vcsLink = $gitInfo->getHeadViewUrl();
971  $vcsDate = $gitInfo->getHeadCommitDate();
972  }
973  $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
974  } else {
975  wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" );
976  }
977  }
978 
979  $versionString = Html::rawElement(
980  'span',
981  [ 'class' => 'mw-version-ext-version' ],
982  $canonicalVersion
983  );
984 
985  if ( $vcsVersion ) {
986  if ( $vcsLink ) {
987  $vcsVerString = Linker::makeExternalLink(
988  $vcsLink,
989  $this->msg( 'version-version', $vcsVersion )->text(),
990  true,
991  '',
992  [ 'class' => 'mw-version-ext-vcs-version' ]
993  );
994  } else {
995  $vcsVerString = Html::element( 'span',
996  [ 'class' => 'mw-version-ext-vcs-version' ],
997  "({$vcsVersion})"
998  );
999  }
1000  $versionString .= " {$vcsVerString}";
1001 
1002  if ( $vcsDate ) {
1003  $versionString .= ' ' . Html::element( 'span', [
1004  'class' => 'mw-version-ext-vcs-timestamp',
1005  'dir' => $this->getLanguage()->getDir(),
1006  ], $this->getLanguage()->timeanddate( $vcsDate, true ) );
1007  }
1008  $versionString = Html::rawElement( 'span',
1009  [ 'class' => 'mw-version-ext-meta-version' ],
1010  $versionString
1011  );
1012  }
1013 
1014  // ... and license information; if a license file exists we
1015  // will link to it
1016  $licenseLink = '';
1017  if ( isset( $extension['name'] ) ) {
1018  $licenseName = null;
1019  if ( isset( $extension['license-name'] ) ) {
1020  $licenseName = new HtmlArmor( $out->parseInlineAsInterface( $extension['license-name'] ) );
1021  } elseif ( $extensionPath !== null && ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
1022  $licenseName = $this->msg( 'version-ext-license' )->text();
1023  }
1024  if ( $licenseName !== null ) {
1025  $licenseLink = $this->getLinkRenderer()->makeLink(
1026  $this->getPageTitle( 'License/' . $extension['name'] ),
1027  $licenseName,
1028  [
1029  'class' => 'mw-version-ext-license',
1030  'dir' => 'auto',
1031  ]
1032  );
1033  }
1034  }
1035 
1036  // ... and generate the description; which can be a parameterized l10n message
1037  // in the form [ <msgname>, <parameter>, <parameter>... ] or just a straight
1038  // up string
1039  if ( isset( $extension['descriptionmsg'] ) ) {
1040  // Localized description of extension
1041  $descriptionMsg = $extension['descriptionmsg'];
1042 
1043  if ( is_array( $descriptionMsg ) ) {
1044  $descriptionMsgKey = array_shift( $descriptionMsg );
1045  $descriptionMsg = array_map( 'htmlspecialchars', $descriptionMsg );
1046  $description = $this->msg( $descriptionMsgKey, ...$descriptionMsg )->text();
1047  } else {
1048  $description = $this->msg( $descriptionMsg )->text();
1049  }
1050  } elseif ( isset( $extension['description'] ) ) {
1051  // Non localized version
1052  $description = $extension['description'];
1053  } else {
1054  $description = '';
1055  }
1056  $description = $out->parseInlineAsInterface( $description );
1057 
1058  // ... now get the authors for this extension
1059  $authors = $extension['author'] ?? [];
1060  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable path is set when there is a name
1061  $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
1062 
1063  // Finally! Create the table
1064  $html = Html::openElement( 'tr', [
1065  'class' => 'mw-version-ext',
1066  'id' => Sanitizer::escapeIdForAttribute( 'mw-version-ext-' . $type . '-' . $extension['name'] )
1067  ]
1068  );
1069 
1070  $html .= Html::rawElement( 'td', [], $extensionNameLink );
1071  $html .= Html::rawElement( 'td', [], $versionString );
1072  $html .= Html::rawElement( 'td', [], $licenseLink );
1073  $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-description' ], $description );
1074  $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-authors' ], $authors );
1075 
1076  $html .= Html::closeElement( 'tr' );
1077 
1078  return $html;
1079  }
1080 
1086  private function getHooks() {
1087  if ( $this->getConfig()->get( MainConfigNames::SpecialVersionShowHooks ) ) {
1088  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1089  $hookNames = $hookContainer->getHookNames();
1090  sort( $hookNames );
1091 
1092  $ret = [];
1093  $this->addTocSection( 'version-hooks', 'mw-version-hooks' );
1094  $ret[] = Html::element(
1095  'h2',
1096  [ 'id' => 'mw-version-hooks' ],
1097  $this->msg( 'version-hooks' )->text()
1098  );
1099  $ret[] = Html::openElement( 'table', [ 'class' => 'wikitable', 'id' => 'sv-hooks' ] );
1100  $ret[] = Html::openElement( 'tr' );
1101  $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-name' )->text() );
1102  $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-subscribedby' )->text() );
1103  $ret[] = Html::closeElement( 'tr' );
1104 
1105  foreach ( $hookNames as $hook ) {
1106  $hooks = $hookContainer->getLegacyHandlers( $hook );
1107  if ( !$hooks ) {
1108  continue;
1109  }
1110  $ret[] = Html::openElement( 'tr' );
1111  $ret[] = Html::element( 'td', [], $hook );
1112  $ret[] = Html::element( 'td', [], $this->listToText( $hooks ) );
1113  $ret[] = Html::closeElement( 'tr' );
1114  }
1115 
1116  $ret[] = Html::closeElement( 'table' );
1117 
1118  return implode( "\n", $ret );
1119  }
1120 
1121  return '';
1122  }
1123 
1124  private function openExtType( string $text = null, string $name = null ) {
1125  $out = '';
1126 
1127  $opt = [ 'colspan' => 5 ];
1128  if ( $this->firstExtOpened ) {
1129  // Insert a spacing line
1130  $out .= Html::rawElement( 'tr', [ 'class' => 'sv-space' ],
1131  Html::element( 'td', $opt )
1132  );
1133  }
1134  $this->firstExtOpened = true;
1135 
1136  if ( $name ) {
1137  $opt['id'] = "sv-$name";
1138  }
1139 
1140  if ( $text !== null ) {
1141  $out .= Html::rawElement( 'tr', [],
1142  Html::element( 'th', $opt, $text )
1143  );
1144  }
1145 
1146  $firstHeadingMsg = ( $name === 'credits-skin' )
1147  ? 'version-skin-colheader-name'
1148  : 'version-ext-colheader-name';
1149  $out .= Html::openElement( 'tr' );
1150  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1151  $this->msg( $firstHeadingMsg )->text() );
1152  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1153  $this->msg( 'version-ext-colheader-version' )->text() );
1154  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1155  $this->msg( 'version-ext-colheader-license' )->text() );
1156  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1157  $this->msg( 'version-ext-colheader-description' )->text() );
1158  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1159  $this->msg( 'version-ext-colheader-credits' )->text() );
1160  $out .= Html::closeElement( 'tr' );
1161 
1162  return $out;
1163  }
1164 
1170  private function IPInfo() {
1171  $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
1172 
1173  return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
1174  }
1175 
1196  public function listAuthors( $authors, $extName, $extDir ): string {
1197  $hasOthers = false;
1198  $linkRenderer = $this->getLinkRenderer();
1199 
1200  $list = [];
1201  $authors = (array)$authors;
1202 
1203  // Special case: if the authors array has only one item and it is "...",
1204  // it should not be rendered as the "version-poweredby-others" i18n msg,
1205  // but rather as "version-poweredby-various" i18n msg instead.
1206  if ( count( $authors ) === 1 && $authors[0] === '...' ) {
1207  // Link to the extension's or skin's AUTHORS or CREDITS file, if there is
1208  // such a file; otherwise just return the i18n msg as-is
1209  if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1210  return $linkRenderer->makeLink(
1211  $this->getPageTitle( "Credits/$extName" ),
1212  $this->msg( 'version-poweredby-various' )->text()
1213  );
1214  } else {
1215  return $this->msg( 'version-poweredby-various' )->escaped();
1216  }
1217  }
1218 
1219  // Otherwise, if we have an actual array that has more than one item,
1220  // process each array item as usual
1221  foreach ( $authors as $item ) {
1222  if ( $item == '...' ) {
1223  $hasOthers = true;
1224 
1225  if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1226  $text = $linkRenderer->makeLink(
1227  $this->getPageTitle( "Credits/$extName" ),
1228  $this->msg( 'version-poweredby-others' )->text()
1229  );
1230  } else {
1231  $text = $this->msg( 'version-poweredby-others' )->escaped();
1232  }
1233  $list[] = $text;
1234  } elseif ( str_ends_with( $item, ' ...]' ) ) {
1235  $hasOthers = true;
1236  $list[] = $this->getOutput()->parseInlineAsInterface(
1237  substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
1238  );
1239  } else {
1240  $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1241  }
1242  }
1243 
1244  if ( $extName && !$hasOthers && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1245  $list[] = $linkRenderer->makeLink(
1246  $this->getPageTitle( "Credits/$extName" ),
1247  $this->msg( 'version-poweredby-others' )->text()
1248  );
1249  }
1250 
1251  return $this->listToText( $list, false );
1252  }
1253 
1261  private function listToText( array $list, bool $sort = true ): string {
1262  if ( !$list ) {
1263  return '';
1264  }
1265  if ( $sort ) {
1266  sort( $list );
1267  }
1268 
1269  return $this->getLanguage()
1270  ->listToText( array_map( [ __CLASS__, 'arrayToString' ], $list ) );
1271  }
1272 
1281  public static function arrayToString( $list ) {
1282  if ( is_array( $list ) && count( $list ) == 1 ) {
1283  $list = $list[0];
1284  }
1285  if ( $list instanceof Closure ) {
1286  // Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$"
1287  return 'Closure';
1288  } elseif ( is_object( $list ) ) {
1289  return wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1290  } elseif ( !is_array( $list ) ) {
1291  return $list;
1292  } else {
1293  if ( is_object( $list[0] ) ) {
1294  $class = get_class( $list[0] );
1295  } else {
1296  $class = $list[0];
1297  }
1298 
1299  return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1300  }
1301  }
1302 
1307  public static function getGitHeadSha1( $dir ) {
1308  return ( new GitInfo( $dir ) )->getHeadSHA1();
1309  }
1310 
1315  public static function getGitCurrentBranch( $dir ) {
1316  return ( new GitInfo( $dir ) )->getCurrentBranch();
1317  }
1318 
1323  public function getEntryPointInfo() {
1324  $config = $this->getConfig();
1325  $scriptPath = $config->get( MainConfigNames::ScriptPath ) ?: '/';
1326 
1327  $entryPoints = [
1328  'version-entrypoints-articlepath' => $config->get( MainConfigNames::ArticlePath ),
1329  'version-entrypoints-scriptpath' => $scriptPath,
1330  'version-entrypoints-index-php' => wfScript( 'index' ),
1331  'version-entrypoints-api-php' => wfScript( 'api' ),
1332  'version-entrypoints-rest-php' => wfScript( 'rest' ),
1333  ];
1334 
1335  $language = $this->getLanguage();
1336  $thAttributes = [
1337  'dir' => $language->getDir(),
1338  'lang' => $language->getHtmlCode()
1339  ];
1340 
1341  $this->addTocSection( 'version-entrypoints', 'mw-version-entrypoints' );
1342 
1343  $out = Html::element(
1344  'h2',
1345  [ 'id' => 'mw-version-entrypoints' ],
1346  $this->msg( 'version-entrypoints' )->text()
1347  ) .
1348  Html::openElement( 'table',
1349  [
1350  'class' => 'wikitable plainlinks',
1351  'id' => 'mw-version-entrypoints-table',
1352  'dir' => 'ltr',
1353  'lang' => 'en'
1354  ]
1355  ) .
1356  Html::openElement( 'tr' ) .
1357  Html::element(
1358  'th',
1359  $thAttributes,
1360  $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1361  ) .
1362  Html::element(
1363  'th',
1364  $thAttributes,
1365  $this->msg( 'version-entrypoints-header-url' )->text()
1366  ) .
1367  Html::closeElement( 'tr' );
1368 
1369  foreach ( $entryPoints as $message => $value ) {
1370  $url = $this->urlUtils->expand( $value, PROTO_RELATIVE );
1371  $out .= Html::openElement( 'tr' ) .
1372  Html::rawElement( 'td', [], $this->msg( $message )->parse() ) .
1373  Html::rawElement( 'td', [], Html::rawElement( 'code', [],
1374  $this->msg( new RawMessage( "[$url $value]" ) )->parse() ) ) .
1375  Html::closeElement( 'tr' );
1376  }
1377 
1378  $out .= Html::closeElement( 'table' );
1379 
1380  return $out;
1381  }
1382 
1383  protected function getGroupName() {
1384  return 'wiki';
1385  }
1386 }
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.
$modules
if(!defined( 'MEDIAWIKI')) if(ini_get( 'mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Definition: Setup.php:94
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgLang
Definition: Setup.php:528
if(!defined('MW_SETUP_CALLBACK'))
Definition: WebStart.php:88
Reads an installed.json file and provides accessors to get what is installed.
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
static getLocalInstance( $ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:565
This class is a collection of static functions that serve two purposes:
Definition: Html.php:55
Variant of the Message class.
Definition: RawMessage.php:40
Some internal bits split of from Skin.php.
Definition: Linker.php:67
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
A service to expand, parse, and otherwise manipulate URLs.
Definition: UrlUtils.php:17
static listParam(array $list, $type='text')
Definition: Message.php:1278
static getInstance( $id)
Get a cached instance of the specified type of cache object.
Definition: ObjectCache.php:82
setTOCData(TOCData $tocData)
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition: Parser.php:107
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:947
Parent class for all special pages.
Definition: SpecialPage.php:45
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:122
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:113
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:44
Interface for configuration instances.
Definition: Config.php:30
$source
const DB_REPLICA
Definition: defines.php:26
$content
Definition: router.php:76
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