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