Completed
Branch master (939199)
by
unknown
39:35
created

includes/specials/SpecialVersion.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
	public function __construct() {
42
		parent::__construct( 'Version' );
43
	}
44
45
	/**
46
	 * main()
47
	 * @param string|null $par
48
	 */
49
	public function execute( $par ) {
50
		global $IP, $wgExtensionCredits;
51
52
		$this->setHeaders();
53
		$this->outputHeader();
54
		$out = $this->getOutput();
55
		$out->allowClickjacking();
56
57
		// Explode the sub page information into useful bits
58
		$parts = explode( '/', (string)$par );
59
		$extNode = null;
60
		if ( isset( $parts[1] ) ) {
61
			$extName = str_replace( '_', ' ', $parts[1] );
62
			// Find it!
63
			foreach ( $wgExtensionCredits as $group => $extensions ) {
64
				foreach ( $extensions as $ext ) {
65
					if ( isset( $ext['name'] ) && ( $ext['name'] === $extName ) ) {
66
						$extNode = &$ext;
67
						break 2;
68
					}
69
				}
70
			}
71
			if ( !$extNode ) {
72
				$out->setStatusCode( 404 );
73
			}
74
		} else {
75
			$extName = 'MediaWiki';
76
		}
77
78
		// Now figure out what to do
79
		switch ( strtolower( $parts[0] ) ) {
80
			case 'credits':
81
				$wikiText = '{{int:version-credits-not-found}}';
82
				if ( $extName === 'MediaWiki' ) {
83
					$wikiText = file_get_contents( $IP . '/CREDITS' );
84 View Code Duplication
				} elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
85
					$file = $this->getExtAuthorsFileName( dirname( $extNode['path'] ) );
86
					if ( $file ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $file of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
87
						$wikiText = file_get_contents( $file );
88
						if ( substr( $file, -4 ) === '.txt' ) {
89
							$wikiText = Html::element(
90
								'pre',
91
								[
92
									'lang' => 'en',
93
									'dir' => 'ltr',
94
								],
95
								$wikiText
96
							);
97
						}
98
					}
99
				}
100
101
				$out->setPageTitle( $this->msg( 'version-credits-title', $extName ) );
102
				$out->addWikiText( $wikiText );
103
				break;
104
105
			case 'license':
106
				$wikiText = '{{int:version-license-not-found}}';
107 View Code Duplication
				if ( $extName === 'MediaWiki' ) {
108
					$wikiText = file_get_contents( $IP . '/COPYING' );
109
				} elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
110
					$file = $this->getExtLicenseFileName( dirname( $extNode['path'] ) );
111
					if ( $file ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $file of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
112
						$wikiText = file_get_contents( $file );
113
						$wikiText = Html::element(
114
							'pre',
115
							[
116
								'lang' => 'en',
117
								'dir' => 'ltr',
118
							],
119
							$wikiText
120
						);
121
					}
122
				}
123
124
				$out->setPageTitle( $this->msg( 'version-license-title', $extName ) );
125
				$out->addWikiText( $wikiText );
126
				break;
127
128
			default:
129
				$out->addModuleStyles( 'mediawiki.special.version' );
130
				$out->addWikiText(
131
					$this->getMediaWikiCredits() .
132
					$this->softwareInformation() .
133
					$this->getEntryPointInfo()
134
				);
135
				$out->addHTML(
136
					$this->getSkinCredits() .
137
					$this->getExtensionCredits() .
138
					$this->getExternalLibraries() .
139
					$this->getParserTags() .
140
					$this->getParserFunctionHooks()
141
				);
142
				$out->addWikiText( $this->getWgHooks() );
143
				$out->addHTML( $this->IPInfo() );
144
145
				break;
146
		}
147
	}
148
149
	/**
150
	 * Returns wiki text showing the license information.
151
	 *
152
	 * @return string
153
	 */
154
	private static function getMediaWikiCredits() {
155
		$ret = Xml::element(
156
			'h2',
157
			[ 'id' => 'mw-version-license' ],
158
			wfMessage( 'version-license' )->text()
159
		);
160
161
		// This text is always left-to-right.
162
		$ret .= '<div class="plainlinks">';
163
		$ret .= "__NOTOC__
164
		" . self::getCopyrightAndAuthorList() . "\n
165
		" . wfMessage( 'version-license-info' )->text();
166
		$ret .= '</div>';
167
168
		return str_replace( "\t\t", '', $ret ) . "\n";
169
	}
170
171
	/**
172
	 * Get the "MediaWiki is copyright 2001-20xx by lots of cool guys" text
173
	 *
174
	 * @return string
175
	 */
176
	public static function getCopyrightAndAuthorList() {
177
		global $wgLang;
178
179
		if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
180
			$othersLink = '[https://www.mediawiki.org/wiki/Special:Version/Credits ' .
181
				wfMessage( 'version-poweredby-others' )->text() . ']';
182
		} else {
183
			$othersLink = '[[Special:Version/Credits|' .
184
				wfMessage( 'version-poweredby-others' )->text() . ']]';
185
		}
186
187
		$translatorsLink = '[https://translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
188
			wfMessage( 'version-poweredby-translators' )->text() . ']';
