Follow up to r70313
[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->getExtensionCredits();
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 getExtensionCredits() {
208 global $wgExtensionCredits, $wgExtensionFunctions, $wgParser, $wgSkinExtensionFunctions;
209
210 if ( !count( $wgExtensionCredits ) && !count( $wgExtensionFunctions ) && !count( $wgSkinExtensionFunctions ) ) {
211 return '';
212 }
213
214 $extensionTypes = array(
215 'specialpage' => wfMsg( 'version-specialpages' ),
216 'parserhook' => wfMsg( 'version-parserhooks' ),
217 'variable' => wfMsg( 'version-variables' ),
218 'media' => wfMsg( 'version-mediahandlers' ),
219 'other' => wfMsg( 'version-other' ),
220 );
221
222 wfRunHooks( 'SpecialVersionExtensionTypes', array( &$this, &$extensionTypes ) );
223
224 $out = Xml::element( 'h2', array( 'id' => 'mw-version-ext' ), wfMsg( 'version-extensions' ) ) .
225 Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-ext' ) );
226
227 foreach ( $extensionTypes as $type => $text ) {
228 if ( isset ( $wgExtensionCredits[$type] ) && count ( $wgExtensionCredits[$type] ) ) {
229 $out .= $this->openExtType( $text, 'credits-' . $type );
230
231 usort( $wgExtensionCredits[$type], array( $this, 'compare' ) );
232
233 foreach ( $wgExtensionCredits[$type] as $extension ) {
234 $out .= $this->getCreditsForExtension( $extension );
235 }
236 }
237 }
238
239 if ( count( $wgExtensionFunctions ) ) {
240 $out .= $this->openExtType( wfMsg( 'version-extension-functions' ), 'extension-functions' );
241 $out .= '<tr><td colspan="4">' . $this->listToText( $wgExtensionFunctions ) . "</td></tr>\n";
242 }
243
244 if ( $cnt = count( $tags = $wgParser->getTags() ) ) {
245 for ( $i = 0; $i < $cnt; ++$i )
246 $tags[$i] = "&lt;{$tags[$i]}&gt;";
247 $out .= $this->openExtType( wfMsg( 'version-parser-extensiontags' ), 'parser-tags' );
248 $out .= '<tr><td colspan="4">' . $this->listToText( $tags ). "</td></tr>\n";
249 }
250
251 if( $cnt = count( $fhooks = $wgParser->getFunctionHooks() ) ) {
252 $out .= $this->openExtType( wfMsg( 'version-parser-function-hooks' ), 'parser-function-hooks' );
253 $out .= '<tr><td colspan="4">' . $this->listToText( $fhooks ) . "</td></tr>\n";
254 }
255
256 if ( count( $wgSkinExtensionFunctions ) ) {
257 $out .= $this->openExtType( wfMsg( 'version-skin-extension-functions' ), 'skin-extension-functions' );
258 $out .= '<tr><td colspan="4">' . $this->listToText( $wgSkinExtensionFunctions ) . "</td></tr>\n";
259 }
260
261 $out .= Xml::closeElement( 'table' );
262
263 return $out;
264 }
265
266 /**
267 * Callback to sort extensions by type.
268 */
269 function compare( $a, $b ) {
270 global $wgLang;
271 if( $a['name'] === $b['name'] ) {
272 return 0;
273 } else {
274 return $wgLang->lc( $a['name'] ) > $wgLang->lc( $b['name'] )
275 ? 1
276 : -1;
277 }
278 }
279
280 /**
281 * Creates and formats the creidts for a single extension and returns this.
282 *
283 * @param $extension String
284 *
285 * @return string
286 */
287 function getCreditsForExtension( $extension ) {
288 $name = isset( $extension['name'] ) ? $extension['name'] : '[no name]';
289
290 if ( isset( $extension['path'] ) ) {
291 $svnInfo = self::getSvnInfo( dirname($extension['path']) );
292 $directoryRev = isset( $svnInfo['directory-rev'] ) ? $svnInfo['directory-rev'] : null;
293 $checkoutRev = isset( $svnInfo['checkout-rev'] ) ? $svnInfo['checkout-rev'] : null;
294 $viewvcUrl = isset( $svnInfo['viewvc-url'] ) ? $svnInfo['viewvc-url'] : null;
295 } else {
296 $directoryRev = null;
297 $checkoutRev = null;
298 $viewvcUrl = null;
299 }
300
301 # Make main link (or just the name if there is no URL).
302 if ( isset( $extension['url'] ) ) {
303 $mainLink = "[{$extension['url']} $name]";
304 } else {
305 $mainLink = $name;
306 }
307
308 if ( isset( $extension['version'] ) ) {
309 $versionText = '<span class="mw-version-ext-version">' .
310 wfMsg( 'version-version', $extension['version'] ) .
311 '</span>';
312 } else {
313 $versionText = '';
314 }
315
316 # Make subversion text/link.
317 if ( $checkoutRev ) {
318 $svnText = wfMsg( 'version-svn-revision', $directoryRev, $checkoutRev );
319 $svnText = isset( $viewvcUrl ) ? "[$viewvcUrl $svnText]" : $svnText;
320 } else {
321 $svnText = false;
322 }
323
324 # Make description text.
325 $description = isset ( $extension['description'] ) ? $extension['description'] : '';
326
327 if( isset ( $extension['descriptionmsg'] ) ) {
328 # Look for a localized description.
329 $descriptionMsg = $extension['descriptionmsg'];
330
331 if( is_array( $descriptionMsg ) ) {
332 $descriptionMsgKey = $descriptionMsg[0]; // Get the message key
333 array_shift( $descriptionMsg ); // Shift out the message key to get the parameters only
334 array_map( "htmlspecialchars", $descriptionMsg ); // For sanity
335 $msg = wfMsg( $descriptionMsgKey, $descriptionMsg );
336 } else {
337 $msg = wfMsg( $descriptionMsg );
338 }
339 if ( !wfEmptyMsg( $descriptionMsg, $msg ) && $msg != '' ) {
340 $description = $msg;
341 }
342 }
343
344 if ( $svnText !== false ) {
345 $extNameVer = "<tr>
346 <td><em>$mainLink $versionText</em></td>
347 <td><em>$svnText</em></td>";
348 } else {
349 $extNameVer = "<tr>
350 <td colspan=\"2\"><em>$mainLink $versionText</em></td>";
351 }
352
353 $author = isset ( $extension['author'] ) ? $extension['author'] : array();
354 $extDescAuthor = "<td>$description</td>
355 <td>" . $this->listToText( (array)$author, false ) . "</td>
356 </tr>\n";
357
358 return $extNameVer . $extDescAuthor;
359 }
360
361 /**
362 * Generate wikitext showing hooks in $wgHooks.
363 *
364 * @return String: wikitext
365 */
366 private function getWgHooks() {
367 global $wgHooks;
368
369 if ( count( $wgHooks ) ) {
370 $myWgHooks = $wgHooks;
371 ksort( $myWgHooks );
372
373 $ret = Xml::element( 'h2', array( 'id' => 'mw-version-hooks' ), wfMsg( 'version-hooks' ) ) .
374 Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-hooks' ) ) .
375 "<tr>
376 <th>" . wfMsg( 'version-hook-name' ) . "</th>
377 <th>" . wfMsg( 'version-hook-subscribedby' ) . "</th>
378 </tr>\n";
379
380 foreach ( $myWgHooks as $hook => $hooks )
381 $ret .= "<tr>
382 <td>$hook</td>
383 <td>" . $this->listToText( $hooks ) . "</td>
384 </tr>\n";
385
386 $ret .= Xml::closeElement( 'table' );
387 return $ret;
388 } else
389 return '';
390 }
391
392 private function openExtType( $text, $name = null ) {
393 $opt = array( 'colspan' => 4 );
394 $out = '';
395
396 if( !$this->firstExtOpened ) {
397 // Insert a spacing line
398 $out .= '<tr class="sv-space">' . Html::element( 'td', $opt ) . "</tr>\n";
399 }
400 $this->firstExtOpened = false;
401
402 if( $name )
403 $opt['id'] = "sv-$name";
404
405 $out .= "<tr>" . Xml::element( 'th', $opt, $text ) . "</tr>\n";
406 return $out;
407 }
408
409 /**
410 * Get information about client's IP address.
411 *
412 * @return String: HTML fragment
413 */
414 private function IPInfo() {
415 $ip = str_replace( '--', ' - ', htmlspecialchars( wfGetIP() ) );
416 return "<!-- visited from $ip -->\n" .
417 "<span style='display:none'>visited from $ip</span>";
418 }
419
420 /**
421 * Convert an array of items into a list for display.
422 *
423 * @param $list Array of elements to display
424 * @param $sort Boolean: whether to sort the items in $list
425 *
426 * @return String
427 */
428 function listToText( $list, $sort = true ) {
429 $cnt = count( $list );
430
431 if ( $cnt == 1 ) {
432 // Enforce always returning a string
433 return (string)self::arrayToString( $list[0] );
434 } elseif ( $cnt == 0 ) {
435 return '';
436 } else {
437 global $wgLang;
438 if ( $sort ) {
439 sort( $list );
440 }
441 return $wgLang->listToText( array_map( array( __CLASS__, 'arrayToString' ), $list ) );
442 }
443 }
444
445 /**
446 * Convert an array or object to a string for display.
447 *
448 * @param $list Mixed: will convert an array to string if given and return
449 * the paramater unaltered otherwise
450 *
451 * @return Mixed
452 */
453 static function arrayToString( $list ) {
454 if( is_array( $list ) && count( $list ) == 1 )
455 $list = $list[0];
456 if( is_object( $list ) ) {
457 $class = get_class( $list );
458 return "($class)";
459 } elseif ( !is_array( $list ) ) {
460 return $list;
461 } else {
462 if( is_object( $list[0] ) )
463 $class = get_class( $list[0] );
464 else
465 $class = $list[0];
466 return "($class, {$list[1]})";
467 }
468 }
469
470 /**
471 * Get an associative array of information about a given path, from its .svn
472 * subdirectory. Returns false on error, such as if the directory was not
473 * checked out with subversion.
474 *
475 * Returned keys are:
476 * Required:
477 * checkout-rev The revision which was checked out
478 * Optional:
479 * directory-rev The revision when the directory was last modified
480 * url The subversion URL of the directory
481 * repo-url The base URL of the repository
482 * viewvc-url A ViewVC URL pointing to the checked-out revision
483 */
484 public static function getSvnInfo( $dir ) {
485 // http://svnbook.red-bean.com/nightly/en/svn.developer.insidewc.html
486 $entries = $dir . '/.svn/entries';
487
488 if( !file_exists( $entries ) ) {
489 return false;
490 }
491
492 $lines = file( $entries );
493 if ( !count( $lines ) ) {
494 return false;
495 }
496
497 // check if file is xml (subversion release <= 1.3) or not (subversion release = 1.4)
498 if( preg_match( '/^<\?xml/', $lines[0] ) ) {
499 // subversion is release <= 1.3
500 if( !function_exists( 'simplexml_load_file' ) ) {
501 // We could fall back to expat... YUCK
502 return false;
503 }
504
505 // SimpleXml whines about the xmlns...
506 wfSuppressWarnings();
507 $xml = simplexml_load_file( $entries );
508 wfRestoreWarnings();
509
510 if( $xml ) {
511 foreach( $xml->entry as $entry ) {
512 if( $xml->entry[0]['name'] == '' ) {
513 // The directory entry should always have a revision marker.
514 if( $entry['revision'] ) {
515 return array( 'checkout-rev' => intval( $entry['revision'] ) );
516 }
517 }
518 }
519 }
520
521 return false;
522 }
523
524 // Subversion is release 1.4 or above.
525 if ( count( $lines ) < 11 ) {
526 return false;
527 }
528
529 $info = array(
530 'checkout-rev' => intval( trim( $lines[3] ) ),
531 'url' => trim( $lines[4] ),
532 'repo-url' => trim( $lines[5] ),
533 'directory-rev' => intval( trim( $lines[10] ) )
534 );
535
536 if ( isset( self::$viewvcUrls[$info['repo-url']] ) ) {
537 $viewvc = str_replace(
538 $info['repo-url'],
539 self::$viewvcUrls[$info['repo-url']],
540 $info['url']
541 );
542
543 $pathRelativeToRepo = substr( $info['url'], strlen( $info['repo-url'] ) );
544 $viewvc .= '/?pathrev=';
545 $viewvc .= urlencode( $info['checkout-rev'] );
546 $info['viewvc-url'] = $viewvc;
547 }
548
549 return $info;
550 }
551
552 /**
553 * Retrieve the revision number of a Subversion working directory.
554 *
555 * @param $dir String: directory of the svn checkout
556 *
557 * @return Integer: revision number as int
558 */
559 public static function getSvnRevision( $dir ) {
560 $info = self::getSvnInfo( $dir );
561
562 if ( $info === false ) {
563 return false;
564 } elseif ( isset( $info['checkout-rev'] ) ) {
565 return $info['checkout-rev'];
566 } else {
567 return false;
568 }
569 }
570
571 }