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  $out->setPageTitle( $this->msg( 'version-license-title', $extName ) );
141 
142  $licenseFound = false;
143 
144  if ( $extName === 'MediaWiki' ) {
145  $out->addWikiTextAsInterface(
146  file_get_contents( $IP . '/COPYING' )
147  );
148  $licenseFound = true;
149  } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
150  $files = ExtensionInfo::getLicenseFileNames( dirname( $extNode['path'] ) );
151 
152  if ( count( $files ) ) {
153  $licenseFound = true;
154  foreach ( $files as $file ) {
155  $out->addWikiTextAsInterface(
157  'pre',
158  [
159  'lang' => 'en',
160  'dir' => 'ltr',
161  ],
162  file_get_contents( $file )
163  )
164  );
165  }
166  }
167  }
168  if ( !$licenseFound ) {
169  $out->addWikiTextAsInterface( '{{int:version-license-not-found}}' );
170  }
171  break;
172  default:
173  $out->addModuleStyles( 'mediawiki.special.version' );
174  $out->addWikiTextAsInterface(
175  self::getMediaWikiCredits() .
176  self::softwareInformation() .
177  $this->getEntryPointInfo()
178  );
179  $out->addHTML(
180  $this->getSkinCredits( $credits ) .
181  $this->getExtensionCredits( $credits ) .
182  $this->getExternalLibraries( $credits ) .
183  $this->getParserTags() .
184  $this->getParserFunctionHooks()
185  );
186  $out->addWikiTextAsInterface( $this->getWgHooks() );
187  $out->addHTML( $this->IPInfo() );
188 
189  break;
190  }
191  }
192 
198  private static function getMediaWikiCredits() {
199  $ret = Xml::element(
200  'h2',
201  [ 'id' => 'mw-version-license' ],
202  wfMessage( 'version-license' )->text()
203  );
204 
205  // This text is always left-to-right.
206  $ret .= '<div class="plainlinks">';
207  $ret .= "__NOTOC__
209  " . '<div class="mw-version-license-info">' .
210  wfMessage( 'version-license-info' )->text() .
211  '</div>';
212  $ret .= '</div>';
213 
214  return str_replace( "\t\t", '', $ret ) . "\n";
215  }
216 
222  public static function getCopyrightAndAuthorList() {
223  global $wgLang;
224 
225  if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
226  $othersLink = '[https://www.mediawiki.org/wiki/Special:Version/Credits ' .
227  wfMessage( 'version-poweredby-others' )->text() . ']';
228  } else {
229  $othersLink = '[[Special:Version/Credits|' .
230  wfMessage( 'version-poweredby-others' )->text() . ']]';
231  }
232 
233  $translatorsLink = '[https://translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
234  wfMessage( 'version-poweredby-translators' )->text() . ']';
235 
236  $authorList = [
237  'Magnus Manske', 'Brion Vibber', 'Lee Daniel Crocker',
238  'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
239  'Niklas Laxström', 'Domas Mituzas', 'Rob Church', 'Yuri Astrakhan',
240  'Aryeh Gregor', 'Aaron Schulz', 'Andrew Garrett', 'Raimond Spekking',
241  'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
242  'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
243  'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
244  'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', 'Brad Jorsch',
245  'Bartosz Dziewoński', 'Ed Sanders', 'Moriel Schottlender',
246  'Kunal Mehta', 'James D. Forrester', 'Brian Wolff', 'Adam Shorland',
247  $othersLink, $translatorsLink
248  ];
249 
250  return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
251  $wgLang->listToText( $authorList ) )->text();
252  }
253 
259  public static function getSoftwareInformation() {
260  $dbr = wfGetDB( DB_REPLICA );
261 
262  // Put the software in an array of form 'name' => 'version'. All messages should
263  // be loaded here, so feel free to use wfMessage in the 'name'. Raw HTML or
264  // wikimarkup can be used.
265  $software = [
266  '[https://www.mediawiki.org/ MediaWiki]' => self::getVersionLinked(),
267  '[https://php.net/ PHP]' => PHP_VERSION . " (" . PHP_SAPI . ")",
268  $dbr->getSoftwareLink() => $dbr->getServerInfo(),
269  ];
270 
271  if ( defined( 'INTL_ICU_VERSION' ) ) {
272  $software['[http://site.icu-project.org/ ICU]'] = INTL_ICU_VERSION;
273  }
274 
275  // Allow a hook to add/remove items.
276  Hooks::runner()->onSoftwareInfo( $software );
277 
278  return $software;
279  }
280 
286  public static function softwareInformation() {
287  $out = Xml::element(
288  'h2',
289  [ 'id' => 'mw-version-software' ],
290  wfMessage( 'version-software' )->text()
291  ) .
292  Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ] ) .
293  "<tr>
294  <th>" . wfMessage( 'version-software-product' )->text() . "</th>
295  <th>" . wfMessage( 'version-software-version' )->text() . "</th>
296  </tr>\n";
297 
298  foreach ( self::getSoftwareInformation() as $name => $version ) {
299  $out .= "<tr>
300  <td>" . $name . "</td>
301  <td dir=\"ltr\">" . $version . "</td>
302  </tr>\n";
303  }
304 
305  return $out . Xml::closeElement( 'table' );
306  }
307 
315  public static function getVersion( $flags = '', $lang = null ) {
316  global $IP;
317 
318  $gitInfo = self::getGitHeadSha1( $IP );
319  if ( !$gitInfo ) {
320  $version = MW_VERSION;
321  } elseif ( $flags === 'nodb' ) {
322  $shortSha1 = substr( $gitInfo, 0, 7 );
323  $version = MW_VERSION . " ($shortSha1)";
324  } else {
325  $shortSha1 = substr( $gitInfo, 0, 7 );
326  $msg = wfMessage( 'parentheses' );
327  if ( $lang !== null ) {
328  $msg->inLanguage( $lang );
329  }
330  $shortSha1 = $msg->params( $shortSha1 )->escaped();
331  $version = MW_VERSION . ' ' . $shortSha1;
332  }
333 
334  return $version;
335  }
336 
344  public static function getVersionLinked() {
345  $gitVersion = self::getVersionLinkedGit();
346  if ( $gitVersion ) {
347  $v = $gitVersion;
348  } else {
349  $v = MW_VERSION; // fallback
350  }
351 
352  return $v;
353  }
354 
358  private static function getMWVersionLinked() {
359  $versionUrl = "";
360  if ( Hooks::runner()->onSpecialVersionVersionUrl( MW_VERSION, $versionUrl ) ) {
361  $versionParts = [];
362  preg_match( "/^(\d+\.\d+)/", MW_VERSION, $versionParts );
363  $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
364  }
365 
366  return '[' . $versionUrl . ' ' . MW_VERSION . ']';
367  }
368 
374  private static function getVersionLinkedGit() {
375  global $IP, $wgLang;
376 
377  $gitInfo = new GitInfo( $IP );
378  $headSHA1 = $gitInfo->getHeadSHA1();
379  if ( !$headSHA1 ) {
380  return false;
381  }
382 
383  $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
384 
385  $gitHeadUrl = $gitInfo->getHeadViewUrl();
386  if ( $gitHeadUrl !== false ) {
387  $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
388  }
389 
390  $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
391  if ( $gitHeadCommitDate ) {
392  $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( $gitHeadCommitDate, true );
393  }
394 
395  return self::getMWVersionLinked() . " $shortSHA1";
396  }
397 
408  public static function getExtensionTypes() {
409  if ( self::$extensionTypes === false ) {
410  self::$extensionTypes = [
411  'specialpage' => wfMessage( 'version-specialpages' )->text(),
412  'editor' => wfMessage( 'version-editors' )->text(),
413  'parserhook' => wfMessage( 'version-parserhooks' )->text(),
414  'variable' => wfMessage( 'version-variables' )->text(),
415  'media' => wfMessage( 'version-mediahandlers' )->text(),
416  'antispam' => wfMessage( 'version-antispam' )->text(),
417  'skin' => wfMessage( 'version-skins' )->text(),
418  'api' => wfMessage( 'version-api' )->text(),
419  'other' => wfMessage( 'version-other' )->text(),
420  ];
421 
422  Hooks::runner()->onExtensionTypes( self::$extensionTypes );
423  }
424 
425  return self::$extensionTypes;
426  }
427 
437  public static function getExtensionTypeName( $type ) {
438  $types = self::getExtensionTypes();
439 
440  return $types[$type] ?? $types['other'];
441  }
442 
449  private function getExtensionCredits( array $credits ) {
450  if (
451  !$credits ||
452  // Skins are displayed separately, see getSkinCredits()
453  ( count( $credits ) === 1 && isset( $credits['skin'] ) )
454  ) {
455  return '';
456  }
457 
459 
460  $out = Xml::element(
461  'h2',
462  [ 'id' => 'mw-version-ext' ],
463  $this->msg( 'version-extensions' )->text()
464  ) .
465  Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-ext' ] );
466 
467  // Make sure the 'other' type is set to an array.
468  if ( !array_key_exists( 'other', $credits ) ) {
469  $credits['other'] = [];
470  }
471 
472  // Find all extensions that do not have a valid type and give them the type 'other'.
473  foreach ( $credits as $type => $extensions ) {
474  if ( !array_key_exists( $type, $extensionTypes ) ) {
475  $credits['other'] = array_merge( $credits['other'], $extensions );
476  }
477  }
478 
479  $this->firstExtOpened = false;
480  // Loop through the extension categories to display their extensions in the list.
481  foreach ( $extensionTypes as $type => $message ) {
482  // Skins have a separate section
483  if ( $type !== 'other' && $type !== 'skin' ) {
484  $out .= $this->getExtensionCategory( $type, $message, $credits[$type] ?? [] );
485  }
486  }
487 
488  // We want the 'other' type to be last in the list.
489  $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'], $credits['other'] );
490 
491  $out .= Xml::closeElement( 'table' );
492 
493  return $out;
494  }
495 
502  private function getSkinCredits( array $credits ) {
503  if ( !isset( $credits['skin'] ) || count( $credits['skin'] ) === 0 ) {
504  return '';
505  }
506 
507  $out = Xml::element(
508  'h2',
509  [ 'id' => 'mw-version-skin' ],
510  $this->msg( 'version-skins' )->text()
511  ) .
512  Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-skin' ] );
513 
514  $this->firstExtOpened = false;
515  $out .= $this->getExtensionCategory( 'skin', null, $credits['skin'] );
516 
517  $out .= Xml::closeElement( 'table' );
518 
519  return $out;
520  }
521 
528  protected function getExternalLibraries( array $credits ) {
529  global $IP;
530  $paths = [
531  "$IP/vendor/composer/installed.json"
532  ];
533 
535  foreach ( $extensionTypes as $type => $message ) {
536  if ( !isset( $credits[$type] ) || $credits[$type] === [] ) {
537  continue;
538  }
539  foreach ( $credits[$type] as $extension ) {
540  if ( !isset( $extension['path'] ) ) {
541  continue;
542  }
543  $paths[] = dirname( $extension['path'] ) . '/vendor/composer/installed.json';
544  }
545  }
546 
547  $dependencies = [];
548 
549  foreach ( $paths as $path ) {
550  if ( !file_exists( $path ) ) {
551  continue;
552  }
553 
554  $installed = new ComposerInstalled( $path );
555 
556  $dependencies += $installed->getInstalledDependencies();
557  }
558 
559  if ( $dependencies === [] ) {
560  return '';
561  }
562 
563  ksort( $dependencies );
564 
565  $out = Html::element(
566  'h2',
567  [ 'id' => 'mw-version-libraries' ],
568  $this->msg( 'version-libraries' )->text()
569  );
570  $out .= Html::openElement(
571  'table',
572  [ 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries' ]
573  );
574  $out .= Html::openElement( 'tr' )
575  . Html::element( 'th', [], $this->msg( 'version-libraries-library' )->text() )
576  . Html::element( 'th', [], $this->msg( 'version-libraries-version' )->text() )
577  . Html::element( 'th', [], $this->msg( 'version-libraries-license' )->text() )
578  . Html::element( 'th', [], $this->msg( 'version-libraries-description' )->text() )
579  . Html::element( 'th', [], $this->msg( 'version-libraries-authors' )->text() )
580  . Html::closeElement( 'tr' );
581 
582  foreach ( $dependencies as $name => $info ) {
583  if ( !is_array( $info ) || strpos( $info['type'], 'mediawiki-' ) === 0 ) {
584  // Skip any extensions or skins since they'll be listed
585  // in their proper section
586  continue;
587  }
588  $authors = array_map( function ( $arr ) {
589  // If a homepage is set, link to it
590  if ( isset( $arr['homepage'] ) ) {
591  return "[{$arr['homepage']} {$arr['name']}]";
592  }
593  return $arr['name'];
594  }, $info['authors'] );
595  $authors = $this->listAuthors( $authors, false, "$IP/vendor/$name" );
596 
597  // We can safely assume that the libraries' names and descriptions
598  // are written in English and aren't going to be translated,
599  // so set appropriate lang and dir attributes
600  $out .= Html::openElement( 'tr', [
601  // Add an anchor so docs can link easily to the version of
602  // this specific library
604  "mw-version-library-$name"
605  ) ] )
607  'td',
608  [],
610  "https://packagist.org/packages/$name", $name,
611  true, '',
612  [ 'class' => 'mw-version-library-name' ]
613  )
614  )
615  . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
616  . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['licenses'] ) )
617  . Html::element( 'td', [ 'lang' => 'en', 'dir' => 'ltr' ], $info['description'] )
618  . Html::rawElement( 'td', [], $authors )
619  . Html::closeElement( 'tr' );
620  }
621  $out .= Html::closeElement( 'table' );
622 
623  return $out;
624  }
625 
631  protected function getParserTags() {
632  $tags = MediaWikiServices::getInstance()->getParser()->getTags();
633 
634  if ( count( $tags ) ) {
635  $out = Html::rawElement(
636  'h2',
637  [
638  'class' => 'mw-headline plainlinks',
639  'id' => 'mw-version-parser-extensiontags',
640  ],
642  'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
643  $this->msg( 'version-parser-extensiontags' )->parse(),
644  false /* msg()->parse() already escapes */
645  )
646  );
647 
648  array_walk( $tags, function ( &$value ) {
649  // Bidirectional isolation improves readability in RTL wikis
650  $value = Html::element(
651  'bdi',
652  // Prevent < and > from slipping to another line
653  [
654  'style' => 'white-space: nowrap;',
655  ],
656  "<$value>"
657  );
658  } );
659 
660  $out .= $this->listToText( $tags );
661  } else {
662  $out = '';
663  }
664 
665  return $out;
666  }
667 
673  protected function getParserFunctionHooks() {
674  $fhooks = MediaWikiServices::getInstance()->getParser()->getFunctionHooks();
675  if ( count( $fhooks ) ) {
676  $out = Html::rawElement(
677  'h2',
678  [
679  'class' => 'mw-headline plainlinks',
680  'id' => 'mw-version-parser-function-hooks',
681  ],
683  'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
684  $this->msg( 'version-parser-function-hooks' )->parse(),
685  false /* msg()->parse() already escapes */
686  )
687  );
688 
689  $out .= $this->listToText( $fhooks );
690  } else {
691  $out = '';
692  }
693 
694  return $out;
695  }
696 
706  protected function getExtensionCategory( $type, $message, array $creditsGroup ) {
707  $config = $this->getConfig();
708  $credits = $config->get( 'ExtensionCredits' );
709 
710  $out = '';
711 
712  if ( $creditsGroup ) {
713  $out .= $this->openExtType( $message, 'credits-' . $type );
714 
715  usort( $creditsGroup, [ $this, 'compare' ] );
716 
717  foreach ( $creditsGroup as $extension ) {
718  $out .= $this->getCreditsForExtension( $type, $extension );
719  }
720  }
721 
722  return $out;
723  }
724 
731  public function compare( $a, $b ) {
732  return $this->getLanguage()->lc( $a['name'] ) <=> $this->getLanguage()->lc( $b['name'] );
733  }
734 
753  public function getCreditsForExtension( $type, array $extension ) {
754  $out = $this->getOutput();
755 
756  // We must obtain the information for all the bits and pieces!
757  // ... such as extension names and links
758  if ( isset( $extension['namemsg'] ) ) {
759  // Localized name of extension
760  $extensionName = $this->msg( $extension['namemsg'] )->text();
761  } elseif ( isset( $extension['name'] ) ) {
762  // Non localized version
763  $extensionName = $extension['name'];
764  } else {
765  $extensionName = $this->msg( 'version-no-ext-name' )->text();
766  }
767 
768  if ( isset( $extension['url'] ) ) {
769  $extensionNameLink = Linker::makeExternalLink(
770  $extension['url'],
771  $extensionName,
772  true,
773  '',
774  [ 'class' => 'mw-version-ext-name' ]
775  );
776  } else {
777  $extensionNameLink = htmlspecialchars( $extensionName );
778  }
779 
780  // ... and the version information
781  // If the extension path is set we will check that directory for GIT
782  // metadata in an attempt to extract date and vcs commit metadata.
783  $canonicalVersion = '&ndash;';
784  $extensionPath = null;
785  $vcsVersion = null;
786  $vcsLink = null;
787  $vcsDate = null;
788 
789  if ( isset( $extension['version'] ) ) {
790  $canonicalVersion = $out->parseInlineAsInterface( $extension['version'] );
791  }
792 
793  if ( isset( $extension['path'] ) ) {
794  global $IP;
795  $extensionPath = dirname( $extension['path'] );
796  if ( $this->coreId == '' ) {
797  wfDebug( 'Looking up core head id' );
798  $coreHeadSHA1 = self::getGitHeadSha1( $IP );
799  if ( $coreHeadSHA1 ) {
800  $this->coreId = $coreHeadSHA1;
801  }
802  }
804  $memcKey = $cache->makeKey(
805  'specialversion-ext-version-text', $extension['path'], $this->coreId
806  );
807  list( $vcsVersion, $vcsLink, $vcsDate ) = $cache->get( $memcKey );
808 
809  if ( !$vcsVersion ) {
810  wfDebug( "Getting VCS info for extension {$extension['name']}" );
811  $gitInfo = new GitInfo( $extensionPath );
812  $vcsVersion = $gitInfo->getHeadSHA1();
813  if ( $vcsVersion !== false ) {
814  $vcsVersion = substr( $vcsVersion, 0, 7 );
815  $vcsLink = $gitInfo->getHeadViewUrl();
816  $vcsDate = $gitInfo->getHeadCommitDate();
817  }
818  $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
819  } else {
820  wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" );
821  }
822  }
823 
824  $versionString = Html::rawElement(
825  'span',
826  [ 'class' => 'mw-version-ext-version' ],
827  $canonicalVersion
828  );
829 
830  if ( $vcsVersion ) {
831  if ( $vcsLink ) {
832  $vcsVerString = Linker::makeExternalLink(
833  $vcsLink,
834  $this->msg( 'version-version', $vcsVersion ),
835  true,
836  '',
837  [ 'class' => 'mw-version-ext-vcs-version' ]
838  );
839  } else {
840  $vcsVerString = Html::element( 'span',
841  [ 'class' => 'mw-version-ext-vcs-version' ],
842  "({$vcsVersion})"
843  );
844  }
845  $versionString .= " {$vcsVerString}";
846 
847  if ( $vcsDate ) {
848  $versionString .= ' ' . Html::element( 'span', [
849  'class' => 'mw-version-ext-vcs-timestamp',
850  'dir' => $this->getLanguage()->getDir(),
851  ], $this->getLanguage()->timeanddate( $vcsDate, true ) );
852  }
853  $versionString = Html::rawElement( 'span',
854  [ 'class' => 'mw-version-ext-meta-version' ],
855  $versionString
856  );
857  }
858 
859  // ... and license information; if a license file exists we
860  // will link to it
861  $licenseLink = '';
862  if ( isset( $extension['name'] ) ) {
863  $licenseName = null;
864  if ( isset( $extension['license-name'] ) ) {
865  $licenseName = new HtmlArmor( $out->parseInlineAsInterface( $extension['license-name'] ) );
866  } elseif ( ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
867  $licenseName = $this->msg( 'version-ext-license' )->text();
868  }
869  if ( $licenseName !== null ) {
870  $licenseLink = $this->getLinkRenderer()->makeLink(
871  $this->getPageTitle( 'License/' . $extension['name'] ),
872  $licenseName,
873  [
874  'class' => 'mw-version-ext-license',
875  'dir' => 'auto',
876  ]
877  );
878  }
879  }
880 
881  // ... and generate the description; which can be a parameterized l10n message
882  // in the form [ <msgname>, <parameter>, <parameter>... ] or just a straight
883  // up string
884  if ( isset( $extension['descriptionmsg'] ) ) {
885  // Localized description of extension
886  $descriptionMsg = $extension['descriptionmsg'];
887 
888  if ( is_array( $descriptionMsg ) ) {
889  $descriptionMsgKey = $descriptionMsg[0]; // Get the message key
890  array_shift( $descriptionMsg ); // Shift out the message key to get the parameters only
891  array_map( "htmlspecialchars", $descriptionMsg ); // For sanity
892  $description = $this->msg( $descriptionMsgKey, $descriptionMsg )->text();
893  } else {
894  $description = $this->msg( $descriptionMsg )->text();
895  }
896  } elseif ( isset( $extension['description'] ) ) {
897  // Non localized version
898  $description = $extension['description'];
899  } else {
900  $description = '';
901  }
902  $description = $out->parseInlineAsInterface( $description );
903 
904  // ... now get the authors for this extension
905  $authors = $extension['author'] ?? [];
906  $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
907 
908  // Finally! Create the table
909  $html = Html::openElement( 'tr', [
910  'class' => 'mw-version-ext',
911  'id' => Sanitizer::escapeIdForAttribute( 'mw-version-ext-' . $type . '-' . $extension['name'] )
912  ]
913  );
914 
915  $html .= Html::rawElement( 'td', [], $extensionNameLink );
916  $html .= Html::rawElement( 'td', [], $versionString );
917  $html .= Html::rawElement( 'td', [], $licenseLink );
918  $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-description' ], $description );
919  $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-authors' ], $authors );
920 
921  $html .= Html::closeElement( 'tr' );
922 
923  return $html;
924  }
925 
931  private function getWgHooks() {
933 
934  if ( $wgSpecialVersionShowHooks && count( $wgHooks ) ) {
935  $myWgHooks = $wgHooks;
936  ksort( $myWgHooks );
937 
938  $ret = [];
939  $ret[] = '== {{int:version-hooks}} ==';
940  $ret[] = Html::openElement( 'table', [ 'class' => 'wikitable', 'id' => 'sv-hooks' ] );
941  $ret[] = Html::openElement( 'tr' );
942  $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-name' )->text() );
943  $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-subscribedby' )->text() );
944  $ret[] = Html::closeElement( 'tr' );
945 
946  foreach ( $myWgHooks as $hook => $hooks ) {
947  $ret[] = Html::openElement( 'tr' );
948  $ret[] = Html::element( 'td', [], $hook );
949  $ret[] = Html::element( 'td', [], $this->listToText( $hooks ) );
950  $ret[] = Html::closeElement( 'tr' );
951  }
952 
953  $ret[] = Html::closeElement( 'table' );
954 
955  return implode( "\n", $ret );
956  }
957 
958  return '';
959  }
960 
961  private function openExtType( $text = null, $name = null ) {
962  $out = '';
963 
964  $opt = [ 'colspan' => 5 ];
965  if ( $this->firstExtOpened ) {
966  // Insert a spacing line
967  $out .= Html::rawElement( 'tr', [ 'class' => 'sv-space' ],
968  Html::element( 'td', $opt )
969  );
970  }
971  $this->firstExtOpened = true;
972 
973  if ( $name ) {
974  $opt['id'] = "sv-$name";
975  }
976 
977  if ( $text !== null ) {
978  $out .= Html::rawElement( 'tr', [],
979  Html::element( 'th', $opt, $text )
980  );
981  }
982 
983  $firstHeadingMsg = ( $name === 'credits-skin' )
984  ? 'version-skin-colheader-name'
985  : 'version-ext-colheader-name';
986  $out .= Html::openElement( 'tr' );
987  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
988  $this->msg( $firstHeadingMsg )->text() );
989  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
990  $this->msg( 'version-ext-colheader-version' )->text() );
991  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
992  $this->msg( 'version-ext-colheader-license' )->text() );
993  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
994  $this->msg( 'version-ext-colheader-description' )->text() );
995  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
996  $this->msg( 'version-ext-colheader-credits' )->text() );
997  $out .= Html::closeElement( 'tr' );
998 
999  return $out;
1000  }
1001 
1007  private function IPInfo() {
1008  $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
1009 
1010  return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
1011  }
1012 
1034  public function listAuthors( $authors, $extName, $extDir ) {
1035  $hasOthers = false;
1036  $linkRenderer = $this->getLinkRenderer();
1037 
1038  $list = [];
1039  $authors = (array)$authors;
1040 
1041  // Special case: if the authors array has only one item and it is "...",
1042  // it should not be rendered as the "version-poweredby-others" i18n msg,
1043  // but rather as "version-poweredby-various" i18n msg instead.
1044  if ( count( $authors ) === 1 && $authors[0] === '...' ) {
1045  // Link to the extension's or skin's AUTHORS or CREDITS file, if there is
1046  // such a file; otherwise just return the i18n msg as-is
1047  if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1048  return $linkRenderer->makeLink(
1049  $this->getPageTitle( "Credits/$extName" ),
1050  $this->msg( 'version-poweredby-various' )->text()
1051  );
1052  } else {
1053  return $this->msg( 'version-poweredby-various' )->escaped();
1054  }
1055  }
1056 
1057  // Otherwise, if we have an actual array that has more than one item,
1058  // process each array item as usual
1059  foreach ( $authors as $item ) {
1060  if ( $item == '...' ) {
1061  $hasOthers = true;
1062 
1063  if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1064  $text = $linkRenderer->makeLink(
1065  $this->getPageTitle( "Credits/$extName" ),
1066  $this->msg( 'version-poweredby-others' )->text()
1067  );
1068  } else {
1069  $text = $this->msg( 'version-poweredby-others' )->escaped();
1070  }
1071  $list[] = $text;
1072  } elseif ( substr( $item, -5 ) == ' ...]' ) {
1073  $hasOthers = true;
1074  $list[] = $this->getOutput()->parseInlineAsInterface(
1075  substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
1076  );
1077  } else {
1078  $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1079  }
1080  }
1081 
1082  if ( $extName && !$hasOthers && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1083  $list[] = $text = $linkRenderer->makeLink(
1084  $this->getPageTitle( "Credits/$extName" ),
1085  $this->msg( 'version-poweredby-others' )->text()
1086  );
1087  }
1088 
1089  return $this->listToText( $list, false );
1090  }
1091 
1104  public static function getExtAuthorsFileName( $extDir ) {
1105  wfDeprecated( __METHOD__, '1.35' );
1106  return ExtensionInfo::getAuthorsFileName( $extDir );
1107  }
1108 
1121  public static function getExtLicenseFileName( $extDir ) {
1122  wfDeprecated( __METHOD__, '1.35' );
1123  $licenses = ExtensionInfo::getLicenseFileNames( $extDir );
1124  if ( count( $licenses ) === 0 ) {
1125  return false;
1126  }
1127  return $licenses[0];
1128  }
1129 
1138  public function listToText( $list, $sort = true ) {
1139  if ( !count( $list ) ) {
1140  return '';
1141  }
1142  if ( $sort ) {
1143  sort( $list );
1144  }
1145 
1146  return $this->getLanguage()
1147  ->listToText( array_map( [ __CLASS__, 'arrayToString' ], $list ) );
1148  }
1149 
1158  public static function arrayToString( $list ) {
1159  if ( is_array( $list ) && count( $list ) == 1 ) {
1160  $list = $list[0];
1161  }
1162  if ( $list instanceof Closure ) {
1163  // Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$"
1164  return 'Closure';
1165  } elseif ( is_object( $list ) ) {
1166  $class = wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1167 
1168  return $class;
1169  } elseif ( !is_array( $list ) ) {
1170  return $list;
1171  } else {
1172  if ( is_object( $list[0] ) ) {
1173  $class = get_class( $list[0] );
1174  } else {
1175  $class = $list[0];
1176  }
1177 
1178  return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1179  }
1180  }
1181 
1186  public static function getGitHeadSha1( $dir ) {
1187  $repo = new GitInfo( $dir );
1188 
1189  return $repo->getHeadSHA1();
1190  }
1191 
1196  public static function getGitCurrentBranch( $dir ) {
1197  $repo = new GitInfo( $dir );
1198  return $repo->getCurrentBranch();
1199  }
1200 
1205  public function getEntryPointInfo() {
1206  $config = $this->getConfig();
1207  $scriptPath = $config->get( 'ScriptPath' ) ?: '/';
1208 
1209  $entryPoints = [
1210  'version-entrypoints-articlepath' => $config->get( 'ArticlePath' ),
1211  'version-entrypoints-scriptpath' => $scriptPath,
1212  'version-entrypoints-index-php' => wfScript( 'index' ),
1213  'version-entrypoints-api-php' => wfScript( 'api' ),
1214  'version-entrypoints-rest-php' => wfScript( 'rest' ),
1215  ];
1216 
1217  $language = $this->getLanguage();
1218  $thAttribures = [
1219  'dir' => $language->getDir(),
1220  'lang' => $language->getHtmlCode()
1221  ];
1222  $out = Html::element(
1223  'h2',
1224  [ 'id' => 'mw-version-entrypoints' ],
1225  $this->msg( 'version-entrypoints' )->text()
1226  ) .
1227  Html::openElement( 'table',
1228  [
1229  'class' => 'wikitable plainlinks',
1230  'id' => 'mw-version-entrypoints-table',
1231  'dir' => 'ltr',
1232  'lang' => 'en'
1233  ]
1234  ) .
1235  Html::openElement( 'tr' ) .
1236  Html::element(
1237  'th',
1238  $thAttribures,
1239  $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1240  ) .
1241  Html::element(
1242  'th',
1243  $thAttribures,
1244  $this->msg( 'version-entrypoints-header-url' )->text()
1245  ) .
1246  Html::closeElement( 'tr' );
1247 
1248  foreach ( $entryPoints as $message => $value ) {
1249  $url = wfExpandUrl( $value, PROTO_RELATIVE );
1250  $out .= Html::openElement( 'tr' ) .
1251  // ->plain() looks like it should be ->parse(), but this function
1252  // returns wikitext, not HTML, boo
1253  Html::rawElement( 'td', [], $this->msg( $message )->plain() ) .
1254  Html::rawElement( 'td', [], Html::rawElement( 'code', [], "[$url $value]" ) ) .
1255  Html::closeElement( 'tr' );
1256  }
1257 
1258  $out .= Html::closeElement( 'table' );
1259 
1260  return $out;
1261  }
1262 
1263  protected function getGroupName() {
1264  return 'wiki';
1265  }
1266 }
SpecialVersion\openExtType
openExtType( $text=null, $name=null)
Definition: SpecialVersion.php:961
SpecialPage\getPageTitle
getPageTitle( $subpage=false)
Get a self-referential title object.
Definition: SpecialPage.php:697
SpecialVersion\__construct
__construct()
Definition: SpecialVersion.php:51
SpecialPage\msg
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
Definition: SpecialPage.php:828
SpecialVersion\listAuthors
listAuthors( $authors, $extName, $extDir)
Return a formatted unsorted list of authors.
Definition: SpecialVersion.php:1034
HtmlArmor
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:30
SpecialVersion\getExtAuthorsFileName
static getExtAuthorsFileName( $extDir)
Obtains the full path of an extensions AUTHORS or CREDITS file if one exists.
Definition: SpecialVersion.php:1104
SpecialPage\getOutput
getOutput()
Get the OutputPage being used for this instance.
Definition: SpecialPage.php:744
SpecialVersion\listToText
listToText( $list, $sort=true)
Convert an array of items into a list for display.
Definition: SpecialVersion.php:1138
SpecialVersion\getMediaWikiCredits
static getMediaWikiCredits()
Returns wiki text showing the license information.
Definition: SpecialVersion.php:198
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:154
$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:753
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:1072
SpecialVersion\compare
compare( $a, $b)
Callback to sort extensions by type.
Definition: SpecialVersion.php:731
SpecialVersion\getCopyrightAndAuthorList
static getCopyrightAndAuthorList()
Get the "MediaWiki is copyright 2001-20xx by lots of cool folks" text.
Definition: SpecialVersion.php:222
SpecialVersion\getExtensionCategory
getExtensionCategory( $type, $message, array $creditsGroup)
Creates and returns the HTML for a single extension category.
Definition: SpecialVersion.php:706
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:645
$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:1219
SpecialPage\getLanguage
getLanguage()
Shortcut to get user's language.
Definition: SpecialPage.php:774
SpecialVersion\getParserFunctionHooks
getParserFunctionHooks()
Obtains a list of installed parser function hooks and the associated H2 header.
Definition: SpecialVersion.php:673
SpecialVersion\getExtensionCredits
getExtensionCredits(array $credits)
Generate wikitext showing the name, URL, author and description of each extension.
Definition: SpecialVersion.php:449
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:344
SpecialVersion\getSkinCredits
getSkinCredits(array $credits)
Generate wikitext showing the name, URL, author and description of each skin.
Definition: SpecialVersion.php:502
$dbr
$dbr
Definition: testCompression.php:54
SpecialVersion\arrayToString
static arrayToString( $list)
Convert an array or object to a string for display.
Definition: SpecialVersion.php:1158
SpecialVersion\softwareInformation
static softwareInformation()
Returns HTML showing the third party software versions (apache, php, mysql).
Definition: SpecialVersion.php:286
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:30
SpecialVersion\getExternalLibraries
getExternalLibraries(array $credits)
Generate an HTML table for external libraries that are installed.
Definition: SpecialVersion.php:528
SpecialVersion\IPInfo
IPInfo()
Get information about client's IP address.
Definition: SpecialVersion.php:1007
$wgHooks
$wgHooks
Global list of hooks.
Definition: DefaultSettings.php:7928
SpecialPage\getConfig
getConfig()
Shortcut to get main config object.
Definition: SpecialPage.php:794
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1026
SpecialVersion\getSoftwareInformation
static getSoftwareInformation()
Definition: SpecialVersion.php:259
wfScript
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
Definition: GlobalFunctions.php:2534
SpecialVersion\getExtensionTypeName
static getExtensionTypeName( $type)
Returns the internationalized name for an extension type.
Definition: SpecialVersion.php:437
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:2467
$wgLang
$wgLang
Definition: Setup.php:776
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:358
SpecialPage\setHeaders
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!...
Definition: SpecialPage.php:571
SpecialVersion\getExtensionTypes
static getExtensionTypes()
Returns an array with the base extension types.
Definition: SpecialVersion.php:408
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:2694
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
SpecialVersion\getWgHooks
getWgHooks()
Generate wikitext showing hooks in $wgHooks.
Definition: SpecialVersion.php:931
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:909
SpecialVersion\getGroupName
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
Definition: SpecialVersion.php:1263
SpecialVersion\getExtLicenseFileName
static getExtLicenseFileName( $extDir)
Obtains the full path of an extensions COPYING or LICENSE file if one exists.
Definition: SpecialVersion.php:1121
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:315
GitInfo
@newable
Definition: GitInfo.php:34
SpecialVersion\getGitCurrentBranch
static getGitCurrentBranch( $dir)
Definition: SpecialVersion.php:1196
SpecialVersion\getCredits
static getCredits(ExtensionRegistry $reg, Config $conf)
Definition: SpecialVersion.php:62
SpecialPage
Parent class for all special pages.
Definition: SpecialPage.php:41
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:172
SpecialPage\getRequest
getRequest()
Get the WebRequest being used for this instance.
Definition: SpecialPage.php:734
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:1186
SpecialVersion\getEntryPointInfo
getEntryPointInfo()
Get the list of entry points and their URLs.
Definition: SpecialVersion.php:1205
SpecialPage\getLinkRenderer
getLinkRenderer()
Definition: SpecialPage.php:944
SpecialVersion\$firstExtOpened
bool $firstExtOpened
Definition: SpecialVersion.php:39
SpecialVersion\getVersionLinkedGit
static getVersionLinkedGit()
Definition: SpecialVersion.php:374
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:71
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:631
$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:205
$wgSpecialVersionShowHooks
$wgSpecialVersionShowHooks
Show the contents of $wgHooks in Special:Version.
Definition: DefaultSettings.php:6722
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:662
wfExpandUrl
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
Definition: GlobalFunctions.php:490
$type
$type
Definition: testCompression.php:52