189
190
		$authorList = [
191
			'Magnus Manske', 'Brion Vibber', 'Lee Daniel Crocker',
192
			'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
193
			'Niklas Laxström', 'Domas Mituzas', 'Rob Church', 'Yuri Astrakhan',
194
			'Aryeh Gregor', 'Aaron Schulz', 'Andrew Garrett', 'Raimond Spekking',
195
			'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
196
			'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
197
			'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
198
			'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', 'Brad Jorsch',
199
			$othersLink, $translatorsLink
200
		];
201
202
		return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
203
			$wgLang->listToText( $authorList ) )->text();
204
	}
205
206
	/**
207
	 * Returns wiki text showing the third party software versions (apache, php, mysql).
208
	 *
209
	 * @return string
210
	 */
211
	public static function softwareInformation() {
212
		$dbr = wfGetDB( DB_REPLICA );
213
214
		// Put the software in an array of form 'name' => 'version'. All messages should
215
		// be loaded here, so feel free to use wfMessage in the 'name'. Raw HTML or
216
		// wikimarkup can be used.
217
		$software = [];
218
		$software['[https://www.mediawiki.org/ MediaWiki]'] = self::getVersionLinked();
219
		if ( wfIsHHVM() ) {
220
			$software['[http://hhvm.com/ HHVM]'] = HHVM_VERSION . " (" . PHP_SAPI . ")";
221
		} else {
222
			$software['[https://php.net/ PHP]'] = PHP_VERSION . " (" . PHP_SAPI . ")";
223
		}
224
		$software[$dbr->getSoftwareLink()] = $dbr->getServerInfo();
225
226
		if ( IcuCollation::getICUVersion() ) {
227
			$software['[http://site.icu-project.org/ ICU]'] = IcuCollation::getICUVersion();
228
		}
229
230
		// Allow a hook to add/remove items.
231
		Hooks::run( 'SoftwareInfo', [ &$software ] );
232
233
		$out = Xml::element(
234
				'h2',
235
				[ 'id' => 'mw-version-software' ],
236
				wfMessage( 'version-software' )->text()
237
			) .
238
				Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ] ) .
239
				"<tr>
240
					<th>" . wfMessage( 'version-software-product' )->text() . "</th>
241
					<th>" . wfMessage( 'version-software-version' )->text() . "</th>
242
				</tr>\n";
243
244
		foreach ( $software as $name => $version ) {
245
			$out .= "<tr>
246
					<td>" . $name . "</td>
247
					<td dir=\"ltr\">" . $version . "</td>
248
				</tr>\n";
249
		}
250
251
		return $out . Xml::closeElement( 'table' );
252
	}
253
254
	/**
255
	 * Return a string of the MediaWiki version with Git revision if available.
256
	 *
257
	 * @param string $flags
258
	 * @param Language|string|null $lang
259
	 * @return mixed
260
	 */
261
	public static function getVersion( $flags = '', $lang = null ) {
262
		global $wgVersion, $IP;
263
264
		$gitInfo = self::getGitHeadSha1( $IP );
265
		if ( !$gitInfo ) {
266
			$version = $wgVersion;
267
		} elseif ( $flags === 'nodb' ) {
268
			$shortSha1 = substr( $gitInfo, 0, 7 );
269
			$version = "$wgVersion ($shortSha1)";
270
		} else {
271
			$shortSha1 = substr( $gitInfo, 0, 7 );
272
			$msg = wfMessage( 'parentheses' );
273
			if ( $lang !== null ) {
274
				$msg->inLanguage( $lang );
275
			}
276
			$shortSha1 = $msg->params( $shortSha1 )->escaped();
277
			$version = "$wgVersion $shortSha1";
278
		}
279
280
		return $version;
281
	}
282
283
	/**
284
	 * Return a wikitext-formatted string of the MediaWiki version with a link to
285
	 * the Git SHA1 of head if available.
286
	 * The fallback is just $wgVersion
287
	 *
288
	 * @return mixed
289
	 */
290
	public static function getVersionLinked() {
291
		global $wgVersion;
292
293
		$gitVersion = self::getVersionLinkedGit();
294
		if ( $gitVersion ) {
295
			$v = $gitVersion;
296
		} else {
297
			$v = $wgVersion; // fallback
298
		}
299
300
		return $v;
301
	}
302
303
	/**
304
	 * @return string
305
	 */
306
	private static function getwgVersionLinked() {
307
		global $wgVersion;
308
		$versionUrl = "";
309
		if ( Hooks::run( 'SpecialVersionVersionUrl', [ $wgVersion, &$versionUrl ] ) ) {
310
			$versionParts = [];
311
			preg_match( "/^(\d+\.\d+)/", $wgVersion, $versionParts );
312
			$versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
313
		}
314
315
		return "[$versionUrl $wgVersion]";
316
	}
317
318
	/**
319
	 * @since 1.22 Returns the HEAD date in addition to the sha1 and link
320
	 * @return bool|string Global wgVersion + HEAD sha1 stripped to the first 7 chars
321
	 *   with link and date, or false on failure
322
	 */
