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