Documentation and style improvements
[lhc/web/wiklou.git] / includes / specials / SpecialVersion.php
1 <?php
2
3 /**
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 * http://www.gnu.org/copyleft/gpl.html
18 */
19
20 /**
21 * Give information about the version of MediaWiki, PHP, the DB and extensions
22 *
23 * @ingroup SpecialPage
24 *
25 * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
26 * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
27 * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
28 */
29 class SpecialVersion extends SpecialPage {
30 private $firstExtOpened = true;
31
32 static $viewvcUrls = array(
33 'svn+ssh://svn.wikimedia.org/svnroot/mediawiki' => 'http://svn.wikimedia.org/viewvc/mediawiki',
34 'http://svn.wikimedia.org/svnroot/mediawiki' => 'http://svn.wikimedia.org/viewvc/mediawiki',
35 # Doesn't work at the time of writing but maybe some day:
36 'https://svn.wikimedia.org/viewvc/mediawiki' => 'http://svn.wikimedia.org/viewvc/mediawiki',
37 );
38
39 function __construct(){
40 parent::__construct( 'Version' );
41 }
42
43 /**
44 * main()
45 */
46 function execute( $par ) {
47 global $wgOut, $wgMessageCache, $wgSpecialVersionShowHooks, $wgContLang;
48
49 $wgMessageCache->loadAllMessages();
50
51 $this->setHeaders();
52 $this->outputHeader();
53
54 $wgOut->addHTML( Xml::openElement( 'div',
55 array( 'dir' => $wgContLang->getDir() ) ) );
56 $text =
57 $this->getMediaWikiCredits() .
58 $this->softwareInformation() .
59 $this->extensionCredits();
60 if ( $wgSpecialVersionShowHooks ) {
61 $text .= $this->getWgHooks();
62 }
63
64 $wgOut->addWikiText( $text );
65 $wgOut->addHTML( $this->IPInfo() );
66 $wgOut->addHTML( '</div>' );
67 }
68
69 /**
70 * Returns wiki text showing the license information.
71 *
72 * @return string
73 */
74 private static function getMediaWikiCredits() {
75 $ret = Xml::element( 'h2', array( 'id' => 'mw-version-license' ), wfMsg( 'version-license' ) );
76
77 // This text is always left-to-right.
78 $ret .= '<div dir="ltr">';
79 $ret .= "__NOTOC__
80 This wiki is powered by '''[http://www.mediawiki.org/ MediaWiki]''',
81 copyright © 2001-2010 Magnus Manske, Brion Vibber, Lee Daniel Crocker,
82 Tim Starling, Erik Möller, Gabriel Wicke, Ævar Arnfjörð Bjarmason,
83 Niklas Laxström, Domas Mituzas, Rob Church, Yuri Astrakhan, Aryeh Gregor,
84 Aaron Schulz, Andrew Garrett, Raimond Spekking, Alexandre Emsenhuber,
85 Siebrand Mazeland, Chad Horohoe and others.
86
87 MediaWiki is free software; you can redistribute it and/or modify
88 it under the terms of the GNU General Public License as published by
89 the Free Software Foundation; either version 2 of the License, or
90 (at your option) any later version.
91
92 MediaWiki is distributed in the hope that it will be useful,
93 but WITHOUT ANY WARRANTY; without even the implied warranty of
94 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
95 GNU General Public License for more details.
96
97 You should have received [{{SERVER}}{{SCRIPTPATH}}/COPYING a copy of the GNU General Public License]
98 along with this program; if not, write to the Free Software
99 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
100 or [http://www.gnu.org/licenses/old-licenses/gpl-2.0.html read it online].
101 ";
102 $ret .= '</div>';
103
104 return str_replace( "\t\t", '', $ret ) . "\n";
105 }
106
107 /**
108 * Returns wiki text showing the third party software versions (apache, php, mysql).
109 *
110 * @return string
111 */
112 static function softwareInformation() {
113 $dbr = wfGetDB( DB_SLAVE );
114
115 // Put the software in an array of form 'name' => 'version'. All messages should
116 // be loaded here, so feel free to use wfMsg*() in the 'name'. Raw HTML or wikimarkup
117 // can be used.
118 $software = array();
119 $software['[http://www.mediawiki.org/ MediaWiki]'] = self::getVersionLinked();
120 $software['[http://www.php.net/ PHP]'] = phpversion() . " (" . php_sapi_name() . ")";
121 $software[$dbr->getSoftwareLink()] = $dbr->getServerVersion();
122
123 // Allow a hook to add/remove items.
124 wfRunHooks( 'SoftwareInfo', array( &$software ) );
125
126 $out = Xml::element( 'h2', array( 'id' => 'mw-version-software' ), wfMsg( 'version-software' ) ) .
127 Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-software' ) ) .
128 "<tr>
129 <th>" . wfMsg( 'version-software-product' ) . "</th>
130 <th>" . wfMsg( 'version-software-version' ) . "</th>
131 </tr>\n";
132
133 foreach( $software as $name => $version ) {
134 $out .= "<tr>
135 <td>" . $name . "</td>
136 <td>" . $version . "</td>
137 </tr>\n";
138 }
139
140 return $out . Xml::closeElement( 'table' );
141 }
142
143 /**
144 * Return a string of the MediaWiki version with SVN revision if available.
145 *
146 * @return mixed
147 */
148 public static function getVersion( $flags = '' ) {
149 global $wgVersion, $IP;
150 wfProfileIn( __METHOD__ );
151
152 $info = self::getSvnInfo( $IP );
153 if ( !$info ) {
154 $version = $wgVersion;
155 } elseif( $flags === 'nodb' ) {
156 $version = "$wgVersion (r{$info['checkout-rev']})";
157 } else {
158 $version = $wgVersion . ' ' .
159 wfMsg(
160 'version-svn-revision',
161 isset( $info['directory-rev'] ) ? $info['directory-rev'] : '',
162 $info['checkout-rev']
163 );
164 }
165
166 wfProfileOut( __METHOD__ );
167 return $version;
168 }
169
170 /**
171 * Return a wikitext-formatted string of the MediaWiki version with a link to
172 * the SVN revision if available.
173 *
174 * @return mixed
175 */
176 public static function getVersionLinked() {
177 global $wgVersion, $IP;
178 wfProfileIn( __METHOD__ );
179
180 $info = self::getSvnInfo( $IP );
181
182 if ( isset( $info['checkout-rev'] ) ) {
183 $linkText = wfMsg(
184 'version-svn-revision',
185 isset( $info['directory-rev'] ) ? $info['directory-rev'] : '',
186 $info['checkout-rev']
187 );
188
189 if ( isset( $info['viewvc-url'] ) ) {
190 $version = "$wgVersion [{$info['viewvc-url']} $linkText]";
191 } else {
192 $version = "$wgVersion $linkText";
193 }
194 } else {
195 $version = $wgVersion;
196 }
197
198 wfProfileOut( __METHOD__ );
199 return $version;
200 }
201
202 /**
203 * Generate wikitext showing extensions name, URL, author and description.
204 *
205 * @return String: Wikitext
206 */
207 function extensionCredits() {
208 global $wgExtensionCredits, $wgExtensionFunctions, $wgParser, $wgSkinExtensionFunctions;
209
210 if ( ! count( $wgExtensionCredits ) && ! count( $wgExtensionFunctions ) && ! count( $wgSkinExtensionFunctions ) )
211 return '';
212
213 $extensionTypes = array(
214 'specialpage' => wfMsg( 'version-specialpages' ),
215 'parserhook' => wfMsg( 'version-parserhooks' ),
216 'variable' => wfMsg( 'version-variables' ),
217 'media' => wfMsg( 'version-mediahandlers' ),
218 'other' => wfMsg( 'version-other' ),
219 );
220 wfRunHooks( 'SpecialVersionExtensionTypes', array( &$this, &$extensionTypes ) );
221
222 $out = Xml::element( 'h2', array( 'id' => 'mw-version-ext' ), wfMsg( 'version-extensions' ) ) .
223 Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-ext' ) );
224
225 foreach ( $extensionTypes as $type => $text ) {
226 if ( isset ( $wgExtensionCredits[$type] ) && count ( $wgExtensionCredits[$type] ) ) {
227 $out .= $this->openExtType( $text, 'credits-' . $type );
228
229 usort( $wgExtensionCredits[$type], array( $this, 'compare' ) );
230
231 foreach ( $wgExtensionCredits[$type] as $extension ) {
232 $out .= $this->formatCredits( $extension );
233 }
234 }
235 }
236
237 if ( count( $wgExtensionFunctions ) ) {
238 $out .= $this->openExtType( wfMsg( 'version-extension-functions' ), 'extension-functions' );
239 $out .= '<tr><td colspan="4">' . $this->listToText( $wgExtensionFunctions ) . "</td></tr>\n";
240 }
241
242 if ( $cnt = count( $tags = $wgParser->getTags() ) ) {
243 for ( $i = 0; $i < $cnt; ++$i )
244 $tags[$i] = "&lt;{$tags[$i]}&gt;";
245 $out .= $this->openExtType( wfMsg( 'version-parser-extensiontags' ), 'parser-tags' );
246 $out .= '<tr><td colspan="4">' . $this->listToText( $tags ). "</td></tr>\n";
247 }
248
249 if( $cnt = count( $fhooks = $wgParser->getFunctionHooks() ) ) {
250 $out .= $this->openExtType( wfMsg( 'version-parser-function-hooks' ), 'parser-function-hooks' );
251 $out .= '<tr><td colspan="4">' . $this->listToText( $fhooks ) . "</td></tr>\n";
252 }
253
254 if ( count( $wgSkinExtensionFunctions ) ) {
255 $out .= $this->openExtType( wfMsg( 'version-skin-extension-functions' ), 'skin-extension-functions' );
256 $out .= '<tr><td colspan="4">' . $this->listToText( $wgSkinExtensionFunctions ) . "</td></tr>\n";
257 }
258
259 $out .= Xml::closeElement( 'table' );
260
261 return $out;
262 }
263
264 /**
265 * Callback to sort extensions by type.
266 */
267 function compare( $a, $b ) {
268 global $wgLang;
269 if( $a['name'] === $b['name'] ) {
270 return 0;
271 } else {
272 return $wgLang->lc( $a['name'] ) > $wgLang->lc( $b['name'] )
273 ? 1
274 : -1;
275 }
276 }
277
278 function formatCredits( $extension ) {
279 $name = isset( $extension['name'] ) ? $extension['name'] : '[no name]';
280
281 if ( isset( $extension['path'] ) ) {
282 $svnInfo = self::getSvnInfo( dirname($extension['path']) );
283 $directoryRev = isset( $svnInfo['directory-rev'] ) ? $svnInfo['directory-rev'] : null;
284 $checkoutRev = isset( $svnInfo['checkout-rev'] ) ? $svnInfo['checkout-rev'] : null;
285 $viewvcUrl = isset( $svnInfo['viewvc-url'] ) ? $svnInfo['viewvc-url'] : null;
286 } else {
287 $directoryRev = null;
288 $checkoutRev = null;
289 $viewvcUrl = null;
290 }
291
292 # Make main link (or just the name if there is no URL).
293 if ( isset( $extension['url'] ) ) {
294 $mainLink = "[{$extension['url']} $name]";
295 } else {
296 $mainLink = $name;
297 }
298
299 if ( isset( $extension['version'] ) ) {
300 $versionText = '<span class="mw-version-ext-version">' .
301 wfMsg( 'version-version', $extension['version'] ) .
302 '</span>';
303 } else {
304 $versionText = '';
305 }
306
307 # Make subversion text/link.
308 if ( $checkoutRev ) {
309 $svnText = wfMsg( 'version-svn-revision', $directoryRev, $checkoutRev );
310 $svnText = isset( $viewvcUrl ) ? "[$viewvcUrl $svnText]" : $svnText;
311 } else {
312 $svnText = false;
313 }
314
315 # Make description text.
316 $description = isset ( $extension['description'] ) ? $extension['description'] : '';
317
318 if( isset ( $extension['descriptionmsg'] ) ) {
319 # Look for a localized description.
320 $descriptionMsg = $extension['descriptionmsg'];
321
322 if( is_array( $descriptionMsg ) ) {
323 $descriptionMsgKey = $descriptionMsg[0]; // Get the message key
324 array_shift( $descriptionMsg ); // Shift out the message key to get the parameters only
325 array_map( "htmlspecialchars", $descriptionMsg ); // For sanity
326 $msg = wfMsg( $descriptionMsgKey, $descriptionMsg );
327 } else {
328 $msg = wfMsg( $descriptionMsg );
329 }
330 if ( !wfEmptyMsg( $descriptionMsg, $msg ) && $msg != '' ) {
331 $description = $msg;
332 }
333 }
334
335 if ( $svnText !== false ) {
336 $extNameVer = "<tr>
337 <td><em>$mainLink $versionText</em></td>
338 <td><em>$svnText</em></td>";
339 } else {
340 $extNameVer = "<tr>
341 <td colspan=\"2\"><em>$mainLink $versionText</em></td>";
342 }
343
344 $author = isset ( $extension['author'] ) ? $extension['author'] : array();
345 $extDescAuthor = "<td>$description</td>
346 <td>" . $this->listToText( (array)$author, false ) . "</td>
347 </tr>\n";
348
349 return $extNameVer . $extDescAuthor;
350 }
351
352 /**
353 * Generate wikitext showing hooks in $wgHooks.
354 *
355 * @return String: wikitext
356 */
357 private function getWgHooks() {
358 global $wgHooks;
359
360 if ( count( $wgHooks ) ) {
361 $myWgHooks = $wgHooks;
362 ksort( $myWgHooks );
363
364 $ret = Xml::element( 'h2', array( 'id' => 'mw-version-hooks' ), wfMsg( 'version-hooks' ) ) .
365 Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-hooks' ) ) .
366 "<tr>
367 <th>" . wfMsg( 'version-hook-name' ) . "</th>
368 <th>" . wfMsg( 'version-hook-subscribedby' ) . "</th>
369 </tr>\n";
370
371 foreach ( $myWgHooks as $hook => $hooks )
372 $ret .= "<tr>
373 <td>$hook</td>
374 <td>" . $this->listToText( $hooks ) . "</td>
375 </tr>\n";
376
377 $ret .= Xml::closeElement( 'table' );
378 return $ret;
379 } else
380 return '';
381 }
382
383 private function openExtType( $text, $name = null ) {
384 $opt = array( 'colspan' => 4 );
385 $out = '';
386
387 if( !$this->firstExtOpened ) {
388 // Insert a spacing line
389 $out .= '<tr class="sv-space">' . Html::element( 'td', $opt ) . "</tr>\n";
390 }
391 $this->firstExtOpened = false;
392
393 if( $name )
394 $opt['id'] = "sv-$name";
395
396 $out .= "<tr>" . Xml::element( 'th', $opt, $text ) . "</tr>\n";
397 return $out;
398 }
399
400 /**
401 * Get information about client's IP address.
402 *
403 * @return String: HTML fragment
404 */
405 private function IPInfo() {
406 $ip = str_replace( '--', ' - ', htmlspecialchars( wfGetIP() ) );
407 return "<!-- visited from $ip -->\n" .
408 "<span style='display:none'>visited from $ip</span>";
409 }
410
411 /**
412 * Convert an array of items into a list for display.
413 *
414 * @param $list Array of elements to display
415 * @param $sort Boolean: whether to sort the items in $list
416 *
417 * @return String
418 */
419 function listToText( $list, $sort = true ) {
420 $cnt = count( $list );
421
422 if ( $cnt == 1 ) {
423 // Enforce always returning a string
424 return (string)self::arrayToString( $list[0] );
425 } elseif ( $cnt == 0 ) {
426 return '';
427 } else {
428 global $wgLang;
429 if ( $sort ) {
430 sort( $list );
431 }
432 return $wgLang->listToText( array_map( array( __CLASS__, 'arrayToString' ), $list ) );
433 }
434 }
435
436 /**
437 * Convert an array or object to a string for display.
438 *
439 * @param $list Mixed: will convert an array to string if given and return
440 * the paramater unaltered otherwise
441 *
442 * @return Mixed
443 */
444 static function arrayToString( $list ) {
445 if( is_array( $list ) && count( $list ) == 1 )
446 $list = $list[0];
447 if( is_object( $list ) ) {
448 $class = get_class( $list );
449 return "($class)";
450 } elseif ( !is_array( $list ) ) {
451 return $list;
452 } else {
453 if( is_object( $list[0] ) )
454 $class = get_class( $list[0] );
455 else
456 $class = $list[0];
457 return "($class, {$list[1]})";
458 }
459 }
460
461 /**
462 * Get an associative array of information about a given path, from its .svn
463 * subdirectory. Returns false on error, such as if the directory was not
464 * checked out with subversion.
465 *
466 * Returned keys are:
467 * Required:
468 * checkout-rev The revision which was checked out
469 * Optional:
470 * directory-rev The revision when the directory was last modified
471 * url The subversion URL of the directory
472 * repo-url The base URL of the repository
473 * viewvc-url A ViewVC URL pointing to the checked-out revision
474 */
475 public static function getSvnInfo( $dir ) {
476 // http://svnbook.red-bean.com/nightly/en/svn.developer.insidewc.html
477 $entries = $dir . '/.svn/entries';
478
479 if( !file_exists( $entries ) ) {
480 return false;
481 }
482
483 $lines = file( $entries );
484 if ( !count( $lines ) ) {
485 return false;
486 }
487
488 // check if file is xml (subversion release <= 1.3) or not (subversion release = 1.4)
489 if( preg_match( '/^<\?xml/', $lines[0] ) ) {
490 // subversion is release <= 1.3
491 if( !function_exists( 'simplexml_load_file' ) ) {
492 // We could fall back to expat... YUCK
493 return false;
494 }
495
496 // SimpleXml whines about the xmlns...
497 wfSuppressWarnings();
498 $xml = simplexml_load_file( $entries );
499 wfRestoreWarnings();
500
501 if( $xml ) {
502 foreach( $xml->entry as $entry ) {
503 if( $xml->entry[0]['name'] == '' ) {
504 // The directory entry should always have a revision marker.
505 if( $entry['revision'] ) {
506 return array( 'checkout-rev' => intval( $entry['revision'] ) );
507 }
508 }
509 }
510 }
511
512 return false;
513 }
514
515 // Subversion is release 1.4 or above.
516 if ( count( $lines ) < 11 ) {
517 return false;
518 }
519
520 $info = array(
521 'checkout-rev' => intval( trim( $lines[3] ) ),
522 'url' => trim( $lines[4] ),
523 'repo-url' => trim( $lines[5] ),
524 'directory-rev' => intval( trim( $lines[10] ) )
525 );
526
527 if ( isset( self::$viewvcUrls[$info['repo-url']] ) ) {
528 $viewvc = str_replace(
529 $info['repo-url'],
530 self::$viewvcUrls[$info['repo-url']],
531 $info['url']
532 );
533
534 $pathRelativeToRepo = substr( $info['url'], strlen( $info['repo-url'] ) );
535 $viewvc .= '/?pathrev=';
536 $viewvc .= urlencode( $info['checkout-rev'] );
537 $info['viewvc-url'] = $viewvc;
538 }
539
540 return $info;
541 }
542
543 /**
544 * Retrieve the revision number of a Subversion working directory.
545 *
546 * @param $dir String: directory of the svn checkout
547 *
548 * @return Integer: revision number as int
549 */
550 public static function getSvnRevision( $dir ) {
551 $info = self::getSvnInfo( $dir );
552
553 if ( $info === false ) {
554 return false;
555 } elseif ( isset( $info['checkout-rev'] ) ) {
556 return $info['checkout-rev'];
557 } else {
558 return false;
559 }
560 }
561
562 }