MediaWiki  master
SpecialVersion.php
Go to the documentation of this file.
1 <?php
28 
34 class SpecialVersion extends SpecialPage {
35 
39  protected $firstExtOpened = false;
40 
44  protected $coreId = '';
45 
49  protected static $extensionTypes = false;
50 
51  public function __construct() {
52  parent::__construct( 'Version' );
53  }
54 
62  public static function getCredits( ExtensionRegistry $reg, Config $conf ) : array {
63  $credits = $conf->get( 'ExtensionCredits' );
64  foreach ( $reg->getAllThings() as $name => $credit ) {
65  $credits[$credit['type']][] = $credit;
66  }
67  return $credits;
68  }
69 
74  public function execute( $par ) {
75  global $IP;
76  $config = $this->getConfig();
77  $credits = self::getCredits( ExtensionRegistry::getInstance(), $config );
78 
79  $this->setHeaders();
80  $this->outputHeader();
81  $out = $this->getOutput();
82  $out->allowClickjacking();
83 
84  // Explode the sub page information into useful bits
85  $parts = explode( '/', (string)$par );
86  $extNode = null;
87  if ( isset( $parts[1] ) ) {
88  $extName = str_replace( '_', ' ', $parts[1] );
89  // Find it!
90  foreach ( $credits as $group => $extensions ) {
91  foreach ( $extensions as $ext ) {
92  if ( isset( $ext['name'] ) && ( $ext['name'] === $extName ) ) {
93  $extNode = &$ext;
94  break 2;
95  }
96  }
97  }
98  if ( !$extNode ) {
99  $out->setStatusCode( 404 );
100  }
101  } else {
102  $extName = 'MediaWiki';
103  }
104 
105  // Now figure out what to do
106  switch ( strtolower( $parts[0] ) ) {
107  case 'credits':
108  $out->addModuleStyles( 'mediawiki.special.version' );
109 
110  $wikiText = '{{int:version-credits-not-found}}';
111  if ( $extName === 'MediaWiki' ) {
112  $wikiText = file_get_contents( $IP . '/CREDITS' );
113  // Put the contributor list into columns
114  $wikiText = str_replace(
115  [ '<!-- BEGIN CONTRIBUTOR LIST -->', '<!-- END CONTRIBUTOR LIST -->' ],
116  [ '<div class="mw-version-credits">', '</div>' ],
117  $wikiText );
118  } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
119  $file = ExtensionInfo::getAuthorsFileName( dirname( $extNode['path'] ) );
120  if ( $file ) {
121  $wikiText = file_get_contents( $file );
122  if ( substr( $file, -4 ) === '.txt' ) {
123  $wikiText = Html::element(
124  'pre',
125  [
126  'lang' => 'en',
127  'dir' => 'ltr',
128  ],
129  $wikiText
130  );
131  }
132  }
133  }
134 
135  $out->setPageTitle( $this->msg( 'version-credits-title', $extName ) );
136  $out->addWikiTextAsInterface( $wikiText );
137  break;
138 
139  case 'license':
140  $wikiText = '{{int:version-license-not-found}}';
141  if ( $extName === 'MediaWiki' ) {
142  $wikiText = file_get_contents( $IP . '/COPYING' );
143  } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
144  $files = ExtensionInfo::getLicenseFileNames( dirname( $extNode['path'] ) );
145 
146  if ( count( $files ) ) {
147  // Only replace '{{int:version-license-not-found}}' if we have files
148  $wikiText = '';
149  foreach ( $files as $file ) {
150  $fileText = file_get_contents( $file );
151  $wikiText .= Html::element(
152  'pre',
153  [
154  'lang' => 'en',
155  'dir' => 'ltr',
156  ],
157  $fileText
158  );
159  }
160  }
161  }
162 
163  $out->setPageTitle( $this->msg( 'version-license-title', $extName ) );
164  $out->addWikiTextAsInterface( $wikiText );
165  break;
166 
167  default:
168  $out->addModuleStyles( 'mediawiki.special.version' );
169  $out->addWikiTextAsInterface(
170  self::getMediaWikiCredits() .
171  self::softwareInformation() .
172  $this->getEntryPointInfo()
173  );
174  $out->addHTML(
175  $this->getSkinCredits( $credits ) .
176  $this->getExtensionCredits( $credits ) .
177  $this->getExternalLibraries( $credits ) .
178  $this->getParserTags() .
179  $this->getParserFunctionHooks()
180  );
181  $out->addWikiTextAsInterface( $this->getWgHooks() );
182  $out->addHTML( $this->IPInfo() );
183 
184  break;
185  }
186  }
187 
193  private static function getMediaWikiCredits() {
194  $ret = Xml::element(
195  'h2',
196  [ 'id' => 'mw-version-license' ],
197  wfMessage( 'version-license' )->text()
198  );
199 
200  // This text is always left-to-right.
201  $ret .= '<div class="plainlinks">';
202  $ret .= "__NOTOC__
204  " . '<div class="mw-version-license-info">' .
205  wfMessage( 'version-license-info' )->text() .
206  '</div>';
207  $ret .= '</div>';
208 
209  return str_replace( "\t\t", '', $ret ) . "\n";
210  }
211 
217  public static function getCopyrightAndAuthorList() {
218  global $wgLang;
219 
220  if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
221  $othersLink = '[https://www.mediawiki.org/wiki/Special:Version/Credits ' .
222  wfMessage( 'version-poweredby-others' )->text() . ']';
223  } else {
224  $othersLink = '[[Special:Version/Credits|' .
225  wfMessage( 'version-poweredby-others' )->text() . ']]';
226  }
227 
228  $translatorsLink = '[https://translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
229  wfMessage( 'version-poweredby-translators' )->text() . ']';
230 
231  $authorList = [
232  'Magnus Manske', 'Brion Vibber', 'Lee Daniel Crocker',
233  'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
234  'Niklas Laxström', 'Domas Mituzas', 'Rob Church', 'Yuri Astrakhan',
235  'Aryeh Gregor', 'Aaron Schulz', 'Andrew Garrett', 'Raimond Spekking',
236  'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
237  'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
238  'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
239  'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', 'Brad Jorsch',
240  'Bartosz Dziewoński', 'Ed Sanders', 'Moriel Schottlender',
241  'Kunal Mehta', 'James D. Forrester', 'Brian Wolff', 'Adam Shorland',
242  $othersLink, $translatorsLink
243  ];
244 
245  return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
246  $wgLang->listToText( $authorList ) )->text();
247  }
248 
254  public static function getSoftwareInformation() {
255  $dbr = wfGetDB( DB_REPLICA );
256 
257  // Put the software in an array of form 'name' => 'version'. All messages should
258  // be loaded here, so feel free to use wfMessage in the 'name'. Raw HTML or
259  // wikimarkup can be used.
260  $software = [
261  '[https://www.mediawiki.org/ MediaWiki]' => self::getVersionLinked(),
262  '[https://php.net/ PHP]' => PHP_VERSION . " (" . PHP_SAPI . ")",
263  $dbr->getSoftwareLink() => $dbr->getServerInfo(),
264  ];
265 
266  if ( defined( 'INTL_ICU_VERSION' ) ) {
267  $software['[http://site.icu-project.org/ ICU]'] = INTL_ICU_VERSION;
268  }
269 
270  // Allow a hook to add/remove items.
271  Hooks::runner()->onSoftwareInfo( $software );
272 
273  return $software;
274  }
275 
281  public static function softwareInformation() {
282  $out = Xml::element(
283  'h2',
284  [ 'id' => 'mw-version-software' ],
285  wfMessage( 'version-software' )->text()
286  ) .
287  Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ] ) .
288  "<tr>
289  <th>" . wfMessage( 'version-software-product' )->text() . "</th>
290  <th>" . wfMessage( 'version-software-version' )->text() . "</th>
291  </tr>\n";
292 
293  foreach ( self::getSoftwareInformation() as $name => $version ) {
294  $out .= "<tr>
295  <td>" . $name . "</td>
296  <td dir=\"ltr\">" . $version . "</td>
297  </tr>\n";
298  }
299 
300  return $out . Xml::closeElement( 'table' );
301  }
302 
310  public static function getVersion( $flags = '', $lang = null ) {
311  global $IP;
312 
313  $gitInfo = self::getGitHeadSha1( $IP );
314  if ( !$gitInfo ) {
315  $version = MW_VERSION;
316  } elseif ( $flags === 'nodb' ) {
317  $shortSha1 = substr( $gitInfo, 0, 7 );
318  $version = MW_VERSION . " ($shortSha1)";
319  } else {
320  $shortSha1 = substr( $gitInfo, 0, 7 );
321  $msg = wfMessage( 'parentheses' );
322  if ( $lang !== null ) {
323  $msg->inLanguage( $lang );
324  }
325  $shortSha1 = $msg->params( $shortSha1 )->escaped();
326  $version = MW_VERSION . ' ' . $shortSha1;
327  }
328 
329  return $version;
330  }
331 
339  public static function getVersionLinked() {
340  $gitVersion = self::getVersionLinkedGit();
341  if ( $gitVersion ) {
342  $v = $gitVersion;
343  } else {
344  $v = MW_VERSION; // fallback
345  }
346 
347  return $v;
348  }
349 
353  private static function getMWVersionLinked() {
354  $versionUrl = "";
355  if ( Hooks::runner()->onSpecialVersionVersionUrl( MW_VERSION, $versionUrl ) ) {
356  $versionParts = [];
357  preg_match( "/^(\d+\.\d+)/", MW_VERSION, $versionParts );
358  $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
359  }
360 
361  return '[' . $versionUrl . ' ' . MW_VERSION . ']';
362  }
363 
369  private static function getVersionLinkedGit() {
370  global $IP, $wgLang;
371 
372  $gitInfo = new GitInfo( $IP );
373  $headSHA1 = $gitInfo->getHeadSHA1();
374  if ( !$headSHA1 ) {
375  return false;
376  }
377 
378  $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
379 
380  $gitHeadUrl = $gitInfo->getHeadViewUrl();
381  if ( $gitHeadUrl !== false ) {
382  $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
383  }
384 
385  $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
386  if ( $gitHeadCommitDate ) {
387  $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( $gitHeadCommitDate, true );
388  }
389 
390  return self::getMWVersionLinked() . " $shortSHA1";
391  }
392 
403  public static function getExtensionTypes() {
404  if ( self::$extensionTypes === false ) {
405  self::$extensionTypes = [
406  'specialpage' => wfMessage( 'version-specialpages' )->text(),
407  'editor' => wfMessage( 'version-editors' )->text(),
408  'parserhook' => wfMessage( 'version-parserhooks' )->text(),
409  'variable' => wfMessage( 'version-variables' )->text(),
410  'media' => wfMessage( 'version-mediahandlers' )->text(),
411  'antispam' => wfMessage( 'version-antispam' )->text(),
412  'skin' => wfMessage( 'version-skins' )->text(),
413  'api' => wfMessage( 'version-api' )->text(),
414  'other' => wfMessage( 'version-other' )->text(),
415  ];
416 
417  Hooks::runner()->onExtensionTypes( self::$extensionTypes );
418  }
419 
420  return self::$extensionTypes;
421  }
422 
432  public static function getExtensionTypeName( $type ) {
433  $types = self::getExtensionTypes();
434 
435  return $types[$type] ?? $types['other'];
436  }
437 
444  private function getExtensionCredits( array $credits ) {
445  if (
446  !$credits ||
447  // Skins are displayed separately, see getSkinCredits()
448  ( count( $credits ) === 1 && isset( $credits['skin'] ) )
449  ) {
450  return '';
451  }
452 
454 
455  $out = Xml::element(
456  'h2',
457  [ 'id' => 'mw-version-ext' ],
458  $this->msg( 'version-extensions' )->text()
459  ) .
460  Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-ext' ] );
461 
462  // Make sure the 'other' type is set to an array.
463  if ( !array_key_exists( 'other', $credits ) ) {
464  $credits['other'] = [];
465  }
466 
467  // Find all extensions that do not have a valid type and give them the type 'other'.
468  foreach ( $credits as $type => $extensions ) {
469  if ( !array_key_exists( $type, $extensionTypes ) ) {
470  $credits['other'] = array_merge( $credits['other'], $extensions );
471  }
472  }
473 
474  $this->firstExtOpened = false;
475  // Loop through the extension categories to display their extensions in the list.
476  foreach ( $extensionTypes as $type => $message ) {
477  // Skins have a separate section
478  if ( $type !== 'other' && $type !== 'skin' ) {
479  $out .= $this->getExtensionCategory( $type, $message, $credits[$type] ?? [] );
480  }
481  }
482 
483  // We want the 'other' type to be last in the list.
484  $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'], $credits['other'] );
485 
486  $out .= Xml::closeElement( 'table' );
487 
488  return $out;
489  }
490 
497  private function getSkinCredits( array $credits ) {
498  if ( !isset( $credits['skin'] ) || count( $credits['skin'] ) === 0 ) {
499  return '';
500  }
501 
502  $out = Xml::element(
503  'h2',
504  [ 'id' => 'mw-version-skin' ],
505  $this->msg( 'version-skins' )->text()
506  ) .
507  Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-skin' ] );
508 
509  $this->firstExtOpened = false;
510  $out .= $this->getExtensionCategory( 'skin', null, $credits['skin'] );
511 
512  $out .= Xml::closeElement( 'table' );
513 
514  return $out;
515  }
516 
523  protected function getExternalLibraries( array $credits ) {
524  global $IP;
525  $paths = [
526  "$IP/vendor/composer/installed.json"
527  ];
528 
530  foreach ( $extensionTypes as $type => $message ) {
531  if ( !isset( $credits[$type] ) || $credits[$type] === [] ) {
532  continue;
533  }
534  foreach ( $credits[$type] as $extension ) {
535  if ( !isset( $extension['path'] ) ) {
536  continue;
537  }
538  $paths[] = dirname( $extension['path'] ) . '/vendor/composer/installed.json';
539  }
540  }
541 
542  $dependencies = [];
543 
544  foreach ( $paths as $path ) {
545  if ( !file_exists( $path ) ) {
546  continue;
547  }
548 
549  $installed = new ComposerInstalled( $path );
550 
551  $dependencies += $installed->getInstalledDependencies();
552  }
553 
554  if ( $dependencies === [] ) {
555  return '';
556  }
557 
558  ksort( $dependencies );
559 
560  $out = Html::element(
561  'h2',
562  [ 'id' => 'mw-version-libraries' ],
563  $this->msg( 'version-libraries' )->text()
564  );
565  $out .= Html::openElement(
566  'table',
567  [ 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries' ]
568  );
569  $out .= Html::openElement( 'tr' )
570  . Html::element( 'th', [], $this->msg( 'version-libraries-library' )->text() )
571  . Html::element( 'th', [], $this->msg( 'version-libraries-version' )->text() )
572  . Html::element( 'th', [], $this->msg( 'version-libraries-license' )->text() )
573  . Html::element( 'th', [], $this->msg( 'version-libraries-description' )->text() )
574  . Html::element( 'th', [], $this->msg( 'version-libraries-authors' )->text() )
575  . Html::closeElement( 'tr' );
576 
577  foreach ( $dependencies as $name => $info ) {
578  if ( !is_array( $info ) || strpos( $info['type'], 'mediawiki-' ) === 0 ) {
579  // Skip any extensions or skins since they'll be listed
580  // in their proper section
581  continue;
582  }
583  $authors = array_map( function ( $arr ) {
584  // If a homepage is set, link to it
585  if ( isset( $arr['homepage'] ) ) {
586  return "[{$arr['homepage']} {$arr['name']}]";
587  }
588  return $arr['name'];
589  }, $info['authors'] );
590  $authors = $this->listAuthors( $authors, false, "$IP/vendor/$name" );
591 
592  // We can safely assume that the libraries' names and descriptions
593  // are written in English and aren't going to be translated,
594  // so set appropriate lang and dir attributes
595  $out .= Html::openElement( 'tr', [
596  // Add an anchor so docs can link easily to the version of
597  // this specific library
599  "mw-version-library-$name"
600  ) ] )
602  'td',
603  [],
605  "https://packagist.org/packages/$name", $name,
606  true, '',
607  [ 'class' => 'mw-version-library-name' ]
608  )
609  )
610  . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
611  . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['licenses'] ) )
612  . Html::element( 'td', [ 'lang' => 'en', 'dir' => 'ltr' ], $info['description'] )
613  . Html::rawElement( 'td', [], $authors )
614  . Html::closeElement( 'tr' );
615  }
616  $out .= Html::closeElement( 'table' );
617 
618  return $out;
619  }
620 
626  protected function getParserTags() {
627  $tags = MediaWikiServices::getInstance()->getParser()->getTags();
628 
629  if ( count( $tags ) ) {
630  $out = Html::rawElement(
631  'h2',
632  [
633  'class' => 'mw-headline plainlinks',
634  'id' => 'mw-version-parser-extensiontags',
635  ],
637  'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
638  $this->msg( 'version-parser-extensiontags' )->parse(),
639  false /* msg()->parse() already escapes */
640  )
641  );
642 
643  array_walk( $tags, function ( &$value ) {
644  // Bidirectional isolation improves readability in RTL wikis
645  $value = Html::element(
646  'bdi',
647  // Prevent < and > from slipping to another line
648  [
649  'style' => 'white-space: nowrap;',
650  ],
651  "<$value>"
652  );
653  } );
654 
655  $out .= $this->listToText( $tags );
656  } else {
657  $out = '';
658  }
659 
660  return $out;
661  }
662 
668  protected function getParserFunctionHooks() {
669  $fhooks = MediaWikiServices::getInstance()->getParser()->getFunctionHooks();
670  if ( count( $fhooks ) ) {
671  $out = Html::rawElement(
672  'h2',
673  [
674  'class' => 'mw-headline plainlinks',
675  'id' => 'mw-version-parser-function-hooks',
676  ],
678  'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
679  $this->msg( 'version-parser-function-hooks' )->parse(),
680  false /* msg()->parse() already escapes */
681  )
682  );
683 
684  $out .= $this->listToText( $fhooks );
685  } else {
686  $out = '';
687  }
688 
689  return $out;
690  }
691 
701  protected function getExtensionCategory( $type, $message, array $creditsGroup ) {
702  $config = $this->getConfig();
703  $credits = $config->get( 'ExtensionCredits' );
704 
705  $out = '';
706 
707  if ( $creditsGroup ) {
708  $out .= $this->openExtType( $message, 'credits-' . $type );
709 
710  usort( $creditsGroup, [ $this, 'compare' ] );
711 
712  foreach ( $creditsGroup as $extension ) {
713  $out .= $this->getCreditsForExtension( $type, $extension );
714  }
715  }
716 
717  return $out;
718  }
719 
726  public function compare( $a, $b ) {
727  return $this->getLanguage()->lc( $a['name'] ) <=> $this->getLanguage()->lc( $b['name'] );
728  }
729 
748  public function getCreditsForExtension( $type, array $extension ) {
749  $out = $this->getOutput();
750 
751  // We must obtain the information for all the bits and pieces!
752  // ... such as extension names and links
753  if ( isset( $extension['namemsg'] ) ) {
754  // Localized name of extension
755  $extensionName = $this->msg( $extension['namemsg'] )->text();
756  } elseif ( isset( $extension['name'] ) ) {
757  // Non localized version
758  $extensionName = $extension['name'];
759  } else {
760  $extensionName = $this->msg( 'version-no-ext-name' )->text();
761  }
762 
763  if ( isset( $extension['url'] ) ) {
764  $extensionNameLink = Linker::makeExternalLink(
765  $extension['url'],
766  $extensionName,
767  true,
768  '',
769  [ 'class' => 'mw-version-ext-name' ]
770  );
771  } else {
772  $extensionNameLink = htmlspecialchars( $extensionName );
773  }
774 
775  // ... and the version information
776  // If the extension path is set we will check that directory for GIT
777  // metadata in an attempt to extract date and vcs commit metadata.
778  $canonicalVersion = '&ndash;';
779  $extensionPath = null;
780  $vcsVersion = null;
781  $vcsLink = null;
782  $vcsDate = null;
783 
784  if ( isset( $extension['version'] ) ) {
785  $canonicalVersion = $out->parseInlineAsInterface( $extension['version'] );
786  }
787 
788  if ( isset( $extension['path'] ) ) {
789  global $IP;
790  $extensionPath = dirname( $extension['path'] );
791  if ( $this->coreId == '' ) {
792  wfDebug( 'Looking up core head id' );
793  $coreHeadSHA1 = self::getGitHeadSha1( $IP );
794  if ( $coreHeadSHA1 ) {
795  $this->coreId = $coreHeadSHA1;
796  }
797  }
799  $memcKey = $cache->makeKey(
800  'specialversion-ext-version-text', $extension['path'], $this->coreId
801  );
802  list( $vcsVersion, $vcsLink, $vcsDate ) = $cache->get( $memcKey );
803 
804  if ( !$vcsVersion ) {
805  wfDebug( "Getting VCS info for extension {$extension['name']}" );
806  $gitInfo = new GitInfo( $extensionPath );
807  $vcsVersion = $gitInfo->getHeadSHA1();
808  if ( $vcsVersion !== false ) {
809  $vcsVersion = substr( $vcsVersion, 0, 7 );
810  $vcsLink = $gitInfo->getHeadViewUrl();
811  $vcsDate = $gitInfo->getHeadCommitDate();
812  }
813  $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
814  } else {
815  wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" );
816  }
817  }
818 
819  $versionString = Html::rawElement(
820  'span',
821  [ 'class' => 'mw-version-ext-version' ],
822  $canonicalVersion
823  );
824 
825  if ( $vcsVersion ) {
826  if ( $vcsLink ) {
827  $vcsVerString = Linker::makeExternalLink(
828  $vcsLink,
829  $this->msg( 'version-version', $vcsVersion ),
830  true,
831  '',
832  [ 'class' => 'mw-version-ext-vcs-version' ]
833  );
834  } else {
835  $vcsVerString = Html::element( 'span',
836  [ 'class' => 'mw-version-ext-vcs-version' ],
837  "({$vcsVersion})"
838  );
839  }
840  $versionString .= " {$vcsVerString}";
841 
842  if ( $vcsDate ) {
843  $versionString .= ' ' . Html::element( 'span', [
844  'class' => 'mw-version-ext-vcs-timestamp',
845  'dir' => $this->getLanguage()->getDir(),
846  ], $this->getLanguage()->timeanddate( $vcsDate, true ) );
847  }
848  $versionString = Html::rawElement( 'span',
849  [ 'class' => 'mw-version-ext-meta-version' ],
850  $versionString
851  );
852  }
853 
854  // ... and license information; if a license file exists we
855  // will link to it
856  $licenseLink = '';
857  if ( isset( $extension['name'] ) ) {
858  $licenseName = null;
859  if ( isset( $extension['license-name'] ) ) {
860  $licenseName = new HtmlArmor( $out->parseInlineAsInterface( $extension['license-name'] ) );
861  } elseif ( ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
862  $licenseName = $this->msg( 'version-ext-license' )->text();
863  }
864  if ( $licenseName !== null ) {
865  $licenseLink = $this->getLinkRenderer()->makeLink(
866  $this->getPageTitle( 'License/' . $extension['name'] ),
867  $licenseName,
868  [
869  'class' => 'mw-version-ext-license',
870  'dir' => 'auto',
871  ]
872  );
873  }
874  }
875 
876  // ... and generate the description; which can be a parameterized l10n message
877  // in the form [ <msgname>, <parameter>, <parameter>... ] or just a straight
878  // up string
879  if ( isset( $extension['descriptionmsg'] ) ) {
880  // Localized description of extension
881  $descriptionMsg = $extension['descriptionmsg'];
882 
883  if ( is_array( $descriptionMsg ) ) {
884  $descriptionMsgKey = $descriptionMsg[0]; // Get the message key
885  array_shift( $descriptionMsg ); // Shift out the message key to get the parameters only
886  array_map( "htmlspecialchars", $descriptionMsg ); // For sanity
887  $description = $this->msg( $descriptionMsgKey, $descriptionMsg )->text();
888  } else {
889  $description = $this->msg( $descriptionMsg )->text();
890  }
891  } elseif ( isset( $extension['description'] ) ) {
892  // Non localized version
893  $description = $extension['description'];
894  } else {
895  $description = '';
896  }
897  $description = $out->parseInlineAsInterface( $description );
898 
899  // ... now get the authors for this extension
900  $authors = $extension['author'] ?? [];
901  $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
902 
903  // Finally! Create the table
904  $html = Html::openElement( 'tr', [
905  'class' => 'mw-version-ext',
906  'id' => Sanitizer::escapeIdForAttribute( 'mw-version-ext-' . $type . '-' . $extension['name'] )
907  ]
908  );
909 
910  $html .= Html::rawElement( 'td', [], $extensionNameLink );
911  $html .= Html::rawElement( 'td', [], $versionString );
912  $html .= Html::rawElement( 'td', [], $licenseLink );
913  $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-description' ], $description );
914  $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-authors' ], $authors );
915 
916  $html .= Html::closeElement( 'tr' );
917 
918  return $html;
919  }
920 
926  private function getWgHooks() {
928 
929  if ( $wgSpecialVersionShowHooks && count( $wgHooks ) ) {
930  $myWgHooks = $wgHooks;
931  ksort( $myWgHooks );
932 
933  $ret = [];
934  $ret[] = '== {{int:version-hooks}} ==';
935  $ret[] = Html::openElement( 'table', [ 'class' => 'wikitable', 'id' => 'sv-hooks' ] );
936  $ret[] = Html::openElement( 'tr' );
937  $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-name' )->text() );
938  $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-subscribedby' )->text() );
939  $ret[] = Html::closeElement( 'tr' );
940 
941  foreach ( $myWgHooks as $hook => $hooks ) {
942  $ret[] = Html::openElement( 'tr' );
943  $ret[] = Html::element( 'td', [], $hook );
944  $ret[] = Html::element( 'td', [], $this->listToText( $hooks ) );
945  $ret[] = Html::closeElement( 'tr' );
946  }
947 
948  $ret[] = Html::closeElement( 'table' );
949 
950  return implode( "\n", $ret );
951  }
952 
953  return '';
954  }
955 
956  private function openExtType( $text = null, $name = null ) {
957  $out = '';
958 
959  $opt = [ 'colspan' => 5 ];
960  if ( $this->firstExtOpened ) {
961  // Insert a spacing line
962  $out .= Html::rawElement( 'tr', [ 'class' => 'sv-space' ],
963  Html::element( 'td', $opt )
964  );
965  }
966  $this->firstExtOpened = true;
967 
968  if ( $name ) {
969  $opt['id'] = "sv-$name";
970  }
971 
972  if ( $text !== null ) {
973  $out .= Html::rawElement( 'tr', [],
974  Html::element( 'th', $opt, $text )
975  );
976  }
977 
978  $firstHeadingMsg = ( $name === 'credits-skin' )
979  ? 'version-skin-colheader-name'
980  : 'version-ext-colheader-name';
981  $out .= Html::openElement( 'tr' );
982  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
983  $this->msg( $firstHeadingMsg )->text() );
984  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
985  $this->msg( 'version-ext-colheader-version' )->text() );
986  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
987  $this->msg( 'version-ext-colheader-license' )->text() );
988  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
989  $this->msg( 'version-ext-colheader-description' )->text() );
990  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
991  $this->msg( 'version-ext-colheader-credits' )->text() );
992  $out .= Html::closeElement( 'tr' );
993 
994  return $out;
995  }
996 
1002  private function IPInfo() {
1003  $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
1004 
1005  return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
1006  }
1007 
1029  public function listAuthors( $authors, $extName, $extDir ) {
1030  $hasOthers = false;
1031  $linkRenderer = $this->getLinkRenderer();
1032 
1033  $list = [];
1034  $authors = (array)$authors;
1035 
1036  // Special case: if the authors array has only one item and it is "...",
1037  // it should not be rendered as the "version-poweredby-others" i18n msg,
1038  // but rather as "version-poweredby-various" i18n msg instead.
1039  if ( count( $authors ) === 1 && $authors[0] === '...' ) {
1040  // Link to the extension's or skin's AUTHORS or CREDITS file, if there is
1041  // such a file; otherwise just return the i18n msg as-is
1042  if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1043  return $linkRenderer->makeLink(
1044  $this->getPageTitle( "Credits/$extName" ),
1045  $this->msg( 'version-poweredby-various' )->text()
1046  );
1047  } else {
1048  return $this->msg( 'version-poweredby-various' )->escaped();
1049  }
1050  }
1051 
1052  // Otherwise, if we have an actual array that has more than one item,
1053  // process each array item as usual
1054  foreach ( $authors as $item ) {
1055  if ( $item == '...' ) {
1056  $hasOthers = true;
1057 
1058  if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1059  $text = $linkRenderer->makeLink(
1060  $this->getPageTitle( "Credits/$extName" ),
1061  $this->msg( 'version-poweredby-others' )->text()
1062  );
1063  } else {
1064  $text = $this->msg( 'version-poweredby-others' )->escaped();
1065  }
1066  $list[] = $text;
1067  } elseif ( substr( $item, -5 ) == ' ...]' ) {
1068  $hasOthers = true;
1069  $list[] = $this->getOutput()->parseInlineAsInterface(
1070  substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
1071  );
1072  } else {
1073  $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1074  }
1075  }
1076 
1077  if ( $extName && !$hasOthers && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1078  $list[] = $text = $linkRenderer->makeLink(
1079  $this->getPageTitle( "Credits/$extName" ),
1080  $this->msg( 'version-poweredby-others' )->text()
1081  );
1082  }
1083 
1084  return $this->listToText( $list, false );
1085  }
1086 
1099  public static function getExtAuthorsFileName( $extDir ) {
1100  wfDeprecated( __METHOD__, '1.35' );
1101  return ExtensionInfo::getAuthorsFileName( $extDir );
1102  }
1103 
1116  public static function getExtLicenseFileName( $extDir ) {
1117  wfDeprecated( __METHOD__, '1.35' );
1118  $licenses = ExtensionInfo::getLicenseFileNames( $extDir );
1119  if ( count( $licenses ) === 0 ) {
1120  return false;
1121  }
1122  return $licenses[0];
1123  }
1124 
1133  public function listToText( $list, $sort = true ) {
1134  if ( !count( $list ) ) {
1135  return '';
1136  }
1137  if ( $sort ) {
1138  sort( $list );
1139  }
1140 
1141  return $this->getLanguage()
1142  ->listToText( array_map( [ __CLASS__, 'arrayToString' ], $list ) );
1143  }
1144 
1153  public static function arrayToString( $list ) {
1154  if ( is_array( $list ) && count( $list ) == 1 ) {
1155  $list = $list[0];
1156  }
1157  if ( $list instanceof Closure ) {
1158  // Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$"
1159  return 'Closure';
1160  } elseif ( is_object( $list ) ) {
1161  $class = wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1162 
1163  return $class;
1164  } elseif ( !is_array( $list ) ) {
1165  return $list;
1166  } else {
1167  if ( is_object( $list[0] ) ) {
1168  $class = get_class( $list[0] );
1169  } else {
1170  $class = $list[0];
1171  }
1172 
1173  return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1174  }
1175  }
1176 
1181  public static function getGitHeadSha1( $dir ) {
1182  $repo = new GitInfo( $dir );
1183 
1184  return $repo->getHeadSHA1();
1185  }
1186 
1191  public static function getGitCurrentBranch( $dir ) {
1192  $repo = new GitInfo( $dir );
1193  return $repo->getCurrentBranch();
1194  }
1195 
1200  public function getEntryPointInfo() {
1201  $config = $this->getConfig();
1202  $scriptPath = $config->get( 'ScriptPath' ) ?: '/';
1203 
1204  $entryPoints = [
1205  'version-entrypoints-articlepath' => $config->get( 'ArticlePath' ),
1206  'version-entrypoints-scriptpath' => $scriptPath,
1207  'version-entrypoints-index-php' => wfScript( 'index' ),
1208  'version-entrypoints-api-php' => wfScript( 'api' ),
1209  'version-entrypoints-rest-php' => wfScript( 'rest' ),
1210  ];
1211 
1212  $language = $this->getLanguage();
1213  $thAttribures = [
1214  'dir' => $language->getDir(),
1215  'lang' => $language->getHtmlCode()
1216  ];
1217  $out = Html::element(
1218  'h2',
1219  [ 'id' => 'mw-version-entrypoints' ],
1220  $this->msg( 'version-entrypoints' )->text()
1221  ) .
1222  Html::openElement( 'table',
1223  [
1224  'class' => 'wikitable plainlinks',
1225  'id' => 'mw-version-entrypoints-table',
1226  'dir' => 'ltr',
1227  'lang' => 'en'
1228  ]
1229  ) .
1230  Html::openElement( 'tr' ) .
1231  Html::element(
1232  'th',
1233  $thAttribures,
1234  $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1235  ) .
1236  Html::element(
1237  'th',
1238  $thAttribures,
1239  $this->msg( 'version-entrypoints-header-url' )->text()
1240  ) .
1241  Html::closeElement( 'tr' );
1242 
1243  foreach ( $entryPoints as $message => $value ) {
1244  $url = wfExpandUrl( $value, PROTO_RELATIVE );
1245  $out .= Html::openElement( 'tr' ) .
1246  // ->plain() looks like it should be ->parse(), but this function
1247  // returns wikitext, not HTML, boo
1248  Html::rawElement( 'td', [], $this->msg( $message )->plain() ) .
1249  Html::rawElement( 'td', [], Html::rawElement( 'code', [], "[$url $value]" ) ) .
1250  Html::closeElement( 'tr' );
1251  }
1252 
1253  $out .= Html::closeElement( 'table' );
1254 
1255  return $out;
1256  }
1257 
1258  protected function getGroupName() {
1259  return 'wiki';
1260  }
1261 }
SpecialVersion\openExtType
openExtType( $text=null, $name=null)
Definition: SpecialVersion.php:956
SpecialPage\getPageTitle
getPageTitle( $subpage=false)
Get a self-referential title object.
Definition: SpecialPage.php:669
SpecialVersion\__construct
__construct()
Definition: SpecialVersion.php:51
SpecialPage\msg
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
Definition: SpecialPage.php:800
SpecialVersion\listAuthors
listAuthors( $authors, $extName, $extDir)
Return a formatted unsorted list of authors.
Definition: SpecialVersion.php:1029
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:1099
SpecialPage\getOutput
getOutput()
Get the OutputPage being used for this instance.
Definition: SpecialPage.php:716
SpecialVersion\listToText
listToText( $list, $sort=true)
Convert an array of items into a list for display.
Definition: SpecialVersion.php:1133
SpecialVersion\getMediaWikiCredits
static getMediaWikiCredits()
Returns wiki text showing the license information.
Definition: SpecialVersion.php:193
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:149
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
SpecialVersion\getCreditsForExtension
getCreditsForExtension( $type, array $extension)
Creates and formats a version line for a single extension.
Definition: SpecialVersion.php:748
SpecialVersion\$coreId
string $coreId
The current rev id/SHA hash of MediaWiki core.
Definition: SpecialVersion.php:44
Sanitizer\escapeIdForAttribute
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:1116
SpecialVersion\compare
compare( $a, $b)
Callback to sort extensions by type.
Definition: SpecialVersion.php:726
SpecialVersion\getCopyrightAndAuthorList
static getCopyrightAndAuthorList()
Get the "MediaWiki is copyright 2001-20xx by lots of cool folks" text.
Definition: SpecialVersion.php:217
SpecialVersion\getExtensionCategory
getExtensionCategory( $type, $message, array $creditsGroup)
Creates and returns the HTML for a single extension category.
Definition: SpecialVersion.php:701
ExtensionRegistry
ExtensionRegistry class.
Definition: ExtensionRegistry.php:18
MW_VERSION
const MW_VERSION
The running version of MediaWiki.
Definition: Defines.php:39
ExtensionRegistry\getAllThings
getAllThings()
Get credits information about all installed extensions and skins.
Definition: ExtensionRegistry.php:644
$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:1198
SpecialPage\getLanguage
getLanguage()
Shortcut to get user's language.
Definition: SpecialPage.php:746
SpecialVersion\getParserFunctionHooks
getParserFunctionHooks()
Obtains a list of installed parser function hooks and the associated H2 header.
Definition: SpecialVersion.php:668
SpecialVersion\getExtensionCredits
getExtensionCredits(array $credits)
Generate wikitext showing the name, URL, author and description of each extension.
Definition: SpecialVersion.php:444
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:339
SpecialVersion\getSkinCredits
getSkinCredits(array $credits)
Generate wikitext showing the name, URL, author and description of each skin.
Definition: SpecialVersion.php:497
$dbr
$dbr
Definition: testCompression.php:54
SpecialVersion\arrayToString
static arrayToString( $list)
Convert an array or object to a string for display.
Definition: SpecialVersion.php:1153
SpecialVersion\softwareInformation
static softwareInformation()
Returns HTML showing the third party software versions (apache, php, mysql).
Definition: SpecialVersion.php:281
Html\closeElement
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:315
ExtensionRegistry\getInstance
static getInstance()
Definition: ExtensionRegistry.php:136
Config
Interface for configuration instances.
Definition: Config.php:28
SpecialVersion\getExternalLibraries
getExternalLibraries(array $credits)
Generate an HTML table for external libraries that are installed.
Definition: SpecialVersion.php:523
SpecialVersion\IPInfo
IPInfo()
Get information about client's IP address.
Definition: SpecialVersion.php:1002
$wgHooks
$wgHooks
Global list of hooks.
Definition: DefaultSettings.php:7856
SpecialPage\getConfig
getConfig()
Shortcut to get main config object.
Definition: SpecialPage.php:766
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1030
SpecialVersion\getSoftwareInformation
static getSoftwareInformation()
Definition: SpecialVersion.php:254
wfScript
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
Definition: GlobalFunctions.php:2530
SpecialVersion\getExtensionTypeName
static getExtensionTypeName( $type)
Returns the internationalized name for an extension type.
Definition: SpecialVersion.php:432
Config\get
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2463
$wgLang
$wgLang
Definition: Setup.php:781
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
SpecialVersion\getMWVersionLinked
static getMWVersionLinked()
Definition: SpecialVersion.php:353
SpecialPage\setHeaders
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
Definition: SpecialPage.php:551
SpecialVersion\getExtensionTypes
static getExtensionTypes()
Returns an array with the base extension types.
Definition: SpecialVersion.php:403
Linker\makeExternalLink
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition: Linker.php:846
wfGetCache
wfGetCache( $cacheType)
Get a specific cache object.
Definition: GlobalFunctions.php:2739
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
SpecialVersion\getWgHooks
getWgHooks()
Generate wikitext showing hooks in $wgHooks.
Definition: SpecialVersion.php:926
SpecialVersion\$extensionTypes
static string[] false $extensionTypes
Lazy initialized key/value with message content.
Definition: SpecialVersion.php:49
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:1258
SpecialVersion\getExtLicenseFileName
static getExtLicenseFileName( $extDir)
Obtains the full path of an extensions COPYING or LICENSE file if one exists.
Definition: SpecialVersion.php:1116
SpecialVersion\execute
execute( $par)
main()
Definition: SpecialVersion.php:74
SpecialVersion\getVersion
static getVersion( $flags='', $lang=null)
Return a string of the MediaWiki version with Git revision if available.
Definition: SpecialVersion.php:310
GitInfo
Definition: GitInfo.php:28
SpecialVersion\getGitCurrentBranch
static getGitCurrentBranch( $dir)
Definition: SpecialVersion.php:1191
SpecialVersion\getCredits
static getCredits(ExtensionRegistry $reg, Config $conf)
Definition: SpecialVersion.php:62
SpecialPage
Parent class for all special pages.
Definition: SpecialPage.php:39
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:171
SpecialPage\getRequest
getRequest()
Get the WebRequest being used for this instance.
Definition: SpecialPage.php:706
CACHE_ANYTHING
const CACHE_ANYTHING
Definition: Defines.php:90
SpecialVersion
Give information about the version of MediaWiki, PHP, the DB and extensions.
Definition: SpecialVersion.php:34
PROTO_RELATIVE
const PROTO_RELATIVE
Definition: Defines.php:210
SpecialVersion\getGitHeadSha1
static getGitHeadSha1( $dir)
Definition: SpecialVersion.php:1181
SpecialVersion\getEntryPointInfo
getEntryPointInfo()
Get the list of entry points and their URLs.
Definition: SpecialVersion.php:1200
SpecialPage\getLinkRenderer
getLinkRenderer()
Definition: SpecialPage.php:912
SpecialVersion\$firstExtOpened
bool $firstExtOpened
Definition: SpecialVersion.php:39
SpecialVersion\getVersionLinkedGit
static getVersionLinkedGit()
Definition: SpecialVersion.php:369
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
Html\openElement
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:251
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
$ext
if(!is_readable( $file)) $ext
Definition: router.php:48
SpecialPage\$linkRenderer
MediaWiki Linker LinkRenderer null $linkRenderer
Definition: SpecialPage.php:69
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:231
SpecialVersion\getParserTags
getParserTags()
Obtains a list of installed parser tags and the associated H2 header.
Definition: SpecialVersion.php:626
$IP
$IP
Definition: WebStart.php:49
MWTimestamp\getLocalInstance
static getLocalInstance( $ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
Definition: MWTimestamp.php:203
$wgSpecialVersionShowHooks
$wgSpecialVersionShowHooks
Show the contents of $wgHooks in Special:Version.
Definition: DefaultSettings.php:6646
MediaWiki\ExtensionInfo
Definition: ExtensionInfo.php:8
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:636
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:52