Completed
Branch master (62f6c6)
by
unknown
21:31
created

Preferences   F

Complexity

Total Complexity 177

Size/Duplication

Total Lines 1522
Duplicated Lines 6.11 %

Coupling/Cohesion

Components 1
Dependencies 26

Importance

Changes 0
Metric Value
dl 93
loc 1522
rs 0.5217
c 0
b 0
f 0
wmc 177
lcom 1
cbo 26

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
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
A tryUISubmit() 0 23 3
B getTimeZoneList() 0 53 5
F profilePreferences() 32 369 45
C tryFormSubmit() 0 57 11

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
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 $wgAuth, $wgContLang, $wgParser, $wgDisableAuthManager;
211
212
		$config = $context->getConfig();
213
		// retrieving user name for GENDER and misc.
214
		$userName = $user->getName();
215
216
		# # User info #####################################
217
		// Information panel
218
		$defaultPreferences['username'] = [
219
			'type' => 'info',
220
			'label-message' => [ 'username', $userName ],
221
			'default' => $userName,
222
			'section' => 'personal/info',
223
		];
224
225
		# Get groups to which the user belongs
226
		$userEffectiveGroups = $user->getEffectiveGroups();
227
		$userGroups = $userMembers = [];
228
		foreach ( $userEffectiveGroups as $ueg ) {
229
			if ( $ueg == '*' ) {
230
				// Skip the default * group, seems useless here
231
				continue;
232
			}
233
			$groupName = User::getGroupName( $ueg );
234
			$userGroups[] = User::makeGroupLinkHTML( $ueg, $groupName );
235
236
			$memberName = User::getGroupMember( $ueg, $userName );
237
			$userMembers[] = User::makeGroupLinkHTML( $ueg, $memberName );
238
		}
239
		asort( $userGroups );
240
		asort( $userMembers );
241
242
		$lang = $context->getLanguage();
243
244
		$defaultPreferences['usergroups'] = [
245
			'type' => 'info',
246
			'label' => $context->msg( 'prefs-memberingroups' )->numParams(
247
				count( $userGroups ) )->params( $userName )->parse(),
248
			'default' => $context->msg( 'prefs-memberingroups-type' )
249
				->rawParams( $lang->commaList( $userGroups ), $lang->commaList( $userMembers ) )
250
				->escaped(),
251
			'raw' => true,
252
			'section' => 'personal/info',
253
		];
254
255
		$editCount = Linker::link( SpecialPage::getTitleFor( "Contributions", $userName ),
256
			$lang->formatNum( $user->getEditCount() ) );
257
258
		$defaultPreferences['editcount'] = [
259
			'type' => 'info',
260
			'raw' => true,
261
			'label-message' => 'prefs-edits',
262
			'default' => $editCount,
263
			'section' => 'personal/info',
264
		];
265
266
		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...
267
			$displayUser = $context->getUser();
268
			$userRegistration = $user->getRegistration();
269
			$defaultPreferences['registrationdate'] = [
270
				'type' => 'info',
271
				'label-message' => 'prefs-registration',
272
				'default' => $context->msg(
273
					'prefs-registration-date-time',
274
					$lang->userTimeAndDate( $userRegistration, $displayUser ),
275
					$lang->userDate( $userRegistration, $displayUser ),
276
					$lang->userTime( $userRegistration, $displayUser )
277
				)->parse(),
278
				'section' => 'personal/info',
279
			];
280
		}
281
282
		$canViewPrivateInfo = $user->isAllowed( 'viewmyprivateinfo' );
283
		$canEditPrivateInfo = $user->isAllowed( 'editmyprivateinfo' );
284
285
		// Actually changeable stuff
286
		$realnameChangeAllowed = $wgDisableAuthManager ? $wgAuth->allowPropChange( 'realname' )
287
			: AuthManager::singleton()->allowsPropertyChange( 'realname' );
288
		$defaultPreferences['realname'] = [
289
			// (not really "private", but still shouldn't be edited without permission)
290
			'type' => $canEditPrivateInfo && $realnameChangeAllowed ? 'text' : 'info',
291
			'default' => $user->getRealName(),
292
			'section' => 'personal/info',
293
			'label-message' => 'yourrealname',
294
			'help-message' => 'prefs-help-realname',
295
		];
296
297
		$allowPasswordChange = $wgDisableAuthManager ? $wgAuth->allowPasswordChange()
298
			: AuthManager::singleton()->allowsAuthenticationDataChange(
299
				new PasswordAuthenticationRequest(), false );
