Completed
Branch master (86dc85)
by
unknown
23:45
created

Preferences   F

Complexity

Total Complexity 173

Size/Duplication

Total Lines 1514
Duplicated Lines 6.14 %

Coupling/Cohesion

Components 1
Dependencies 24

Importance

Changes 2
Bugs 0 Features 1
Metric Value
wmc 173
lcom 1
cbo 24
dl 93
loc 1514
rs 0.5217
c 2
b 0
f 1

27 Methods

Rating   Name   Duplication   Size   Complexity  
A getSaveBlacklist() 0 3 1
B getPreferences() 0 24 2
C loadPreferenceValues() 0 41 11
C getOptionFromUser() 12 36 16
F profilePreferences() 32 360 41
B skinPreferences() 16 43 6
A filesPreferences() 0 15 1
C datetimePreferences() 8 67 8
A renderingPreferences() 0 56 3
B editingPreferences() 0 80 3
B rcPreferences() 0 62 5
F watchlistPreferences() 7 143 12
A searchPreferences() 0 7 2
A miscPreferences() 0 2 1
C generateSkinOptions() 8 63 8
B getDateOptions() 0 30 6
A getImageSizes() 0 11 2
A getThumbSizes() 0 11 2
B validateSignature() 0 19 5
A cleanSignature() 0 11 3
B getFormObject() 0 35 5
B getTimezoneOptions() 0 39 5
A filterIntval() 0 3 1
B filterTimezoneInput() 10 26 5
C tryFormSubmit() 0 58 11
A tryUISubmit() 0 23 3
B getTimeZoneList() 0 53 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Preferences often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Preferences, and based on these observations, apply Extract Interface, too.

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
23
/**
24
 * We're now using the HTMLForm object with some customisation to generate the
25
 * Preferences form. This object handles generic submission, CSRF protection,
26
 * layout and other logic in a reusable manner. We subclass it as a PreferencesForm
27
 * to make some minor customisations.
28
 *
29
 * In order to generate the form, the HTMLForm object needs an array structure
30
 * detailing the form fields available, and that's what this class is for. Each
31
 * element of the array is a basic property-list, including the type of field,
32
 * the label it is to be given in the form, callbacks for validation and
33
 * 'filtering', and other pertinent information. Note that the 'default' field
34
 * is named for generic forms, and does not represent the preference's default
35
 * (which is stored in $wgDefaultUserOptions), but the default for the form
36
 * field, which should be whatever the user has set for that preference. There
37
 * is no need to override it unless you have some special storage logic (for
38
 * instance, those not presently stored as options, but which are best set from
39
 * the user preferences view).
40
 *
41
 * Field types are implemented as subclasses of the generic HTMLFormField
42
 * object, and typically implement at least getInputHTML, which generates the
43
 * HTML for the input field to be placed in the table.
44
 *
45
 * Once fields have been retrieved and validated, submission logic is handed
46
 * over to the tryUISubmit static method of this class.
47
 */
48
class Preferences {
49
	/** @var array */
50
	protected static $defaultPreferences = null;
51
52
	/** @var array */
53
	protected static $saveFilters = [
54
		'timecorrection' => [ 'Preferences', 'filterTimezoneInput' ],
55
		'cols' => [ 'Preferences', 'filterIntval' ],
56
		'rows' => [ 'Preferences', 'filterIntval' ],
57
		'rclimit' => [ 'Preferences', 'filterIntval' ],
58
		'wllimit' => [ 'Preferences', 'filterIntval' ],
59
		'searchlimit' => [ 'Preferences', 'filterIntval' ],
60
	];
61
62
	// Stuff that shouldn't be saved as a preference.
63
	private static $saveBlacklist = [
64
		'realname',
65
		'emailaddress',
66
	];
67
68
	/**
69
	 * @return array
70
	 */
71
	static function getSaveBlacklist() {
72
		return self::$saveBlacklist;
73
	}
74
75
	/**
76
	 * @throws MWException
77
	 * @param User $user
78
	 * @param IContextSource $context
79
	 * @return array|null
80
	 */
81
	static function getPreferences( $user, IContextSource $context ) {
82
		if ( self::$defaultPreferences ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression self::$defaultPreferences of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
83
			return self::$defaultPreferences;
84
		}
85
86
		$defaultPreferences = [];
87
88
		self::profilePreferences( $user, $context, $defaultPreferences );
89
		self::skinPreferences( $user, $context, $defaultPreferences );
90
		self::datetimePreferences( $user, $context, $defaultPreferences );
91
		self::filesPreferences( $user, $context, $defaultPreferences );
92
		self::renderingPreferences( $user, $context, $defaultPreferences );
93
		self::editingPreferences( $user, $context, $defaultPreferences );
94
		self::rcPreferences( $user, $context, $defaultPreferences );
95
		self::watchlistPreferences( $user, $context, $defaultPreferences );
96
		self::searchPreferences( $user, $context, $defaultPreferences );
97
		self::miscPreferences( $user, $context, $defaultPreferences );
98
99
		Hooks::run( 'GetPreferences', [ $user, &$defaultPreferences ] );
100
101
		self::loadPreferenceValues( $user, $context, $defaultPreferences );
102
		self::$defaultPreferences = $defaultPreferences;
103
		return $defaultPreferences;
104
	}
105
106
	/**
107
	 * Loads existing values for a given array of preferences
108
	 * @throws MWException
109
	 * @param User $user
110
	 * @param IContextSource $context
111
	 * @param array $defaultPreferences Array to load values for
112
	 * @return array|null
113
	 */
114
	static function loadPreferenceValues( $user, $context, &$defaultPreferences ) {
115
		# # Remove preferences that wikis don't want to use
116
		foreach ( $context->getConfig()->get( 'HiddenPrefs' ) as $pref ) {
117
			if ( isset( $defaultPreferences[$pref] ) ) {
118
				unset( $defaultPreferences[$pref] );
119
			}
120
		}
121
122
		# # Make sure that form fields have their parent set. See bug 41337.
123
		$dummyForm = new HTMLForm( [], $context );
124
125
		$disable = !$user->isAllowed( 'editmyoptions' );
126
127
		$defaultOptions = User::getDefaultOptions();
128
		# # Prod in defaults from the user
129
		foreach ( $defaultPreferences as $name => &$info ) {
130
			$prefFromUser = self::getOptionFromUser( $name, $info, $user );
131
			if ( $disable && !in_array( $name, self::$saveBlacklist ) ) {
132
				$info['disabled'] = 'disabled';
133
			}
134
			$field = HTMLForm::loadInputFromParameters( $name, $info, $dummyForm ); // For validation
135
			$globalDefault = isset( $defaultOptions[$name] )
136
				? $defaultOptions[$name]
137
				: null;
138
139
			// If it validates, set it as the default
140
			if ( isset( $info['default'] ) ) {
141
				// Already set, no problem
142
				continue;
143
			} elseif ( !is_null( $prefFromUser ) && // Make sure we're not just pulling nothing
144
					$field->validate( $prefFromUser, $user->getOptions() ) === true ) {
145
				$info['default'] = $prefFromUser;
146
			} elseif ( $field->validate( $globalDefault, $user->getOptions() ) === true ) {
147
				$info['default'] = $globalDefault;
148
			} else {
149
				throw new MWException( "Global default '$globalDefault' is invalid for field $name" );
150
			}
151
		}
152
153
		return $defaultPreferences;
154
	}
155
156
	/**
157
	 * Pull option from a user account. Handles stuff like array-type preferences.
158
	 *
159
	 * @param string $name
160
	 * @param array $info
161
	 * @param User $user
162
	 * @return array|string
163
	 */
164
	static function getOptionFromUser( $name, $info, $user ) {
165
		$val = $user->getOption( $name );
166
167
		// Handling for multiselect preferences
168 View Code Duplication
		if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
169
				( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) {
170
			$options = HTMLFormField::flattenOptions( $info['options'] );
171
			$prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
172
			$val = [];
173
174
			foreach ( $options as $value ) {
175
				if ( $user->getOption( "$prefix$value" ) ) {
176
					$val[] = $value;
177
				}
178
			}
179
		}
180
181
		// Handling for checkmatrix preferences
182
		if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
183
				( isset( $info['class'] ) && $info['class'] == 'HTMLCheckMatrix' ) ) {
184
			$columns = HTMLFormField::flattenOptions( $info['columns'] );
185
			$rows = HTMLFormField::flattenOptions( $info['rows'] );
186
			$prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
187
			$val = [];
188
189
			foreach ( $columns as $column ) {
190
				foreach ( $rows as $row ) {
191
					if ( $user->getOption( "$prefix$column-$row" ) ) {
192
						$val[] = "$column-$row";
193
					}
194
				}
195
			}
196
		}
197
198
		return $val;
199
	}
