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