300
		if ( $canEditPrivateInfo && $allowPasswordChange ) {
301
			$link = Linker::link( SpecialPage::getTitleFor( 'ChangePassword' ),
302
				$context->msg( 'prefs-resetpass' )->escaped(), [],
303
				[ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
304
305
			$defaultPreferences['password'] = [
306
				'type' => 'info',
307
				'raw' => true,
308
				'default' => $link,
309
				'label-message' => 'yourpassword',
310
				'section' => 'personal/info',
311
			];
312
		}
313
		// Only show prefershttps if secure login is turned on
314
		if ( $config->get( 'SecureLogin' ) && wfCanIPUseHTTPS( $context->getRequest()->getIP() ) ) {
315
			$defaultPreferences['prefershttps'] = [
316
				'type' => 'toggle',
317
				'label-message' => 'tog-prefershttps',
318
				'help-message' => 'prefs-help-prefershttps',
319
				'section' => 'personal/info'
320
			];
321
		}
322
323
		// Language
324
		$languages = Language::fetchLanguageNames( null, 'mw' );
325
		$languageCode = $config->get( 'LanguageCode' );
326
		if ( !array_key_exists( $languageCode, $languages ) ) {
327
			$languages[$languageCode] = $languageCode;
328
		}
329
		ksort( $languages );
330
331
		$options = [];
332
		foreach ( $languages as $code => $name ) {
333
			$display = wfBCP47( $code ) . ' - ' . $name;
334
			$options[$display] = $code;
335
		}
336
		$defaultPreferences['language'] = [
337
			'type' => 'select',
338
			'section' => 'personal/i18n',
339
			'options' => $options,
340
			'label-message' => 'yourlanguage',
341
		];
342
343
		$defaultPreferences['gender'] = [
344
			'type' => 'radio',
345
			'section' => 'personal/i18n',
346
			'options' => [
347
				$context->msg( 'parentheses' )
348
					->params( $context->msg( 'gender-unknown' )->plain() )
349
					->escaped() => 'unknown',
350
				$context->msg( 'gender-female' )->escaped() => 'female',
351
				$context->msg( 'gender-male' )->escaped() => 'male',
352
			],
353
			'label-message' => 'yourgender',
354
			'help-message' => 'prefs-help-gender',
355
		];
356
357
		// see if there are multiple language variants to choose from
358
		if ( !$config->get( 'DisableLangConversion' ) ) {
359
			foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
360
				if ( $langCode == $wgContLang->getCode() ) {
361
					$variants = $wgContLang->getVariants();
362
363
					if ( count( $variants ) <= 1 ) {
364
						continue;
365
					}
366
367
					$variantArray = [];
368
					foreach ( $variants as $v ) {
369
						$v = str_replace( '_', '-', strtolower( $v ) );
370
						$variantArray[$v] = $lang->getVariantname( $v, false );
371
					}
372
373
					$options = [];
374
					foreach ( $variantArray as $code => $name ) {
375
						$display = wfBCP47( $code ) . ' - ' . $name;
376
						$options[$display] = $code;
377
					}
378
379
					$defaultPreferences['variant'] = [
380
						'label-message' => 'yourvariant',
381
						'type' => 'select',
382
						'options' => $options,
383
						'section' => 'personal/i18n',
384
						'help-message' => 'prefs-help-variant',
385
					];
386
				} else {
387
					$defaultPreferences["variant-$langCode"] = [
388
						'type' => 'api',
389
					];
390
				}
391
			}
392
		}
393
394
		// Stuff from Language::getExtraUserToggles()
395
		// FIXME is this dead code? $extraUserToggles doesn't seem to be defined for any language
396
		$toggles = $wgContLang->getExtraUserToggles();
397
398
		foreach ( $toggles as $toggle ) {
399
			$defaultPreferences[$toggle] = [
400
				'type' => 'toggle',
401
				'section' => 'personal/i18n',
402
				'label-message' => "tog-$toggle",
403
			];
404
		}
405
406
		// show a preview of the old signature first
407
		$oldsigWikiText = $wgParser->preSaveTransform(
408
			'~~~',
409
			$context->getTitle(),
410
			$user,
411
			ParserOptions::newFromContext( $context )
412
		);
413
		$oldsigHTML = $context->getOutput()->parseInline( $oldsigWikiText, true, true );
414
		$defaultPreferences['oldsig'] = [
415
			'type' => 'info',
416
			'raw' => true,
417
			'label-message' => 'tog-oldsig',
418
			'default' => $oldsigHTML,
419
			'section' => 'personal/signature',
420
		];
421
		$nicknameChangeAllowed = $wgDisableAuthManager ? $wgAuth->allowPropChange( 'nickname' )
422
			: AuthManager::singleton()->allowsPropertyChange( 'nickname' );
423
		$defaultPreferences['nickname'] = [
424
			'type' => $nicknameChangeAllowed ? 'text' : 'info',
425
			'maxlength' => $config->get( 'MaxSigChars' ),
426
			'label-message' => 'yournick',
427
			'validation-callback' => [ 'Preferences', 'validateSignature' ],
428
			'section' => 'personal/signature',
429
			'filter-callback' => [ 'Preferences', 'cleanSignature' ],
430
		];
431
		$defaultPreferences['fancysig'] = [
432
			'type' => 'toggle',
433
			'label-message' => 'tog-fancysig',
434
			// show general help about signature at the bottom of the section
435
			'help-message' => 'prefs-help-signature',
436
			'section' => 'personal/signature'
437
		];
438
439
		# # Email stuff
440
441
		if ( $config->get( 'EnableEmail' ) ) {
442
			if ( $canViewPrivateInfo ) {
443
				$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...
444
						? 'prefs-help-email-required'
445
						: 'prefs-help-email';
446
447
				if ( $config->get( 'EnableUserEmail' ) ) {
448
					// additional messages when users can send email to each other
449
					$helpMessages[] = 'prefs-help-email-others';
450
				}
451
452
				$emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
453
				$emailChangeAllowed = $wgDisableAuthManager ? $wgAuth->allowPropChange( 'emailaddress' )
454
					: AuthManager::singleton()->allowsPropertyChange( 'emailaddress' );
455
				if ( $canEditPrivateInfo && $emailChangeAllowed ) {
456
					$link = Linker::link(
457
						SpecialPage::getTitleFor( 'ChangeEmail' ),
458
						$context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->escaped(),
459
						[],
460
						[ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
461
462
					$emailAddress .= $emailAddress == '' ? $link : (
463
						$context->msg( 'word-separator' )->escaped()
464
						. $context->msg( 'parentheses' )->rawParams( $link )->escaped()
465
					);
466
				}
467
468
				$defaultPreferences['emailaddress'] = [
469
					'type' => 'info',
470
					'raw' => true,
471
					'default' => $emailAddress,
472
					'label-message' => 'youremail',
473
					'section' => 'personal/email',
474
					'help-messages' => $helpMessages,
475
					# 'cssclass' chosen below
476
				];
477
			}
478
479
			$disableEmailPrefs = false;
480
481
			if ( $config->get( 'EmailAuthentication' ) ) {
482
				$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...
483
				if ( $user->getEmail() ) {
484
					if ( $user->getEmailAuthenticationTimestamp() ) {
485
						// date and time are separate parameters to facilitate localisation.
486
						// $time is kept for backward compat reasons.
487
						// 'emailauthenticated' is also used in SpecialConfirmemail.php
488
						$displayUser = $context->getUser();
489
						$emailTimestamp = $user->getEmailAuthenticationTimestamp();
490
						$time = $lang->userTimeAndDate( $emailTimestamp, $displayUser );
491
						$d = $lang->userDate( $emailTimestamp, $displayUser );
492
						$t = $lang->userTime( $emailTimestamp, $displayUser );
493
						$emailauthenticated = $context->msg( 'emailauthenticated',
494
							$time, $d, $t )->parse() . '<br />';
495
						$disableEmailPrefs = false;
496
						$emailauthenticationclass = 'mw-email-authenticated';
497
					} else {
498
						$disableEmailPrefs = true;
499
						$emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
500
							Linker::linkKnown(
501
								SpecialPage::getTitleFor( 'Confirmemail' ),
502
								$context->msg( 'emailconfirmlink' )->escaped()
503
							) . '<br />';
504
						$emailauthenticationclass = "mw-email-not-authenticated";
505
					}
506
				} else {
507
					$disableEmailPrefs = true;
508
					$emailauthenticated = $context->msg( 'noemailprefs' )->escaped();
509
					$emailauthenticationclass = 'mw-email-none';
510
				}
511
512
				if ( $canViewPrivateInfo ) {
513
					$defaultPreferences['emailauthentication'] = [
514
						'type' => 'info',
515
						'raw' => true,
516
						'section' => 'personal/email',
517
						'label-message' => 'prefs-emailconfirm-label',
518
						'default' => $emailauthenticated,
519
						# Apply the same CSS class used on the input to the message:
520
						'cssclass' => $emailauthenticationclass,
521
					];
522
				}
523
			}
524
525
			if ( $config->get( 'EnableUserEmail' ) && $user->isAllowed( 'sendemail' ) ) {
526
				$defaultPreferences['disablemail'] = [
527
					'type' => 'toggle',
528
					'invert' => true,
529
					'section' => 'personal/email',
530
					'label-message' => 'allowemail',
531
					'disabled' => $disableEmailPrefs,
532
				];
533
				$defaultPreferences['ccmeonemails'] = [
534
					'type' => 'toggle',
535
					'section' => 'personal/email',
536
					'label-message' => 'tog-ccmeonemails',
537
					'disabled' => $disableEmailPrefs,
538
				];
539
			}
540
541 View Code Duplication
			if ( $config->get( 'EnotifWatchlist' ) ) {
542
				$defaultPreferences['enotifwatchlistpages'] = [
543
					'type' => 'toggle',
544
					'section' => 'personal/email',
545
					'label-message' => 'tog-enotifwatchlistpages',
546
					'disabled' => $disableEmailPrefs,
547
				];
548
			}
549 View Code Duplication
			if ( $config->get( 'EnotifUserTalk' ) ) {
550
				$defaultPreferences['enotifusertalkpages'] = [
551
					'type' => 'toggle',
552
					'section' => 'personal/email',
553
					'label-message' => 'tog-enotifusertalkpages',
554
					'disabled' => $disableEmailPrefs,
555
				];
556
			}
557
			if ( $config->get( 'EnotifUserTalk' ) || $config->get( 'EnotifWatchlist' ) ) {
558 View Code Duplication
				if ( $config->get( 'EnotifMinorEdits' ) ) {
559
					$defaultPreferences['enotifminoredits'] = [
560
						'type' => 'toggle',
561
						'section' => 'personal/email',
562
						'label-message' => 'tog-enotifminoredits',
563
						'disabled' => $disableEmailPrefs,
564
					];
565
				}
566
567 View Code Duplication
				if ( $config->get( 'EnotifRevealEditorAddress' ) ) {
568
					$defaultPreferences['enotifrevealaddr'] = [
569
						'type' => 'toggle',
570
						'section' => 'personal/email',
571
						'label-message' => 'tog-enotifrevealaddr',
572
						'disabled' => $disableEmailPrefs,
573
					];
574
				}
575
			}
576
		}
577
	}
578
579
	/**
580
	 * @param User $user
581
	 * @param IContextSource $context
582
	 * @param array $defaultPreferences
583
	 * @return void
584
	 */
585
	static function skinPreferences( $user, IContextSource $context, &$defaultPreferences ) {
586
		# # Skin #####################################
587
588
		// Skin selector, if there is at least one valid skin
589
		$skinOptions = self::generateSkinOptions( $user, $context );
590 View Code Duplication
		if ( $skinOptions ) {
591
			$defaultPreferences['skin'] = [
592
				'type' => 'radio',
593
				'options' => $skinOptions,
594
				'label' => '&#160;',
595
				'section' => 'rendering/skin',
596
			];
597
		}
598
599
		$config = $context->getConfig();
600
		$allowUserCss = $config->get( 'AllowUserCss' );
601
		$allowUserJs = $config->get( 'AllowUserJs' );
602
		# Create links to user CSS/JS pages for all skins
603
		# This code is basically copied from generateSkinOptions().  It'd
604
		# be nice to somehow merge this back in there to avoid redundancy.
605
		if ( $allowUserCss || $allowUserJs ) {
606
			$linkTools = [];
607
			$userName = $user->getName();
608
609 View Code Duplication
			if ( $allowUserCss ) {
610
				$cssPage = Title::makeTitleSafe( NS_USER, $userName . '/common.css' );
611
				$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 610 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...
612
			}
613
614 View Code Duplication
			if ( $allowUserJs ) {
615
				$jsPage = Title::makeTitleSafe( NS_USER, $userName . '/common.js' );
616
				$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 615 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...
617
			}
618
619
			$defaultPreferences['commoncssjs'] = [
620
				'type' => 'info',
621
				'raw' => true,
622
				'default' => $context->getLanguage()->pipeList( $linkTools ),
623
				'label-message' => 'prefs-common-css-js',
624
				'section' => 'rendering/skin',
625
			];
626
		}
627
	}
628
629
	/**
630
	 * @param User $user
631
	 * @param IContextSource $context
632
	 * @param array $defaultPreferences
633
	 */
634
	static function filesPreferences( $user, IContextSource $context, &$defaultPreferences ) {
635
		# # Files #####################################
636
		$defaultPreferences['imagesize'] = [
637
			'type' => 'select',
638
			'options' => self::getImageSizes( $context ),
639
			'label-message' => 'imagemaxsize',
640
			'section' => 'rendering/files',
641
		];
642
		$defaultPreferences['thumbsize'] = [
643
			'type' => 'select',
644
			'options' => self::getThumbSizes( $context ),
645
			'label-message' => 'thumbsize',
646
			'section' => 'rendering/files',
647
		];
648
	}
649
650
	/**
651
	 * @param User $user
652
	 * @param IContextSource $context
653
	 * @param array $defaultPreferences
654
	 * @return void
655
	 */
656
	static function datetimePreferences( $user, IContextSource $context, &$defaultPreferences ) {
657
		# # Date and time #####################################
658
		$dateOptions = self::getDateOptions( $context );
659 View Code Duplication
		if ( $dateOptions ) {
660
			$defaultPreferences['date'] = [
661
				'type' => 'radio',
662
				'options' => $dateOptions,
663
				'label' => '&#160;',
664
				'section' => 'rendering/dateformat',
665
			];
666
		}
667
668
		// Info
669
		$now = wfTimestampNow();
670
		$lang = $context->getLanguage();
671
		$nowlocal = Xml::element( 'span', [ 'id' => 'wpLocalTime' ],
672
			$lang->userTime( $now, $user ) );
673
		$nowserver = $lang->userTime( $now, $user,
674
				[ 'format' => false, 'timecorrection' => false ] ) .
675
			Html::hidden( 'wpServerTime', (int)substr( $now, 8, 2 ) * 60 + (int)substr( $now, 10, 2 ) );
676
677
		$defaultPreferences['nowserver'] = [
678
			'type' => 'info',
679
			'raw' => 1,
680
			'label-message' => 'servertime',
681
			'default' => $nowserver,
682
			'section' => 'rendering/timeoffset',
683
		];
684
685
		$defaultPreferences['nowlocal'] = [
686
			'type' => 'info',
687
			'raw' => 1,
688
			'label-message' => 'localtime',
689
			'default' => $nowlocal,
690
			'section' => 'rendering/timeoffset',
691
		];
692
693
		// Grab existing pref.
694
		$tzOffset = $user->getOption( 'timecorrection' );
695
		$tz = explode( '|', $tzOffset, 3 );
696
697
		$tzOptions = self::getTimezoneOptions( $context );
698
699
		$tzSetting = $tzOffset;
700
		if ( count( $tz ) > 1 && $tz[0] == 'Offset' ) {
701
			$minDiff = $tz[1];
702
			$tzSetting = sprintf( '%+03d:%02d', floor( $minDiff / 60 ), abs( $minDiff ) % 60 );
703
		} elseif ( count( $tz ) > 1 && $tz[0] == 'ZoneInfo' &&
704
			!in_array( $tzOffset, HTMLFormField::flattenOptions( $tzOptions ) )
705
		) {
706
			# Timezone offset can vary with DST
707
			$userTZ = timezone_open( $tz[2] );
708
			if ( $userTZ !== false ) {
709
				$minDiff = floor( timezone_offset_get( $userTZ, date_create( 'now' ) ) / 60 );
710
				$tzSetting = "ZoneInfo|$minDiff|{$tz[2]}";
711
			}
712
		}
713
714
		$defaultPreferences['timecorrection'] = [
715
			'class' => 'HTMLSelectOrOtherField',
716
			'label-message' => 'timezonelegend',
717
			'options' => $tzOptions,
718
			'default' => $tzSetting,
719
			'size' => 20,
720
			'section' => 'rendering/timeoffset',
721
		];
722
	}
723
724
	/**
725
	 * @param User $user
726
	 * @param IContextSource $context
727
	 * @param array $defaultPreferences
728
	 */
729
	static function renderingPreferences( $user, IContextSource $context, &$defaultPreferences ) {
730
		# # Diffs ####################################
731
		$defaultPreferences['diffonly'] = [
732
			'type' => 'toggle',
733
			'section' => 'rendering/diffs',
734
			'label-message' => 'tog-diffonly',
735
		];
736
		$defaultPreferences['norollbackdiff'] = [
737
			'type' => 'toggle',
738
			'section' => 'rendering/diffs',
739
			'label-message' => 'tog-norollbackdiff',
740
		];
741
742
		# # Page Rendering ##############################
743
		if ( $context->getConfig()->get( 'AllowUserCssPrefs' ) ) {
744
			$defaultPreferences['underline'] = [
745
				'type' => 'select',
746
				'options' => [
747
					$context->msg( 'underline-never' )->text() => 0,
748
					$context->msg( 'underline-always' )->text() => 1,
749
					$context->msg( 'underline-default' )->text() => 2,
750
				],
751
				'label-message' => 'tog-underline',
752
				'section' => 'rendering/advancedrendering',
753
			];
754
		}
755
756
		$stubThresholdValues = [ 50, 100, 500, 1000, 2000, 5000, 10000 ];
757
		$stubThresholdOptions = [ $context->msg( 'stub-threshold-disabled' )->text() => 0 ];
758
		foreach ( $stubThresholdValues as $value ) {
759
			$stubThresholdOptions[$context->msg( 'size-bytes', $value )->text()] = $value;
760
		}
761
762
		$defaultPreferences['stubthreshold'] = [
763
			'type' => 'select',
764
			'section' => 'rendering/advancedrendering',
765
			'options' => $stubThresholdOptions,
766
			// This is not a raw HTML message; label-raw is needed for the manual <a></a>
767
			'label-raw' => $context->msg( 'stub-threshold' )->rawParams(
768
				'<a href="#" class="stub">' .
769
				$context->msg( 'stub-threshold-sample-link' )->parse() .
770
				'</a>' )->parse(),
771
		];
772
773
		$defaultPreferences['showhiddencats'] = [
774
			'type' => 'toggle',
775
			'section' => 'rendering/advancedrendering',
776
			'label-message' => 'tog-showhiddencats'
777
		];
778
779
		$defaultPreferences['numberheadings'] = [
780
			'type' => 'toggle',
781
			'section' => 'rendering/advancedrendering',
782
			'label-message' => 'tog-numberheadings',
783
		];
784
	}
785
786
	/**
787
	 * @param User $user
788
	 * @param IContextSource $context
789
	 * @param array $defaultPreferences
790
	 */
791
	static function editingPreferences( $user, IContextSource $context, &$defaultPreferences ) {
792
		# # Editing #####################################
793
		$defaultPreferences['editsectiononrightclick'] = [
794
			'type' => 'toggle',
795
			'section' => 'editing/advancedediting',
796
			'label-message' => 'tog-editsectiononrightclick',
797
		];
798
		$defaultPreferences['editondblclick'] = [
799
			'type' => 'toggle',
800
			'section' => 'editing/advancedediting',
801
			'label-message' => 'tog-editondblclick',
802
		];
803
804
		if ( $context->getConfig()->get( 'AllowUserCssPrefs' ) ) {
805
			$defaultPreferences['editfont'] = [
806
				'type' => 'select',
807
				'section' => 'editing/editor',
808
				'label-message' => 'editfont-style',
809
				'options' => [
810
					$context->msg( 'editfont-default' )->text() => 'default',
811
					$context->msg( 'editfont-monospace' )->text() => 'monospace',
812
					$context->msg( 'editfont-sansserif' )->text() => 'sans-serif',
813
					$context->msg( 'editfont-serif' )->text() => 'serif',
814
				]
815
			];
816
		}
817
		$defaultPreferences['cols'] = [
818
			'type' => 'int',
819
			'label-message' => 'columns',
820
			'section' => 'editing/editor',
821
			'min' => 4,
822
			'max' => 1000,
823
		];
824
		$defaultPreferences['rows'] = [
825
			'type' => 'int',
826
			'label-message' => 'rows',
827
			'section' => 'editing/editor',
828
			'min' => 4,
829
			'max' => 1000,
830
		];
831
		if ( $user->isAllowed( 'minoredit' ) ) {
832
			$defaultPreferences['minordefault'] = [
833
				'type' => 'toggle',
834
				'section' => 'editing/editor',
835
				'label-message' => 'tog-minordefault',
836
			];
837
		}
838
		$defaultPreferences['forceeditsummary'] = [
839
			'type' => 'toggle',
840
			'section' => 'editing/editor',
841
			'label-message' => 'tog-forceeditsummary',
842
		];
843
		$defaultPreferences['useeditwarning'] = [
844
			'type' => 'toggle',
845
			'section' => 'editing/editor',
846
			'label-message' => 'tog-useeditwarning',
847
		];
848
		$defaultPreferences['showtoolbar'] = [
849
			'type' => 'toggle',
850
			'section' => 'editing/editor',
851
			'label-message' => 'tog-showtoolbar',
852
		];
853
854
		$defaultPreferences['previewonfirst'] = [
855
			'type' => 'toggle',
856
			'section' => 'editing/preview',
857
			'label-message' => 'tog-previewonfirst',
858
		];
859
		$defaultPreferences['previewontop'] = [
860
			'type' => 'toggle',
861
			'section' => 'editing/preview',
862
			'label-message' => 'tog-previewontop',
863
		];
864
		$defaultPreferences['uselivepreview'] = [
865
			'type' => 'toggle',
866
			'section' => 'editing/preview',
867
			'label-message' => 'tog-uselivepreview',
868
		];
869
870
	}
871
872
	/**
873
	 * @param User $user
874
	 * @param IContextSource $context
875
	 * @param array $defaultPreferences
876
	 */
877
	static function rcPreferences( $user, IContextSource $context, &$defaultPreferences ) {
878
		$config = $context->getConfig();
879
		$rcMaxAge = $config->get( 'RCMaxAge' );
880
		# # RecentChanges #####################################
881
		$defaultPreferences['rcdays'] = [
882
			'type' => 'float',
883
			'label-message' => 'recentchangesdays',
884
			'section' => 'rc/displayrc',
885
			'min' => 1,
886
			'max' => ceil( $rcMaxAge / ( 3600 * 24 ) ),
887
			'help' => $context->msg( 'recentchangesdays-max' )->numParams(
888
				ceil( $rcMaxAge / ( 3600 * 24 ) ) )->escaped()
889
		];
890
		$defaultPreferences['rclimit'] = [
891
			'type' => 'int',
892
			'label-message' => 'recentchangescount',
893
			'help-message' => 'prefs-help-recentchangescount',
894
			'section' => 'rc/displayrc',
895
		];
896
		$defaultPreferences['usenewrc'] = [
897
			'type' => 'toggle',
898
			'label-message' => 'tog-usenewrc',
899
			'section' => 'rc/advancedrc',
900
		];
901
		$defaultPreferences['hideminor'] = [
902
			'type' => 'toggle',
903
			'label-message' => 'tog-hideminor',
904
			'section' => 'rc/advancedrc',
905
		];
906
907
		if ( $config->get( 'RCWatchCategoryMembership' ) ) {
908
			$defaultPreferences['hidecategorization'] = [
909
				'type' => 'toggle',
910
				'label-message' => 'tog-hidecategorization',
911
				'section' => 'rc/advancedrc',
912
			];
913
		}
914
915
		if ( $user->useRCPatrol() ) {
916
			$defaultPreferences['hidepatrolled'] = [
917
				'type' => 'toggle',
918
				'section' => 'rc/advancedrc',
919
				'label-message' => 'tog-hidepatrolled',
920
			];
921
		}
922
923
		if ( $user->useNPPatrol() ) {
924
			$defaultPreferences['newpageshidepatrolled'] = [
925
				'type' => 'toggle',
926
				'section' => 'rc/advancedrc',
927
				'label-message' => 'tog-newpageshidepatrolled',
928
			];
929
		}
930
931
		if ( $config->get( 'RCShowWatchingUsers' ) ) {
932
			$defaultPreferences['shownumberswatching'] = [
933
				'type' => 'toggle',
934
				'section' => 'rc/advancedrc',
935
				'label-message' => 'tog-shownumberswatching',
936
			];
937
		}
938
	}
939
940
	/**
941
	 * @param User $user
942
	 * @param IContextSource $context
943
	 * @param array $defaultPreferences
944
	 */
945
	static function watchlistPreferences( $user, IContextSource $context, &$defaultPreferences ) {
946
		$config = $context->getConfig();
947
		$watchlistdaysMax = ceil( $config->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
948
949
		# # Watchlist #####################################
950
		if ( $user->isAllowed( 'editmywatchlist' ) ) {
951
			$editWatchlistLinks = [];
952
			$editWatchlistModes = [
953
				'edit' => [ 'EditWatchlist', false ],
954
				'raw' => [ 'EditWatchlist', 'raw' ],
955
				'clear' => [ 'EditWatchlist', 'clear' ],
956
			];
957 View Code Duplication
			foreach ( $editWatchlistModes as $editWatchlistMode => $mode ) {
958
				// Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear
959
				$editWatchlistLinks[] = Linker::linkKnown(
960
					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...
961
					$context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse()
962
				);
963
			}
964
965
			$defaultPreferences['editwatchlist'] = [
966
				'type' => 'info',
967
				'raw' => true,
968
				'default' => $context->getLanguage()->pipeList( $editWatchlistLinks ),
969
				'label-message' => 'prefs-editwatchlist-label',
970
				'section' => 'watchlist/editwatchlist',
971
			];
972
		}
973
974
		$defaultPreferences['watchlistdays'] = [
975
			'type' => 'float',
976
			'min' => 0,
977
			'max' => $watchlistdaysMax,
978
			'section' => 'watchlist/displaywatchlist',
979
			'help' => $context->msg( 'prefs-watchlist-days-max' )->numParams(
980
				$watchlistdaysMax )->escaped(),
981
			'label-message' => 'prefs-watchlist-days',
982
		];
983
		$defaultPreferences['wllimit'] = [
984
			'type' => 'int',
985
			'min' => 0,
986
			'max' => 1000,
987
			'label-message' => 'prefs-watchlist-edits',
988
			'help' => $context->msg( 'prefs-watchlist-edits-max' )->escaped(),
989
			'section' => 'watchlist/displaywatchlist',
990
		];
991
		$defaultPreferences['extendwatchlist'] = [
992
			'type' => 'toggle',
993
			'section' => 'watchlist/advancedwatchlist',
994
			'label-message' => 'tog-extendwatchlist',
995
		];
996
		$defaultPreferences['watchlisthideminor'] = [
997
			'type' => 'toggle',
998
			'section' => 'watchlist/advancedwatchlist',
999
			'label-message' => 'tog-watchlisthideminor',
1000
		];
1001
		$defaultPreferences['watchlisthidebots'] = [
1002
			'type' => 'toggle',
1003
			'section' => 'watchlist/advancedwatchlist',
1004
			'label-message' => 'tog-watchlisthidebots',
1005
		];
1006
		$defaultPreferences['watchlisthideown'] = [
1007
			'type' => 'toggle',
1008
			'section' => 'watchlist/advancedwatchlist',
1009
			'label-message' => 'tog-watchlisthideown',
1010
		];
1011
		$defaultPreferences['watchlisthideanons'] = [
1012
			'type' => 'toggle',
1013
			'section' => 'watchlist/advancedwatchlist',
1014
			'label-message' => 'tog-watchlisthideanons',
1015
		];
1016
		$defaultPreferences['watchlisthideliu'] = [
1017
			'type' => 'toggle',
1018
			'section' => 'watchlist/advancedwatchlist',
1019
			'label-message' => 'tog-watchlisthideliu',
1020
		];
1021
		$defaultPreferences['watchlistreloadautomatically'] = [
1022
			'type' => 'toggle',
1023
			'section' => 'watchlist/advancedwatchlist',
1024
			'label-message' => 'tog-watchlistreloadautomatically',
1025
		];
1026
1027
		if ( $config->get( 'RCWatchCategoryMembership' ) ) {
1028
			$defaultPreferences['watchlisthidecategorization'] = [
1029
				'type' => 'toggle',
1030
				'section' => 'watchlist/advancedwatchlist',
1031
				'label-message' => 'tog-watchlisthidecategorization',
1032
			];
1033
		}
1034
1035
		if ( $user->useRCPatrol() ) {
1036
			$defaultPreferences['watchlisthidepatrolled'] = [
1037
				'type' => 'toggle',
1038
				'section' => 'watchlist/advancedwatchlist',
1039
				'label-message' => 'tog-watchlisthidepatrolled',
1040
			];
1041
		}
1042
1043
		$watchTypes = [
1044
			'edit' => 'watchdefault',
1045
			'move' => 'watchmoves',
1046
			'delete' => 'watchdeletion'
1047
		];
1048
1049
		// Kinda hacky
1050
		if ( $user->isAllowed( 'createpage' ) || $user->isAllowed( 'createtalk' ) ) {
1051
			$watchTypes['read'] = 'watchcreations';
1052
		}
1053
1054
		if ( $user->isAllowed( 'rollback' ) ) {
1055
			$watchTypes['rollback'] = 'watchrollback';
1056
		}
1057
1058
		if ( $user->isAllowed( 'upload' ) ) {
1059
			$watchTypes['upload'] = 'watchuploads';
1060
		}
1061
1062
		foreach ( $watchTypes as $action => $pref ) {
1063
			if ( $user->isAllowed( $action ) ) {
1064
				// Messages:
1065
				// tog-watchdefault, tog-watchmoves, tog-watchdeletion, tog-watchcreations, tog-watchuploads
1066
				// tog-watchrollback
1067
				$defaultPreferences[$pref] = [
1068
					'type' => 'toggle',
1069
					'section' => 'watchlist/advancedwatchlist',
1070
					'label-message' => "tog-$pref",
1071
				];
1072
			}
1073
		}
1074
1075
		if ( $config->get( 'EnableAPI' ) ) {
1076
			$defaultPreferences['watchlisttoken'] = [
1077
				'type' => 'api',
1078
			];
1079
			$defaultPreferences['watchlisttoken-info'] = [
1080
				'type' => 'info',
1081
				'section' => 'watchlist/tokenwatchlist',
1082
				'label-message' => 'prefs-watchlist-token',
1083
				'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...
1084
				'help-message' => 'prefs-help-watchlist-token2',
1085
			];
1086
		}
1087
	}
1088
1089
	/**
1090
	 * @param User $user
1091
	 * @param IContextSource $context
1092
	 * @param array $defaultPreferences
1093
	 */
1094
	static function searchPreferences( $user, IContextSource $context, &$defaultPreferences ) {
1095
		foreach ( MWNamespace::getValidNamespaces() as $n ) {
1096
			$defaultPreferences['searchNs' . $n] = [
1097
				'type' => 'api',
1098
			];
1099
		}
1100
	}
1101
1102
	/**
1103
	 * Dummy, kept for backwards-compatibility.
1104
	 */
1105
	static function miscPreferences( $user, IContextSource $context, &$defaultPreferences ) {
1106
	}
1107
1108
	/**
1109
	 * @param User $user The User object
1110
	 * @param IContextSource $context
1111
	 * @return array Text/links to display as key; $skinkey as value
1112
	 */
1113
	static function generateSkinOptions( $user, IContextSource $context ) {
1114
		$ret = [];
1115
1116
		$mptitle = Title::newMainPage();
1117
		$previewtext = $context->msg( 'skin-preview' )->escaped();
1118
1119
		# Only show skins that aren't disabled in $wgSkipSkins
1120
		$validSkinNames = Skin::getAllowedSkins();
1121
1122
		# Sort by UI skin name. First though need to update validSkinNames as sometimes
1123
		# the skinkey & UI skinname differ (e.g. "standard" skinkey is "Classic" in the UI).
1124
		foreach ( $validSkinNames as $skinkey => &$skinname ) {
1125
			$msg = $context->msg( "skinname-{$skinkey}" );
1126
			if ( $msg->exists() ) {
1127
				$skinname = htmlspecialchars( $msg->text() );
1128
			}
1129
		}
1130
		asort( $validSkinNames );
1131
1132
		$config = $context->getConfig();
1133
		$defaultSkin = $config->get( 'DefaultSkin' );
1134
		$allowUserCss = $config->get( 'AllowUserCss' );
1135
		$allowUserJs = $config->get( 'AllowUserJs' );
1136
1137
		$foundDefault = false;
1138
		foreach ( $validSkinNames as $skinkey => $sn ) {
1139
			$linkTools = [];
1140
1141
			# Mark the default skin
1142
			if ( strcasecmp( $skinkey, $defaultSkin ) === 0 ) {
1143
				$linkTools[] = $context->msg( 'default' )->escaped();
1144
				$foundDefault = true;
1145
			}
1146
1147
			# Create preview link
1148
			$mplink = htmlspecialchars( $mptitle->getLocalURL( [ 'useskin' => $skinkey ] ) );
1149
			$linkTools[] = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
1150
1151
			# Create links to user CSS/JS pages
1152 View Code Duplication
			if ( $allowUserCss ) {
1153
				$cssPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.css' );
1154
				$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 1153 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...
1155
			}
1156
1157 View Code Duplication
			if ( $allowUserJs ) {
1158
				$jsPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.js' );
1159
				$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 1158 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...
1160
			}
1161
1162
			$display = $sn . ' ' . $context->msg( 'parentheses' )
1163
				->rawParams( $context->getLanguage()->pipeList( $linkTools ) )
1164
				->escaped();
1165
			$ret[$display] = $skinkey;
1166
		}
1167
1168
		if ( !$foundDefault ) {
1169
			// If the default skin is not available, things are going to break horribly because the
1170
			// default value for skin selector will not be a valid value. Let's just not show it then.
1171
			return [];
1172
		}
1173
1174
		return $ret;
1175
	}
1176
1177
	/**
1178
	 * @param IContextSource $context
1179
	 * @return array
1180
	 */
1181
	static function getDateOptions( IContextSource $context ) {
1182
		$lang = $context->getLanguage();
1183
		$dateopts = $lang->getDatePreferences();
1184
1185
		$ret = [];
1186
1187
		if ( $dateopts ) {
1188
			if ( !in_array( 'default', $dateopts ) ) {
1189
				$dateopts[] = 'default'; // Make sure default is always valid
1190
										// Bug 19237
1191
			}
1192
1193
			// FIXME KLUGE: site default might not be valid for user language
1194
			global $wgDefaultUserOptions;
1195
			if ( !in_array( $wgDefaultUserOptions['date'], $dateopts ) ) {
1196
				$wgDefaultUserOptions['date'] = 'default';
1197
			}
1198
1199
			$epoch = wfTimestampNow();
1200
			foreach ( $dateopts as $key ) {
1201
				if ( $key == 'default' ) {
1202
					$formatted = $context->msg( 'datedefault' )->escaped();
1203
				} else {
1204
					$formatted = htmlspecialchars( $lang->timeanddate( $epoch, false, $key ) );
0 ignored issues
show
Security Bug introduced by
It seems like $epoch defined by wfTimestampNow() on line 1199 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...
1205
				}
1206
				$ret[$formatted] = $key;
1207
			}
1208
		}
1209
		return $ret;
1210
	}
1211
1212
	/**
1213
	 * @param IContextSource $context
1214
	 * @return array
1215
	 */
1216
	static function getImageSizes( IContextSource $context ) {
1217
		$ret = [];
1218
		$pixels = $context->msg( 'unit-pixel' )->text();
1219
1220
		foreach ( $context->getConfig()->get( 'ImageLimits' ) as $index => $limits ) {
1221
			$display = "{$limits[0]}×{$limits[1]}" . $pixels;
1222
			$ret[$display] = $index;
1223
		}
1224
1225
		return $ret;
1226
	}
1227
1228
	/**
1229
	 * @param IContextSource $context
1230
	 * @return array
1231
	 */
1232
	static function getThumbSizes( IContextSource $context ) {
1233
		$ret = [];
1234
		$pixels = $context->msg( 'unit-pixel' )->text();
1235
1236
		foreach ( $context->getConfig()->get( 'ThumbLimits' ) as $index => $size ) {
1237
			$display = $size . $pixels;
1238
			$ret[$display] = $index;
1239
		}
1240
1241
		return $ret;
1242
	}
1243
1244
	/**
1245
	 * @param string $signature
1246
	 * @param array $alldata
1247
	 * @param HTMLForm $form
1248
	 * @return bool|string
1249
	 */
1250
	static function validateSignature( $signature, $alldata, $form ) {
1251
		global $wgParser;
1252
		$maxSigChars = $form->getConfig()->get( 'MaxSigChars' );
1253
		if ( mb_strlen( $signature ) > $maxSigChars ) {
1254
			return Xml::element( 'span', [ 'class' => 'error' ],
1255
				$form->msg( 'badsiglength' )->numParams( $maxSigChars )->text() );
1256
		} elseif ( isset( $alldata['fancysig'] ) &&
1257
				$alldata['fancysig'] &&
1258
				$wgParser->validateSig( $signature ) === false
1259
		) {
1260
			return Xml::element(
1261
				'span',
1262
				[ 'class' => 'error' ],
1263
				$form->msg( 'badsig' )->text()
1264
			);
1265
		} else {
1266
			return true;
1267
		}
1268
	}
1269
1270
	/**
1271
	 * @param string $signature
1272
	 * @param array $alldata
1273
	 * @param HTMLForm $form
1274
	 * @return string
1275
	 */
1276
	static function cleanSignature( $signature, $alldata, $form ) {
1277
		if ( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) {
1278
			global $wgParser;
1279
			$signature = $wgParser->cleanSig( $signature );
1280
		} else {
1281
			// When no fancy sig used, make sure ~{3,5} get removed.
1282
			$signature = Parser::cleanSigInSig( $signature );
1283
		}
1284
1285
		return $signature;
1286
	}
1287
1288
	/**
1289
	 * @param User $user
1290
	 * @param IContextSource $context
1291
	 * @param string $formClass
1292
	 * @param array $remove Array of items to remove
1293
	 * @return PreferencesForm|HtmlForm
1294
	 */
1295
	static function getFormObject(
1296
		$user,
1297
		IContextSource $context,
1298
		$formClass = 'PreferencesForm',
1299
		array $remove = []
1300
	) {
1301
		$formDescriptor = Preferences::getPreferences( $user, $context );
1302
		if ( count( $remove ) ) {
1303
			$removeKeys = array_flip( $remove );
1304
			$formDescriptor = array_diff_key( $formDescriptor, $removeKeys );
1305
		}
1306
1307
		// Remove type=api preferences. They are not intended for rendering in the form.
1308
		foreach ( $formDescriptor as $name => $info ) {
1309
			if ( isset( $info['type'] ) && $info['type'] === 'api' ) {
1310
				unset( $formDescriptor[$name] );
1311
			}
1312
		}
1313
1314
		/**
1315
		 * @var $htmlForm PreferencesForm
1316
		 */
1317
		$htmlForm = new $formClass( $formDescriptor, $context, 'prefs' );
1318
1319
		$htmlForm->setModifiedUser( $user );
1320
		$htmlForm->setId( 'mw-prefs-form' );
1321
		$htmlForm->setAutocomplete( 'off' );
1322
		$htmlForm->setSubmitText( $context->msg( 'saveprefs' )->text() );
1323
		# Used message keys: 'accesskey-preferences-save', 'tooltip-preferences-save'
1324
		$htmlForm->setSubmitTooltip( 'preferences-save' );
1325
		$htmlForm->setSubmitID( 'prefsubmit' );
1326
		$htmlForm->setSubmitCallback( [ 'Preferences', 'tryFormSubmit' ] );
1327
1328
		return $htmlForm;
1329
	}
1330
1331
	/**
1332
	 * @param IContextSource $context
1333
	 * @return array
1334
	 */
1335
	static function getTimezoneOptions( IContextSource $context ) {
1336
		$opt = [];
1337
1338
		$localTZoffset = $context->getConfig()->get( 'LocalTZoffset' );
1339
		$timeZoneList = self::getTimeZoneList( $context->getLanguage() );
1340
1341
		$timestamp = MWTimestamp::getLocalInstance();
1342
		// Check that the LocalTZoffset is the same as the local time zone offset
1343
		if ( $localTZoffset == $timestamp->format( 'Z' ) / 60 ) {
1344
			$timezoneName = $timestamp->getTimezone()->getName();
1345
			// Localize timezone
1346
			if ( isset( $timeZoneList[$timezoneName] ) ) {
1347
				$timezoneName = $timeZoneList[$timezoneName]['name'];
1348
			}
1349
			$server_tz_msg = $context->msg(
1350
				'timezoneuseserverdefault',
1351
				$timezoneName
1352
			)->text();
1353
		} else {
1354
			$tzstring = sprintf(
1355
				'%+03d:%02d',
1356
				floor( $localTZoffset / 60 ),
1357
				abs( $localTZoffset ) % 60
1358
			);
1359
			$server_tz_msg = $context->msg( 'timezoneuseserverdefault', $tzstring )->text();
1360
		}
1361
		$opt[$server_tz_msg] = "System|$localTZoffset";
1362
		$opt[$context->msg( 'timezoneuseoffset' )->text()] = 'other';
1363
		$opt[$context->msg( 'guesstimezone' )->text()] = 'guess';
1364
1365
		foreach ( $timeZoneList as $timeZoneInfo ) {
1366
			$region = $timeZoneInfo['region'];
1367
			if ( !isset( $opt[$region] ) ) {
1368
				$opt[$region] = [];
1369
			}
1370
			$opt[$region][$timeZoneInfo['name']] = $timeZoneInfo['timecorrection'];
1371
		}
1372
		return $opt;
1373
	}
1374
1375
	/**
1376
	 * @param string $value
1377
	 * @param array $alldata
1378
	 * @return int
1379
	 */
1380
	static function filterIntval( $value, $alldata ) {
1381
		return intval( $value );
1382
	}
1383
1384
	/**
1385
	 * @param string $tz
1386
	 * @param array $alldata
1387
	 * @return string
1388
	 */
1389
	static function filterTimezoneInput( $tz, $alldata ) {
1390
		$data = explode( '|', $tz, 3 );
1391
		switch ( $data[0] ) {
1392
			case 'ZoneInfo':
1393
			case 'System':
1394
				return $tz;
1395
			default:
1396
				$data = explode( ':', $tz, 2 );
1397 View Code Duplication
				if ( count( $data ) == 2 ) {
1398
					$data[0] = intval( $data[0] );
1399
					$data[1] = intval( $data[1] );
1400
					$minDiff = abs( $data[0] ) * 60 + $data[1];
1401
					if ( $data[0] < 0 ) {
1402
						$minDiff = - $minDiff;
1403
					}
1404
				} else {
1405
					$minDiff = intval( $data[0] ) * 60;
1406
				}
1407
1408
				# Max is +14:00 and min is -12:00, see:
1409
				# https://en.wikipedia.org/wiki/Timezone
1410
				$minDiff = min( $minDiff, 840 );  # 14:00
1411
				$minDiff = max( $minDiff, - 720 ); # -12:00
1412
				return 'Offset|' . $minDiff;
1413
		}
1414
	}
1415
1416
	/**
1417
	 * Handle the form submission if everything validated properly
1418
	 *
1419
	 * @param array $formData
1420
	 * @param PreferencesForm $form
1421
	 * @return bool|Status|string
1422
	 */
1423
	static function tryFormSubmit( $formData, $form ) {
1424
		$user = $form->getModifiedUser();
1425
		$hiddenPrefs = $form->getConfig()->get( 'HiddenPrefs' );
1426
		$result = true;
1427
1428
		if ( !$user->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
1429
			return Status::newFatal( 'mypreferencesprotected' );
1430
		}
1431
1432
		// Filter input
1433
		foreach ( array_keys( $formData ) as $name ) {
1434
			if ( isset( self::$saveFilters[$name] ) ) {
1435
				$formData[$name] =
1436
					call_user_func( self::$saveFilters[$name], $formData[$name], $formData );
1437
			}
1438
		}
1439
1440
		// Fortunately, the realname field is MUCH simpler
1441
		// (not really "private", but still shouldn't be edited without permission)
1442
1443
		if ( !in_array( 'realname', $hiddenPrefs )
1444
			&& $user->isAllowed( 'editmyprivateinfo' )
1445
			&& array_key_exists( 'realname', $formData )
1446
		) {
1447
			$realName = $formData['realname'];
1448
			$user->setRealName( $realName );
1449
		}
1450
1451
		if ( $user->isAllowed( 'editmyoptions' ) ) {
1452
			foreach ( self::$saveBlacklist as $b ) {
1453
				unset( $formData[$b] );
1454
			}
1455
1456
			# If users have saved a value for a preference which has subsequently been disabled
1457
			# via $wgHiddenPrefs, we don't want to destroy that setting in case the preference
1458
			# is subsequently re-enabled
1459
			foreach ( $hiddenPrefs as $pref ) {
1460
				# If the user has not set a non-default value here, the default will be returned
1461
				# and subsequently discarded
1462
				$formData[$pref] = $user->getOption( $pref, null, true );
1463
			}
1464
1465
			// Keep old preferences from interfering due to back-compat code, etc.
1466
			$user->resetOptions( 'unused', $form->getContext() );
1467
1468
			foreach ( $formData as $key => $value ) {
1469
				$user->setOption( $key, $value );
1470
			}
1471
1472
			Hooks::run( 'PreferencesFormPreSave', [ $formData, $form, $user, &$result ] );
1473
		}
1474
1475
		MediaWiki\Auth\AuthManager::callLegacyAuthPlugin( 'updateExternalDB', [ $user ] );
0 ignored issues
show
Deprecated Code introduced by
The method MediaWiki\Auth\AuthManager::callLegacyAuthPlugin() has been deprecated with message: For backwards compatibility only, should be avoided in new code

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...
1476
		$user->saveSettings();
1477
1478
		return $result;
1479
	}
1480
1481
	/**
1482
	 * @param array $formData
1483
	 * @param PreferencesForm $form
1484
	 * @return Status
1485
	 */
1486
	public static function tryUISubmit( $formData, $form ) {
1487
		$res = self::tryFormSubmit( $formData, $form );
1488
1489
		if ( $res ) {
1490
			$urlOptions = [];
1491
1492
			if ( $res === 'eauth' ) {
1493
				$urlOptions['eauth'] = 1;
1494
			}
1495
1496
			$urlOptions += $form->getExtraSuccessRedirectParameters();
1497
1498
			$url = $form->getTitle()->getFullURL( $urlOptions );
1499
1500
			$context = $form->getContext();
1501
			// Set session data for the success message
1502
			$context->getRequest()->setSessionData( 'specialPreferencesSaveSuccess', 1 );
1503
1504
			$context->getOutput()->redirect( $url );
1505
		}
1506
1507
		return Status::newGood();
1508
	}
1509
1510
	/**
1511
	 * Get a list of all time zones
1512
	 * @param Language $language Language used for the localized names
1513
	 * @return array A list of all time zones. The system name of the time zone is used as key and
1514
	 *  the value is an array which contains localized name, the timecorrection value used for
1515
	 *  preferences and the region
1516
	 * @since 1.26
1517
	 */
1518
	public static function getTimeZoneList( Language $language ) {
1519
		$identifiers = DateTimeZone::listIdentifiers();
1520
		if ( $identifiers === false ) {
1521
			return [];
1522
		}
1523
		sort( $identifiers );
1524
1525
		$tzRegions = [
1526
			'Africa' => wfMessage( 'timezoneregion-africa' )->inLanguage( $language )->text(),
1527
			'America' => wfMessage( 'timezoneregion-america' )->inLanguage( $language )->text(),
1528
			'Antarctica' => wfMessage( 'timezoneregion-antarctica' )->inLanguage( $language )->text(),
1529
			'Arctic' => wfMessage( 'timezoneregion-arctic' )->inLanguage( $language )->text(),
1530
			'Asia' => wfMessage( 'timezoneregion-asia' )->inLanguage( $language )->text(),
1531
			'Atlantic' => wfMessage( 'timezoneregion-atlantic' )->inLanguage( $language )->text(),
1532
			'Australia' => wfMessage( 'timezoneregion-australia' )->inLanguage( $language )->text(),
1533
			'Europe' => wfMessage( 'timezoneregion-europe' )->inLanguage( $language )->text(),
1534
			'Indian' => wfMessage( 'timezoneregion-indian' )->inLanguage( $language )->text(),
1535
			'Pacific' => wfMessage( 'timezoneregion-pacific' )->inLanguage( $language )->text(),
1536
		];
1537
		asort( $tzRegions );
1538
1539
		$timeZoneList = [];
1540
1541
		$now = new DateTime();
1542
1543
		foreach ( $identifiers as $identifier ) {
1544
			$parts = explode( '/', $identifier, 2 );
1545
1546
			// DateTimeZone::listIdentifiers() returns a number of
1547
			// backwards-compatibility entries. This filters them out of the
1548
			// list presented to the user.
1549
			if ( count( $parts ) !== 2 || !array_key_exists( $parts[0], $tzRegions ) ) {
1550
				continue;
1551
			}
1552
1553
			// Localize region
1554
			$parts[0] = $tzRegions[$parts[0]];
1555
1556
			$dateTimeZone = new DateTimeZone( $identifier );
1557
			$minDiff = floor( $dateTimeZone->getOffset( $now ) / 60 );
1558
1559
			$display = str_replace( '_', ' ', $parts[0] . '/' . $parts[1] );
1560
			$value = "ZoneInfo|$minDiff|$identifier";
1561
1562
			$timeZoneList[$identifier] = [
1563
				'name' => $display,
1564
				'timecorrection' => $value,
1565
				'region' => $parts[0],
1566
			];
1567
		}
1568
1569
		return $timeZoneList;
1570
	}
1571
}
1572
1573
/** Some tweaks to allow js prefs to work */
1574
class PreferencesForm extends HTMLForm {
1575
	// Override default value from HTMLForm
1576
	protected $mSubSectionBeforeFields = false;
1577
1578
	private $modifiedUser;
1579
1580
	/**
1581
	 * @param User $user
1582
	 */
1583
	public function setModifiedUser( $user ) {
1584
		$this->modifiedUser = $user;
1585
	}
1586
1587
	/**
1588
	 * @return User
1589
	 */
1590
	public function getModifiedUser() {
1591
		if ( $this->modifiedUser === null ) {
1592
			return $this->getUser();
1593
		} else {
1594
			return $this->modifiedUser;
1595
		}
1596
	}
1597
1598
	/**
1599
	 * Get extra parameters for the query string when redirecting after
1600
	 * successful save.
1601
	 *
1602
	 * @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...
1603
	 */
1604
	public function getExtraSuccessRedirectParameters() {
1605
		return [];
1606
	}
1607
1608
	/**
1609
	 * @param string $html
1610
	 * @return string
1611
	 */
1612
	function wrapForm( $html ) {
1613
		$html = Xml::tags( 'div', [ 'id' => 'preferences' ], $html );
1614
1615
		return parent::wrapForm( $html );
1616
	}
1617
1618
	/**
1619
	 * @return string
1620
	 */
1621
	function getButtons() {
1622
1623
		$attrs = [ 'id' => 'mw-prefs-restoreprefs' ];
1624
1625
		if ( !$this->getModifiedUser()->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
1626
			return '';
1627
		}
1628
1629
		$html = parent::getButtons();
1630
1631
		if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) {
1632
			$t = SpecialPage::getTitleFor( 'Preferences', 'reset' );
1633
1634
			$html .= "\n" . Linker::link( $t, $this->msg( 'restoreprefs' )->escaped(),
1635
				Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ) );
1636
1637
			$html = Xml::tags( 'div', [ 'class' => 'mw-prefs-buttons' ], $html );
1638
		}
1639
1640
		return $html;
1641
	}
1642
1643
	/**
1644
	 * Separate multi-option preferences into multiple preferences, since we
1645
	 * have to store them separately
1646
	 * @param array $data
1647
	 * @return array
1648
	 */
1649
	function filterDataForSubmit( $data ) {
1650
		foreach ( $this->mFlatFields as $fieldname => $field ) {
1651
			if ( $field instanceof HTMLNestedFilterable ) {
1652
				$info = $field->mParams;
1653
				$prefix = isset( $info['prefix'] ) ? $info['prefix'] : $fieldname;
1654
				foreach ( $field->filterDataForSubmit( $data[$fieldname] ) as $key => $value ) {
1655
					$data["$prefix$key"] = $value;
1656
				}
1657
				unset( $data[$fieldname] );
1658
			}
1659
		}
1660
1661
		return $data;
1662
	}
1663
1664
	/**
1665
	 * Get the whole body of the form.
1666
	 * @return string
1667
	 */
1668
	function getBody() {
1669
		return $this->displaySection( $this->mFieldTree, '', 'mw-prefsection-' );
1670
	}
1671
1672
	/**
1673
	 * Get the "<legend>" for a given section key. Normally this is the
1674
	 * prefs-$key message but we'll allow extensions to override it.
1675
	 * @param string $key
1676
	 * @return string
1677
	 */
1678
	function getLegend( $key ) {
1679
		$legend = parent::getLegend( $key );
1680
		Hooks::run( 'PreferencesGetLegend', [ $this, $key, &$legend ] );
1681
		return $legend;
1682
	}
1683
1684
	/**
1685
	 * Get the keys of each top level preference section.
1686
	 * @return array of section keys
1687
	 */
1688
	function getPreferenceSections() {
1689
		return array_keys( array_filter( $this->mFieldTree, 'is_array' ) );
1690
	}
1691
}
1692