200
201
	/**
202
	 * @param User $user
203
	 * @param IContextSource $context
204
	 * @param array $defaultPreferences
205
	 * @return void
206
	 */
207
	static function profilePreferences( $user, IContextSource $context, &$defaultPreferences ) {
208
		global $wgAuth, $wgContLang, $wgParser;
209
210
		$config = $context->getConfig();
211
		// retrieving user name for GENDER and misc.
212
		$userName = $user->getName();
213
214
		# # User info #####################################
215
		// Information panel
216
		$defaultPreferences['username'] = [
217
			'type' => 'info',
218
			'label-message' => [ 'username', $userName ],
219
			'default' => $userName,
220
			'section' => 'personal/info',
221
		];
222
223
		# Get groups to which the user belongs
224
		$userEffectiveGroups = $user->getEffectiveGroups();
225
		$userGroups = $userMembers = [];
226
		foreach ( $userEffectiveGroups as $ueg ) {
227
			if ( $ueg == '*' ) {
228
				// Skip the default * group, seems useless here
229
				continue;
230
			}
231
			$groupName = User::getGroupName( $ueg );
232
			$userGroups[] = User::makeGroupLinkHTML( $ueg, $groupName );
233
234
			$memberName = User::getGroupMember( $ueg, $userName );
235
			$userMembers[] = User::makeGroupLinkHTML( $ueg, $memberName );
236
		}
237
		asort( $userGroups );
238
		asort( $userMembers );
239
240
		$lang = $context->getLanguage();
241
242
		$defaultPreferences['usergroups'] = [
243
			'type' => 'info',
244
			'label' => $context->msg( 'prefs-memberingroups' )->numParams(
245
				count( $userGroups ) )->params( $userName )->parse(),
246
			'default' => $context->msg( 'prefs-memberingroups-type' )
247
				->rawParams( $lang->commaList( $userGroups ), $lang->commaList( $userMembers ) )
248
				->escaped(),
249
			'raw' => true,
250
			'section' => 'personal/info',
251
		];
252
253
		$editCount = Linker::link( SpecialPage::getTitleFor( "Contributions", $userName ),
254
			$lang->formatNum( $user->getEditCount() ) );
255
256
		$defaultPreferences['editcount'] = [
257
			'type' => 'info',
258
			'raw' => true,
259
			'label-message' => 'prefs-edits',
260
			'default' => $editCount,
261
			'section' => 'personal/info',
262
		];
263
264
		if ( $user->getRegistration() ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $user->getRegistration() of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
265
			$displayUser = $context->getUser();
266
			$userRegistration = $user->getRegistration();
267
			$defaultPreferences['registrationdate'] = [
268
				'type' => 'info',
269
				'label-message' => 'prefs-registration',
270
				'default' => $context->msg(
271
					'prefs-registration-date-time',
272
					$lang->userTimeAndDate( $userRegistration, $displayUser ),
273
					$lang->userDate( $userRegistration, $displayUser ),
274
					$lang->userTime( $userRegistration, $displayUser )
275
				)->parse(),
276
				'section' => 'personal/info',
277
			];
278
		}
279
280
		$canViewPrivateInfo = $user->isAllowed( 'viewmyprivateinfo' );
281
		$canEditPrivateInfo = $user->isAllowed( 'editmyprivateinfo' );
282
283
		// Actually changeable stuff
284
		$defaultPreferences['realname'] = [
285
			// (not really "private", but still shouldn't be edited without permission)
286
			'type' => $canEditPrivateInfo && $wgAuth->allowPropChange( 'realname' ) ? 'text' : 'info',
287
			'default' => $user->getRealName(),
288
			'section' => 'personal/info',
289
			'label-message' => 'yourrealname',
290
			'help-message' => 'prefs-help-realname',
291
		];
292
293
		if ( $canEditPrivateInfo && $wgAuth->allowPasswordChange() ) {
294
			$link = Linker::link( SpecialPage::getTitleFor( 'ChangePassword' ),
295
				$context->msg( 'prefs-resetpass' )->escaped(), [],
296
				[ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
297
298
			$defaultPreferences['password'] = [
299
				'type' => 'info',
300
				'raw' => true,
301
				'default' => $link,
302
				'label-message' => 'yourpassword',
303
				'section' => 'personal/info',
304
			];
305
		}
306
		// Only show prefershttps if secure login is turned on
307
		if ( $config->get( 'SecureLogin' ) && wfCanIPUseHTTPS( $context->getRequest()->getIP() ) ) {
308
			$defaultPreferences['prefershttps'] = [
309
				'type' => 'toggle',
310
				'label-message' => 'tog-prefershttps',
311
				'help-message' => 'prefs-help-prefershttps',
312
				'section' => 'personal/info'
313
			];
314
		}
315
316
		// Language
317
		$languages = Language::fetchLanguageNames( null, 'mw' );
318
		$languageCode = $config->get( 'LanguageCode' );
319
		if ( !array_key_exists( $languageCode, $languages ) ) {
320
			$languages[$languageCode] = $languageCode;
321
		}
322
		ksort( $languages );
323
324
		$options = [];
325
		foreach ( $languages as $code => $name ) {
326
			$display = wfBCP47( $code ) . ' - ' . $name;
327
			$options[$display] = $code;
328
		}
329
		$defaultPreferences['language'] = [
330
			'type' => 'select',
331
			'section' => 'personal/i18n',
332
			'options' => $options,
333
			'label-message' => 'yourlanguage',
334
		];
335
336
		$defaultPreferences['gender'] = [
337
			'type' => 'radio',
338
			'section' => 'personal/i18n',
339
			'options' => [
340
				$context->msg( 'parentheses' )
341
					->params( $context->msg( 'gender-unknown' )->plain() )
342
					->escaped() => 'unknown',
343
				$context->msg( 'gender-female' )->escaped() => 'female',
344
				$context->msg( 'gender-male' )->escaped() => 'male',
345
			],
346
			'label-message' => 'yourgender',
347
			'help-message' => 'prefs-help-gender',
348
		];
349
350
		// see if there are multiple language variants to choose from
351
		if ( !$config->get( 'DisableLangConversion' ) ) {
352
			foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
353
				if ( $langCode == $wgContLang->getCode() ) {
354
					$variants = $wgContLang->getVariants();
355
356
					if ( count( $variants ) <= 1 ) {
357
						continue;
358
					}
359
360
					$variantArray = [];
361
					foreach ( $variants as $v ) {
362
						$v = str_replace( '_', '-', strtolower( $v ) );
363
						$variantArray[$v] = $lang->getVariantname( $v, false );
364
					}
365
366
					$options = [];
367
					foreach ( $variantArray as $code => $name ) {
368
						$display = wfBCP47( $code ) . ' - ' . $name;
369
						$options[$display] = $code;
370
					}
371
372
					$defaultPreferences['variant'] = [
373
						'label-message' => 'yourvariant',
374
						'type' => 'select',
375
						'options' => $options,
376
						'section' => 'personal/i18n',
377
						'help-message' => 'prefs-help-variant',
378
					];
379
				} else {
380
					$defaultPreferences["variant-$langCode"] = [
381
						'type' => 'api',
382
					];
383
				}
384
			}
385
		}
386
387
		// Stuff from Language::getExtraUserToggles()
388
		// FIXME is this dead code? $extraUserToggles doesn't seem to be defined for any language
389
		$toggles = $wgContLang->getExtraUserToggles();
390
391
		foreach ( $toggles as $toggle ) {
392
			$defaultPreferences[$toggle] = [
393
				'type' => 'toggle',
394
				'section' => 'personal/i18n',
395
				'label-message' => "tog-$toggle",
396
			];
397
		}
398
399
		// show a preview of the old signature first
400
		$oldsigWikiText = $wgParser->preSaveTransform(
401
			'~~~',
402
			$context->getTitle(),
403
			$user,
404
			ParserOptions::newFromContext( $context )
405
		);
406
		$oldsigHTML = $context->getOutput()->parseInline( $oldsigWikiText, true, true );
407
		$defaultPreferences['oldsig'] = [
408
			'type' => 'info',
409
			'raw' => true,
410
			'label-message' => 'tog-oldsig',
411
			'default' => $oldsigHTML,
412
			'section' => 'personal/signature',
413
		];
414
		$defaultPreferences['nickname'] = [
415
			'type' => $wgAuth->allowPropChange( 'nickname' ) ? 'text' : 'info',
416
			'maxlength' => $config->get( 'MaxSigChars' ),
417
			'label-message' => 'yournick',
418
			'validation-callback' => [ 'Preferences', 'validateSignature' ],
419
			'section' => 'personal/signature',
420
			'filter-callback' => [ 'Preferences', 'cleanSignature' ],
421
		];
422
		$defaultPreferences['fancysig'] = [
423
			'type' => 'toggle',
424
			'label-message' => 'tog-fancysig',
425
			// show general help about signature at the bottom of the section
426
			'help-message' => 'prefs-help-signature',
427
			'section' => 'personal/signature'
428
		];
429
430
		# # Email stuff
431
432
		if ( $config->get( 'EnableEmail' ) ) {
433
			if ( $canViewPrivateInfo ) {
434
				$helpMessages[] = $config->get( 'EmailConfirmToEdit' )
0 ignored issues
show
Coding Style Comprehensibility introduced by
$helpMessages was never initialized. Although not strictly required by PHP, it is generally a good practice to add $helpMessages = 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...
435
						? 'prefs-help-email-required'
436
						: 'prefs-help-email';
437
438
				if ( $config->get( 'EnableUserEmail' ) ) {
439
					// additional messages when users can send email to each other
440
					$helpMessages[] = 'prefs-help-email-others';
441
				}
442
443
				$emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
444
				if ( $canEditPrivateInfo && $wgAuth->allowPropChange( 'emailaddress' ) ) {
445
					$link = Linker::link(
446
						SpecialPage::getTitleFor( 'ChangeEmail' ),
447
						$context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->escaped(),
448
						[],
449
						[ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
450
451
					$emailAddress .= $emailAddress == '' ? $link : (
452
						$context->msg( 'word-separator' )->escaped()
453
						. $context->msg( 'parentheses' )->rawParams( $link )->escaped()
454
					);
455
				}
456
457
				$defaultPreferences['emailaddress'] = [
458
					'type' => 'info',
459
					'raw' => true,
460
					'default' => $emailAddress,
461
					'label-message' => 'youremail',
462
					'section' => 'personal/email',
463
					'help-messages' => $helpMessages,
464
					# 'cssclass' chosen below
465
				];
466
			}
467
468
			$disableEmailPrefs = false;
469
470
			if ( $config->get( 'EmailAuthentication' ) ) {
471
				$emailauthenticationclass = 'mw-email-not-authenticated';
0 ignored issues
show
Unused Code introduced by
$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...
472
				if ( $user->getEmail() ) {
473
					if ( $user->getEmailAuthenticationTimestamp() ) {
474
						// date and time are separate parameters to facilitate localisation.
475
						// $time is kept for backward compat reasons.
476
						// 'emailauthenticated' is also used in SpecialConfirmemail.php
477
						$displayUser = $context->getUser();
478
						$emailTimestamp = $user->getEmailAuthenticationTimestamp();
479
						$time = $lang->userTimeAndDate( $emailTimestamp, $displayUser );
480
						$d = $lang->userDate( $emailTimestamp, $displayUser );
481
						$t = $lang->userTime( $emailTimestamp, $displayUser );
482
						$emailauthenticated = $context->msg( 'emailauthenticated',
483
							$time, $d, $t )->parse() . '<br />';
484
						$disableEmailPrefs = false;
485
						$emailauthenticationclass = 'mw-email-authenticated';
486
					} else {
487
						$disableEmailPrefs = true;
488
						$emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
489
							Linker::linkKnown(
490
								SpecialPage::getTitleFor( 'Confirmemail' ),
491
								$context->msg( 'emailconfirmlink' )->escaped()
492
							) . '<br />';
493
						$emailauthenticationclass = "mw-email-not-authenticated";
494
					}
495
				} else {
496
					$disableEmailPrefs = true;
497
					$emailauthenticated = $context->msg( 'noemailprefs' )->escaped();
498
					$emailauthenticationclass = 'mw-email-none';
499
				}
500
501
				if ( $canViewPrivateInfo ) {
502
					$defaultPreferences['emailauthentication'] = [
503
						'type' => 'info',
504
						'raw' => true,
505
						'section' => 'personal/email',
506
						'label-message' => 'prefs-emailconfirm-label',
507
						'default' => $emailauthenticated,
508
						# Apply the same CSS class used on the input to the message:
509
						'cssclass' => $emailauthenticationclass,
510
					];
511
				}
512
			}
513
514
			if ( $config->get( 'EnableUserEmail' ) && $user->isAllowed( 'sendemail' ) ) {
515
				$defaultPreferences['disablemail'] = [
516
					'type' => 'toggle',
517
					'invert' => true,
518
					'section' => 'personal/email',
519
					'label-message' => 'allowemail',
520
					'disabled' => $disableEmailPrefs,
521
				];
522
				$defaultPreferences['ccmeonemails'] = [
523
					'type' => 'toggle',
524
					'section' => 'personal/email',
525
					'label-message' => 'tog-ccmeonemails',
526
					'disabled' => $disableEmailPrefs,
527
				];
528
			}
529
530 View Code Duplication
			if ( $config->get( 'EnotifWatchlist' ) ) {
531
				$defaultPreferences['enotifwatchlistpages'] = [
532
					'type' => 'toggle',
533
					'section' => 'personal/email',
534
					'label-message' => 'tog-enotifwatchlistpages',
535
					'disabled' => $disableEmailPrefs,
536
				];
537
			}
538 View Code Duplication
			if ( $config->get( 'EnotifUserTalk' ) ) {
539
				$defaultPreferences['enotifusertalkpages'] = [
540
					'type' => 'toggle',
541
					'section' => 'personal/email',
542
					'label-message' => 'tog-enotifusertalkpages',
543
					'disabled' => $disableEmailPrefs,
544
				];
545
			}
546
			if ( $config->get( 'EnotifUserTalk' ) || $config->get( 'EnotifWatchlist' ) ) {
547 View Code Duplication
				if ( $config->get( 'EnotifMinorEdits' ) ) {
548
					$defaultPreferences['enotifminoredits'] = [
549
						'type' => 'toggle',
550
						'section' => 'personal/email',
551
						'label-message' => 'tog-enotifminoredits',
552
						'disabled' => $disableEmailPrefs,
553
					];
554
				}
555
556 View Code Duplication
				if ( $config->get( 'EnotifRevealEditorAddress' ) ) {
557
					$defaultPreferences['enotifrevealaddr'] = [
558
						'type' => 'toggle',
559
						'section' => 'personal/email',
560
						'label-message' => 'tog-enotifrevealaddr',
561
						'disabled' => $disableEmailPrefs,
562
					];
563
				}
564
			}
565
		}
566
	}
567
568
	/**
569
	 * @param User $user
570
	 * @param IContextSource $context
571
	 * @param array $defaultPreferences
572
	 * @return void
573
	 */
574
	static function skinPreferences( $user, IContextSource $context, &$defaultPreferences ) {
575
		# # Skin #####################################
576
577
		// Skin selector, if there is at least one valid skin
578
		$skinOptions = self::generateSkinOptions( $user, $context );
579 View Code Duplication
		if ( $skinOptions ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $skinOptions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
580
			$defaultPreferences['skin'] = [
581
				'type' => 'radio',
582
				'options' => $skinOptions,
583
				'label' => '&#160;',
584
				'section' => 'rendering/skin',
585
			];
586
		}
587
588
		$config = $context->getConfig();
589
		$allowUserCss = $config->get( 'AllowUserCss' );
590
		$allowUserJs = $config->get( 'AllowUserJs' );
591
		# Create links to user CSS/JS pages for all skins
592
		# This code is basically copied from generateSkinOptions().  It'd
593
		# be nice to somehow merge this back in there to avoid redundancy.
594
		if ( $allowUserCss || $allowUserJs ) {
595
			$linkTools = [];
596
			$userName = $user->getName();
597
598 View Code Duplication
			if ( $allowUserCss ) {
599
				$cssPage = Title::makeTitleSafe( NS_USER, $userName . '/common.css' );
600
				$linkTools[] = Linker::link( $cssPage, $context->msg( 'prefs-custom-css' )->escaped() );
0 ignored issues
show
Bug introduced by
It seems like $cssPage defined by \Title::makeTitleSafe(NS...erName . '/common.css') on line 599 can be null; however, Linker::link() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
601
			}
602
603 View Code Duplication
			if ( $allowUserJs ) {
604
				$jsPage = Title::makeTitleSafe( NS_USER, $userName . '/common.js' );
605
				$linkTools[] = Linker::link( $jsPage, $context->msg( 'prefs-custom-js' )->escaped() );
0 ignored issues
show
Bug introduced by
It seems like $jsPage defined by \Title::makeTitleSafe(NS...serName . '/common.js') on line 604 can be null; however, Linker::link() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
606
			}
607
608
			$defaultPreferences['commoncssjs'] = [
609
				'type' => 'info',
610
				'raw' => true,
611
				'default' => $context->getLanguage()->pipeList( $linkTools ),
612
				'label-message' => 'prefs-common-css-js',
613
				'section' => 'rendering/skin',
614
			];
615
		}
616
	}
617
618
	/**
619
	 * @param User $user
620
	 * @param IContextSource $context
621
	 * @param array $defaultPreferences
622
	 */
623
	static function filesPreferences( $user, IContextSource $context, &$defaultPreferences ) {
624
		# # Files #####################################
625
		$defaultPreferences['imagesize'] = [
626
			'type' => 'select',
627
			'options' => self::getImageSizes( $context ),
628
			'label-message' => 'imagemaxsize',
629
			'section' => 'rendering/files',
630
		];
631
		$defaultPreferences['thumbsize'] = [
632
			'type' => 'select',
633
			'options' => self::getThumbSizes( $context ),
634
			'label-message' => 'thumbsize',
635
			'section' => 'rendering/files',
636
		];
637
	}
638
639
	/**
640
	 * @param User $user
641
	 * @param IContextSource $context
642
	 * @param array $defaultPreferences
643
	 * @return void
644
	 */
645
	static function datetimePreferences( $user, IContextSource $context, &$defaultPreferences ) {
646
		# # Date and time #####################################
647
		$dateOptions = self::getDateOptions( $context );
648 View Code Duplication
		if ( $dateOptions ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dateOptions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
649
			$defaultPreferences['date'] = [
650
				'type' => 'radio',
651
				'options' => $dateOptions,
652
				'label' => '&#160;',
653
				'section' => 'rendering/dateformat',
654
			];
655
		}
656
657
		// Info
658
		$now = wfTimestampNow();
659
		$lang = $context->getLanguage();
660
		$nowlocal = Xml::element( 'span', [ 'id' => 'wpLocalTime' ],
661
			$lang->userTime( $now, $user ) );
662
		$nowserver = $lang->userTime( $now, $user,
663
				[ 'format' => false, 'timecorrection' => false ] ) .
664
			Html::hidden( 'wpServerTime', (int)substr( $now, 8, 2 ) * 60 + (int)substr( $now, 10, 2 ) );
665
666
		$defaultPreferences['nowserver'] = [
667
			'type' => 'info',
668
			'raw' => 1,
669
			'label-message' => 'servertime',
670
			'default' => $nowserver,
671
			'section' => 'rendering/timeoffset',
672
		];
673
674
		$defaultPreferences['nowlocal'] = [
675
			'type' => 'info',
676
			'raw' => 1,
677
			'label-message' => 'localtime',
678
			'default' => $nowlocal,
679
			'section' => 'rendering/timeoffset',
680
		];
681
682
		// Grab existing pref.
683
		$tzOffset = $user->getOption( 'timecorrection' );
684
		$tz = explode( '|', $tzOffset, 3 );
685
686
		$tzOptions = self::getTimezoneOptions( $context );
687
688
		$tzSetting = $tzOffset;
689
		if ( count( $tz ) > 1 && $tz[0] == 'Offset' ) {
690
			$minDiff = $tz[1];
691
			$tzSetting = sprintf( '%+03d:%02d', floor( $minDiff / 60 ), abs( $minDiff ) % 60 );
692
		} elseif ( count( $tz ) > 1 && $tz[0] == 'ZoneInfo' &&
693
			!in_array( $tzOffset, HTMLFormField::flattenOptions( $tzOptions ) )
694
		) {
695
			# Timezone offset can vary with DST
696
			$userTZ = timezone_open( $tz[2] );
697
			if ( $userTZ !== false ) {
698
				$minDiff = floor( timezone_offset_get( $userTZ, date_create( 'now' ) ) / 60 );
699
				$tzSetting = "ZoneInfo|$minDiff|{$tz[2]}";
700
			}
701
		}
702
703
		$defaultPreferences['timecorrection'] = [
704
			'class' => 'HTMLSelectOrOtherField',
705
			'label-message' => 'timezonelegend',
706
			'options' => $tzOptions,
707
			'default' => $tzSetting,
708
			'size' => 20,
709
			'section' => 'rendering/timeoffset',
710
		];
711
	}
712
713
	/**
714
	 * @param User $user
715
	 * @param IContextSource $context
716
	 * @param array $defaultPreferences
717
	 */
718
	static function renderingPreferences( $user, IContextSource $context, &$defaultPreferences ) {
719
		# # Diffs ####################################
720
		$defaultPreferences['diffonly'] = [
721
			'type' => 'toggle',
722
			'section' => 'rendering/diffs',
723
			'label-message' => 'tog-diffonly',
724
		];
725
		$defaultPreferences['norollbackdiff'] = [
726
			'type' => 'toggle',
727
			'section' => 'rendering/diffs',
728
			'label-message' => 'tog-norollbackdiff',
729
		];
730
731
		# # Page Rendering ##############################
732
		if ( $context->getConfig()->get( 'AllowUserCssPrefs' ) ) {
733
			$defaultPreferences['underline'] = [
734
				'type' => 'select',
735
				'options' => [
736
					$context->msg( 'underline-never' )->text() => 0,
737
					$context->msg( 'underline-always' )->text() => 1,
738
					$context->msg( 'underline-default' )->text() => 2,
739
				],
740
				'label-message' => 'tog-underline',
741
				'section' => 'rendering/advancedrendering',
742
			];
743
		}
744
745
		$stubThresholdValues = [ 50, 100, 500, 1000, 2000, 5000, 10000 ];
746
		$stubThresholdOptions = [ $context->msg( 'stub-threshold-disabled' )->text() => 0 ];
747
		foreach ( $stubThresholdValues as $value ) {
748
			$stubThresholdOptions[$context->msg( 'size-bytes', $value )->text()] = $value;
749
		}
750
751
		$defaultPreferences['stubthreshold'] = [
752
			'type' => 'select',
753
			'section' => 'rendering/advancedrendering',
754
			'options' => $stubThresholdOptions,
755
			// This is not a raw HTML message; label-raw is needed for the manual <a></a>
756
			'label-raw' => $context->msg( 'stub-threshold' )->rawParams(
757
				'<a href="#" class="stub">' .
758
				$context->msg( 'stub-threshold-sample-link' )->parse() .
759
				'</a>' )->parse(),
760
		];
761
762
		$defaultPreferences['showhiddencats'] = [
763
			'type' => 'toggle',
764
			'section' => 'rendering/advancedrendering',
765
			'label-message' => 'tog-showhiddencats'
766
		];
767
768
		$defaultPreferences['numberheadings'] = [
769
			'type' => 'toggle',
770
			'section' => 'rendering/advancedrendering',
771
			'label-message' => 'tog-numberheadings',
772
		];
773
	}
774
775
	/**
776
	 * @param User $user
777
	 * @param IContextSource $context
778
	 * @param array $defaultPreferences
779
	 */
780
	static function editingPreferences( $user, IContextSource $context, &$defaultPreferences ) {
781
		# # Editing #####################################
782
		$defaultPreferences['editsectiononrightclick'] = [
783
			'type' => 'toggle',
784
			'section' => 'editing/advancedediting',
785
			'label-message' => 'tog-editsectiononrightclick',
786
		];
787
		$defaultPreferences['editondblclick'] = [
788
			'type' => 'toggle',
789
			'section' => 'editing/advancedediting',
790
			'label-message' => 'tog-editondblclick',
791
		];
792
793
		if ( $context->getConfig()->get( 'AllowUserCssPrefs' ) ) {
794
			$defaultPreferences['editfont'] = [
795
				'type' => 'select',
796
				'section' => 'editing/editor',
797
				'label-message' => 'editfont-style',
798
				'options' => [
799
					$context->msg( 'editfont-default' )->text() => 'default',
800
					$context->msg( 'editfont-monospace' )->text() => 'monospace',
801
					$context->msg( 'editfont-sansserif' )->text() => 'sans-serif',
802
					$context->msg( 'editfont-serif' )->text() => 'serif',
803
				]
804
			];
805
		}
806
		$defaultPreferences['cols'] = [
807
			'type' => 'int',
808
			'label-message' => 'columns',
809
			'section' => 'editing/editor',
810
			'min' => 4,
811
			'max' => 1000,
812
		];
813
		$defaultPreferences['rows'] = [
814
			'type' => 'int',
815
			'label-message' => 'rows',
816
			'section' => 'editing/editor',
817
			'min' => 4,
818
			'max' => 1000,
819
		];
820
		if ( $user->isAllowed( 'minoredit' ) ) {
821
			$defaultPreferences['minordefault'] = [
822
				'type' => 'toggle',
823
				'section' => 'editing/editor',
824
				'label-message' => 'tog-minordefault',
825
			];
826
		}
827
		$defaultPreferences['forceeditsummary'] = [
828
			'type' => 'toggle',
829
			'section' => 'editing/editor',
830
			'label-message' => 'tog-forceeditsummary',
831
		];
832
		$defaultPreferences['useeditwarning'] = [
833
			'type' => 'toggle',
834
			'section' => 'editing/editor',
835
			'label-message' => 'tog-useeditwarning',
836
		];
837
		$defaultPreferences['showtoolbar'] = [
838
			'type' => 'toggle',
839
			'section' => 'editing/editor',
840
			'label-message' => 'tog-showtoolbar',
841
		];
842
843
		$defaultPreferences['previewonfirst'] = [
844
			'type' => 'toggle',
845
			'section' => 'editing/preview',
846
			'label-message' => 'tog-previewonfirst',
847
		];
848
		$defaultPreferences['previewontop'] = [
849
			'type' => 'toggle',
850
			'section' => 'editing/preview',
851
			'label-message' => 'tog-previewontop',
852
		];
853
		$defaultPreferences['uselivepreview'] = [
854
			'type' => 'toggle',
855
			'section' => 'editing/preview',
856
			'label-message' => 'tog-uselivepreview',
857
		];
858
859
	}
860
861
	/**
862
	 * @param User $user
863
	 * @param IContextSource $context
864
	 * @param array $defaultPreferences
865
	 */
866
	static function rcPreferences( $user, IContextSource $context, &$defaultPreferences ) {
867
		$config = $context->getConfig();
868
		$rcMaxAge = $config->get( 'RCMaxAge' );
869
		# # RecentChanges #####################################
870
		$defaultPreferences['rcdays'] = [
871
			'type' => 'float',
872
			'label-message' => 'recentchangesdays',
873
			'section' => 'rc/displayrc',
874
			'min' => 1,
875
			'max' => ceil( $rcMaxAge / ( 3600 * 24 ) ),
876
			'help' => $context->msg( 'recentchangesdays-max' )->numParams(
877
				ceil( $rcMaxAge / ( 3600 * 24 ) ) )->escaped()
878
		];
879
		$defaultPreferences['rclimit'] = [
880
			'type' => 'int',
881
			'label-message' => 'recentchangescount',
882
			'help-message' => 'prefs-help-recentchangescount',
883
			'section' => 'rc/displayrc',
884
		];
885
		$defaultPreferences['usenewrc'] = [
886
			'type' => 'toggle',
887
			'label-message' => 'tog-usenewrc',
888
			'section' => 'rc/advancedrc',
889
		];
890
		$defaultPreferences['hideminor'] = [
891
			'type' => 'toggle',
892
			'label-message' => 'tog-hideminor',
893
			'section' => 'rc/advancedrc',
894
		];
895
896
		if ( $config->get( 'RCWatchCategoryMembership' ) ) {
897
			$defaultPreferences['hidecategorization'] = [
898
				'type' => 'toggle',
899
				'label-message' => 'tog-hidecategorization',
900
				'section' => 'rc/advancedrc',
901
			];
902
		}
903
904
		if ( $user->useRCPatrol() ) {
905
			$defaultPreferences['hidepatrolled'] = [
906
				'type' => 'toggle',
907
				'section' => 'rc/advancedrc',
908
				'label-message' => 'tog-hidepatrolled',
909
			];
910
		}
911
912
		if ( $user->useNPPatrol() ) {
913
			$defaultPreferences['newpageshidepatrolled'] = [
914
				'type' => 'toggle',
915
				'section' => 'rc/advancedrc',
916
				'label-message' => 'tog-newpageshidepatrolled',
917
			];
918
		}
919
920
		if ( $config->get( 'RCShowWatchingUsers' ) ) {
921
			$defaultPreferences['shownumberswatching'] = [
922
				'type' => 'toggle',
923
				'section' => 'rc/advancedrc',
924
				'label-message' => 'tog-shownumberswatching',
925
			];
926
		}
927
	}
928
929
	/**
930
	 * @param User $user
931
	 * @param IContextSource $context
932
	 * @param array $defaultPreferences
933
	 */
934
	static function watchlistPreferences( $user, IContextSource $context, &$defaultPreferences ) {
935
		$config = $context->getConfig();
936
		$watchlistdaysMax = ceil( $config->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
937
938
		# # Watchlist #####################################
939
		if ( $user->isAllowed( 'editmywatchlist' ) ) {
940
			$editWatchlistLinks = [];
941
			$editWatchlistModes = [
942
				'edit' => [ 'EditWatchlist', false ],
943
				'raw' => [ 'EditWatchlist', 'raw' ],
944
				'clear' => [ 'EditWatchlist', 'clear' ],
945
			];
946 View Code Duplication
			foreach ( $editWatchlistModes as $editWatchlistMode => $mode ) {
947
				// Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear
948
				$editWatchlistLinks[] = Linker::linkKnown(
949
					SpecialPage::getTitleFor( $mode[0], $mode[1] ),
0 ignored issues
show
Security Bug introduced by
It seems like $mode[0] can also be of type false; however, SpecialPage::getTitleFor() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
950
					$context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse()
951
				);
952
			}
953
954
			$defaultPreferences['editwatchlist'] = [
955
				'type' => 'info',
956
				'raw' => true,
957
				'default' => $context->getLanguage()->pipeList( $editWatchlistLinks ),
958
				'label-message' => 'prefs-editwatchlist-label',
959
				'section' => 'watchlist/editwatchlist',
960
			];
961
		}
962
963
		$defaultPreferences['watchlistdays'] = [
964
			'type' => 'float',
965
			'min' => 0,
966
			'max' => $watchlistdaysMax,
967
			'section' => 'watchlist/displaywatchlist',
968
			'help' => $context->msg( 'prefs-watchlist-days-max' )->numParams(
969
				$watchlistdaysMax )->escaped(),
970
			'label-message' => 'prefs-watchlist-days',
971
		];
972
		$defaultPreferences['wllimit'] = [
973
			'type' => 'int',
974
			'min' => 0,
975
			'max' => 1000,
976
			'label-message' => 'prefs-watchlist-edits',
977
			'help' => $context->msg( 'prefs-watchlist-edits-max' )->escaped(),
978
			'section' => 'watchlist/displaywatchlist',
979
		];
980
		$defaultPreferences['extendwatchlist'] = [
981
			'type' => 'toggle',
982
			'section' => 'watchlist/advancedwatchlist',
983
			'label-message' => 'tog-extendwatchlist',
984
		];
985
		$defaultPreferences['watchlisthideminor'] = [
986
			'type' => 'toggle',
987
			'section' => 'watchlist/advancedwatchlist',
988
			'label-message' => 'tog-watchlisthideminor',
989
		];
990
		$defaultPreferences['watchlisthidebots'] = [
991
			'type' => 'toggle',
992
			'section' => 'watchlist/advancedwatchlist',
993
			'label-message' => 'tog-watchlisthidebots',
994
		];
995
		$defaultPreferences['watchlisthideown'] = [
996
			'type' => 'toggle',
997
			'section' => 'watchlist/advancedwatchlist',
998
			'label-message' => 'tog-watchlisthideown',
999
		];
1000
		$defaultPreferences['watchlisthideanons'] = [
1001
			'type' => 'toggle',
1002
			'section' => 'watchlist/advancedwatchlist',
1003
			'label-message' => 'tog-watchlisthideanons',
1004
		];
1005
		$defaultPreferences['watchlisthideliu'] = [
1006
			'type' => 'toggle',
1007
			'section' => 'watchlist/advancedwatchlist',
1008
			'label-message' => 'tog-watchlisthideliu',
1009
		];
1010
		$defaultPreferences['watchlistreloadautomatically'] = [
1011
			'type' => 'toggle',
1012
			'section' => 'watchlist/advancedwatchlist',
1013
			'label-message' => 'tog-watchlistreloadautomatically',
1014
		];
1015
1016
		if ( $config->get( 'RCWatchCategoryMembership' ) ) {
1017
			$defaultPreferences['watchlisthidecategorization'] = [
1018
				'type' => 'toggle',
1019
				'section' => 'watchlist/advancedwatchlist',
1020
				'label-message' => 'tog-watchlisthidecategorization',
1021
			];
1022
		}
1023
1024
		if ( $user->useRCPatrol() ) {
1025
			$defaultPreferences['watchlisthidepatrolled'] = [
1026
				'type' => 'toggle',
1027
				'section' => 'watchlist/advancedwatchlist',
1028
				'label-message' => 'tog-watchlisthidepatrolled',
1029
			];
1030
		}
1031
1032
		$watchTypes = [
1033
			'edit' => 'watchdefault',
1034
			'move' => 'watchmoves',
1035
			'delete' => 'watchdeletion'
1036
		];
1037
1038
		// Kinda hacky
1039
		if ( $user->isAllowed( 'createpage' ) || $user->isAllowed( 'createtalk' ) ) {
1040
			$watchTypes['read'] = 'watchcreations';
1041
		}
1042
1043
		if ( $user->isAllowed( 'rollback' ) ) {
1044
			$watchTypes['rollback'] = 'watchrollback';
1045
		}
1046
1047
		if ( $user->isAllowed( 'upload' ) ) {
1048
			$watchTypes['upload'] = 'watchuploads';
1049
		}
1050
1051
		foreach ( $watchTypes as $action => $pref ) {
1052
			if ( $user->isAllowed( $action ) ) {
1053
				// Messages:
1054
				// tog-watchdefault, tog-watchmoves, tog-watchdeletion, tog-watchcreations, tog-watchuploads
1055
				// tog-watchrollback
1056
				$defaultPreferences[$pref] = [
1057
					'type' => 'toggle',
1058
					'section' => 'watchlist/advancedwatchlist',
1059
					'label-message' => "tog-$pref",
1060
				];
1061
			}
1062
		}
1063
1064
		if ( $config->get( 'EnableAPI' ) ) {
1065
			$defaultPreferences['watchlisttoken'] = [
1066
				'type' => 'api',
1067
			];
1068
			$defaultPreferences['watchlisttoken-info'] = [
1069
				'type' => 'info',
1070
				'section' => 'watchlist/tokenwatchlist',
1071
				'label-message' => 'prefs-watchlist-token',
1072
				'default' => $user->getTokenFromOption( 'watchlisttoken' ),
0 ignored issues
show
Deprecated Code introduced by
The method User::getTokenFromOption() has been deprecated with message: 1.26 Applications should use the OAuth extension

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1073
				'help-message' => 'prefs-help-watchlist-token2',
1074
			];
1075
		}
1076
	}
1077
1078
	/**
1079
	 * @param User $user
1080
	 * @param IContextSource $context
1081
	 * @param array $defaultPreferences
1082
	 */
1083
	static function searchPreferences( $user, IContextSource $context, &$defaultPreferences ) {
1084
		foreach ( MWNamespace::getValidNamespaces() as $n ) {
1085
			$defaultPreferences['searchNs' . $n] = [
1086
				'type' => 'api',
1087
			];
1088
		}
1089
	}
1090
1091
	/**
1092
	 * Dummy, kept for backwards-compatibility.
1093
	 */
1094
	static function miscPreferences( $user, IContextSource $context, &$defaultPreferences ) {
1095
	}
1096
1097
	/**
1098
	 * @param User $user The User object
1099
	 * @param IContextSource $context
1100
	 * @return array Text/links to display as key; $skinkey as value
1101
	 */
1102
	static function generateSkinOptions( $user, IContextSource $context ) {
1103
		$ret = [];
1104
1105
		$mptitle = Title::newMainPage();
1106
		$previewtext = $context->msg( 'skin-preview' )->escaped();
1107
1108
		# Only show skins that aren't disabled in $wgSkipSkins
1109
		$validSkinNames = Skin::getAllowedSkins();
1110
1111
		# Sort by UI skin name. First though need to update validSkinNames as sometimes
1112
		# the skinkey & UI skinname differ (e.g. "standard" skinkey is "Classic" in the UI).
1113
		foreach ( $validSkinNames as $skinkey => &$skinname ) {
1114
			$msg = $context->msg( "skinname-{$skinkey}" );
1115
			if ( $msg->exists() ) {
1116
				$skinname = htmlspecialchars( $msg->text() );
1117
			}
1118
		}
1119
		asort( $validSkinNames );
1120
1121
		$config = $context->getConfig();
1122
		$defaultSkin = $config->get( 'DefaultSkin' );
1123
		$allowUserCss = $config->get( 'AllowUserCss' );
1124
		$allowUserJs = $config->get( 'AllowUserJs' );
1125
1126
		$foundDefault = false;
1127
		foreach ( $validSkinNames as $skinkey => $sn ) {
1128
			$linkTools = [];
1129
1130
			# Mark the default skin
1131
			if ( strcasecmp( $skinkey, $defaultSkin ) === 0 ) {
1132
				$linkTools[] = $context->msg( 'default' )->escaped();
1133
				$foundDefault = true;
1134
			}
1135
1136
			# Create preview link
1137
			$mplink = htmlspecialchars( $mptitle->getLocalURL( [ 'useskin' => $skinkey ] ) );
1138
			$linkTools[] = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
1139
1140
			# Create links to user CSS/JS pages
1141 View Code Duplication
			if ( $allowUserCss ) {
1142
				$cssPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.css' );
1143
				$linkTools[] = Linker::link( $cssPage, $context->msg( 'prefs-custom-css' )->escaped() );
0 ignored issues
show
Bug introduced by
It seems like $cssPage defined by \Title::makeTitleSafe(NS.../' . $skinkey . '.css') on line 1142 can be null; however, Linker::link() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1144
			}
1145
1146 View Code Duplication
			if ( $allowUserJs ) {
1147
				$jsPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.js' );
1148
				$linkTools[] = Linker::link( $jsPage, $context->msg( 'prefs-custom-js' )->escaped() );
0 ignored issues
show
Bug introduced by
It seems like $jsPage defined by \Title::makeTitleSafe(NS...'/' . $skinkey . '.js') on line 1147 can be null; however, Linker::link() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1149
			}
1150
1151
			$display = $sn . ' ' . $context->msg( 'parentheses' )
1152
				->rawParams( $context->getLanguage()->pipeList( $linkTools ) )
1153
				->escaped();
1154
			$ret[$display] = $skinkey;
1155
		}
1156
1157
		if ( !$foundDefault ) {
1158
			// If the default skin is not available, things are going to break horribly because the
1159
			// default value for skin selector will not be a valid value. Let's just not show it then.
1160
			return [];
1161
		}
1162
1163
		return $ret;
1164
	}
1165
1166
	/**
1167
	 * @param IContextSource $context
1168
	 * @return array
1169
	 */
1170
	static function getDateOptions( IContextSource $context ) {
1171
		$lang = $context->getLanguage();
1172
		$dateopts = $lang->getDatePreferences();
1173
1174
		$ret = [];
1175
1176
		if ( $dateopts ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dateopts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1177
			if ( !in_array( 'default', $dateopts ) ) {
1178
				$dateopts[] = 'default'; // Make sure default is always valid
1179
										// Bug 19237
1180
			}
1181
1182
			// FIXME KLUGE: site default might not be valid for user language
1183
			global $wgDefaultUserOptions;
1184
			if ( !in_array( $wgDefaultUserOptions['date'], $dateopts ) ) {
1185
				$wgDefaultUserOptions['date'] = 'default';
1186
			}
1187
1188
			$epoch = wfTimestampNow();
1189
			foreach ( $dateopts as $key ) {
1190
				if ( $key == 'default' ) {
1191
					$formatted = $context->msg( 'datedefault' )->escaped();
1192
				} else {
1193
					$formatted = htmlspecialchars( $lang->timeanddate( $epoch, false, $key ) );
0 ignored issues
show
Security Bug introduced by
It seems like $epoch defined by wfTimestampNow() on line 1188 can also be of type false; however, Language::timeanddate() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
1194
				}
1195
				$ret[$formatted] = $key;
1196
			}
1197
		}
1198
		return $ret;
1199
	}
1200
1201
	/**
1202
	 * @param IContextSource $context
1203
	 * @return array
1204
	 */
1205
	static function getImageSizes( IContextSource $context ) {
1206
		$ret = [];
1207
		$pixels = $context->msg( 'unit-pixel' )->text();
1208
1209
		foreach ( $context->getConfig()->get( 'ImageLimits' ) as $index => $limits ) {
1210
			$display = "{$limits[0]}×{$limits[1]}" . $pixels;
1211
			$ret[$display] = $index;
1212
		}
1213
1214
		return $ret;
1215
	}
1216
1217
	/**
1218
	 * @param IContextSource $context
1219
	 * @return array
1220
	 */
1221
	static function getThumbSizes( IContextSource $context ) {
1222
		$ret = [];
1223
		$pixels = $context->msg( 'unit-pixel' )->text();
1224
1225
		foreach ( $context->getConfig()->get( 'ThumbLimits' ) as $index => $size ) {
1226
			$display = $size . $pixels;
1227
			$ret[$display] = $index;
1228
		}
1229
1230
		return $ret;
1231
	}
1232
1233
	/**
1234
	 * @param string $signature
1235
	 * @param array $alldata
1236
	 * @param HTMLForm $form
1237
	 * @return bool|string
1238
	 */
1239
	static function validateSignature( $signature, $alldata, $form ) {
1240
		global $wgParser;
1241
		$maxSigChars = $form->getConfig()->get( 'MaxSigChars' );
1242
		if ( mb_strlen( $signature ) > $maxSigChars ) {
1243
			return Xml::element( 'span', [ 'class' => 'error' ],
1244
				$form->msg( 'badsiglength' )->numParams( $maxSigChars )->text() );
1245
		} elseif ( isset( $alldata['fancysig'] ) &&
1246
				$alldata['fancysig'] &&
1247
				$wgParser->validateSig( $signature ) === false
1248
		) {
1249
			return Xml::element(
1250
				'span',
1251
				[ 'class' => 'error' ],
1252
				$form->msg( 'badsig' )->text()
1253
			);
1254
		} else {
1255
			return true;
1256
		}
1257
	}
1258
1259
	/**
1260
	 * @param string $signature
1261
	 * @param array $alldata
1262
	 * @param HTMLForm $form
1263
	 * @return string
1264
	 */
1265
	static function cleanSignature( $signature, $alldata, $form ) {
1266
		if ( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) {
1267
			global $wgParser;
1268
			$signature = $wgParser->cleanSig( $signature );
1269
		} else {
1270
			// When no fancy sig used, make sure ~{3,5} get removed.
1271
			$signature = Parser::cleanSigInSig( $signature );
1272
		}
1273
1274
		return $signature;
1275
	}
1276
1277
	/**
1278
	 * @param User $user
1279
	 * @param IContextSource $context
1280
	 * @param string $formClass
1281
	 * @param array $remove Array of items to remove
1282
	 * @return PreferencesForm|HtmlForm
1283
	 */
1284
	static function getFormObject(
1285
		$user,
1286
		IContextSource $context,
1287
		$formClass = 'PreferencesForm',
1288
		array $remove = []
1289
	) {
1290
		$formDescriptor = Preferences::getPreferences( $user, $context );
1291
		if ( count( $remove ) ) {
1292
			$removeKeys = array_flip( $remove );
1293
			$formDescriptor = array_diff_key( $formDescriptor, $removeKeys );
1294
		}
1295
1296
		// Remove type=api preferences. They are not intended for rendering in the form.
1297
		foreach ( $formDescriptor as $name => $info ) {
1298
			if ( isset( $info['type'] ) && $info['type'] === 'api' ) {
1299
				unset( $formDescriptor[$name] );
1300
			}
1301
		}
1302
1303
		/**
1304
		 * @var $htmlForm PreferencesForm
1305
		 */
1306
		$htmlForm = new $formClass( $formDescriptor, $context, 'prefs' );
1307
1308
		$htmlForm->setModifiedUser( $user );
1309
		$htmlForm->setId( 'mw-prefs-form' );
1310
		$htmlForm->setAutocomplete( 'off' );
1311
		$htmlForm->setSubmitText( $context->msg( 'saveprefs' )->text() );
1312
		# Used message keys: 'accesskey-preferences-save', 'tooltip-preferences-save'
1313
		$htmlForm->setSubmitTooltip( 'preferences-save' );
1314
		$htmlForm->setSubmitID( 'prefsubmit' );
1315
		$htmlForm->setSubmitCallback( [ 'Preferences', 'tryFormSubmit' ] );
1316
1317
		return $htmlForm;
1318
	}
1319
1320
	/**
1321
	 * @param IContextSource $context
1322
	 * @return array
1323
	 */
1324
	static function getTimezoneOptions( IContextSource $context ) {
1325
		$opt = [];
1326
1327
		$localTZoffset = $context->getConfig()->get( 'LocalTZoffset' );
1328
		$timeZoneList = self::getTimeZoneList( $context->getLanguage() );
1329
1330
		$timestamp = MWTimestamp::getLocalInstance();
1331
		// Check that the LocalTZoffset is the same as the local time zone offset
1332
		if ( $localTZoffset == $timestamp->format( 'Z' ) / 60 ) {
1333
			$timezoneName = $timestamp->getTimezone()->getName();
1334
			// Localize timezone
1335
			if ( isset( $timeZoneList[$timezoneName] ) ) {
1336
				$timezoneName = $timeZoneList[$timezoneName]['name'];
1337
			}
1338
			$server_tz_msg = $context->msg(
1339
				'timezoneuseserverdefault',
1340
				$timezoneName
1341
			)->text();
1342
		} else {
1343
			$tzstring = sprintf(
1344
				'%+03d:%02d',
1345
				floor( $localTZoffset / 60 ),
1346
				abs( $localTZoffset ) % 60
1347
			);
1348
			$server_tz_msg = $context->msg( 'timezoneuseserverdefault', $tzstring )->text();
1349
		}
1350
		$opt[$server_tz_msg] = "System|$localTZoffset";
1351
		$opt[$context->msg( 'timezoneuseoffset' )->text()] = 'other';
1352
		$opt[$context->msg( 'guesstimezone' )->text()] = 'guess';
1353
1354
		foreach ( $timeZoneList as $timeZoneInfo ) {
1355
			$region = $timeZoneInfo['region'];
1356
			if ( !isset( $opt[$region] ) ) {
1357
				$opt[$region] = [];
1358
			}
1359
			$opt[$region][$timeZoneInfo['name']] = $timeZoneInfo['timecorrection'];
1360
		}
1361
		return $opt;
1362
	}
1363
1364
	/**
1365
	 * @param string $value
1366
	 * @param array $alldata
1367
	 * @return int
1368
	 */
1369
	static function filterIntval( $value, $alldata ) {
1370
		return intval( $value );
1371
	}
1372
1373
	/**
1374
	 * @param string $tz
1375
	 * @param array $alldata
1376
	 * @return string
1377
	 */
1378
	static function filterTimezoneInput( $tz, $alldata ) {
1379
		$data = explode( '|', $tz, 3 );
1380
		switch ( $data[0] ) {
1381
			case 'ZoneInfo':
1382
			case 'System':
1383
				return $tz;
1384
			default:
1385
				$data = explode( ':', $tz, 2 );
1386 View Code Duplication
				if ( count( $data ) == 2 ) {
1387
					$data[0] = intval( $data[0] );
1388
					$data[1] = intval( $data[1] );
1389
					$minDiff = abs( $data[0] ) * 60 + $data[1];
1390
					if ( $data[0] < 0 ) {
1391
						$minDiff = - $minDiff;
1392
					}
1393
				} else {
1394
					$minDiff = intval( $data[0] ) * 60;
1395
				}
1396
1397
				# Max is +14:00 and min is -12:00, see:
1398
				# https://en.wikipedia.org/wiki/Timezone
1399
				$minDiff = min( $minDiff, 840 );  # 14:00
1400
				$minDiff = max( $minDiff, - 720 ); # -12:00
1401
				return 'Offset|' . $minDiff;
1402
		}
1403
	}
1404
1405
	/**
1406
	 * Handle the form submission if everything validated properly
1407
	 *
1408
	 * @param array $formData
1409
	 * @param PreferencesForm $form
1410
	 * @return bool|Status|string
1411
	 */
1412
	static function tryFormSubmit( $formData, $form ) {
1413
		global $wgAuth;
1414
1415
		$user = $form->getModifiedUser();
1416
		$hiddenPrefs = $form->getConfig()->get( 'HiddenPrefs' );
1417
		$result = true;
1418
1419
		if ( !$user->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
1420
			return Status::newFatal( 'mypreferencesprotected' );
1421
		}
1422
1423
		// Filter input
1424
		foreach ( array_keys( $formData ) as $name ) {
1425
			if ( isset( self::$saveFilters[$name] ) ) {
1426
				$formData[$name] =
1427
					call_user_func( self::$saveFilters[$name], $formData[$name], $formData );
1428
			}
1429
		}
1430
1431
		// Fortunately, the realname field is MUCH simpler
1432
		// (not really "private", but still shouldn't be edited without permission)
1433
		if ( !in_array( 'realname', $hiddenPrefs )
1434
			&& $user->isAllowed( 'editmyprivateinfo' )
1435
			&& array_key_exists( 'realname', $formData )
1436
		) {
1437
			$realName = $formData['realname'];
1438
			$user->setRealName( $realName );
1439
		}
1440
1441
		if ( $user->isAllowed( 'editmyoptions' ) ) {
1442
			foreach ( self::$saveBlacklist as $b ) {
1443
				unset( $formData[$b] );
1444
			}
1445
1446
			# If users have saved a value for a preference which has subsequently been disabled
1447
			# via $wgHiddenPrefs, we don't want to destroy that setting in case the preference
1448
			# is subsequently re-enabled
1449
			foreach ( $hiddenPrefs as $pref ) {
1450
				# If the user has not set a non-default value here, the default will be returned
1451
				# and subsequently discarded
1452
				$formData[$pref] = $user->getOption( $pref, null, true );
1453
			}
1454
1455
			// Keep old preferences from interfering due to back-compat code, etc.
1456
			$user->resetOptions( 'unused', $form->getContext() );
1457
1458
			foreach ( $formData as $key => $value ) {
1459
				$user->setOption( $key, $value );
1460
			}
1461
1462
			Hooks::run( 'PreferencesFormPreSave', [ $formData, $form, $user, &$result ] );
1463
		}
1464
1465
		$wgAuth->updateExternalDB( $user );
1466
		$user->saveSettings();
1467
1468
		return $result;
1469
	}
1470
1471
	/**
1472
	 * @param array $formData
1473
	 * @param PreferencesForm $form
1474
	 * @return Status
1475
	 */
1476
	public static function tryUISubmit( $formData, $form ) {
1477
		$res = self::tryFormSubmit( $formData, $form );
1478
1479
		if ( $res ) {
1480
			$urlOptions = [];
1481
1482
			if ( $res === 'eauth' ) {
1483
				$urlOptions['eauth'] = 1;
1484
			}
1485
1486
			$urlOptions += $form->getExtraSuccessRedirectParameters();
1487
1488
			$url = $form->getTitle()->getFullURL( $urlOptions );
1489
1490
			$context = $form->getContext();
1491
			// Set session data for the success message
1492
			$context->getRequest()->setSessionData( 'specialPreferencesSaveSuccess', 1 );
1493
1494
			$context->getOutput()->redirect( $url );
1495
		}
1496
1497
		return Status::newGood();
1498
	}
1499
1500
	/**
1501
	 * Get a list of all time zones
1502
	 * @param Language $language Language used for the localized names
1503
	 * @return array A list of all time zones. The system name of the time zone is used as key and
1504
	 *  the value is an array which contains localized name, the timecorrection value used for
1505
	 *  preferences and the region
1506
	 * @since 1.26
1507
	 */
1508
	public static function getTimeZoneList( Language $language ) {
1509
		$identifiers = DateTimeZone::listIdentifiers();
1510
		if ( $identifiers === false ) {
1511
			return [];
1512
		}
1513
		sort( $identifiers );
1514
1515
		$tzRegions = [
1516
			'Africa' => wfMessage( 'timezoneregion-africa' )->inLanguage( $language )->text(),
1517
			'America' => wfMessage( 'timezoneregion-america' )->inLanguage( $language )->text(),
1518
			'Antarctica' => wfMessage( 'timezoneregion-antarctica' )->inLanguage( $language )->text(),
1519
			'Arctic' => wfMessage( 'timezoneregion-arctic' )->inLanguage( $language )->text(),
1520
			'Asia' => wfMessage( 'timezoneregion-asia' )->inLanguage( $language )->text(),
1521
			'Atlantic' => wfMessage( 'timezoneregion-atlantic' )->inLanguage( $language )->text(),
1522
			'Australia' => wfMessage( 'timezoneregion-australia' )->inLanguage( $language )->text(),
1523
			'Europe' => wfMessage( 'timezoneregion-europe' )->inLanguage( $language )->text(),
1524
			'Indian' => wfMessage( 'timezoneregion-indian' )->inLanguage( $language )->text(),
1525
			'Pacific' => wfMessage( 'timezoneregion-pacific' )->inLanguage( $language )->text(),
1526
		];
1527
		asort( $tzRegions );
1528
1529
		$timeZoneList = [];
1530
1531
		$now = new DateTime();
1532
1533
		foreach ( $identifiers as $identifier ) {
1534
			$parts = explode( '/', $identifier, 2 );
1535
1536
			// DateTimeZone::listIdentifiers() returns a number of
1537
			// backwards-compatibility entries. This filters them out of the
1538
			// list presented to the user.
1539
			if ( count( $parts ) !== 2 || !array_key_exists( $parts[0], $tzRegions ) ) {
1540
				continue;
1541
			}
1542
1543
			// Localize region
1544
			$parts[0] = $tzRegions[$parts[0]];
1545
1546
			$dateTimeZone = new DateTimeZone( $identifier );
1547
			$minDiff = floor( $dateTimeZone->getOffset( $now ) / 60 );
1548
1549
			$display = str_replace( '_', ' ', $parts[0] . '/' . $parts[1] );
1550
			$value = "ZoneInfo|$minDiff|$identifier";
1551
1552
			$timeZoneList[$identifier] = [
1553
				'name' => $display,
1554
				'timecorrection' => $value,
1555
				'region' => $parts[0],
1556
			];
1557
		}
1558
1559
		return $timeZoneList;
1560
	}
1561
}
1562
1563
/** Some tweaks to allow js prefs to work */
1564
class PreferencesForm extends HTMLForm {
1565
	// Override default value from HTMLForm
1566
	protected $mSubSectionBeforeFields = false;
1567
1568
	private $modifiedUser;
1569
1570
	/**
1571
	 * @param User $user
1572
	 */
1573
	public function setModifiedUser( $user ) {
1574
		$this->modifiedUser = $user;
1575
	}
1576
1577
	/**
1578
	 * @return User
1579
	 */
1580
	public function getModifiedUser() {
1581
		if ( $this->modifiedUser === null ) {
1582
			return $this->getUser();
1583
		} else {
1584
			return $this->modifiedUser;
1585
		}
1586
	}
1587
1588
	/**
1589
	 * Get extra parameters for the query string when redirecting after
1590
	 * successful save.
1591
	 *
1592
	 * @return array()
0 ignored issues
show
Documentation introduced by
The doc-type array() could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
1593
	 */
1594
	public function getExtraSuccessRedirectParameters() {
1595
		return [];
1596
	}
1597
1598
	/**
1599
	 * @param string $html
1600
	 * @return string
1601
	 */
1602
	function wrapForm( $html ) {
1603
		$html = Xml::tags( 'div', [ 'id' => 'preferences' ], $html );
1604
1605
		return parent::wrapForm( $html );
1606
	}
1607
1608
	/**
1609
	 * @return string
1610
	 */
1611
	function getButtons() {
1612
1613
		$attrs = [ 'id' => 'mw-prefs-restoreprefs' ];
1614
1615
		if ( !$this->getModifiedUser()->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
1616
			return '';
1617
		}
1618
1619
		$html = parent::getButtons();
1620
1621
		if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) {
1622
			$t = SpecialPage::getTitleFor( 'Preferences', 'reset' );
1623
1624
			$html .= "\n" . Linker::link( $t, $this->msg( 'restoreprefs' )->escaped(),
1625
				Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ) );
1626
1627
			$html = Xml::tags( 'div', [ 'class' => 'mw-prefs-buttons' ], $html );
1628
		}
1629
1630
		return $html;
1631
	}
1632
1633
	/**
1634
	 * Separate multi-option preferences into multiple preferences, since we
1635
	 * have to store them separately
1636
	 * @param array $data
1637
	 * @return array
1638
	 */
1639
	function filterDataForSubmit( $data ) {
1640
		foreach ( $this->mFlatFields as $fieldname => $field ) {
1641
			if ( $field instanceof HTMLNestedFilterable ) {
1642
				$info = $field->mParams;
1643
				$prefix = isset( $info['prefix'] ) ? $info['prefix'] : $fieldname;
1644
				foreach ( $field->filterDataForSubmit( $data[$fieldname] ) as $key => $value ) {
1645
					$data["$prefix$key"] = $value;
1646
				}
1647
				unset( $data[$fieldname] );
1648
			}
1649
		}
1650
1651
		return $data;
1652
	}
1653
1654
	/**
1655
	 * Get the whole body of the form.
1656
	 * @return string
1657
	 */
1658
	function getBody() {
1659
		return $this->displaySection( $this->mFieldTree, '', 'mw-prefsection-' );
1660
	}
1661
1662
	/**
1663
	 * Get the "<legend>" for a given section key. Normally this is the
1664
	 * prefs-$key message but we'll allow extensions to override it.
1665
	 * @param string $key
1666
	 * @return string
1667
	 */
1668
	function getLegend( $key ) {
1669
		$legend = parent::getLegend( $key );
1670
		Hooks::run( 'PreferencesGetLegend', [ $this, $key, &$legend ] );
1671
		return $legend;
1672
	}
1673
1674
	/**
1675
	 * Get the keys of each top level preference section.
1676
	 * @return array of section keys
1677
	 */
1678
	function getPreferenceSections() {
1679
		return array_keys( array_filter( $this->mFieldTree, 'is_array' ) );
1680
	}
1681
}
1682