MediaWiki  1.34.0
SpecialVersion.php
Go to the documentation of this file.
1 <?php
27 
33 class SpecialVersion extends SpecialPage {
34 
38  protected $firstExtOpened = false;
39 
43  protected $coreId = '';
44 
48  protected static $extensionTypes = false;
49 
50  public function __construct() {
51  parent::__construct( 'Version' );
52  }
53 
58  public function execute( $par ) {
59  global $IP;
60  $config = $this->getConfig();
61  $extensionCredits = $config->get( 'ExtensionCredits' );
62 
63  $this->setHeaders();
64  $this->outputHeader();
65  $out = $this->getOutput();
66  $out->allowClickjacking();
67 
68  // Explode the sub page information into useful bits
69  $parts = explode( '/', (string)$par );
70  $extNode = null;
71  if ( isset( $parts[1] ) ) {
72  $extName = str_replace( '_', ' ', $parts[1] );
73  // Find it!
74  foreach ( $extensionCredits as $group => $extensions ) {
75  foreach ( $extensions as $ext ) {
76  if ( isset( $ext['name'] ) && ( $ext['name'] === $extName ) ) {
77  $extNode = &$ext;
78  break 2;
79  }
80  }
81  }
82  if ( !$extNode ) {
83  $out->setStatusCode( 404 );
84  }
85  } else {
86  $extName = 'MediaWiki';
87  }
88 
89  // Now figure out what to do
90  switch ( strtolower( $parts[0] ) ) {
91  case 'credits':
92  $out->addModuleStyles( 'mediawiki.special.version' );
93 
94  $wikiText = '{{int:version-credits-not-found}}';
95  if ( $extName === 'MediaWiki' ) {
96  $wikiText = file_get_contents( $IP . '/CREDITS' );
97  // Put the contributor list into columns
98  $wikiText = str_replace(
99  [ '<!-- BEGIN CONTRIBUTOR LIST -->', '<!-- END CONTRIBUTOR LIST -->' ],
100  [ '<div class="mw-version-credits">', '</div>' ],
101  $wikiText );
102  } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
103  $file = $this->getExtAuthorsFileName( dirname( $extNode['path'] ) );
104  if ( $file ) {
105  $wikiText = file_get_contents( $file );
106  if ( substr( $file, -4 ) === '.txt' ) {
107  $wikiText = Html::element(
108  'pre',
109  [
110  'lang' => 'en',
111  'dir' => 'ltr',
112  ],
113  $wikiText
114  );
115  }
116  }
117  }
118 
119  $out->setPageTitle( $this->msg( 'version-credits-title', $extName ) );
120  $out->addWikiTextAsInterface( $wikiText );
121  break;
122 
123  case 'license':
124  $wikiText = '{{int:version-license-not-found}}';
125  if ( $extName === 'MediaWiki' ) {
126  $wikiText = file_get_contents( $IP . '/COPYING' );
127  } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
128  $file = $this->getExtLicenseFileName( dirname( $extNode['path'] ) );
129  if ( $file ) {
130  $wikiText = file_get_contents( $file );
131  $wikiText = Html::element(
132  'pre',
133  [
134  'lang' => 'en',
135  'dir' => 'ltr',
136  ],
137  $wikiText
138  );
139  }
140  }
141 
142  $out->setPageTitle( $this->msg( 'version-license-title', $extName ) );
143  $out->addWikiTextAsInterface( $wikiText );
144  break;
145 
146  default:
147  $out->addModuleStyles( 'mediawiki.special.version' );
148  $out->addWikiTextAsInterface(
149  $this->getMediaWikiCredits() .
150  $this->softwareInformation() .
151  $this->getEntryPointInfo()
152  );
153  $out->addHTML(
154  $this->getSkinCredits() .
155  $this->getExtensionCredits() .
156  $this->getExternalLibraries() .
157  $this->getParserTags() .
158  $this->getParserFunctionHooks()
159  );
160  $out->addWikiTextAsInterface( $this->getWgHooks() );
161  $out->addHTML( $this->IPInfo() );
162 
163  break;
164  }
165  }
166 
172  private static function getMediaWikiCredits() {
173  $ret = Xml::element(
174  'h2',
175  [ 'id' => 'mw-version-license' ],
176  wfMessage( 'version-license' )->text()
177  );
178 
179  // This text is always left-to-right.
180  $ret .= '<div class="plainlinks">';
181  $ret .= "__NOTOC__
183  " . '<div class="mw-version-license-info">' .
184  wfMessage( 'version-license-info' )->text() .
185  '</div>';
186  $ret .= '</div>';
187 
188  return str_replace( "\t\t", '', $ret ) . "\n";
189  }
190 
196  public static function getCopyrightAndAuthorList() {
197  global $wgLang;
198 
199  if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
200  $othersLink = '[https://www.mediawiki.org/wiki/Special:Version/Credits ' .
201  wfMessage( 'version-poweredby-others' )->text() . ']';
202  } else {
203  $othersLink = '[[Special:Version/Credits|' .
204  wfMessage( 'version-poweredby-others' )->text() . ']]';
205  }
206 
207  $translatorsLink = '[https://translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
208  wfMessage( 'version-poweredby-translators' )->text() . ']';
209 
210  $authorList = [
211  'Magnus Manske', 'Brion Vibber', 'Lee Daniel Crocker',
212  'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
213  'Niklas Laxström', 'Domas Mituzas', 'Rob Church', 'Yuri Astrakhan',
214  'Aryeh Gregor', 'Aaron Schulz', 'Andrew Garrett', 'Raimond Spekking',
215  'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
216  'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
217  'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
218  'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', 'Brad Jorsch',
219  'Bartosz Dziewoński', 'Ed Sanders', 'Moriel Schottlender',
220  'Kunal Mehta', 'James D. Forrester', 'Brian Wolff', 'Adam Shorland',
221  $othersLink, $translatorsLink
222  ];
223 
224  return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
225  $wgLang->listToText( $authorList ) )->text();
226  }
227 
233  public static function getSoftwareInformation() {
234  $dbr = wfGetDB( DB_REPLICA );
235 
236  // Put the software in an array of form 'name' => 'version'. All messages should
237  // be loaded here, so feel free to use wfMessage in the 'name'. Raw HTML or
238  // wikimarkup can be used.
239  $software = [
240  '[https://www.mediawiki.org/ MediaWiki]' => self::getVersionLinked()
241  ];
242 
243  if ( wfIsHHVM() ) {
244  $software['[https://hhvm.com/ HHVM]'] = HHVM_VERSION . " (" . PHP_SAPI . ")";
245  } else {
246  $software['[https://php.net/ PHP]'] = PHP_VERSION . " (" . PHP_SAPI . ")";
247  }
248 
249  $software[$dbr->getSoftwareLink()] = $dbr->getServerInfo();
250 
251  if ( defined( 'INTL_ICU_VERSION' ) ) {
252  $software['[http://site.icu-project.org/ ICU]'] = INTL_ICU_VERSION;
253  }
254 
255  // Allow a hook to add/remove items.
256  Hooks::run( 'SoftwareInfo', [ &$software ] );
257 
258  return $software;
259  }
260 
266  public static function softwareInformation() {
267  $out = Xml::element(
268  'h2',
269  [ 'id' => 'mw-version-software' ],
270  wfMessage( 'version-software' )->text()
271  ) .
272  Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ] ) .
273  "<tr>
274  <th>" . wfMessage( 'version-software-product' )->text() . "</th>
275  <th>" . wfMessage( 'version-software-version' )->text() . "</th>
276  </tr>\n";
277 
278  foreach ( self::getSoftwareInformation() as $name => $version ) {
279  $out .= "<tr>
280  <td>" . $name . "</td>
281  <td dir=\"ltr\">" . $version . "</td>
282  </tr>\n";
283  }
284 
285  return $out . Xml::closeElement( 'table' );
286  }
287 
295  public static function getVersion( $flags = '', $lang = null ) {
296  global $wgVersion, $IP;
297 
298  $gitInfo = self::getGitHeadSha1( $IP );
299  if ( !$gitInfo ) {
300  $version = $wgVersion;
301  } elseif ( $flags === 'nodb' ) {
302  $shortSha1 = substr( $gitInfo, 0, 7 );
303  $version = "$wgVersion ($shortSha1)";
304  } else {
305  $shortSha1 = substr( $gitInfo, 0, 7 );
306  $msg = wfMessage( 'parentheses' );
307  if ( $lang !== null ) {
308  $msg->inLanguage( $lang );
309  }
310  $shortSha1 = $msg->params( $shortSha1 )->escaped();
311  $version = "$wgVersion $shortSha1";
312  }
313 
314  return $version;
315  }
316 
324  public static function getVersionLinked() {
325  global $wgVersion;
326 
327  $gitVersion = self::getVersionLinkedGit();
328  if ( $gitVersion ) {
329  $v = $gitVersion;
330  } else {
331  $v = $wgVersion; // fallback
332  }
333 
334  return $v;
335  }
336 
340  private static function getwgVersionLinked() {
341  global $wgVersion;
342  $versionUrl = "";
343  if ( Hooks::run( 'SpecialVersionVersionUrl', [ $wgVersion, &$versionUrl ] ) ) {
344  $versionParts = [];
345  preg_match( "/^(\d+\.\d+)/", $wgVersion, $versionParts );
346  $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
347  }
348 
349  return "[$versionUrl $wgVersion]";
350  }
351 
357  private static function getVersionLinkedGit() {
358  global $IP, $wgLang;
359 
360  $gitInfo = new GitInfo( $IP );
361  $headSHA1 = $gitInfo->getHeadSHA1();
362  if ( !$headSHA1 ) {
363  return false;
364  }
365 
366  $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
367 
368  $gitHeadUrl = $gitInfo->getHeadViewUrl();
369  if ( $gitHeadUrl !== false ) {
370  $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
371  }
372 
373  $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
374  if ( $gitHeadCommitDate ) {
375  $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( $gitHeadCommitDate, true );
376  }
377 
378  return self::getwgVersionLinked() . " $shortSHA1";
379  }
380 
391  public static function getExtensionTypes() {
392  if ( self::$extensionTypes === false ) {
393  self::$extensionTypes = [
394  'specialpage' => wfMessage( 'version-specialpages' )->text(),
395  'editor' => wfMessage( 'version-editors' )->text(),
396  'parserhook' => wfMessage( 'version-parserhooks' )->text(),
397  'variable' => wfMessage( 'version-variables' )->text(),
398  'media' => wfMessage( 'version-mediahandlers' )->text(),
399  'antispam' => wfMessage( 'version-antispam' )->text(),
400  'skin' => wfMessage( 'version-skins' )->text(),
401  'api' => wfMessage( 'version-api' )->text(),
402  'other' => wfMessage( 'version-other' )->text(),
403  ];
404 
405  Hooks::run( 'ExtensionTypes', [ &self::$extensionTypes ] );
406  }
407 
408  return self::$extensionTypes;
409  }
410 
420  public static function getExtensionTypeName( $type ) {
421  $types = self::getExtensionTypes();
422 
423  return $types[$type] ?? $types['other'];
424  }
425 
431  public function getExtensionCredits() {
432  $config = $this->getConfig();
433  $extensionCredits = $config->get( 'ExtensionCredits' );
434 
435  if (
436  count( $extensionCredits ) === 0 ||
437  // Skins are displayed separately, see getSkinCredits()
438  ( count( $extensionCredits ) === 1 && isset( $extensionCredits['skin'] ) )
439  ) {
440  return '';
441  }
442 
444 
445  $out = Xml::element(
446  'h2',
447  [ 'id' => 'mw-version-ext' ],
448  $this->msg( 'version-extensions' )->text()
449  ) .
450  Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-ext' ] );
451 
452  // Make sure the 'other' type is set to an array.
453  if ( !array_key_exists( 'other', $extensionCredits ) ) {
454  $extensionCredits['other'] = [];
455  }
456 
457  // Find all extensions that do not have a valid type and give them the type 'other'.
458  foreach ( $extensionCredits as $type => $extensions ) {
459  if ( !array_key_exists( $type, $extensionTypes ) ) {
460  $extensionCredits['other'] = array_merge( $extensionCredits['other'], $extensions );
461  }
462  }
463 
464  $this->firstExtOpened = false;
465  // Loop through the extension categories to display their extensions in the list.
466  foreach ( $extensionTypes as $type => $message ) {
467  // Skins have a separate section
468  if ( $type !== 'other' && $type !== 'skin' ) {
469  $out .= $this->getExtensionCategory( $type, $message );
470  }
471  }
472 
473  // We want the 'other' type to be last in the list.
474  $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'] );
475 
476  $out .= Xml::closeElement( 'table' );
477 
478  return $out;
479  }
480 
486  public function getSkinCredits() {
487  global $wgExtensionCredits;
488  if ( !isset( $wgExtensionCredits['skin'] ) || count( $wgExtensionCredits['skin'] ) === 0 ) {
489  return '';
490  }
491 
492  $out = Xml::element(
493  'h2',
494  [ 'id' => 'mw-version-skin' ],
495  $this->msg( 'version-skins' )->text()
496  ) .
497  Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-skin' ] );
498 
499  $this->firstExtOpened = false;
500  $out .= $this->getExtensionCategory( 'skin', null );
501 
502  $out .= Xml::closeElement( 'table' );
503 
504  return $out;
505  }
506 
512  protected function getExternalLibraries() {
513  global $IP;
514  $path = "$IP/vendor/composer/installed.json";
515  if ( !file_exists( $path ) ) {
516  return '';
517  }
518 
519  $installed = new ComposerInstalled( $path );
520  $out = Html::element(
521  'h2',
522  [ 'id' => 'mw-version-libraries' ],
523  $this->msg( 'version-libraries' )->text()
524  );
525  $out .= Html::openElement(
526  'table',
527  [ 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries' ]
528  );
529  $out .= Html::openElement( 'tr' )
530  . Html::element( 'th', [], $this->msg( 'version-libraries-library' )->text() )
531  . Html::element( 'th', [], $this->msg( 'version-libraries-version' )->text() )
532  . Html::element( 'th', [], $this->msg( 'version-libraries-license' )->text() )
533  . Html::element( 'th', [], $this->msg( 'version-libraries-description' )->text() )
534  . Html::element( 'th', [], $this->msg( 'version-libraries-authors' )->text() )
535  . Html::closeElement( 'tr' );
536 
537  foreach ( $installed->getInstalledDependencies() as $name => $info ) {
538  if ( strpos( $info['type'], 'mediawiki-' ) === 0 ) {
539  // Skip any extensions or skins since they'll be listed
540  // in their proper section
541  continue;
542  }
543  $authors = array_map( function ( $arr ) {
544  // If a homepage is set, link to it
545  if ( isset( $arr['homepage'] ) ) {
546  return "[{$arr['homepage']} {$arr['name']}]";
547  }
548  return $arr['name'];
549  }, $info['authors'] );
550  $authors = $this->listAuthors( $authors, false, "$IP/vendor/$name" );
551 
552  // We can safely assume that the libraries' names and descriptions
553  // are written in English and aren't going to be translated,
554  // so set appropriate lang and dir attributes
555  $out .= Html::openElement( 'tr' )
556  . Html::rawElement(
557  'td',
558  [],
560  "https://packagist.org/packages/$name", $name,
561  true, '',
562  [ 'class' => 'mw-version-library-name' ]
563  )
564  )
565  . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
566  . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['licenses'] ) )
567  . Html::element( 'td', [ 'lang' => 'en', 'dir' => 'ltr' ], $info['description'] )
568  . Html::rawElement( 'td', [], $authors )
569  . Html::closeElement( 'tr' );
570  }
571  $out .= Html::closeElement( 'table' );
572 
573  return $out;
574  }
575 
581  protected function getParserTags() {
582  $tags = MediaWikiServices::getInstance()->getParser()->getTags();
583 
584  if ( count( $tags ) ) {
585  $out = Html::rawElement(
586  'h2',
587  [
588  'class' => 'mw-headline plainlinks',
589  'id' => 'mw-version-parser-extensiontags',
590  ],
592  'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
593  $this->msg( 'version-parser-extensiontags' )->parse(),
594  false /* msg()->parse() already escapes */
595  )
596  );
597 
598  array_walk( $tags, function ( &$value ) {
599  // Bidirectional isolation improves readability in RTL wikis
600  $value = Html::element(
601  'bdi',
602  // Prevent < and > from slipping to another line
603  [
604  'style' => 'white-space: nowrap;',
605  ],
606  "<$value>"
607  );
608  } );
609 
610  $out .= $this->listToText( $tags );
611  } else {
612  $out = '';
613  }
614 
615  return $out;
616  }
617 
623  protected function getParserFunctionHooks() {
624  $fhooks = MediaWikiServices::getInstance()->getParser()->getFunctionHooks();
625  if ( count( $fhooks ) ) {
626  $out = Html::rawElement(
627  'h2',
628  [
629  'class' => 'mw-headline plainlinks',
630  'id' => 'mw-version-parser-function-hooks',
631  ],
633  'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
634  $this->msg( 'version-parser-function-hooks' )->parse(),
635  false /* msg()->parse() already escapes */
636  )
637  );
638 
639  $out .= $this->listToText( $fhooks );
640  } else {
641  $out = '';
642  }
643 
644  return $out;
645  }
646 
657  protected function getExtensionCategory( $type, $message ) {
658  $config = $this->getConfig();
659  $extensionCredits = $config->get( 'ExtensionCredits' );
660 
661  $out = '';
662 
663  if ( array_key_exists( $type, $extensionCredits ) && count( $extensionCredits[$type] ) > 0 ) {
664  $out .= $this->openExtType( $message, 'credits-' . $type );
665 
666  usort( $extensionCredits[$type], [ $this, 'compare' ] );
667 
668  foreach ( $extensionCredits[$type] as $extension ) {
669  $out .= $this->getCreditsForExtension( $type, $extension );
670  }
671  }
672 
673  return $out;
674  }
675 
682  public function compare( $a, $b ) {
683  return $this->getLanguage()->lc( $a['name'] ) <=> $this->getLanguage()->lc( $b['name'] );
684  }
685 
704  public function getCreditsForExtension( $type, array $extension ) {
705  $out = $this->getOutput();
706 
707  // We must obtain the information for all the bits and pieces!
708  // ... such as extension names and links
709  if ( isset( $extension['namemsg'] ) ) {
710  // Localized name of extension
711  $extensionName = $this->msg( $extension['namemsg'] )->text();
712  } elseif ( isset( $extension['name'] ) ) {
713  // Non localized version
714  $extensionName = $extension['name'];
715  } else {
716  $extensionName = $this->msg( 'version-no-ext-name' )->text();
717  }
718 
719  if ( isset( $extension['url'] ) ) {
720  $extensionNameLink = Linker::makeExternalLink(
721  $extension['url'],
722  $extensionName,
723  true,
724  '',
725  [ 'class' => 'mw-version-ext-name' ]
726  );
727  } else {
728  $extensionNameLink = htmlspecialchars( $extensionName );
729  }
730 
731  // ... and the version information
732  // If the extension path is set we will check that directory for GIT
733  // metadata in an attempt to extract date and vcs commit metadata.
734  $canonicalVersion = '&ndash;';
735  $extensionPath = null;
736  $vcsVersion = null;
737  $vcsLink = null;
738  $vcsDate = null;
739 
740  if ( isset( $extension['version'] ) ) {
741  $canonicalVersion = $out->parseInlineAsInterface( $extension['version'] );
742  }
743 
744  if ( isset( $extension['path'] ) ) {
745  global $IP;
746  $extensionPath = dirname( $extension['path'] );
747  if ( $this->coreId == '' ) {
748  wfDebug( 'Looking up core head id' );
749  $coreHeadSHA1 = self::getGitHeadSha1( $IP );
750  if ( $coreHeadSHA1 ) {
751  $this->coreId = $coreHeadSHA1;
752  }
753  }
755  $memcKey = $cache->makeKey(
756  'specialversion-ext-version-text', $extension['path'], $this->coreId
757  );
758  list( $vcsVersion, $vcsLink, $vcsDate ) = $cache->get( $memcKey );
759 
760  if ( !$vcsVersion ) {
761  wfDebug( "Getting VCS info for extension {$extension['name']}" );
762  $gitInfo = new GitInfo( $extensionPath );
763  $vcsVersion = $gitInfo->getHeadSHA1();
764  if ( $vcsVersion !== false ) {
765  $vcsVersion = substr( $vcsVersion, 0, 7 );
766  $vcsLink = $gitInfo->getHeadViewUrl();
767  $vcsDate = $gitInfo->getHeadCommitDate();
768  }
769  $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
770  } else {
771  wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" );
772  }
773  }
774 
775  $versionString = Html::rawElement(
776  'span',
777  [ 'class' => 'mw-version-ext-version' ],
778  $canonicalVersion
779  );
780 
781  if ( $vcsVersion ) {
782  if ( $vcsLink ) {
783  $vcsVerString = Linker::makeExternalLink(
784  $vcsLink,
785  $this->msg( 'version-version', $vcsVersion ),
786  true,
787  '',
788  [ 'class' => 'mw-version-ext-vcs-version' ]
789  );
790  } else {
791  $vcsVerString = Html::element( 'span',
792  [ 'class' => 'mw-version-ext-vcs-version' ],
793  "({$vcsVersion})"
794  );
795  }
796  $versionString .= " {$vcsVerString}";
797 
798  if ( $vcsDate ) {
799  $vcsTimeString = Html::element( 'span',
800  [ 'class' => 'mw-version-ext-vcs-timestamp' ],
801  $this->getLanguage()->timeanddate( $vcsDate, true )
802  );
803  $versionString .= " {$vcsTimeString}";
804  }
805  $versionString = Html::rawElement( 'span',
806  [ 'class' => 'mw-version-ext-meta-version' ],
807  $versionString
808  );
809  }
810 
811  // ... and license information; if a license file exists we
812  // will link to it
813  $licenseLink = '';
814  if ( isset( $extension['name'] ) ) {
815  $licenseName = null;
816  if ( isset( $extension['license-name'] ) ) {
817  $licenseName = new HtmlArmor( $out->parseInlineAsInterface( $extension['license-name'] ) );
818  } elseif ( $this->getExtLicenseFileName( $extensionPath ) ) {
819  $licenseName = $this->msg( 'version-ext-license' )->text();
820  }
821  if ( $licenseName !== null ) {
822  $licenseLink = $this->getLinkRenderer()->makeLink(
823  $this->getPageTitle( 'License/' . $extension['name'] ),
824  $licenseName,
825  [
826  'class' => 'mw-version-ext-license',
827  'dir' => 'auto',
828  ]
829  );
830  }
831  }
832 
833  // ... and generate the description; which can be a parameterized l10n message
834  // in the form [ <msgname>, <parameter>, <parameter>... ] or just a straight
835  // up string
836  if ( isset( $extension['descriptionmsg'] ) ) {
837  // Localized description of extension
838  $descriptionMsg = $extension['descriptionmsg'];
839 
840  if ( is_array( $descriptionMsg ) ) {
841  $descriptionMsgKey = $descriptionMsg[0]; // Get the message key
842  array_shift( $descriptionMsg ); // Shift out the message key to get the parameters only
843  array_map( "htmlspecialchars", $descriptionMsg ); // For sanity
844  $description = $this->msg( $descriptionMsgKey, $descriptionMsg )->text();
845  } else {
846  $description = $this->msg( $descriptionMsg )->text();
847  }
848  } elseif ( isset( $extension['description'] ) ) {
849  // Non localized version
850  $description = $extension['description'];
851  } else {
852  $description = '';
853  }
854  $description = $out->parseInlineAsInterface( $description );
855 
856  // ... now get the authors for this extension
857  $authors = $extension['author'] ?? [];
858  $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
859 
860  // Finally! Create the table
861  $html = Html::openElement( 'tr', [
862  'class' => 'mw-version-ext',
863  'id' => Sanitizer::escapeIdForAttribute( 'mw-version-ext-' . $type . '-' . $extension['name'] )
864  ]
865  );
866 
867  $html .= Html::rawElement( 'td', [], $extensionNameLink );
868  $html .= Html::rawElement( 'td', [], $versionString );
869  $html .= Html::rawElement( 'td', [], $licenseLink );
870  $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-description' ], $description );
871  $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-authors' ], $authors );
872 
873  $html .= Html::closeElement( 'tr' );
874 
875  return $html;
876  }
877 
883  private function getWgHooks() {
885 
886  if ( $wgSpecialVersionShowHooks && count( $wgHooks ) ) {
887  $myWgHooks = $wgHooks;
888  ksort( $myWgHooks );
889 
890  $ret = [];
891  $ret[] = '== {{int:version-hooks}} ==';
892  $ret[] = Html::openElement( 'table', [ 'class' => 'wikitable', 'id' => 'sv-hooks' ] );
893  $ret[] = Html::openElement( 'tr' );
894  $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-name' )->text() );
895  $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-subscribedby' )->text() );
896  $ret[] = Html::closeElement( 'tr' );
897 
898  foreach ( $myWgHooks as $hook => $hooks ) {
899  $ret[] = Html::openElement( 'tr' );
900  $ret[] = Html::element( 'td', [], $hook );
901  $ret[] = Html::element( 'td', [], $this->listToText( $hooks ) );
902  $ret[] = Html::closeElement( 'tr' );
903  }
904 
905  $ret[] = Html::closeElement( 'table' );
906 
907  return implode( "\n", $ret );
908  }
909 
910  return '';
911  }
912 
913  private function openExtType( $text = null, $name = null ) {
914  $out = '';
915 
916  $opt = [ 'colspan' => 5 ];
917  if ( $this->firstExtOpened ) {
918  // Insert a spacing line
919  $out .= Html::rawElement( 'tr', [ 'class' => 'sv-space' ],
920  Html::element( 'td', $opt )
921  );
922  }
923  $this->firstExtOpened = true;
924 
925  if ( $name ) {
926  $opt['id'] = "sv-$name";
927  }
928 
929  if ( $text !== null ) {
930  $out .= Html::rawElement( 'tr', [],
931  Html::element( 'th', $opt, $text )
932  );
933  }
934 
935  $firstHeadingMsg = ( $name === 'credits-skin' )
936  ? 'version-skin-colheader-name'
937  : 'version-ext-colheader-name';
938  $out .= Html::openElement( 'tr' );
939  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
940  $this->msg( $firstHeadingMsg )->text() );
941  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
942  $this->msg( 'version-ext-colheader-version' )->text() );
943  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
944  $this->msg( 'version-ext-colheader-license' )->text() );
945  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
946  $this->msg( 'version-ext-colheader-description' )->text() );
947  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
948  $this->msg( 'version-ext-colheader-credits' )->text() );
949  $out .= Html::closeElement( 'tr' );
950 
951  return $out;
952  }
953 
959  private function IPInfo() {
960  $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
961 
962  return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
963  }
964 
986  public function listAuthors( $authors, $extName, $extDir ) {
987  $hasOthers = false;
988  $linkRenderer = $this->getLinkRenderer();
989 
990  $list = [];
991  $authors = (array)$authors;
992 
993  // Special case: if the authors array has only one item and it is "...",
994  // it should not be rendered as the "version-poweredby-others" i18n msg,
995  // but rather as "version-poweredby-various" i18n msg instead.
996  if ( count( $authors ) === 1 && $authors[0] === '...' ) {
997  // Link to the extension's or skin's AUTHORS or CREDITS file, if there is
998  // such a file; otherwise just return the i18n msg as-is
999  if ( $extName && $this->getExtAuthorsFileName( $extDir ) ) {
1000  return $linkRenderer->makeLink(
1001  $this->getPageTitle( "Credits/$extName" ),
1002  $this->msg( 'version-poweredby-various' )->text()
1003  );
1004  } else {
1005  return $this->msg( 'version-poweredby-various' )->escaped();
1006  }
1007  }
1008 
1009  // Otherwise, if we have an actual array that has more than one item,
1010  // process each array item as usual
1011  foreach ( $authors as $item ) {
1012  if ( $item == '...' ) {
1013  $hasOthers = true;
1014 
1015  if ( $extName && $this->getExtAuthorsFileName( $extDir ) ) {
1016  $text = $linkRenderer->makeLink(
1017  $this->getPageTitle( "Credits/$extName" ),
1018  $this->msg( 'version-poweredby-others' )->text()
1019  );
1020  } else {
1021  $text = $this->msg( 'version-poweredby-others' )->escaped();
1022  }
1023  $list[] = $text;
1024  } elseif ( substr( $item, -5 ) == ' ...]' ) {
1025  $hasOthers = true;
1026  $list[] = $this->getOutput()->parseInlineAsInterface(
1027  substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
1028  );
1029  } else {
1030  $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1031  }
1032  }
1033 
1034  if ( $extName && !$hasOthers && $this->getExtAuthorsFileName( $extDir ) ) {
1035  $list[] = $text = $linkRenderer->makeLink(
1036  $this->getPageTitle( "Credits/$extName" ),
1037  $this->msg( 'version-poweredby-others' )->text()
1038  );
1039  }
1040 
1041  return $this->listToText( $list, false );
1042  }
1043 
1055  public static function getExtAuthorsFileName( $extDir ) {
1056  if ( !$extDir ) {
1057  return false;
1058  }
1059 
1060  foreach ( scandir( $extDir ) as $file ) {
1061  $fullPath = $extDir . DIRECTORY_SEPARATOR . $file;
1062  if ( preg_match( '/^((AUTHORS)|(CREDITS))(\.txt|\.wiki|\.mediawiki)?$/', $file ) &&
1063  is_readable( $fullPath ) &&
1064  is_file( $fullPath )
1065  ) {
1066  return $fullPath;
1067  }
1068  }
1069 
1070  return false;
1071  }
1072 
1084  public static function getExtLicenseFileName( $extDir ) {
1085  if ( !$extDir ) {
1086  return false;
1087  }
1088 
1089  foreach ( scandir( $extDir ) as $file ) {
1090  $fullPath = $extDir . DIRECTORY_SEPARATOR . $file;
1091  if ( preg_match( '/^((COPYING)|(LICENSE))(\.txt)?$/', $file ) &&
1092  is_readable( $fullPath ) &&
1093  is_file( $fullPath )
1094  ) {
1095  return $fullPath;
1096  }
1097  }
1098 
1099  return false;
1100  }
1101 
1110  public function listToText( $list, $sort = true ) {
1111  if ( !count( $list ) ) {
1112  return '';
1113  }
1114  if ( $sort ) {
1115  sort( $list );
1116  }
1117 
1118  return $this->getLanguage()
1119  ->listToText( array_map( [ __CLASS__, 'arrayToString' ], $list ) );
1120  }
1121 
1130  public static function arrayToString( $list ) {
1131  if ( is_array( $list ) && count( $list ) == 1 ) {
1132  $list = $list[0];
1133  }
1134  if ( $list instanceof Closure ) {
1135  // Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$"
1136  return 'Closure';
1137  } elseif ( is_object( $list ) ) {
1138  $class = wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1139 
1140  return $class;
1141  } elseif ( !is_array( $list ) ) {
1142  return $list;
1143  } else {
1144  if ( is_object( $list[0] ) ) {
1145  $class = get_class( $list[0] );
1146  } else {
1147  $class = $list[0];
1148  }
1149 
1150  return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1151  }
1152  }
1153 
1158  public static function getGitHeadSha1( $dir ) {
1159  $repo = new GitInfo( $dir );
1160 
1161  return $repo->getHeadSHA1();
1162  }
1163 
1168  public static function getGitCurrentBranch( $dir ) {
1169  $repo = new GitInfo( $dir );
1170  return $repo->getCurrentBranch();
1171  }
1172 
1177  public function getEntryPointInfo() {
1178  $config = $this->getConfig();
1179  $scriptPath = $config->get( 'ScriptPath' ) ?: '/';
1180 
1181  $entryPoints = [
1182  'version-entrypoints-articlepath' => $config->get( 'ArticlePath' ),
1183  'version-entrypoints-scriptpath' => $scriptPath,
1184  'version-entrypoints-index-php' => wfScript( 'index' ),
1185  'version-entrypoints-api-php' => wfScript( 'api' ),
1186  'version-entrypoints-load-php' => wfScript( 'load' ),
1187  ];
1188 
1189  $language = $this->getLanguage();
1190  $thAttribures = [
1191  'dir' => $language->getDir(),
1192  'lang' => $language->getHtmlCode()
1193  ];
1194  $out = Html::element(
1195  'h2',
1196  [ 'id' => 'mw-version-entrypoints' ],
1197  $this->msg( 'version-entrypoints' )->text()
1198  ) .
1199  Html::openElement( 'table',
1200  [
1201  'class' => 'wikitable plainlinks',
1202  'id' => 'mw-version-entrypoints-table',
1203  'dir' => 'ltr',
1204  'lang' => 'en'
1205  ]
1206  ) .
1207  Html::openElement( 'tr' ) .
1208  Html::element(
1209  'th',
1210  $thAttribures,
1211  $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1212  ) .
1213  Html::element(
1214  'th',
1215  $thAttribures,
1216  $this->msg( 'version-entrypoints-header-url' )->text()
1217  ) .
1218  Html::closeElement( 'tr' );
1219 
1220  foreach ( $entryPoints as $message => $value ) {
1221  $url = wfExpandUrl( $value, PROTO_RELATIVE );
1222  $out .= Html::openElement( 'tr' ) .
1223  // ->plain() looks like it should be ->parse(), but this function
1224  // returns wikitext, not HTML, boo
1225  Html::rawElement( 'td', [], $this->msg( $message )->plain() ) .
1226  Html::rawElement( 'td', [], Html::rawElement( 'code', [], "[$url $value]" ) ) .
1227  Html::closeElement( 'tr' );
1228  }
1229 
1230  $out .= Html::closeElement( 'table' );
1231 
1232  return $out;
1233  }
1234 
1235  protected function getGroupName() {
1236  return 'wiki';
1237  }
1238 }
SpecialVersion\getExtensionCredits
getExtensionCredits()
Generate wikitext showing the name, URL, author and description of each extension.
Definition: SpecialVersion.php:431
SpecialVersion\openExtType
openExtType( $text=null, $name=null)
Definition: SpecialVersion.php:913
SpecialPage\getPageTitle
getPageTitle( $subpage=false)
Get a self-referential title object.
Definition: SpecialPage.php:672
SpecialVersion\__construct
__construct()
Definition: SpecialVersion.php:50
SpecialPage\msg
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
Definition: SpecialPage.php:792
$wgExtensionCredits
$wgExtensionCredits['specialpage'][]
Definition: ReplaceText.php:40
SpecialVersion\listAuthors
listAuthors( $authors, $extName, $extDir)
Return a formatted unsorted list of authors.
Definition: SpecialVersion.php:986
HtmlArmor
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:28
SpecialVersion\getExtAuthorsFileName
static getExtAuthorsFileName( $extDir)
Obtains the full path of an extensions authors or credits file if one exists.
Definition: SpecialVersion.php:1055
SpecialPage\getOutput
getOutput()
Get the OutputPage being used for this instance.
Definition: SpecialPage.php:719
SpecialVersion\listToText
listToText( $list, $sort=true)
Convert an array of items into a list for display.
Definition: SpecialVersion.php:1110
SpecialVersion\getMediaWikiCredits
static getMediaWikiCredits()
Returns wiki text showing the license information.
Definition: SpecialVersion.php:172
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:117
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:33
SpecialVersion\getCreditsForExtension
getCreditsForExtension( $type, array $extension)
Creates and formats a version line for a single extension.
Definition: SpecialVersion.php:704
SpecialVersion\$coreId
string $coreId
The current rev id/SHA hash of MediaWiki core.
Definition: SpecialVersion.php:43
SpecialVersion\compare
compare( $a, $b)
Callback to sort extensions by type.
Definition: SpecialVersion.php:682
SpecialVersion\getCopyrightAndAuthorList
static getCopyrightAndAuthorList()
Get the "MediaWiki is copyright 2001-20xx by lots of cool guys" text.
Definition: SpecialVersion.php:196
$wgVersion
$wgVersion
MediaWiki version number.
Definition: DefaultSettings.php:75
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
$wgHooks
$wgHooks['AdminLinks'][]
Definition: ReplaceText.php:58
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1264
SpecialPage\getLanguage
getLanguage()
Shortcut to get user's language.
Definition: SpecialPage.php:749
SpecialVersion\getParserFunctionHooks
getParserFunctionHooks()
Obtains a list of installed parser function hooks and the associated H2 header.
Definition: SpecialVersion.php:623
Xml\openElement
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:108
SpecialVersion\getVersionLinked
static getVersionLinked()
Return a wikitext-formatted string of the MediaWiki version with a link to the Git SHA1 of head if av...
Definition: SpecialVersion.php:324
$dbr
$dbr
Definition: testCompression.php:50
SpecialVersion\arrayToString
static arrayToString( $list)
Convert an array or object to a string for display.
Definition: SpecialVersion.php:1130
SpecialVersion\softwareInformation
static softwareInformation()
Returns HTML showing the third party software versions (apache, php, mysql).
Definition: SpecialVersion.php:266
SpecialVersion\IPInfo
IPInfo()
Get information about client's IP address.
Definition: SpecialVersion.php:959
SpecialVersion\getExternalLibraries
getExternalLibraries()
Generate an HTML table for external libraries that are installed.
Definition: SpecialVersion.php:512
SpecialPage\getConfig
getConfig()
Shortcut to get main config object.
Definition: SpecialPage.php:758
SpecialVersion\getSkinCredits
getSkinCredits()
Generate wikitext showing the name, URL, author and description of each skin.
Definition: SpecialVersion.php:486
SpecialVersion\getSoftwareInformation
static getSoftwareInformation()
Definition: SpecialVersion.php:233
wfScript
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
Definition: GlobalFunctions.php:2642
SpecialVersion\getExtensionTypeName
static getExtensionTypeName( $type)
Returns the internationalized name for an extension type.
Definition: SpecialVersion.php:420
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2575
$wgLang
$wgLang
Definition: Setup.php:881
$IP
$IP
Definition: update.php:3
Xml\element
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:41
SpecialPage\setHeaders
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
Definition: SpecialPage.php:537
SpecialVersion\getExtensionTypes
static getExtensionTypes()
Returns an array with the base extension types.
Definition: SpecialVersion.php:391
Linker\makeExternalLink
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition: Linker.php:848
wfGetCache
wfGetCache( $cacheType)
Get a specific cache object.
Definition: GlobalFunctions.php:2848
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
SpecialVersion\getWgHooks
getWgHooks()
Generate wikitext showing hooks in $wgHooks.
Definition: SpecialVersion.php:883
SpecialVersion\$extensionTypes
static string[] false $extensionTypes
Lazy initialized key/value with message content.
Definition: SpecialVersion.php:48
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:913
$sort
$sort
Definition: profileinfo.php:331
SpecialVersion\getGroupName
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
Definition: SpecialVersion.php:1235
SpecialVersion\getExtLicenseFileName
static getExtLicenseFileName( $extDir)
Obtains the full path of an extensions copying or license file if one exists.
Definition: SpecialVersion.php:1084
SpecialVersion\getwgVersionLinked
static getwgVersionLinked()
Definition: SpecialVersion.php:340
SpecialVersion\execute
execute( $par)
main()
Definition: SpecialVersion.php:58
SpecialVersion\getVersion
static getVersion( $flags='', $lang=null)
Return a string of the MediaWiki version with Git revision if available.
Definition: SpecialVersion.php:295
GitInfo
Definition: GitInfo.php:28
SpecialVersion\getGitCurrentBranch
static getGitCurrentBranch( $dir)
Definition: SpecialVersion.php:1168
SpecialPage
Parent class for all special pages.
Definition: SpecialPage.php:37
SpecialPage\getRequest
getRequest()
Get the WebRequest being used for this instance.
Definition: SpecialPage.php:709
CACHE_ANYTHING
const CACHE_ANYTHING
Definition: Defines.php:81
SpecialVersion
Give information about the version of MediaWiki, PHP, the DB and extensions.
Definition: SpecialVersion.php:33
PROTO_RELATIVE
const PROTO_RELATIVE
Definition: Defines.php:201
SpecialVersion\getGitHeadSha1
static getGitHeadSha1( $dir)
Definition: SpecialVersion.php:1158
SpecialVersion\getEntryPointInfo
getEntryPointInfo()
Get the list of entry points and their URLs.
Definition: SpecialVersion.php:1177
SpecialPage\getLinkRenderer
getLinkRenderer()
Definition: SpecialPage.php:904
SpecialVersion\$firstExtOpened
bool $firstExtOpened
Definition: SpecialVersion.php:38
SpecialVersion\getVersionLinkedGit
static getVersionLinkedGit()
Definition: SpecialVersion.php:357
Xml\closeElement
static closeElement( $element)
Shortcut to close an XML element.
Definition: Xml.php:117
$cache
$cache
Definition: mcc.php:33
$path
$path
Definition: NoLocalSettings.php:25
$ext
if(!is_readable( $file)) $ext
Definition: router.php:48
SpecialPage\$linkRenderer
MediaWiki Linker LinkRenderer null $linkRenderer
Definition: SpecialPage.php:67
SpecialVersion\getParserTags
getParserTags()
Obtains a list of installed parser tags and the associated H2 header.
Definition: SpecialVersion.php:581
wfIsHHVM
wfIsHHVM()
Check if we are running under HHVM.
Definition: GlobalFunctions.php:1921
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
MWTimestamp\getLocalInstance
static getLocalInstance( $ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
Definition: MWTimestamp.php:204
$wgSpecialVersionShowHooks
$wgSpecialVersionShowHooks
Show the contents of $wgHooks in Special:Version.
Definition: DefaultSettings.php:6292
ComposerInstalled
Reads an installed.json file and provides accessors to get what is installed.
Definition: ComposerInstalled.php:9
SpecialPage\outputHeader
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
Definition: SpecialPage.php:639
wfExpandUrl
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
Definition: GlobalFunctions.php:491
$type
$type
Definition: testCompression.php:48
SpecialVersion\getExtensionCategory
getExtensionCategory( $type, $message)
Creates and returns the HTML for a single extension category.
Definition: SpecialVersion.php:657