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.

includes/Preferences.php (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
 * Form to edit user preferences.
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
 */
22
use MediaWiki\Auth\AuthManager;
23
use MediaWiki\Auth\PasswordAuthenticationRequest;
24
25
/**
26
 * We're now using the HTMLForm object with some customisation to generate the
27
 * Preferences form. This object handles generic submission, CSRF protection,
28
 * layout and other logic in a reusable manner. We subclass it as a PreferencesForm
29
 * to make some minor customisations.
30
 *
31
 * In order to generate the form, the HTMLForm object needs an array structure
32
 * detailing the form fields available, and that's what this class is for. Each
33
 * element of the array is a basic property-list, including the type of field,
34
 * the label it is to be given in the form, callbacks for validation and
35
 * 'filtering', and other pertinent information. Note that the 'default' field
36
 * is named for generic forms, and does not represent the preference's default
37
 * (which is stored in $wgDefaultUserOptions), but the default for the form
38
 * field, which should be whatever the user has set for that preference. There
39
 * is no need to override it unless you have some special storage logic (for
40
 * instance, those not presently stored as options, but which are best set from
41
 * the user preferences view).
42
 *
43
 * Field types are implemented as subclasses of the generic HTMLFormField
44
 * object, and typically implement at least getInputHTML, which generates the
45
 * HTML for the input field to be placed in the table.
46
 *
47
 * Once fields have been retrieved and validated, submission logic is handed
48
 * over to the tryUISubmit static method of this class.
49
 */
50
class Preferences {
51
	/** @var array */
52
	protected static $defaultPreferences = null;
53
54
	/** @var array */
55
	protected static $saveFilters = [
56
		'timecorrection' => [ 'Preferences', 'filterTimezoneInput' ],
57
		'cols' => [ 'Preferences', 'filterIntval' ],
58
		'rows' => [ 'Preferences', 'filterIntval' ],
59
		'rclimit' => [ 'Preferences', 'filterIntval' ],
60
		'wllimit' => [ 'Preferences', 'filterIntval' ],
61
		'searchlimit' => [ 'Preferences', 'filterIntval' ],
62
	];
63
64
	// Stuff that shouldn't be saved as a preference.
65
	private static $saveBlacklist = [
66
		'realname',
67
		'emailaddress',
68
	];
69
70
	/**
71
	 * @return array
72
	 */
73
	static function getSaveBlacklist() {
74
		return self::$saveBlacklist;
75
	}
76
77
	/**
78
	 * @throws MWException
79
	 * @param User $user
80
	 * @param IContextSource $context
81
	 * @return array|null
82
	 */
83
	static function getPreferences( $user, IContextSource $context ) {
84
		if ( self::$defaultPreferences ) {
85
			return self::$defaultPreferences;
86
		}
87
88
		$defaultPreferences = [];
89
90
		self::profilePreferences( $user, $context, $defaultPreferences );
91
		self::skinPreferences( $user, $context, $defaultPreferences );
92
		self::datetimePreferences( $user, $context, $defaultPreferences );
93
		self::filesPreferences( $user, $context, $defaultPreferences );
94
		self::renderingPreferences( $user, $context, $defaultPreferences );
95
		self::editingPreferences( $user, $context, $defaultPreferences );
96
		self::rcPreferences( $user, $context, $defaultPreferences );
97
		self::watchlistPreferences( $user, $context, $defaultPreferences );
98
		self::searchPreferences( $user, $context, $defaultPreferences );
99
		self::miscPreferences( $user, $context, $defaultPreferences );
100
101
		Hooks::run( 'GetPreferences', [ $user, &$defaultPreferences ] );
102
103
		self::loadPreferenceValues( $user, $context, $defaultPreferences );
104
		self::$defaultPreferences = $defaultPreferences;
105
		return $defaultPreferences;
106
	}
107
108
	/**
109
	 * Loads existing values for a given array of preferences
110
	 * @throws MWException
111
	 * @param User $user
112
	 * @param IContextSource $context
113
	 * @param array $defaultPreferences Array to load values for
114
	 * @return array|null
115
	 */
116
	static function loadPreferenceValues( $user, $context, &$defaultPreferences ) {
117
		# # Remove preferences that wikis don't want to use
118
		foreach ( $context->getConfig()->get( 'HiddenPrefs' ) as $pref ) {
119
			if ( isset( $defaultPreferences[$pref] ) ) {
120
				unset( $defaultPreferences[$pref] );
121
			}
122
		}
123
124
		# # Make sure that form fields have their parent set. See bug 41337.
125
		$dummyForm = new HTMLForm( [], $context );
126
127
		$disable = !$user->isAllowed( 'editmyoptions' );
128
129
		$defaultOptions = User::getDefaultOptions();
130
		# # Prod in defaults from the user
131
		foreach ( $defaultPreferences as $name => &$info ) {
132
			$prefFromUser = self::getOptionFromUser( $name, $info, $user );
133
			if ( $disable && !in_array( $name, self::$saveBlacklist ) ) {
134
				$info['disabled'] = 'disabled';
135
			}
136
			$field = HTMLForm::loadInputFromParameters( $name, $info, $dummyForm ); // For validation
137
			$globalDefault = isset( $defaultOptions[$name] )
138
				? $defaultOptions[$name]
139
				: null;
140
141
			// If it validates, set it as the default
142
			if ( isset( $info['default'] ) ) {
143
				// Already set, no problem
144
				continue;
145
			} elseif ( !is_null( $prefFromUser ) && // Make sure we're not just pulling nothing
146
					$field->validate( $prefFromUser, $user->getOptions() ) === true ) {
147
				$info['default'] = $prefFromUser;
148
			} elseif ( $field->validate( $globalDefault, $user->getOptions() ) === true ) {
149
				$info['default'] = $globalDefault;
150
			} else {
151
				throw new MWException( "Global default '$globalDefault' is invalid for field $name" );
152
			}
153
		}
154
155
		return $defaultPreferences;
156
	}
157
158
	/**
159
	 * Pull option from a user account. Handles stuff like array-type preferences.
160
	 *
161
	 * @param string $name
162
	 * @param array $info
163
	 * @param User $user
164
	 * @return array|string
165
	 */
166
	static function getOptionFromUser( $name, $info, $user ) {
167
		$val = $user->getOption( $name );
168
169
		// Handling for multiselect preferences
170 View Code Duplication
		if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
171
				( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) {
172
			$options = HTMLFormField::flattenOptions( $info['options'] );
173
			$prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
174
			$val = [];
175
176
			foreach ( $options as $value ) {
177
				if ( $user->getOption( "$prefix$value" ) ) {
178
					$val[] = $value;
179
				}
180
			}
181
		}
182
183
		// Handling for checkmatrix preferences
184
		if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
185
				( isset( $info['class'] ) && $info['class'] == 'HTMLCheckMatrix' ) ) {
186
			$columns = HTMLFormField::flattenOptions( $info['columns'] );
187
			$rows = HTMLFormField::flattenOptions( $info['rows'] );
188
			$prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
189
			$val = [];
190
191
			foreach ( $columns as $column ) {
192
				foreach ( $rows as $row ) {
193
					if ( $user->getOption( "$prefix$column-$row" ) ) {
194
						$val[] = "$column-$row";
195
					}
196
				}
197
			}
198
		}
199
200
		return $val;
201
	}
202
203
	/**
204
	 * @param User $user
205
	 * @param IContextSource $context
206
	 * @param array $defaultPreferences
207
	 * @return void
208
	 */
209
	static function profilePreferences( $user, IContextSource $context, &$defaultPreferences ) {
210
		global $wgContLang, $wgParser;
211
212
		$authManager = AuthManager::singleton();
213
		$config = $context->getConfig();
214
		// retrieving user name for GENDER and misc.
215
		$userName = $user->getName();
216
217
		# # User info #####################################
218
		// Information panel
219
		$defaultPreferences['username'] = [
220
			'type' => 'info',
221
			'label-message' => [ 'username', $userName ],
222
			'default' => $userName,
223
			'section' => 'personal/info',
224
		];
225
226
		# Get groups to which the user belongs
227
		$userEffectiveGroups = $user->getEffectiveGroups();
228
		$userGroups = $userMembers = [];
229
		foreach ( $userEffectiveGroups as $ueg ) {
230
			if ( $ueg == '*' ) {
231
				// Skip the default * group, seems useless here
232
				continue;
233
			}
234
			$groupName = User::getGroupName( $ueg );
235
			$userGroups[] = User::makeGroupLinkHTML( $ueg, $groupName );
236
237
			$memberName = User::getGroupMember( $ueg, $userName );
238
			$userMembers[] = User::makeGroupLinkHTML( $ueg, $memberName );
239
		}
240
		asort( $userGroups );
241
		asort( $userMembers );
242
243
		$lang = $context->getLanguage();
244
245
		$defaultPreferences['usergroups'] = [
246
			'type' => 'info',
247
			'label' => $context->msg( 'prefs-memberingroups' )->numParams(
248
				count( $userGroups ) )->params( $userName )->parse(),
249
			'default' => $context->msg( 'prefs-memberingroups-type' )
250
				->rawParams( $lang->commaList( $userGroups ), $lang->commaList( $userMembers ) )
251
				->escaped(),
252
			'raw' => true,
253
			'section' => 'personal/info',
254
		];
255
256
		$editCount = Linker::link( SpecialPage::getTitleFor( "Contributions", $userName ),
257
			$lang->formatNum( $user->getEditCount() ) );
258
259
		$defaultPreferences['editcount'] = [
260
			'type' => 'info',
261
			'raw' => true,
262
			'label-message' => 'prefs-edits',
263
			'default' => $editCount,
264
			'section' => 'personal/info',
265
		];
266
267
		if ( $user->getRegistration() ) {
268
			$displayUser = $context->getUser();
269
			$userRegistration = $user->getRegistration();
270
			$defaultPreferences['registrationdate'] = [
271
				'type' => 'info',
272
				'label-message' => 'prefs-registration',
273
				'default' => $context->msg(
274
					'prefs-registration-date-time',
275
					$lang->userTimeAndDate( $userRegistration, $displayUser ),
276
					$lang->userDate( $userRegistration, $displayUser ),
277
					$lang->userTime( $userRegistration, $displayUser )
278
				)->parse(),
279
				'section' => 'personal/info',
280
			];
281
		}
282
283
		$canViewPrivateInfo = $user->isAllowed( 'viewmyprivateinfo' );
284
		$canEditPrivateInfo = $user->isAllowed( 'editmyprivateinfo' );
285
286
		// Actually changeable stuff
287
		$defaultPreferences['realname'] = [
288
			// (not really "private", but still shouldn't be edited without permission)
289
			'type' => $canEditPrivateInfo && $authManager->allowsPropertyChange( 'realname' )
290
				? 'text' : 'info',
291
			'default' => $user->getRealName(),
292
			'section' => 'personal/info',
293
			'label-message' => 'yourrealname',
294
			'help-message' => 'prefs-help-realname',
295
		];
296
297
		if ( $canEditPrivateInfo && $authManager->allowsAuthenticationDataChange(
298
			new PasswordAuthenticationRequest(), false )->isGood()
299
		) {
300
			$link = Linker::link( SpecialPage::getTitleFor( 'ChangePassword' ),
301
				$context->msg( 'prefs-resetpass' )->escaped(), [],
302
				[ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
303
304
			$defaultPreferences['password'] = [
305
				'type' => 'info',
306
				'raw' => true,
307
				'default' => $link,
308
				'label-message' => 'yourpassword',
309
				'section' => 'personal/info',
310
			];
311
		}
312
		// Only show prefershttps if secure login is turned on
313
		if ( $config->get( 'SecureLogin' ) && wfCanIPUseHTTPS( $context->getRequest()->getIP() ) ) {
314
			$defaultPreferences['prefershttps'] = [
315
				'type' => 'toggle',
316
				'label-message' => 'tog-prefershttps',
317
				'help-message' => 'prefs-help-prefershttps',
318
				'section' => 'personal/info'
319
			];
320
		}
321
322
		// Language
323
		$languages = Language::fetchLanguageNames( null, 'mw' );
324
		$languageCode = $config->get( 'LanguageCode' );
325
		if ( !array_key_exists( $languageCode, $languages ) ) {
326
			$languages[$languageCode] = $languageCode;
327
		}
328
		ksort( $languages );
329
330
		$options = [];
331
		foreach ( $languages as $code => $name ) {
332
			$display = wfBCP47( $code ) . ' - ' . $name;
333
			$options[$display] = $code;
334
		}
335
		$defaultPreferences['language'] = [
336
			'type' => 'select',
337
			'section' => 'personal/i18n',
338
			'options' => $options,
339
			'label-message' => 'yourlanguage',
340
		];
341
342
		$defaultPreferences['gender'] = [
343
			'type' => 'radio',
344
			'section' => 'personal/i18n',
345
			'options' => [
346
				$context->msg( 'parentheses' )
347
					->params( $context->msg( 'gender-unknown' )->plain() )
348
					->escaped() => 'unknown',
349
				$context->msg( 'gender-female' )->escaped() => 'female',
350
				$context->msg( 'gender-male' )->escaped() => 'male',
351
			],
352
			'label-message' => 'yourgender',
353
			'help-message' => 'prefs-help-gender',
354
		];
355
356
		// see if there are multiple language variants to choose from
357
		if ( !$config->get( 'DisableLangConversion' ) ) {
358
			foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
359
				if ( $langCode == $wgContLang->getCode() ) {
360
					$variants = $wgContLang->getVariants();
361
362
					if ( count( $variants ) <= 1 ) {
363
						continue;
364
					}
365
366
					$variantArray = [];
367
					foreach ( $variants as $v ) {
368
						$v = str_replace( '_', '-', strtolower( $v ) );
369
						$variantArray[$v] = $lang->getVariantname( $v, false );
370
					}
371
372
					$options = [];
373
					foreach ( $variantArray as $code => $name ) {
374
						$display = wfBCP47( $code ) . ' - ' . $name;
375
						$options[$display] = $code;
376
					}
377
378
					$defaultPreferences['variant'] = [
379
						'label-message' => 'yourvariant',
380
						'type' => 'select',
381
						'options' => $options,
382
						'section' => 'personal/i18n',
383
						'help-message' => 'prefs-help-variant',
384
					];
385
				} else {
386
					$defaultPreferences["variant-$langCode"] = [
387
						'type' => 'api',
388
					];
389
				}
390
			}
391
		}
392
393
		// Stuff from Language::getExtraUserToggles()
394
		// FIXME is this dead code? $extraUserToggles doesn't seem to be defined for any language
395
		$toggles = $wgContLang->getExtraUserToggles();
396
397
		foreach ( $toggles as $toggle ) {
398
			$defaultPreferences[$toggle] = [
399
				'type' => 'toggle',
400
				'section' => 'personal/i18n',
401
				'label-message' => "tog-$toggle",
402
			];
403
		}
404
405
		// show a preview of the old signature first
406
		$oldsigWikiText = $wgParser->preSaveTransform(
407
			'~~~',
408
			$context->getTitle(),
409
			$user,
410
			ParserOptions::newFromContext( $context )
411
		);
412
		$oldsigHTML = $context->getOutput()->parseInline( $oldsigWikiText, true, true );
413
		$defaultPreferences['oldsig'] = [
414
			'type' => 'info',
415
			'raw' => true,
416
			'label-message' => 'tog-oldsig',
417
			'default' => $oldsigHTML,
418
			'section' => 'personal/signature',
419
		];
420
		$defaultPreferences['nickname'] = [
421
			'type' => $authManager->allowsPropertyChange( 'nickname' ) ? 'text' : 'info',
422
			'maxlength' => $config->get( 'MaxSigChars' ),
423
			'label-message' => 'yournick',
424
			'validation-callback' => [ 'Preferences', 'validateSignature' ],
425
			'section' => 'personal/signature',
426
			'filter-callback' => [ 'Preferences', 'cleanSignature' ],
427
		];
428
		$defaultPreferences['fancysig'] = [
429
			'type' => 'toggle',
430
			'label-message' => 'tog-fancysig',
431
			// show general help about signature at the bottom of the section
432
			'help-message' => 'prefs-help-signature',
433
			'section' => 'personal/signature'
434
		];
435
436
		# # Email stuff
437
438
		if ( $config->get( 'EnableEmail' ) ) {
439
			if ( $canViewPrivateInfo ) {
440
				$helpMessages[] = $config->get( 'EmailConfirmToEdit' )
441
						? 'prefs-help-email-required'
442
						: 'prefs-help-email';
443
444
				if ( $config->get( 'EnableUserEmail' ) ) {
445
					// additional messages when users can send email to each other
446
					$helpMessages[] = 'prefs-help-email-others';
447
				}
448
449
				$emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
450
				if ( $canEditPrivateInfo && $authManager->allowsPropertyChange( 'emailaddress' ) ) {
451
					$link = Linker::link(
452
						SpecialPage::getTitleFor( 'ChangeEmail' ),
453
						$context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->escaped(),
454
						[],
455
						[ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
456
457
					$emailAddress .= $emailAddress == '' ? $link : (
458
						$context->msg( 'word-separator' )->escaped()
459
						. $context->msg( 'parentheses' )->rawParams( $link )->escaped()
460
					);
461
				}
462
463
				$defaultPreferences['emailaddress'] = [
464
					'type' => 'info',
465
					'raw' => true,
466
					'default' => $emailAddress,
467
					'label-message' => 'youremail',
468
					'section' => 'personal/email',
469
					'help-messages' => $helpMessages,
470
					# 'cssclass' chosen below
471
				];
472
			}
473
474
			$disableEmailPrefs = false;
475
476
			if ( $config->get( 'EmailAuthentication' ) ) {
477
				$emailauthenticationclass = 'mw-email-not-authenticated';
0 ignored issues
show
$emailauthenticationclass 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...
478
				if ( $user->getEmail() ) {
479
					if ( $user->getEmailAuthenticationTimestamp() ) {
480
						// date and time are separate parameters to facilitate localisation.
481
						// $time is kept for backward compat reasons.
482
						// 'emailauthenticated' is also used in SpecialConfirmemail.php
483
						$displayUser = $context->getUser();
484
						$emailTimestamp = $user->getEmailAuthenticationTimestamp();
485
						$time = $lang->userTimeAndDate( $emailTimestamp, $displayUser );
486
						$d = $lang->userDate( $emailTimestamp, $displayUser );
487
						$t = $lang->userTime( $emailTimestamp, $displayUser );
488
						$emailauthenticated = $context->msg( 'emailauthenticated',
489
							$time, $d, $t )->parse() . '<br />';
490
						$disableEmailPrefs = false;
491
						$emailauthenticationclass = 'mw-email-authenticated';
492
					} else {
493
						$disableEmailPrefs = true;
494
						$emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
495
							Linker::linkKnown(
496
								SpecialPage::getTitleFor( 'Confirmemail' ),
497
								$context->msg( 'emailconfirmlink' )->escaped()
498
							) . '<br />';
499
						$emailauthenticationclass = "mw-email-not-authenticated";
500
					}
501
				} else {
502
					$disableEmailPrefs = true;
503
					$emailauthenticated = $context->msg( 'noemailprefs' )->escaped();
504
					$emailauthenticationclass = 'mw-email-none';
505
				}
506
507
				if ( $canViewPrivateInfo ) {
508
					$defaultPreferences['emailauthentication'] = [
509
						'type' => 'info',
510
						'raw' => true,
511
						'section' => 'personal/email',
512
						'label-message' => 'prefs-emailconfirm-label',
513
						'default' => $emailauthenticated,
514
						# Apply the same CSS class used on the input to the message:
515
						'cssclass' => $emailauthenticationclass,
516
					];
517
				}
518
			}
519
520
			if ( $config->get( 'EnableUserEmail' ) && $user->isAllowed( 'sendemail' ) ) {
521
				$defaultPreferences['disablemail'] = [
522
					'type' => 'toggle',
523
					'invert' => true,
524
					'section' => 'personal/email',
525
					'label-message' => 'allowemail',
526
					'disabled' => $disableEmailPrefs,
527
				];
528
				$defaultPreferences['ccmeonemails'] = [
529
					'type' => 'toggle',
530
					'section' => 'personal/email',
531
					'label-message' => 'tog-ccmeonemails',
532
					'disabled' => $disableEmailPrefs,
533
				];
534
			}
535
536 View Code Duplication
			if ( $config->get( 'EnotifWatchlist' ) ) {
537
				$defaultPreferences['enotifwatchlistpages'] = [
538
					'type' => 'toggle',
539
					'section' => 'personal/email',
540
					'label-message' => 'tog-enotifwatchlistpages',
541
					'disabled' => $disableEmailPrefs,
542
				];
543
			}
544 View Code Duplication
			if ( $config->get( 'EnotifUserTalk' ) ) {
545
				$defaultPreferences['enotifusertalkpages'] = [
546
					'type' => 'toggle',
547
					'section' => 'personal/email',
548
					'label-message' => 'tog-enotifusertalkpages',
549
					'disabled' => $disableEmailPrefs,
550
				];
551
			}
552
			if ( $config->get( 'EnotifUserTalk' ) || $config->get( 'EnotifWatchlist' ) ) {
553 View Code Duplication
				if ( $config->get( 'EnotifMinorEdits' ) ) {
554
					$defaultPreferences['enotifminoredits'] = [
555
						'type' => 'toggle',
556
						'section' => 'personal/email',
557
						'label-message' => 'tog-enotifminoredits',
558
						'disabled' => $disableEmailPrefs,
559
					];
560
				}
561
562 View Code Duplication
				if ( $config->get( 'EnotifRevealEditorAddress' ) ) {
563
					$defaultPreferences['enotifrevealaddr'] = [
564
						'type' => 'toggle',
565
						'section' => 'personal/email',
566
						'label-message' => 'tog-enotifrevealaddr',
567
						'disabled' => $disableEmailPrefs,
568
					];
569
				}
570
			}
571
		}
572
	}
573
574
	/**
575
	 * @param User $user
576
	 * @param IContextSource $context
577
	 * @param array $defaultPreferences
578
	 * @return void
579
	 */
580
	static function skinPreferences( $user, IContextSource $context, &$defaultPreferences ) {
581
		# # Skin #####################################
582
583
		// Skin selector, if there is at least one valid skin
584
		$skinOptions = self::generateSkinOptions( $user, $context );
585 View Code Duplication
		if ( $skinOptions ) {
586
			$defaultPreferences['skin'] = [
587
				'type' => 'radio',
588
				'options' => $skinOptions,
589
				'label' => '&#160;',
590
				'section' => 'rendering/skin',
591
			];
592
		}
593
594
		$config = $context->getConfig();
595
		$allowUserCss = $config->get( 'AllowUserCss' );
596
		$allowUserJs = $config->get( 'AllowUserJs' );
597
		# Create links to user CSS/JS pages for all skins
598
		# This code is basically copied from generateSkinOptions().  It'd
599
		# be nice to somehow merge this back in there to avoid redundancy.
600
		if ( $allowUserCss || $allowUserJs ) {
601
			$linkTools = [];
602
			$userName = $user->getName();
603
604 View Code Duplication
			if ( $allowUserCss ) {
605
				$cssPage = Title::makeTitleSafe( NS_USER, $userName . '/common.css' );
606
				$linkTools[] = Linker::link( $cssPage, $context->msg( 'prefs-custom-css' )->escaped() );
607
			}
608
609 View Code Duplication
			if ( $allowUserJs ) {
610
				$jsPage = Title::makeTitleSafe( NS_USER, $userName . '/common.js' );
611
				$linkTools[] = Linker::link( $jsPage, $context->msg( 'prefs-custom-js' )->escaped() );
612
			}
613
614
			$defaultPreferences['commoncssjs'] = [
615
				'type' => 'info',
616
				'raw' => true,
617
				'default' => $context->getLanguage()->pipeList( $linkTools ),
618
				'label-message' => 'prefs-common-css-js',
619
				'section' => 'rendering/skin',
620
			];
621
		}
622
	}
623
624
	/**
625
	 * @param User $user
626
	 * @param IContextSource $context
627
	 * @param array $defaultPreferences
628
	 */
629
	static function filesPreferences( $user, IContextSource $context, &$defaultPreferences ) {
630
		# # Files #####################################
631
		$defaultPreferences['imagesize'] = [
632
			'type' => 'select',
633
			'options' => self::getImageSizes( $context ),
634
			'label-message' => 'imagemaxsize',
635
			'section' => 'rendering/files',
636
		];
637
		$defaultPreferences['thumbsize'] = [
638
			'type' => 'select',
639
			'options' => self::getThumbSizes( $context ),
640
			'label-message' => 'thumbsize',
641
			'section' => 'rendering/files',
642
		];
643
	}
644
645
	/**
646
	 * @param User $user
647
	 * @param IContextSource $context
648
	 * @param array $defaultPreferences
649
	 * @return void
650
	 */
651
	static function datetimePreferences( $user, IContextSource $context, &$defaultPreferences ) {
652
		# # Date and time #####################################
653
		$dateOptions = self::getDateOptions( $context );
654 View Code Duplication
		if ( $dateOptions ) {
655
			$defaultPreferences['date'] = [
656
				'type' => 'radio',
657
				'options' => $dateOptions,
658
				'label' => '&#160;',
659
				'section' => 'rendering/dateformat',
660
			];
661
		}
662
663
		// Info
664
		$now = wfTimestampNow();
665
		$lang = $context->getLanguage();
666
		$nowlocal = Xml::element( 'span', [ 'id' => 'wpLocalTime' ],
667
			$lang->userTime( $now, $user ) );
668
		$nowserver = $lang->userTime( $now, $user,
669
				[ 'format' => false, 'timecorrection' => false ] ) .
670
			Html::hidden( 'wpServerTime', (int)substr( $now, 8, 2 ) * 60 + (int)substr( $now, 10, 2 ) );
671
672
		$defaultPreferences['nowserver'] = [
673
			'type' => 'info',
674
			'raw' => 1,
675
			'label-message' => 'servertime',
676
			'default' => $nowserver,
677
			'section' => 'rendering/timeoffset',
678
		];
679
680
		$defaultPreferences['nowlocal'] = [
681
			'type' => 'info',
682
			'raw' => 1,
683
			'label-message' => 'localtime',
684
			'default' => $nowlocal,
685
			'section' => 'rendering/timeoffset',
686
		];
687
688
		// Grab existing pref.
689
		$tzOffset = $user->getOption( 'timecorrection' );
690
		$tz = explode( '|', $tzOffset, 3 );
691
692
		$tzOptions = self::getTimezoneOptions( $context );
693
694
		$tzSetting = $tzOffset;
695
		if ( count( $tz ) > 1 && $tz[0] == 'Offset' ) {
696
			$minDiff = $tz[1];
697
			$tzSetting = sprintf( '%+03d:%02d', floor( $minDiff / 60 ), abs( $minDiff ) % 60 );
698
		} elseif ( count( $tz ) > 1 && $tz[0] == 'ZoneInfo' &&
699
			!in_array( $tzOffset, HTMLFormField::flattenOptions( $tzOptions ) )
700
		) {
701
			# Timezone offset can vary with DST
702
			$userTZ = timezone_open( $tz[2] );
703
			if ( $userTZ !== false ) {
704
				$minDiff = floor( timezone_offset_get( $userTZ, date_create( 'now' ) ) / 60 );
705
				$tzSetting = "ZoneInfo|$minDiff|{$tz[2]}";
706
			}
707
		}
708
709
		$defaultPreferences['timecorrection'] = [
710
			'class' => 'HTMLSelectOrOtherField',
711
			'label-message' => 'timezonelegend',
712
			'options' => $tzOptions,
713
			'default' => $tzSetting,
714
			'size' => 20,
715
			'section' => 'rendering/timeoffset',
716
		];
717
	}
718
719
	/**
720
	 * @param User $user
721
	 * @param IContextSource $context
722
	 * @param array $defaultPreferences
723
	 */
724
	static function renderingPreferences( $user, IContextSource $context, &$defaultPreferences ) {
725
		# # Diffs ####################################
726
		$defaultPreferences['diffonly'] = [
727
			'type' => 'toggle',
728
			'section' => 'rendering/diffs',
729
			'label-message' => 'tog-diffonly',
730
		];
731
		$defaultPreferences['norollbackdiff'] = [
732
			'type' => 'toggle',
733
			'section' => 'rendering/diffs',
734
			'label-message' => 'tog-norollbackdiff',
735
		];
736
737
		# # Page Rendering ##############################
738
		if ( $context->getConfig()->get( 'AllowUserCssPrefs' ) ) {
739
			$defaultPreferences['underline'] = [
740
				'type' => 'select',
741
				'options' => [
742
					$context->msg( 'underline-never' )->text() => 0,
743
					$context->msg( 'underline-always' )->text() => 1,
744
					$context->msg( 'underline-default' )->text() => 2,
745
				],
746
				'label-message' => 'tog-underline',
747
				'section' => 'rendering/advancedrendering',
748
			];
749
		}
750
751
		$stubThresholdValues = [ 50, 100, 500, 1000, 2000, 5000, 10000 ];
752
		$stubThresholdOptions = [ $context->msg( 'stub-threshold-disabled' )->text() => 0 ];
753
		foreach ( $stubThresholdValues as $value ) {
754
			$stubThresholdOptions[$context->msg( 'size-bytes', $value )->text()] = $value;
755
		}
756
757
		$defaultPreferences['stubthreshold'] = [
758
			'type' => 'select',
759
			'section' => 'rendering/advancedrendering',
760
			'options' => $stubThresholdOptions,
761
			// This is not a raw HTML message; label-raw is needed for the manual <a></a>
762
			'label-raw' => $context->msg( 'stub-threshold' )->rawParams(
763
				'<a href="#" class="stub">' .
764
				$context->msg( 'stub-threshold-sample-link' )->parse() .
765
				'</a>' )->parse(),
766
		];
767
768
		$defaultPreferences['showhiddencats'] = [
769
			'type' => 'toggle',
770
			'section' => 'rendering/advancedrendering',
771
			'label-message' => 'tog-showhiddencats'
772
		];
773
774
		$defaultPreferences['numberheadings'] = [
775
			'type' => 'toggle',
776
			'section' => 'rendering/advancedrendering',
777
			'label-message' => 'tog-numberheadings',
778
		];
779
	}
780
781
	/**
782
	 * @param User $user
783
	 * @param IContextSource $context
784
	 * @param array $defaultPreferences
785
	 */
786
	static function editingPreferences( $user, IContextSource $context, &$defaultPreferences ) {
787
		# # Editing #####################################
788
		$defaultPreferences['editsectiononrightclick'] = [
789
			'type' => 'toggle',
790
			'section' => 'editing/advancedediting',
791
			'label-message' => 'tog-editsectiononrightclick',
792
		];
793
		$defaultPreferences['editondblclick'] = [
794
			'type' => 'toggle',
795
			'section' => 'editing/advancedediting',
796
			'label-message' => 'tog-editondblclick',
797
		];
798
799
		if ( $context->getConfig()->get( 'AllowUserCssPrefs' ) ) {
800
			$defaultPreferences['editfont'] = [
801
				'type' => 'select',
802
				'section' => 'editing/editor',
803
				'label-message' => 'editfont-style',
804
				'options' => [
805
					$context->msg( 'editfont-default' )->text() => 'default',
806
					$context->msg( 'editfont-monospace' )->text() => 'monospace',
807
					$context->msg( 'editfont-sansserif' )->text() => 'sans-serif',
808
					$context->msg( 'editfont-serif' )->text() => 'serif',
809
				]
810
			];
811
		}
812
		$defaultPreferences['cols'] = [
813
			'type' => 'int',
814
			'label-message' => 'columns',
815
			'section' => 'editing/editor',
816
			'min' => 4,
817
			'max' => 1000,
818
		];
819
		$defaultPreferences['rows'] = [
820
			'type' => 'int',
821
			'label-message' => 'rows',
822
			'section' => 'editing/editor',
823
			'min' => 4,
824
			'max' => 1000,
825
		];
826
		if ( $user->isAllowed( 'minoredit' ) ) {
827
			$defaultPreferences['minordefault'] = [
828
				'type' => 'toggle',
829
				'section' => 'editing/editor',
830
				'label-message' => 'tog-minordefault',
831
			];
832
		}
833
		$defaultPreferences['forceeditsummary'] = [
834
			'type' => 'toggle',
835
			'section' => 'editing/editor',
836
			'label-message' => 'tog-forceeditsummary',
837
		];
838
		$defaultPreferences['useeditwarning'] = [
839
			'type' => 'toggle',
840
			'section' => 'editing/editor',
841
			'label-message' => 'tog-useeditwarning',
842
		];
843
		$defaultPreferences['showtoolbar'] = [
844
			'type' => 'toggle',
845
			'section' => 'editing/editor',
846
			'label-message' => 'tog-showtoolbar',
847
		];
848
849
		$defaultPreferences['previewonfirst'] = [
850
			'type' => 'toggle',
851
			'section' => 'editing/preview',
852
			'label-message' => 'tog-previewonfirst',
853
		];
854
		$defaultPreferences['previewontop'] = [
855
			'type' => 'toggle',
856
			'section' => 'editing/preview',
857
			'label-message' => 'tog-previewontop',
858
		];
859
		$defaultPreferences['uselivepreview'] = [
860
			'type' => 'toggle',
861
			'section' => 'editing/preview',
862
			'label-message' => 'tog-uselivepreview',
863
		];
864
	}
865
866
	/**
867
	 * @param User $user
868
	 * @param IContextSource $context
869
	 * @param array $defaultPreferences
870
	 */
871
	static function rcPreferences( $user, IContextSource $context, &$defaultPreferences ) {
872
		$config = $context->getConfig();
873
		$rcMaxAge = $config->get( 'RCMaxAge' );
874
		# # RecentChanges #####################################
875
		$defaultPreferences['rcdays'] = [
876
			'type' => 'float',
877
			'label-message' => 'recentchangesdays',
878
			'section' => 'rc/displayrc',
879
			'min' => 1,
880
			'max' => ceil( $rcMaxAge / ( 3600 * 24 ) ),
881
			'help' => $context->msg( 'recentchangesdays-max' )->numParams(
882
				ceil( $rcMaxAge / ( 3600 * 24 ) ) )->escaped()
883
		];
884
		$defaultPreferences['rclimit'] = [
885
			'type' => 'int',
886
			'label-message' => 'recentchangescount',
887
			'help-message' => 'prefs-help-recentchangescount',
888
			'section' => 'rc/displayrc',
889
		];
890
		$defaultPreferences['usenewrc'] = [
891
			'type' => 'toggle',
892
			'label-message' => 'tog-usenewrc',
893
			'section' => 'rc/advancedrc',
894
		];
895
		$defaultPreferences['hideminor'] = [
896
			'type' => 'toggle',
897
			'label-message' => 'tog-hideminor',
898
			'section' => 'rc/advancedrc',
899
		];
900
901
		if ( $config->get( 'RCWatchCategoryMembership' ) ) {
902
			$defaultPreferences['hidecategorization'] = [
903
				'type' => 'toggle',
904
				'label-message' => 'tog-hidecategorization',
905
				'section' => 'rc/advancedrc',
906
			];
907
		}
908
909
		if ( $user->useRCPatrol() ) {
910
			$defaultPreferences['hidepatrolled'] = [
911
				'type' => 'toggle',
912
				'section' => 'rc/advancedrc',
913
				'label-message' => 'tog-hidepatrolled',
914
			];
915
		}
916
917
		if ( $user->useNPPatrol() ) {
918
			$defaultPreferences['newpageshidepatrolled'] = [
919
				'type' => 'toggle',
920
				'section' => 'rc/advancedrc',
921
				'label-message' => 'tog-newpageshidepatrolled',
922
			];
923
		}
924
925
		if ( $config->get( 'RCShowWatchingUsers' ) ) {
926
			$defaultPreferences['shownumberswatching'] = [
927
				'type' => 'toggle',
928
				'section' => 'rc/advancedrc',
929
				'label-message' => 'tog-shownumberswatching',
930
			];
931
		}
932
	}
933
934
	/**
935
	 * @param User $user
936
	 * @param IContextSource $context
937
	 * @param array $defaultPreferences
938
	 */
939
	static function watchlistPreferences( $user, IContextSource $context, &$defaultPreferences ) {
940
		$config = $context->getConfig();
941
		$watchlistdaysMax = ceil( $config->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
942
943
		# # Watchlist #####################################
944
		if ( $user->isAllowed( 'editmywatchlist' ) ) {
945
			$editWatchlistLinks = [];
946
			$editWatchlistModes = [
947
				'edit' => [ 'EditWatchlist', false ],
948
				'raw' => [ 'EditWatchlist', 'raw' ],
949
				'clear' => [ 'EditWatchlist', 'clear' ],
950
			];
951
			foreach ( $editWatchlistModes as $editWatchlistMode => $mode ) {
952
				// Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear
953
				$editWatchlistLinks[] = Linker::linkKnown(
954
					SpecialPage::getTitleFor( $mode[0], $mode[1] ),
955
					$context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse()
956
				);
957
			}
958
959
			$defaultPreferences['editwatchlist'] = [
960
				'type' => 'info',
961
				'raw' => true,
962
				'default' => $context->getLanguage()->pipeList( $editWatchlistLinks ),
963
				'label-message' => 'prefs-editwatchlist-label',
964
				'section' => 'watchlist/editwatchlist',
965
			];
966
		}
967
968
		$defaultPreferences['watchlistdays'] = [
969
			'type' => 'float',
970
			'min' => 0,
971
			'max' => $watchlistdaysMax,
972
			'section' => 'watchlist/displaywatchlist',
973
			'help' => $context->msg( 'prefs-watchlist-days-max' )->numParams(
974
				$watchlistdaysMax )->escaped(),
975
			'label-message' => 'prefs-watchlist-days',
976
		];
977
		$defaultPreferences['wllimit'] = [
978
			'type' => 'int',
979
			'min' => 0,
980
			'max' => 1000,
981
			'label-message' => 'prefs-watchlist-edits',
982
			'help' => $context->msg( 'prefs-watchlist-edits-max' )->escaped(),
983
			'section' => 'watchlist/displaywatchlist',
984
		];
985
		$defaultPreferences['extendwatchlist'] = [
986
			'type' => 'toggle',
987
			'section' => 'watchlist/advancedwatchlist',
988
			'label-message' => 'tog-extendwatchlist',
989
		];
990
		$defaultPreferences['watchlisthideminor'] = [
991
			'type' => 'toggle',
992
			'section' => 'watchlist/advancedwatchlist',
993
			'label-message' => 'tog-watchlisthideminor',
994
		];
995
		$defaultPreferences['watchlisthidebots'] = [
996
			'type' => 'toggle',
997
			'section' => 'watchlist/advancedwatchlist',
998
			'label-message' => 'tog-watchlisthidebots',
999
		];
1000
		$defaultPreferences['watchlisthideown'] = [
1001
			'type' => 'toggle',
1002
			'section' => 'watchlist/advancedwatchlist',
1003
			'label-message' => 'tog-watchlisthideown',
1004
		];
1005
		$defaultPreferences['watchlisthideanons'] = [
1006
			'type' => 'toggle',
1007
			'section' => 'watchlist/advancedwatchlist',
1008
			'label-message' => 'tog-watchlisthideanons',
1009
		];
1010
		$defaultPreferences['watchlisthideliu'] = [
1011
			'type' => 'toggle',
1012
			'section' => 'watchlist/advancedwatchlist',
1013
			'label-message' => 'tog-watchlisthideliu',
1014
		];
1015
		$defaultPreferences['watchlistreloadautomatically'] = [
1016
			'type' => 'toggle',
1017
			'section' => 'watchlist/advancedwatchlist',
1018
			'label-message' => 'tog-watchlistreloadautomatically',
1019
		];
1020
1021
		if ( $config->get( 'RCWatchCategoryMembership' ) ) {
1022
			$defaultPreferences['watchlisthidecategorization'] = [
1023
				'type' => 'toggle',
1024
				'section' => 'watchlist/advancedwatchlist',
1025
				'label-message' => 'tog-watchlisthidecategorization',
1026
			];
1027
		}
1028
1029
		if ( $user->useRCPatrol() ) {
1030
			$defaultPreferences['watchlisthidepatrolled'] = [
1031
				'type' => 'toggle',
1032
				'section' => 'watchlist/advancedwatchlist',
1033
				'label-message' => 'tog-watchlisthidepatrolled',
1034
			];
1035
		}
1036
1037
		$watchTypes = [
1038
			'edit' => 'watchdefault',
1039
			'move' => 'watchmoves',
1040
			'delete' => 'watchdeletion'
1041
		];
1042
1043
		// Kinda hacky
1044
		if ( $user->isAllowed( 'createpage' ) || $user->isAllowed( 'createtalk' ) ) {
1045
			$watchTypes['read'] = 'watchcreations';
1046
		}
1047
1048
		if ( $user->isAllowed( 'rollback' ) ) {
1049
			$watchTypes['rollback'] = 'watchrollback';
1050
		}
1051
1052
		if ( $user->isAllowed( 'upload' ) ) {
1053
			$watchTypes['upload'] = 'watchuploads';
1054
		}
1055
1056
		foreach ( $watchTypes as $action => $pref ) {
1057
			if ( $user->isAllowed( $action ) ) {
1058
				// Messages:
1059
				// tog-watchdefault, tog-watchmoves, tog-watchdeletion, tog-watchcreations, tog-watchuploads
1060
				// tog-watchrollback
1061
				$defaultPreferences[$pref] = [
1062
					'type' => 'toggle',
1063
					'section' => 'watchlist/advancedwatchlist',
1064
					'label-message' => "tog-$pref",
1065
				];
1066
			}
1067
		}
1068
1069
		if ( $config->get( 'EnableAPI' ) ) {
1070
			$defaultPreferences['watchlisttoken'] = [
1071
				'type' => 'api',
1072
			];
1073
			$defaultPreferences['watchlisttoken-info'] = [
1074
				'type' => 'info',
1075
				'section' => 'watchlist/tokenwatchlist',
1076
				'label-message' => 'prefs-watchlist-token',
1077
				'default' => $user->getTokenFromOption( 'watchlisttoken' ),
1078
				'help-message' => 'prefs-help-watchlist-token2',
1079
			];
1080
		}
1081
	}
1082
1083
	/**
1084
	 * @param User $user
1085
	 * @param IContextSource $context
1086
	 * @param array $defaultPreferences
1087
	 */
1088
	static function searchPreferences( $user, IContextSource $context, &$defaultPreferences ) {
1089
		foreach ( MWNamespace::getValidNamespaces() as $n ) {
1090
			$defaultPreferences['searchNs' . $n] = [
1091
				'type' => 'api',
1092
			];
1093
		}
1094
	}
1095
1096
	/**
1097
	 * Dummy, kept for backwards-compatibility.
1098
	 */
1099
	static function miscPreferences( $user, IContextSource $context, &$defaultPreferences ) {
1100
	}
1101
1102
	/**
1103
	 * @param User $user The User object
1104
	 * @param IContextSource $context
1105
	 * @return array Text/links to display as key; $skinkey as value
1106
	 */
1107
	static function generateSkinOptions( $user, IContextSource $context ) {
1108
		$ret = [];
1109
1110
		$mptitle = Title::newMainPage();
1111
		$previewtext = $context->msg( 'skin-preview' )->escaped();
1112
1113
		# Only show skins that aren't disabled in $wgSkipSkins
1114
		$validSkinNames = Skin::getAllowedSkins();
1115
1116
		# Sort by UI skin name. First though need to update validSkinNames as sometimes
1117
		# the skinkey & UI skinname differ (e.g. "standard" skinkey is "Classic" in the UI).
1118
		foreach ( $validSkinNames as $skinkey => &$skinname ) {
1119
			$msg = $context->msg( "skinname-{$skinkey}" );
1120
			if ( $msg->exists() ) {
1121
				$skinname = htmlspecialchars( $msg->text() );
1122
			}
1123
		}
1124
		asort( $validSkinNames );
1125
1126
		$config = $context->getConfig();
1127
		$defaultSkin = $config->get( 'DefaultSkin' );
1128
		$allowUserCss = $config->get( 'AllowUserCss' );
1129
		$allowUserJs = $config->get( 'AllowUserJs' );
1130
1131
		$foundDefault = false;
1132
		foreach ( $validSkinNames as $skinkey => $sn ) {
1133
			$linkTools = [];
1134
1135
			# Mark the default skin
1136
			if ( strcasecmp( $skinkey, $defaultSkin ) === 0 ) {
1137
				$linkTools[] = $context->msg( 'default' )->escaped();
1138
				$foundDefault = true;
1139
			}
1140
1141
			# Create preview link
1142
			$mplink = htmlspecialchars( $mptitle->getLocalURL( [ 'useskin' => $skinkey ] ) );
1143
			$linkTools[] = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
1144
1145
			# Create links to user CSS/JS pages
1146 View Code Duplication
			if ( $allowUserCss ) {
1147
				$cssPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.css' );
1148
				$linkTools[] = Linker::link( $cssPage, $context->msg( 'prefs-custom-css' )->escaped() );
1149
			}
1150
1151 View Code Duplication
			if ( $allowUserJs ) {
1152
				$jsPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.js' );
1153
				$linkTools[] = Linker::link( $jsPage, $context->msg( 'prefs-custom-js' )->escaped() );
1154
			}
1155
1156
			$display = $sn . ' ' . $context->msg( 'parentheses' )
1157
				->rawParams( $context->getLanguage()->pipeList( $linkTools ) )
1158
				->escaped();
1159
			$ret[$display] = $skinkey;
1160
		}
1161
1162
		if ( !$foundDefault ) {
1163
			// If the default skin is not available, things are going to break horribly because the
1164
			// default value for skin selector will not be a valid value. Let's just not show it then.
1165
			return [];
1166
		}
1167
1168
		return $ret;
1169
	}
1170
1171
	/**
1172
	 * @param IContextSource $context
1173
	 * @return array
1174
	 */
1175
	static function getDateOptions( IContextSource $context ) {
1176
		$lang = $context->getLanguage();
1177
		$dateopts = $lang->getDatePreferences();
1178
1179
		$ret = [];
1180
1181
		if ( $dateopts ) {
1182
			if ( !in_array( 'default', $dateopts ) ) {
1183
				$dateopts[] = 'default'; // Make sure default is always valid
1184
										// Bug 19237
1185
			}
1186
1187
			// FIXME KLUGE: site default might not be valid for user language
1188
			global $wgDefaultUserOptions;
1189
			if ( !in_array( $wgDefaultUserOptions['date'], $dateopts ) ) {
1190
				$wgDefaultUserOptions['date'] = 'default';
1191
			}
1192
1193
			$epoch = wfTimestampNow();
1194
			foreach ( $dateopts as $key ) {
1195
				if ( $key == 'default' ) {
1196
					$formatted = $context->msg( 'datedefault' )->escaped();
1197
				} else {
1198
					$formatted = htmlspecialchars( $lang->timeanddate( $epoch, false, $key ) );
1199
				}
1200
				$ret[$formatted] = $key;
1201
			}
1202
		}
1203
		return $ret;
1204
	}
1205
1206
	/**
1207
	 * @param IContextSource $context
1208
	 * @return array
1209
	 */
1210
	static function getImageSizes( IContextSource $context ) {
1211
		$ret = [];
1212
		$pixels = $context->msg( 'unit-pixel' )->text();
1213
1214
		foreach ( $context->getConfig()->get( 'ImageLimits' ) as $index => $limits ) {
1215
			$display = "{$limits[0]}×{$limits[1]}" . $pixels;
1216
			$ret[$display] = $index;
1217
		}
1218
1219
		return $ret;
1220
	}
1221
1222
	/**
1223
	 * @param IContextSource $context
1224
	 * @return array
1225
	 */
1226
	static function getThumbSizes( IContextSource $context ) {
1227
		$ret = [];
1228
		$pixels = $context->msg( 'unit-pixel' )->text();
1229
1230
		foreach ( $context->getConfig()->get( 'ThumbLimits' ) as $index => $size ) {
1231
			$display = $size . $pixels;
1232
			$ret[$display] = $index;
1233
		}
1234
1235
		return $ret;
1236
	}
1237
1238
	/**
1239
	 * @param string $signature
1240
	 * @param array $alldata
1241
	 * @param HTMLForm $form
1242
	 * @return bool|string
1243
	 */
1244
	static function validateSignature( $signature, $alldata, $form ) {
1245
		global $wgParser;
1246
		$maxSigChars = $form->getConfig()->get( 'MaxSigChars' );
1247
		if ( mb_strlen( $signature ) > $maxSigChars ) {
1248
			return Xml::element( 'span', [ 'class' => 'error' ],
1249
				$form->msg( 'badsiglength' )->numParams( $maxSigChars )->text() );
1250
		} elseif ( isset( $alldata['fancysig'] ) &&
1251
				$alldata['fancysig'] &&
1252
				$wgParser->validateSig( $signature ) === false
1253
		) {
1254
			return Xml::element(
1255
				'span',
1256
				[ 'class' => 'error' ],
1257
				$form->msg( 'badsig' )->text()
1258
			);
1259
		} else {
1260
			return true;
1261
		}
1262
	}
1263
1264
	/**
1265
	 * @param string $signature
1266
	 * @param array $alldata
1267
	 * @param HTMLForm $form
1268
	 * @return string
1269
	 */
1270
	static function cleanSignature( $signature, $alldata, $form ) {
1271
		if ( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) {
1272
			global $wgParser;
1273
			$signature = $wgParser->cleanSig( $signature );
1274
		} else {
1275
			// When no fancy sig used, make sure ~{3,5} get removed.
1276
			$signature = Parser::cleanSigInSig( $signature );
1277
		}
1278
1279
		return $signature;
1280
	}
1281
1282
	/**
1283
	 * @param User $user
1284
	 * @param IContextSource $context
1285
	 * @param string $formClass
1286
	 * @param array $remove Array of items to remove
1287
	 * @return PreferencesForm|HtmlForm
1288
	 */
1289
	static function getFormObject(
1290
		$user,
1291
		IContextSource $context,
1292
		$formClass = 'PreferencesForm',
1293
		array $remove = []
1294
	) {
1295
		$formDescriptor = Preferences::getPreferences( $user, $context );
1296
		if ( count( $remove ) ) {
1297
			$removeKeys = array_flip( $remove );
1298
			$formDescriptor = array_diff_key( $formDescriptor, $removeKeys );
1299
		}
1300
1301
		// Remove type=api preferences. They are not intended for rendering in the form.
1302
		foreach ( $formDescriptor as $name => $info ) {
1303
			if ( isset( $info['type'] ) && $info['type'] === 'api' ) {
1304
				unset( $formDescriptor[$name] );
1305
			}
1306
		}
1307
1308
		/**
1309
		 * @var $htmlForm PreferencesForm
1310
		 */
1311
		$htmlForm = new $formClass( $formDescriptor, $context, 'prefs' );
1312
1313
		$htmlForm->setModifiedUser( $user );
1314
		$htmlForm->setId( 'mw-prefs-form' );
1315
		$htmlForm->setAutocomplete( 'off' );
1316
		$htmlForm->setSubmitText( $context->msg( 'saveprefs' )->text() );
1317
		# Used message keys: 'accesskey-preferences-save', 'tooltip-preferences-save'
1318
		$htmlForm->setSubmitTooltip( 'preferences-save' );
1319
		$htmlForm->setSubmitID( 'prefsubmit' );
1320
		$htmlForm->setSubmitCallback( [ 'Preferences', 'tryFormSubmit' ] );
1321
1322
		return $htmlForm;
1323
	}
1324
1325
	/**
1326
	 * @param IContextSource $context
1327
	 * @return array
1328
	 */
1329
	static function getTimezoneOptions( IContextSource $context ) {
1330
		$opt = [];
1331
1332
		$localTZoffset = $context->getConfig()->get( 'LocalTZoffset' );
1333
		$timeZoneList = self::getTimeZoneList( $context->getLanguage() );
1334
1335
		$timestamp = MWTimestamp::getLocalInstance();
1336
		// Check that the LocalTZoffset is the same as the local time zone offset
1337
		if ( $localTZoffset == $timestamp->format( 'Z' ) / 60 ) {
1338
			$timezoneName = $timestamp->getTimezone()->getName();
1339
			// Localize timezone
1340
			if ( isset( $timeZoneList[$timezoneName] ) ) {
1341
				$timezoneName = $timeZoneList[$timezoneName]['name'];
1342
			}
1343
			$server_tz_msg = $context->msg(
1344
				'timezoneuseserverdefault',
1345
				$timezoneName
1346
			)->text();
1347
		} else {
1348
			$tzstring = sprintf(
1349
				'%+03d:%02d',
1350
				floor( $localTZoffset / 60 ),
1351
				abs( $localTZoffset ) % 60
1352
			);
1353
			$server_tz_msg = $context->msg( 'timezoneuseserverdefault', $tzstring )->text();
1354
		}
1355
		$opt[$server_tz_msg] = "System|$localTZoffset";
1356
		$opt[$context->msg( 'timezoneuseoffset' )->text()] = 'other';
1357
		$opt[$context->msg( 'guesstimezone' )->text()] = 'guess';
1358
1359
		foreach ( $timeZoneList as $timeZoneInfo ) {
1360
			$region = $timeZoneInfo['region'];
1361
			if ( !isset( $opt[$region] ) ) {
1362
				$opt[$region] = [];
1363
			}
1364
			$opt[$region][$timeZoneInfo['name']] = $timeZoneInfo['timecorrection'];
1365
		}
1366
		return $opt;
1367
	}
1368
1369
	/**
1370
	 * @param string $value
1371
	 * @param array $alldata
1372
	 * @return int
1373
	 */
1374
	static function filterIntval( $value, $alldata ) {
1375
		return intval( $value );
1376
	}
1377
1378
	/**
1379
	 * @param string $tz
1380
	 * @param array $alldata
1381
	 * @return string
1382
	 */
1383
	static function filterTimezoneInput( $tz, $alldata ) {
1384
		$data = explode( '|', $tz, 3 );
1385
		switch ( $data[0] ) {
1386
			case 'ZoneInfo':
1387
			case 'System':
1388
				return $tz;
1389
			default:
1390
				$data = explode( ':', $tz, 2 );
1391 View Code Duplication
				if ( count( $data ) == 2 ) {
1392
					$data[0] = intval( $data[0] );
1393
					$data[1] = intval( $data[1] );
1394
					$minDiff = abs( $data[0] ) * 60 + $data[1];
1395
					if ( $data[0] < 0 ) {
1396
						$minDiff = - $minDiff;
1397
					}
1398
				} else {
1399
					$minDiff = intval( $data[0] ) * 60;
1400
				}
1401
1402
				# Max is +14:00 and min is -12:00, see:
1403
				# https://en.wikipedia.org/wiki/Timezone
1404
				$minDiff = min( $minDiff, 840 );  # 14:00
1405
				$minDiff = max( $minDiff, - 720 ); # -12:00
1406
				return 'Offset|' . $minDiff;
1407
		}
1408
	}
1409
1410
	/**
1411
	 * Handle the form submission if everything validated properly
1412
	 *
1413
	 * @param array $formData
1414
	 * @param PreferencesForm $form
1415
	 * @return bool|Status|string
1416
	 */
1417
	static function tryFormSubmit( $formData, $form ) {
1418
		$user = $form->getModifiedUser();
1419
		$hiddenPrefs = $form->getConfig()->get( 'HiddenPrefs' );
1420
		$result = true;
1421
1422
		if ( !$user->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
1423
			return Status::newFatal( 'mypreferencesprotected' );
1424
		}
1425
1426
		// Filter input
1427
		foreach ( array_keys( $formData ) as $name ) {
1428
			if ( isset( self::$saveFilters[$name] ) ) {
1429
				$formData[$name] =
1430
					call_user_func( self::$saveFilters[$name], $formData[$name], $formData );
1431
			}
1432
		}
1433
1434
		// Fortunately, the realname field is MUCH simpler
1435
		// (not really "private", but still shouldn't be edited without permission)
1436
1437
		if ( !in_array( 'realname', $hiddenPrefs )
1438
			&& $user->isAllowed( 'editmyprivateinfo' )
1439
			&& array_key_exists( 'realname', $formData )
1440
		) {
1441
			$realName = $formData['realname'];
1442
			$user->setRealName( $realName );
1443
		}
1444
1445
		if ( $user->isAllowed( 'editmyoptions' ) ) {
1446
			foreach ( self::$saveBlacklist as $b ) {
1447
				unset( $formData[$b] );
1448
			}
1449
1450
			# If users have saved a value for a preference which has subsequently been disabled
1451
			# via $wgHiddenPrefs, we don't want to destroy that setting in case the preference
1452
			# is subsequently re-enabled
1453
			foreach ( $hiddenPrefs as $pref ) {
1454
				# If the user has not set a non-default value here, the default will be returned
1455
				# and subsequently discarded
1456
				$formData[$pref] = $user->getOption( $pref, null, true );
1457
			}
1458
1459
			// Keep old preferences from interfering due to back-compat code, etc.
1460
			$user->resetOptions( 'unused', $form->getContext() );
1461
1462
			foreach ( $formData as $key => $value ) {
1463
				$user->setOption( $key, $value );
1464
			}
1465
1466
			Hooks::run( 'PreferencesFormPreSave', [ $formData, $form, $user, &$result ] );
1467
		}
1468
1469
		MediaWiki\Auth\AuthManager::callLegacyAuthPlugin( 'updateExternalDB', [ $user ] );
1470
		$user->saveSettings();
1471
1472
		return $result;
1473
	}
1474
1475
	/**
1476
	 * @param array $formData
1477
	 * @param PreferencesForm $form
1478
	 * @return Status
1479
	 */
1480
	public static function tryUISubmit( $formData, $form ) {
1481
		$res = self::tryFormSubmit( $formData, $form );
1482
1483
		if ( $res ) {
1484
			$urlOptions = [];
1485
1486
			if ( $res === 'eauth' ) {
1487
				$urlOptions['eauth'] = 1;
1488
			}
1489
1490
			$urlOptions += $form->getExtraSuccessRedirectParameters();
1491
1492
			$url = $form->getTitle()->getFullURL( $urlOptions );
1493
1494
			$context = $form->getContext();
1495
			// Set session data for the success message
1496
			$context->getRequest()->getSession()->set( 'specialPreferencesSaveSuccess', 1 );
1497
1498
			$context->getOutput()->redirect( $url );
1499
		}
1500
1501
		return Status::newGood();
1502
	}
1503
1504
	/**
1505
	 * Get a list of all time zones
1506
	 * @param Language $language Language used for the localized names
1507
	 * @return array A list of all time zones. The system name of the time zone is used as key and
1508
	 *  the value is an array which contains localized name, the timecorrection value used for
1509
	 *  preferences and the region
1510
	 * @since 1.26
1511
	 */
1512
	public static function getTimeZoneList( Language $language ) {
1513
		$identifiers = DateTimeZone::listIdentifiers();
1514
		if ( $identifiers === false ) {
1515
			return [];
1516
		}
1517
		sort( $identifiers );
1518
1519
		$tzRegions = [
1520
			'Africa' => wfMessage( 'timezoneregion-africa' )->inLanguage( $language )->text(),
1521
			'America' => wfMessage( 'timezoneregion-america' )->inLanguage( $language )->text(),
1522
			'Antarctica' => wfMessage( 'timezoneregion-antarctica' )->inLanguage( $language )->text(),
1523
			'Arctic' => wfMessage( 'timezoneregion-arctic' )->inLanguage( $language )->text(),
1524
			'Asia' => wfMessage( 'timezoneregion-asia' )->inLanguage( $language )->text(),
1525
			'Atlantic' => wfMessage( 'timezoneregion-atlantic' )->inLanguage( $language )->text(),
1526
			'Australia' => wfMessage( 'timezoneregion-australia' )->inLanguage( $language )->text(),
1527
			'Europe' => wfMessage( 'timezoneregion-europe' )->inLanguage( $language )->text(),
1528
			'Indian' => wfMessage( 'timezoneregion-indian' )->inLanguage( $language )->text(),
1529
			'Pacific' => wfMessage( 'timezoneregion-pacific' )->inLanguage( $language )->text(),
1530
		];
1531
		asort( $tzRegions );
1532
1533
		$timeZoneList = [];
1534
1535
		$now = new DateTime();
1536
1537
		foreach ( $identifiers as $identifier ) {
1538
			$parts = explode( '/', $identifier, 2 );
1539
1540
			// DateTimeZone::listIdentifiers() returns a number of
1541
			// backwards-compatibility entries. This filters them out of the
1542
			// list presented to the user.
1543
			if ( count( $parts ) !== 2 || !array_key_exists( $parts[0], $tzRegions ) ) {
1544
				continue;
1545
			}
1546
1547
			// Localize region
1548
			$parts[0] = $tzRegions[$parts[0]];
1549
1550
			$dateTimeZone = new DateTimeZone( $identifier );
1551
			$minDiff = floor( $dateTimeZone->getOffset( $now ) / 60 );
1552
1553
			$display = str_replace( '_', ' ', $parts[0] . '/' . $parts[1] );
1554
			$value = "ZoneInfo|$minDiff|$identifier";
1555
1556
			$timeZoneList[$identifier] = [
1557
				'name' => $display,
1558
				'timecorrection' => $value,
1559
				'region' => $parts[0],
1560
			];
1561
		}
1562
1563
		return $timeZoneList;
1564
	}
1565
}
1566
1567
/** Some tweaks to allow js prefs to work */
1568
class PreferencesForm extends HTMLForm {
1569
	// Override default value from HTMLForm
1570
	protected $mSubSectionBeforeFields = false;
1571
1572
	private $modifiedUser;
1573
1574
	/**
1575
	 * @param User $user
1576
	 */
1577
	public function setModifiedUser( $user ) {
1578
		$this->modifiedUser = $user;
1579
	}
1580
1581
	/**
1582
	 * @return User
1583
	 */
1584
	public function getModifiedUser() {
1585
		if ( $this->modifiedUser === null ) {
1586
			return $this->getUser();
1587
		} else {
1588
			return $this->modifiedUser;
1589
		}
1590
	}
1591
1592
	/**
1593
	 * Get extra parameters for the query string when redirecting after
1594
	 * successful save.
1595
	 *
1596
	 * @return array
1597
	 */
1598
	public function getExtraSuccessRedirectParameters() {
1599
		return [];
1600
	}
1601
1602
	/**
1603
	 * @param string $html
1604
	 * @return string
1605
	 */
1606
	function wrapForm( $html ) {
1607
		$html = Xml::tags( 'div', [ 'id' => 'preferences' ], $html );
1608
1609
		return parent::wrapForm( $html );
1610
	}
1611
1612
	/**
1613
	 * @return string
1614
	 */
1615
	function getButtons() {
1616
		$attrs = [ 'id' => 'mw-prefs-restoreprefs' ];
1617
1618
		if ( !$this->getModifiedUser()->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
1619
			return '';
1620
		}
1621
1622
		$html = parent::getButtons();
1623
1624
		if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) {
1625
			$t = SpecialPage::getTitleFor( 'Preferences', 'reset' );
1626
1627
			$html .= "\n" . Linker::link( $t, $this->msg( 'restoreprefs' )->escaped(),
1628
				Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ) );
1629
1630
			$html = Xml::tags( 'div', [ 'class' => 'mw-prefs-buttons' ], $html );
1631
		}
1632
1633
		return $html;
1634
	}
1635
1636
	/**
1637
	 * Separate multi-option preferences into multiple preferences, since we
1638
	 * have to store them separately
1639
	 * @param array $data
1640
	 * @return array
1641
	 */
1642
	function filterDataForSubmit( $data ) {
1643
		foreach ( $this->mFlatFields as $fieldname => $field ) {
1644
			if ( $field instanceof HTMLNestedFilterable ) {
1645
				$info = $field->mParams;
1646
				$prefix = isset( $info['prefix'] ) ? $info['prefix'] : $fieldname;
1647
				foreach ( $field->filterDataForSubmit( $data[$fieldname] ) as $key => $value ) {
1648
					$data["$prefix$key"] = $value;
1649
				}
1650
				unset( $data[$fieldname] );
1651
			}
1652
		}
1653
1654
		return $data;
1655
	}
1656
1657
	/**
1658
	 * Get the whole body of the form.
1659
	 * @return string
1660
	 */
1661
	function getBody() {
1662
		return $this->displaySection( $this->mFieldTree, '', 'mw-prefsection-' );
1663
	}
1664
1665
	/**
1666
	 * Get the "<legend>" for a given section key. Normally this is the
1667
	 * prefs-$key message but we'll allow extensions to override it.
1668
	 * @param string $key
1669
	 * @return string
1670
	 */
1671
	function getLegend( $key ) {
1672
		$legend = parent::getLegend( $key );
1673
		Hooks::run( 'PreferencesGetLegend', [ $this, $key, &$legend ] );
1674
		return $legend;
1675
	}
1676
1677
	/**
1678
	 * Get the keys of each top level preference section.
1679
	 * @return array of section keys
1680
	 */
1681
	function getPreferenceSections() {
1682
		return array_keys( array_filter( $this->mFieldTree, 'is_array' ) );
1683
	}
1684
}
1685