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 
51  private $parser;
52 
56  public function __construct( Parser $parser ) {
57  parent::__construct( 'Version' );
58  $this->parser = $parser;
59  }
60 
68  public static function getCredits( ExtensionRegistry $reg, Config $conf ) : array {
69  $credits = $conf->get( 'ExtensionCredits' );
70  foreach ( $reg->getAllThings() as $name => $credit ) {
71  $credits[$credit['type']][] = $credit;
72  }
73  return $credits;
74  }
75 
80  public function execute( $par ) {
81  global $IP;
82  $config = $this->getConfig();
83  $credits = self::getCredits( ExtensionRegistry::getInstance(), $config );
84 
85  $this->setHeaders();
86  $this->outputHeader();
87  $out = $this->getOutput();
88  $out->allowClickjacking();
89 
90  // Explode the sub page information into useful bits
91  $parts = explode( '/', (string)$par );
92  $extNode = null;
93  if ( isset( $parts[1] ) ) {
94  $extName = str_replace( '_', ' ', $parts[1] );
95  // Find it!
96  foreach ( $credits as $group => $extensions ) {
97  foreach ( $extensions as $ext ) {
98  if ( isset( $ext['name'] ) && ( $ext['name'] === $extName ) ) {
99  $extNode = &$ext;
100  break 2;
101  }
102  }
103  }
104  if ( !$extNode ) {
105  $out->setStatusCode( 404 );
106  }
107  } else {
108  $extName = 'MediaWiki';
109  }
110 
111  // Now figure out what to do
112  switch ( strtolower( $parts[0] ) ) {
113  case 'credits':
114  $out->addModuleStyles( 'mediawiki.special.version' );
115 
116  $wikiText = '{{int:version-credits-not-found}}';
117  if ( $extName === 'MediaWiki' ) {
118  $wikiText = file_get_contents( $IP . '/CREDITS' );
119  // Put the contributor list into columns
120  $wikiText = str_replace(
121  [ '<!-- BEGIN CONTRIBUTOR LIST -->', '<!-- END CONTRIBUTOR LIST -->' ],
122  [ '<div class="mw-version-credits">', '</div>' ],
123  $wikiText );
124  } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
125  $file = ExtensionInfo::getAuthorsFileName( dirname( $extNode['path'] ) );
126  if ( $file ) {
127  $wikiText = file_get_contents( $file );
128  if ( substr( $file, -4 ) === '.txt' ) {
129  $wikiText = Html::element(
130  'pre',
131  [
132  'lang' => 'en',
133  'dir' => 'ltr',
134  ],
135  $wikiText
136  );
137  }
138  }
139  }
140 
141  $out->setPageTitle( $this->msg( 'version-credits-title', $extName ) );
142  $out->addWikiTextAsInterface( $wikiText );
143  break;
144 
145  case 'license':
146  $out->setPageTitle( $this->msg( 'version-license-title', $extName ) );
147 
148  $licenseFound = false;
149 
150  if ( $extName === 'MediaWiki' ) {
151  $out->addWikiTextAsInterface(
152  file_get_contents( $IP . '/COPYING' )
153  );
154  $licenseFound = true;
155  } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
156  $files = ExtensionInfo::getLicenseFileNames( dirname( $extNode['path'] ) );
157 
158  if ( count( $files ) ) {
159  $licenseFound = true;
160  foreach ( $files as $file ) {
161  $out->addWikiTextAsInterface(
163  'pre',
164  [
165  'lang' => 'en',
166  'dir' => 'ltr',
167  ],
168  file_get_contents( $file )
169  )
170  );
171  }
172  }
173  }
174  if ( !$licenseFound ) {
175  $out->addWikiTextAsInterface( '{{int:version-license-not-found}}' );
176  }
177  break;
178  default:
179  $out->addModuleStyles( 'mediawiki.special.version' );
180  $out->addWikiTextAsInterface(
181  self::getMediaWikiCredits() .
182  self::softwareInformation() .
183  $this->getEntryPointInfo()
184  );
185  $out->addHTML(
186  $this->getSkinCredits( $credits ) .
187  $this->getExtensionCredits( $credits ) .
188  $this->getExternalLibraries( $credits ) .
189  $this->getParserTags() .
190  $this->getParserFunctionHooks()
191  );
192  $out->addWikiTextAsInterface( $this->getWgHooks() );
193  $out->addHTML( $this->IPInfo() );
194 
195  break;
196  }
197  }
198 
204  private static function getMediaWikiCredits() {
205  $ret = Xml::element(
206  'h2',
207  [ 'id' => 'mw-version-license' ],
208  wfMessage( 'version-license' )->text()
209  );
210 
211  // This text is always left-to-right.
212  $ret .= '<div class="plainlinks">';
213  $ret .= "__NOTOC__
215  " . '<div class="mw-version-license-info">' .
216  wfMessage( 'version-license-info' )->text() .
217  '</div>';
218  $ret .= '</div>';
219 
220  return str_replace( "\t\t", '', $ret ) . "\n";
221  }
222 
228  public static function getCopyrightAndAuthorList() {
229  global $wgLang;
230 
231  if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
232  $othersLink = '[https://www.mediawiki.org/wiki/Special:Version/Credits ' .
233  wfMessage( 'version-poweredby-others' )->text() . ']';
234  } else {
235  $othersLink = '[[Special:Version/Credits|' .
236  wfMessage( 'version-poweredby-others' )->text() . ']]';
237  }
238 
239  $translatorsLink = '[https://translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
240  wfMessage( 'version-poweredby-translators' )->text() . ']';
241 
242  $authorList = [
243  'Magnus Manske', 'Brion Vibber', 'Lee Daniel Crocker',
244  'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
245  'Niklas Laxström', 'Domas Mituzas', 'Rob Church', 'Yuri Astrakhan',
246  'Aryeh Gregor', 'Aaron Schulz', 'Andrew Garrett', 'Raimond Spekking',
247  'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
248  'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
249  'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
250  'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', 'Brad Jorsch',
251  'Bartosz Dziewoński', 'Ed Sanders', 'Moriel Schottlender',
252  'Kunal Mehta', 'James D. Forrester', 'Brian Wolff', 'Adam Shorland',
253  'DannyS712',
254  $othersLink, $translatorsLink
255  ];
256 
257  return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
258  $wgLang->listToText( $authorList ) )->text();
259  }
260 
266  public static function getSoftwareInformation() {
267  $dbr = wfGetDB( DB_REPLICA );
268 
269  // Put the software in an array of form 'name' => 'version'. All messages should
270  // be loaded here, so feel free to use wfMessage in the 'name'. Raw HTML or
271  // wikimarkup can be used.
272  $software = [
273  '[https://www.mediawiki.org/ MediaWiki]' => self::getVersionLinked(),
274  '[https://php.net/ PHP]' => PHP_VERSION . " (" . PHP_SAPI . ")",
275  $dbr->getSoftwareLink() => $dbr->getServerInfo(),
276  ];
277 
278  if ( defined( 'INTL_ICU_VERSION' ) ) {
279  $software['[http://site.icu-project.org/ ICU]'] = INTL_ICU_VERSION;
280  }
281 
282  // Allow a hook to add/remove items.
283  Hooks::runner()->onSoftwareInfo( $software );
284 
285  return $software;
286  }
287 
293  public static function softwareInformation() {
294  $out = Xml::element(
295  'h2',
296  [ 'id' => 'mw-version-software' ],
297  wfMessage( 'version-software' )->text()
298  ) .
299  Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ] ) .
300  "<tr>
301  <th>" . wfMessage( 'version-software-product' )->text() . "</th>
302  <th>" . wfMessage( 'version-software-version' )->text() . "</th>
303  </tr>\n";
304 
305  foreach ( self::getSoftwareInformation() as $name => $version ) {
306  $out .= "<tr>
307  <td>" . $name . "</td>
308  <td dir=\"ltr\">" . $version . "</td>
309  </tr>\n";
310  }
311 
312  return $out . Xml::closeElement( 'table' );
313  }
314 
322  public static function getVersion( $flags = '', $lang = null ) {
323  global $IP;
324 
325  $gitInfo = self::getGitHeadSha1( $IP );
326  if ( !$gitInfo ) {
327  $version = MW_VERSION;
328  } elseif ( $flags === 'nodb' ) {
329  $shortSha1 = substr( $gitInfo, 0, 7 );
330  $version = MW_VERSION . " ($shortSha1)";
331  } else {
332  $shortSha1 = substr( $gitInfo, 0, 7 );
333  $msg = wfMessage( 'parentheses' );
334  if ( $lang !== null ) {
335  $msg->inLanguage( $lang );
336  }
337  $shortSha1 = $msg->params( $shortSha1 )->escaped();
338  $version = MW_VERSION . ' ' . $shortSha1;
339  }
340 
341  return $version;
342  }
343 
351  public static function getVersionLinked() {
352  $gitVersion = self::getVersionLinkedGit();
353  if ( $gitVersion ) {
354  $v = $gitVersion;
355  } else {
356  $v = MW_VERSION; // fallback
357  }
358 
359  return $v;
360  }
361 
365  private static function getMWVersionLinked() {
366  $versionUrl = "";
367  if ( Hooks::runner()->onSpecialVersionVersionUrl( MW_VERSION, $versionUrl ) ) {
368  $versionParts = [];
369  preg_match( "/^(\d+\.\d+)/", MW_VERSION, $versionParts );
370  $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
371  }
372 
373  return '[' . $versionUrl . ' ' . MW_VERSION . ']';
374  }
375 
381  private static function getVersionLinkedGit() {
382  global $IP, $wgLang;
383 
384  $gitInfo = new GitInfo( $IP );
385  $headSHA1 = $gitInfo->getHeadSHA1();
386  if ( !$headSHA1 ) {
387  return false;
388  }
389 
390  $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
391 
392  $gitHeadUrl = $gitInfo->getHeadViewUrl();
393  if ( $gitHeadUrl !== false ) {
394  $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
395  }
396 
397  $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
398  if ( $gitHeadCommitDate ) {
399  $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( $gitHeadCommitDate, true );
400  }
401 
402  return self::getMWVersionLinked() . " $shortSHA1";
403  }
404 
415  public static function getExtensionTypes() {
416  if ( self::$extensionTypes === false ) {
417  self::$extensionTypes = [
418  'specialpage' => wfMessage( 'version-specialpages' )->text(),
419  'editor' => wfMessage( 'version-editors' )->text(),
420  'parserhook' => wfMessage( 'version-parserhooks' )->text(),
421  'variable' => wfMessage( 'version-variables' )->text(),
422  'media' => wfMessage( 'version-mediahandlers' )->text(),
423  'antispam' => wfMessage( 'version-antispam' )->text(),
424  'skin' => wfMessage( 'version-skins' )->text(),
425  'api' => wfMessage( 'version-api' )->text(),
426  'other' => wfMessage( 'version-other' )->text(),
427  ];
428 
429  Hooks::runner()->onExtensionTypes( self::$extensionTypes );
430  }
431 
432  return self::$extensionTypes;
433  }
434 
444  public static function getExtensionTypeName( $type ) {
445  $types = self::getExtensionTypes();
446 
447  return $types[$type] ?? $types['other'];
448  }
449 
456  private function getExtensionCredits( array $credits ) {
457  if (
458  !$credits ||
459  // Skins are displayed separately, see getSkinCredits()
460  ( count( $credits ) === 1 && isset( $credits['skin'] ) )
461  ) {
462  return '';
463  }
464 
466 
467  $out = Xml::element(
468  'h2',
469  [ 'id' => 'mw-version-ext' ],
470  $this->msg( 'version-extensions' )->text()
471  ) .
472  Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-ext' ] );
473 
474  // Make sure the 'other' type is set to an array.
475  if ( !array_key_exists( 'other', $credits ) ) {
476  $credits['other'] = [];
477  }
478 
479  // Find all extensions that do not have a valid type and give them the type 'other'.
480  foreach ( $credits as $type => $extensions ) {
481  if ( !array_key_exists( $type, $extensionTypes ) ) {
482  $credits['other'] = array_merge( $credits['other'], $extensions );
483  }
484  }
485 
486  $this->firstExtOpened = false;
487  // Loop through the extension categories to display their extensions in the list.
488  foreach ( $extensionTypes as $type => $message ) {
489  // Skins have a separate section
490  if ( $type !== 'other' && $type !== 'skin' ) {
491  $out .= $this->getExtensionCategory( $type, $message, $credits[$type] ?? [] );
492  }
493  }
494 
495  // We want the 'other' type to be last in the list.
496  $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'], $credits['other'] );
497 
498  $out .= Xml::closeElement( 'table' );
499 
500  return $out;
501  }
502 
509  private function getSkinCredits( array $credits ) {
510  if ( !isset( $credits['skin'] ) || count( $credits['skin'] ) === 0 ) {
511  return '';
512  }
513 
514  $out = Xml::element(
515  'h2',
516  [ 'id' => 'mw-version-skin' ],
517  $this->msg( 'version-skins' )->text()
518  ) .
519  Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-skin' ] );
520 
521  $this->firstExtOpened = false;
522  $out .= $this->getExtensionCategory( 'skin', null, $credits['skin'] );
523 
524  $out .= Xml::closeElement( 'table' );
525 
526  return $out;
527  }
528 
535  protected function getExternalLibraries( array $credits ) {
536  global $IP;
537  $paths = [
538  "$IP/vendor/composer/installed.json"
539  ];
540 
542  foreach ( $extensionTypes as $type => $message ) {
543  if ( !isset( $credits[$type] ) || $credits[$type] === [] ) {
544  continue;
545  }
546  foreach ( $credits[$type] as $extension ) {
547  if ( !isset( $extension['path'] ) ) {
548  continue;
549  }
550  $paths[] = dirname( $extension['path'] ) . '/vendor/composer/installed.json';
551  }
552  }
553 
554  $dependencies = [];
555 
556  foreach ( $paths as $path ) {
557  if ( !file_exists( $path ) ) {
558  continue;
559  }
560 
561  $installed = new ComposerInstalled( $path );
562 
563  $dependencies += $installed->getInstalledDependencies();
564  }
565 
566  if ( $dependencies === [] ) {
567  return '';
568  }
569 
570  ksort( $dependencies );
571 
572  $out = Html::element(
573  'h2',
574  [ 'id' => 'mw-version-libraries' ],
575  $this->msg( 'version-libraries' )->text()
576  );
577  $out .= Html::openElement(
578  'table',
579  [ 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries' ]
580  );
581  $out .= Html::openElement( 'tr' )
582  . Html::element( 'th', [], $this->msg( 'version-libraries-library' )->text() )
583  . Html::element( 'th', [], $this->msg( 'version-libraries-version' )->text() )
584  . Html::element( 'th', [], $this->msg( 'version-libraries-license' )->text() )
585  . Html::element( 'th', [], $this->msg( 'version-libraries-description' )->text() )
586  . Html::element( 'th', [], $this->msg( 'version-libraries-authors' )->text() )
587  . Html::closeElement( 'tr' );
588 
589  foreach ( $dependencies as $name => $info ) {
590  if ( !is_array( $info ) || strpos( $info['type'], 'mediawiki-' ) === 0 ) {
591  // Skip any extensions or skins since they'll be listed
592  // in their proper section
593  continue;
594  }
595  $authors = array_map( function ( $arr ) {
596  // If a homepage is set, link to it
597  if ( isset( $arr['homepage'] ) ) {
598  return "[{$arr['homepage']} {$arr['name']}]";
599  }
600  return $arr['name'];
601  }, $info['authors'] );
602  $authors = $this->listAuthors( $authors, false, "$IP/vendor/$name" );
603 
604  // We can safely assume that the libraries' names and descriptions
605  // are written in English and aren't going to be translated,
606  // so set appropriate lang and dir attributes
607  $out .= Html::openElement( 'tr', [
608  // Add an anchor so docs can link easily to the version of
609  // this specific library
611  "mw-version-library-$name"
612  ) ] )
614  'td',
615  [],
617  "https://packagist.org/packages/$name", $name,
618  true, '',
619  [ 'class' => 'mw-version-library-name' ]
620  )
621  )
622  . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
623  . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['licenses'] ) )
624  . Html::element( 'td', [ 'lang' => 'en', 'dir' => 'ltr' ], $info['description'] )
625  . Html::rawElement( 'td', [], $authors )
626  . Html::closeElement( 'tr' );
627  }
628  $out .= Html::closeElement( 'table' );
629 
630  return $out;
631  }
632 
638  protected function getParserTags() {
639  $tags = $this->parser->getTags();
640 
641  if ( count( $tags ) ) {
642  $out = Html::rawElement(
643  'h2',
644  [
645  'class' => 'mw-headline plainlinks',
646  'id' => 'mw-version-parser-extensiontags',
647  ],
648  // @phan-suppress-next-line SecurityCheck-DoubleEscaped Using false for escape is safe
650  'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
651  $this->msg( 'version-parser-extensiontags' )->parse(),
652  false /* msg()->parse() already escapes */
653  )
654  );
655 
656  array_walk( $tags, function ( &$value ) {
657  // Bidirectional isolation improves readability in RTL wikis
658  $value = Html::element(
659  'bdi',
660  // Prevent < and > from slipping to another line
661  [
662  'style' => 'white-space: nowrap;',
663  ],
664  "<$value>"
665  );
666  } );
667 
668  $out .= $this->listToText( $tags );
669  } else {
670  $out = '';
671  }
672 
673  return $out;
674  }
675 
681  protected function getParserFunctionHooks() {
682  $fhooks = $this->parser->getFunctionHooks();
683  if ( count( $fhooks ) ) {
684  $out = Html::rawElement(
685  'h2',
686  [
687  'class' => 'mw-headline plainlinks',
688  'id' => 'mw-version-parser-function-hooks',
689  ],
690  // @phan-suppress-next-line SecurityCheck-DoubleEscaped Using false for escape is safe
692  'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
693  $this->msg( 'version-parser-function-hooks' )->parse(),
694  false /* msg()->parse() already escapes */
695  )
696  );
697 
698  $out .= $this->listToText( $fhooks );
699  } else {
700  $out = '';
701  }
702 
703  return $out;
704  }
705 
715  protected function getExtensionCategory( $type, $message, array $creditsGroup ) {
716  $config = $this->getConfig();
717  $credits = $config->get( 'ExtensionCredits' );
718 
719  $out = '';
720 
721  if ( $creditsGroup ) {
722  $out .= $this->openExtType( $message, 'credits-' . $type );
723 
724  usort( $creditsGroup, [ $this, 'compare' ] );
725 
726  foreach ( $creditsGroup as $extension ) {
727  $out .= $this->getCreditsForExtension( $type, $extension );
728  }
729  }
730 
731  return $out;
732  }
733 
740  public function compare( $a, $b ) {
741  return $this->getLanguage()->lc( $a['name'] ) <=> $this->getLanguage()->lc( $b['name'] );
742  }
743 
762  public function getCreditsForExtension( $type, array $extension ) {
763  $out = $this->getOutput();
764 
765  // We must obtain the information for all the bits and pieces!
766  // ... such as extension names and links
767  if ( isset( $extension['namemsg'] ) ) {
768  // Localized name of extension
769  $extensionName = $this->msg( $extension['namemsg'] )->text();
770  } elseif ( isset( $extension['name'] ) ) {
771  // Non localized version
772  $extensionName = $extension['name'];
773  } else {
774  $extensionName = $this->msg( 'version-no-ext-name' )->text();
775  }
776 
777  if ( isset( $extension['url'] ) ) {
778  $extensionNameLink = Linker::makeExternalLink(
779  $extension['url'],
780  $extensionName,
781  true,
782  '',
783  [ 'class' => 'mw-version-ext-name' ]
784  );
785  } else {
786  $extensionNameLink = htmlspecialchars( $extensionName );
787  }
788 
789  // ... and the version information
790  // If the extension path is set we will check that directory for GIT
791  // metadata in an attempt to extract date and vcs commit metadata.
792  $canonicalVersion = '&ndash;';
793  $extensionPath = null;
794  $vcsVersion = null;
795  $vcsLink = null;
796  $vcsDate = null;
797 
798  if ( isset( $extension['version'] ) ) {
799  $canonicalVersion = $out->parseInlineAsInterface( $extension['version'] );
800  }
801 
802  if ( isset( $extension['path'] ) ) {
803  global $IP;
804  $extensionPath = dirname( $extension['path'] );
805  if ( $this->coreId == '' ) {
806  wfDebug( 'Looking up core head id' );
807  $coreHeadSHA1 = self::getGitHeadSha1( $IP );
808  if ( $coreHeadSHA1 ) {
809  $this->coreId = $coreHeadSHA1;
810  }
811  }
813  $memcKey = $cache->makeKey(
814  'specialversion-ext-version-text', $extension['path'], $this->coreId
815  );
816  list( $vcsVersion, $vcsLink, $vcsDate ) = $cache->get( $memcKey );
817 
818  if ( !$vcsVersion ) {
819  wfDebug( "Getting VCS info for extension {$extension['name']}" );
820  $gitInfo = new GitInfo( $extensionPath );
821  $vcsVersion = $gitInfo->getHeadSHA1();
822  if ( $vcsVersion !== false ) {
823  $vcsVersion = substr( $vcsVersion, 0, 7 );
824  $vcsLink = $gitInfo->getHeadViewUrl();
825  $vcsDate = $gitInfo->getHeadCommitDate();
826  }
827  $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
828  } else {
829  wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" );
830  }
831  }
832 
833  $versionString = Html::rawElement(
834  'span',
835  [ 'class' => 'mw-version-ext-version' ],
836  $canonicalVersion
837  );
838 
839  if ( $vcsVersion ) {
840  if ( $vcsLink ) {
841  $vcsVerString = Linker::makeExternalLink(
842  $vcsLink,
843  $this->msg( 'version-version', $vcsVersion )->text(),
844  true,
845  '',
846  [ 'class' => 'mw-version-ext-vcs-version' ]
847  );
848  } else {
849  $vcsVerString = Html::element( 'span',
850  [ 'class' => 'mw-version-ext-vcs-version' ],
851  "({$vcsVersion})"
852  );
853  }
854  $versionString .= " {$vcsVerString}";
855 
856  if ( $vcsDate ) {
857  $versionString .= ' ' . Html::element( 'span', [
858  'class' => 'mw-version-ext-vcs-timestamp',
859  'dir' => $this->getLanguage()->getDir(),
860  ], $this->getLanguage()->timeanddate( $vcsDate, true ) );
861  }
862  $versionString = Html::rawElement( 'span',
863  [ 'class' => 'mw-version-ext-meta-version' ],
864  $versionString
865  );
866  }
867 
868  // ... and license information; if a license file exists we
869  // will link to it
870  $licenseLink = '';
871  if ( isset( $extension['name'] ) ) {
872  $licenseName = null;
873  if ( isset( $extension['license-name'] ) ) {
874  $licenseName = new HtmlArmor( $out->parseInlineAsInterface( $extension['license-name'] ) );
875  } elseif ( ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
876  $licenseName = $this->msg( 'version-ext-license' )->text();
877  }
878  if ( $licenseName !== null ) {
879  $licenseLink = $this->getLinkRenderer()->makeLink(
880  $this->getPageTitle( 'License/' . $extension['name'] ),
881  $licenseName,
882  [
883  'class' => 'mw-version-ext-license',
884  'dir' => 'auto',
885  ]
886  );
887  }
888  }
889 
890  // ... and generate the description; which can be a parameterized l10n message
891  // in the form [ <msgname>, <parameter>, <parameter>... ] or just a straight
892  // up string
893  if ( isset( $extension['descriptionmsg'] ) ) {
894  // Localized description of extension
895  $descriptionMsg = $extension['descriptionmsg'];
896 
897  if ( is_array( $descriptionMsg ) ) {
898  $descriptionMsgKey = $descriptionMsg[0]; // Get the message key
899  array_shift( $descriptionMsg ); // Shift out the message key to get the parameters only
900  array_map( "htmlspecialchars", $descriptionMsg ); // For sanity
901  $description = $this->msg( $descriptionMsgKey, $descriptionMsg )->text();
902  } else {
903  $description = $this->msg( $descriptionMsg )->text();
904  }
905  } elseif ( isset( $extension['description'] ) ) {
906  // Non localized version
907  $description = $extension['description'];
908  } else {
909  $description = '';
910  }
911  $description = $out->parseInlineAsInterface( $description );
912 
913  // ... now get the authors for this extension
914  $authors = $extension['author'] ?? [];
915  $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
916 
917  // Finally! Create the table
918  $html = Html::openElement( 'tr', [
919  'class' => 'mw-version-ext',
920  'id' => Sanitizer::escapeIdForAttribute( 'mw-version-ext-' . $type . '-' . $extension['name'] )
921  ]
922  );
923 
924  $html .= Html::rawElement( 'td', [], $extensionNameLink );
925  $html .= Html::rawElement( 'td', [], $versionString );
926  $html .= Html::rawElement( 'td', [], $licenseLink );
927  $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-description' ], $description );
928  $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-authors' ], $authors );
929 
930  $html .= Html::closeElement( 'tr' );
931 
932  return $html;
933  }
934 
940  private function getWgHooks() {
942 
943  if ( $wgSpecialVersionShowHooks && count( $wgHooks ) ) {
944  $myWgHooks = $wgHooks;
945  ksort( $myWgHooks );
946 
947  $ret = [];
948  $ret[] = '== {{int:version-hooks}} ==';
949  $ret[] = Html::openElement( 'table', [ 'class' => 'wikitable', 'id' => 'sv-hooks' ] );
950  $ret[] = Html::openElement( 'tr' );
951  $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-name' )->text() );
952  $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-subscribedby' )->text() );
953  $ret[] = Html::closeElement( 'tr' );
954 
955  foreach ( $myWgHooks as $hook => $hooks ) {
956  $ret[] = Html::openElement( 'tr' );
957  $ret[] = Html::element( 'td', [], $hook );
958  $ret[] = Html::element( 'td', [], $this->listToText( $hooks ) );
959  $ret[] = Html::closeElement( 'tr' );
960  }
961 
962  $ret[] = Html::closeElement( 'table' );
963 
964  return implode( "\n", $ret );
965  }
966 
967  return '';
968  }
969 
970  private function openExtType( $text = null, $name = null ) {
971  $out = '';
972 
973  $opt = [ 'colspan' => 5 ];
974  if ( $this->firstExtOpened ) {
975  // Insert a spacing line
976  $out .= Html::rawElement( 'tr', [ 'class' => 'sv-space' ],
977  Html::element( 'td', $opt )
978  );
979  }
980  $this->firstExtOpened = true;
981 
982  if ( $name ) {
983  $opt['id'] = "sv-$name";
984  }
985 
986  if ( $text !== null ) {
987  $out .= Html::rawElement( 'tr', [],
988  Html::element( 'th', $opt, $text )
989  );
990  }
991 
992  $firstHeadingMsg = ( $name === 'credits-skin' )
993  ? 'version-skin-colheader-name'
994  : 'version-ext-colheader-name';
995  $out .= Html::openElement( 'tr' );
996  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
997  $this->msg( $firstHeadingMsg )->text() );
998  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
999  $this->msg( 'version-ext-colheader-version' )->text() );
1000  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1001  $this->msg( 'version-ext-colheader-license' )->text() );
1002  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1003  $this->msg( 'version-ext-colheader-description' )->text() );
1004  $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1005  $this->msg( 'version-ext-colheader-credits' )->text() );
1006  $out .= Html::closeElement( 'tr' );
1007 
1008  return $out;
1009  }
1010 
1016  private function IPInfo() {
1017  $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
1018 
1019  return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
1020  }
1021 
1043  public function listAuthors( $authors, $extName, $extDir ) {
1044  $hasOthers = false;
1045  $linkRenderer = $this->getLinkRenderer();
1046 
1047  $list = [];
1048  $authors = (array)$authors;
1049 
1050  // Special case: if the authors array has only one item and it is "...",
1051  // it should not be rendered as the "version-poweredby-others" i18n msg,
1052  // but rather as "version-poweredby-various" i18n msg instead.
1053  if ( count( $authors ) === 1 && $authors[0] === '...' ) {
1054  // Link to the extension's or skin's AUTHORS or CREDITS file, if there is
1055  // such a file; otherwise just return the i18n msg as-is
1056  if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1057  return $linkRenderer->makeLink(
1058  $this->getPageTitle( "Credits/$extName" ),
1059  $this->msg( 'version-poweredby-various' )->text()
1060  );
1061  } else {
1062  return $this->msg( 'version-poweredby-various' )->escaped();
1063  }
1064  }
1065 
1066  // Otherwise, if we have an actual array that has more than one item,
1067  // process each array item as usual
1068  foreach ( $authors as $item ) {
1069  if ( $item == '...' ) {
1070  $hasOthers = true;
1071 
1072  if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1073  $text = $linkRenderer->makeLink(
1074  $this->getPageTitle( "Credits/$extName" ),
1075  $this->msg( 'version-poweredby-others' )->text()
1076  );
1077  } else {
1078  $text = $this->msg( 'version-poweredby-others' )->escaped();
1079  }
1080  $list[] = $text;
1081  } elseif ( substr( $item, -5 ) == ' ...]' ) {
1082  $hasOthers = true;
1083  $list[] = $this->getOutput()->parseInlineAsInterface(
1084  substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
1085  );
1086  } else {
1087  $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1088  }
1089  }
1090 
1091  if ( $extName && !$hasOthers && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1092  $list[] = $text = $linkRenderer->makeLink(
1093  $this->getPageTitle( "Credits/$extName" ),
1094  $this->msg( 'version-poweredby-others' )->text()
1095  );
1096  }
1097 
1098  return $this->listToText( $list, false );
1099  }
1100 
1113  public static function getExtAuthorsFileName( $extDir ) {
1114  wfDeprecated( __METHOD__, '1.35' );
1115  return ExtensionInfo::getAuthorsFileName( $extDir );
1116  }
1117 
1130  public static function getExtLicenseFileName( $extDir ) {
1131  wfDeprecated( __METHOD__, '1.35' );
1132  $licenses = ExtensionInfo::getLicenseFileNames( $extDir );
1133  if ( count( $licenses ) === 0 ) {
1134  return false;
1135  }
1136  return $licenses[0];
1137  }
1138 
1147  public function listToText( $list, $sort = true ) {
1148  if ( !count( $list ) ) {
1149  return '';
1150  }
1151  if ( $sort ) {
1152  sort( $list );
1153  }
1154 
1155  return $this->getLanguage()
1156  ->listToText( array_map( [ __CLASS__, 'arrayToString' ], $list ) );
1157  }
1158 
1167  public static function arrayToString( $list ) {
1168  if ( is_array( $list ) && count( $list ) == 1 ) {
1169  $list = $list[0];
1170  }
1171  if ( $list instanceof Closure ) {
1172  // Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$"
1173  return 'Closure';
1174  } elseif ( is_object( $list ) ) {
1175  $class = wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1176 
1177  return $class;
1178  } elseif ( !is_array( $list ) ) {
1179  return $list;
1180  } else {
1181  if ( is_object( $list[0] ) ) {
1182  $class = get_class( $list[0] );
1183  } else {
1184  $class = $list[0];
1185  }
1186 
1187  return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1188  }
1189  }
1190 
1195  public static function getGitHeadSha1( $dir ) {
1196  $repo = new GitInfo( $dir );
1197 
1198  return $repo->getHeadSHA1();
1199  }
1200 
1205  public static function getGitCurrentBranch( $dir ) {
1206  $repo = new GitInfo( $dir );
1207  return $repo->getCurrentBranch();
1208  }
1209 
1214  public function getEntryPointInfo() {
1215  $config = $this->getConfig();
1216  $scriptPath = $config->get( 'ScriptPath' ) ?: '/';
1217 
1218  $entryPoints = [
1219  'version-entrypoints-articlepath' => $config->get( 'ArticlePath' ),
1220  'version-entrypoints-scriptpath' => $scriptPath,
1221  'version-entrypoints-index-php' => wfScript( 'index' ),
1222  'version-entrypoints-api-php' => wfScript( 'api' ),
1223  'version-entrypoints-rest-php' => wfScript( 'rest' ),
1224  ];
1225 
1226  $language = $this->getLanguage();
1227  $thAttribures = [
1228  'dir' => $language->getDir(),
1229  'lang' => $language->getHtmlCode()
1230  ];
1231  $out = Html::element(
1232  'h2',
1233  [ 'id' => 'mw-version-entrypoints' ],
1234  $this->msg( 'version-entrypoints' )->text()
1235  ) .
1236  Html::openElement( 'table',
1237  [
1238  'class' => 'wikitable plainlinks',
1239  'id' => 'mw-version-entrypoints-table',
1240  'dir' => 'ltr',
1241  'lang' => 'en'
1242  ]
1243  ) .
1244  Html::openElement( 'tr' ) .
1245  Html::element(
1246  'th',
1247  $thAttribures,
1248  $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1249  ) .
1250  Html::element(
1251  'th',
1252  $thAttribures,
1253  $this->msg( 'version-entrypoints-header-url' )->text()
1254  ) .
1255  Html::closeElement( 'tr' );
1256 
1257  foreach ( $entryPoints as $message => $value ) {
1258  $url = wfExpandUrl( $value, PROTO_RELATIVE );
1259  $out .= Html::openElement( 'tr' ) .
1260  // ->plain() looks like it should be ->parse(), but this function
1261  // returns wikitext, not HTML, boo
1262  Html::rawElement( 'td', [], $this->msg( $message )->plain() ) .
1263  Html::rawElement( 'td', [], Html::rawElement( 'code', [], "[$url $value]" ) ) .
1264  Html::closeElement( 'tr' );
1265  }
1266 
1267  $out .= Html::closeElement( 'table' );
1268 
1269  return $out;
1270  }
1271 
1272  protected function getGroupName() {
1273  return 'wiki';
1274  }
1275 }
SpecialPage\$linkRenderer
LinkRenderer null $linkRenderer
Definition: SpecialPage.php:79
SpecialVersion\openExtType
openExtType( $text=null, $name=null)
Definition: SpecialVersion.php:970
SpecialPage\getPageTitle
getPageTitle( $subpage=false)
Get a self-referential title object.
Definition: SpecialPage.php:742
SpecialPage\msg
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
Definition: SpecialPage.php:900
SpecialVersion\listAuthors
listAuthors( $authors, $extName, $extDir)
Return a formatted unsorted list of authors.
Definition: SpecialVersion.php:1043
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:1113
CACHE_ANYTHING
const CACHE_ANYTHING
Definition: Defines.php:84
SpecialPage\getOutput
getOutput()
Get the OutputPage being used for this instance.
Definition: SpecialPage.php:788
SpecialVersion\listToText
listToText( $list, $sort=true)
Convert an array of items into a list for display.
Definition: SpecialVersion.php:1147
SpecialVersion\getMediaWikiCredits
static getMediaWikiCredits()
Returns wiki text showing the license information.
Definition: SpecialVersion.php:204
$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:762
SpecialVersion\$coreId
string $coreId
The current rev id/SHA hash of MediaWiki core.
Definition: SpecialVersion.php:43
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:815
SpecialVersion\compare
compare( $a, $b)
Callback to sort extensions by type.
Definition: SpecialVersion.php:740
SpecialVersion\getCopyrightAndAuthorList
static getCopyrightAndAuthorList()
Get the "MediaWiki is copyright 2001-20xx by lots of cool folks" text.
Definition: SpecialVersion.php:228
SpecialVersion\getExtensionCategory
getExtensionCategory( $type, $message, array $creditsGroup)
Creates and returns the HTML for a single extension category.
Definition: SpecialVersion.php:715
ExtensionRegistry
ExtensionRegistry class.
Definition: ExtensionRegistry.php:17
MW_VERSION
const MW_VERSION
The running version of MediaWiki.
Definition: Defines.php:35
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:1230
SpecialPage\getLanguage
getLanguage()
Shortcut to get user's language.
Definition: SpecialPage.php:818
SpecialVersion\getParserFunctionHooks
getParserFunctionHooks()
Obtains a list of installed parser function hooks and the associated H2 header.
Definition: SpecialVersion.php:681
SpecialVersion\getExtensionCredits
getExtensionCredits(array $credits)
Generate wikitext showing the name, URL, author and description of each extension.
Definition: SpecialVersion.php:456
Xml\openElement
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:110
$wgLang
$wgLang
Definition: Setup.php:776
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:351
SpecialVersion\getSkinCredits
getSkinCredits(array $credits)
Generate wikitext showing the name, URL, author and description of each skin.
Definition: SpecialVersion.php:509
$dbr
$dbr
Definition: testCompression.php:54
SpecialVersion\arrayToString
static arrayToString( $list)
Convert an array or object to a string for display.
Definition: SpecialVersion.php:1167
SpecialVersion\softwareInformation
static softwareInformation()
Returns HTML showing the third party software versions (apache, php, mysql).
Definition: SpecialVersion.php:293
SpecialVersion\__construct
__construct(Parser $parser)
Definition: SpecialVersion.php:56
Html\closeElement
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:318
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:535
SpecialVersion\IPInfo
IPInfo()
Get information about client's IP address.
Definition: SpecialVersion.php:1016
PROTO_RELATIVE
const PROTO_RELATIVE
Definition: Defines.php:204
$wgHooks
$wgHooks
Global list of hooks.
Definition: DefaultSettings.php:7937
SpecialPage\getConfig
getConfig()
Shortcut to get main config object.
Definition: SpecialPage.php:866
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1033
SpecialVersion\getSoftwareInformation
static getSoftwareInformation()
Definition: SpecialVersion.php:266
wfScript
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
Definition: GlobalFunctions.php:2533
SpecialVersion\getExtensionTypeName
static getExtensionTypeName( $type)
Returns the internationalized name for an extension type.
Definition: SpecialVersion.php:444
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:2466
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
ObjectCache\getInstance
static getInstance( $id)
Get a cached instance of the specified type of cache object.
Definition: ObjectCache.php:78
SpecialVersion\getMWVersionLinked
static getMWVersionLinked()
Definition: SpecialVersion.php:365
SpecialPage\setHeaders
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!...
Definition: SpecialPage.php:616
SpecialVersion\getExtensionTypes
static getExtensionTypes()
Returns an array with the base extension types.
Definition: SpecialVersion.php:415
Linker\makeExternalLink
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition: Linker.php:845
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
SpecialVersion\getWgHooks
getWgHooks()
Generate wikitext showing hooks in $wgHooks.
Definition: SpecialVersion.php:940
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:914
SpecialVersion\getGroupName
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
Definition: SpecialVersion.php:1272
SpecialVersion\getExtLicenseFileName
static getExtLicenseFileName( $extDir)
Obtains the full path of an extensions COPYING or LICENSE file if one exists.
Definition: SpecialVersion.php:1130
SpecialVersion\execute
execute( $par)
main()
Definition: SpecialVersion.php:80
SpecialVersion\getVersion
static getVersion( $flags='', $lang=null)
Return a string of the MediaWiki version with Git revision if available.
Definition: SpecialVersion.php:322
GitInfo
@newable
Definition: GitInfo.php:34
SpecialVersion\getGitCurrentBranch
static getGitCurrentBranch( $dir)
Definition: SpecialVersion.php:1205
SpecialVersion\getCredits
static getCredits(ExtensionRegistry $reg, Config $conf)
Definition: SpecialVersion.php:68
SpecialPage
Parent class for all special pages.
Definition: SpecialPage.php:42
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:778
SpecialVersion
Give information about the version of MediaWiki, PHP, the DB and extensions.
Definition: SpecialVersion.php:33
SpecialVersion\getGitHeadSha1
static getGitHeadSha1( $dir)
Definition: SpecialVersion.php:1195
Parser
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition: Parser.php:84
SpecialVersion\getEntryPointInfo
getEntryPointInfo()
Get the list of entry points and their URLs.
Definition: SpecialVersion.php:1214
SpecialPage\getLinkRenderer
getLinkRenderer()
Definition: SpecialPage.php:1016
SpecialVersion\$firstExtOpened
bool $firstExtOpened
Definition: SpecialVersion.php:38
SpecialVersion\getVersionLinkedGit
static getVersionLinkedGit()
Definition: SpecialVersion.php:381
Xml\closeElement
static closeElement( $element)
Shortcut to close an XML element.
Definition: Xml.php:119
$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:254
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:212
SpecialVersion\$parser
Parser $parser
Definition: SpecialVersion.php:51
$ext
if(!is_readable( $file)) $ext
Definition: router.php:48
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:234
SpecialVersion\getParserTags
getParserTags()
Obtains a list of installed parser tags and the associated H2 header.
Definition: SpecialVersion.php:638
$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:6738
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:707
wfExpandUrl
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
Definition: GlobalFunctions.php:494
$type
$type
Definition: testCompression.php:52