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