Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

maintenance/language/checkLanguage.inc (1 issue)

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
 * Helper class for checkLanguage.php script.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup MaintenanceLanguage
22
 */
23
24
/**
25
 * @ingroup MaintenanceLanguage
26
 */
27
class CheckLanguageCLI {
28
	protected $code = null;
29
	protected $level = 2;
30
	protected $doLinks = false;
31
	protected $linksPrefix = '';
32
	protected $wikiCode = 'en';
33
	protected $checkAll = false;
34
	protected $output = 'plain';
35
	protected $checks = [];
36
	protected $L = null;
37
38
	protected $results = [];
39
40
	private $includeExif = false;
41
42
	/**
43
	 * Constructor.
44
	 * @param array $options Options for script.
45
	 */
46
	public function __construct( array $options ) {
47
		if ( isset( $options['help'] ) ) {
48
			echo $this->help();
49
			exit( 1 );
50
		}
51
52 View Code Duplication
		if ( isset( $options['lang'] ) ) {
53
			$this->code = $options['lang'];
54
		} else {
55
			global $wgLanguageCode;
56
			$this->code = $wgLanguageCode;
57
		}
58
59
		if ( isset( $options['level'] ) ) {
60
			$this->level = $options['level'];
61
		}
62
63
		$this->doLinks = isset( $options['links'] );
64
		$this->includeExif = !isset( $options['noexif'] );
65
		$this->checkAll = isset( $options['all'] );
66
67
		if ( isset( $options['prefix'] ) ) {
68
			$this->linksPrefix = $options['prefix'];
69
		}
70
71
		if ( isset( $options['wikilang'] ) ) {
72
			$this->wikiCode = $options['wikilang'];
73
		}
74
75 View Code Duplication
		if ( isset( $options['whitelist'] ) ) {
76
			$this->checks = explode( ',', $options['whitelist'] );
77
		} elseif ( isset( $options['blacklist'] ) ) {
78
			$this->checks = array_diff(
79
				isset( $options['easy'] ) ? $this->easyChecks() : $this->defaultChecks(),
80
				explode( ',', $options['blacklist'] )
81
			);
82
		} elseif ( isset( $options['easy'] ) ) {
83
			$this->checks = $this->easyChecks();
84
		} else {
85
			$this->checks = $this->defaultChecks();
86
		}
87
88
		if ( isset( $options['output'] ) ) {
89
			$this->output = $options['output'];
90
		}
91
92
		$this->L = new Languages( $this->includeExif );
93
	}
94
95
	/**
96
	 * Get the default checks.
97
	 * @return array A list of the default checks.
98
	 */
99
	protected function defaultChecks() {
100
		return [
101
			'untranslated', 'duplicate', 'obsolete', 'variables', 'empty', 'plural',
102
			'whitespace', 'xhtml', 'chars', 'links', 'unbalanced', 'namespace',
103
			'projecttalk', 'magic', 'magic-old', 'magic-over', 'magic-case',
104
			'special', 'special-old',
105
		];
106
	}
107
108
	/**
109
	 * Get the checks which check other things than messages.
110
	 * @return array A list of the non-message checks.
111
	 */
112
	protected function nonMessageChecks() {
113
		return [
114
			'namespace', 'projecttalk', 'magic', 'magic-old', 'magic-over',
115
			'magic-case', 'special', 'special-old',
116
		];
117
	}
118
119
	/**
120
	 * Get the checks that can easily be treated by non-speakers of the language.
121
	 * @return array A list of the easy checks.
122
	 */
123
	protected function easyChecks() {
124
		return [
125
			'duplicate', 'obsolete', 'empty', 'whitespace', 'xhtml', 'chars', 'magic-old',
126
			'magic-over', 'magic-case', 'special-old',
127
		];
128
	}
129
130
	/**
131
	 * Get all checks.
132
	 * @return array An array of all check names mapped to their function names.
133
	 */
134 View Code Duplication
	protected function getChecks() {
135
		return [
136
			'untranslated' => 'getUntranslatedMessages',
137
			'duplicate' => 'getDuplicateMessages',
138
			'obsolete' => 'getObsoleteMessages',
139
			'variables' => 'getMessagesWithMismatchVariables',
140
			'plural' => 'getMessagesWithoutPlural',
141
			'empty' => 'getEmptyMessages',
142
			'whitespace' => 'getMessagesWithWhitespace',
143
			'xhtml' => 'getNonXHTMLMessages',
144
			'chars' => 'getMessagesWithWrongChars',
145
			'links' => 'getMessagesWithDubiousLinks',
146
			'unbalanced' => 'getMessagesWithUnbalanced',
147
			'namespace' => 'getUntranslatedNamespaces',
148
			'projecttalk' => 'getProblematicProjectTalks',
149
			'magic' => 'getUntranslatedMagicWords',
150
			'magic-old' => 'getObsoleteMagicWords',
151
			'magic-over' => 'getOverridingMagicWords',
152
			'magic-case' => 'getCaseMismatchMagicWords',
153
			'special' => 'getUntraslatedSpecialPages',
154
			'special-old' => 'getObsoleteSpecialPages',
155
		];
156
	}
157
158
	/**
159
	 * Get total count for each check non-messages check.
160
	 * @return array An array of all check names mapped to a two-element array:
161
	 * function name to get the total count and language code or null
162
	 * for checked code.
163
	 */
164
	protected function getTotalCount() {
165
		return [
166
			'namespace' => [ 'getNamespaceNames', 'en' ],
167
			'projecttalk' => null,
168
			'magic' => [ 'getMagicWords', 'en' ],
169
			'magic-old' => [ 'getMagicWords', null ],
170
			'magic-over' => [ 'getMagicWords', null ],
171
			'magic-case' => [ 'getMagicWords', null ],
172
			'special' => [ 'getSpecialPageAliases', 'en' ],
173
			'special-old' => [ 'getSpecialPageAliases', null ],
174
		];
175
	}
176
177
	/**
178
	 * Get all check descriptions.
179
	 * @return array An array of all check names mapped to their descriptions.
180
	 */
181 View Code Duplication
	protected function getDescriptions() {
182
		return [
183
			'untranslated' => '$1 message(s) of $2 are not translated to $3, but exist in en:',
184
			'duplicate' => '$1 message(s) of $2 are translated the same in en and $3:',
185
			'obsolete' =>
186
				'$1 message(s) of $2 do not exist in en or are in the ignore list, but exist in $3:',
187
			'variables' => '$1 message(s) of $2 in $3 don\'t match the variables used in en:',
188
			'plural' => '$1 message(s) of $2 in $3 don\'t use {{plural}} while en uses:',
189
			'empty' => '$1 message(s) of $2 in $3 are empty or -:',
190
			'whitespace' => '$1 message(s) of $2 in $3 have trailing whitespace:',
191
			'xhtml' => '$1 message(s) of $2 in $3 contain illegal XHTML:',
192
			'chars' =>
193
				'$1 message(s) of $2 in $3 include hidden chars which should not be used in the messages:',
194
			'links' => '$1 message(s) of $2 in $3 have problematic link(s):',
195
			'unbalanced' => '$1 message(s) of $2 in $3 have unbalanced {[]}:',
196
			'namespace' => '$1 namespace name(s) of $2 are not translated to $3, but exist in en:',
197
			'projecttalk' =>
198
				'$1 namespace name(s) and alias(es) in $3 are project talk namespaces without the parameter:',
199
			'magic' => '$1 magic word(s) of $2 are not translated to $3, but exist in en:',
200
			'magic-old' => '$1 magic word(s) of $2 do not exist in en, but exist in $3:',
201
			'magic-over' => '$1 magic word(s) of $2 in $3 do not contain the original en word(s):',
202
			'magic-case' =>
203
				'$1 magic word(s) of $2 in $3 change the case-sensitivity of the original en word:',
204
			'special' => '$1 special page alias(es) of $2 are not translated to $3, but exist in en:',
205
			'special-old' => '$1 special page alias(es) of $2 do not exist in en, but exist in $3:',
206
		];
207
	}
208
209
	/**
210
	 * Get help.
211
	 * @return string The help string.
212
	 */
213
	protected function help() {
214
		return <<<ENDS
215
Run this script to check a specific language file, or all of them.
216
Command line settings are in form --parameter[=value].
217
Parameters:
218
	--help: Show this help.
219
	--lang: Language code (default: the installation default language).
220
	--all: Check all customized languages.
221
	--level: Show the following display level (default: 2):
222
		* 0: Skip the checks (useful for checking syntax).
223
		* 1: Show only the stub headers and number of wrong messages, without
224
		     list of messages.
225
		* 2: Show only the headers and the message keys, without the message
226
		     values.
227
		* 3: Show both the headers and the complete messages, with both keys and
228
		     values.
229
	--links: Link the message values (default off).
230
	--prefix: prefix to add to links.
231
	--wikilang: For the links, what is the content language of the wiki to
232
	    display the output in (default en).
233
	--noexif: Do not check for Exif messages (a bit hard and boring to
234
	    translate), if you know what they are currently not translated and want
235
	    to focus on other problems (default off).
236
	--whitelist: Do only the following checks (form: code,code).
237
	--blacklist: Do not do the following checks (form: code,code).
238
	--easy: Do only the easy checks, which can be treated by non-speakers of
239
	    the language.
240
241
Check codes (ideally, all of them should result 0; all the checks are executed
242
by default (except language-specific check blacklists in checkLanguage.inc):
243
	* untranslated: Messages which are required to translate, but are not
244
	  translated.
245
	* duplicate: Messages which translation equal to fallback.
246
	* obsolete: Messages which are untranslatable or do not exist, but are
247
	  translated.
248
	* variables: Messages without variables which should be used, or with
249
	  variables which should not be used.
250
	* empty: Empty messages and messages that contain only -.
251
	* whitespace: Messages which have trailing whitespace.
252
	* xhtml: Messages which are not well-formed XHTML (checks only few common
253
	  errors).
254
	* chars: Messages with hidden characters.
255
	* links: Messages which contains broken links to pages (does not find all).
256
	* unbalanced: Messages which contains unequal numbers of opening {[ and
257
	  closing ]}.
258
	* namespace: Namespace names that were not translated.
259
	* projecttalk: Namespace names and aliases where the project talk does not
260
	  contain $1.
261
	* magic: Magic words that were not translated.
262
	* magic-old: Magic words which do not exist.
263
	* magic-over: Magic words that override the original English word.
264
	* magic-case: Magic words whose translation changes the case-sensitivity of
265
	  the original English word.
266
	* special: Special page names that were not translated.
267
	* special-old: Special page names which do not exist.
268
269
ENDS;
270
	}
271
272
	/**
273
	 * Execute the script.
274
	 */
275
	public function execute() {
276
		$this->doChecks();
277 View Code Duplication
		if ( $this->level > 0 ) {
278
			switch ( $this->output ) {
279
				case 'plain':
280
					$this->outputText();
281
					break;
282
				case 'wiki':
283
					$this->outputWiki();
284
					break;
285
				default:
286
					throw new MWException( "Invalid output type $this->output" );
287
			}
288
		}
289
	}
290
291
	/**
292
	 * Execute the checks.
293
	 */
294
	protected function doChecks() {
295
		$ignoredCodes = [ 'en', 'enRTL' ];
296
297
		$this->results = [];
298
		# Check the language
299
		if ( $this->checkAll ) {
300
			foreach ( $this->L->getLanguages() as $language ) {
301
				if ( !in_array( $language, $ignoredCodes ) ) {
302
					$this->results[$language] = $this->checkLanguage( $language );
303
				}
304
			}
305
		} else {
306
			if ( in_array( $this->code, $ignoredCodes ) ) {
307
				throw new MWException( "Cannot check code $this->code." );
308
			} else {
309
				$this->results[$this->code] = $this->checkLanguage( $this->code );
310
			}
311
		}
312
313
		$results = $this->results;
314
		foreach ( $results as $code => $checks ) {
315
			foreach ( $checks as $check => $messages ) {
316
				foreach ( $messages as $key => $details ) {
317
					if ( $this->isCheckBlacklisted( $check, $code, $key ) ) {
318
						unset( $this->results[$code][$check][$key] );
319
					}
320
				}
321
			}
322
		}
323
	}
324
325
	/**
326
	 * Get the check blacklist.
327
	 * @return array The list of checks which should not be executed.
328
	 */
329
	protected function getCheckBlacklist() {
330
		static $blacklist = null;
331
332
		if ( $blacklist !== null ) {
333
			return $blacklist;
334
		}
335
336
		// @codingStandardsIgnoreStart Ignore that globals should have a "wg" prefix.
337
		global $checkBlacklist;
338
		// @codingStandardsIgnoreEnd
339
340
		$blacklist = $checkBlacklist;
341
342
		Hooks::run( 'LocalisationChecksBlacklist', [ &$blacklist ] );
343
344
		return $blacklist;
345
	}
346
347
	/**
348
	 * Verify whether a check is blacklisted.
349
	 *
350
	 * @param string $check Check name
351
	 * @param string $code Language code
352
	 * @param string|bool $message Message name, or False for a whole language
353
	 * @return bool Whether the check is blacklisted
354
	 */
355
	protected function isCheckBlacklisted( $check, $code, $message ) {
356
		$blacklist = $this->getCheckBlacklist();
357
358
		foreach ( $blacklist as $item ) {
359
			if ( isset( $item['check'] ) && $check !== $item['check'] ) {
360
				continue;
361
			}
362
363
			if ( isset( $item['code'] ) && !in_array( $code, $item['code'] ) ) {
364
				continue;
365
			}
366
367
			if ( isset( $item['message'] ) &&
368
				( $message === false || !in_array( $message, $item['message'] ) )
369
			) {
370
				continue;
371
			}
372
373
			return true;
374
		}
375
376
		return false;
377
	}
378
379
	/**
380
	 * Check a language.
381
	 * @param string $code The language code.
382
	 * @throws MWException
383
	 * @return array The results.
384
	 */
385
	protected function checkLanguage( $code ) {
386
		# Syntax check only
387
		$results = [];
388
		if ( $this->level === 0 ) {
389
			$this->L->getMessages( $code );
390
391
			return $results;
392
		}
393
394
		$checkFunctions = $this->getChecks();
395
		foreach ( $this->checks as $check ) {
396
			if ( $this->isCheckBlacklisted( $check, $code, false ) ) {
397
				$results[$check] = [];
398
				continue;
399
			}
400
401
			$callback = [ $this->L, $checkFunctions[$check] ];
402
			if ( !is_callable( $callback ) ) {
403
				throw new MWException( "Unkown check $check." );
404
			}
405
			$results[$check] = call_user_func( $callback, $code );
406
		}
407
408
		return $results;
409
	}
410
411
	/**
412
	 * Format a message key.
413
	 * @param string $key The message key.
414
	 * @param string $code The language code.
415
	 * @return string The formatted message key.
416
	 */
417
	protected function formatKey( $key, $code ) {
418
		if ( $this->doLinks ) {
419
			$displayKey = ucfirst( $key );
420
			if ( $code == $this->wikiCode ) {
421
				return "[[{$this->linksPrefix}MediaWiki:$displayKey|$key]]";
422
			} else {
423
				return "[[{$this->linksPrefix}MediaWiki:$displayKey/$code|$key]]";
424
			}
425
		} else {
426
			return $key;
427
		}
428
	}
429
430
	/**
431
	 * Output the checks results as plain text.
432
	 */
433
	protected function outputText() {
434
		foreach ( $this->results as $code => $results ) {
435
			$translated = $this->L->getMessages( $code );
436
			$translated = count( $translated['translated'] );
437
			foreach ( $results as $check => $messages ) {
438
				$count = count( $messages );
439
				if ( $count ) {
440
					if ( $check == 'untranslated' ) {
441
						$translatable = $this->L->getGeneralMessages();
442
						$total = count( $translatable['translatable'] );
443
					} elseif ( in_array( $check, $this->nonMessageChecks() ) ) {
444
						$totalCount = $this->getTotalCount();
445
						$totalCount = $totalCount[$check];
446
						$callback = [ $this->L, $totalCount[0] ];
447
						$callCode = $totalCount[1] ? $totalCount[1] : $code;
448
						$total = count( call_user_func( $callback, $callCode ) );
449
					} else {
450
						$total = $translated;
451
					}
452
					$search = [ '$1', '$2', '$3' ];
453
					$replace = [ $count, $total, $code ];
454
					$descriptions = $this->getDescriptions();
455
					echo "\n" . str_replace( $search, $replace, $descriptions[$check] ) . "\n";
456
					if ( $this->level == 1 ) {
457
						echo "[messages are hidden]\n";
458
					} else {
459
						foreach ( $messages as $key => $value ) {
460
							if ( !in_array( $check, $this->nonMessageChecks() ) ) {
461
								$key = $this->formatKey( $key, $code );
462
							}
463
							if ( $this->level == 2 || empty( $value ) ) {
464
								echo "* $key\n";
465
							} else {
466
								echo "* $key:		'$value'\n";
467
							}
468
						}
469
					}
470
				}
471
			}
472
		}
473
	}
474
475
	/**
476
	 * Output the checks results as wiki text.
477
	 */
478
	function outputWiki() {
479
		$detailText = '';
480
		$rows[] = '! Language !! Code !! Total !! ' .
0 ignored issues
show
Coding Style Comprehensibility introduced by
$rows was never initialized. Although not strictly required by PHP, it is generally a good practice to add $rows = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
481
			implode( ' !! ', array_diff( $this->checks, $this->nonMessageChecks() ) );
482
		foreach ( $this->results as $code => $results ) {
483
			$detailTextForLang = "==$code==\n";
484
			$numbers = [];
485
			$problems = 0;
486
			$detailTextForLangChecks = [];
487
			foreach ( $results as $check => $messages ) {
488
				if ( in_array( $check, $this->nonMessageChecks() ) ) {
489
					continue;
490
				}
491
				$count = count( $messages );
492
				if ( $count ) {
493
					$problems += $count;
494
					$messageDetails = [];
495
					foreach ( $messages as $key => $details ) {
496
						$displayKey = $this->formatKey( $key, $code );
497
						$messageDetails[] = $displayKey;
498
					}
499
					$detailTextForLangChecks[] = "=== $code-$check ===\n* " . implode( ', ', $messageDetails );
500
					$numbers[] = "'''[[#$code-$check|$count]]'''";
501
				} else {
502
					$numbers[] = $count;
503
				}
504
			}
505
506
			if ( count( $detailTextForLangChecks ) ) {
507
				$detailText .= $detailTextForLang . implode( "\n", $detailTextForLangChecks ) . "\n";
508
			}
509
510
			if ( !$problems ) {
511
				# Don't list languages without problems
512
				continue;
513
			}
514
			$language = Language::fetchLanguageName( $code );
515
			$rows[] = "| $language || $code || $problems || " . implode( ' || ', $numbers );
516
		}
517
518
		$tableRows = implode( "\n|-\n", $rows );
519
520
		$version = SpecialVersion::getVersion( 'nodb' );
521
		// @codingStandardsIgnoreStart Long line.
522
		echo <<<EOL
523
'''Check results are for:''' <code>$version</code>
524
525
526
{| class="sortable wikitable" border="2" cellpadding="4" cellspacing="0" style="background-color: #F9F9F9; border: 1px #AAAAAA solid; border-collapse: collapse; clear: both;"
527
$tableRows
528
|}
529
530
$detailText
531
532
EOL;
533
		// @codingStandardsIgnoreEnd
534
	}
535
536
	/**
537
	 * Check if there are any results for the checks, in any language.
538
	 * @return bool True if there are any results, false if not.
539
	 */
540
	protected function isEmpty() {
541
		foreach ( $this->results as $results ) {
542
			foreach ( $results as $messages ) {
543
				if ( !empty( $messages ) ) {
544
					return false;
545
				}
546
			}
547
		}
548
549
		return true;
550
	}
551
}
552
553
/**
554
 * @ingroup MaintenanceLanguage
555
 */
556
class CheckExtensionsCLI extends CheckLanguageCLI {
557
	private $extensions;
558
559
	/**
560
	 * Constructor.
561
	 * @param array $options Options for script.
562
	 * @param string $extension The extension name (or names).
563
	 */
564
	public function __construct( array $options, $extension ) {
565
		if ( isset( $options['help'] ) ) {
566
			echo $this->help();
567
			exit( 1 );
568
		}
569
570 View Code Duplication
		if ( isset( $options['lang'] ) ) {
571
			$this->code = $options['lang'];
572
		} else {
573
			global $wgLanguageCode;
574
			$this->code = $wgLanguageCode;
575
		}
576
577
		if ( isset( $options['level'] ) ) {
578
			$this->level = $options['level'];
579
		}
580
581
		$this->doLinks = isset( $options['links'] );
582
583
		if ( isset( $options['wikilang'] ) ) {
584
			$this->wikiCode = $options['wikilang'];
585
		}
586
587 View Code Duplication
		if ( isset( $options['whitelist'] ) ) {
588
			$this->checks = explode( ',', $options['whitelist'] );
589
		} elseif ( isset( $options['blacklist'] ) ) {
590
			$this->checks = array_diff(
591
				isset( $options['easy'] ) ? $this->easyChecks() : $this->defaultChecks(),
592
				explode( ',', $options['blacklist'] )
593
			);
594
		} elseif ( isset( $options['easy'] ) ) {
595
			$this->checks = $this->easyChecks();
596
		} else {
597
			$this->checks = $this->defaultChecks();
598
		}
599
600
		if ( isset( $options['output'] ) ) {
601
			$this->output = $options['output'];
602
		}
603
604
		# Some additional checks not enabled by default
605
		if ( isset( $options['duplicate'] ) ) {
606
			$this->checks[] = 'duplicate';
607
		}
608
609
		$this->extensions = [];
610
		$extensions = new PremadeMediawikiExtensionGroups();
611
		$extensions->addAll();
612
		if ( $extension == 'all' ) {
613 View Code Duplication
			foreach ( MessageGroups::singleton()->getGroups() as $group ) {
614
				if ( strpos( $group->getId(), 'ext-' ) === 0 && !$group->isMeta() ) {
615
					$this->extensions[] = new ExtensionLanguages( $group );
616
				}
617
			}
618
		} elseif ( $extension == 'wikimedia' ) {
619
			$wikimedia = MessageGroups::getGroup( 'ext-0-wikimedia' );
620
			foreach ( $wikimedia->wmfextensions() as $extension ) {
621
				$group = MessageGroups::getGroup( $extension );
622
				$this->extensions[] = new ExtensionLanguages( $group );
623
			}
624
		} elseif ( $extension == 'flaggedrevs' ) {
625 View Code Duplication
			foreach ( MessageGroups::singleton()->getGroups() as $group ) {
626
				if ( strpos( $group->getId(), 'ext-flaggedrevs-' ) === 0 && !$group->isMeta() ) {
627
					$this->extensions[] = new ExtensionLanguages( $group );
628
				}
629
			}
630
		} else {
631
			$extensions = explode( ',', $extension );
632
			foreach ( $extensions as $extension ) {
633
				$group = MessageGroups::getGroup( 'ext-' . $extension );
634
				if ( $group ) {
635
					$extension = new ExtensionLanguages( $group );
636
					$this->extensions[] = $extension;
637
				} else {
638
					print "No such extension $extension.\n";
639
				}
640
			}
641
		}
642
	}
643
644
	/**
645
	 * Get the default checks.
646
	 * @return array A list of the default checks.
647
	 */
648
	protected function defaultChecks() {
649
		return [
650
			'untranslated', 'duplicate', 'obsolete', 'variables', 'empty', 'plural',
651
			'whitespace', 'xhtml', 'chars', 'links', 'unbalanced',
652
		];
653
	}
654
655
	/**
656
	 * Get the checks which check other things than messages.
657
	 * @return array A list of the non-message checks.
658
	 */
659
	protected function nonMessageChecks() {
660
		return [];
661
	}
662
663
	/**
664
	 * Get the checks that can easily be treated by non-speakers of the language.
665
	 * @return array A list of the easy checks.
666
	 */
667
	protected function easyChecks() {
668
		return [
669
			'duplicate', 'obsolete', 'empty', 'whitespace', 'xhtml', 'chars',
670
		];
671
	}
672
673
	/**
674
	 * Get help.
675
	 * @return string The help string.
676
	 */
677
	protected function help() {
678
		return <<<ENDS
679
Run this script to check the status of a specific language in extensions, or
680
all of them. Command line settings are in form --parameter[=value], except for
681
the first one.
682
Parameters:
683
	* First parameter (mandatory): Extension name, multiple extension names
684
	  (separated by commas), "all" for all the extensions, "wikimedia" for
685
	  extensions used by Wikimedia or "flaggedrevs" for all FLaggedRevs
686
	  extension messages.
687
	* lang: Language code (default: the installation default language).
688
	* help: Show this help.
689
	* level: Show the following display level (default: 2).
690
	* links: Link the message values (default off).
691
	* wikilang: For the links, what is the content language of the wiki to
692
	  display the output in (default en).
693
	* whitelist: Do only the following checks (form: code,code).
694
	* blacklist: Do not perform the following checks (form: code,code).
695
	* easy: Do only the easy checks, which can be treated by non-speakers of
696
	  the language.
697
698
Check codes (ideally, all of them should result 0; all the checks are executed
699
by default (except language-specific check blacklists in checkLanguage.inc):
700
	* untranslated: Messages which are required to translate, but are not
701
	  translated.
702
	* duplicate: Messages which translation equal to fallback.
703
	* obsolete: Messages which are untranslatable, but translated.
704
	* variables: Messages without variables which should be used, or with
705
	  variables which should not be used.
706
	* empty: Empty messages.
707
	* whitespace: Messages which have trailing whitespace.
708
	* xhtml: Messages which are not well-formed XHTML (checks only few common
709
	  errors).
710
	* chars: Messages with hidden characters.
711
	* links: Messages which contains broken links to pages (does not find all).
712
	* unbalanced: Messages which contains unequal numbers of opening {[ and
713
	  closing ]}.
714
715
Display levels (default: 2):
716
	* 0: Skip the checks (useful for checking syntax).
717
	* 1: Show only the stub headers and number of wrong messages, without list
718
	  of messages.
719
	* 2: Show only the headers and the message keys, without the message
720
	  values.
721
	* 3: Show both the headers and the complete messages, with both keys and
722
	  values.
723
724
ENDS;
725
	}
726
727
	/**
728
	 * Execute the script.
729
	 */
730
	public function execute() {
731
		$this->doChecks();
732
	}
733
734
	/**
735
	 * Check a language and show the results.
736
	 * @param string $code The language code.
737
	 * @throws MWException
738
	 */
739
	protected function checkLanguage( $code ) {
740
		foreach ( $this->extensions as $extension ) {
741
			$this->L = $extension;
742
			$this->results = [];
743
			$this->results[$code] = parent::checkLanguage( $code );
744
745
			if ( !$this->isEmpty() ) {
746
				echo $extension->name() . ":\n";
747
748 View Code Duplication
				if ( $this->level > 0 ) {
749
					switch ( $this->output ) {
750
						case 'plain':
751
							$this->outputText();
752
							break;
753
						case 'wiki':
754
							$this->outputWiki();
755
							break;
756
						default:
757
							throw new MWException( "Invalid output type $this->output" );
758
					}
759
				}
760
761
				echo "\n";
762
			}
763
		}
764
	}
765
}
766
767
// Blacklist some checks for some languages or some messages
768
// Possible keys of the sub arrays are: 'check', 'code' and 'message'.
769
$checkBlacklist = [
770
	[
771
		'check' => 'plural',
772
		'code' => [ 'az', 'bo', 'cdo', 'dz', 'id', 'fa', 'gan', 'gan-hans',
773
			'gan-hant', 'gn', 'hak', 'hu', 'ja', 'jv', 'ka', 'kk-arab',
774
			'kk-cyrl', 'kk-latn', 'km', 'kn', 'ko', 'lzh', 'mn', 'ms',
775
			'my', 'sah', 'sq', 'tet', 'th', 'to', 'tr', 'vi', 'wuu', 'xmf',
776
			'yo', 'yue', 'zh', 'zh-classical', 'zh-cn', 'zh-hans',
777
			'zh-hant', 'zh-hk', 'zh-sg', 'zh-tw', 'zh-yue'
778
		],
779
	],
780
	[
781
		'check' => 'chars',
782
		'code' => [ 'my' ],
783
	],
784
];
785