323
	private static function getVersionLinkedGit() {
324
		global $IP, $wgLang;
325
326
		$gitInfo = new GitInfo( $IP );
327
		$headSHA1 = $gitInfo->getHeadSHA1();
328
		if ( !$headSHA1 ) {
329
			return false;
330
		}
331
332
		$shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
333
334
		$gitHeadUrl = $gitInfo->getHeadViewUrl();
335
		if ( $gitHeadUrl !== false ) {
336
			$shortSHA1 = "[$gitHeadUrl $shortSHA1]";
337
		}
338
339
		$gitHeadCommitDate = $gitInfo->getHeadCommitDate();
340
		if ( $gitHeadCommitDate ) {
341
			$shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( $gitHeadCommitDate, true );
342
		}
343
344
		return self::getwgVersionLinked() . " $shortSHA1";
345
	}
346
347
	/**
348
	 * Returns an array with the base extension types.
349
	 * Type is stored as array key, the message as array value.
350
	 *
351
	 * TODO: ideally this would return all extension types.
352
	 *
353
	 * @since 1.17
354
	 *
355
	 * @return array
356
	 */
357
	public static function getExtensionTypes() {
358
		if ( self::$extensionTypes === false ) {
359
			self::$extensionTypes = [
360
				'specialpage' => wfMessage( 'version-specialpages' )->text(),
361
				'parserhook' => wfMessage( 'version-parserhooks' )->text(),
362
				'variable' => wfMessage( 'version-variables' )->text(),
363
				'media' => wfMessage( 'version-mediahandlers' )->text(),
364
				'antispam' => wfMessage( 'version-antispam' )->text(),
365
				'skin' => wfMessage( 'version-skins' )->text(),
366
				'api' => wfMessage( 'version-api' )->text(),
367
				'other' => wfMessage( 'version-other' )->text(),
368
			];
369
370
			Hooks::run( 'ExtensionTypes', [ &self::$extensionTypes ] );
371
		}
372
373
		return self::$extensionTypes;
374
	}
375
376
	/**
377
	 * Returns the internationalized name for an extension type.
378
	 *
379
	 * @since 1.17
380
	 *
381
	 * @param string $type
382
	 *
383
	 * @return string
384
	 */
385
	public static function getExtensionTypeName( $type ) {
386
		$types = self::getExtensionTypes();
387
388
		return isset( $types[$type] ) ? $types[$type] : $types['other'];
389
	}
390
391
	/**
392
	 * Generate wikitext showing the name, URL, author and description of each extension.
393
	 *
394
	 * @return string Wikitext
395
	 */
396
	public function getExtensionCredits() {
397
		global $wgExtensionCredits;
398
399 View Code Duplication
		if (
400
			count( $wgExtensionCredits ) === 0 ||
401
			// Skins are displayed separately, see getSkinCredits()
402
			( count( $wgExtensionCredits ) === 1 && isset( $wgExtensionCredits['skin'] ) )
403
		) {
404
			return '';
405
		}
406
407
		$extensionTypes = self::getExtensionTypes();
408
409
		$out = Xml::element(
410
				'h2',
411
				[ 'id' => 'mw-version-ext' ],
412
				$this->msg( 'version-extensions' )->text()
413
			) .
414
			Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-ext' ] );
415
416
		// Make sure the 'other' type is set to an array.
417
		if ( !array_key_exists( 'other', $wgExtensionCredits ) ) {
418
			$wgExtensionCredits['other'] = [];
419
		}
420
421
		// Find all extensions that do not have a valid type and give them the type 'other'.
422
		foreach ( $wgExtensionCredits as $type => $extensions ) {
423
			if ( !array_key_exists( $type, $extensionTypes ) ) {
424
				$wgExtensionCredits['other'] = array_merge( $wgExtensionCredits['other'], $extensions );
425
			}
426
		}
427
428
		$this->firstExtOpened = false;
429
		// Loop through the extension categories to display their extensions in the list.
430
		foreach ( $extensionTypes as $type => $message ) {
431
			// Skins have a separate section
432
			if ( $type !== 'other' && $type !== 'skin' ) {
433
				$out .= $this->getExtensionCategory( $type, $message );
434
			}
435
		}
436
437
		// We want the 'other' type to be last in the list.
438
		$out .= $this->getExtensionCategory( 'other', $extensionTypes['other'] );
439
440
		$out .= Xml::closeElement( 'table' );
441
442
		return $out;
443
	}
444
445
	/**
446
	 * Generate wikitext showing the name, URL, author and description of each skin.
447
	 *
448
	 * @return string Wikitext
449
	 */
450
	public function getSkinCredits() {
451
		global $wgExtensionCredits;
452 View Code Duplication
		if ( !isset( $wgExtensionCredits['skin'] ) || count( $wgExtensionCredits['skin'] ) === 0 ) {
453
			return '';
454
		}
455
456
		$out = Xml::element(
457
				'h2',
458
				[ 'id' => 'mw-version-skin' ],
459
				$this->msg( 'version-skins' )->text()
460
			) .
461
			Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-skin' ] );
462
463
		$this->firstExtOpened = false;
464
		$out .= $this->getExtensionCategory( 'skin', null );
465
466
		$out .= Xml::closeElement( 'table' );
467
468
		return $out;
469
	}
470
471
	/**
472
	 * Generate an HTML table for external libraries that are installed
473
	 *
474
	 * @return string
475
	 */
