700d10fe682a55fde2491458513e9d0dc5c591b3
[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 # Doesn't work at the time of writing but maybe some day:
41 'https://svn.wikimedia.org/viewvc/mediawiki' => 'http://svn.wikimedia.org/viewvc/mediawiki',
42 );
43
44 public function __construct(){
45 parent::__construct( 'Version' );
46 }
47
48 /**
49 * main()
50 */
51 public function execute( $par ) {
52 global $wgOut, $wgSpecialVersionShowHooks, $wgContLang;
53
54 $this->setHeaders();
55 $this->outputHeader();
56
57 $wgOut->addHTML( Xml::openElement( 'div',
58 array( 'dir' => $wgContLang->getDir() ) ) );
59 $text =
60 $this->getMediaWikiCredits() .
61 $this->softwareInformation() .
62 $this->getExtensionCredits();
63 if ( $wgSpecialVersionShowHooks ) {
64 $text .= $this->getWgHooks();
65 }
66
67 $wgOut->addWikiText( $text );
68 $wgOut->addHTML( $this->IPInfo() );
69 $wgOut->addHTML( '</div>' );
70 }
71
72 /**
73 * Returns wiki text showing the license information.
74 *
75 * @return string
76 */
77 private static function getMediaWikiCredits() {
78 $ret = Xml::element( 'h2', array( 'id' => 'mw-version-license' ), wfMsg( 'version-license' ) );
79
80 // This text is always left-to-right.
81 $ret .= '<div>';
82 $ret .= "__NOTOC__
83 " . self::getCopyrightAndAuthorList() . "\n
84 " . wfMsg( 'version-license-info' );
85 $ret .= '</div>';
86
87 return str_replace( "\t\t", '', $ret ) . "\n";
88 }
89
90 /**
91 * Get the "Mediawiki is copyright 2001-20xx by lots of cool guys" text
92 *
93 * @return String
94 */
95 public static function getCopyrightAndAuthorList() {
96 global $wgLang;
97
98 $authorList = array( 'Magnus Manske', 'Brion Vibber', 'Lee Daniel Crocker',
99 'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
100 'Niklas Laxström', 'Domas Mituzas', 'Rob Church', 'Yuri Astrakhan',
101 'Aryeh Gregor', 'Aaron Schulz', 'Andrew Garrett', 'Raimond Spekking',
102 'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
103 wfMsg( 'version-poweredby-others' )
104 );
105
106 return wfMsg( 'version-poweredby-credits', date( 'Y' ),
107 $wgLang->listToText( $authorList ) );
108 }
109
110 /**
111 * Returns wiki text showing the third party software versions (apache, php, mysql).
112 *
113 * @return string
114 */
115 static function softwareInformation() {
116 $dbr = wfGetDB( DB_SLAVE );
117
118 // Put the software in an array of form 'name' => 'version'. All messages should
119 // be loaded here, so feel free to use wfMsg*() in the 'name'. Raw HTML or wikimarkup
120 // can be used.
121 $software = array();
122 $software['[http://www.mediawiki.org/ MediaWiki]'] = self::getVersionLinked();
123 $software['[http://www.php.net/ PHP]'] = phpversion() . " (" . php_sapi_name() . ")";
124 $software[$dbr->getSoftwareLink()] = $dbr->getServerInfo();
125
126 // Allow a hook to add/remove items.
127 wfRunHooks( 'SoftwareInfo', array( &$software ) );
128
129 $out = Xml::element( 'h2', array( 'id' => 'mw-version-software' ), wfMsg( 'version-software' ) ) .
130 Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-software' ) ) .
131 "<tr>
132 <th>" . wfMsg( 'version-software-product' ) . "</th>
133 <th>" . wfMsg( 'version-software-version' ) . "</th>
134 </tr>\n";
135
136 foreach( $software as $name => $version ) {
137 $out .= "<tr>
138 <td>" . $name . "</td>
139 <td>" . $version . "</td>
140 </tr>\n";
141 }
142
143 return $out . Xml::closeElement( 'table' );
144 }
145
146 /**
147 * Return a string of the MediaWiki version with SVN revision if available.
148 *
149 * @return mixed
150 */
151 public static function getVersion( $flags = '' ) {
152 global $wgVersion, $IP;
153 wfProfileIn( __METHOD__ );
154
155 $info = self::getSvnInfo( $IP );
156 if ( !$info ) {
157 $version = $wgVersion;
158 } elseif( $flags === 'nodb' ) {
159 $version = "$wgVersion (r{$info['checkout-rev']})";
160 } else {
161 $version = $wgVersion . ' ' .
162 wfMsg(
163 'version-svn-revision',
164 isset( $info['directory-rev'] ) ? $info['directory-rev'] : '',
165 $info['checkout-rev']
166 );
167 }
168
169 wfProfileOut( __METHOD__ );
170 return $version;
171 }
172
173 /**
174 * Return a wikitext-formatted string of the MediaWiki version with a link to
175 * the SVN revision if available.
176 *
177 * @return mixed
178 */
179 public static function getVersionLinked() {
180 global $wgVersion, $IP;
181 wfProfileIn( __METHOD__ );
182
183 $info = self::getSvnInfo( $IP );
184
185 if ( isset( $info['checkout-rev'] ) ) {
186 $linkText = wfMsg(
187 'version-svn-revision',
188 isset( $info['directory-rev'] ) ? $info['directory-rev'] : '',
189 $info['checkout-rev']
190 );
191
192 if ( isset( $info['viewvc-url'] ) ) {
193 $version = "$wgVersion [{$info['viewvc-url']} $linkText]";
194 } else {
195 $version = "$wgVersion $linkText";
196 }
197 } else {
198 $version = $wgVersion;
199 }
200
201 wfProfileOut( __METHOD__ );
202 return $version;
203 }
204
205 /**
206 * Returns an array with the base extension types.
207 * Type is stored as array key, the message as array value.
208 *
209 * TODO: ideally this would return all extension types, including
210 * those added by SpecialVersionExtensionTypes. This is not possible
211 * since this hook is passing along $this though.
212 *
213 * @since 1.17
214 *
215 * @return array
216 */
217 public static function getExtensionTypes() {
218 if ( self::$extensionTypes === false ) {
219 self::$extensionTypes = array(
220 'specialpage' => wfMsg( 'version-specialpages' ),
221 'parserhook' => wfMsg( 'version-parserhooks' ),
222 'variable' => wfMsg( 'version-variables' ),
223 'media' => wfMsg( 'version-mediahandlers' ),
224 'other' => wfMsg( 'version-other' ),
225 );
226
227 wfRunHooks( 'ExtensionTypes', array( &self::$extensionTypes ) );
228 }
229
230 return self::$extensionTypes;
231 }
232
233 /**
234 * Returns the internationalized name for an extension type.
235 *
236 * @since 1.17
237 *
238 * @param $type String
239 *
240 * @return string
241 */
242 public static function getExtensionTypeName( $type ) {
243 $types = self::getExtensionTypes();
244 return $types[$type];
245 }
246
247 /**
248 * Generate wikitext showing extensions name, URL, author and description.
249 *
250 * @return String: Wikitext
251 */
252 function getExtensionCredits() {
253 global $wgExtensionCredits, $wgExtensionFunctions, $wgParser, $wgSkinExtensionFunctions;
254
255 if ( !count( $wgExtensionCredits ) && !count( $wgExtensionFunctions ) && !count( $wgSkinExtensionFunctions ) ) {
256 return '';
257 }
258
259 $extensionTypes = self::getExtensionTypes();
260
261 /**
262 * @deprecated as of 1.17, use hook ExtensionTypes instead.
263 */
264 wfRunHooks( 'SpecialVersionExtensionTypes', array( &$this, &$extensionTypes ) );
265
266 $out = Xml::element( 'h2', array( 'id' => 'mw-version-ext' ), wfMsg( 'version-extensions' ) ) .
267 Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-ext' ) );
268
269 // Make sure the 'other' type is set to an array.
270 if ( !array_key_exists( 'other', $wgExtensionCredits ) ) {
271 $wgExtensionCredits['other'] = array();
272 }
273
274 // Find all extensions that do not have a valid type and give them the type 'other'.
275 foreach ( $wgExtensionCredits as $type => $extensions ) {
276 if ( !array_key_exists( $type, $extensionTypes ) ) {
277 $wgExtensionCredits['other'] = array_merge( $wgExtensionCredits['other'], $extensions );
278 }
279 }
280
281 // Loop through the extension categories to display their extensions in the list.
282 foreach ( $extensionTypes as $type => $message ) {
283 if ( $type != 'other' ) {
284 $out .= $this->getExtensionCategory( $type, $message );
285 }
286 }
287
288 // We want the 'other' type to be last in the list.
289 $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'] );
290
291 if ( count( $wgExtensionFunctions ) ) {
292 $out .= $this->openExtType( wfMsg( 'version-extension-functions' ), 'extension-functions' );
293 $out .= '<tr><td colspan="4">' . $this->listToText( $wgExtensionFunctions ) . "</td></tr>\n";
294 }
295
296 if ( $cnt = count( $tags = $wgParser->getTags() ) ) {
297 for ( $i = 0; $i < $cnt; ++$i ) {
298 $tags[$i] = "&lt;{$tags[$i]}&gt;";
299 }
300 $out .= $this->openExtType( wfMsg( 'version-parser-extensiontags' ), 'parser-tags' );
301 $out .= '<tr><td colspan="4">' . $this->listToText( $tags ). "</td></tr>\n";
302 }
303
304 if( count( $fhooks = $wgParser->getFunctionHooks() ) ) {
305 $out .= $this->openExtType( wfMsg( 'version-parser-function-hooks' ), 'parser-function-hooks' );
306 $out .= '<tr><td colspan="4">' . $this->listToText( $fhooks ) . "</td></tr>\n";
307 }
308
309 if ( count( $wgSkinExtensionFunctions ) ) {
310 $out .= $this->openExtType( wfMsg( 'version-skin-extension-functions' ), 'skin-extension-functions' );
311 $out .= '<tr><td colspan="4">' . $this->listToText( $wgSkinExtensionFunctions ) . "</td></tr>\n";
312 }
313
314 $out .= Xml::closeElement( 'table' );
315
316 return $out;
317 }
318
319 /**
320 * Creates and returns the HTML for a single extension category.
321 *
322 * @since 1.17
323 *
324 * @param $type String
325 * @param $message String
326 *
327 * @return string
328 */
329 protected function getExtensionCategory( $type, $message ) {
330 global $wgExtensionCredits;
331
332 $out = '';
333
334 if ( array_key_exists( $type, $wgExtensionCredits ) && count( $wgExtensionCredits[$type] ) > 0 ) {
335 $out .= $this->openExtType( $message, 'credits-' . $type );
336
337 usort( $wgExtensionCredits[$type], array( $this, 'compare' ) );
338
339 foreach ( $wgExtensionCredits[$type] as $extension ) {
340 $out .= $this->getCreditsForExtension( $extension );
341 }
342 }
343
344 return $out;
345 }
346
347 /**
348 * Callback to sort extensions by type.
349 */
350 function compare( $a, $b ) {
351 global $wgLang;
352 if( $a['name'] === $b['name'] ) {
353 return 0;
354 } else {
355 return $wgLang->lc( $a['name'] ) > $wgLang->lc( $b['name'] )
356 ? 1
357 : -1;
358 }
359 }
360
361 /**
362 * Creates and formats the creidts for a single extension and returns this.
363 *
364 * @param $extension Array
365 *
366 * @return string
367 */
368 function getCreditsForExtension( array $extension ) {
369 $name = isset( $extension['name'] ) ? $extension['name'] : '[no name]';
370
371 if ( isset( $extension['path'] ) ) {
372 $svnInfo = self::getSvnInfo( dirname($extension['path']) );
373 $directoryRev = isset( $svnInfo['directory-rev'] ) ? $svnInfo['directory-rev'] : null;
374 $checkoutRev = isset( $svnInfo['checkout-rev'] ) ? $svnInfo['checkout-rev'] : null;
375 $viewvcUrl = isset( $svnInfo['viewvc-url'] ) ? $svnInfo['viewvc-url'] : null;
376 } else {
377 $directoryRev = null;
378 $checkoutRev = null;
379 $viewvcUrl = null;
380 }
381
382 # Make main link (or just the name if there is no URL).
383 if ( isset( $extension['url'] ) ) {
384 $mainLink = "[{$extension['url']} $name]";
385 } else {
386 $mainLink = $name;
387 }
388
389 if ( isset( $extension['version'] ) ) {
390 $versionText = '<span class="mw-version-ext-version">' .
391 wfMsg( 'version-version', $extension['version'] ) .
392 '</span>';
393 } else {
394 $versionText = '';
395 }
396
397 # Make subversion text/link.
398 if ( $checkoutRev ) {
399 $svnText = wfMsg( 'version-svn-revision', $directoryRev, $checkoutRev );
400 $svnText = isset( $viewvcUrl ) ? "[$viewvcUrl $svnText]" : $svnText;
401 } else {
402 $svnText = false;
403 }
404
405 # Make description text.
406 $description = isset ( $extension['description'] ) ? $extension['description'] : '';
407
408 if( isset ( $extension['descriptionmsg'] ) ) {
409 # Look for a localized description.
410 $descriptionMsg = $extension['descriptionmsg'];
411
412 if( is_array( $descriptionMsg ) ) {
413 $descriptionMsgKey = $descriptionMsg[0]; // Get the message key
414 array_shift( $descriptionMsg ); // Shift out the message key to get the parameters only
415 array_map( "htmlspecialchars", $descriptionMsg ); // For sanity
416 $msg = wfMsg( $descriptionMsgKey, $descriptionMsg );
417 } else {
418 $msg = wfMsg( $descriptionMsg );
419 }
420 if ( !wfEmptyMsg( $descriptionMsg, $msg ) && $msg != '' ) {
421 $description = $msg;
422 }
423 }
424
425 if ( $svnText !== false ) {
426 $extNameVer = "<tr>
427 <td><em>$mainLink $versionText</em></td>
428 <td><em>$svnText</em></td>";
429 } else {
430 $extNameVer = "<tr>
431 <td colspan=\"2\"><em>$mainLink $versionText</em></td>";
432 }
433
434 $author = isset ( $extension['author'] ) ? $extension['author'] : array();
435 $extDescAuthor = "<td>$description</td>
436 <td>" . $this->listToText( (array)$author, false ) . "</td>
437 </tr>\n";
438
439 return $extNameVer . $extDescAuthor;
440 }
441
442 /**
443 * Generate wikitext showing hooks in $wgHooks.
444 *
445 * @return String: wikitext
446 */
447 private function getWgHooks() {
448 global $wgHooks;
449
450 if ( count( $wgHooks ) ) {
451 $myWgHooks = $wgHooks;
452 ksort( $myWgHooks );
453
454 $ret = Xml::element( 'h2', array( 'id' => 'mw-version-hooks' ), wfMsg( 'version-hooks' ) ) .
455 Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-hooks' ) ) .
456 "<tr>
457 <th>" . wfMsg( 'version-hook-name' ) . "</th>
458 <th>" . wfMsg( 'version-hook-subscribedby' ) . "</th>
459 </tr>\n";
460
461 foreach ( $myWgHooks as $hook => $hooks )
462 $ret .= "<tr>
463 <td>$hook</td>
464 <td>" . $this->listToText( $hooks ) . "</td>
465 </tr>\n";
466
467 $ret .= Xml::closeElement( 'table' );
468 return $ret;
469 } else
470 return '';
471 }
472
473 private function openExtType( $text, $name = null ) {
474 $opt = array( 'colspan' => 4 );
475 $out = '';
476
477 if( $this->firstExtOpened ) {
478 // Insert a spacing line
479 $out .= '<tr class="sv-space">' . Html::element( 'td', $opt ) . "</tr>\n";
480 }
481 $this->firstExtOpened = true;
482
483 if( $name ) {
484 $opt['id'] = "sv-$name";
485 }
486
487 $out .= "<tr>" . Xml::element( 'th', $opt, $text ) . "</tr>\n";
488
489 return $out;
490 }
491
492 /**
493 * Get information about client's IP address.
494 *
495 * @return String: HTML fragment
496 */
497 private function IPInfo() {
498 $ip = str_replace( '--', ' - ', htmlspecialchars( wfGetIP() ) );
499 return "<!-- visited from $ip -->\n" .
500 "<span style='display:none'>visited from $ip</span>";
501 }
502
503 /**
504 * Convert an array of items into a list for display.
505 *
506 * @param $list Array of elements to display
507 * @param $sort Boolean: whether to sort the items in $list
508 *
509 * @return String
510 */
511 function listToText( $list, $sort = true ) {
512 $cnt = count( $list );
513
514 if ( $cnt == 1 ) {
515 // Enforce always returning a string
516 return (string)self::arrayToString( $list[0] );
517 } elseif ( $cnt == 0 ) {
518 return '';
519 } else {
520 global $wgLang;
521 if ( $sort ) {
522 sort( $list );
523 }
524 return $wgLang->listToText( array_map( array( __CLASS__, 'arrayToString' ), $list ) );
525 }
526 }
527
528 /**
529 * Convert an array or object to a string for display.
530 *
531 * @param $list Mixed: will convert an array to string if given and return
532 * the paramater unaltered otherwise
533 *
534 * @return Mixed
535 */
536 static function arrayToString( $list ) {
537 if( is_array( $list ) && count( $list ) == 1 )
538 $list = $list[0];
539 if( is_object( $list ) ) {
540 $class = get_class( $list );
541 return "($class)";
542 } elseif ( !is_array( $list ) ) {
543 return $list;
544 } else {
545 if( is_object( $list[0] ) )
546 $class = get_class( $list[0] );
547 else
548 $class = $list[0];
549 return "($class, {$list[1]})";
550 }
551 }
552
553 /**
554 * Get an associative array of information about a given path, from its .svn
555 * subdirectory. Returns false on error, such as if the directory was not
556 * checked out with subversion.
557 *
558 * Returned keys are:
559 * Required:
560 * checkout-rev The revision which was checked out
561 * Optional:
562 * directory-rev The revision when the directory was last modified
563 * url The subversion URL of the directory
564 * repo-url The base URL of the repository
565 * viewvc-url A ViewVC URL pointing to the checked-out revision
566 */
567 public static function getSvnInfo( $dir ) {
568 // http://svnbook.red-bean.com/nightly/en/svn.developer.insidewc.html
569 $entries = $dir . '/.svn/entries';
570
571 if( !file_exists( $entries ) ) {
572 return false;
573 }
574
575 $lines = file( $entries );
576 if ( !count( $lines ) ) {
577 return false;
578 }
579
580 // check if file is xml (subversion release <= 1.3) or not (subversion release = 1.4)
581 if( preg_match( '/^<\?xml/', $lines[0] ) ) {
582 // subversion is release <= 1.3
583 if( !function_exists( 'simplexml_load_file' ) ) {
584 // We could fall back to expat... YUCK
585 return false;
586 }
587
588 // SimpleXml whines about the xmlns...
589 wfSuppressWarnings();
590 $xml = simplexml_load_file( $entries );
591 wfRestoreWarnings();
592
593 if( $xml ) {
594 foreach( $xml->entry as $entry ) {
595 if( $xml->entry[0]['name'] == '' ) {
596 // The directory entry should always have a revision marker.
597 if( $entry['revision'] ) {
598 return array( 'checkout-rev' => intval( $entry['revision'] ) );
599 }
600 }
601 }
602 }
603
604 return false;
605 }
606
607 // Subversion is release 1.4 or above.
608 if ( count( $lines ) < 11 ) {
609 return false;
610 }
611
612 $info = array(
613 'checkout-rev' => intval( trim( $lines[3] ) ),
614 'url' => trim( $lines[4] ),
615 'repo-url' => trim( $lines[5] ),
616 'directory-rev' => intval( trim( $lines[10] ) )
617 );
618
619 if ( isset( self::$viewvcUrls[$info['repo-url']] ) ) {
620 $viewvc = str_replace(
621 $info['repo-url'],
622 self::$viewvcUrls[$info['repo-url']],
623 $info['url']
624 );
625
626 $viewvc .= '/?pathrev=';
627 $viewvc .= urlencode( $info['checkout-rev'] );
628 $info['viewvc-url'] = $viewvc;
629 }
630
631 return $info;
632 }
633
634 /**
635 * Retrieve the revision number of a Subversion working directory.
636 *
637 * @param $dir String: directory of the svn checkout
638 *
639 * @return Integer: revision number as int
640 */
641 public static function getSvnRevision( $dir ) {
642 $info = self::getSvnInfo( $dir );
643
644 if ( $info === false ) {
645 return false;
646 } elseif ( isset( $info['checkout-rev'] ) ) {
647 return $info['checkout-rev'];
648 } else {
649 return false;
650 }
651 }
652
653 }