Merge "Cleanup some docs (includes/[s-z])"
[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 protected $firstExtOpened = false;
33
34 /**
35 * Stores the current rev id/SHA hash of MediaWiki core
36 */
37 protected $coreId = '';
38
39 protected static $extensionTypes = false;
40
41 protected static $viewvcUrls = array(
42 'svn+ssh://svn.wikimedia.org/svnroot/mediawiki' => 'http://svn.wikimedia.org/viewvc/mediawiki',
43 'http://svn.wikimedia.org/svnroot/mediawiki' => 'http://svn.wikimedia.org/viewvc/mediawiki',
44 'https://svn.wikimedia.org/svnroot/mediawiki' => 'https://svn.wikimedia.org/viewvc/mediawiki',
45 );
46
47 public function __construct() {
48 parent::__construct( 'Version' );
49 }
50
51 /**
52 * main()
53 */
54 public function execute( $par ) {
55 global $IP, $wgExtensionCredits;
56
57 $this->setHeaders();
58 $this->outputHeader();
59 $out = $this->getOutput();
60 $out->allowClickjacking();
61
62 // Explode the sub page information into useful bits
63 $parts = explode( '/', (string)$par );
64 $extNode = null;
65 if ( isset( $parts[1] ) ) {
66 $extName = str_replace( '_', ' ', $parts[1] );
67 // Find it!
68 foreach ( $wgExtensionCredits as $group => $extensions ) {
69 foreach ( $extensions as $ext ) {
70 if ( isset( $ext['name'] ) && ( $ext['name'] === $extName ) ) {
71 $extNode = &$ext;
72 break 2;
73 }
74 }
75 }
76 if ( !$extNode ) {
77 $out->setStatusCode( 404 );
78 }
79 } else {
80 $extName = 'MediaWiki';
81 }
82
83 // Now figure out what to do
84 switch ( strtolower( $parts[0] ) ) {
85 case 'credits':
86 $wikiText = '{{int:version-credits-not-found}}';
87 if ( $extName === 'MediaWiki' ) {
88 $wikiText = file_get_contents( $IP . '/CREDITS' );
89 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
90 $file = $this->getExtAuthorsFileName( dirname( $extNode['path'] ) );
91 if ( $file ) {
92 $wikiText = file_get_contents( $file );
93 if ( substr( $file, -4 ) === '.txt' ) {
94 $wikiText = Html::element( 'pre', array(), $wikiText );
95 }
96 }
97 }
98
99 $out->setPageTitle( $this->msg( 'version-credits-title', $extName ) );
100 $out->addWikiText( $wikiText );
101 break;
102
103 case 'license':
104 $wikiText = '{{int:version-license-not-found}}';
105 if ( $extName === 'MediaWiki' ) {
106 $wikiText = file_get_contents( $IP . '/COPYING' );
107 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
108 $file = $this->getExtLicenseFileName( dirname( $extNode['path'] ) );
109 if ( $file ) {
110 $wikiText = file_get_contents( $file );
111 if ( !isset( $extNode['license-name'] ) ) {
112 // If the developer did not explicitly set license-name they probably
113 // are unaware that we're now sucking this file in and thus it's probably
114 // not wikitext friendly.
115 $wikiText = "<pre>$wikiText</pre>";
116 }
117 }
118 }
119
120 $out->setPageTitle( $this->msg( 'version-license-title', $extName ) );
121 $out->addWikiText( $wikiText );
122 break;
123
124 default:
125 $out->addModules( 'mediawiki.special.version' );
126 $out->addWikiText(
127 $this->getMediaWikiCredits() .
128 $this->softwareInformation() .
129 $this->getEntryPointInfo()
130 );
131 $out->addHtml(
132 $this->getSkinCredits() .
133 $this->getExtensionCredits() .
134 $this->getParserTags() .
135 $this->getParserFunctionHooks()
136 );
137 $out->addWikiText( $this->getWgHooks() );
138 $out->addHTML( $this->IPInfo() );
139
140 break;
141 }
142 }
143
144 /**
145 * Returns wiki text showing the license information.
146 *
147 * @return string
148 */
149 private static function getMediaWikiCredits() {
150 $ret = Xml::element(
151 'h2',
152 array( 'id' => 'mw-version-license' ),
153 wfMessage( 'version-license' )->text()
154 );
155
156 // This text is always left-to-right.
157 $ret .= '<div class="plainlinks">';
158 $ret .= "__NOTOC__
159 " . self::getCopyrightAndAuthorList() . "\n
160 " . wfMessage( 'version-license-info' )->text();
161 $ret .= '</div>';
162
163 return str_replace( "\t\t", '', $ret ) . "\n";
164 }
165
166 /**
167 * Get the "MediaWiki is copyright 2001-20xx by lots of cool guys" text
168 *
169 * @return string
170 */
171 public static function getCopyrightAndAuthorList() {
172 global $wgLang;
173
174 if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
175 $othersLink = '[//www.mediawiki.org/wiki/Special:Version/Credits ' .
176 wfMessage( 'version-poweredby-others' )->text() . ']';
177 } else {
178 $othersLink = '[[Special:Version/Credits|' .
179 wfMessage( 'version-poweredby-others' )->text() . ']]';
180 }
181
182 $translatorsLink = '[//translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
183 wfMessage( 'version-poweredby-translators' )->text() . ']';
184
185 $authorList = array(
186 'Magnus Manske', 'Brion Vibber', 'Lee Daniel Crocker',
187 'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
188 'Niklas Laxström', 'Domas Mituzas', 'Rob Church', 'Yuri Astrakhan',
189 'Aryeh Gregor', 'Aaron Schulz', 'Andrew Garrett', 'Raimond Spekking',
190 'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
191 'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
192 'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
193 'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', $othersLink,
194 $translatorsLink
195 );
196
197 return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
198 $wgLang->listToText( $authorList ) )->text();
199 }
200
201 /**
202 * Returns wiki text showing the third party software versions (apache, php, mysql).
203 *
204 * @return string
205 */
206 static function softwareInformation() {
207 $dbr = wfGetDB( DB_SLAVE );
208
209 // Put the software in an array of form 'name' => 'version'. All messages should
210 // be loaded here, so feel free to use wfMessage in the 'name'. Raw HTML or
211 // wikimarkup can be used.
212 $software = array();
213 $software['[https://www.mediawiki.org/ MediaWiki]'] = self::getVersionLinked();
214 $phpKey = wfIsHHVM() ? '[http://hhvm.com/ HHVM]' :
215 '[http://www.php.net/ PHP]';
216 $software[$phpKey] = PHP_VERSION . " (" . PHP_SAPI . ")";
217 $software[$dbr->getSoftwareLink()] = $dbr->getServerInfo();
218
219 // Allow a hook to add/remove items.
220 wfRunHooks( 'SoftwareInfo', array( &$software ) );
221
222 $out = Xml::element(
223 'h2',
224 array( 'id' => 'mw-version-software' ),
225 wfMessage( 'version-software' )->text()
226 ) .
227 Xml::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ) ) .
228 "<tr>
229 <th>" . wfMessage( 'version-software-product' )->text() . "</th>
230 <th>" . wfMessage( 'version-software-version' )->text() . "</th>
231 </tr>\n";
232
233 foreach ( $software as $name => $version ) {
234 $out .= "<tr>
235 <td>" . $name . "</td>
236 <td dir=\"ltr\">" . $version . "</td>
237 </tr>\n";
238 }
239
240 return $out . Xml::closeElement( 'table' );
241 }
242
243 /**
244 * Return a string of the MediaWiki version with SVN revision if available.
245 *
246 * @param string $flags
247 * @return mixed
248 */
249 public static function getVersion( $flags = '' ) {
250 global $wgVersion, $IP;
251 wfProfileIn( __METHOD__ );
252
253 $gitInfo = self::getGitHeadSha1( $IP );
254 $svnInfo = self::getSvnInfo( $IP );
255 if ( !$svnInfo && !$gitInfo ) {
256 $version = $wgVersion;
257 } elseif ( $gitInfo && $flags === 'nodb' ) {
258 $shortSha1 = substr( $gitInfo, 0, 7 );
259 $version = "$wgVersion ($shortSha1)";
260 } elseif ( $gitInfo ) {
261 $shortSha1 = substr( $gitInfo, 0, 7 );
262 $shortSha1 = wfMessage( 'parentheses' )->params( $shortSha1 )->escaped();
263 $version = "$wgVersion $shortSha1";
264 } elseif ( $flags === 'nodb' ) {
265 $version = "$wgVersion (r{$svnInfo['checkout-rev']})";
266 } else {
267 $version = $wgVersion . ' ' .
268 wfMessage(
269 'version-svn-revision',
270 isset( $info['directory-rev'] ) ? $info['directory-rev'] : '',
271 $info['checkout-rev']
272 )->text();
273 }
274
275 wfProfileOut( __METHOD__ );
276
277 return $version;
278 }
279
280 /**
281 * Return a wikitext-formatted string of the MediaWiki version with a link to
282 * the SVN revision or the git SHA1 of head if available.
283 * Git is prefered over Svn
284 * The fallback is just $wgVersion
285 *
286 * @return mixed
287 */
288 public static function getVersionLinked() {
289 global $wgVersion;
290 wfProfileIn( __METHOD__ );
291
292 $gitVersion = self::getVersionLinkedGit();
293 if ( $gitVersion ) {
294 $v = $gitVersion;
295 } else {
296 $svnVersion = self::getVersionLinkedSvn();
297 if ( $svnVersion ) {
298 $v = $svnVersion;
299 } else {
300 $v = $wgVersion; // fallback
301 }
302 }
303
304 wfProfileOut( __METHOD__ );
305
306 return $v;
307 }
308
309 /**
310 * @return string Global wgVersion + a link to subversion revision of svn BASE
311 */
312 private static function getVersionLinkedSvn() {
313 global $IP;
314
315 $info = self::getSvnInfo( $IP );
316 if ( !isset( $info['checkout-rev'] ) ) {
317 return false;
318 }
319
320 $linkText = wfMessage(
321 'version-svn-revision',
322 isset( $info['directory-rev'] ) ? $info['directory-rev'] : '',
323 $info['checkout-rev']
324 )->text();
325
326 if ( isset( $info['viewvc-url'] ) ) {
327 $version = "[{$info['viewvc-url']} $linkText]";
328 } else {
329 $version = $linkText;
330 }
331
332 return self::getwgVersionLinked() . " $version";
333 }
334
335 /**
336 * @return string
337 */
338 private static function getwgVersionLinked() {
339 global $wgVersion;
340 $versionUrl = "";
341 if ( wfRunHooks( 'SpecialVersionVersionUrl', array( $wgVersion, &$versionUrl ) ) ) {
342 $versionParts = array();
343 preg_match( "/^(\d+\.\d+)/", $wgVersion, $versionParts );
344 $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
345 }
346
347 return "[$versionUrl $wgVersion]";
348 }
349
350 /**
351 * @since 1.22 Returns the HEAD date in addition to the sha1 and link
352 * @return bool|string Global wgVersion + HEAD sha1 stripped to the first 7 chars
353 * with link and date, or false on failure
354 */
355 private static function getVersionLinkedGit() {
356 global $IP, $wgLang;
357
358 $gitInfo = new GitInfo( $IP );
359 $headSHA1 = $gitInfo->getHeadSHA1();
360 if ( !$headSHA1 ) {
361 return false;
362 }
363
364 $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
365
366 $gitHeadUrl = $gitInfo->getHeadViewUrl();
367 if ( $gitHeadUrl !== false ) {
368 $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
369 }
370
371 $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
372 if ( $gitHeadCommitDate ) {
373 $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( $gitHeadCommitDate, true );
374 }
375
376 return self::getwgVersionLinked() . " $shortSHA1";
377 }
378
379 /**
380 * Returns an array with the base extension types.
381 * Type is stored as array key, the message as array value.
382 *
383 * TODO: ideally this would return all extension types.
384 *
385 * @since 1.17
386 *
387 * @return array
388 */
389 public static function getExtensionTypes() {
390 if ( self::$extensionTypes === false ) {
391 self::$extensionTypes = array(
392 'specialpage' => wfMessage( 'version-specialpages' )->text(),
393 'parserhook' => wfMessage( 'version-parserhooks' )->text(),
394 'variable' => wfMessage( 'version-variables' )->text(),
395 'media' => wfMessage( 'version-mediahandlers' )->text(),
396 'antispam' => wfMessage( 'version-antispam' )->text(),
397 'skin' => wfMessage( 'version-skins' )->text(),
398 'api' => wfMessage( 'version-api' )->text(),
399 'other' => wfMessage( 'version-other' )->text(),
400 );
401
402 wfRunHooks( 'ExtensionTypes', array( &self::$extensionTypes ) );
403 }
404
405 return self::$extensionTypes;
406 }
407
408 /**
409 * Returns the internationalized name for an extension type.
410 *
411 * @since 1.17
412 *
413 * @param string $type
414 *
415 * @return string
416 */
417 public static function getExtensionTypeName( $type ) {
418 $types = self::getExtensionTypes();
419
420 return isset( $types[$type] ) ? $types[$type] : $types['other'];
421 }
422
423 /**
424 * Generate wikitext showing the name, URL, author and description of each extension.
425 *
426 * @return string Wikitext
427 */
428 function getExtensionCredits() {
429 global $wgExtensionCredits;
430
431 if ( !count( $wgExtensionCredits ) ) {
432 return '';
433 }
434
435 $extensionTypes = self::getExtensionTypes();
436
437 $out = Xml::element(
438 'h2',
439 array( 'id' => 'mw-version-ext' ),
440 $this->msg( 'version-extensions' )->text()
441 ) .
442 Xml::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'sv-ext' ) );
443
444 // Make sure the 'other' type is set to an array.
445 if ( !array_key_exists( 'other', $wgExtensionCredits ) ) {
446 $wgExtensionCredits['other'] = array();
447 }
448
449 // Find all extensions that do not have a valid type and give them the type 'other'.
450 foreach ( $wgExtensionCredits as $type => $extensions ) {
451 if ( !array_key_exists( $type, $extensionTypes ) ) {
452 $wgExtensionCredits['other'] = array_merge( $wgExtensionCredits['other'], $extensions );
453 }
454 }
455
456 $this->firstExtOpened = false;
457 // Loop through the extension categories to display their extensions in the list.
458 foreach ( $extensionTypes as $type => $message ) {
459 // Skins have a separate section
460 if ( $type !== 'other' && $type !== 'skin' ) {
461 $out .= $this->getExtensionCategory( $type, $message );
462 }
463 }
464
465 // We want the 'other' type to be last in the list.
466 $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'] );
467
468 $out .= Xml::closeElement( 'table' );
469
470 return $out;
471 }
472
473 /**
474 * Generate wikitext showing the name, URL, author and description of each skin.
475 *
476 * @return string Wikitext
477 */
478 function getSkinCredits() {
479 $out = Xml::element(
480 'h2',
481 array( 'id' => 'mw-version-skin' ),
482 $this->msg( 'version-skins' )->text()
483 ) .
484 Xml::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'sv-skin' ) );
485
486 $this->firstExtOpened = false;
487 $out .= $this->getExtensionCategory( 'skin', null );
488
489 $out .= Xml::closeElement( 'table' );
490
491 return $out;
492 }
493
494 /**
495 * Obtains a list of installed parser tags and the associated H2 header
496 *
497 * @return string HTML output
498 */
499 protected function getParserTags() {
500 global $wgParser;
501
502 $tags = $wgParser->getTags();
503
504 if ( count( $tags ) ) {
505 $out = Html::rawElement(
506 'h2',
507 array( 'class' => 'mw-headline' ),
508 Linker::makeExternalLink(
509 '//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
510 $this->msg( 'version-parser-extensiontags' )->parse(),
511 false /* msg()->parse() already escapes */
512 )
513 );
514
515 array_walk( $tags, function ( &$value ) {
516 $value = '&lt;' . htmlspecialchars( $value ) . '&gt;';
517 } );
518 $out .= $this->listToText( $tags );
519 } else {
520 $out = '';
521 }
522
523 return $out;
524 }
525
526 /**
527 * Obtains a list of installed parser function hooks and the associated H2 header
528 *
529 * @return string HTML output
530 */
531 protected function getParserFunctionHooks() {
532 global $wgParser;
533
534 $fhooks = $wgParser->getFunctionHooks();
535 if ( count( $fhooks ) ) {
536 $out = Html::rawElement( 'h2', array( 'class' => 'mw-headline' ), Linker::makeExternalLink(
537 '//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
538 $this->msg( 'version-parser-function-hooks' )->parse(),
539 false /* msg()->parse() already escapes */
540 ) );
541
542 $out .= $this->listToText( $fhooks );
543 } else {
544 $out = '';
545 }
546
547 return $out;
548 }
549
550 /**
551 * Creates and returns the HTML for a single extension category.
552 *
553 * @since 1.17
554 *
555 * @param string $type
556 * @param string $message
557 *
558 * @return string
559 */
560 protected function getExtensionCategory( $type, $message ) {
561 global $wgExtensionCredits;
562
563 $out = '';
564
565 if ( array_key_exists( $type, $wgExtensionCredits ) && count( $wgExtensionCredits[$type] ) > 0 ) {
566 $out .= $this->openExtType( $message, 'credits-' . $type );
567
568 usort( $wgExtensionCredits[$type], array( $this, 'compare' ) );
569
570 foreach ( $wgExtensionCredits[$type] as $extension ) {
571 $out .= $this->getCreditsForExtension( $extension );
572 }
573 }
574
575 return $out;
576 }
577
578 /**
579 * Callback to sort extensions by type.
580 * @param array $a
581 * @param array $b
582 * @return int
583 */
584 function compare( $a, $b ) {
585 if ( $a['name'] === $b['name'] ) {
586 return 0;
587 } else {
588 return $this->getLanguage()->lc( $a['name'] ) > $this->getLanguage()->lc( $b['name'] )
589 ? 1
590 : -1;
591 }
592 }
593
594 /**
595 * Creates and formats a version line for a single extension.
596 *
597 * Information for five columns will be created. Parameters required in the
598 * $extension array for part rendering are indicated in ()
599 * - The name of (name), and URL link to (url), the extension
600 * - Official version number (version) and if available version control system
601 * revision (path), link, and date
602 * - If available the short name of the license (license-name) and a linke
603 * to ((LICENSE)|(COPYING))(\.txt)? if it exists.
604 * - Description of extension (descriptionmsg or description)
605 * - List of authors (author) and link to a ((AUTHORS)|(CREDITS))(\.txt)? file if it exists
606 *
607 * @param array $extension
608 *
609 * @return string Raw HTML
610 */
611 function getCreditsForExtension( array $extension ) {
612 $out = $this->getOutput();
613
614 // We must obtain the information for all the bits and pieces!
615 // ... such as extension names and links
616 if ( isset( $extension['namemsg'] ) ) {
617 // Localized name of extension
618 $extensionName = $this->msg( $extension['namemsg'] )->text();
619 } elseif ( isset( $extension['name'] ) ) {
620 // Non localized version
621 $extensionName = $extension['name'];
622 } else {
623 $extensionName = $this->msg( 'version-no-ext-name' )->text();
624 }
625
626 if ( isset( $extension['url'] ) ) {
627 $extensionNameLink = Linker::makeExternalLink(
628 $extension['url'],
629 $extensionName,
630 true,
631 '',
632 array( 'class' => 'mw-version-ext-name' )
633 );
634 } else {
635 $extensionNameLink = $extensionName;
636 }
637
638 // ... and the version information
639 // If the extension path is set we will check that directory for GIT and SVN
640 // metadata in an attempt to extract date and vcs commit metadata.
641 $canonicalVersion = '&ndash;';
642 $extensionPath = null;
643 $vcsVersion = null;
644 $vcsLink = null;
645 $vcsDate = null;
646
647 if ( isset( $extension['version'] ) ) {
648 $canonicalVersion = $out->parseInline( $extension['version'] );
649 }
650
651 if ( isset( $extension['path'] ) ) {
652 global $IP;
653 $extensionPath = dirname( $extension['path'] );
654 if ( $this->coreId == '' ) {
655 wfDebug( 'Looking up core head id' );
656 $coreHeadSHA1 = self::getGitHeadSha1( $IP );
657 if ( $coreHeadSHA1 ) {
658 $this->coreId = $coreHeadSHA1;
659 } else {
660 $svnInfo = self::getSvnInfo( $IP );
661 if ( $svnInfo !== false ) {
662 $this->coreId = $svnInfo['checkout-rev'];
663 }
664 }
665 }
666 $cache = wfGetCache( CACHE_ANYTHING );
667 $memcKey = wfMemcKey( 'specialversion-ext-version-text', $extension['path'], $this->coreId );
668 list( $vcsVersion, $vcsLink, $vcsDate ) = $cache->get( $memcKey );
669
670 if ( !$vcsVersion ) {
671 wfDebug( "Getting VCS info for extension $extensionName" );
672 $gitInfo = new GitInfo( $extensionPath );
673 $vcsVersion = $gitInfo->getHeadSHA1();
674 if ( $vcsVersion !== false ) {
675 $vcsVersion = substr( $vcsVersion, 0, 7 );
676 $vcsLink = $gitInfo->getHeadViewUrl();
677 $vcsDate = $gitInfo->getHeadCommitDate();
678 } else {
679 $svnInfo = self::getSvnInfo( $extensionPath );
680 if ( $svnInfo !== false ) {
681 $vcsVersion = $this->msg( 'version-svn-revision', $svnInfo['checkout-rev'] )->text();
682 $vcsLink = isset( $svnInfo['viewvc-url'] ) ? $svnInfo['viewvc-url'] : '';
683 }
684 }
685 $cache->set( $memcKey, array( $vcsVersion, $vcsLink, $vcsDate ), 60 * 60 * 24 );
686 } else {
687 wfDebug( "Pulled VCS info for extension $extensionName from cache" );
688 }
689 }
690
691 $versionString = Html::rawElement(
692 'span',
693 array( 'class' => 'mw-version-ext-version' ),
694 $canonicalVersion
695 );
696
697 if ( $vcsVersion ) {
698 if ( $vcsLink ) {
699 $vcsVerString = Linker::makeExternalLink(
700 $vcsLink,
701 $this->msg( 'version-version', $vcsVersion ),
702 true,
703 '',
704 array( 'class' => 'mw-version-ext-vcs-version' )
705 );
706 } else {
707 $vcsVerString = Html::element( 'span',
708 array( 'class' => 'mw-version-ext-vcs-version' ),
709 "({$vcsVersion})"
710 );
711 }
712 $versionString .= " {$vcsVerString}";
713
714 if ( $vcsDate ) {
715 $vcsTimeString = Html::element( 'span',
716 array( 'class' => 'mw-version-ext-vcs-timestamp' ),
717 $this->getLanguage()->timeanddate( $vcsDate )
718 );
719 $versionString .= " {$vcsTimeString}";
720 }
721 $versionString = Html::rawElement( 'span',
722 array( 'class' => 'mw-version-ext-meta-version' ),
723 $versionString
724 );
725 }
726
727 // ... and license information; if a license file exists we
728 // will link to it
729 $licenseLink = '';
730 if ( isset( $extension['license-name'] ) ) {
731 $licenseLink = Linker::link(
732 $this->getPageTitle( 'License/' . $extensionName ),
733 $out->parseInline( $extension['license-name'] ),
734 array( 'class' => 'mw-version-ext-license' )
735 );
736 } elseif ( $this->getExtLicenseFileName( $extensionPath ) ) {
737 $licenseLink = Linker::link(
738 $this->getPageTitle( 'License/' . $extensionName ),
739 $this->msg( 'version-ext-license' ),
740 array( 'class' => 'mw-version-ext-license' )
741 );
742 }
743
744 // ... and generate the description; which can be a parameterized l10n message
745 // in the form array( <msgname>, <parameter>, <parameter>... ) or just a straight
746 // up string
747 if ( isset( $extension['descriptionmsg'] ) ) {
748 // Localized description of extension
749 $descriptionMsg = $extension['descriptionmsg'];
750
751 if ( is_array( $descriptionMsg ) ) {
752 $descriptionMsgKey = $descriptionMsg[0]; // Get the message key
753 array_shift( $descriptionMsg ); // Shift out the message key to get the parameters only
754 array_map( "htmlspecialchars", $descriptionMsg ); // For sanity
755 $description = $this->msg( $descriptionMsgKey, $descriptionMsg )->text();
756 } else {
757 $description = $this->msg( $descriptionMsg )->text();
758 }
759 } elseif ( isset( $extension['description'] ) ) {
760 // Non localized version
761 $description = $extension['description'];
762 } else {
763 $description = '';
764 }
765 $description = $out->parseInline( $description );
766
767 // ... now get the authors for this extension
768 $authors = isset( $extension['author'] ) ? $extension['author'] : array();
769 $authors = $this->listAuthors( $authors, $extensionName, $extensionPath );
770
771 // Finally! Create the table
772 $html = Html::openElement( 'tr', array(
773 'class' => 'mw-version-ext',
774 'id' => "mw-version-ext-{$extensionName}"
775 )
776 );
777
778 $html .= Html::rawElement( 'td', array(), $extensionNameLink );
779 $html .= Html::rawElement( 'td', array(), $versionString );
780 $html .= Html::rawElement( 'td', array(), $licenseLink );
781 $html .= Html::rawElement( 'td', array( 'class' => 'mw-version-ext-description' ), $description );
782 $html .= Html::rawElement( 'td', array( 'class' => 'mw-version-ext-authors' ), $authors );
783
784 $html .= Html::closeElement( 'td' );
785
786 return $html;
787 }
788
789 /**
790 * Generate wikitext showing hooks in $wgHooks.
791 *
792 * @return string Wikitext
793 */
794 private function getWgHooks() {
795 global $wgSpecialVersionShowHooks, $wgHooks;
796
797 if ( $wgSpecialVersionShowHooks && count( $wgHooks ) ) {
798 $myWgHooks = $wgHooks;
799 ksort( $myWgHooks );
800
801 $ret = array();
802 $ret[] = '== {{int:version-hooks}} ==';
803 $ret[] = Html::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-hooks' ) );
804 $ret[] = Html::openElement( 'tr' );
805 $ret[] = Html::element( 'th', array(), $this->msg( 'version-hook-name' )->text() );
806 $ret[] = Html::element( 'th', array(), $this->msg( 'version-hook-subscribedby' )->text() );
807 $ret[] = Html::closeElement( 'tr' );
808
809 foreach ( $myWgHooks as $hook => $hooks ) {
810 $ret[] = Html::openElement( 'tr' );
811 $ret[] = Html::element( 'td', array(), $hook );
812 $ret[] = Html::element( 'td', array(), $this->listToText( $hooks ) );
813 $ret[] = Html::closeElement( 'tr' );
814 }
815
816 $ret[] = Html::closeElement( 'table' );
817
818 return implode( "\n", $ret );
819 } else {
820 return '';
821 }
822 }
823
824 private function openExtType( $text = null, $name = null ) {
825 $out = '';
826
827 $opt = array( 'colspan' => 5 );
828 if ( $this->firstExtOpened ) {
829 // Insert a spacing line
830 $out .= Html::rawElement( 'tr', array( 'class' => 'sv-space' ),
831 Html::element( 'td', $opt )
832 );
833 }
834 $this->firstExtOpened = true;
835
836 if ( $name ) {
837 $opt['id'] = "sv-$name";
838 }
839
840 if ( $text !== null ) {
841 $out .= Html::rawElement( 'tr', array(),
842 Html::element( 'th', $opt, $text )
843 );
844 }
845
846 $firstHeadingMsg = ( $name === 'credits-skin' )
847 ? 'version-skin-colheader-name'
848 : 'version-ext-colheader-name';
849 $out .= Html::openElement( 'tr' );
850 $out .= Html::element( 'th', array( 'class' => 'mw-version-ext-col-label' ),
851 $this->msg( $firstHeadingMsg )->text() );
852 $out .= Html::element( 'th', array( 'class' => 'mw-version-ext-col-label' ),
853 $this->msg( 'version-ext-colheader-version' )->text() );
854 $out .= Html::element( 'th', array( 'class' => 'mw-version-ext-col-label' ),
855 $this->msg( 'version-ext-colheader-license' )->text() );
856 $out .= Html::element( 'th', array( 'class' => 'mw-version-ext-col-label' ),
857 $this->msg( 'version-ext-colheader-description' )->text() );
858 $out .= Html::element( 'th', array( 'class' => 'mw-version-ext-col-label' ),
859 $this->msg( 'version-ext-colheader-credits' )->text() );
860 $out .= Html::closeElement( 'tr' );
861
862 return $out;
863 }
864
865 /**
866 * Get information about client's IP address.
867 *
868 * @return string HTML fragment
869 */
870 private function IPInfo() {
871 $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
872
873 return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
874 }
875
876 /**
877 * Return a formatted unsorted list of authors
878 *
879 * 'And Others'
880 * If an item in the $authors array is '...' it is assumed to indicate an
881 * 'and others' string which will then be linked to an ((AUTHORS)|(CREDITS))(\.txt)?
882 * file if it exists in $dir.
883 *
884 * Similarly an entry ending with ' ...]' is assumed to be a link to an
885 * 'and others' page.
886 *
887 * If no '...' string variant is found, but an authors file is found an
888 * 'and others' will be added to the end of the credits.
889 *
890 * @param string|array $authors
891 * @param string $extName Name of the extension for link creation
892 * @param string $extDir Path to the extension root directory
893 *
894 * @return string HTML fragment
895 */
896 function listAuthors( $authors, $extName, $extDir ) {
897 $hasOthers = false;
898
899 $list = array();
900 foreach ( (array)$authors as $item ) {
901 if ( $item == '...' ) {
902 $hasOthers = true;
903
904 if ( $this->getExtAuthorsFileName( $extDir ) ) {
905 $text = Linker::link(
906 $this->getPageTitle( "Credits/$extName" ),
907 $this->msg( 'version-poweredby-others' )->text()
908 );
909 } else {
910 $text = $this->msg( 'version-poweredby-others' )->text();
911 }
912 $list[] = $text;
913 } elseif ( substr( $item, -5 ) == ' ...]' ) {
914 $hasOthers = true;
915 $list[] = $this->getOutput()->parseInline(
916 substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
917 );
918 } else {
919 $list[] = $this->getOutput()->parseInline( $item );
920 }
921 }
922
923 if ( !$hasOthers && $this->getExtAuthorsFileName( $extDir ) ) {
924 $list[] = $text = Linker::link(
925 $this->getPageTitle( "Credits/$extName" ),
926 $this->msg( 'version-poweredby-others' )->text()
927 );
928 }
929
930 return $this->listToText( $list, false );
931 }
932
933 /**
934 * Obtains the full path of an extensions authors or credits file if
935 * one exists.
936 *
937 * @param string $extDir Path to the extensions root directory
938 *
939 * @since 1.23
940 *
941 * @return bool|string False if no such file exists, otherwise returns
942 * a path to it.
943 */
944 public static function getExtAuthorsFileName( $extDir ) {
945 if ( !$extDir ) {
946 return false;
947 }
948
949 foreach ( scandir( $extDir ) as $file ) {
950 $fullPath = $extDir . DIRECTORY_SEPARATOR . $file;
951 if ( preg_match( '/^((AUTHORS)|(CREDITS))(\.txt)?$/', $file ) &&
952 is_readable( $fullPath ) &&
953 is_file( $fullPath )
954 ) {
955 return $fullPath;
956 }
957 }
958
959 return false;
960 }
961
962 /**
963 * Obtains the full path of an extensions copying or license file if
964 * one exists.
965 *
966 * @param string $extDir Path to the extensions root directory
967 *
968 * @since 1.23
969 *
970 * @return bool|string False if no such file exists, otherwise returns
971 * a path to it.
972 */
973 public static function getExtLicenseFileName( $extDir ) {
974 if ( !$extDir ) {
975 return false;
976 }
977
978 foreach ( scandir( $extDir ) as $file ) {
979 $fullPath = $extDir . DIRECTORY_SEPARATOR . $file;
980 if ( preg_match( '/^((COPYING)|(LICENSE))(\.txt)?$/', $file ) &&
981 is_readable( $fullPath ) &&
982 is_file( $fullPath )
983 ) {
984 return $fullPath;
985 }
986 }
987
988 return false;
989 }
990
991 /**
992 * Convert an array of items into a list for display.
993 *
994 * @param array $list List of elements to display
995 * @param bool $sort Whether to sort the items in $list
996 *
997 * @return string
998 */
999 function listToText( $list, $sort = true ) {
1000 if ( !count( $list ) ) {
1001 return '';
1002 }
1003 if ( $sort ) {
1004 sort( $list );
1005 }
1006
1007 return $this->getLanguage()
1008 ->listToText( array_map( array( __CLASS__, 'arrayToString' ), $list ) );
1009 }
1010
1011 /**
1012 * Convert an array or object to a string for display.
1013 *
1014 * @param mixed $list Will convert an array to string if given and return
1015 * the paramater unaltered otherwise
1016 *
1017 * @return mixed
1018 */
1019 public static function arrayToString( $list ) {
1020 if ( is_array( $list ) && count( $list ) == 1 ) {
1021 $list = $list[0];
1022 }
1023 if ( is_object( $list ) ) {
1024 $class = wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1025
1026 return $class;
1027 } elseif ( !is_array( $list ) ) {
1028 return $list;
1029 } else {
1030 if ( is_object( $list[0] ) ) {
1031 $class = get_class( $list[0] );
1032 } else {
1033 $class = $list[0];
1034 }
1035
1036 return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1037 }
1038 }
1039
1040 /**
1041 * Get an associative array of information about a given path, from its .svn
1042 * subdirectory. Returns false on error, such as if the directory was not
1043 * checked out with subversion.
1044 *
1045 * Returned keys are:
1046 * Required:
1047 * checkout-rev The revision which was checked out
1048 * Optional:
1049 * directory-rev The revision when the directory was last modified
1050 * url The subversion URL of the directory
1051 * repo-url The base URL of the repository
1052 * viewvc-url A ViewVC URL pointing to the checked-out revision
1053 * @param string $dir
1054 * @return array|bool
1055 */
1056 public static function getSvnInfo( $dir ) {
1057 // http://svnbook.red-bean.com/nightly/en/svn.developer.insidewc.html
1058 $entries = $dir . '/.svn/entries';
1059
1060 if ( !file_exists( $entries ) ) {
1061 return false;
1062 }
1063
1064 $lines = file( $entries );
1065 if ( !count( $lines ) ) {
1066 return false;
1067 }
1068
1069 // check if file is xml (subversion release <= 1.3) or not (subversion release = 1.4)
1070 if ( preg_match( '/^<\?xml/', $lines[0] ) ) {
1071 // subversion is release <= 1.3
1072 if ( !function_exists( 'simplexml_load_file' ) ) {
1073 // We could fall back to expat... YUCK
1074 return false;
1075 }
1076
1077 // SimpleXml whines about the xmlns...
1078 wfSuppressWarnings();
1079 $xml = simplexml_load_file( $entries );
1080 wfRestoreWarnings();
1081
1082 if ( $xml ) {
1083 foreach ( $xml->entry as $entry ) {
1084 if ( $xml->entry[0]['name'] == '' ) {
1085 // The directory entry should always have a revision marker.
1086 if ( $entry['revision'] ) {
1087 return array( 'checkout-rev' => intval( $entry['revision'] ) );
1088 }
1089 }
1090 }
1091 }
1092
1093 return false;
1094 }
1095
1096 // Subversion is release 1.4 or above.
1097 if ( count( $lines ) < 11 ) {
1098 return false;
1099 }
1100
1101 $info = array(
1102 'checkout-rev' => intval( trim( $lines[3] ) ),
1103 'url' => trim( $lines[4] ),
1104 'repo-url' => trim( $lines[5] ),
1105 'directory-rev' => intval( trim( $lines[10] ) )
1106 );
1107
1108 if ( isset( self::$viewvcUrls[$info['repo-url']] ) ) {
1109 $viewvc = str_replace(
1110 $info['repo-url'],
1111 self::$viewvcUrls[$info['repo-url']],
1112 $info['url']
1113 );
1114
1115 $viewvc .= '/?pathrev=';
1116 $viewvc .= urlencode( $info['checkout-rev'] );
1117 $info['viewvc-url'] = $viewvc;
1118 }
1119
1120 return $info;
1121 }
1122
1123 /**
1124 * Retrieve the revision number of a Subversion working directory.
1125 *
1126 * @param string $dir Directory of the svn checkout
1127 *
1128 * @return int Revision number
1129 */
1130 public static function getSvnRevision( $dir ) {
1131 $info = self::getSvnInfo( $dir );
1132
1133 if ( $info === false ) {
1134 return false;
1135 } elseif ( isset( $info['checkout-rev'] ) ) {
1136 return $info['checkout-rev'];
1137 } else {
1138 return false;
1139 }
1140 }
1141
1142 /**
1143 * @param string $dir Directory of the git checkout
1144 * @return bool|string Sha1 of commit HEAD points to
1145 */
1146 public static function getGitHeadSha1( $dir ) {
1147 $repo = new GitInfo( $dir );
1148
1149 return $repo->getHeadSHA1();
1150 }
1151
1152 /**
1153 * @param string $dir Directory of the git checkout
1154 * @return bool|string Branch currently checked out
1155 */
1156 public static function getGitCurrentBranch( $dir ) {
1157 $repo = new GitInfo( $dir );
1158 return $repo->getCurrentBranch();
1159 }
1160
1161 /**
1162 * Get the list of entry points and their URLs
1163 * @return string Wikitext
1164 */
1165 public function getEntryPointInfo() {
1166 global $wgArticlePath, $wgScriptPath;
1167 $scriptPath = $wgScriptPath ? $wgScriptPath : "/";
1168 $entryPoints = array(
1169 'version-entrypoints-articlepath' => $wgArticlePath,
1170 'version-entrypoints-scriptpath' => $scriptPath,
1171 'version-entrypoints-index-php' => wfScript( 'index' ),
1172 'version-entrypoints-api-php' => wfScript( 'api' ),
1173 'version-entrypoints-load-php' => wfScript( 'load' ),
1174 );
1175
1176 $language = $this->getLanguage();
1177 $thAttribures = array(
1178 'dir' => $language->getDir(),
1179 'lang' => $language->getCode()
1180 );
1181 $out = Html::element(
1182 'h2',
1183 array( 'id' => 'mw-version-entrypoints' ),
1184 $this->msg( 'version-entrypoints' )->text()
1185 ) .
1186 Html::openElement( 'table',
1187 array(
1188 'class' => 'wikitable plainlinks',
1189 'id' => 'mw-version-entrypoints-table',
1190 'dir' => 'ltr',
1191 'lang' => 'en'
1192 )
1193 ) .
1194 Html::openElement( 'tr' ) .
1195 Html::element(
1196 'th',
1197 $thAttribures,
1198 $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1199 ) .
1200 Html::element(
1201 'th',
1202 $thAttribures,
1203 $this->msg( 'version-entrypoints-header-url' )->text()
1204 ) .
1205 Html::closeElement( 'tr' );
1206
1207 foreach ( $entryPoints as $message => $value ) {
1208 $url = wfExpandUrl( $value, PROTO_RELATIVE );
1209 $out .= Html::openElement( 'tr' ) .
1210 // ->text() looks like it should be ->parse(), but this function
1211 // returns wikitext, not HTML, boo
1212 Html::rawElement( 'td', array(), $this->msg( $message )->text() ) .
1213 Html::rawElement( 'td', array(), Html::rawElement( 'code', array(), "[$url $value]" ) ) .
1214 Html::closeElement( 'tr' );
1215 }
1216
1217 $out .= Html::closeElement( 'table' );
1218
1219 return $out;
1220 }
1221
1222 protected function getGroupName() {
1223 return 'wiki';
1224 }
1225 }