476
	protected function getExternalLibraries() {
477
		global $IP;
478
		$path = "$IP/vendor/composer/installed.json";
479
		if ( !file_exists( $path ) ) {
480
			return '';
481
		}
482
483
		$installed = new ComposerInstalled( $path );
484
		$out = Html::element(
485
			'h2',
486
			[ 'id' => 'mw-version-libraries' ],
487
			$this->msg( 'version-libraries' )->text()
488
		);
489
		$out .= Html::openElement(
490
			'table',
491
			[ 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries' ]
492
		);
493
		$out .= Html::openElement( 'tr' )
494
			. Html::element( 'th', [], $this->msg( 'version-libraries-library' )->text() )
495
			. Html::element( 'th', [], $this->msg( 'version-libraries-version' )->text() )
496
			. Html::element( 'th', [], $this->msg( 'version-libraries-license' )->text() )
497
			. Html::element( 'th', [], $this->msg( 'version-libraries-description' )->text() )
498
			. Html::element( 'th', [], $this->msg( 'version-libraries-authors' )->text() )
499
			. Html::closeElement( 'tr' );
500
501
		foreach ( $installed->getInstalledDependencies() as $name => $info ) {
502
			if ( strpos( $info['type'], 'mediawiki-' ) === 0 ) {
503
				// Skip any extensions or skins since they'll be listed
504
				// in their proper section
505
				continue;
506
			}
507
			$authors = array_map( function( $arr ) {
508
				// If a homepage is set, link to it
509
				if ( isset( $arr['homepage'] ) ) {
510
					return "[{$arr['homepage']} {$arr['name']}]";
511
				}
512
				return $arr['name'];
513
			}, $info['authors'] );
514
			$authors = $this->listAuthors( $authors, false, "$IP/vendor/$name" );
515
516
			// We can safely assume that the libraries' names and descriptions
517
			// are written in English and aren't going to be translated,
518
			// so set appropriate lang and dir attributes
519
			$out .= Html::openElement( 'tr' )
520
				. Html::rawElement(
521
					'td',
522
					[],
523
					Linker::makeExternalLink(
524
						"https://packagist.org/packages/$name", $name,
525
						true, '',
526
						[ 'class' => 'mw-version-library-name' ]
527
					)
528
				)
529
				. Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
530
				. Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['licenses'] ) )
531
				. Html::element( 'td', [ 'lang' => 'en', 'dir' => 'ltr' ], $info['description'] )
532
				. Html::rawElement( 'td', [], $authors )
533
				. Html::closeElement( 'tr' );
534
		}
535
		$out .= Html::closeElement( 'table' );
536
537
		return $out;
538
	}
539
540
	/**
541
	 * Obtains a list of installed parser tags and the associated H2 header
542
	 *
543
	 * @return string HTML output
544
	 */
545
	protected function getParserTags() {
546
		global $wgParser;
547
548
		$tags = $wgParser->getTags();
549
550
		if ( count( $tags ) ) {
551
			$out = Html::rawElement(
552
				'h2',
553
				[
554
					'class' => 'mw-headline plainlinks',
555
					'id' => 'mw-version-parser-extensiontags',
556
				],
557
				Linker::makeExternalLink(
558
					'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
559
					$this->msg( 'version-parser-extensiontags' )->parse(),
560
					false /* msg()->parse() already escapes */
561
				)
562
			);
563
564
			array_walk( $tags, function ( &$value ) {
565
				// Bidirectional isolation improves readability in RTL wikis
566
				$value = Html::element(
567
					'bdi',
568
					// Prevent < and > from slipping to another line
569
					[
570
						'style' => 'white-space: nowrap;',
571
					],
572
					"<$value>"
573
				);
574
			} );
575
576
			$out .= $this->listToText( $tags );
577
		} else {
578
			$out = '';
579
		}
580
581
		return $out;
582
	}
583
584
	/**
585
	 * Obtains a list of installed parser function hooks and the associated H2 header
586
	 *
587
	 * @return string HTML output
588
	 */
589
	protected function getParserFunctionHooks() {
590
		global $wgParser;
591
592
		$fhooks = $wgParser->getFunctionHooks();
593
		if ( count( $fhooks ) ) {
594
			$out = Html::rawElement(
595
				'h2',
596
				[
597
					'class' => 'mw-headline plainlinks',
598
					'id' => 'mw-version-parser-function-hooks',
599
				],
600
				Linker::makeExternalLink(
601
					'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
602
					$this->msg( 'version-parser-function-hooks' )->parse(),
603
					false /* msg()->parse() already escapes */
604
				)
605
			);
606
607
			$out .= $this->listToText( $fhooks );
608
		} else {
609
			$out = '';
610
		}
611
612
		return $out;
613
	}
614
615
	/**
616
	 * Creates and returns the HTML for a single extension category.
617
	 *
618
	 * @since 1.17
619
	 *
620
	 * @param string $type
621
	 * @param string $message
622
	 *
623
	 * @return string
624
	 */
