MediaWiki  master
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__
182  " . self::getCopyrightAndAuthorList() . "\n
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  '[https://php.net/ PHP]' => PHP_VERSION . " (" . PHP_SAPI . ")",
242  $dbr->getSoftwareLink() => $dbr->getServerInfo(),
243  ];
244 
245  if ( defined( 'INTL_ICU_VERSION' ) ) {
246  $software['[http://site.icu-project.org/ ICU]'] = INTL_ICU_VERSION;
247  }
248 
249  // Allow a hook to add/remove items.
250  Hooks::run( 'SoftwareInfo', [ &$software ] );
251 
252  return $software;
253  }
254 
260  public static function softwareInformation() {
261  $out = Xml::element(
262  'h2',
263  [ 'id' => 'mw-version-software' ],
264  wfMessage( 'version-software' )->text()
265  ) .
266  Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ] ) .
267  "<tr>
268  <th>" . wfMessage( 'version-software-product' )->text() . "</th>
269  <th>" . wfMessage( 'version-software-version' )->text() . "</th>
270  </tr>\n";
271 
272  foreach ( self::getSoftwareInformation() as $name => $version ) {
273  $out .= "<tr>
274  <td>" . $name . "</td>
275  <td dir=\"ltr\">" . $version . "</td>
276  </tr>\n";
277  }
278 
279  return $out . Xml::closeElement( 'table' );
280  }
281 
289  public static function getVersion( $flags = '', $lang = null ) {
290  global $wgVersion, $IP;
291 
292  $gitInfo = self::getGitHeadSha1( $IP );
293  if ( !$gitInfo ) {
294  $version = $wgVersion;
295  } elseif ( $flags === 'nodb' ) {
296  $shortSha1 = substr( $gitInfo, 0, 7 );
297  $version = "$wgVersion ($shortSha1)";
298  } else {
299  $shortSha1 = substr( $gitInfo, 0, 7 );
300  $msg = wfMessage( 'parentheses' );
301  if ( $lang !== null ) {
302  $msg->inLanguage( $lang );
303  }
304  $shortSha1 = $msg->params( $shortSha1 )->escaped();
305  $version = "$wgVersion $shortSha1";
306  }
307 
308  return $version;
309  }
310 
318  public static function getVersionLinked() {
319  global $wgVersion;
320 
321  $gitVersion = self::getVersionLinkedGit();
322  if ( $gitVersion ) {
323  $v = $gitVersion;
324  } else {
325  $v = $wgVersion; // fallback
326  }
327 
328  return $v;
329  }
330 
334  private static function getwgVersionLinked() {
335  global $wgVersion;
336  $versionUrl = "";
337  if ( Hooks::run( 'SpecialVersionVersionUrl', [ $wgVersion, &$versionUrl ] ) ) {
338  $versionParts = [];
339  preg_match( "/^(\d+\.\d+)/", $wgVersion, $versionParts );
340  $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
341  }
342 
343  return "[$versionUrl $wgVersion]";
344  }
345 
351  private static function getVersionLinkedGit() {
352  global $IP, $wgLang;
353 
354  $gitInfo = new GitInfo( $IP );
355  $headSHA1 = $gitInfo->getHeadSHA1();
356  if ( !$headSHA1 ) {
357  return false;
358  }
359 
360  $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
361 
362  $gitHeadUrl = $gitInfo->getHeadViewUrl();
363  if ( $gitHeadUrl !== false ) {
364  $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
365  }
366 
367  $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
368  if ( $gitHeadCommitDate ) {
369  $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( $gitHeadCommitDate, true );
370  }
371 
372  return self::getwgVersionLinked() . " $shortSHA1";
373  }
374 
385  public static function getExtensionTypes() {
386  if ( self::$extensionTypes === false ) {
387  self::$extensionTypes = [
388  'specialpage' => wfMessage( 'version-specialpages' )->text(),
389  'editor' => wfMessage( 'version-editors' )->text(),
390  'parserhook' => wfMessage( 'version-parserhooks' )->text(),
391  'variable' => wfMessage( 'version-variables' )->text(),
392  'media' => wfMessage( 'version-mediahandlers' )->text(),
393  'antispam' => wfMessage( 'version-antispam' )->text(),
394  'skin' => wfMessage( 'version-skins' )->text(),
395  'api' => wfMessage( 'version-api' )->text(),
396  'other' => wfMessage( 'version-other' )->text(),
397  ];
398 
399  Hooks::run( 'ExtensionTypes', [ &self::$extensionTypes ] );
400  }
401 
402  return self::$extensionTypes;
403  }
404 
414  public static function getExtensionTypeName( $type ) {
415  $types = self::getExtensionTypes();
416 
417  return $types[$type] ?? $types['other'];
418  }
419 
425  public function getExtensionCredits() {
426  $config = $this->getConfig();
427  $extensionCredits = $config->get( 'ExtensionCredits' );
428 
429  if (
430  count( $extensionCredits ) === 0 ||
431  // Skins are displayed separately, see getSkinCredits()
432  ( count( $extensionCredits ) === 1 && isset( $extensionCredits['skin'] ) )
433  ) {
434  return '';
435  }
436 
437  $extensionTypes = self::getExtensionTypes();
438 
439  $out = Xml::element(
440  'h2',
441  [ 'id' => 'mw-version-ext' ],
442  $this->msg( 'version-extensions' )->text()
443  ) .
444  Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-ext' ] );
445 
446  // Make sure the 'other' type is set to an array.
447  if ( !array_key_exists( 'other', $extensionCredits ) ) {
448  $extensionCredits['other'] = [];
449  }
450 
451  // Find all extensions that do not have a valid type and give them the type 'other'.
452  foreach ( $extensionCredits as $type => $extensions ) {
453  if ( !array_key_exists( $type, $extensionTypes ) ) {
454  $extensionCredits['other'] = array_merge( $extensionCredits['other'], $extensions );
455  }
456  }
457 
458  $this->firstExtOpened = false;
459  // Loop through the extension categories to display their extensions in the list.
460  foreach ( $extensionTypes as $type => $message ) {
461  // Skins have a separate section
462  if ( $type !== 'other' && $type !== 'skin' ) {
463  $out .= $this->getExtensionCategory( $type, $message );
464  }
465  }
466 
467  // We want the 'other' type to be last in the list.
468  $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'] );
469 
470  $out .= Xml::closeElement( 'table' );
471 
472  return $out;
473  }
474 
480  public function getSkinCredits() {
481  global $wgExtensionCredits;
482  if ( !isset( $wgExtensionCredits['skin'] ) || count( $wgExtensionCredits['skin'] ) === 0 ) {
483  return '';
484  }
485 
486  $out = Xml::element(
487  'h2',
488  [ 'id' => 'mw-version-skin' ],
489  $this->msg( 'version-skins' )->text()
490  ) .
491  Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-skin' ] );
492 
493  $this->firstExtOpened = false;
494  $out .= $this->getExtensionCategory( 'skin', null );
495 
496  $out .= Xml::closeElement( 'table' );
497 
498  return $out;
499  }
500 
506  protected function getExternalLibraries() {
507  global $IP;
508  $path = "$IP/vendor/composer/installed.json";
509  if ( !file_exists( $path ) ) {
510  return '';
511  }
512 
513  $installed = new ComposerInstalled( $path );
514  $out = Html::element(
515  'h2',
516  [ 'id' => 'mw-version-libraries' ],
517  $this->msg( 'version-libraries' )->text()
518  );
519  $out .= Html::openElement(
520  'table',
521  [ 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries' ]
522  );
523  $out .= Html::openElement( 'tr' )
524  . Html::element( 'th', [], $this->msg( 'version-libraries-library' )->text() )
525  . Html::element( 'th', [], $this->msg( 'version-libraries-version' )->text() )
526  . Html::element( 'th', [], $this->msg( 'version-libraries-license' )->text() )
527  . Html::element( 'th', [], $this->msg( 'version-libraries-description' )->text() )
528  . Html::element( 'th', [], $this->msg( 'version-libraries-authors' )->text() )
529  . Html::closeElement( 'tr' );
530 
531  foreach ( $installed->getInstalledDependencies() as $name => $info ) {
532  if ( strpos( $info['type'], 'mediawiki-' ) === 0 ) {
533  // Skip any extensions or skins since they'll be listed
534  // in their proper section
535  continue;
536  }
537  $authors = array_map( function ( $arr ) {
538  // If a homepage is set, link to it
539  if ( isset( $arr['homepage'] ) ) {
540  return "[{$arr['homepage']} {$arr['name']}]";
541  }
542  return $arr['name'];
543  }, $info['authors'] );
544  $authors = $this->listAuthors( $authors, false, "$IP/vendor/$name" );
545 
546  // We can safely assume that the libraries' names and descriptions
547  // are written in English and aren't going to be translated,
548  // so set appropriate lang and dir attributes
549  $out .= Html::openElement( 'tr' )
551  'td',
552  [],
554  "https://packagist.org/packages/$name", $name,
555  true, '',
556  [ 'class' => 'mw-version-library-name' ]
557  )
558  )
559  . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
560  . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['licenses'] ) )
561  . Html::element( 'td', [ 'lang' => 'en', 'dir' => 'ltr' ], $info['description'] )
562  . Html::rawElement( 'td', [], $authors )
563  . Html::closeElement( 'tr' );
564  }
565  $out .= Html::closeElement( 'table' );
566 
567  return $out;
568  }
569 
575  protected function getParserTags() {
576  $tags = MediaWikiServices::getInstance()->getParser()->getTags();
577 
578  if ( count( $tags ) ) {
579  $out = Html::rawElement(
580  'h2',
581  [
582  'class' => 'mw-headline plainlinks',
583  'id' => 'mw-version-parser-extensiontags',
584  ],
586  'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
587  $this->msg( 'version-parser-extensiontags' )->parse(),
588  false /* msg()->parse() already escapes */
589  )
590  );
591 
592  array_walk( $tags, function ( &$value ) {
593  // Bidirectional isolation improves readability in RTL wikis
594  $value = Html::element(
595  'bdi',
596  // Prevent < and > from slipping to another line
597  [
598  'style' => 'white-space: nowrap;',
599  ],
600  "<$value>"
601  );
602  } );
603 
604  $out .= $this->listToText( $tags );
605  } else {
606  $out = '';
607  }
608 
609  return $out;
610  }
611 
617  protected function getParserFunctionHooks() {
618  $fhooks = MediaWikiServices::getInstance()->getParser()->getFunctionHooks();
619  if ( count( $fhooks ) ) {
620  $out = Html::rawElement(
621  'h2',
622  [
623  'class' => 'mw-headline plainlinks',
624  'id' => 'mw-version-parser-function-hooks',
625  ],
627  'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
628  $this->msg( 'version-parser-function-hooks' )->parse(),
629  false /* msg()->parse() already escapes */
630  )
631  );
632 
633  $out .= $this->listToText( $fhooks );
634  } else {
635  $out = '';
636  }
637 
638  return $out;
639  }
640 
651  protected function getExtensionCategory( $type, $message ) {
652  $config = $this->getConfig();
653  $extensionCredits = $config->get( 'ExtensionCredits' );
654 
655  $out = '';
656 
657  if ( array_key_exists( $type, $extensionCredits ) && count( $extensionCredits[$type] ) > 0 ) {
658  $out .= $this->openExtType( $message, 'credits-' . $type );
659 
660  usort( $extensionCredits[$type], [ $this, 'compare' ] );
661 
662  foreach ( $extensionCredits[$type] as $extension ) {
663  $out .= $this->getCreditsForExtension( $type, $extension );
664  }
665  }
666 
667  return $out;
668  }
669 
676  public function compare( $a, $b ) {
677  return $this->getLanguage()->lc( $a['name'] ) <=> $this->getLanguage()->lc( $b['name'] );
678  }
679 
698  public function getCreditsForExtension( $type, array $extension ) {
699  $out = $this->getOutput();
700 
701  // We must obtain the information for all the bits and pieces!
702  // ... such as extension names and links
703  if ( isset( $extension['namemsg'] ) ) {
704  // Localized name of extension
705  $extensionName = $this->msg( $extension['namemsg'] )->text();
706  } elseif ( isset( $extension['name'] ) ) {
707  // Non localized version
708  $extensionName = $extension['name'];
709  } else {
710  $extensionName = $this->msg( 'version-no-ext-name' )->text();
711  }
712 
713  if ( isset( $extension['url'] ) ) {
714  $extensionNameLink = Linker::makeExternalLink(
715  $extension['url'],
716  $extensionName,
717  true,
718  '',
719  [ 'class' => 'mw-version-ext-name' ]
720  );
721  } else {
722  $extensionNameLink = htmlspecialchars( $extensionName );
723  }
724 
725  // ... and the version information
726  // If the extension path is set we will check that directory for GIT
727  // metadata in an attempt to extract date and vcs commit metadata.
728  $canonicalVersion = '&ndash;';
729  $extensionPath = null;
730  $vcsVersion = null;
731  $vcsLink = null;
732  $vcsDate = null;
733 
734  if ( isset( $extension['version'] ) ) {
735  $canonicalVersion = $out->parseInlineAsInterface( $extension['version'] );
736  }
737 
738  if ( isset( $extension['path'] ) ) {
739  global $IP;
740  $extensionPath = dirname( $extension['path'] );
741  if ( $this->coreId == '' ) {
742  wfDebug( 'Looking up core head id' );
743  $coreHeadSHA1 = self::getGitHeadSha1( $IP );
744  if ( $coreHeadSHA1 ) {
745  $this->coreId = $coreHeadSHA1;
746  }
747  }
749  $memcKey = $cache->makeKey(
750  'specialversion-ext-version-text', $extension['path'], $this->coreId
751  );
752  list( $vcsVersion, $vcsLink, $vcsDate ) = $cache->get( $memcKey );
753 
754  if ( !$vcsVersion ) {
755  wfDebug( "Getting VCS info for extension {$extension['name']}" );
756  $gitInfo = new GitInfo( $extensionPath );
757  $vcsVersion = $gitInfo->getHeadSHA1();
758  if ( $vcsVersion !== false ) {
759  $vcsVersion = substr( $vcsVersion, 0, 7 );
760  $vcsLink = $gitInfo->getHeadViewUrl();
761  $vcsDate = $gitInfo->getHeadCommitDate();
762  }
763  $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
764  } else {
765  wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" );
766  }
767  }
768 
769  $versionString = Html::rawElement(
770  'span',
771  [ 'class' => 'mw-version-ext-version' ],
772  $canonicalVersion
773  );
774 
775  if ( $vcsVersion ) {
776  if ( $vcsLink ) {
777  $vcsVerString = Linker::makeExternalLink(
778  $vcsLink,
779  $this->msg( 'version-version', $vcsVersion ),
780  true,
781  '',
782  [ 'class' => 'mw-version-ext-vcs-version' ]
783  );
784  } else {
785  $vcsVerString = Html::element( 'span',
786  [ 'class' => 'mw-version-ext-vcs-version' ],
787  "({$vcsVersion})"
788  );
789  }
790  $versionString .= " {$vcsVerString}";
791 
792  if ( $vcsDate ) {
793  $vcsTimeString = Html::element( 'span',
794  [ 'class' => 'mw-version-ext-vcs-timestamp' ],
795  $this->getLanguage()->timeanddate( $vcsDate, true )
796  );
797  $versionString .= " {$vcsTimeString}";
798  }
799  $versionString = Html::rawElement( 'span',
800  [ 'class' => 'mw-version-ext-meta-version' ],
801  $versionString
802  );
803  }
804 
805  // ... and license information; if a license file exists we
806  // will link to it
807  $licenseLink = '';
808  if ( isset( $extension['name'] ) ) {
809  $licenseName = null;
810  if ( isset( $extension['license-name'] ) ) {
811  $licenseName = new HtmlArmor( $out->parseInlineAsInterface( $extension['license-name'] ) );
812  } elseif ( $this->getExtLicenseFileName( $extensionPath ) ) {
813  $licenseName = $this->msg( 'version-ext-license' )->text();
814  }
815  if ( $licenseName !== null ) {
816  $licenseLink = $this->getLinkRenderer()->makeLink(
817  $this->getPageTitle( 'License/' . $extension['name'] ),
818  $licenseName,
819  [
820  'class' => 'mw-version-ext-license',
821  'dir' => 'auto',
822  ]
823  );
824  }
825  }
826 
827  // ... and generate the description; which can be a parameterized l10n message
828  // in the form [ <msgname>, <parameter>, <parameter>... ] or just a straight
829  // up string
830  if ( isset( $extension['descriptionmsg'] ) ) {
831  // Localized description of extension
832  $descriptionMsg = $extension['descriptionmsg'];
833 
834  if ( is_array( $descriptionMsg ) ) {
835  $descriptionMsgKey = $descriptionMsg[0]; // Get the message key
836  array_shift( $descriptionMsg ); // Shift out the message key to get the parameters only
837  array_map( "htmlspecialchars", $descriptionMsg ); // For sanity
838  $description = $this->msg( $descriptionMsgKey, $descriptionMsg )->text();
839  } else {
840  $description = $this->msg( $descriptionMsg )->text();
841  }
842  } elseif ( isset( $extension['description'] ) ) {
843  // Non localized version
844  $description = $extension['description'];
845  } else {
846  $description = '';
847  }
848  $description = $out->parseInlineAsInterface( $description );
849 
850  // ... now get the authors for this extension
851  $authors = $extension['author'] ?? [];
852  $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
853 
854  // Finally! Create the table
855  $html = Html::openElement( 'tr', [
856  'class' => 'mw-version-ext',
857  'id' => Sanitizer::escapeIdForAttribute( 'mw-version-ext-' . $type . '-' . $extension['name'] )
858  ]
859  );
860 
861  $html .= Html::rawElement( 'td', [], $extensionNameLink );
862  $html .= Html::rawElement( 'td', [], $versionString );
863  $html .= Html::rawElement( 'td', [], $licenseLink );
864  $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-description' ], $description );
865  $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-authors' ], $authors );
866 
867  $html .= Html::closeElement( 'tr' );
868 
869  return $html;
870  }
871 
877  private function getWgHooks() {
879 
880  if ( $wgSpecialVersionShowHooks && count( $wgHooks ) ) {
881  $myWgHooks = $wgHooks;
882  ksort( $myWgHooks );
883 
884  $ret = [];
885  $ret[] = '== {{int:version-hooks}} ==';
886  $ret[] = Html::openElement( 'table', [ 'class' => 'wikitable', 'id' => 'sv-hooks' ] );
887  $ret[] = Html::openElement( 'tr' );
888  $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-name' )->text() );
889  $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-subscribedby' )->text() );
890  $ret[] = Html::closeElement( 'tr' );
891 
892  foreach ( $myWgHooks as $hook => $hooks ) {
893  $ret[] = Html::openElement( 'tr' );
894  $ret[] = Html::element( 'td', [], $hook );
895  $ret[] = Html::element( 'td', [], $this->listToText( $hooks ) );
896  $ret[] = Html::closeElement( 'tr' );
897  }
898 
899  $ret[] = Html::closeElement( 'table' );
900 
901  return implode( "\n", $ret );
902  }
903 
904  return '';
905  }
906 
907  private function openExtType( $text = null, $name = null ) {
908  $out = '';
909 
910  $opt = [ 'colspan' => 5 ];
911  if ( $this->firstExtOpened ) {
912  // Insert a spacing line
913  $out .= Html::rawElement( 'tr', [ 'class' => 'sv-space' ],
914  Html::element( 'td', $opt )
915  );
916  }
917  $this->firstExtOpened = true;
918 
919  if ( $name ) {
920  $opt['id'] = "sv-$name";
921  }
922 
923  if ( $text !== null ) {
924  $out .= Html::rawElement( 'tr', [],
925  Html::element( 'th', $opt, $text )
926  );
927  }
928 
929  $firstHeadingMsg = ( $name === 'credits-skin' )
930  ? 'version-skin-colheader-name'
931  : 'version-ext-colheader-name';
932  $out .= Html::openElement( 'tr' );
933  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
934  $this->msg( $firstHeadingMsg )->text() );
935  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
936  $this->msg( 'version-ext-colheader-version' )->text() );
937  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
938  $this->msg( 'version-ext-colheader-license' )->text() );
939  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
940  $this->msg( 'version-ext-colheader-description' )->text() );
941  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
942  $this->msg( 'version-ext-colheader-credits' )->text() );
943  $out .= Html::closeElement( 'tr' );
944 
945  return $out;
946  }
947 
953  private function IPInfo() {
954  $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
955 
956  return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
957  }
958 
980  public function listAuthors( $authors, $extName, $extDir ) {
981  $hasOthers = false;
982  $linkRenderer = $this->getLinkRenderer();
983 
984  $list = [];
985  $authors = (array)$authors;
986 
987  // Special case: if the authors array has only one item and it is "...",
988  // it should not be rendered as the "version-poweredby-others" i18n msg,
989  // but rather as "version-poweredby-various" i18n msg instead.
990  if ( count( $authors ) === 1 && $authors[0] === '...' ) {
991  // Link to the extension's or skin's AUTHORS or CREDITS file, if there is
992  // such a file; otherwise just return the i18n msg as-is
993  if ( $extName && $this->getExtAuthorsFileName( $extDir ) ) {
994  return $linkRenderer->makeLink(
995  $this->getPageTitle( "Credits/$extName" ),
996  $this->msg( 'version-poweredby-various' )->text()
997  );
998  } else {
999  return $this->msg( 'version-poweredby-various' )->escaped();
1000  }
1001  }
1002 
1003  // Otherwise, if we have an actual array that has more than one item,
1004  // process each array item as usual
1005  foreach ( $authors as $item ) {
1006  if ( $item == '...' ) {
1007  $hasOthers = true;
1008 
1009  if ( $extName && $this->getExtAuthorsFileName( $extDir ) ) {
1010  $text = $linkRenderer->makeLink(
1011  $this->getPageTitle( "Credits/$extName" ),
1012  $this->msg( 'version-poweredby-others' )->text()
1013  );
1014  } else {
1015  $text = $this->msg( 'version-poweredby-others' )->escaped();
1016  }
1017  $list[] = $text;
1018  } elseif ( substr( $item, -5 ) == ' ...]' ) {
1019  $hasOthers = true;
1020  $list[] = $this->getOutput()->parseInlineAsInterface(
1021  substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
1022  );
1023  } else {
1024  $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1025  }
1026  }
1027 
1028  if ( $extName && !$hasOthers && $this->getExtAuthorsFileName( $extDir ) ) {
1029  $list[] = $text = $linkRenderer->makeLink(
1030  $this->getPageTitle( "Credits/$extName" ),
1031  $this->msg( 'version-poweredby-others' )->text()
1032  );
1033  }
1034 
1035  return $this->listToText( $list, false );
1036  }
1037 
1049  public static function getExtAuthorsFileName( $extDir ) {
1050  if ( !$extDir ) {
1051  return false;
1052  }
1053 
1054  foreach ( scandir( $extDir ) as $file ) {
1055  $fullPath = $extDir . DIRECTORY_SEPARATOR . $file;
1056  if ( preg_match( '/^((AUTHORS)|(CREDITS))(\.txt|\.wiki|\.mediawiki)?$/', $file ) &&
1057  is_readable( $fullPath ) &&
1058  is_file( $fullPath )
1059  ) {
1060  return $fullPath;
1061  }
1062  }
1063 
1064  return false;
1065  }
1066 
1078  public static function getExtLicenseFileName( $extDir ) {
1079  if ( !$extDir ) {
1080  return false;
1081  }
1082 
1083  foreach ( scandir( $extDir ) as $file ) {
1084  $fullPath = $extDir . DIRECTORY_SEPARATOR . $file;
1085  if ( preg_match( '/^((COPYING)|(LICENSE))(\.txt)?$/', $file ) &&
1086  is_readable( $fullPath ) &&
1087  is_file( $fullPath )
1088  ) {
1089  return $fullPath;
1090  }
1091  }
1092 
1093  return false;
1094  }
1095 
1104  public function listToText( $list, $sort = true ) {
1105  if ( !count( $list ) ) {
1106  return '';
1107  }
1108  if ( $sort ) {
1109  sort( $list );
1110  }
1111 
1112  return $this->getLanguage()
1113  ->listToText( array_map( [ __CLASS__, 'arrayToString' ], $list ) );
1114  }
1115 
1124  public static function arrayToString( $list ) {
1125  if ( is_array( $list ) && count( $list ) == 1 ) {
1126  $list = $list[0];
1127  }
1128  if ( $list instanceof Closure ) {
1129  // Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$"
1130  return 'Closure';
1131  } elseif ( is_object( $list ) ) {
1132  $class = wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1133 
1134  return $class;
1135  } elseif ( !is_array( $list ) ) {
1136  return $list;
1137  } else {
1138  if ( is_object( $list[0] ) ) {
1139  $class = get_class( $list[0] );
1140  } else {
1141  $class = $list[0];
1142  }
1143 
1144  return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1145  }
1146  }
1147 
1152  public static function getGitHeadSha1( $dir ) {
1153  $repo = new GitInfo( $dir );
1154 
1155  return $repo->getHeadSHA1();
1156  }
1157 
1162  public static function getGitCurrentBranch( $dir ) {
1163  $repo = new GitInfo( $dir );
1164  return $repo->getCurrentBranch();
1165  }
1166 
1171  public function getEntryPointInfo() {
1172  $config = $this->getConfig();
1173  $scriptPath = $config->get( 'ScriptPath' ) ?: '/';
1174 
1175  $entryPoints = [
1176  'version-entrypoints-articlepath' => $config->get( 'ArticlePath' ),
1177  'version-entrypoints-scriptpath' => $scriptPath,
1178  'version-entrypoints-index-php' => wfScript( 'index' ),
1179  'version-entrypoints-api-php' => wfScript( 'api' ),
1180  'version-entrypoints-rest-php' => wfScript( 'rest' ),
1181  ];
1182 
1183  $language = $this->getLanguage();
1184  $thAttribures = [
1185  'dir' => $language->getDir(),
1186  'lang' => $language->getHtmlCode()
1187  ];
1188  $out = Html::element(
1189  'h2',
1190  [ 'id' => 'mw-version-entrypoints' ],
1191  $this->msg( 'version-entrypoints' )->text()
1192  ) .
1193  Html::openElement( 'table',
1194  [
1195  'class' => 'wikitable plainlinks',
1196  'id' => 'mw-version-entrypoints-table',
1197  'dir' => 'ltr',
1198  'lang' => 'en'
1199  ]
1200  ) .
1201  Html::openElement( 'tr' ) .
1202  Html::element(
1203  'th',
1204  $thAttribures,
1205  $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1206  ) .
1207  Html::element(
1208  'th',
1209  $thAttribures,
1210  $this->msg( 'version-entrypoints-header-url' )->text()
1211  ) .
1212  Html::closeElement( 'tr' );
1213 
1214  foreach ( $entryPoints as $message => $value ) {
1215  $url = wfExpandUrl( $value, PROTO_RELATIVE );
1216  $out .= Html::openElement( 'tr' ) .
1217  // ->plain() looks like it should be ->parse(), but this function
1218  // returns wikitext, not HTML, boo
1219  Html::rawElement( 'td', [], $this->msg( $message )->plain() ) .
1220  Html::rawElement( 'td', [], Html::rawElement( 'code', [], "[$url $value]" ) ) .
1221  Html::closeElement( 'tr' );
1222  }
1223 
1224  $out .= Html::closeElement( 'table' );
1225 
1226  return $out;
1227  }
1228 
1229  protected function getGroupName() {
1230  return 'wiki';
1231  }
1232 }
static getExtensionTypes()
Returns an array with the base extension types.
static getVersionLinkedGit()
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
compare( $a, $b)
Callback to sort extensions by type.
$wgExtensionCredits
An array of information about installed extensions keyed by their type.
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:231
$wgVersion
MediaWiki version number.
static getLocalInstance( $ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
$IP
Definition: WebStart.php:41
getExtensionCategory( $type, $message)
Creates and returns the HTML for a single extension category.
openExtType( $text=null, $name=null)
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfGetCache( $cacheType)
Get a specific cache object.
if(!isset( $args[0])) $lang
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing &#39;/&#39;...
Definition: Html.php:251
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
$sort
getOutput()
Get the OutputPage being used for this instance.
string $coreId
The current rev id/SHA hash of MediaWiki core.
getParserTags()
Obtains a list of installed parser tags and the associated H2 header.
getParserFunctionHooks()
Obtains a list of installed parser function hooks and the associated H2 header.
static getSoftwareInformation()
static getExtensionTypeName( $type)
Returns the internationalized name for an extension type.
IPInfo()
Get information about client&#39;s IP address.
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
$wgSpecialVersionShowHooks
Show the contents of $wgHooks in Special:Version.
$wgLang
Definition: Setup.php:856
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:108
static getExtAuthorsFileName( $extDir)
Obtains the full path of an extensions authors or credits file if one exists.
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:1295
static getVersion( $flags='', $lang=null)
Return a string of the MediaWiki version with Git revision if available.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
listToText( $list, $sort=true)
Convert an array of items into a list for display.
getExternalLibraries()
Generate an HTML table for external libraries that are installed.
getWgHooks()
Generate wikitext showing hooks in $wgHooks.
$cache
Definition: mcc.php:33
getSkinCredits()
Generate wikitext showing the name, URL, author and description of each skin.
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:315
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition: Linker.php:848
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes! ...
static getwgVersionLinked()
static getGitHeadSha1( $dir)
const PROTO_RELATIVE
Definition: Defines.php:201
static getExtLicenseFileName( $extDir)
Obtains the full path of an extensions copying or license file if one exists.
static getVersionLinked()
Return a wikitext-formatted string of the MediaWiki version with a link to the Git SHA1 of head if av...
$wgHooks
Global list of hooks.
static arrayToString( $list)
Convert an array or object to a string for display.
static getCopyrightAndAuthorList()
Get the "MediaWiki is copyright 2001-20xx by lots of cool guys" text.
execute( $par)
main()
static closeElement( $element)
Shortcut to close an XML element.
Definition: Xml.php:117
getCreditsForExtension( $type, array $extension)
Creates and formats a version line for a single extension.
getExtensionCredits()
Generate wikitext showing the name, URL, author and description of each extension.
static getMediaWikiCredits()
Returns wiki text showing the license information.
getConfig()
Shortcut to get main config object.
const CACHE_ANYTHING
Definition: Defines.php:81
if(!is_readable( $file)) $ext
Definition: router.php:48
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:41
getLanguage()
Shortcut to get user&#39;s language.
listAuthors( $authors, $extName, $extDir)
Return a formatted unsorted list of authors.
Give information about the version of MediaWiki, PHP, the DB and extensions.
static softwareInformation()
Returns HTML showing the third party software versions (apache, php, mysql).
Reads an installed.json file and provides accessors to get what is installed.
const DB_REPLICA
Definition: defines.php:25
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
getRequest()
Get the WebRequest being used for this instance.
static getGitCurrentBranch( $dir)
static string [] false $extensionTypes
Lazy initialized key/value with message content.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
getPageTitle( $subpage=false)
Get a self-referential title object.
getEntryPointInfo()
Get the list of entry points and their URLs.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
MediaWiki Linker LinkRenderer null $linkRenderer
Definition: SpecialPage.php:67