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