625
	protected function getExtensionCategory( $type, $message ) {
626
		global $wgExtensionCredits;
627
628
		$out = '';
629
630
		if ( array_key_exists( $type, $wgExtensionCredits ) && count( $wgExtensionCredits[$type] ) > 0 ) {
631
			$out .= $this->openExtType( $message, 'credits-' . $type );
632
633
			usort( $wgExtensionCredits[$type], [ $this, 'compare' ] );
634
635
			foreach ( $wgExtensionCredits[$type] as $extension ) {
636
				$out .= $this->getCreditsForExtension( $type, $extension );
637
			}
638
		}
639
640
		return $out;
641
	}
642
643
	/**
644
	 * Callback to sort extensions by type.
645
	 * @param array $a
646
	 * @param array $b
647
	 * @return int
648
	 */
649
	public function compare( $a, $b ) {
650
		if ( $a['name'] === $b['name'] ) {
651
			return 0;
652
		} else {
653
			return $this->getLanguage()->lc( $a['name'] ) > $this->getLanguage()->lc( $b['name'] )
654
				? 1
655
				: -1;
656
		}
657
	}
658
659
	/**
660
	 * Creates and formats a version line for a single extension.
661
	 *
662
	 * Information for five columns will be created. Parameters required in the
663
	 * $extension array for part rendering are indicated in ()
664
	 *  - The name of (name), and URL link to (url), the extension
665
	 *  - Official version number (version) and if available version control system
666
	 *    revision (path), link, and date
667
	 *  - If available the short name of the license (license-name) and a link
668
	 *    to ((LICENSE)|(COPYING))(\.txt)? if it exists.
669
	 *  - Description of extension (descriptionmsg or description)
670
	 *  - List of authors (author) and link to a ((AUTHORS)|(CREDITS))(\.txt)? file if it exists
671
	 *
672
	 * @param string $type Category name of the extension
673
	 * @param array $extension
674
	 *
675
	 * @return string Raw HTML
676
	 */
677
	public function getCreditsForExtension( $type, array $extension ) {
678
		$out = $this->getOutput();
679
680
		// We must obtain the information for all the bits and pieces!
681
		// ... such as extension names and links
682
		if ( isset( $extension['namemsg'] ) ) {
683
			// Localized name of extension
684
			$extensionName = $this->msg( $extension['namemsg'] )->text();
685
		} elseif ( isset( $extension['name'] ) ) {
686
			// Non localized version
687
			$extensionName = $extension['name'];
688
		} else {
689
			$extensionName = $this->msg( 'version-no-ext-name' )->text();
690
		}
691
692
		if ( isset( $extension['url'] ) ) {
693
			$extensionNameLink = Linker::makeExternalLink(
694
				$extension['url'],
695
				$extensionName,
696
				true,
697
				'',
698
				[ 'class' => 'mw-version-ext-name' ]
699
			);
700
		} else {
701
			$extensionNameLink = $extensionName;
702
		}
703
704
		// ... and the version information
705
		// If the extension path is set we will check that directory for GIT
706
		// metadata in an attempt to extract date and vcs commit metadata.
707
		$canonicalVersion = '&ndash;';
708
		$extensionPath = null;
709
		$vcsVersion = null;
710
		$vcsLink = null;
711
		$vcsDate = null;
712
713
		if ( isset( $extension['version'] ) ) {
714
			$canonicalVersion = $out->parseInline( $extension['version'] );
715
		}
716
717
		if ( isset( $extension['path'] ) ) {
718
			global $IP;
719
			$extensionPath = dirname( $extension['path'] );
720
			if ( $this->coreId == '' ) {
721
				wfDebug( 'Looking up core head id' );
722
				$coreHeadSHA1 = self::getGitHeadSha1( $IP );
723
				if ( $coreHeadSHA1 ) {
724
					$this->coreId = $coreHeadSHA1;
725
				}
726
			}
727
			$cache = wfGetCache( CACHE_ANYTHING );
728
			$memcKey = wfMemcKey( 'specialversion-ext-version-text', $extension['path'], $this->coreId );
729
			list( $vcsVersion, $vcsLink, $vcsDate ) = $cache->get( $memcKey );
730
731
			if ( !$vcsVersion ) {
732
				wfDebug( "Getting VCS info for extension {$extension['name']}" );
733
				$gitInfo = new GitInfo( $extensionPath );
734
				$vcsVersion = $gitInfo->getHeadSHA1();
735
				if ( $vcsVersion !== false ) {
736
					$vcsVersion = substr( $vcsVersion, 0, 7 );
737
					$vcsLink = $gitInfo->getHeadViewUrl();
738
					$vcsDate = $gitInfo->getHeadCommitDate();
739
				}
740
				$cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
741
			} else {
742
				wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" );
743
			}
744
		}
745
746
		$versionString = Html::rawElement(
747
			'span',
748
			[ 'class' => 'mw-version-ext-version' ],
749
			$canonicalVersion
750
		);
751
752
		if ( $vcsVersion ) {
753
			if ( $vcsLink ) {
754
				$vcsVerString = Linker::makeExternalLink(
755
					$vcsLink,
756
					$this->msg( 'version-version', $vcsVersion ),
757
					true,
758
					'',
759
					[ 'class' => 'mw-version-ext-vcs-version' ]
760
				);
761
			} else {
762
				$vcsVerString = Html::element( 'span',
763
					[ 'class' => 'mw-version-ext-vcs-version' ],
764
					"({$vcsVersion})"
765
				);
766
			}
767
			$versionString .= " {$vcsVerString}";
768
769
			if ( $vcsDate ) {
770
				$vcsTimeString = Html::element( 'span',
771
					[ 'class' => 'mw-version-ext-vcs-timestamp' ],
772
					$this->getLanguage()->timeanddate( $vcsDate, true )
773
				);
774
				$versionString .= " {$vcsTimeString}";
775
			}
776
			$versionString = Html::rawElement( 'span',
777
				[ 'class' => 'mw-version-ext-meta-version' ],
778
				$versionString
779
			);
780
		}
781
782
		// ... and license information; if a license file exists we
783
		// will link to it
784
		$licenseLink = '';
785
		if ( isset( $extension['name'] ) ) {
786
			$licenseName = null;
787
			if ( isset( $extension['license-name'] ) ) {
788
				$licenseName = $out->parseInline( $extension['license-name'] );
789
			} elseif ( $this->getExtLicenseFileName( $extensionPath ) ) {
790
				$licenseName = $this->msg( 'version-ext-license' )->escaped();
791
			}
792
			if ( $licenseName !== null ) {
793
				$licenseLink = Linker::link(
794
					$this->getPageTitle( 'License/' . $extension['name'] ),
795
					$licenseName,
796
					[
797
						'class' => 'mw-version-ext-license',
798
						'dir' => 'auto',
799
					]
800
				);
801
			}
802
		}
803
804
		// ... and generate the description; which can be a parameterized l10n message
805
		// in the form array( <msgname>, <parameter>, <parameter>... ) or just a straight
806
		// up string
807
		if ( isset( $extension['descriptionmsg'] ) ) {
808
			// Localized description of extension
809
			$descriptionMsg = $extension['descriptionmsg'];
810
811
			if ( is_array( $descriptionMsg ) ) {
812
				$descriptionMsgKey = $descriptionMsg[0]; // Get the message key
813
				array_shift( $descriptionMsg ); // Shift out the message key to get the parameters only
814
				array_map( "htmlspecialchars", $descriptionMsg ); // For sanity
815
				$description = $this->msg( $descriptionMsgKey, $descriptionMsg )->text();
816
			} else {
817
				$description = $this->msg( $descriptionMsg )->text();
818
			}
819
		} elseif ( isset( $extension['description'] ) ) {
820
			// Non localized version
821
			$description = $extension['description'];
822
		} else {
823
			$description = '';
824
		}
825
		$description = $out->parseInline( $description );
826
827
		// ... now get the authors for this extension
828
		$authors = isset( $extension['author'] ) ? $extension['author'] : [];
829
		$authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
830
831
		// Finally! Create the table
832
		$html = Html::openElement( 'tr', [
833
				'class' => 'mw-version-ext',
834
				'id' => Sanitizer::escapeId( 'mw-version-ext-' . $type . '-' . $extension['name'] )
835
			]
836
		);
837
838
		$html .= Html::rawElement( 'td', [], $extensionNameLink );
839
		$html .= Html::rawElement( 'td', [], $versionString );
840
		$html .= Html::rawElement( 'td', [], $licenseLink );
841
		$html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-description' ], $description );
842
		$html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-authors' ], $authors );
843
844
		$html .= Html::closeElement( 'tr' );
845
846
		return $html;
847
	}
