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