Don't try to auto-create users when MW_NO_SESSION is defined
[mediawiki.git] / includes / specials / SpecialVersion.php
blob17442be5f3794f6b9fd3d01966d7c4adc97edd70
1 <?php
2 /**
3 * Implements Special:Version
5 * Copyright © 2005 Ævar Arnfjörð Bjarmason
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 * http://www.gnu.org/copyleft/gpl.html
22 * @file
23 * @ingroup SpecialPage
26 /**
27 * Give information about the version of MediaWiki, PHP, the DB and extensions
29 * @ingroup SpecialPage
31 class SpecialVersion extends SpecialPage {
32 protected $firstExtOpened = false;
34 /**
35 * Stores the current rev id/SHA hash of MediaWiki core
37 protected $coreId = '';
39 protected static $extensionTypes = false;
41 public function __construct() {
42 parent::__construct( 'Version' );
45 /**
46 * main()
47 * @param string|null $par
49 public function execute( $par ) {
50 global $IP, $wgExtensionCredits;
52 $this->setHeaders();
53 $this->outputHeader();
54 $out = $this->getOutput();
55 $out->allowClickjacking();
57 // Explode the sub page information into useful bits
58 $parts = explode( '/', (string)$par );
59 $extNode = null;
60 if ( isset( $parts[1] ) ) {
61 $extName = str_replace( '_', ' ', $parts[1] );
62 // Find it!
63 foreach ( $wgExtensionCredits as $group => $extensions ) {
64 foreach ( $extensions as $ext ) {
65 if ( isset( $ext['name'] ) && ( $ext['name'] === $extName ) ) {
66 $extNode = &$ext;
67 break 2;
71 if ( !$extNode ) {
72 $out->setStatusCode( 404 );
74 } else {
75 $extName = 'MediaWiki';
78 // Now figure out what to do
79 switch ( strtolower( $parts[0] ) ) {
80 case 'credits':
81 $wikiText = '{{int:version-credits-not-found}}';
82 if ( $extName === 'MediaWiki' ) {
83 $wikiText = file_get_contents( $IP . '/CREDITS' );
84 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
85 $file = $this->getExtAuthorsFileName( dirname( $extNode['path'] ) );
86 if ( $file ) {
87 $wikiText = file_get_contents( $file );
88 if ( substr( $file, -4 ) === '.txt' ) {
89 $wikiText = Html::element(
90 'pre',
91 array(
92 'lang' => 'en',
93 'dir' => 'ltr',
95 $wikiText
101 $out->setPageTitle( $this->msg( 'version-credits-title', $extName ) );
102 $out->addWikiText( $wikiText );
103 break;
105 case 'license':
106 $wikiText = '{{int:version-license-not-found}}';
107 if ( $extName === 'MediaWiki' ) {
108 $wikiText = file_get_contents( $IP . '/COPYING' );
109 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
110 $file = $this->getExtLicenseFileName( dirname( $extNode['path'] ) );
111 if ( $file ) {
112 $wikiText = file_get_contents( $file );
113 $wikiText = Html::element(
114 'pre',
115 array(
116 'lang' => 'en',
117 'dir' => 'ltr',
119 $wikiText
124 $out->setPageTitle( $this->msg( 'version-license-title', $extName ) );
125 $out->addWikiText( $wikiText );
126 break;
128 default:
129 $out->addModuleStyles( 'mediawiki.special.version' );
130 $out->addWikiText(
131 $this->getMediaWikiCredits() .
132 $this->softwareInformation() .
133 $this->getEntryPointInfo()
135 $out->addHtml(
136 $this->getSkinCredits() .
137 $this->getExtensionCredits() .
138 $this->getExternalLibraries() .
139 $this->getParserTags() .
140 $this->getParserFunctionHooks()
142 $out->addWikiText( $this->getWgHooks() );
143 $out->addHTML( $this->IPInfo() );
145 break;
150 * Returns wiki text showing the license information.
152 * @return string
154 private static function getMediaWikiCredits() {
155 $ret = Xml::element(
156 'h2',
157 array( 'id' => 'mw-version-license' ),
158 wfMessage( 'version-license' )->text()
161 // This text is always left-to-right.
162 $ret .= '<div class="plainlinks">';
163 $ret .= "__NOTOC__
164 " . self::getCopyrightAndAuthorList() . "\n
165 " . wfMessage( 'version-license-info' )->text();
166 $ret .= '</div>';
168 return str_replace( "\t\t", '', $ret ) . "\n";
172 * Get the "MediaWiki is copyright 2001-20xx by lots of cool guys" text
174 * @return string
176 public static function getCopyrightAndAuthorList() {
177 global $wgLang;
179 if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
180 $othersLink = '[//www.mediawiki.org/wiki/Special:Version/Credits ' .
181 wfMessage( 'version-poweredby-others' )->text() . ']';
182 } else {
183 $othersLink = '[[Special:Version/Credits|' .
184 wfMessage( 'version-poweredby-others' )->text() . ']]';
187 $translatorsLink = '[//translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
188 wfMessage( 'version-poweredby-translators' )->text() . ']';
190 $authorList = array(
191 'Magnus Manske', 'Brion Vibber', 'Lee Daniel Crocker',
192 'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
193 'Niklas Laxström', 'Domas Mituzas', 'Rob Church', 'Yuri Astrakhan',
194 'Aryeh Gregor', 'Aaron Schulz', 'Andrew Garrett', 'Raimond Spekking',
195 'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
196 'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
197 'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
198 'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', 'Brad Jorsch',
199 $othersLink, $translatorsLink
202 return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
203 $wgLang->listToText( $authorList ) )->text();
207 * Returns wiki text showing the third party software versions (apache, php, mysql).
209 * @return string
211 public static function softwareInformation() {
212 $dbr = wfGetDB( DB_SLAVE );
214 // Put the software in an array of form 'name' => 'version'. All messages should
215 // be loaded here, so feel free to use wfMessage in the 'name'. Raw HTML or
216 // wikimarkup can be used.
217 $software = array();
218 $software['[https://www.mediawiki.org/ MediaWiki]'] = self::getVersionLinked();
219 if ( wfIsHHVM() ) {
220 $software['[http://hhvm.com/ HHVM]'] = HHVM_VERSION . " (" . PHP_SAPI . ")";
221 } else {
222 $software['[https://php.net/ PHP]'] = PHP_VERSION . " (" . PHP_SAPI . ")";
224 $software[$dbr->getSoftwareLink()] = $dbr->getServerInfo();
226 if ( IcuCollation::getICUVersion() ) {
227 $software['[http://site.icu-project.org/ ICU]'] = IcuCollation::getICUVersion();
230 // Allow a hook to add/remove items.
231 Hooks::run( 'SoftwareInfo', array( &$software ) );
233 $out = Xml::element(
234 'h2',
235 array( 'id' => 'mw-version-software' ),
236 wfMessage( 'version-software' )->text()
238 Xml::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ) ) .
239 "<tr>
240 <th>" . wfMessage( 'version-software-product' )->text() . "</th>
241 <th>" . wfMessage( 'version-software-version' )->text() . "</th>
242 </tr>\n";
244 foreach ( $software as $name => $version ) {
245 $out .= "<tr>
246 <td>" . $name . "</td>
247 <td dir=\"ltr\">" . $version . "</td>
248 </tr>\n";
251 return $out . Xml::closeElement( 'table' );
255 * Return a string of the MediaWiki version with Git revision if available.
257 * @param string $flags
258 * @return mixed
260 public static function getVersion( $flags = '' ) {
261 global $wgVersion, $IP;
263 $gitInfo = self::getGitHeadSha1( $IP );
264 if ( !$gitInfo ) {
265 $version = $wgVersion;
266 } elseif ( $flags === 'nodb' ) {
267 $shortSha1 = substr( $gitInfo, 0, 7 );
268 $version = "$wgVersion ($shortSha1)";
269 } else {
270 $shortSha1 = substr( $gitInfo, 0, 7 );
271 $shortSha1 = wfMessage( 'parentheses' )->params( $shortSha1 )->escaped();
272 $version = "$wgVersion $shortSha1";
275 return $version;
279 * Return a wikitext-formatted string of the MediaWiki version with a link to
280 * the Git SHA1 of head if available.
281 * The fallback is just $wgVersion
283 * @return mixed
285 public static function getVersionLinked() {
286 global $wgVersion;
288 $gitVersion = self::getVersionLinkedGit();
289 if ( $gitVersion ) {
290 $v = $gitVersion;
291 } else {
292 $v = $wgVersion; // fallback
295 return $v;
299 * @return string
301 private static function getwgVersionLinked() {
302 global $wgVersion;
303 $versionUrl = "";
304 if ( Hooks::run( 'SpecialVersionVersionUrl', array( $wgVersion, &$versionUrl ) ) ) {
305 $versionParts = array();
306 preg_match( "/^(\d+\.\d+)/", $wgVersion, $versionParts );
307 $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
310 return "[$versionUrl $wgVersion]";
314 * @since 1.22 Returns the HEAD date in addition to the sha1 and link
315 * @return bool|string Global wgVersion + HEAD sha1 stripped to the first 7 chars
316 * with link and date, or false on failure
318 private static function getVersionLinkedGit() {
319 global $IP, $wgLang;
321 $gitInfo = new GitInfo( $IP );
322 $headSHA1 = $gitInfo->getHeadSHA1();
323 if ( !$headSHA1 ) {
324 return false;
327 $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
329 $gitHeadUrl = $gitInfo->getHeadViewUrl();
330 if ( $gitHeadUrl !== false ) {
331 $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
334 $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
335 if ( $gitHeadCommitDate ) {
336 $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( $gitHeadCommitDate, true );
339 return self::getwgVersionLinked() . " $shortSHA1";
343 * Returns an array with the base extension types.
344 * Type is stored as array key, the message as array value.
346 * TODO: ideally this would return all extension types.
348 * @since 1.17
350 * @return array
352 public static function getExtensionTypes() {
353 if ( self::$extensionTypes === false ) {
354 self::$extensionTypes = array(
355 'specialpage' => wfMessage( 'version-specialpages' )->text(),
356 'parserhook' => wfMessage( 'version-parserhooks' )->text(),
357 'variable' => wfMessage( 'version-variables' )->text(),
358 'media' => wfMessage( 'version-mediahandlers' )->text(),
359 'antispam' => wfMessage( 'version-antispam' )->text(),
360 'skin' => wfMessage( 'version-skins' )->text(),
361 'api' => wfMessage( 'version-api' )->text(),
362 'other' => wfMessage( 'version-other' )->text(),
365 Hooks::run( 'ExtensionTypes', array( &self::$extensionTypes ) );
368 return self::$extensionTypes;
372 * Returns the internationalized name for an extension type.
374 * @since 1.17
376 * @param string $type
378 * @return string
380 public static function getExtensionTypeName( $type ) {
381 $types = self::getExtensionTypes();
383 return isset( $types[$type] ) ? $types[$type] : $types['other'];
387 * Generate wikitext showing the name, URL, author and description of each extension.
389 * @return string Wikitext
391 public function getExtensionCredits() {
392 global $wgExtensionCredits;
394 if (
395 count( $wgExtensionCredits ) === 0 ||
396 // Skins are displayed separately, see getSkinCredits()
397 ( count( $wgExtensionCredits ) === 1 && isset( $wgExtensionCredits['skin'] ) )
399 return '';
402 $extensionTypes = self::getExtensionTypes();
404 $out = Xml::element(
405 'h2',
406 array( 'id' => 'mw-version-ext' ),
407 $this->msg( 'version-extensions' )->text()
409 Xml::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'sv-ext' ) );
411 // Make sure the 'other' type is set to an array.
412 if ( !array_key_exists( 'other', $wgExtensionCredits ) ) {
413 $wgExtensionCredits['other'] = array();
416 // Find all extensions that do not have a valid type and give them the type 'other'.
417 foreach ( $wgExtensionCredits as $type => $extensions ) {
418 if ( !array_key_exists( $type, $extensionTypes ) ) {
419 $wgExtensionCredits['other'] = array_merge( $wgExtensionCredits['other'], $extensions );
423 $this->firstExtOpened = false;
424 // Loop through the extension categories to display their extensions in the list.
425 foreach ( $extensionTypes as $type => $message ) {
426 // Skins have a separate section
427 if ( $type !== 'other' && $type !== 'skin' ) {
428 $out .= $this->getExtensionCategory( $type, $message );
432 // We want the 'other' type to be last in the list.
433 $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'] );
435 $out .= Xml::closeElement( 'table' );
437 return $out;
441 * Generate wikitext showing the name, URL, author and description of each skin.
443 * @return string Wikitext
445 public function getSkinCredits() {
446 global $wgExtensionCredits;
447 if ( !isset( $wgExtensionCredits['skin'] ) || count( $wgExtensionCredits['skin'] ) === 0 ) {
448 return '';
451 $out = Xml::element(
452 'h2',
453 array( 'id' => 'mw-version-skin' ),
454 $this->msg( 'version-skins' )->text()
456 Xml::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'sv-skin' ) );
458 $this->firstExtOpened = false;
459 $out .= $this->getExtensionCategory( 'skin', null );
461 $out .= Xml::closeElement( 'table' );
463 return $out;
467 * Generate an HTML table for external libraries that are installed
469 * @return string
471 protected function getExternalLibraries() {
472 global $IP;
473 $path = "$IP/vendor/composer/installed.json";
474 if ( !file_exists( $path ) ) {
475 return '';
478 $installed = new ComposerInstalled( $path );
479 $out = Html::element(
480 'h2',
481 array( 'id' => 'mw-version-libraries' ),
482 $this->msg( 'version-libraries' )->text()
484 $out .= Html::openElement(
485 'table',
486 array( 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries' )
488 $out .= Html::openElement( 'tr' )
489 . Html::element( 'th', array(), $this->msg( 'version-libraries-library' )->text() )
490 . Html::element( 'th', array(), $this->msg( 'version-libraries-version' )->text() )
491 . Html::element( 'th', array(), $this->msg( 'version-libraries-license' )->text() )
492 . Html::element( 'th', array(), $this->msg( 'version-libraries-description' )->text() )
493 . Html::element( 'th', array(), $this->msg( 'version-libraries-authors' )->text() )
494 . Html::closeElement( 'tr' );
496 foreach ( $installed->getInstalledDependencies() as $name => $info ) {
497 if ( strpos( $info['type'], 'mediawiki-' ) === 0 ) {
498 // Skip any extensions or skins since they'll be listed
499 // in their proper section
500 continue;
502 $authors = array_map( function( $arr ) {
503 // If a homepage is set, link to it
504 if ( isset( $arr['homepage'] ) ) {
505 return "[{$arr['homepage']} {$arr['name']}]";
507 return $arr['name'];
508 }, $info['authors'] );
509 $authors = $this->listAuthors( $authors, false, "$IP/vendor/$name" );
511 // We can safely assume that the libraries' names and descriptions
512 // are written in English and aren't going to be translated,
513 // so set appropriate lang and dir attributes
514 $out .= Html::openElement( 'tr' )
515 . Html::rawElement(
516 'td',
517 array(),
518 Linker::makeExternalLink(
519 "https://packagist.org/packages/$name", $name,
520 true, '',
521 array( 'class' => 'mw-version-library-name' )
524 . Html::element( 'td', array( 'dir' => 'auto' ), $info['version'] )
525 . Html::element( 'td', array( 'dir' => 'auto' ), $this->listToText( $info['licenses'] ) )
526 . Html::element( 'td', array( 'lang' => 'en', 'dir' => 'ltr' ), $info['description'] )
527 . Html::rawElement( 'td', array(), $authors )
528 . Html::closeElement( 'tr' );
530 $out .= Html::closeElement( 'table' );
532 return $out;
536 * Obtains a list of installed parser tags and the associated H2 header
538 * @return string HTML output
540 protected function getParserTags() {
541 global $wgParser;
543 $tags = $wgParser->getTags();
545 if ( count( $tags ) ) {
546 $out = Html::rawElement(
547 'h2',
548 array(
549 'class' => 'mw-headline plainlinks',
550 'id' => 'mw-version-parser-extensiontags',
552 Linker::makeExternalLink(
553 '//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
554 $this->msg( 'version-parser-extensiontags' )->parse(),
555 false /* msg()->parse() already escapes */
559 array_walk( $tags, function ( &$value ) {
560 // Bidirectional isolation improves readability in RTL wikis
561 $value = Html::element(
562 'bdi',
563 // Prevent < and > from slipping to another line
564 array(
565 'style' => 'white-space: nowrap;',
567 "<$value>"
569 } );
571 $out .= $this->listToText( $tags );
572 } else {
573 $out = '';
576 return $out;
580 * Obtains a list of installed parser function hooks and the associated H2 header
582 * @return string HTML output
584 protected function getParserFunctionHooks() {
585 global $wgParser;
587 $fhooks = $wgParser->getFunctionHooks();
588 if ( count( $fhooks ) ) {
589 $out = Html::rawElement(
590 'h2',
591 array(
592 'class' => 'mw-headline plainlinks',
593 'id' => 'mw-version-parser-function-hooks',
595 Linker::makeExternalLink(
596 '//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
597 $this->msg( 'version-parser-function-hooks' )->parse(),
598 false /* msg()->parse() already escapes */
602 $out .= $this->listToText( $fhooks );
603 } else {
604 $out = '';
607 return $out;
611 * Creates and returns the HTML for a single extension category.
613 * @since 1.17
615 * @param string $type
616 * @param string $message
618 * @return string
620 protected function getExtensionCategory( $type, $message ) {
621 global $wgExtensionCredits;
623 $out = '';
625 if ( array_key_exists( $type, $wgExtensionCredits ) && count( $wgExtensionCredits[$type] ) > 0 ) {
626 $out .= $this->openExtType( $message, 'credits-' . $type );
628 usort( $wgExtensionCredits[$type], array( $this, 'compare' ) );
630 foreach ( $wgExtensionCredits[$type] as $extension ) {
631 $out .= $this->getCreditsForExtension( $extension );
635 return $out;
639 * Callback to sort extensions by type.
640 * @param array $a
641 * @param array $b
642 * @return int
644 public function compare( $a, $b ) {
645 if ( $a['name'] === $b['name'] ) {
646 return 0;
647 } else {
648 return $this->getLanguage()->lc( $a['name'] ) > $this->getLanguage()->lc( $b['name'] )
650 : -1;
655 * Creates and formats a version line for a single extension.
657 * Information for five columns will be created. Parameters required in the
658 * $extension array for part rendering are indicated in ()
659 * - The name of (name), and URL link to (url), the extension
660 * - Official version number (version) and if available version control system
661 * revision (path), link, and date
662 * - If available the short name of the license (license-name) and a link
663 * to ((LICENSE)|(COPYING))(\.txt)? if it exists.
664 * - Description of extension (descriptionmsg or description)
665 * - List of authors (author) and link to a ((AUTHORS)|(CREDITS))(\.txt)? file if it exists
667 * @param array $extension
669 * @return string Raw HTML
671 public function getCreditsForExtension( array $extension ) {
672 $out = $this->getOutput();
674 // We must obtain the information for all the bits and pieces!
675 // ... such as extension names and links
676 if ( isset( $extension['namemsg'] ) ) {
677 // Localized name of extension
678 $extensionName = $this->msg( $extension['namemsg'] )->text();
679 } elseif ( isset( $extension['name'] ) ) {
680 // Non localized version
681 $extensionName = $extension['name'];
682 } else {
683 $extensionName = $this->msg( 'version-no-ext-name' )->text();
686 if ( isset( $extension['url'] ) ) {
687 $extensionNameLink = Linker::makeExternalLink(
688 $extension['url'],
689 $extensionName,
690 true,
692 array( 'class' => 'mw-version-ext-name' )
694 } else {
695 $extensionNameLink = $extensionName;
698 // ... and the version information
699 // If the extension path is set we will check that directory for GIT
700 // metadata in an attempt to extract date and vcs commit metadata.
701 $canonicalVersion = '&ndash;';
702 $extensionPath = null;
703 $vcsVersion = null;
704 $vcsLink = null;
705 $vcsDate = null;
707 if ( isset( $extension['version'] ) ) {
708 $canonicalVersion = $out->parseInline( $extension['version'] );
711 if ( isset( $extension['path'] ) ) {
712 global $IP;
713 $extensionPath = dirname( $extension['path'] );
714 if ( $this->coreId == '' ) {
715 wfDebug( 'Looking up core head id' );
716 $coreHeadSHA1 = self::getGitHeadSha1( $IP );
717 if ( $coreHeadSHA1 ) {
718 $this->coreId = $coreHeadSHA1;
721 $cache = wfGetCache( CACHE_ANYTHING );
722 $memcKey = wfMemcKey( 'specialversion-ext-version-text', $extension['path'], $this->coreId );
723 list( $vcsVersion, $vcsLink, $vcsDate ) = $cache->get( $memcKey );
725 if ( !$vcsVersion ) {
726 wfDebug( "Getting VCS info for extension {$extension['name']}" );
727 $gitInfo = new GitInfo( $extensionPath );
728 $vcsVersion = $gitInfo->getHeadSHA1();
729 if ( $vcsVersion !== false ) {
730 $vcsVersion = substr( $vcsVersion, 0, 7 );
731 $vcsLink = $gitInfo->getHeadViewUrl();
732 $vcsDate = $gitInfo->getHeadCommitDate();
734 $cache->set( $memcKey, array( $vcsVersion, $vcsLink, $vcsDate ), 60 * 60 * 24 );
735 } else {
736 wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" );
740 $versionString = Html::rawElement(
741 'span',
742 array( 'class' => 'mw-version-ext-version' ),
743 $canonicalVersion
746 if ( $vcsVersion ) {
747 if ( $vcsLink ) {
748 $vcsVerString = Linker::makeExternalLink(
749 $vcsLink,
750 $this->msg( 'version-version', $vcsVersion ),
751 true,
753 array( 'class' => 'mw-version-ext-vcs-version' )
755 } else {
756 $vcsVerString = Html::element( 'span',
757 array( 'class' => 'mw-version-ext-vcs-version' ),
758 "({$vcsVersion})"
761 $versionString .= " {$vcsVerString}";
763 if ( $vcsDate ) {
764 $vcsTimeString = Html::element( 'span',
765 array( 'class' => 'mw-version-ext-vcs-timestamp' ),
766 $this->getLanguage()->timeanddate( $vcsDate, true )
768 $versionString .= " {$vcsTimeString}";
770 $versionString = Html::rawElement( 'span',
771 array( 'class' => 'mw-version-ext-meta-version' ),
772 $versionString
776 // ... and license information; if a license file exists we
777 // will link to it
778 $licenseLink = '';
779 if ( isset( $extension['name'] ) ) {
780 $licenseName = null;
781 if ( isset( $extension['license-name'] ) ) {
782 $licenseName = $out->parseInline( $extension['license-name'] );
783 } elseif ( $this->getExtLicenseFileName( $extensionPath ) ) {
784 $licenseName = $this->msg( 'version-ext-license' )->escaped();
786 if ( $licenseName !== null ) {
787 $licenseLink = Linker::link(
788 $this->getPageTitle( 'License/' . $extension['name'] ),
789 $licenseName,
790 array(
791 'class' => 'mw-version-ext-license',
792 'dir' => 'auto',
798 // ... and generate the description; which can be a parameterized l10n message
799 // in the form array( <msgname>, <parameter>, <parameter>... ) or just a straight
800 // up string
801 if ( isset( $extension['descriptionmsg'] ) ) {
802 // Localized description of extension
803 $descriptionMsg = $extension['descriptionmsg'];
805 if ( is_array( $descriptionMsg ) ) {
806 $descriptionMsgKey = $descriptionMsg[0]; // Get the message key
807 array_shift( $descriptionMsg ); // Shift out the message key to get the parameters only
808 array_map( "htmlspecialchars", $descriptionMsg ); // For sanity
809 $description = $this->msg( $descriptionMsgKey, $descriptionMsg )->text();
810 } else {
811 $description = $this->msg( $descriptionMsg )->text();
813 } elseif ( isset( $extension['description'] ) ) {
814 // Non localized version
815 $description = $extension['description'];
816 } else {
817 $description = '';
819 $description = $out->parseInline( $description );
821 // ... now get the authors for this extension
822 $authors = isset( $extension['author'] ) ? $extension['author'] : array();
823 $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
825 // Finally! Create the table
826 $html = Html::openElement( 'tr', array(
827 'class' => 'mw-version-ext',
828 'id' => Sanitizer::escapeId( 'mw-version-ext-' . $extension['name'] )
832 $html .= Html::rawElement( 'td', array(), $extensionNameLink );
833 $html .= Html::rawElement( 'td', array(), $versionString );
834 $html .= Html::rawElement( 'td', array(), $licenseLink );
835 $html .= Html::rawElement( 'td', array( 'class' => 'mw-version-ext-description' ), $description );
836 $html .= Html::rawElement( 'td', array( 'class' => 'mw-version-ext-authors' ), $authors );
838 $html .= Html::closeElement( 'tr' );
840 return $html;
844 * Generate wikitext showing hooks in $wgHooks.
846 * @return string Wikitext
848 private function getWgHooks() {
849 global $wgSpecialVersionShowHooks, $wgHooks;
851 if ( $wgSpecialVersionShowHooks && count( $wgHooks ) ) {
852 $myWgHooks = $wgHooks;
853 ksort( $myWgHooks );
855 $ret = array();
856 $ret[] = '== {{int:version-hooks}} ==';
857 $ret[] = Html::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-hooks' ) );
858 $ret[] = Html::openElement( 'tr' );
859 $ret[] = Html::element( 'th', array(), $this->msg( 'version-hook-name' )->text() );
860 $ret[] = Html::element( 'th', array(), $this->msg( 'version-hook-subscribedby' )->text() );
861 $ret[] = Html::closeElement( 'tr' );
863 foreach ( $myWgHooks as $hook => $hooks ) {
864 $ret[] = Html::openElement( 'tr' );
865 $ret[] = Html::element( 'td', array(), $hook );
866 $ret[] = Html::element( 'td', array(), $this->listToText( $hooks ) );
867 $ret[] = Html::closeElement( 'tr' );
870 $ret[] = Html::closeElement( 'table' );
872 return implode( "\n", $ret );
873 } else {
874 return '';
878 private function openExtType( $text = null, $name = null ) {
879 $out = '';
881 $opt = array( 'colspan' => 5 );
882 if ( $this->firstExtOpened ) {
883 // Insert a spacing line
884 $out .= Html::rawElement( 'tr', array( 'class' => 'sv-space' ),
885 Html::element( 'td', $opt )
888 $this->firstExtOpened = true;
890 if ( $name ) {
891 $opt['id'] = "sv-$name";
894 if ( $text !== null ) {
895 $out .= Html::rawElement( 'tr', array(),
896 Html::element( 'th', $opt, $text )
900 $firstHeadingMsg = ( $name === 'credits-skin' )
901 ? 'version-skin-colheader-name'
902 : 'version-ext-colheader-name';
903 $out .= Html::openElement( 'tr' );
904 $out .= Html::element( 'th', array( 'class' => 'mw-version-ext-col-label' ),
905 $this->msg( $firstHeadingMsg )->text() );
906 $out .= Html::element( 'th', array( 'class' => 'mw-version-ext-col-label' ),
907 $this->msg( 'version-ext-colheader-version' )->text() );
908 $out .= Html::element( 'th', array( 'class' => 'mw-version-ext-col-label' ),
909 $this->msg( 'version-ext-colheader-license' )->text() );
910 $out .= Html::element( 'th', array( 'class' => 'mw-version-ext-col-label' ),
911 $this->msg( 'version-ext-colheader-description' )->text() );
912 $out .= Html::element( 'th', array( 'class' => 'mw-version-ext-col-label' ),
913 $this->msg( 'version-ext-colheader-credits' )->text() );
914 $out .= Html::closeElement( 'tr' );
916 return $out;
920 * Get information about client's IP address.
922 * @return string HTML fragment
924 private function IPInfo() {
925 $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
927 return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
931 * Return a formatted unsorted list of authors
933 * 'And Others'
934 * If an item in the $authors array is '...' it is assumed to indicate an
935 * 'and others' string which will then be linked to an ((AUTHORS)|(CREDITS))(\.txt)?
936 * file if it exists in $dir.
938 * Similarly an entry ending with ' ...]' is assumed to be a link to an
939 * 'and others' page.
941 * If no '...' string variant is found, but an authors file is found an
942 * 'and others' will be added to the end of the credits.
944 * @param string|array $authors
945 * @param string|bool $extName Name of the extension for link creation,
946 * false if no links should be created
947 * @param string $extDir Path to the extension root directory
949 * @return string HTML fragment
951 public function listAuthors( $authors, $extName, $extDir ) {
952 $hasOthers = false;
954 $list = array();
955 foreach ( (array)$authors as $item ) {
956 if ( $item == '...' ) {
957 $hasOthers = true;
959 if ( $extName && $this->getExtAuthorsFileName( $extDir ) ) {
960 $text = Linker::link(
961 $this->getPageTitle( "Credits/$extName" ),
962 $this->msg( 'version-poweredby-others' )->escaped()
964 } else {
965 $text = $this->msg( 'version-poweredby-others' )->escaped();
967 $list[] = $text;
968 } elseif ( substr( $item, -5 ) == ' ...]' ) {
969 $hasOthers = true;
970 $list[] = $this->getOutput()->parseInline(
971 substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
973 } else {
974 $list[] = $this->getOutput()->parseInline( $item );
978 if ( $extName && !$hasOthers && $this->getExtAuthorsFileName( $extDir ) ) {
979 $list[] = $text = Linker::link(
980 $this->getPageTitle( "Credits/$extName" ),
981 $this->msg( 'version-poweredby-others' )->escaped()
985 return $this->listToText( $list, false );
989 * Obtains the full path of an extensions authors or credits file if
990 * one exists.
992 * @param string $extDir Path to the extensions root directory
994 * @since 1.23
996 * @return bool|string False if no such file exists, otherwise returns
997 * a path to it.
999 public static function getExtAuthorsFileName( $extDir ) {
1000 if ( !$extDir ) {
1001 return false;
1004 foreach ( scandir( $extDir ) as $file ) {
1005 $fullPath = $extDir . DIRECTORY_SEPARATOR . $file;
1006 if ( preg_match( '/^((AUTHORS)|(CREDITS))(\.txt|\.wiki|\.mediawiki)?$/', $file ) &&
1007 is_readable( $fullPath ) &&
1008 is_file( $fullPath )
1010 return $fullPath;
1014 return false;
1018 * Obtains the full path of an extensions copying or license file if
1019 * one exists.
1021 * @param string $extDir Path to the extensions root directory
1023 * @since 1.23
1025 * @return bool|string False if no such file exists, otherwise returns
1026 * a path to it.
1028 public static function getExtLicenseFileName( $extDir ) {
1029 if ( !$extDir ) {
1030 return false;
1033 foreach ( scandir( $extDir ) as $file ) {
1034 $fullPath = $extDir . DIRECTORY_SEPARATOR . $file;
1035 if ( preg_match( '/^((COPYING)|(LICENSE))(\.txt)?$/', $file ) &&
1036 is_readable( $fullPath ) &&
1037 is_file( $fullPath )
1039 return $fullPath;
1043 return false;
1047 * Convert an array of items into a list for display.
1049 * @param array $list List of elements to display
1050 * @param bool $sort Whether to sort the items in $list
1052 * @return string
1054 public function listToText( $list, $sort = true ) {
1055 if ( !count( $list ) ) {
1056 return '';
1058 if ( $sort ) {
1059 sort( $list );
1062 return $this->getLanguage()
1063 ->listToText( array_map( array( __CLASS__, 'arrayToString' ), $list ) );
1067 * Convert an array or object to a string for display.
1069 * @param mixed $list Will convert an array to string if given and return
1070 * the parameter unaltered otherwise
1072 * @return mixed
1074 public static function arrayToString( $list ) {
1075 if ( is_array( $list ) && count( $list ) == 1 ) {
1076 $list = $list[0];
1078 if ( $list instanceof Closure ) {
1079 // Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$"
1080 return 'Closure';
1081 } elseif ( is_object( $list ) ) {
1082 $class = wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1084 return $class;
1085 } elseif ( !is_array( $list ) ) {
1086 return $list;
1087 } else {
1088 if ( is_object( $list[0] ) ) {
1089 $class = get_class( $list[0] );
1090 } else {
1091 $class = $list[0];
1094 return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1099 * @param string $dir Directory of the git checkout
1100 * @return bool|string Sha1 of commit HEAD points to
1102 public static function getGitHeadSha1( $dir ) {
1103 $repo = new GitInfo( $dir );
1105 return $repo->getHeadSHA1();
1109 * @param string $dir Directory of the git checkout
1110 * @return bool|string Branch currently checked out
1112 public static function getGitCurrentBranch( $dir ) {
1113 $repo = new GitInfo( $dir );
1114 return $repo->getCurrentBranch();
1118 * Get the list of entry points and their URLs
1119 * @return string Wikitext
1121 public function getEntryPointInfo() {
1122 global $wgArticlePath, $wgScriptPath;
1123 $scriptPath = $wgScriptPath ? $wgScriptPath : "/";
1124 $entryPoints = array(
1125 'version-entrypoints-articlepath' => $wgArticlePath,
1126 'version-entrypoints-scriptpath' => $scriptPath,
1127 'version-entrypoints-index-php' => wfScript( 'index' ),
1128 'version-entrypoints-api-php' => wfScript( 'api' ),
1129 'version-entrypoints-load-php' => wfScript( 'load' ),
1132 $language = $this->getLanguage();
1133 $thAttribures = array(
1134 'dir' => $language->getDir(),
1135 'lang' => $language->getHtmlCode()
1137 $out = Html::element(
1138 'h2',
1139 array( 'id' => 'mw-version-entrypoints' ),
1140 $this->msg( 'version-entrypoints' )->text()
1142 Html::openElement( 'table',
1143 array(
1144 'class' => 'wikitable plainlinks',
1145 'id' => 'mw-version-entrypoints-table',
1146 'dir' => 'ltr',
1147 'lang' => 'en'
1150 Html::openElement( 'tr' ) .
1151 Html::element(
1152 'th',
1153 $thAttribures,
1154 $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1156 Html::element(
1157 'th',
1158 $thAttribures,
1159 $this->msg( 'version-entrypoints-header-url' )->text()
1161 Html::closeElement( 'tr' );
1163 foreach ( $entryPoints as $message => $value ) {
1164 $url = wfExpandUrl( $value, PROTO_RELATIVE );
1165 $out .= Html::openElement( 'tr' ) .
1166 // ->text() looks like it should be ->parse(), but this function
1167 // returns wikitext, not HTML, boo
1168 Html::rawElement( 'td', array(), $this->msg( $message )->text() ) .
1169 Html::rawElement( 'td', array(), Html::rawElement( 'code', array(), "[$url $value]" ) ) .
1170 Html::closeElement( 'tr' );
1173 $out .= Html::closeElement( 'table' );
1175 return $out;
1178 protected function getGroupName() {
1179 return 'wiki';