848
849
	/**
850
	 * Generate wikitext showing hooks in $wgHooks.
851
	 *
852
	 * @return string Wikitext
853
	 */
854
	private function getWgHooks() {
855
		global $wgSpecialVersionShowHooks, $wgHooks;
856
857
		if ( $wgSpecialVersionShowHooks && count( $wgHooks ) ) {
858
			$myWgHooks = $wgHooks;
859
			ksort( $myWgHooks );
860
861
			$ret = [];
862
			$ret[] = '== {{int:version-hooks}} ==';
863
			$ret[] = Html::openElement( 'table', [ 'class' => 'wikitable', 'id' => 'sv-hooks' ] );
864
			$ret[] = Html::openElement( 'tr' );
865
			$ret[] = Html::element( 'th', [], $this->msg( 'version-hook-name' )->text() );
866
			$ret[] = Html::element( 'th', [], $this->msg( 'version-hook-subscribedby' )->text() );
867
			$ret[] = Html::closeElement( 'tr' );
868
869
			foreach ( $myWgHooks as $hook => $hooks ) {
870
				$ret[] = Html::openElement( 'tr' );
871
				$ret[] = Html::element( 'td', [], $hook );
872
				$ret[] = Html::element( 'td', [], $this->listToText( $hooks ) );
873
				$ret[] = Html::closeElement( 'tr' );
874
			}
875
876
			$ret[] = Html::closeElement( 'table' );
877
878
			return implode( "\n", $ret );
879
		} else {
880
			return '';
881
		}
882
	}
