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__
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 
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  $paths = [
509  "$IP/vendor/composer/installed.json"
510  ];
511 
512  $extensionCredits = $this->getConfig()->get( 'ExtensionCredits' );
514 
515  foreach ( $extensionTypes as $type => $message ) {
516  if ( !isset( $extensionCredits[$type] ) || $extensionCredits[$type] === [] ) {
517  continue;
518  }
519  foreach ( $extensionCredits[$type] as $extension ) {
520  if ( !isset( $extension['path'] ) ) {
521  continue;
522  }
523  $paths[] = dirname( $extension['path'] ) . '/vendor/composer/installed.json';
524  }
525  }
526 
527  $dependencies = [];
528 
529  foreach ( $paths as $path ) {
530  if ( !file_exists( $path ) ) {
531  continue;
532  }
533 
534  $installed = new ComposerInstalled( $path );
535 
536  $dependencies += $installed->getInstalledDependencies();
537  }
538 
539  if ( $dependencies === [] ) {
540  return '';
541  }
542 
543  ksort( $dependencies );
544 
545  $out = Html::element(
546  'h2',
547  [ 'id' => 'mw-version-libraries' ],
548  $this->msg( 'version-libraries' )->text()
549  );
550  $out .= Html::openElement(
551  'table',
552  [ 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries' ]
553  );
554  $out .= Html::openElement( 'tr' )
555  . Html::element( 'th', [], $this->msg( 'version-libraries-library' )->text() )
556  . Html::element( 'th', [], $this->msg( 'version-libraries-version' )->text() )
557  . Html::element( 'th', [], $this->msg( 'version-libraries-license' )->text() )
558  . Html::element( 'th', [], $this->msg( 'version-libraries-description' )->text() )
559  . Html::element( 'th', [], $this->msg( 'version-libraries-authors' )->text() )
560  . Html::closeElement( 'tr' );
561 
562  foreach ( $dependencies as $name => $info ) {
563  if ( !is_array( $info ) || strpos( $info['type'], 'mediawiki-' ) === 0 ) {
564  // Skip any extensions or skins since they'll be listed
565  // in their proper section
566  continue;
567  }
568  $authors = array_map( function ( $arr ) {
569  // If a homepage is set, link to it
570  if ( isset( $arr['homepage'] ) ) {
571  return "[{$arr['homepage']} {$arr['name']}]";
572  }
573  return $arr['name'];
574  }, $info['authors'] );
575  $authors = $this->listAuthors( $authors, false, "$IP/vendor/$name" );
576 
577  // We can safely assume that the libraries' names and descriptions
578  // are written in English and aren't going to be translated,
579  // so set appropriate lang and dir attributes
580  $out .= Html::openElement( 'tr' )
581  . Html::rawElement(
582  'td',
583  [],
585  "https://packagist.org/packages/$name", $name,
586  true, '',
587  [ 'class' => 'mw-version-library-name' ]
588  )
589  )
590  . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
591  . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['licenses'] ) )
592  . Html::element( 'td', [ 'lang' => 'en', 'dir' => 'ltr' ], $info['description'] )
593  . Html::rawElement( 'td', [], $authors )
594  . Html::closeElement( 'tr' );
595  }
596  $out .= Html::closeElement( 'table' );
597 
598  return $out;
599  }
600 
606  protected function getParserTags() {
607  $tags = MediaWikiServices::getInstance()->getParser()->getTags();
608 
609  if ( count( $tags ) ) {
610  $out = Html::rawElement(
611  'h2',
612  [
613  'class' => 'mw-headline plainlinks',
614  'id' => 'mw-version-parser-extensiontags',
615  ],
617  'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
618  $this->msg( 'version-parser-extensiontags' )->parse(),
619  false /* msg()->parse() already escapes */
620  )
621  );
622 
623  array_walk( $tags, function ( &$value ) {
624  // Bidirectional isolation improves readability in RTL wikis
625  $value = Html::element(
626  'bdi',
627  // Prevent < and > from slipping to another line
628  [
629  'style' => 'white-space: nowrap;',
630  ],
631  "<$value>"
632  );
633  } );
634 
635  $out .= $this->listToText( $tags );
636  } else {
637  $out = '';
638  }
639 
640  return $out;
641  }
642 
648  protected function getParserFunctionHooks() {
649  $fhooks = MediaWikiServices::getInstance()->getParser()->getFunctionHooks();
650  if ( count( $fhooks ) ) {
651  $out = Html::rawElement(
652  'h2',
653  [
654  'class' => 'mw-headline plainlinks',
655  'id' => 'mw-version-parser-function-hooks',
656  ],
658  'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
659  $this->msg( 'version-parser-function-hooks' )->parse(),
660  false /* msg()->parse() already escapes */
661  )
662  );
663 
664  $out .= $this->listToText( $fhooks );
665  } else {
666  $out = '';
667  }
668 
669  return $out;
670  }
671 
682  protected function getExtensionCategory( $type, $message ) {
683  $config = $this->getConfig();
684  $extensionCredits = $config->get( 'ExtensionCredits' );
685 
686  $out = '';
687 
688  if ( array_key_exists( $type, $extensionCredits ) && count( $extensionCredits[$type] ) > 0 ) {
689  $out .= $this->openExtType( $message, 'credits-' . $type );
690 
691  usort( $extensionCredits[$type], [ $this, 'compare' ] );
692 
693  foreach ( $extensionCredits[$type] as $extension ) {
694  $out .= $this->getCreditsForExtension( $type, $extension );
695  }
696  }
697 
698  return $out;
699  }
700 
707  public function compare( $a, $b ) {
708  return $this->getLanguage()->lc( $a['name'] ) <=> $this->getLanguage()->lc( $b['name'] );
709  }
710 
729  public function getCreditsForExtension( $type, array $extension ) {
730  $out = $this->getOutput();
731 
732  // We must obtain the information for all the bits and pieces!
733  // ... such as extension names and links
734  if ( isset( $extension['namemsg'] ) ) {
735  // Localized name of extension
736  $extensionName = $this->msg( $extension['namemsg'] )->text();
737  } elseif ( isset( $extension['name'] ) ) {
738  // Non localized version
739  $extensionName = $extension['name'];
740  } else {
741  $extensionName = $this->msg( 'version-no-ext-name' )->text();
742  }
743 
744  if ( isset( $extension['url'] ) ) {
745  $extensionNameLink = Linker::makeExternalLink(
746  $extension['url'],
747  $extensionName,
748  true,
749  '',
750  [ 'class' => 'mw-version-ext-name' ]
751  );
752  } else {
753  $extensionNameLink = htmlspecialchars( $extensionName );
754  }
755 
756  // ... and the version information
757  // If the extension path is set we will check that directory for GIT
758  // metadata in an attempt to extract date and vcs commit metadata.
759  $canonicalVersion = '&ndash;';
760  $extensionPath = null;
761  $vcsVersion = null;
762  $vcsLink = null;
763  $vcsDate = null;
764 
765  if ( isset( $extension['version'] ) ) {
766  $canonicalVersion = $out->parseInlineAsInterface( $extension['version'] );
767  }
768 
769  if ( isset( $extension['path'] ) ) {
770  global $IP;
771  $extensionPath = dirname( $extension['path'] );
772  if ( $this->coreId == '' ) {
773  wfDebug( 'Looking up core head id' );
774  $coreHeadSHA1 = self::getGitHeadSha1( $IP );
775  if ( $coreHeadSHA1 ) {
776  $this->coreId = $coreHeadSHA1;
777  }
778  }
780  $memcKey = $cache->makeKey(
781  'specialversion-ext-version-text', $extension['path'], $this->coreId
782  );
783  list( $vcsVersion, $vcsLink, $vcsDate ) = $cache->get( $memcKey );
784 
785  if ( !$vcsVersion ) {
786  wfDebug( "Getting VCS info for extension {$extension['name']}" );
787  $gitInfo = new GitInfo( $extensionPath );
788  $vcsVersion = $gitInfo->getHeadSHA1();
789  if ( $vcsVersion !== false ) {
790  $vcsVersion = substr( $vcsVersion, 0, 7 );
791  $vcsLink = $gitInfo->getHeadViewUrl();
792  $vcsDate = $gitInfo->getHeadCommitDate();
793  }
794  $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
795  } else {
796  wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" );
797  }
798  }
799 
800  $versionString = Html::rawElement(
801  'span',
802  [ 'class' => 'mw-version-ext-version' ],
803  $canonicalVersion
804  );
805 
806  if ( $vcsVersion ) {
807  if ( $vcsLink ) {
808  $vcsVerString = Linker::makeExternalLink(
809  $vcsLink,
810  $this->msg( 'version-version', $vcsVersion ),
811  true,
812  '',
813  [ 'class' => 'mw-version-ext-vcs-version' ]
814  );
815  } else {
816  $vcsVerString = Html::element( 'span',
817  [ 'class' => 'mw-version-ext-vcs-version' ],
818  "({$vcsVersion})"
819  );
820  }
821  $versionString .= " {$vcsVerString}";
822 
823  if ( $vcsDate ) {
824  $versionString .= ' ' . Html::element( 'span', [
825  'class' => 'mw-version-ext-vcs-timestamp',
826  'dir' => $this->getLanguage()->getDir(),
827  ], $this->getLanguage()->timeanddate( $vcsDate, true ) );
828  }
829  $versionString = Html::rawElement( 'span',
830  [ 'class' => 'mw-version-ext-meta-version' ],
831  $versionString
832  );
833  }
834 
835  // ... and license information; if a license file exists we
836  // will link to it
837  $licenseLink = '';
838  if ( isset( $extension['name'] ) ) {
839  $licenseName = null;
840  if ( isset( $extension['license-name'] ) ) {
841  $licenseName = new HtmlArmor( $out->parseInlineAsInterface( $extension['license-name'] ) );
842  } elseif ( $this->getExtLicenseFileName( $extensionPath ) ) {
843  $licenseName = $this->msg( 'version-ext-license' )->text();
844  }
845  if ( $licenseName !== null ) {
846  $licenseLink = $this->getLinkRenderer()->makeLink(
847  $this->getPageTitle( 'License/' . $extension['name'] ),
848  $licenseName,
849  [
850  'class' => 'mw-version-ext-license',
851  'dir' => 'auto',
852  ]
853  );
854  }
855  }
856 
857  // ... and generate the description; which can be a parameterized l10n message
858  // in the form [ <msgname>, <parameter>, <parameter>... ] or just a straight
859  // up string
860  if ( isset( $extension['descriptionmsg'] ) ) {
861  // Localized description of extension
862  $descriptionMsg = $extension['descriptionmsg'];
863 
864  if ( is_array( $descriptionMsg ) ) {
865  $descriptionMsgKey = $descriptionMsg[0]; // Get the message key
866  array_shift( $descriptionMsg ); // Shift out the message key to get the parameters only
867  array_map( "htmlspecialchars", $descriptionMsg ); // For sanity
868  $description = $this->msg( $descriptionMsgKey, $descriptionMsg )->text();
869  } else {
870  $description = $this->msg( $descriptionMsg )->text();
871  }
872  } elseif ( isset( $extension['description'] ) ) {
873  // Non localized version
874  $description = $extension['description'];
875  } else {
876  $description = '';
877  }
878  $description = $out->parseInlineAsInterface( $description );
879 
880  // ... now get the authors for this extension
881  $authors = $extension['author'] ?? [];
882  $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
883 
884  // Finally! Create the table
885  $html = Html::openElement( 'tr', [
886  'class' => 'mw-version-ext',
887  'id' => Sanitizer::escapeIdForAttribute( 'mw-version-ext-' . $type . '-' . $extension['name'] )
888  ]
889  );
890 
891  $html .= Html::rawElement( 'td', [], $extensionNameLink );
892  $html .= Html::rawElement( 'td', [], $versionString );
893  $html .= Html::rawElement( 'td', [], $licenseLink );
894  $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-description' ], $description );
895  $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-authors' ], $authors );
896 
897  $html .= Html::closeElement( 'tr' );
898 
899  return $html;
900  }
901 
907  private function getWgHooks() {
909 
910  if ( $wgSpecialVersionShowHooks && count( $wgHooks ) ) {
911  $myWgHooks = $wgHooks;
912  ksort( $myWgHooks );
913 
914  $ret = [];
915  $ret[] = '== {{int:version-hooks}} ==';
916  $ret[] = Html::openElement( 'table', [ 'class' => 'wikitable', 'id' => 'sv-hooks' ] );
917  $ret[] = Html::openElement( 'tr' );
918  $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-name' )->text() );
919  $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-subscribedby' )->text() );
920  $ret[] = Html::closeElement( 'tr' );
921 
922  foreach ( $myWgHooks as $hook => $hooks ) {
923  $ret[] = Html::openElement( 'tr' );
924  $ret[] = Html::element( 'td', [], $hook );
925  $ret[] = Html::element( 'td', [], $this->listToText( $hooks ) );
926  $ret[] = Html::closeElement( 'tr' );
927  }
928 
929  $ret[] = Html::closeElement( 'table' );
930 
931  return implode( "\n", $ret );
932  }
933 
934  return '';
935  }
936 
937  private function openExtType( $text = null, $name = null ) {
938  $out = '';
939 
940  $opt = [ 'colspan' => 5 ];
941  if ( $this->firstExtOpened ) {
942  // Insert a spacing line
943  $out .= Html::rawElement( 'tr', [ 'class' => 'sv-space' ],
944  Html::element( 'td', $opt )
945  );
946  }
947  $this->firstExtOpened = true;
948 
949  if ( $name ) {
950  $opt['id'] = "sv-$name";
951  }
952 
953  if ( $text !== null ) {
954  $out .= Html::rawElement( 'tr', [],
955  Html::element( 'th', $opt, $text )
956  );
957  }
958 
959  $firstHeadingMsg = ( $name === 'credits-skin' )
960  ? 'version-skin-colheader-name'
961  : 'version-ext-colheader-name';
962  $out .= Html::openElement( 'tr' );
963  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
964  $this->msg( $firstHeadingMsg )->text() );
965  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
966  $this->msg( 'version-ext-colheader-version' )->text() );
967  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
968  $this->msg( 'version-ext-colheader-license' )->text() );
969  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
970  $this->msg( 'version-ext-colheader-description' )->text() );
971  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
972  $this->msg( 'version-ext-colheader-credits' )->text() );
973  $out .= Html::closeElement( 'tr' );
974 
975  return $out;
976  }
977 
983  private function IPInfo() {
984  $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
985 
986  return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
987  }
988 
1010  public function listAuthors( $authors, $extName, $extDir ) {
1011  $hasOthers = false;
1012  $linkRenderer = $this->getLinkRenderer();
1013 
1014  $list = [];
1015  $authors = (array)$authors;
1016 
1017  // Special case: if the authors array has only one item and it is "...",
1018  // it should not be rendered as the "version-poweredby-others" i18n msg,
1019  // but rather as "version-poweredby-various" i18n msg instead.
1020  if ( count( $authors ) === 1 && $authors[0] === '...' ) {
1021  // Link to the extension's or skin's AUTHORS or CREDITS file, if there is
1022  // such a file; otherwise just return the i18n msg as-is
1023  if ( $extName && $this->getExtAuthorsFileName( $extDir ) ) {
1024  return $linkRenderer->makeLink(
1025  $this->getPageTitle( "Credits/$extName" ),
1026  $this->msg( 'version-poweredby-various' )->text()
1027  );
1028  } else {
1029  return $this->msg( 'version-poweredby-various' )->escaped();
1030  }
1031  }
1032 
1033  // Otherwise, if we have an actual array that has more than one item,
1034  // process each array item as usual
1035  foreach ( $authors as $item ) {
1036  if ( $item == '...' ) {
1037  $hasOthers = true;
1038 
1039  if ( $extName && $this->getExtAuthorsFileName( $extDir ) ) {
1040  $text = $linkRenderer->makeLink(
1041  $this->getPageTitle( "Credits/$extName" ),
1042  $this->msg( 'version-poweredby-others' )->text()
1043  );
1044  } else {
1045  $text = $this->msg( 'version-poweredby-others' )->escaped();
1046  }
1047  $list[] = $text;
1048  } elseif ( substr( $item, -5 ) == ' ...]' ) {
1049  $hasOthers = true;
1050  $list[] = $this->getOutput()->parseInlineAsInterface(
1051  substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
1052  );
1053  } else {
1054  $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1055  }
1056  }
1057 
1058  if ( $extName && !$hasOthers && $this->getExtAuthorsFileName( $extDir ) ) {
1059  $list[] = $text = $linkRenderer->makeLink(
1060  $this->getPageTitle( "Credits/$extName" ),
1061  $this->msg( 'version-poweredby-others' )->text()
1062  );
1063  }
1064 
1065  return $this->listToText( $list, false );
1066  }
1067 
1079  public static function getExtAuthorsFileName( $extDir ) {
1080  if ( !$extDir ) {
1081  return false;
1082  }
1083 
1084  foreach ( scandir( $extDir ) as $file ) {
1085  $fullPath = $extDir . DIRECTORY_SEPARATOR . $file;
1086  if ( preg_match( '/^((AUTHORS)|(CREDITS))(\.txt|\.wiki|\.mediawiki)?$/', $file ) &&
1087  is_readable( $fullPath ) &&
1088  is_file( $fullPath )
1089  ) {
1090  return $fullPath;
1091  }
1092  }
1093 
1094  return false;
1095  }
1096 
1108  public static function getExtLicenseFileName( $extDir ) {
1109  if ( !$extDir ) {
1110  return false;
1111  }
1112 
1113  foreach ( scandir( $extDir ) as $file ) {
1114  $fullPath = $extDir . DIRECTORY_SEPARATOR . $file;
1115  if ( preg_match( '/^((COPYING)|(LICENSE))(\.txt)?$/', $file ) &&
1116  is_readable( $fullPath ) &&
1117  is_file( $fullPath )
1118  ) {
1119  return $fullPath;
1120  }
1121  }
1122 
1123  return false;
1124  }
1125 
1134  public function listToText( $list, $sort = true ) {
1135  if ( !count( $list ) ) {
1136  return '';
1137  }
1138  if ( $sort ) {
1139  sort( $list );
1140  }
1141 
1142  return $this->getLanguage()
1143  ->listToText( array_map( [ __CLASS__, 'arrayToString' ], $list ) );
1144  }
1145 
1154  public static function arrayToString( $list ) {
1155  if ( is_array( $list ) && count( $list ) == 1 ) {
1156  $list = $list[0];
1157  }
1158  if ( $list instanceof Closure ) {
1159  // Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$"
1160  return 'Closure';
1161  } elseif ( is_object( $list ) ) {
1162  $class = wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1163 
1164  return $class;
1165  } elseif ( !is_array( $list ) ) {
1166  return $list;
1167  } else {
1168  if ( is_object( $list[0] ) ) {
1169  $class = get_class( $list[0] );
1170  } else {
1171  $class = $list[0];
1172  }
1173 
1174  return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1175  }
1176  }
1177 
1182  public static function getGitHeadSha1( $dir ) {
1183  $repo = new GitInfo( $dir );
1184 
1185  return $repo->getHeadSHA1();
1186  }
1187 
1192  public static function getGitCurrentBranch( $dir ) {
1193  $repo = new GitInfo( $dir );
1194  return $repo->getCurrentBranch();
1195  }
1196 
1201  public function getEntryPointInfo() {
1202  $config = $this->getConfig();
1203  $scriptPath = $config->get( 'ScriptPath' ) ?: '/';
1204 
1205  $entryPoints = [
1206  'version-entrypoints-articlepath' => $config->get( 'ArticlePath' ),
1207  'version-entrypoints-scriptpath' => $scriptPath,
1208  'version-entrypoints-index-php' => wfScript( 'index' ),
1209  'version-entrypoints-api-php' => wfScript( 'api' ),
1210  'version-entrypoints-rest-php' => wfScript( 'rest' ),
1211  ];
1212 
1213  $language = $this->getLanguage();
1214  $thAttribures = [
1215  'dir' => $language->getDir(),
1216  'lang' => $language->getHtmlCode()
1217  ];
1218  $out = Html::element(
1219  'h2',
1220  [ 'id' => 'mw-version-entrypoints' ],
1221  $this->msg( 'version-entrypoints' )->text()
1222  ) .
1223  Html::openElement( 'table',
1224  [
1225  'class' => 'wikitable plainlinks',
1226  'id' => 'mw-version-entrypoints-table',
1227  'dir' => 'ltr',
1228  'lang' => 'en'
1229  ]
1230  ) .
1231  Html::openElement( 'tr' ) .
1232  Html::element(
1233  'th',
1234  $thAttribures,
1235  $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1236  ) .
1237  Html::element(
1238  'th',
1239  $thAttribures,
1240  $this->msg( 'version-entrypoints-header-url' )->text()
1241  ) .
1242  Html::closeElement( 'tr' );
1243 
1244  foreach ( $entryPoints as $message => $value ) {
1245  $url = wfExpandUrl( $value, PROTO_RELATIVE );
1246  $out .= Html::openElement( 'tr' ) .
1247  // ->plain() looks like it should be ->parse(), but this function
1248  // returns wikitext, not HTML, boo
1249  Html::rawElement( 'td', [], $this->msg( $message )->plain() ) .
1250  Html::rawElement( 'td', [], Html::rawElement( 'code', [], "[$url $value]" ) ) .
1251  Html::closeElement( 'tr' );
1252  }
1253 
1254  $out .= Html::closeElement( 'table' );
1255 
1256  return $out;
1257  }
1258 
1259  protected function getGroupName() {
1260  return 'wiki';
1261  }
1262 }
SpecialVersion\getExtensionCredits
getExtensionCredits()
Generate wikitext showing the name, URL, author and description of each extension.
Definition: SpecialVersion.php:425
SpecialVersion\openExtType
openExtType( $text=null, $name=null)
Definition: SpecialVersion.php:937
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
SpecialVersion\listAuthors
listAuthors( $authors, $extName, $extDir)
Return a formatted unsorted list of authors.
Definition: SpecialVersion.php:1010
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:1079
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:1134
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:130
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:35
SpecialVersion\getCreditsForExtension
getCreditsForExtension( $type, array $extension)
Creates and formats a version line for a single extension.
Definition: SpecialVersion.php:729
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:707
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
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1263
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:648
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:318
$dbr
$dbr
Definition: testCompression.php:52
SpecialVersion\arrayToString
static arrayToString( $list)
Convert an array or object to a string for display.
Definition: SpecialVersion.php:1154
SpecialVersion\softwareInformation
static softwareInformation()
Returns HTML showing the third party software versions (apache, php, mysql).
Definition: SpecialVersion.php:260
SpecialVersion\IPInfo
IPInfo()
Get information about client's IP address.
Definition: SpecialVersion.php:983
SpecialVersion\getExternalLibraries
getExternalLibraries()
Generate an HTML table for external libraries that are installed.
Definition: SpecialVersion.php:506
$wgHooks
$wgHooks
Global list of hooks.
Definition: DefaultSettings.php:7477
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:480
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:2629
SpecialVersion\getExtensionTypeName
static getExtensionTypeName( $type)
Returns the internationalized name for an extension type.
Definition: SpecialVersion.php:414
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2562
$wgLang
$wgLang
Definition: Setup.php:842
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:385
Linker\makeExternalLink
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition: Linker.php:849
wfGetCache
wfGetCache( $cacheType)
Get a specific cache object.
Definition: GlobalFunctions.php:2836
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
SpecialVersion\getWgHooks
getWgHooks()
Generate wikitext showing hooks in $wgHooks.
Definition: SpecialVersion.php:907
$wgExtensionCredits
$wgExtensionCredits
An array of information about installed extensions keyed by their type.
Definition: DefaultSettings.php:7441
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
SpecialVersion\getGroupName
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
Definition: SpecialVersion.php:1259
SpecialVersion\getExtLicenseFileName
static getExtLicenseFileName( $extDir)
Obtains the full path of an extensions copying or license file if one exists.
Definition: SpecialVersion.php:1108
SpecialVersion\getwgVersionLinked
static getwgVersionLinked()
Definition: SpecialVersion.php:334
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:289
GitInfo
Definition: GitInfo.php:28
SpecialVersion\getGitCurrentBranch
static getGitCurrentBranch( $dir)
Definition: SpecialVersion.php:1192
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:1182
SpecialVersion\getEntryPointInfo
getEntryPointInfo()
Get the list of entry points and their URLs.
Definition: SpecialVersion.php:1201
SpecialPage\getLinkRenderer
getLinkRenderer()
Definition: SpecialPage.php:904
SpecialVersion\$firstExtOpened
bool $firstExtOpened
Definition: SpecialVersion.php:38
SpecialVersion\getVersionLinkedGit
static getVersionLinkedGit()
Definition: SpecialVersion.php:351
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:606
$IP
$IP
Definition: WebStart.php:41
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:6293
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:50
SpecialVersion\getExtensionCategory
getExtensionCategory( $type, $message)
Creates and returns the HTML for a single extension category.
Definition: SpecialVersion.php:682