318ceb148e15e020dd391ba5bf31ac4068091828
[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 protected static $viewvcUrls = array(
42 'svn+ssh://svn.wikimedia.org/svnroot/mediawiki' => 'http://svn.wikimedia.org/viewvc/mediawiki',
43 'http://svn.wikimedia.org/svnroot/mediawiki' => 'http://svn.wikimedia.org/viewvc/mediawiki',
44 'https://svn.wikimedia.org/svnroot/mediawiki' => 'https://svn.wikimedia.org/viewvc/mediawiki',
45 );
46
47 public function __construct() {
48 parent::__construct( 'Version' );
49 }
50
51 /**
52 * main()
53 */
54 public function execute( $par ) {
55 global $IP, $wgExtensionCredits;
56
57 $this->setHeaders();
58 $this->outputHeader();
59 $out = $this->getOutput();
60 $out->allowClickjacking();
61
62 // Explode the sub page information into useful bits
63 $parts = explode( '/', (string)$par );
64 $extNode = null;
65 if ( isset( $parts[1] ) ) {
66 $extName = str_replace( '_', ' ', $parts[1] );
67 // Find it!
68 foreach ( $wgExtensionCredits as $group => $extensions ) {
69 foreach ( $extensions as $ext ) {
70 if ( isset( $ext['name'] ) && ( $ext['name'] === $extName ) ) {
71 $extNode = &$ext;
72 break 2;
73 }
74 }
75 }
76 if ( !$extNode ) {
77 $out->setStatusCode( 404 );
78 }
79 } else {
80 $extName = 'MediaWiki';
81 }
82
83 // Now figure out what to do
84 switch ( strtolower( $parts[0] ) ) {
85 case 'credits':
86 $wikiText = '{{int:version-credits-not-found}}';
87 if ( $extName === 'MediaWiki' ) {
88 $wikiText = file_get_contents( $IP . '/CREDITS' );
89 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
90 $file = $this->getExtAuthorsFileName( dirname( $extNode['path'] ) );
91 if ( $file ) {
92 $wikiText = file_get_contents( $file );
93 if ( substr( $file, -4 ) === '.txt' ) {
94 $wikiText = Html::element( 'pre', array(), $wikiText );
95 }
96 }
97 }
98
99 $out->setPageTitle( $this->msg( 'version-credits-title', $extName ) );
100 $out->addWikiText( $wikiText );
101 break;
102
103 case 'license':
104 $wikiText = '{{int:version-license-not-found}}';
105 if ( $extName === 'MediaWiki' ) {
106 $wikiText = file_get_contents( $IP . '/COPYING' );
107 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
108 $file = $this->getExtLicenseFileName( dirname( $extNode['path'] ) );
109 if ( $file ) {
110 $wikiText = file_get_contents( $file );
111 if ( !isset( $extNode['license-name'] ) ) {
112 // If the developer did not explicitly set license-name they probably
113 // are unaware that we're now sucking this file in and thus it's probably
114 // not wikitext friendly.
115 $wikiText = "<pre>$wikiText</pre>";
116 }
117 }
118 }
119
120 $out->setPageTitle( $this->msg( 'version-license-title', $extName ) );
121 $out->addWikiText( $wikiText );
122 break;
123
124 default:
125 $out->addModules( 'mediawiki.special.version' );
126 $out->addWikiText(
127 $this->getMediaWikiCredits() .
128 $this->softwareInformation() .
129 $this->getEntryPointInfo()
130 );
131 $out->addHtml(
132 $this->getSkinCredits() .
133 $this->getExtensionCredits() .
134 $this->getParserTags() .
135 $this->getParserFunctionHooks()
136 );
137 $out->addWikiText( $this->getWgHooks() );
138 $out->addHTML( $this->IPInfo() );
139
140 break;
141 }
142 }
143
144 /**
145 * Returns wiki text showing the license information.
146 *
147 * @return string
148 */
149 private static function getMediaWikiCredits() {
150 $ret = Xml::element(
151 'h2',
152 array( 'id' => 'mw-version-license' ),
153 wfMessage( 'version-license' )->text()
154 );
155
156 // This text is always left-to-right.
157 $ret .= '<div class="plainlinks">';
158 $ret .= "__NOTOC__
159 " . self::getCopyrightAndAuthorList() . "\n
160 " . wfMessage( 'version-license-info' )->text();
161 $ret .= '</div>';
162
163 return str_replace( "\t\t", '', $ret ) . "\n";
164 }
165
166 /**
167 * Get the "MediaWiki is copyright 2001-20xx by lots of cool guys" text
168 *
169 * @return string
170 */
171 public static function getCopyrightAndAuthorList() {
172 global $wgLang;
173
174 if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
175 $othersLink = '[//www.mediawiki.org/wiki/Special:Version/Credits ' .
176 wfMessage( 'version-poweredby-others' )->text() . ']';
177 } else {
178 $othersLink = '[[Special:Version/Credits|' .
179 wfMessage( 'version-poweredby-others' )->text() . ']]';
180 }
181
182 $translatorsLink = '[//translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
183 wfMessage( 'version-poweredby-translators' )->text() . ']';
184
185 $authorList = array(
186 'Magnus Manske', 'Brion Vibber', 'Lee Daniel Crocker',
187 'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
188 'Niklas Laxström', 'Domas Mituzas', 'Rob Church', 'Yuri Astrakhan',
189 'Aryeh Gregor', 'Aaron Schulz', 'Andrew Garrett', 'Raimond Spekking',
190 'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
191 'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
192 'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
193 'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', $othersLink,
194 $translatorsLink
195 );
196
197 return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
198 $wgLang->listToText( $authorList ) )->text();
199 }
200
201 /**
202 * Returns wiki text showing the third party software versions (apache, php, mysql).
203 *
204 * @return string
205 */
206 static function softwareInformation() {
207 $dbr = wfGetDB( DB_SLAVE );
208
209 // Put the software in an array of form 'name' => 'version'. All messages should
210 // be loaded here, so feel free to use wfMessage in the 'name'. Raw HTML or
211 // wikimarkup can be used.
212 $software = array();
213 $software['[https://www.mediawiki.org/ MediaWiki]'] = self::getVersionLinked();
214 $phpKey = wfIsHHVM() ? '[http://hhvm.com/ HHVM]' :
215 '[https://php.net/ PHP]';
216 $software[$phpKey] = PHP_VERSION . " (" . PHP_SAPI . ")";
217 $software[$dbr->getSoftwareLink()] = $dbr->getServerInfo();
218
219 // Allow a hook to add/remove items.
220 wfRunHooks( 'SoftwareInfo', array( &$software ) );
221
222 $out = Xml::element(
223 'h2',
224 array( 'id' => 'mw-version-software' ),
225 wfMessage( 'version-software' )->text()
226 ) .
227 Xml::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ) ) .
228 "<tr>
229 <th>" . wfMessage( 'version-software-product' )->text() . "</th>
230 <th>" . wfMessage( 'version-software-version' )->text() . "</th>
231 </tr>\n";
232
233 foreach ( $software as $name => $version ) {
234 $out .= "<tr>
235 <td>" . $name . "</td>
236 <td dir=\"ltr\">" . $version . "</td>
237 </tr>\n";
238 }
239
240 return $out . Xml::closeElement( 'table' );
241 }
242
243 /**
244 * Return a string of the MediaWiki version with SVN revision if available.
245 *
246 * @param string $flags
247 * @return mixed
248 */
249 public static function getVersion( $flags = '' ) {
250 global $wgVersion, $IP;
251 wfProfileIn( __METHOD__ );
252
253 $gitInfo = self::getGitHeadSha1( $IP );
254 $svnInfo = self::getSvnInfo( $IP );
255 if ( !$svnInfo && !$gitInfo ) {
256 $version = $wgVersion;
257 } elseif ( $gitInfo && $flags === 'nodb' ) {
258 $shortSha1 = substr( $gitInfo, 0, 7 );
259 $version = "$wgVersion ($shortSha1)";
260 } elseif ( $gitInfo ) {
261 $shortSha1 = substr( $gitInfo, 0, 7 );
262 $shortSha1 = wfMessage( 'parentheses' )->params( $shortSha1 )->escaped();
263 $version = "$wgVersion $shortSha1";
264 } elseif ( $flags === 'nodb' ) {
265 $version = "$wgVersion (r{$svnInfo['checkout-rev']})";
266 } else {
267 $version = $wgVersion . ' ' .
268 wfMessage(
269 'version-svn-revision',
270 isset( $info['directory-rev'] ) ? $info['directory-rev'] : '',
271 $info['checkout-rev']
272 )->text();
273 }
274
275 wfProfileOut( __METHOD__ );
276
277 return $version;
278 }
279
280 /**
281 * Return a wikitext-formatted string of the MediaWiki version with a link to
282 * the SVN revision or the git SHA1 of head if available.
283 * Git is prefered over Svn
284 * The fallback is just $wgVersion
285 *
286 * @return mixed
287 */
288 public static function getVersionLinked() {
289 global $wgVersion;
290 wfProfileIn( __METHOD__ );
291
292 $gitVersion = self::getVersionLinkedGit();
293 if ( $gitVersion ) {
294 $v = $gitVersion;
295 } else {
296 $svnVersion = self::getVersionLinkedSvn();
297 if ( $svnVersion ) {
298 $v = $svnVersion;
299 } else {
300 $v = $wgVersion; // fallback
301 }
302 }
303
304 wfProfileOut( __METHOD__ );
305
306 return $v;
307 }
308
309 /**
310 * @return string Global wgVersion + a link to subversion revision of svn BASE
311 */
312 private static function getVersionLinkedSvn() {
313 global $IP;
314
315 $info = self::getSvnInfo( $IP );
316 if ( !isset( $info['checkout-rev'] ) ) {
317 return false;
318 }
319
320 $linkText = wfMessage(
321 'version-svn-revision',
322 isset( $info['directory-rev'] ) ? $info['directory-rev'] : '',
323 $info['checkout-rev']
324 )->text();
325
326 if ( isset( $info['viewvc-url'] ) ) {
327 $version = "[{$info['viewvc-url']} $linkText]";
328 } else {
329 $version = $linkText;
330 }
331
332 return self::getwgVersionLinked() . " $version";
333 }
334
335 /**
336 * @return string
337 */
338 private static function getwgVersionLinked() {
339 global $wgVersion;
340 $versionUrl = "";
341 if ( wfRunHooks( 'SpecialVersionVersionUrl', array( $wgVersion, &$versionUrl ) ) ) {
342 $versionParts = array();
343 preg_match( "/^(\d+\.\d+)/", $wgVersion, $versionParts );
344 $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
345 }
346
347 return "[$versionUrl $wgVersion]";
348 }
349
350 /**
351 * @since 1.22 Returns the HEAD date in addition to the sha1 and link
352 * @return bool|string Global wgVersion + HEAD sha1 stripped to the first 7 chars
353 * with link and date, or false on failure
354 */
355 private static function getVersionLinkedGit() {
356 global $IP, $wgLang;
357
358 $gitInfo = new GitInfo( $IP );
359 $headSHA1 = $gitInfo->getHeadSHA1();
360 if ( !$headSHA1 ) {
361 return false;
362 }
363
364 $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
365
366 $gitHeadUrl = $gitInfo->getHeadViewUrl();
367 if ( $gitHeadUrl !== false ) {
368 $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
369 }
370
371 $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
372 if ( $gitHeadCommitDate ) {
373 $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( $gitHeadCommitDate, true );
374 }
375
376 return self::getwgVersionLinked() . " $shortSHA1";
377 }
378
379 /**
380 * Returns an array with the base extension types.
381 * Type is stored as array key, the message as array value.
382 *
383 * TODO: ideally this would return all extension types.
384 *
385 * @since 1.17
386 *
387 * @return array
388 */
389 public static function getExtensionTypes() {
390 if ( self::$extensionTypes === false ) {
391 self::$extensionTypes = array(
392 'specialpage' => wfMessage( 'version-specialpages' )->text(),
393 'parserhook' => wfMessage( 'version-parserhooks' )->text(),
394 'variable' => wfMessage( 'version-variables' )->text(),
395 'media' => wfMessage( 'version-mediahandlers' )->text(),
396 'antispam' => wfMessage( 'version-antispam' )->text(),
397 'skin' => wfMessage( 'version-skins' )->text(),
398 'api' => wfMessage( 'version-api' )->text(),
399 'other' => wfMessage( 'version-other' )->text(),
400 );
401
402 wfRunHooks( 'ExtensionTypes', array( &self::$extensionTypes ) );
403 }
404
405 return self::$extensionTypes;
406 }
407
408 /**
409 * Returns the internationalized name for an extension type.
410 *
411 * @since 1.17
412 *
413 * @param string $type
414 *
415 * @return string
416 */
417 public static function getExtensionTypeName( $type ) {
418 $types = self::getExtensionTypes();
419
420 return isset( $types[$type] ) ? $types[$type] : $types['other'];
421 }
422
423 /**
424 * Generate wikitext showing the name, URL, author and description of each extension.
425 *
426 * @return string Wikitext
427 */
428 function getExtensionCredits() {
429 global $wgExtensionCredits;
430
431 if (
432 count( $wgExtensionCredits ) === 0 ||
433 // Skins are displayed separately, see getSkinCredits()
434 ( count( $wgExtensionCredits ) === 1 && isset( $wgExtensionCredits['skin'] ) )
435 ) {
436 return '';
437 }
438
439 $extensionTypes = self::getExtensionTypes();
440
441 $out = Xml::element(
442 'h2',
443 array( 'id' => 'mw-version-ext' ),
444 $this->msg( 'version-extensions' )->text()
445 ) .
446 Xml::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'sv-ext' ) );
447
448 // Make sure the 'other' type is set to an array.
449 if ( !array_key_exists( 'other', $wgExtensionCredits ) ) {
450 $wgExtensionCredits['other'] = array();
451 }
452
453 // Find all extensions that do not have a valid type and give them the type 'other'.
454 foreach ( $wgExtensionCredits as $type => $extensions ) {
455 if ( !array_key_exists( $type, $extensionTypes ) ) {
456 $wgExtensionCredits['other'] = array_merge( $wgExtensionCredits['other'], $extensions );
457 }
458 }
459
460 $this->firstExtOpened = false;
461 // Loop through the extension categories to display their extensions in the list.
462 foreach ( $extensionTypes as $type => $message ) {
463 // Skins have a separate section
464 if ( $type !== 'other' && $type !== 'skin' ) {
465 $out .= $this->getExtensionCategory( $type, $message );
466 }
467 }
468
469 // We want the 'other' type to be last in the list.
470 $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'] );
471
472 $out .= Xml::closeElement( 'table' );
473
474 return $out;
475 }
476
477 /**
478 * Generate wikitext showing the name, URL, author and description of each skin.
479 *
480 * @return string Wikitext
481 */
482 function getSkinCredits() {
483 global $wgExtensionCredits;
484 if ( !isset( $wgExtensionCredits['skin'] ) || count( $wgExtensionCredits['skin'] ) === 0 ) {
485 return '';
486 }
487
488 $out = Xml::element(
489 'h2',
490 array( 'id' => 'mw-version-skin' ),
491 $this->msg( 'version-skins' )->text()
492 ) .
493 Xml::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'sv-skin' ) );
494
495 $this->firstExtOpened = false;
496 $out .= $this->getExtensionCategory( 'skin', null );
497
498 $out .= Xml::closeElement( 'table' );
499
500 return $out;
501 }
502
503 /**
504 * Obtains a list of installed parser tags and the associated H2 header
505 *
506 * @return string HTML output
507 */
508 protected function getParserTags() {
509 global $wgParser;
510
511 $tags = $wgParser->getTags();
512
513 if ( count( $tags ) ) {
514 $out = Html::rawElement(
515 'h2',
516 array( 'class' => 'mw-headline' ),
517 Linker::makeExternalLink(
518 '//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
519 $this->msg( 'version-parser-extensiontags' )->parse(),
520 false /* msg()->parse() already escapes */
521 )
522 );
523
524 array_walk( $tags, function ( &$value ) {
525 $value = '&lt;' . htmlspecialchars( $value ) . '&gt;';
526 } );
527 $out .= $this->listToText( $tags );
528 } else {
529 $out = '';
530 }
531
532 return $out;
533 }
534
535 /**
536 * Obtains a list of installed parser function hooks and the associated H2 header
537 *
538 * @return string HTML output
539 */
540 protected function getParserFunctionHooks() {
541 global $wgParser;
542
543 $fhooks = $wgParser->getFunctionHooks();
544 if ( count( $fhooks ) ) {
545 $out = Html::rawElement( 'h2', array( 'class' => 'mw-headline' ), Linker::makeExternalLink(
546 '//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
547 $this->msg( 'version-parser-function-hooks' )->parse(),
548 false /* msg()->parse() already escapes */
549 ) );
550
551 $out .= $this->listToText( $fhooks );
552 } else {
553 $out = '';
554 }
555
556 return $out;
557 }
558
559 /**
560 * Creates and returns the HTML for a single extension category.
561 *
562 * @since 1.17
563 *
564 * @param string $type
565 * @param string $message
566 *
567 * @return string
568 */
569 protected function getExtensionCategory( $type, $message ) {
570 global $wgExtensionCredits;
571
572 $out = '';
573
574 if ( array_key_exists( $type, $wgExtensionCredits ) && count( $wgExtensionCredits[$type] ) > 0 ) {
575 $out .= $this->openExtType( $message, 'credits-' . $type );
576
577 usort( $wgExtensionCredits[$type], array( $this, 'compare' ) );
578
579 foreach ( $wgExtensionCredits[$type] as $extension ) {
580 $out .= $this->getCreditsForExtension( $extension );
581 }
582 }
583
584 return $out;
585 }
586
587 /**
588 * Callback to sort extensions by type.
589 * @param array $a
590 * @param array $b
591 * @return int
592 */
593 function compare( $a, $b ) {
594 if ( $a['name'] === $b['name'] ) {
595 return 0;
596 } else {
597 return $this->getLanguage()->lc( $a['name'] ) > $this->getLanguage()->lc( $b['name'] )
598 ? 1
599 : -1;
600 }
601 }
602
603 /**
604 * Creates and formats a version line for a single extension.
605 *
606 * Information for five columns will be created. Parameters required in the
607 * $extension array for part rendering are indicated in ()
608 * - The name of (name), and URL link to (url), the extension
609 * - Official version number (version) and if available version control system
610 * revision (path), link, and date
611 * - If available the short name of the license (license-name) and a linke
612 * to ((LICENSE)|(COPYING))(\.txt)? if it exists.
613 * - Description of extension (descriptionmsg or description)
614 * - List of authors (author) and link to a ((AUTHORS)|(CREDITS))(\.txt)? file if it exists
615 *
616 * @param array $extension
617 *
618 * @return string Raw HTML
619 */
620 function getCreditsForExtension( array $extension ) {
621 $out = $this->getOutput();
622
623 // We must obtain the information for all the bits and pieces!
624 // ... such as extension names and links
625 if ( isset( $extension['namemsg'] ) ) {
626 // Localized name of extension
627 $extensionName = $this->msg( $extension['namemsg'] )->text();
628 } elseif ( isset( $extension['name'] ) ) {
629 // Non localized version
630 $extensionName = $extension['name'];
631 } else {
632 $extensionName = $this->msg( 'version-no-ext-name' )->text();
633 }
634
635 if ( isset( $extension['url'] ) ) {
636 $extensionNameLink = Linker::makeExternalLink(
637 $extension['url'],
638 $extensionName,
639 true,
640 '',
641 array( 'class' => 'mw-version-ext-name' )
642 );
643 } else {
644 $extensionNameLink = $extensionName;
645 }
646
647 // ... and the version information
648 // If the extension path is set we will check that directory for GIT and SVN
649 // metadata in an attempt to extract date and vcs commit metadata.
650 $canonicalVersion = '&ndash;';
651 $extensionPath = null;
652 $vcsVersion = null;
653 $vcsLink = null;
654 $vcsDate = null;
655
656 if ( isset( $extension['version'] ) ) {
657 $canonicalVersion = $out->parseInline( $extension['version'] );
658 }
659
660 if ( isset( $extension['path'] ) ) {
661 global $IP;
662 $extensionPath = dirname( $extension['path'] );
663 if ( $this->coreId == '' ) {
664 wfDebug( 'Looking up core head id' );
665 $coreHeadSHA1 = self::getGitHeadSha1( $IP );
666 if ( $coreHeadSHA1 ) {
667 $this->coreId = $coreHeadSHA1;
668 } else {
669 $svnInfo = self::getSvnInfo( $IP );
670 if ( $svnInfo !== false ) {
671 $this->coreId = $svnInfo['checkout-rev'];
672 }
673 }
674 }
675 $cache = wfGetCache( CACHE_ANYTHING );
676 $memcKey = wfMemcKey( 'specialversion-ext-version-text', $extension['path'], $this->coreId );
677 list( $vcsVersion, $vcsLink, $vcsDate ) = $cache->get( $memcKey );
678
679 if ( !$vcsVersion ) {
680 wfDebug( "Getting VCS info for extension $extensionName" );
681 $gitInfo = new GitInfo( $extensionPath );
682 $vcsVersion = $gitInfo->getHeadSHA1();
683 if ( $vcsVersion !== false ) {
684 $vcsVersion = substr( $vcsVersion, 0, 7 );
685 $vcsLink = $gitInfo->getHeadViewUrl();
686 $vcsDate = $gitInfo->getHeadCommitDate();
687 } else {
688 $svnInfo = self::getSvnInfo( $extensionPath );
689 if ( $svnInfo !== false ) {
690 $vcsVersion = $this->msg( 'version-svn-revision', $svnInfo['checkout-rev'] )->text();
691 $vcsLink = isset( $svnInfo['viewvc-url'] ) ? $svnInfo['viewvc-url'] : '';
692 }
693 }
694 $cache->set( $memcKey, array( $vcsVersion, $vcsLink, $vcsDate ), 60 * 60 * 24 );
695 } else {
696 wfDebug( "Pulled VCS info for extension $extensionName from cache" );
697 }
698 }
699
700 $versionString = Html::rawElement(
701 'span',
702 array( 'class' => 'mw-version-ext-version' ),
703 $canonicalVersion
704 );
705
706 if ( $vcsVersion ) {
707 if ( $vcsLink ) {
708 $vcsVerString = Linker::makeExternalLink(
709 $vcsLink,
710 $this->msg( 'version-version', $vcsVersion ),
711 true,
712 '',
713 array( 'class' => 'mw-version-ext-vcs-version' )
714 );
715 } else {
716 $vcsVerString = Html::element( 'span',
717 array( 'class' => 'mw-version-ext-vcs-version' ),
718 "({$vcsVersion})"
719 );
720 }
721 $versionString .= " {$vcsVerString}";
722
723 if ( $vcsDate ) {
724 $vcsTimeString = Html::element( 'span',
725 array( 'class' => 'mw-version-ext-vcs-timestamp' ),
726 $this->getLanguage()->timeanddate( $vcsDate )
727 );
728 $versionString .= " {$vcsTimeString}";
729 }
730 $versionString = Html::rawElement( 'span',
731 array( 'class' => 'mw-version-ext-meta-version' ),
732 $versionString
733 );
734 }
735
736 // ... and license information; if a license file exists we
737 // will link to it
738 $licenseLink = '';
739 if ( isset( $extension['license-name'] ) ) {
740 $licenseLink = Linker::link(
741 $this->getPageTitle( 'License/' . $extensionName ),
742 $out->parseInline( $extension['license-name'] ),
743 array( 'class' => 'mw-version-ext-license' )
744 );
745 } elseif ( $this->getExtLicenseFileName( $extensionPath ) ) {
746 $licenseLink = Linker::link(
747 $this->getPageTitle( 'License/' . $extensionName ),
748 $this->msg( 'version-ext-license' ),
749 array( 'class' => 'mw-version-ext-license' )
750 );
751 }
752
753 // ... and generate the description; which can be a parameterized l10n message
754 // in the form array( <msgname>, <parameter>, <parameter>... ) or just a straight
755 // up string
756 if ( isset( $extension['descriptionmsg'] ) ) {
757 // Localized description of extension
758 $descriptionMsg = $extension['descriptionmsg'];
759
760 if ( is_array( $descriptionMsg ) ) {
761 $descriptionMsgKey = $descriptionMsg[0]; // Get the message key
762 array_shift( $descriptionMsg ); // Shift out the message key to get the parameters only
763 array_map( "htmlspecialchars", $descriptionMsg ); // For sanity
764 $description = $this->msg( $descriptionMsgKey, $descriptionMsg )->text();
765 } else {
766 $description = $this->msg( $descriptionMsg )->text();
767 }
768 } elseif ( isset( $extension['description'] ) ) {
769 // Non localized version
770 $description = $extension['description'];
771 } else {
772 $description = '';
773 }
774 $description = $out->parseInline( $description );
775
776 // ... now get the authors for this extension
777 $authors = isset( $extension['author'] ) ? $extension['author'] : array();
778 $authors = $this->listAuthors( $authors, $extensionName, $extensionPath );
779
780 // Finally! Create the table
781 $html = Html::openElement( 'tr', array(
782 'class' => 'mw-version-ext',
783 'id' => "mw-version-ext-{$extensionName}"
784 )
785 );
786
787 $html .= Html::rawElement( 'td', array(), $extensionNameLink );
788 $html .= Html::rawElement( 'td', array(), $versionString );
789 $html .= Html::rawElement( 'td', array(), $licenseLink );
790 $html .= Html::rawElement( 'td', array( 'class' => 'mw-version-ext-description' ), $description );
791 $html .= Html::rawElement( 'td', array( 'class' => 'mw-version-ext-authors' ), $authors );
792
793 $html .= Html::closeElement( 'td' );
794
795 return $html;
796 }
797
798 /**
799 * Generate wikitext showing hooks in $wgHooks.
800 *
801 * @return string Wikitext
802 */
803 private function getWgHooks() {
804 global $wgSpecialVersionShowHooks, $wgHooks;
805
806 if ( $wgSpecialVersionShowHooks && count( $wgHooks ) ) {
807 $myWgHooks = $wgHooks;
808 ksort( $myWgHooks );
809
810 $ret = array();
811 $ret[] = '== {{int:version-hooks}} ==';
812 $ret[] = Html::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-hooks' ) );
813 $ret[] = Html::openElement( 'tr' );
814 $ret[] = Html::element( 'th', array(), $this->msg( 'version-hook-name' )->text() );
815 $ret[] = Html::element( 'th', array(), $this->msg( 'version-hook-subscribedby' )->text() );
816 $ret[] = Html::closeElement( 'tr' );
817
818 foreach ( $myWgHooks as $hook => $hooks ) {
819 $ret[] = Html::openElement( 'tr' );
820 $ret[] = Html::element( 'td', array(), $hook );
821 $ret[] = Html::element( 'td', array(), $this->listToText( $hooks ) );
822 $ret[] = Html::closeElement( 'tr' );
823 }
824
825 $ret[] = Html::closeElement( 'table' );
826
827 return implode( "\n", $ret );
828 } else {
829 return '';
830 }
831 }
832
833 private function openExtType( $text = null, $name = null ) {
834 $out = '';
835
836 $opt = array( 'colspan' => 5 );
837 if ( $this->firstExtOpened ) {
838 // Insert a spacing line
839 $out .= Html::rawElement( 'tr', array( 'class' => 'sv-space' ),
840 Html::element( 'td', $opt )
841 );
842 }
843 $this->firstExtOpened = true;
844
845 if ( $name ) {
846 $opt['id'] = "sv-$name";
847 }
848
849 if ( $text !== null ) {
850 $out .= Html::rawElement( 'tr', array(),
851 Html::element( 'th', $opt, $text )
852 );
853 }
854
855 $firstHeadingMsg = ( $name === 'credits-skin' )
856 ? 'version-skin-colheader-name'
857 : 'version-ext-colheader-name';
858 $out .= Html::openElement( 'tr' );
859 $out .= Html::element( 'th', array( 'class' => 'mw-version-ext-col-label' ),
860 $this->msg( $firstHeadingMsg )->text() );
861 $out .= Html::element( 'th', array( 'class' => 'mw-version-ext-col-label' ),
862 $this->msg( 'version-ext-colheader-version' )->text() );
863 $out .= Html::element( 'th', array( 'class' => 'mw-version-ext-col-label' ),
864 $this->msg( 'version-ext-colheader-license' )->text() );
865 $out .= Html::element( 'th', array( 'class' => 'mw-version-ext-col-label' ),
866 $this->msg( 'version-ext-colheader-description' )->text() );
867 $out .= Html::element( 'th', array( 'class' => 'mw-version-ext-col-label' ),
868 $this->msg( 'version-ext-colheader-credits' )->text() );
869 $out .= Html::closeElement( 'tr' );
870
871 return $out;
872 }
873
874 /**
875 * Get information about client's IP address.
876 *
877 * @return string HTML fragment
878 */
879 private function IPInfo() {
880 $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
881
882 return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
883 }
884
885 /**
886 * Return a formatted unsorted list of authors
887 *
888 * 'And Others'
889 * If an item in the $authors array is '...' it is assumed to indicate an
890 * 'and others' string which will then be linked to an ((AUTHORS)|(CREDITS))(\.txt)?
891 * file if it exists in $dir.
892 *
893 * Similarly an entry ending with ' ...]' is assumed to be a link to an
894 * 'and others' page.
895 *
896 * If no '...' string variant is found, but an authors file is found an
897 * 'and others' will be added to the end of the credits.
898 *
899 * @param string|array $authors
900 * @param string $extName Name of the extension for link creation
901 * @param string $extDir Path to the extension root directory
902 *
903 * @return string HTML fragment
904 */
905 function listAuthors( $authors, $extName, $extDir ) {
906 $hasOthers = false;
907
908 $list = array();
909 foreach ( (array)$authors as $item ) {
910 if ( $item == '...' ) {
911 $hasOthers = true;
912
913 if ( $this->getExtAuthorsFileName( $extDir ) ) {
914 $text = Linker::link(
915 $this->getPageTitle( "Credits/$extName" ),
916 $this->msg( 'version-poweredby-others' )->text()
917 );
918 } else {
919 $text = $this->msg( 'version-poweredby-others' )->text();
920 }
921 $list[] = $text;
922 } elseif ( substr( $item, -5 ) == ' ...]' ) {
923 $hasOthers = true;
924 $list[] = $this->getOutput()->parseInline(
925 substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
926 );
927 } else {
928 $list[] = $this->getOutput()->parseInline( $item );
929 }
930 }
931
932 if ( !$hasOthers && $this->getExtAuthorsFileName( $extDir ) ) {
933 $list[] = $text = Linker::link(
934 $this->getPageTitle( "Credits/$extName" ),
935 $this->msg( 'version-poweredby-others' )->text()
936 );
937 }
938
939 return $this->listToText( $list, false );
940 }
941
942 /**
943 * Obtains the full path of an extensions authors or credits file if
944 * one exists.
945 *
946 * @param string $extDir Path to the extensions root directory
947 *
948 * @since 1.23
949 *
950 * @return bool|string False if no such file exists, otherwise returns
951 * a path to it.
952 */
953 public static function getExtAuthorsFileName( $extDir ) {
954 if ( !$extDir ) {
955 return false;
956 }
957
958 foreach ( scandir( $extDir ) as $file ) {
959 $fullPath = $extDir . DIRECTORY_SEPARATOR . $file;
960 if ( preg_match( '/^((AUTHORS)|(CREDITS))(\.txt)?$/', $file ) &&
961 is_readable( $fullPath ) &&
962 is_file( $fullPath )
963 ) {
964 return $fullPath;
965 }
966 }
967
968 return false;
969 }
970
971 /**
972 * Obtains the full path of an extensions copying or license file if
973 * one exists.
974 *
975 * @param string $extDir Path to the extensions root directory
976 *
977 * @since 1.23
978 *
979 * @return bool|string False if no such file exists, otherwise returns
980 * a path to it.
981 */
982 public static function getExtLicenseFileName( $extDir ) {
983 if ( !$extDir ) {
984 return false;
985 }
986
987 foreach ( scandir( $extDir ) as $file ) {
988 $fullPath = $extDir . DIRECTORY_SEPARATOR . $file;
989 if ( preg_match( '/^((COPYING)|(LICENSE))(\.txt)?$/', $file ) &&
990 is_readable( $fullPath ) &&
991 is_file( $fullPath )
992 ) {
993 return $fullPath;
994 }
995 }
996
997 return false;
998 }
999
1000 /**
1001 * Convert an array of items into a list for display.
1002 *
1003 * @param array $list List of elements to display
1004 * @param bool $sort Whether to sort the items in $list
1005 *
1006 * @return string
1007 */
1008 function listToText( $list, $sort = true ) {
1009 if ( !count( $list ) ) {
1010 return '';
1011 }
1012 if ( $sort ) {
1013 sort( $list );
1014 }
1015
1016 return $this->getLanguage()
1017 ->listToText( array_map( array( __CLASS__, 'arrayToString' ), $list ) );
1018 }
1019
1020 /**
1021 * Convert an array or object to a string for display.
1022 *
1023 * @param mixed $list Will convert an array to string if given and return
1024 * the paramater unaltered otherwise
1025 *
1026 * @return mixed
1027 */
1028 public static function arrayToString( $list ) {
1029 if ( is_array( $list ) && count( $list ) == 1 ) {
1030 $list = $list[0];
1031 }
1032 if ( is_object( $list ) ) {
1033 $class = wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1034
1035 return $class;
1036 } elseif ( !is_array( $list ) ) {
1037 return $list;
1038 } else {
1039 if ( is_object( $list[0] ) ) {
1040 $class = get_class( $list[0] );
1041 } else {
1042 $class = $list[0];
1043 }
1044
1045 return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1046 }
1047 }
1048
1049 /**
1050 * Get an associative array of information about a given path, from its .svn
1051 * subdirectory. Returns false on error, such as if the directory was not
1052 * checked out with subversion.
1053 *
1054 * Returned keys are:
1055 * Required:
1056 * checkout-rev The revision which was checked out
1057 * Optional:
1058 * directory-rev The revision when the directory was last modified
1059 * url The subversion URL of the directory
1060 * repo-url The base URL of the repository
1061 * viewvc-url A ViewVC URL pointing to the checked-out revision
1062 * @param string $dir
1063 * @return array|bool
1064 */
1065 public static function getSvnInfo( $dir ) {
1066 // http://svnbook.red-bean.com/nightly/en/svn.developer.insidewc.html
1067 $entries = $dir . '/.svn/entries';
1068
1069 if ( !file_exists( $entries ) ) {
1070 return false;
1071 }
1072
1073 $lines = file( $entries );
1074 if ( !count( $lines ) ) {
1075 return false;
1076 }
1077
1078 // check if file is xml (subversion release <= 1.3) or not (subversion release = 1.4)
1079 if ( preg_match( '/^<\?xml/', $lines[0] ) ) {
1080 // subversion is release <= 1.3
1081 if ( !function_exists( 'simplexml_load_file' ) ) {
1082 // We could fall back to expat... YUCK
1083 return false;
1084 }
1085
1086 // SimpleXml whines about the xmlns...
1087 wfSuppressWarnings();
1088 $xml = simplexml_load_file( $entries );
1089 wfRestoreWarnings();
1090
1091 if ( $xml ) {
1092 foreach ( $xml->entry as $entry ) {
1093 if ( $xml->entry[0]['name'] == '' ) {
1094 // The directory entry should always have a revision marker.
1095 if ( $entry['revision'] ) {
1096 return array( 'checkout-rev' => intval( $entry['revision'] ) );
1097 }
1098 }
1099 }
1100 }
1101
1102 return false;
1103 }
1104
1105 // Subversion is release 1.4 or above.
1106 if ( count( $lines ) < 11 ) {
1107 return false;
1108 }
1109
1110 $info = array(
1111 'checkout-rev' => intval( trim( $lines[3] ) ),
1112 'url' => trim( $lines[4] ),
1113 'repo-url' => trim( $lines[5] ),
1114 'directory-rev' => intval( trim( $lines[10] ) )
1115 );
1116
1117 if ( isset( self::$viewvcUrls[$info['repo-url']] ) ) {
1118 $viewvc = str_replace(
1119 $info['repo-url'],
1120 self::$viewvcUrls[$info['repo-url']],
1121 $info['url']
1122 );
1123
1124 $viewvc .= '/?pathrev=';
1125 $viewvc .= urlencode( $info['checkout-rev'] );
1126 $info['viewvc-url'] = $viewvc;
1127 }
1128
1129 return $info;
1130 }
1131
1132 /**
1133 * Retrieve the revision number of a Subversion working directory.
1134 *
1135 * @param string $dir Directory of the svn checkout
1136 *
1137 * @return int Revision number
1138 */
1139 public static function getSvnRevision( $dir ) {
1140 $info = self::getSvnInfo( $dir );
1141
1142 if ( $info === false ) {
1143 return false;
1144 } elseif ( isset( $info['checkout-rev'] ) ) {
1145 return $info['checkout-rev'];
1146 } else {
1147 return false;
1148 }
1149 }
1150
1151 /**
1152 * @param string $dir Directory of the git checkout
1153 * @return bool|string Sha1 of commit HEAD points to
1154 */
1155 public static function getGitHeadSha1( $dir ) {
1156 $repo = new GitInfo( $dir );
1157
1158 return $repo->getHeadSHA1();
1159 }
1160
1161 /**
1162 * @param string $dir Directory of the git checkout
1163 * @return bool|string Branch currently checked out
1164 */
1165 public static function getGitCurrentBranch( $dir ) {
1166 $repo = new GitInfo( $dir );
1167 return $repo->getCurrentBranch();
1168 }
1169
1170 /**
1171 * Get the list of entry points and their URLs
1172 * @return string Wikitext
1173 */
1174 public function getEntryPointInfo() {
1175 global $wgArticlePath, $wgScriptPath;
1176 $scriptPath = $wgScriptPath ? $wgScriptPath : "/";
1177 $entryPoints = array(
1178 'version-entrypoints-articlepath' => $wgArticlePath,
1179 'version-entrypoints-scriptpath' => $scriptPath,
1180 'version-entrypoints-index-php' => wfScript( 'index' ),
1181 'version-entrypoints-api-php' => wfScript( 'api' ),
1182 'version-entrypoints-load-php' => wfScript( 'load' ),
1183 );
1184
1185 $language = $this->getLanguage();
1186 $thAttribures = array(
1187 'dir' => $language->getDir(),
1188 'lang' => $language->getCode()
1189 );
1190 $out = Html::element(
1191 'h2',
1192 array( 'id' => 'mw-version-entrypoints' ),
1193 $this->msg( 'version-entrypoints' )->text()
1194 ) .
1195 Html::openElement( 'table',
1196 array(
1197 'class' => 'wikitable plainlinks',
1198 'id' => 'mw-version-entrypoints-table',
1199 'dir' => 'ltr',
1200 'lang' => 'en'
1201 )
1202 ) .
1203 Html::openElement( 'tr' ) .
1204 Html::element(
1205 'th',
1206 $thAttribures,
1207 $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1208 ) .
1209 Html::element(
1210 'th',
1211 $thAttribures,
1212 $this->msg( 'version-entrypoints-header-url' )->text()
1213 ) .
1214 Html::closeElement( 'tr' );
1215
1216 foreach ( $entryPoints as $message => $value ) {
1217 $url = wfExpandUrl( $value, PROTO_RELATIVE );
1218 $out .= Html::openElement( 'tr' ) .
1219 // ->text() looks like it should be ->parse(), but this function
1220 // returns wikitext, not HTML, boo
1221 Html::rawElement( 'td', array(), $this->msg( $message )->text() ) .
1222 Html::rawElement( 'td', array(), Html::rawElement( 'code', array(), "[$url $value]" ) ) .
1223 Html::closeElement( 'tr' );
1224 }
1225
1226 $out .= Html::closeElement( 'table' );
1227
1228 return $out;
1229 }
1230
1231 protected function getGroupName() {
1232 return 'wiki';
1233 }
1234 }