883
884
	private function openExtType( $text = null, $name = null ) {
885
		$out = '';
886
887
		$opt = [ 'colspan' => 5 ];
888
		if ( $this->firstExtOpened ) {
889
			// Insert a spacing line
890
			$out .= Html::rawElement( 'tr', [ 'class' => 'sv-space' ],
891
				Html::element( 'td', $opt )
892
			);
893
		}
894
		$this->firstExtOpened = true;
895
896
		if ( $name ) {
897
			$opt['id'] = "sv-$name";
898
		}
899
900
		if ( $text !== null ) {
901
			$out .= Html::rawElement( 'tr', [],
902
				Html::element( 'th', $opt, $text )
903
			);
904
		}
905
906
		$firstHeadingMsg = ( $name === 'credits-skin' )
907
			? 'version-skin-colheader-name'
908
			: 'version-ext-colheader-name';
909
		$out .= Html::openElement( 'tr' );
910
		$out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
911
			$this->msg( $firstHeadingMsg )->text() );
912
		$out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
913
			$this->msg( 'version-ext-colheader-version' )->text() );
914
		$out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
915
			$this->msg( 'version-ext-colheader-license' )->text() );
916
		$out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
917
			$this->msg( 'version-ext-colheader-description' )->text() );
918
		$out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
919
			$this->msg( 'version-ext-colheader-credits' )->text() );
920
		$out .= Html::closeElement( 'tr' );
921
922
		return $out;
923
	}
924
925
	/**
926
	 * Get information about client's IP address.
927
	 *
928
	 * @return string HTML fragment
929
	 */
930
	private function IPInfo() {
931
		$ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
932
933
		return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
934
	}
935
936
	/**
937
	 * Return a formatted unsorted list of authors
938
	 *
939
	 * 'And Others'
940
	 *   If an item in the $authors array is '...' it is assumed to indicate an
941
	 *   'and others' string which will then be linked to an ((AUTHORS)|(CREDITS))(\.txt)?
942
	 *   file if it exists in $dir.
943
	 *
944
	 *   Similarly an entry ending with ' ...]' is assumed to be a link to an
945
	 *   'and others' page.
946
	 *
947
	 *   If no '...' string variant is found, but an authors file is found an
948
	 *   'and others' will be added to the end of the credits.
949
	 *
950
	 * @param string|array $authors
951
	 * @param string|bool $extName Name of the extension for link creation,
952
	 *   false if no links should be created
953
	 * @param string $extDir Path to the extension root directory
954
	 *
955
	 * @return string HTML fragment
956
	 */
957
	public function listAuthors( $authors, $extName, $extDir ) {
958
		$hasOthers = false;
959
960
		$list = [];
961
		foreach ( (array)$authors as $item ) {
962
			if ( $item == '...' ) {
963
				$hasOthers = true;
964
965
				if ( $extName && $this->getExtAuthorsFileName( $extDir ) ) {
966
					$text = Linker::link(
967
						$this->getPageTitle( "Credits/$extName" ),
968
						$this->msg( 'version-poweredby-others' )->escaped()
969
					);
970
				} else {
971
					$text = $this->msg( 'version-poweredby-others' )->escaped();
972
				}
973
				$list[] = $text;
974
			} elseif ( substr( $item, -5 ) == ' ...]' ) {
975
				$hasOthers = true;
976
				$list[] = $this->getOutput()->parseInline(
977
					substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
978
				);
979
			} else {
980
				$list[] = $this->getOutput()->parseInline( $item );
981
			}
982
		}
983
984
		if ( $extName && !$hasOthers && $this->getExtAuthorsFileName( $extDir ) ) {
985
			$list[] = $text = Linker::link(
0 ignored issues
show
$text is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
986
				$this->getPageTitle( "Credits/$extName" ),
987
				$this->msg( 'version-poweredby-others' )->escaped()
988
			);
989
		}
990
991
		return $this->listToText( $list, false );
992
	}
993
994
	/**
995
	 * Obtains the full path of an extensions authors or credits file if
996
	 * one exists.
997
	 *
998
	 * @param string $extDir Path to the extensions root directory
999
	 *
1000
	 * @since 1.23
1001
	 *
1002
	 * @return bool|string False if no such file exists, otherwise returns
1003
	 * a path to it.
1004
	 */
1005 View Code Duplication
	public static function getExtAuthorsFileName( $extDir ) {
1006
		if ( !$extDir ) {
1007
			return false;
1008
		}
1009
1010
		foreach ( scandir( $extDir ) as $file ) {
1011
			$fullPath = $extDir . DIRECTORY_SEPARATOR . $file;
1012
			if ( preg_match( '/^((AUTHORS)|(CREDITS))(\.txt|\.wiki|\.mediawiki)?$/', $file ) &&
1013
				is_readable( $fullPath ) &&
1014
				is_file( $fullPath )
1015
			) {
1016
				return $fullPath;
1017
			}
1018
		}
1019
1020
		return false;
1021
	}
1022
1023
	/**
1024
	 * Obtains the full path of an extensions copying or license file if
1025
	 * one exists.
1026
	 *
1027
	 * @param string $extDir Path to the extensions root directory
1028
	 *
1029
	 * @since 1.23
1030
	 *
1031
	 * @return bool|string False if no such file exists, otherwise returns
1032
	 * a path to it.
1033
	 */
1034 View Code Duplication
	public static function getExtLicenseFileName( $extDir ) {
1035
		if ( !$extDir ) {
1036
			return false;
1037
		}
1038
1039
		foreach ( scandir( $extDir ) as $file ) {
1040
			$fullPath = $extDir . DIRECTORY_SEPARATOR . $file;
1041
			if ( preg_match( '/^((COPYING)|(LICENSE))(\.txt)?$/', $file ) &&
1042
				is_readable( $fullPath ) &&
1043
				is_file( $fullPath )
1044
			) {
1045
				return $fullPath;
1046
			}
1047
		}
1048
1049
		return false;
1050
	}
