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