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