1051
1052
	/**
1053
	 * Convert an array of items into a list for display.
1054
	 *
1055
	 * @param array $list List of elements to display
1056
	 * @param bool $sort Whether to sort the items in $list
1057
	 *
1058
	 * @return string
1059
	 */
1060
	public function listToText( $list, $sort = true ) {
1061
		if ( !count( $list ) ) {
1062
			return '';
1063
		}
1064
		if ( $sort ) {
1065
			sort( $list );
1066
		}
1067
1068
		return $this->getLanguage()
1069
			->listToText( array_map( [ __CLASS__, 'arrayToString' ], $list ) );
1070
	}
1071
1072
	/**
1073
	 * Convert an array or object to a string for display.
1074
	 *
1075
	 * @param mixed $list Will convert an array to string if given and return
1076
	 *   the parameter unaltered otherwise
1077
	 *
1078
	 * @return mixed
1079
	 */
1080
	public static function arrayToString( $list ) {
1081
		if ( is_array( $list ) && count( $list ) == 1 ) {
1082
			$list = $list[0];
1083
		}
1084
		if ( $list instanceof Closure ) {
1085
			// Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$"
1086
			return 'Closure';
1087
		} elseif ( is_object( $list ) ) {
1088
			$class = wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1089
1090
			return $class;
1091
		} elseif ( !is_array( $list ) ) {
1092
			return $list;
1093
		} else {
1094
			if ( is_object( $list[0] ) ) {
1095
				$class = get_class( $list[0] );
1096
			} else {
1097
				$class = $list[0];
1098
			}
1099
1100
			return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1101
		}
1102
	}
1103
1104
	/**
1105
	 * @param string $dir Directory of the git checkout
1106
	 * @return bool|string Sha1 of commit HEAD points to
1107
	 */
1108
	public static function getGitHeadSha1( $dir ) {
1109
		$repo = new GitInfo( $dir );
1110
1111
		return $repo->getHeadSHA1();
1112
	}
1113
1114
	/**
1115
	 * @param string $dir Directory of the git checkout
1116
	 * @return bool|string Branch currently checked out
1117
	 */
1118
	public static function getGitCurrentBranch( $dir ) {
1119
		$repo = new GitInfo( $dir );
1120
		return $repo->getCurrentBranch();
1121
	}
1122
1123
	/**
1124
	 * Get the list of entry points and their URLs
1125
	 * @return string Wikitext
1126
	 */
1127
	public function getEntryPointInfo() {
1128
		global $wgArticlePath, $wgScriptPath;
1129
		$scriptPath = $wgScriptPath ? $wgScriptPath : "/";
1130
		$entryPoints = [
1131
			'version-entrypoints-articlepath' => $wgArticlePath,
1132
			'version-entrypoints-scriptpath' => $scriptPath,
1133
			'version-entrypoints-index-php' => wfScript( 'index' ),
1134
			'version-entrypoints-api-php' => wfScript( 'api' ),
1135
			'version-entrypoints-load-php' => wfScript( 'load' ),
1136
		];
1137
1138
		$language = $this->getLanguage();
1139
		$thAttribures = [
1140
			'dir' => $language->getDir(),
1141
			'lang' => $language->getHtmlCode()
1142
		];
1143
		$out = Html::element(
1144
				'h2',
1145
				[ 'id' => 'mw-version-entrypoints' ],
1146
				$this->msg( 'version-entrypoints' )->text()
1147
			) .
1148
			Html::openElement( 'table',
1149
				[
1150
					'class' => 'wikitable plainlinks',
1151
					'id' => 'mw-version-entrypoints-table',
1152
					'dir' => 'ltr',
1153
					'lang' => 'en'
1154
				]
1155
			) .
1156
			Html::openElement( 'tr' ) .
1157
			Html::element(
1158
				'th',
1159
				$thAttribures,
1160
				$this->msg( 'version-entrypoints-header-entrypoint' )->text()
1161
			) .
1162
			Html::element(
1163
				'th',
1164
				$thAttribures,
1165
				$this->msg( 'version-entrypoints-header-url' )->text()
1166
			) .
1167
			Html::closeElement( 'tr' );
1168
1169
		foreach ( $entryPoints as $message => $value ) {
1170
			$url = wfExpandUrl( $value, PROTO_RELATIVE );
1171
			$out .= Html::openElement( 'tr' ) .
1172
				// ->text() looks like it should be ->parse(), but this function
1173
				// returns wikitext, not HTML, boo
1174
				Html::rawElement( 'td', [], $this->msg( $message )->text() ) .
1175
				Html::rawElement( 'td', [], Html::rawElement( 'code', [], "[$url $value]" ) ) .
1176
				Html::closeElement( 'tr' );
1177
		}
1178
1179
		$out .= Html::closeElement( 'table' );
1180
1181
		return $out;
1182
	}
1183
1184
	protected function getGroupName() {
1185
		return 'wiki';
1186
	}
1187
}
1188