Merge "Fix doc of LogFormatter::newFromRow"
[lhc/web/wiklou.git] / includes / specials / SpecialVersion.php
1 <?php
2 /**
3 * Implements Special:Version
4 *
5 * Copyright © 2005 Ævar Arnfjörð Bjarmason
6 *
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.
11 *
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.
16 *
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
21 *
22 * @file
23 * @ingroup SpecialPage
24 */
25
26 /**
27 * Give information about the version of MediaWiki, PHP, the DB and extensions
28 *
29 * @ingroup SpecialPage
30 */
31 class SpecialVersion extends SpecialPage {
32 protected $firstExtOpened = false;
33
34 /**
35 * Stores the current rev id/SHA hash of MediaWiki core
36 */
37 protected $coreId = '';
38
39 protected static $extensionTypes = false;
40
41 public function __construct() {
42 parent::__construct( 'Version' );
43 }
44
45 /**
46 * main()
47 * @param string|null $par
48 */
49 public function execute( $par ) {
50 global $IP, $wgExtensionCredits;
51
52 $this->setHeaders();
53 $this->outputHeader();
54 $out = $this->getOutput();
55 $out->allowClickjacking();
56
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;
68 }
69 }
70 }
71 if ( !$extNode ) {
72 $out->setStatusCode( 404 );
73 }
74 } else {
75 $extName = 'MediaWiki';
76 }
77
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',
94 ),
95 $wikiText
96 );
97 }
98 }
99 }
100
101 $out->setPageTitle( $this->msg( 'version-credits-title', $extName ) );
102 $out->addWikiText( $wikiText );
103 break;
104
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',
118 ),
119 $wikiText
120 );
121 }
122 }
123
124 $out->setPageTitle( $this->msg( 'version-license-title', $extName ) );
125 $out->addWikiText( $wikiText );
126 break;
127
128 default:
129 $out->addModules( 'mediawiki.special.version' );
130 $out->addWikiText(
131 $this->getMediaWikiCredits() .
132 $this->softwareInformation() .
133 $this->getEntryPointInfo()
134 );
135 $out->addHtml(
136 $this->getSkinCredits() .
137 $this->getExtensionCredits() .
138 $this->getExternalLibraries() .
139 $this->getParserTags() .
140 $this->getParserFunctionHooks()
141 );
142 $out->addWikiText( $this->getWgHooks() );
143 $out->addHTML( $this->IPInfo() );
144
145 break;
146 }
147 }
148
149 /**
150 * Returns wiki text showing the license information.
151 *
152 * @return string
153 */
154 private static function getMediaWikiCredits() {
155 $ret = Xml::element(
156 'h2',
157 array( 'id' => 'mw-version-license' ),
158 wfMessage( 'version-license' )->text()
159 );
160
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>';
167
168 return str_replace( "\t\t", '', $ret ) . "\n";
169 }
170
171 /**
172 * Get the "MediaWiki is copyright 2001-20xx by lots of cool guys" text
173 *
174 * @return string
175 */
176 public static function getCopyrightAndAuthorList() {
177 global $wgLang;
178
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() . ']]';
185 }
186
187 $translatorsLink = '[//translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
188 wfMessage( 'version-poweredby-translators' )->text() . ']';
189
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
200 );
201
202 return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
203 $wgLang->listToText( $authorList ) )->text();
204 }
205
206 /**
207 * Returns wiki text showing the third party software versions (apache, php, mysql).
208 *
209 * @return string
210 */
211 public static function softwareInformation() {
212 $dbr = wfGetDB( DB_SLAVE );
213
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 . ")";
223 }
224 $software[$dbr->getSoftwareLink()] = $dbr->getServerInfo();
225
226 if ( IcuCollation::getICUVersion() ) {
227 $software['[http://site.icu-project.org/ ICU]'] = IcuCollation::getICUVersion();
228 }
229
230 // Allow a hook to add/remove items.
231 Hooks::run( 'SoftwareInfo', array( &$software ) );
232
233 $out = Xml::element(
234 'h2',
235 array( 'id' => 'mw-version-software' ),
236 wfMessage( 'version-software' )->text()
237 ) .
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";
243
244 foreach ( $software as $name => $version ) {
245 $out .= "<tr>
246 <td>" . $name . "</td>
247 <td dir=\"ltr\">" . $version . "</td>
248 </tr>\n";
249 }
250
251 return $out . Xml::closeElement( 'table' );
252 }
253
254 /**
255 * Return a string of the MediaWiki version with Git revision if available.
256 *
257 * @param string $flags
258 * @return mixed
259 */
260 public static function getVersion( $flags = '' ) {
261 global $wgVersion, $IP;
262
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";
273 }
274
275 return $version;
276 }
277
278 /**
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
282 *
283 * @return mixed
284 */
285 public static function getVersionLinked() {
286 global $wgVersion;
287
288 $gitVersion = self::getVersionLinkedGit();
289 if ( $gitVersion ) {
290 $v = $gitVersion;
291 } else {
292 $v = $wgVersion; // fallback
293 }
294
295 return $v;
296 }
297
298 /**
299 * @return string
300 */
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]}";
308 }
309
310 return "[$versionUrl $wgVersion]";
311 }
312
313 /**
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
317 */
318 private static function getVersionLinkedGit() {
319 global $IP, $wgLang;
320
321 $gitInfo = new GitInfo( $IP );
322 $headSHA1 = $gitInfo->getHeadSHA1();
323 if ( !$headSHA1 ) {
324 return false;
325 }
326
327 $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
328
329 $gitHeadUrl = $gitInfo->getHeadViewUrl();
330 if ( $gitHeadUrl !== false ) {
331 $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
332 }
333
334 $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
335 if ( $gitHeadCommitDate ) {
336 $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( $gitHeadCommitDate, true );
337 }
338
339 return self::getwgVersionLinked() . " $shortSHA1";
340 }
341
342 /**
343 * Returns an array with the base extension types.
344 * Type is stored as array key, the message as array value.
345 *
346 * TODO: ideally this would return all extension types.
347 *
348 * @since 1.17
349 *
350 * @return array
351 */
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(),
363 );
364
365 Hooks::run( 'ExtensionTypes', array( &self::$extensionTypes ) );
366 }
367
368 return self::$extensionTypes;
369 }
370
371 /**
372 * Returns the internationalized name for an extension type.
373 *
374 * @since 1.17
375 *
376 * @param string $type
377 *
378 * @return string
379 */
380 public static function getExtensionTypeName( $type ) {
381 $types = self::getExtensionTypes();
382
383 return isset( $types[$type] ) ? $types[$type] : $types['other'];
384 }
385
386 /**
387 * Generate wikitext showing the name, URL, author and description of each extension.
388 *
389 * @return string Wikitext
390 */
391 public function getExtensionCredits() {
392 global $wgExtensionCredits;
393
394 if (
395 count( $wgExtensionCredits ) === 0 ||
396 // Skins are displayed separately, see getSkinCredits()
397 ( count( $wgExtensionCredits ) === 1 && isset( $wgExtensionCredits['skin'] ) )
398 ) {
399 return '';
400 }
401
402 $extensionTypes = self::getExtensionTypes();
403
404 $out = Xml::element(
405 'h2',
406 array( 'id' => 'mw-version-ext' ),
407 $this->msg( 'version-extensions' )->text()
408 ) .
409 Xml::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'sv-ext' ) );
410
411 // Make sure the 'other' type is set to an array.
412 if ( !array_key_exists( 'other', $wgExtensionCredits ) ) {
413 $wgExtensionCredits['other'] = array();
414 }
415
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 );
420 }
421 }
422
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 );
429 }
430 }
431
432 // We want the 'other' type to be last in the list.
433 $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'] );
434
435 $out .= Xml::closeElement( 'table' );
436
437 return $out;
438 }
439
440 /**
441 * Generate wikitext showing the name, URL, author and description of each skin.
442 *
443 * @return string Wikitext
444 */
445 public function getSkinCredits() {
446 global $wgExtensionCredits;
447 if ( !isset( $wgExtensionCredits['skin'] ) || count( $wgExtensionCredits['skin'] ) === 0 ) {
448 return '';
449 }
450
451 $out = Xml::element(
452 'h2',
453 array( 'id' => 'mw-version-skin' ),
454 $this->msg( 'version-skins' )->text()
455 ) .
456 Xml::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'sv-skin' ) );
457
458 $this->firstExtOpened = false;
459 $out .= $this->getExtensionCategory( 'skin', null );
460
461 $out .= Xml::closeElement( 'table' );
462
463 return $out;
464 }
465
466 /**
467 * Generate an HTML table for external libraries that are installed
468 *
469 * @return string
470 */
471 protected function getExternalLibraries() {
472 global $IP;
473 $path = "$IP/vendor/composer/installed.json";
474 if ( !file_exists( $path ) ) {
475 return '';
476 }
477
478 $installed = new ComposerInstalled( $path );
479 $out = Html::element(
480 'h2',
481 array( 'id' => 'mw-version-libraries' ),
482 $this->msg( 'version-libraries' )->text()
483 );
484 $out .= Html::openElement(
485 'table',
486 array( 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries' )
487 );
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' );
495
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;
501 }
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']}]";
506 }
507 return $arr['name'];
508 }, $info['authors'] );
509 $authors = $this->listAuthors( $authors, false, "$IP/vendor/$name" );
510
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' )
522 )
523 )
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' );
529 }
530 $out .= Html::closeElement( 'table' );
531
532 return $out;
533 }
534
535 /**
536 * Obtains a list of installed parser tags and the associated H2 header
537 *
538 * @return string HTML output
539 */
540 protected function getParserTags() {
541 global $wgParser;
542
543 $tags = $wgParser->getTags();
544
545 if ( count( $tags ) ) {
546 $out = Html::rawElement(
547 'h2',
548 array(
549 'class' => 'mw-headline plainlinks',
550 'id' => 'mw-version-parser-extensiontags',
551 ),
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 */
556 )
557 );
558
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;',
566 ),
567 "<$value>"
568 );
569 } );
570
571 $out .= $this->listToText( $tags );
572 } else {
573 $out = '';
574 }
575
576 return $out;
577 }
578
579 /**
580 * Obtains a list of installed parser function hooks and the associated H2 header
581 *
582 * @return string HTML output
583 */
584 protected function getParserFunctionHooks() {
585 global $wgParser;
586
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',
594 ),
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 */
599 )
600 );
601
602 $out .= $this->listToText( $fhooks );
603 } else {
604 $out = '';
605 }
606
607 return $out;
608 }
609
610 /**
611 * Creates and returns the HTML for a single extension category.
612 *
613 * @since 1.17
614 *
615 * @param string $type
616 * @param string $message
617 *
618 * @return string
619 */
620 protected function getExtensionCategory( $type, $message ) {
621 global $wgExtensionCredits;
622
623 $out = '';
624
625 if ( array_key_exists( $type, $wgExtensionCredits ) && count( $wgExtensionCredits[$type] ) > 0 ) {
626 $out .= $this->openExtType( $message, 'credits-' . $type );
627
628 usort( $wgExtensionCredits[$type], array( $this, 'compare' ) );
629
630 foreach ( $wgExtensionCredits[$type] as $extension ) {
631 $out .= $this->getCreditsForExtension( $extension );
632 }
633 }
634
635 return $out;
636 }
637
638 /**
639 * Callback to sort extensions by type.
640 * @param array $a
641 * @param array $b
642 * @return int
643 */
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'] )
649 ? 1
650 : -1;
651 }
652 }
653
654 /**
655 * Creates and formats a version line for a single extension.
656 *
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
666 *
667 * @param array $extension
668 *
669 * @return string Raw HTML
670 */
671 public function getCreditsForExtension( array $extension ) {
672 $out = $this->getOutput();
673
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();
684 }
685
686 if ( isset( $extension['url'] ) ) {
687 $extensionNameLink = Linker::makeExternalLink(
688 $extension['url'],
689 $extensionName,
690 true,
691 '',
692 array( 'class' => 'mw-version-ext-name' )
693 );
694 } else {
695 $extensionNameLink = $extensionName;
696 }
697
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;
706
707 if ( isset( $extension['version'] ) ) {
708 $canonicalVersion = $out->parseInline( $extension['version'] );
709 }
710
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;
719 }
720 }
721 $cache = wfGetCache( CACHE_ANYTHING );
722 $memcKey = wfMemcKey( 'specialversion-ext-version-text', $extension['path'], $this->coreId );
723 list( $vcsVersion, $vcsLink, $vcsDate ) = $cache->get( $memcKey );
724
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();
733 }
734 $cache->set( $memcKey, array( $vcsVersion, $vcsLink, $vcsDate ), 60 * 60 * 24 );
735 } else {
736 wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" );
737 }
738 }
739
740 $versionString = Html::rawElement(
741 'span',
742 array( 'class' => 'mw-version-ext-version' ),
743 $canonicalVersion
744 );
745
746 if ( $vcsVersion ) {
747 if ( $vcsLink ) {
748 $vcsVerString = Linker::makeExternalLink(
749 $vcsLink,
750 $this->msg( 'version-version', $vcsVersion ),
751 true,
752 '',
753 array( 'class' => 'mw-version-ext-vcs-version' )
754 );
755 } else {
756 $vcsVerString = Html::element( 'span',
757 array( 'class' => 'mw-version-ext-vcs-version' ),
758 "({$vcsVersion})"
759 );
760 }
761 $versionString .= " {$vcsVerString}";
762
763 if ( $vcsDate ) {
764 $vcsTimeString = Html::element( 'span',
765 array( 'class' => 'mw-version-ext-vcs-timestamp' ),
766 $this->getLanguage()->timeanddate( $vcsDate, true )
767 );
768 $versionString .= " {$vcsTimeString}";
769 }
770 $versionString = Html::rawElement( 'span',
771 array( 'class' => 'mw-version-ext-meta-version' ),
772 $versionString
773 );
774 }
775
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();
785 }
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',
793 )
794 );
795 }
796 }
797
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'];
804
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();
812 }
813 } elseif ( isset( $extension['description'] ) ) {
814 // Non localized version
815 $description = $extension['description'];
816 } else {
817 $description = '';
818 }
819 $description = $out->parseInline( $description );
820
821 // ... now get the authors for this extension
822 $authors = isset( $extension['author'] ) ? $extension['author'] : array();
823 $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
824
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'] )
829 )
830 );
831
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 );
837
838 $html .= Html::closeElement( 'tr' );
839
840 return $html;
841 }
842
843 /**
844 * Generate wikitext showing hooks in $wgHooks.
845 *
846 * @return string Wikitext
847 */
848 private function getWgHooks() {
849 global $wgSpecialVersionShowHooks, $wgHooks;
850
851 if ( $wgSpecialVersionShowHooks && count( $wgHooks ) ) {
852 $myWgHooks = $wgHooks;
853 ksort( $myWgHooks );
854
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' );
862
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' );
868 }
869
870 $ret[] = Html::closeElement( 'table' );
871
872 return implode( "\n", $ret );
873 } else {
874 return '';
875 }
876 }
877
878 private function openExtType( $text = null, $name = null ) {
879 $out = '';
880
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 )
886 );
887 }
888 $this->firstExtOpened = true;
889
890 if ( $name ) {
891 $opt['id'] = "sv-$name";
892 }
893
894 if ( $text !== null ) {
895 $out .= Html::rawElement( 'tr', array(),
896 Html::element( 'th', $opt, $text )
897 );
898 }
899
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' );
915
916 return $out;
917 }
918
919 /**
920 * Get information about client's IP address.
921 *
922 * @return string HTML fragment
923 */
924 private function IPInfo() {
925 $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
926
927 return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
928 }
929
930 /**
931 * Return a formatted unsorted list of authors
932 *
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.
937 *
938 * Similarly an entry ending with ' ...]' is assumed to be a link to an
939 * 'and others' page.
940 *
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.
943 *
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
948 *
949 * @return string HTML fragment
950 */
951 public function listAuthors( $authors, $extName, $extDir ) {
952 $hasOthers = false;
953
954 $list = array();
955 foreach ( (array)$authors as $item ) {
956 if ( $item == '...' ) {
957 $hasOthers = true;
958
959 if ( $extName && $this->getExtAuthorsFileName( $extDir ) ) {
960 $text = Linker::link(
961 $this->getPageTitle( "Credits/$extName" ),
962 $this->msg( 'version-poweredby-others' )->escaped()
963 );
964 } else {
965 $text = $this->msg( 'version-poweredby-others' )->escaped();
966 }
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() . "]"
972 );
973 } else {
974 $list[] = $this->getOutput()->parseInline( $item );
975 }
976 }
977
978 if ( $extName && !$hasOthers && $this->getExtAuthorsFileName( $extDir ) ) {
979 $list[] = $text = Linker::link(
980 $this->getPageTitle( "Credits/$extName" ),
981 $this->msg( 'version-poweredby-others' )->escaped()
982 );
983 }
984
985 return $this->listToText( $list, false );
986 }
987
988 /**
989 * Obtains the full path of an extensions authors or credits file if
990 * one exists.
991 *
992 * @param string $extDir Path to the extensions root directory
993 *
994 * @since 1.23
995 *
996 * @return bool|string False if no such file exists, otherwise returns
997 * a path to it.
998 */
999 public static function getExtAuthorsFileName( $extDir ) {
1000 if ( !$extDir ) {
1001 return false;
1002 }
1003
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 )
1009 ) {
1010 return $fullPath;
1011 }
1012 }
1013
1014 return false;
1015 }
1016
1017 /**
1018 * Obtains the full path of an extensions copying or license file if
1019 * one exists.
1020 *
1021 * @param string $extDir Path to the extensions root directory
1022 *
1023 * @since 1.23
1024 *
1025 * @return bool|string False if no such file exists, otherwise returns
1026 * a path to it.
1027 */
1028 public static function getExtLicenseFileName( $extDir ) {
1029 if ( !$extDir ) {
1030 return false;
1031 }
1032
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 )
1038 ) {
1039 return $fullPath;
1040 }
1041 }
1042
1043 return false;
1044 }
1045
1046 /**
1047 * Convert an array of items into a list for display.
1048 *
1049 * @param array $list List of elements to display
1050 * @param bool $sort Whether to sort the items in $list
1051 *
1052 * @return string
1053 */
1054 public function listToText( $list, $sort = true ) {
1055 if ( !count( $list ) ) {
1056 return '';
1057 }
1058 if ( $sort ) {
1059 sort( $list );
1060 }
1061
1062 return $this->getLanguage()
1063 ->listToText( array_map( array( __CLASS__, 'arrayToString' ), $list ) );
1064 }
1065
1066 /**
1067 * Convert an array or object to a string for display.
1068 *
1069 * @param mixed $list Will convert an array to string if given and return
1070 * the parameter unaltered otherwise
1071 *
1072 * @return mixed
1073 */
1074 public static function arrayToString( $list ) {
1075 if ( is_array( $list ) && count( $list ) == 1 ) {
1076 $list = $list[0];
1077 }
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();
1083
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];
1092 }
1093
1094 return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1095 }
1096 }
1097
1098 /**
1099 * @param string $dir Directory of the git checkout
1100 * @return bool|string Sha1 of commit HEAD points to
1101 */
1102 public static function getGitHeadSha1( $dir ) {
1103 $repo = new GitInfo( $dir );
1104
1105 return $repo->getHeadSHA1();
1106 }
1107
1108 /**
1109 * @param string $dir Directory of the git checkout
1110 * @return bool|string Branch currently checked out
1111 */
1112 public static function getGitCurrentBranch( $dir ) {
1113 $repo = new GitInfo( $dir );
1114 return $repo->getCurrentBranch();
1115 }
1116
1117 /**
1118 * Get the list of entry points and their URLs
1119 * @return string Wikitext
1120 */
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' ),
1130 );
1131
1132 $language = $this->getLanguage();
1133 $thAttribures = array(
1134 'dir' => $language->getDir(),
1135 'lang' => $language->getHtmlCode()
1136 );
1137 $out = Html::element(
1138 'h2',
1139 array( 'id' => 'mw-version-entrypoints' ),
1140 $this->msg( 'version-entrypoints' )->text()
1141 ) .
1142 Html::openElement( 'table',
1143 array(
1144 'class' => 'wikitable plainlinks',
1145 'id' => 'mw-version-entrypoints-table',
1146 'dir' => 'ltr',
1147 'lang' => 'en'
1148 )
1149 ) .
1150 Html::openElement( 'tr' ) .
1151 Html::element(
1152 'th',
1153 $thAttribures,
1154 $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1155 ) .
1156 Html::element(
1157 'th',
1158 $thAttribures,
1159 $this->msg( 'version-entrypoints-header-url' )->text()
1160 ) .
1161 Html::closeElement( 'tr' );
1162
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' );
1171 }
1172
1173 $out .= Html::closeElement( 'table' );
1174
1175 return $out;
1176 }
1177
1178 protected function getGroupName() {
1179 return 'wiki';
1180 }
1181 }