MediaWiki master
DefaultPreferencesFactory.php
Go to the documentation of this file.
1<?php
22
25use Language;
26use LanguageCode;
63use OOUI\ButtonWidget;
64use OOUI\FieldLayout;
65use OOUI\HtmlSnippet;
66use OOUI\LabelWidget;
70use Psr\Log\LoggerAwareTrait;
71use Psr\Log\NullLogger;
72use SkinFactory;
73use UnexpectedValueException;
74
79 use LoggerAwareTrait;
80
82 protected $options;
83
85 protected $contLang;
86
89
91 protected $authManager;
92
94 protected $linkRenderer;
95
97 protected $nsInfo;
98
101
103 private $languageConverter;
104
106 private $hookRunner;
107
110
112 private $languageConverterFactory;
113
115 private $parserFactory;
116
118 private $skinFactory;
119
121 private $userGroupManager;
122
124 private $signatureValidatorFactory;
125
129 public const CONSTRUCTOR_OPTIONS = [
161 ];
162
180 public function __construct(
187 ILanguageConverter $languageConverter,
189 HookContainer $hookContainer,
190 UserOptionsLookup $userOptionsLookup,
191 LanguageConverterFactory $languageConverterFactory = null,
192 ParserFactory $parserFactory = null,
193 SkinFactory $skinFactory = null,
194 UserGroupManager $userGroupManager = null,
195 SignatureValidatorFactory $signatureValidatorFactory = null
196 ) {
197 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
198
199 $this->options = $options;
200 $this->contLang = $contLang;
201 $this->authManager = $authManager;
202 $this->linkRenderer = $linkRenderer;
203 $this->nsInfo = $nsInfo;
204
205 // We don't use the PermissionManager anymore, but we need to be careful
206 // removing the parameter since this class is extended by GlobalPreferencesFactory
207 // in the GlobalPreferences extension, and that class uses it
208 $this->permissionManager = $permissionManager;
209
210 $this->logger = new NullLogger();
211 $this->languageConverter = $languageConverter;
212 $this->languageNameUtils = $languageNameUtils;
213 $this->hookRunner = new HookRunner( $hookContainer );
214
215 // Don't break GlobalPreferences, fall back to global state if missing services
216 // or if passed a UserOptionsLookup that isn't UserOptionsManager
217 $services = static function () {
218 // BC hack. Use a closure so this can be unit-tested.
220 };
221 $this->userOptionsManager = ( $userOptionsLookup instanceof UserOptionsManager )
222 ? $userOptionsLookup
223 : $services()->getUserOptionsManager();
224 $this->languageConverterFactory = $languageConverterFactory ?? $services()->getLanguageConverterFactory();
225
226 $this->parserFactory = $parserFactory ?? $services()->getParserFactory();
227 $this->skinFactory = $skinFactory ?? $services()->getSkinFactory();
228 $this->userGroupManager = $userGroupManager ?? $services()->getUserGroupManager();
229 $this->signatureValidatorFactory = $signatureValidatorFactory
230 ?? $services()->getSignatureValidatorFactory();
231 }
232
236 public function getSaveBlacklist() {
237 return [
238 'realname',
239 'emailaddress',
240 ];
241 }
242
248 public function getFormDescriptor( User $user, IContextSource $context ) {
249 $preferences = [];
250
251 OutputPage::setupOOUI(
252 strtolower( $context->getSkin()->getSkinName() ),
253 $context->getLanguage()->getDir()
254 );
255
256 $this->profilePreferences( $user, $context, $preferences );
257 $this->skinPreferences( $user, $context, $preferences );
258 $this->datetimePreferences( $user, $context, $preferences );
259 $this->filesPreferences( $context, $preferences );
260 $this->renderingPreferences( $user, $context, $preferences );
261 $this->editingPreferences( $user, $context, $preferences );
262 $this->rcPreferences( $user, $context, $preferences );
263 $this->watchlistPreferences( $user, $context, $preferences );
264 $this->searchPreferences( $context, $preferences );
265
266 $this->hookRunner->onGetPreferences( $user, $preferences );
267
268 $this->loadPreferenceValues( $user, $context, $preferences );
269 $this->logger->debug( "Created form descriptor for user '{$user->getName()}'" );
270 return $preferences;
271 }
272
279 public static function simplifyFormDescriptor( array $descriptor ) {
280 foreach ( $descriptor as $name => &$params ) {
281 // Info fields are useless and can use complicated closure to provide
282 // text, skip all of them.
283 if ( ( isset( $params['type'] ) && $params['type'] === 'info' ) ||
284 // Checking old alias for compatibility with unchanged extensions
285 ( isset( $params['class'] ) && $params['class'] === \HTMLInfoField::class ) ||
286 ( isset( $params['class'] ) && $params['class'] === HTMLInfoField::class )
287 ) {
288 unset( $descriptor[$name] );
289 continue;
290 }
291 // Message parsing is the heaviest load when constructing the field,
292 // but we just want to validate data.
293 foreach ( $params as $key => $value ) {
294 switch ( $key ) {
295 // Special case, should be kept.
296 case 'options-message':
297 break;
298 // Special case, should be transferred.
299 case 'options-messages':
300 unset( $params[$key] );
301 $params['options'] = $value;
302 break;
303 default:
304 if ( preg_match( '/-messages?$/', $key ) ) {
305 // Unwanted.
306 unset( $params[$key] );
307 }
308 }
309 }
310 }
311 return $descriptor;
312 }
313
321 private function loadPreferenceValues( User $user, IContextSource $context, &$defaultPreferences ) {
322 // Remove preferences that wikis don't want to use
323 foreach ( $this->options->get( MainConfigNames::HiddenPrefs ) as $pref ) {
324 unset( $defaultPreferences[$pref] );
325 }
326
327 // For validation.
328 $simplified = self::simplifyFormDescriptor( $defaultPreferences );
329 $form = new HTMLForm( $simplified, $context );
330
331 $disable = !$user->isAllowed( 'editmyoptions' );
332
333 $defaultOptions = $this->userOptionsManager->getDefaultOptions( $user );
334 $userOptions = $this->userOptionsManager->getOptions( $user );
335 $this->applyFilters( $userOptions, $defaultPreferences, 'filterForForm' );
336 // Add in defaults from the user
337 foreach ( $simplified as $name => $_ ) {
338 $info = &$defaultPreferences[$name];
339 if ( $disable && !in_array( $name, $this->getSaveBlacklist() ) ) {
340 $info['disabled'] = 'disabled';
341 }
342 if ( isset( $info['default'] ) ) {
343 // Already set, no problem
344 continue;
345 }
346 $field = $form->getField( $name );
347 $globalDefault = $defaultOptions[$name] ?? null;
348 $prefFromUser = static::getPreferenceForField( $name, $field, $userOptions );
349
350 // If it validates, set it as the default
351 // FIXME: That's not how the validate() function works! Values of nested fields
352 // (e.g. CheckMatix) would be missing.
353 if ( $prefFromUser !== null && // Make sure we're not just pulling nothing
354 $field->validate( $prefFromUser, $this->userOptionsManager->getOptions( $user ) ) === true ) {
355 $info['default'] = $prefFromUser;
356 } elseif ( $field->validate( $globalDefault, $this->userOptionsManager->getOptions( $user ) ) === true ) {
357 $info['default'] = $globalDefault;
358 } else {
359 $globalDefault = json_encode( $globalDefault );
360 throw new UnexpectedValueException(
361 "Default '$globalDefault' is invalid for preference $name of user " . $user->getName()
362 );
363 }
364 }
365
366 return $defaultPreferences;
367 }
368
379 public static function getPreferenceForField( $name, HTMLFormField $field, array $userOptions ) {
380 $val = $userOptions[$name] ?? null;
381
382 if ( $field instanceof HTMLNestedFilterable ) {
383 $val = [];
384 $prefix = $field->mParams['prefix'] ?? $name;
385 // Fetch all possible preference keys of the given field on this wiki.
386 $keys = array_keys( $field->filterDataForSubmit( [] ) );
387 foreach ( $keys as $key ) {
388 if ( $userOptions[$prefix . $key] ?? false ) {
389 $val[] = $key;
390 }
391 }
392 }
393
394 return $val;
395 }
396
406 protected function getOptionFromUser( $name, $info, array $userOptions ) {
407 $val = $userOptions[$name] ?? null;
408
409 // Handling for multiselect preferences
410 if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
411 // Checking old alias for compatibility with unchanged extensions
412 ( isset( $info['class'] ) && $info['class'] === \HTMLMultiSelectField::class ) ||
413 ( isset( $info['class'] ) && $info['class'] === HTMLMultiSelectField::class )
414 ) {
415 $options = HTMLFormField::flattenOptions( $info['options-messages'] ?? $info['options'] );
416 $prefix = $info['prefix'] ?? $name;
417 $val = [];
418
419 foreach ( $options as $value ) {
420 if ( $userOptions["$prefix$value"] ?? false ) {
421 $val[] = $value;
422 }
423 }
424 }
425
426 // Handling for checkmatrix preferences
427 if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
428 // Checking old alias for compatibility with unchanged extensions
429 ( isset( $info['class'] ) && $info['class'] === \HTMLCheckMatrix::class ) ||
430 ( isset( $info['class'] ) && $info['class'] === HTMLCheckMatrix::class )
431 ) {
432 $columns = HTMLFormField::flattenOptions( $info['columns'] );
433 $rows = HTMLFormField::flattenOptions( $info['rows'] );
434 $prefix = $info['prefix'] ?? $name;
435 $val = [];
436
437 foreach ( $columns as $column ) {
438 foreach ( $rows as $row ) {
439 if ( $userOptions["$prefix$column-$row"] ?? false ) {
440 $val[] = "$column-$row";
441 }
442 }
443 }
444 }
445
446 return $val;
447 }
448
456 protected function profilePreferences(
457 User $user, IContextSource $context, &$defaultPreferences
458 ) {
459 // retrieving user name for GENDER and misc.
460 $userName = $user->getName();
461
462 // Information panel
463 $defaultPreferences['username'] = [
464 'type' => 'info',
465 'label-message' => [ 'username', $userName ],
466 'default' => $userName,
467 'section' => 'personal/info',
468 ];
469
470 $lang = $context->getLanguage();
471
472 // Get groups to which the user belongs, Skip the default * group, seems useless here
473 $userEffectiveGroups = array_diff(
474 $this->userGroupManager->getUserEffectiveGroups( $user ),
475 [ '*' ]
476 );
477 $defaultPreferences['usergroups'] = [
478 'type' => 'info',
479 'label-message' => [ 'prefs-memberingroups',
480 Message::numParam( count( $userEffectiveGroups ) ), $userName ],
481 'default' => function () use ( $user, $userEffectiveGroups, $context, $lang, $userName ) {
482 $userGroupMemberships = $this->userGroupManager->getUserGroupMemberships( $user );
483 $userGroups = $userMembers = $userTempGroups = $userTempMembers = [];
484 foreach ( $userEffectiveGroups as $ueg ) {
485 $groupStringOrObject = $userGroupMemberships[$ueg] ?? $ueg;
486
487 $userG = UserGroupMembership::getLinkHTML( $groupStringOrObject, $context );
488 $userM = UserGroupMembership::getLinkHTML( $groupStringOrObject, $context, $userName );
489
490 // Store expiring groups separately, so we can place them before non-expiring
491 // groups in the list. This is to avoid the ambiguity of something like
492 // "administrator, bureaucrat (until X date)" -- users might wonder whether the
493 // expiry date applies to both groups, or just the last one
494 if ( $groupStringOrObject instanceof UserGroupMembership &&
495 $groupStringOrObject->getExpiry()
496 ) {
497 $userTempGroups[] = $userG;
498 $userTempMembers[] = $userM;
499 } else {
500 $userGroups[] = $userG;
501 $userMembers[] = $userM;
502 }
503 }
504 sort( $userGroups );
505 sort( $userMembers );
506 sort( $userTempGroups );
507 sort( $userTempMembers );
508 $userGroups = array_merge( $userTempGroups, $userGroups );
509 $userMembers = array_merge( $userTempMembers, $userMembers );
510 return $context->msg( 'prefs-memberingroups-type' )
511 ->rawParams( $lang->commaList( $userGroups ), $lang->commaList( $userMembers ) )
512 ->escaped();
513 },
514 'raw' => true,
515 'section' => 'personal/info',
516 ];
517
518 $contribTitle = SpecialPage::getTitleFor( "Contributions", $userName );
519 $formattedEditCount = $lang->formatNum( $user->getEditCount() );
520 $editCount = $this->linkRenderer->makeLink( $contribTitle, $formattedEditCount );
521
522 $defaultPreferences['editcount'] = [
523 'type' => 'info',
524 'raw' => true,
525 'label-message' => 'prefs-edits',
526 'default' => $editCount,
527 'section' => 'personal/info',
528 ];
529
530 if ( $user->getRegistration() ) {
531 $displayUser = $context->getUser();
532 $userRegistration = $user->getRegistration();
533 $defaultPreferences['registrationdate'] = [
534 'type' => 'info',
535 'label-message' => 'prefs-registration',
536 'default' => $context->msg(
537 'prefs-registration-date-time',
538 $lang->userTimeAndDate( $userRegistration, $displayUser ),
539 $lang->userDate( $userRegistration, $displayUser ),
540 $lang->userTime( $userRegistration, $displayUser )
541 )->text(),
542 'section' => 'personal/info',
543 ];
544 }
545
546 $canViewPrivateInfo = $user->isAllowed( 'viewmyprivateinfo' );
547 $canEditPrivateInfo = $user->isAllowed( 'editmyprivateinfo' );
548
549 // Actually changeable stuff
550 $defaultPreferences['realname'] = [
551 // (not really "private", but still shouldn't be edited without permission)
552 'type' => $canEditPrivateInfo && $this->authManager->allowsPropertyChange( 'realname' )
553 ? 'text' : 'info',
554 'default' => $user->getRealName(),
555 'section' => 'personal/info',
556 'label-message' => 'yourrealname',
557 'help-message' => 'prefs-help-realname',
558 ];
559
560 if ( $canEditPrivateInfo && $this->authManager->allowsAuthenticationDataChange(
561 new PasswordAuthenticationRequest(), false )->isGood()
562 ) {
563 $defaultPreferences['password'] = [
564 'type' => 'info',
565 'raw' => true,
566 'default' => (string)new ButtonWidget( [
567 'href' => SpecialPage::getTitleFor( 'ChangePassword' )->getLinkURL( [
568 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
569 ] ),
570 'label' => $context->msg( 'prefs-resetpass' )->text(),
571 ] ),
572 'label-message' => 'yourpassword',
573 // email password reset feature only works for users that have an email set up
574 'help' => $this->options->get( MainConfigNames::AllowRequiringEmailForResets ) &&
575 $user->getEmail()
576 ? $context->msg( 'prefs-help-yourpassword',
577 '[[#mw-prefsection-personal-email|{{int:prefs-email}}]]' )->parse()
578 : '',
579 'section' => 'personal/info',
580 ];
581 }
582 // Only show prefershttps if secure login is turned on
583 if ( !$this->options->get( MainConfigNames::ForceHTTPS )
584 && $this->options->get( MainConfigNames::SecureLogin )
585 ) {
586 $defaultPreferences['prefershttps'] = [
587 'type' => 'toggle',
588 'label-message' => 'tog-prefershttps',
589 'help-message' => 'prefs-help-prefershttps',
590 'section' => 'personal/info'
591 ];
592 }
593
594 $defaultPreferences['downloaduserdata'] = [
595 'type' => 'info',
596 'raw' => true,
597 'label-message' => 'prefs-user-downloaddata-label',
598 'default' => Html::element(
599 'a',
600 [
601 'href' => $this->options->get( MainConfigNames::ScriptPath ) .
602 '/api.php?action=query&meta=userinfo&uiprop=*&formatversion=2',
603 ],
604 $context->msg( 'prefs-user-downloaddata-info' )->text()
605 ),
606 'help-message' => [ 'prefs-user-downloaddata-help-message', urlencode( $user->getTitleKey() ) ],
607 'section' => 'personal/info',
608 ];
609
610 $defaultPreferences['restoreprefs'] = [
611 'type' => 'info',
612 'raw' => true,
613 'label-message' => 'prefs-user-restoreprefs-label',
614 'default' => Html::element(
615 'a',
616 [
617 'href' => SpecialPage::getTitleFor( 'Preferences' )
618 ->getSubpage( 'reset' )->getLocalURL()
619 ],
620 $context->msg( 'prefs-user-restoreprefs-info' )->text()
621 ),
622 'section' => 'personal/info',
623 ];
624
625 $languages = $this->languageNameUtils->getLanguageNames(
626 LanguageNameUtils::AUTONYMS,
627 LanguageNameUtils::SUPPORTED
628 );
629 $languageCode = $this->options->get( MainConfigNames::LanguageCode );
630 if ( !array_key_exists( $languageCode, $languages ) ) {
631 $languages[$languageCode] = $languageCode;
632 // Sort the array again
633 ksort( $languages );
634 }
635
636 $options = [];
637 foreach ( $languages as $code => $name ) {
638 $display = LanguageCode::bcp47( $code ) . ' - ' . $name;
639 $options[$display] = $code;
640 }
641 $defaultPreferences['language'] = [
642 'type' => 'select',
643 'section' => 'personal/i18n',
644 'options' => $options,
645 'label-message' => 'yourlanguage',
646 ];
647
648 $neutralGenderMessage = $context->msg( 'gender-notknown' )->escaped() . (
649 !$context->msg( 'gender-unknown' )->isDisabled()
650 ? "<br>" . $context->msg( 'parentheses' )
651 ->params( $context->msg( 'gender-unknown' )->plain() )
652 ->escaped()
653 : ''
654 );
655
656 $defaultPreferences['gender'] = [
657 'type' => 'radio',
658 'section' => 'personal/i18n',
659 'options' => [
660 $neutralGenderMessage => 'unknown',
661 $context->msg( 'gender-female' )->escaped() => 'female',
662 $context->msg( 'gender-male' )->escaped() => 'male',
663 ],
664 'label-message' => 'yourgender',
665 'help-message' => 'prefs-help-gender',
666 ];
667
668 // see if there are multiple language variants to choose from
669 if ( !$this->languageConverterFactory->isConversionDisabled() ) {
670
671 foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
672 if ( $langCode == $this->contLang->getCode() ) {
673 if ( !$this->languageConverter->hasVariants() ) {
674 continue;
675 }
676
677 $variants = $this->languageConverter->getVariants();
678 $variantArray = [];
679 foreach ( $variants as $v ) {
680 $v = str_replace( '_', '-', strtolower( $v ) );
681 $variantArray[$v] = $lang->getVariantname( $v, false );
682 }
683
684 $options = [];
685 foreach ( $variantArray as $code => $name ) {
686 $display = LanguageCode::bcp47( $code ) . ' - ' . $name;
687 $options[$display] = $code;
688 }
689
690 $defaultPreferences['variant'] = [
691 'label-message' => 'yourvariant',
692 'type' => 'select',
693 'options' => $options,
694 'section' => 'personal/i18n',
695 'help-message' => 'prefs-help-variant',
696 ];
697 } else {
698 $defaultPreferences["variant-$langCode"] = [
699 'type' => 'api',
700 ];
701 }
702 }
703 }
704
705 // show a preview of the old signature first
706 $oldsigWikiText = $this->parserFactory->getInstance()->preSaveTransform(
707 '~~~',
708 $context->getTitle(),
709 $user,
710 ParserOptions::newFromContext( $context )
711 );
712 $oldsigHTML = Parser::stripOuterParagraph(
713 $context->getOutput()->parseAsContent( $oldsigWikiText )
714 );
715 $signatureFieldConfig = [];
716 // Validate existing signature and show a message about it
717 $signature = $this->userOptionsManager->getOption( $user, 'nickname' );
718 $useFancySig = $this->userOptionsManager->getBoolOption( $user, 'fancysig' );
719 if ( $useFancySig && $signature !== '' ) {
720 $parserOpts = ParserOptions::newFromContext( $context );
721 $validator = $this->signatureValidatorFactory
722 ->newSignatureValidator( $user, $context, $parserOpts );
723 $signatureErrors = $validator->validateSignature( $signature );
724 if ( $signatureErrors ) {
725 $sigValidation = $this->options->get( MainConfigNames::SignatureValidation );
726 $oldsigHTML .= '<p><strong>' .
727 // Messages used here:
728 // * prefs-signature-invalid-warning
729 // * prefs-signature-invalid-new
730 // * prefs-signature-invalid-disallow
731 $context->msg( "prefs-signature-invalid-$sigValidation" )->parse() .
732 '</strong></p>';
733
734 // On initial page load, show the warnings as well
735 // (when posting, you get normal validation errors instead)
736 foreach ( $signatureErrors as &$sigError ) {
737 $sigError = new HtmlSnippet( $sigError );
738 }
739 if ( !$context->getRequest()->wasPosted() ) {
740 $signatureFieldConfig = [
741 'warnings' => $sigValidation !== 'disallow' ? $signatureErrors : null,
742 'errors' => $sigValidation === 'disallow' ? $signatureErrors : null,
743 ];
744 }
745 }
746 }
747 $defaultPreferences['oldsig'] = [
748 'type' => 'info',
749 // Normally HTMLFormFields do not display warnings, so we need to use 'rawrow'
750 // and provide the entire OOUI\FieldLayout here
751 'rawrow' => true,
752 'default' => new FieldLayout(
753 new LabelWidget( [
754 'label' => new HtmlSnippet( $oldsigHTML ),
755 ] ),
756 [
757 'align' => 'top',
758 'label' => new HtmlSnippet( $context->msg( 'tog-oldsig' )->parse() )
759 ] + $signatureFieldConfig
760 ),
761 'section' => 'personal/signature',
762 ];
763 $defaultPreferences['nickname'] = [
764 'type' => $this->authManager->allowsPropertyChange( 'nickname' ) ? 'text' : 'info',
765 'maxlength' => $this->options->get( MainConfigNames::MaxSigChars ),
766 'label-message' => 'yournick',
767 'validation-callback' => function ( $signature, $alldata, HTMLForm $form ) {
768 return $this->validateSignature( $signature, $alldata, $form );
769 },
770 'section' => 'personal/signature',
771 'filter-callback' => function ( $signature, array $alldata, HTMLForm $form ) {
772 return $this->cleanSignature( $signature, $alldata, $form );
773 },
774 ];
775 $defaultPreferences['fancysig'] = [
776 'type' => 'toggle',
777 'label-message' => 'tog-fancysig',
778 // show general help about signature at the bottom of the section
779 'help-message' => 'prefs-help-signature',
780 'section' => 'personal/signature'
781 ];
782
783 // Email preferences
784 if ( $this->options->get( MainConfigNames::EnableEmail ) ) {
785 if ( $canViewPrivateInfo ) {
786 $helpMessages = [];
787 $helpMessages[] = $this->options->get( MainConfigNames::EmailConfirmToEdit )
788 ? 'prefs-help-email-required'
789 : 'prefs-help-email';
790
791 if ( $this->options->get( MainConfigNames::EnableUserEmail ) ) {
792 // additional messages when users can send email to each other
793 $helpMessages[] = 'prefs-help-email-others';
794 }
795
796 $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
797 if ( $canEditPrivateInfo && $this->authManager->allowsPropertyChange( 'emailaddress' ) ) {
798 $button = new ButtonWidget( [
799 'href' => SpecialPage::getTitleFor( 'ChangeEmail' )->getLinkURL( [
800 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
801 ] ),
802 'label' =>
803 $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(),
804 ] );
805
806 $emailAddress .= $emailAddress == '' ? $button : ( '<br />' . $button );
807 }
808
809 $defaultPreferences['emailaddress'] = [
810 'type' => 'info',
811 'raw' => true,
812 'default' => $emailAddress,
813 'label-message' => 'youremail',
814 'section' => 'personal/email',
815 'help-messages' => $helpMessages,
816 // 'cssclass' chosen below
817 ];
818 }
819
820 $disableEmailPrefs = false;
821
822 if ( $this->options->get( MainConfigNames::AllowRequiringEmailForResets ) ) {
823 $defaultPreferences['requireemail'] = [
824 'type' => 'toggle',
825 'label-message' => 'tog-requireemail',
826 'help-message' => 'prefs-help-requireemail',
827 'section' => 'personal/email',
828 'disabled' => !$user->getEmail(),
829 ];
830 }
831
832 if ( $this->options->get( MainConfigNames::EmailAuthentication ) ) {
833 if ( $user->getEmail() ) {
834 if ( $user->getEmailAuthenticationTimestamp() ) {
835 // date and time are separate parameters to facilitate localisation.
836 // $time is kept for backward compat reasons.
837 // 'emailauthenticated' is also used in SpecialConfirmemail.php
838 $displayUser = $context->getUser();
839 $emailTimestamp = $user->getEmailAuthenticationTimestamp();
840 $time = $lang->userTimeAndDate( $emailTimestamp, $displayUser );
841 $d = $lang->userDate( $emailTimestamp, $displayUser );
842 $t = $lang->userTime( $emailTimestamp, $displayUser );
843 $emailauthenticated = $context->msg( 'emailauthenticated',
844 $time, $d, $t )->parse() . '<br />';
845 $emailauthenticationclass = 'mw-email-authenticated';
846 } else {
847 $disableEmailPrefs = true;
848 $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
849 new ButtonWidget( [
850 'href' => SpecialPage::getTitleFor( 'Confirmemail' )->getLinkURL(),
851 'label' => $context->msg( 'emailconfirmlink' )->text(),
852 ] );
853 $emailauthenticationclass = "mw-email-not-authenticated";
854 }
855 } else {
856 $disableEmailPrefs = true;
857 $emailauthenticated = $context->msg( 'noemailprefs' )->escaped();
858 $emailauthenticationclass = 'mw-email-none';
859 }
860
861 if ( $canViewPrivateInfo ) {
862 $defaultPreferences['emailauthentication'] = [
863 'type' => 'info',
864 'raw' => true,
865 'section' => 'personal/email',
866 'label-message' => 'prefs-emailconfirm-label',
867 'default' => $emailauthenticated,
868 // Apply the same CSS class used on the input to the message:
869 'cssclass' => $emailauthenticationclass,
870 ];
871 }
872 }
873
874 if ( $this->options->get( MainConfigNames::EnableUserEmail ) &&
875 $user->isAllowed( 'sendemail' )
876 ) {
877 $defaultPreferences['disablemail'] = [
878 'id' => 'wpAllowEmail',
879 'type' => 'toggle',
880 'invert' => true,
881 'section' => 'personal/email',
882 'label-message' => 'allowemail',
883 'disabled' => $disableEmailPrefs,
884 ];
885
886 $defaultPreferences['email-allow-new-users'] = [
887 'id' => 'wpAllowEmailFromNewUsers',
888 'type' => 'toggle',
889 'section' => 'personal/email',
890 'label-message' => 'email-allow-new-users-label',
891 'disabled' => $disableEmailPrefs,
892 'disable-if' => [ '!==', 'disablemail', '1' ],
893 ];
894
895 $defaultPreferences['ccmeonemails'] = [
896 'type' => 'toggle',
897 'section' => 'personal/email',
898 'label-message' => 'tog-ccmeonemails',
899 'disabled' => $disableEmailPrefs,
900 ];
901
902 if ( $this->options->get( MainConfigNames::EnableUserEmailMuteList ) ) {
903 $defaultPreferences['email-blacklist'] = [
904 'type' => 'usersmultiselect',
905 'label-message' => 'email-mutelist-label',
906 'section' => 'personal/email',
907 'disabled' => $disableEmailPrefs,
908 'filter' => MultiUsernameFilter::class,
909 ];
910 }
911 }
912
913 if ( $this->options->get( MainConfigNames::EnotifWatchlist ) ) {
914 $defaultPreferences['enotifwatchlistpages'] = [
915 'type' => 'toggle',
916 'section' => 'personal/email',
917 'label-message' => 'tog-enotifwatchlistpages',
918 'disabled' => $disableEmailPrefs,
919 ];
920 }
921 if ( $this->options->get( MainConfigNames::EnotifUserTalk ) ) {
922 $defaultPreferences['enotifusertalkpages'] = [
923 'type' => 'toggle',
924 'section' => 'personal/email',
925 'label-message' => 'tog-enotifusertalkpages',
926 'disabled' => $disableEmailPrefs,
927 ];
928 }
929 if ( $this->options->get( MainConfigNames::EnotifUserTalk ) ||
930 $this->options->get( MainConfigNames::EnotifWatchlist ) ) {
931 if ( $this->options->get( MainConfigNames::EnotifMinorEdits ) ) {
932 $defaultPreferences['enotifminoredits'] = [
933 'type' => 'toggle',
934 'section' => 'personal/email',
935 'label-message' => 'tog-enotifminoredits',
936 'disabled' => $disableEmailPrefs,
937 ];
938 }
939
940 if ( $this->options->get( MainConfigNames::EnotifRevealEditorAddress ) ) {
941 $defaultPreferences['enotifrevealaddr'] = [
942 'type' => 'toggle',
943 'section' => 'personal/email',
944 'label-message' => 'tog-enotifrevealaddr',
945 'disabled' => $disableEmailPrefs,
946 ];
947 }
948 }
949 }
950 }
951
958 protected function skinPreferences( User $user, IContextSource $context, &$defaultPreferences ) {
959 // Skin selector, if there is at least one valid skin
960 $validSkinNames = $this->getValidSkinNames( $user, $context );
961 if ( $validSkinNames ) {
962 $defaultPreferences['skin'] = [
963 // @phan-suppress-next-line SecurityCheck-XSS False +ve, label is escaped in generateSkinOptions()
964 'type' => 'radio',
965 'options' => $this->generateSkinOptions( $user, $context, $validSkinNames ),
966 'section' => 'rendering/skin',
967 ];
968 $hideCond = [ 'AND' ];
969 foreach ( $validSkinNames as $skinName => $_ ) {
970 $options = $this->skinFactory->getSkinOptions( $skinName );
971 if ( $options['responsive'] ?? false ) {
972 $hideCond[] = [ '!==', 'skin', $skinName ];
973 }
974 }
975 if ( $hideCond === [ 'AND' ] ) {
976 $hideCond = [];
977 }
978 $defaultPreferences['skin-responsive'] = [
979 'type' => 'check',
980 'label-message' => 'prefs-skin-responsive',
981 'section' => 'rendering/skin/skin-prefs',
982 'help-message' => 'prefs-help-skin-responsive',
983 'hide-if' => $hideCond,
984 ];
985 }
986
987 $allowUserCss = $this->options->get( MainConfigNames::AllowUserCss );
988 $allowUserJs = $this->options->get( MainConfigNames::AllowUserJs );
989 $safeMode = $this->userOptionsManager->getOption( $user, 'forcesafemode' );
990 // Create links to user CSS/JS pages for all skins.
991 // This code is basically copied from generateSkinOptions().
992 // @todo Refactor this and the similar code in generateSkinOptions().
993 if ( $allowUserCss || $allowUserJs ) {
994 if ( $safeMode ) {
995 $defaultPreferences['customcssjs-safemode'] = [
996 'type' => 'info',
997 'raw' => true,
998 'default' => Html::warningBox( $context->msg( 'prefs-custom-cssjs-safemode' )->parse() ),
999 'section' => 'rendering/skin',
1000 ];
1001 } else {
1002 $linkTools = [];
1003 $userName = $user->getName();
1004
1005 if ( $allowUserCss ) {
1006 $cssPage = Title::makeTitleSafe( NS_USER, $userName . '/common.css' );
1007 $cssLinkText = $context->msg( 'prefs-custom-css' )->text();
1008 $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
1009 }
1010
1011 if ( $allowUserJs ) {
1012 $jsPage = Title::makeTitleSafe( NS_USER, $userName . '/common.js' );
1013 $jsLinkText = $context->msg( 'prefs-custom-js' )->text();
1014 $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
1015 }
1016
1017 $defaultPreferences['commoncssjs'] = [
1018 'type' => 'info',
1019 'raw' => true,
1020 'default' => $context->getLanguage()->pipeList( $linkTools ),
1021 'label-message' => 'prefs-common-config',
1022 'section' => 'rendering/skin',
1023 ];
1024 }
1025 }
1026 }
1027
1032 protected function filesPreferences( IContextSource $context, &$defaultPreferences ) {
1033 $defaultPreferences['imagesize'] = [
1034 'type' => 'select',
1035 'options' => $this->getImageSizes( $context ),
1036 'label-message' => 'imagemaxsize',
1037 'section' => 'rendering/files',
1038 ];
1039 $defaultPreferences['thumbsize'] = [
1040 'type' => 'select',
1041 'options' => $this->getThumbSizes( $context ),
1042 'label-message' => 'thumbsize',
1043 'section' => 'rendering/files',
1044 ];
1045 }
1046
1053 protected function datetimePreferences(
1054 User $user, IContextSource $context, &$defaultPreferences
1055 ) {
1056 $dateOptions = $this->getDateOptions( $context );
1057 if ( $dateOptions ) {
1058 $defaultPreferences['date'] = [
1059 'type' => 'radio',
1060 'options' => $dateOptions,
1061 'section' => 'rendering/dateformat',
1062 ];
1063 }
1064
1065 // Info
1066 $now = wfTimestampNow();
1067 $lang = $context->getLanguage();
1068 $nowlocal = Xml::element( 'span', [ 'id' => 'wpLocalTime' ],
1069 $lang->userTime( $now, $user ) );
1070 $nowserver = $lang->userTime( $now, $user,
1071 [ 'format' => false, 'timecorrection' => false ] ) .
1072 Html::hidden( 'wpServerTime', (int)substr( $now, 8, 2 ) * 60 + (int)substr( $now, 10, 2 ) );
1073
1074 $defaultPreferences['nowserver'] = [
1075 'type' => 'info',
1076 'raw' => 1,
1077 'label-message' => 'servertime',
1078 'default' => $nowserver,
1079 'section' => 'rendering/timeoffset',
1080 ];
1081
1082 $defaultPreferences['nowlocal'] = [
1083 'type' => 'info',
1084 'raw' => 1,
1085 'label-message' => 'localtime',
1086 'default' => $nowlocal,
1087 'section' => 'rendering/timeoffset',
1088 ];
1089
1090 $userTimeCorrection = (string)$this->userOptionsManager->getOption( $user, 'timecorrection' );
1091 // This value should already be normalized by UserTimeCorrection, so it should always be valid and not
1092 // in the legacy format. However, let's be sure about that and normalize it again.
1093 // Also, recompute the offset because it can change with DST.
1094 $userTimeCorrectionObj = new UserTimeCorrection(
1095 $userTimeCorrection,
1096 null,
1097 $this->options->get( MainConfigNames::LocalTZoffset )
1098 );
1099
1100 if ( $userTimeCorrectionObj->getCorrectionType() === UserTimeCorrection::OFFSET ) {
1101 $tzDefault = UserTimeCorrection::formatTimezoneOffset( $userTimeCorrectionObj->getTimeOffset() );
1102 } else {
1103 $tzDefault = $userTimeCorrectionObj->toString();
1104 }
1105
1106 $defaultPreferences['timecorrection'] = [
1107 'type' => 'timezone',
1108 'label-message' => 'timezonelegend',
1109 'default' => $tzDefault,
1110 'size' => 20,
1111 'section' => 'rendering/timeoffset',
1112 'id' => 'wpTimeCorrection',
1113 'filter' => TimezoneFilter::class,
1114 ];
1115 }
1116
1122 protected function renderingPreferences(
1123 User $user,
1124 MessageLocalizer $l10n,
1125 &$defaultPreferences
1126 ) {
1127 // Diffs
1128 $defaultPreferences['diffonly'] = [
1129 'type' => 'toggle',
1130 'section' => 'rendering/diffs',
1131 'label-message' => 'tog-diffonly',
1132 ];
1133 $defaultPreferences['norollbackdiff'] = [
1134 'type' => 'toggle',
1135 'section' => 'rendering/diffs',
1136 'label-message' => 'tog-norollbackdiff',
1137 ];
1138 $defaultPreferences['diff-type'] = [
1139 'type' => 'api',
1140 ];
1141
1142 // Page Rendering
1143 if ( $this->options->get( MainConfigNames::AllowUserCssPrefs ) ) {
1144 $defaultPreferences['underline'] = [
1145 'type' => 'select',
1146 'options' => [
1147 $l10n->msg( 'underline-never' )->text() => 0,
1148 $l10n->msg( 'underline-always' )->text() => 1,
1149 $l10n->msg( 'underline-default' )->text() => 2,
1150 ],
1151 'label-message' => 'tog-underline',
1152 'section' => 'rendering/advancedrendering',
1153 ];
1154 }
1155
1156 $defaultPreferences['showhiddencats'] = [
1157 'type' => 'toggle',
1158 'section' => 'rendering/advancedrendering',
1159 'label-message' => 'tog-showhiddencats'
1160 ];
1161
1162 if ( $user->isAllowed( 'rollback' ) ) {
1163 $defaultPreferences['showrollbackconfirmation'] = [
1164 'type' => 'toggle',
1165 'section' => 'rendering/advancedrendering',
1166 'label-message' => 'tog-showrollbackconfirmation',
1167 ];
1168 }
1169
1170 $defaultPreferences['forcesafemode'] = [
1171 'type' => 'toggle',
1172 'section' => 'rendering/advancedrendering',
1173 'label-message' => 'tog-forcesafemode',
1174 'help-message' => 'prefs-help-forcesafemode'
1175 ];
1176 }
1177
1183 protected function editingPreferences( User $user, MessageLocalizer $l10n, &$defaultPreferences ) {
1184 $defaultPreferences['editsectiononrightclick'] = [
1185 'type' => 'toggle',
1186 'section' => 'editing/advancedediting',
1187 'label-message' => 'tog-editsectiononrightclick',
1188 ];
1189 $defaultPreferences['editondblclick'] = [
1190 'type' => 'toggle',
1191 'section' => 'editing/advancedediting',
1192 'label-message' => 'tog-editondblclick',
1193 ];
1194
1195 if ( $this->options->get( MainConfigNames::AllowUserCssPrefs ) ) {
1196 $defaultPreferences['editfont'] = [
1197 'type' => 'select',
1198 'section' => 'editing/editor',
1199 'label-message' => 'editfont-style',
1200 'options' => [
1201 $l10n->msg( 'editfont-monospace' )->text() => 'monospace',
1202 $l10n->msg( 'editfont-sansserif' )->text() => 'sans-serif',
1203 $l10n->msg( 'editfont-serif' )->text() => 'serif',
1204 ]
1205 ];
1206 }
1207
1208 if ( $user->isAllowed( 'minoredit' ) ) {
1209 $defaultPreferences['minordefault'] = [
1210 'type' => 'toggle',
1211 'section' => 'editing/editor',
1212 'label-message' => 'tog-minordefault',
1213 ];
1214 }
1215
1216 $defaultPreferences['forceeditsummary'] = [
1217 'type' => 'toggle',
1218 'section' => 'editing/editor',
1219 'label-message' => 'tog-forceeditsummary',
1220 ];
1221
1222 // T350653
1223 if ( $this->options->get( MainConfigNames::EnableEditRecovery ) ) {
1224 $defaultPreferences['editrecovery'] = [
1225 'type' => 'toggle',
1226 'section' => 'editing/editor',
1227 'label-message' => 'tog-editrecovery',
1228 'help-message' => [
1229 'tog-editrecovery-help',
1230 'https://meta.wikimedia.org/wiki/Talk:Community_Wishlist_Survey_2023/Edit-recovery_feature',
1231 ],
1232 ];
1233 }
1234
1235 $defaultPreferences['useeditwarning'] = [
1236 'type' => 'toggle',
1237 'section' => 'editing/editor',
1238 'label-message' => 'tog-useeditwarning',
1239 ];
1240
1241 $defaultPreferences['previewonfirst'] = [
1242 'type' => 'toggle',
1243 'section' => 'editing/preview',
1244 'label-message' => 'tog-previewonfirst',
1245 ];
1246 $defaultPreferences['previewontop'] = [
1247 'type' => 'toggle',
1248 'section' => 'editing/preview',
1249 'label-message' => 'tog-previewontop',
1250 ];
1251 $defaultPreferences['uselivepreview'] = [
1252 'type' => 'toggle',
1253 'section' => 'editing/preview',
1254 'label-message' => 'tog-uselivepreview',
1255 ];
1256 }
1257
1263 protected function rcPreferences( User $user, MessageLocalizer $l10n, &$defaultPreferences ) {
1264 $rcMaxAge = $this->options->get( MainConfigNames::RCMaxAge );
1265 $rcMax = ceil( $rcMaxAge / ( 3600 * 24 ) );
1266 $defaultPreferences['rcdays'] = [
1267 'type' => 'float',
1268 'label-message' => 'recentchangesdays',
1269 'section' => 'rc/displayrc',
1270 'min' => 1 / 24,
1271 'max' => $rcMax,
1272 'help-message' => [ 'recentchangesdays-max', Message::numParam( $rcMax ) ],
1273 ];
1274 $defaultPreferences['rclimit'] = [
1275 'type' => 'int',
1276 'min' => 1,
1277 'max' => 1000,
1278 'label-message' => 'recentchangescount',
1279 'help-message' => 'prefs-help-recentchangescount',
1280 'section' => 'rc/displayrc',
1281 'filter' => IntvalFilter::class,
1282 ];
1283 $defaultPreferences['usenewrc'] = [
1284 'type' => 'toggle',
1285 'label-message' => 'tog-usenewrc',
1286 'section' => 'rc/advancedrc',
1287 ];
1288 $defaultPreferences['hideminor'] = [
1289 'type' => 'toggle',
1290 'label-message' => 'tog-hideminor',
1291 'section' => 'rc/changesrc',
1292 ];
1293 $defaultPreferences['pst-cssjs'] = [
1294 'type' => 'api',
1295 ];
1296 $defaultPreferences['rcfilters-rc-collapsed'] = [
1297 'type' => 'api',
1298 ];
1299 $defaultPreferences['rcfilters-wl-collapsed'] = [
1300 'type' => 'api',
1301 ];
1302 $defaultPreferences['rcfilters-saved-queries'] = [
1303 'type' => 'api',
1304 ];
1305 $defaultPreferences['rcfilters-wl-saved-queries'] = [
1306 'type' => 'api',
1307 ];
1308 // Override RCFilters preferences for RecentChanges 'limit'
1309 $defaultPreferences['rcfilters-limit'] = [
1310 'type' => 'api',
1311 ];
1312 $defaultPreferences['rcfilters-saved-queries-versionbackup'] = [
1313 'type' => 'api',
1314 ];
1315 $defaultPreferences['rcfilters-wl-saved-queries-versionbackup'] = [
1316 'type' => 'api',
1317 ];
1318
1319 if ( $this->options->get( MainConfigNames::RCWatchCategoryMembership ) ) {
1320 $defaultPreferences['hidecategorization'] = [
1321 'type' => 'toggle',
1322 'label-message' => 'tog-hidecategorization',
1323 'section' => 'rc/changesrc',
1324 ];
1325 }
1326
1327 if ( $user->useRCPatrol() ) {
1328 $defaultPreferences['hidepatrolled'] = [
1329 'type' => 'toggle',
1330 'section' => 'rc/changesrc',
1331 'label-message' => 'tog-hidepatrolled',
1332 ];
1333 }
1334
1335 if ( $user->useNPPatrol() ) {
1336 $defaultPreferences['newpageshidepatrolled'] = [
1337 'type' => 'toggle',
1338 'section' => 'rc/changesrc',
1339 'label-message' => 'tog-newpageshidepatrolled',
1340 ];
1341 }
1342
1343 if ( $this->options->get( MainConfigNames::RCShowWatchingUsers ) ) {
1344 $defaultPreferences['shownumberswatching'] = [
1345 'type' => 'toggle',
1346 'section' => 'rc/advancedrc',
1347 'label-message' => 'tog-shownumberswatching',
1348 ];
1349 }
1350
1351 $defaultPreferences['rcenhancedfilters-disable'] = [
1352 'type' => 'toggle',
1353 'section' => 'rc/advancedrc',
1354 'label-message' => 'rcfilters-preference-label',
1355 'help-message' => 'rcfilters-preference-help',
1356 ];
1357 }
1358
1364 protected function watchlistPreferences(
1365 User $user, IContextSource $context, &$defaultPreferences
1366 ) {
1367 $watchlistdaysMax = ceil( $this->options->get( MainConfigNames::RCMaxAge ) / ( 3600 * 24 ) );
1368
1369 if ( $user->isAllowed( 'editmywatchlist' ) ) {
1370 $editWatchlistLinks = '';
1371 $editWatchlistModes = [
1372 'edit' => [ 'subpage' => false, 'flags' => [] ],
1373 'raw' => [ 'subpage' => 'raw', 'flags' => [] ],
1374 'clear' => [ 'subpage' => 'clear', 'flags' => [ 'destructive' ] ],
1375 ];
1376 foreach ( $editWatchlistModes as $mode => $options ) {
1377 // Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear
1378 $editWatchlistLinks .=
1379 new ButtonWidget( [
1380 'href' => SpecialPage::getTitleFor( 'EditWatchlist', $options['subpage'] )->getLinkURL(),
1381 'flags' => $options[ 'flags' ],
1382 'label' => new HtmlSnippet(
1383 $context->msg( "prefs-editwatchlist-{$mode}" )->parse()
1384 ),
1385 ] );
1386 }
1387
1388 $defaultPreferences['editwatchlist'] = [
1389 'type' => 'info',
1390 'raw' => true,
1391 'default' => $editWatchlistLinks,
1392 'label-message' => 'prefs-editwatchlist-label',
1393 'section' => 'watchlist/editwatchlist',
1394 ];
1395 }
1396
1397 $defaultPreferences['watchlistdays'] = [
1398 'type' => 'float',
1399 'min' => 1 / 24,
1400 'max' => $watchlistdaysMax,
1401 'section' => 'watchlist/displaywatchlist',
1402 'help-message' => [ 'prefs-watchlist-days-max', Message::numParam( $watchlistdaysMax ) ],
1403 'label-message' => 'prefs-watchlist-days',
1404 ];
1405 $defaultPreferences['wllimit'] = [
1406 'type' => 'int',
1407 'min' => 1,
1408 'max' => 1000,
1409 'label-message' => 'prefs-watchlist-edits',
1410 'help-message' => 'prefs-watchlist-edits-max',
1411 'section' => 'watchlist/displaywatchlist',
1412 'filter' => IntvalFilter::class,
1413 ];
1414 $defaultPreferences['extendwatchlist'] = [
1415 'type' => 'toggle',
1416 'section' => 'watchlist/advancedwatchlist',
1417 'label-message' => 'tog-extendwatchlist',
1418 ];
1419 $defaultPreferences['watchlisthideminor'] = [
1420 'type' => 'toggle',
1421 'section' => 'watchlist/changeswatchlist',
1422 'label-message' => 'tog-watchlisthideminor',
1423 ];
1424 $defaultPreferences['watchlisthidebots'] = [
1425 'type' => 'toggle',
1426 'section' => 'watchlist/changeswatchlist',
1427 'label-message' => 'tog-watchlisthidebots',
1428 ];
1429 $defaultPreferences['watchlisthideown'] = [
1430 'type' => 'toggle',
1431 'section' => 'watchlist/changeswatchlist',
1432 'label-message' => 'tog-watchlisthideown',
1433 ];
1434 $defaultPreferences['watchlisthideanons'] = [
1435 'type' => 'toggle',
1436 'section' => 'watchlist/changeswatchlist',
1437 'label-message' => 'tog-watchlisthideanons',
1438 ];
1439 $defaultPreferences['watchlisthideliu'] = [
1440 'type' => 'toggle',
1441 'section' => 'watchlist/changeswatchlist',
1442 'label-message' => 'tog-watchlisthideliu',
1443 ];
1444
1446 $defaultPreferences['watchlistreloadautomatically'] = [
1447 'type' => 'toggle',
1448 'section' => 'watchlist/advancedwatchlist',
1449 'label-message' => 'tog-watchlistreloadautomatically',
1450 ];
1451 }
1452
1453 $defaultPreferences['watchlistunwatchlinks'] = [
1454 'type' => 'toggle',
1455 'section' => 'watchlist/advancedwatchlist',
1456 'label-message' => 'tog-watchlistunwatchlinks',
1457 ];
1458
1459 if ( $this->options->get( MainConfigNames::RCWatchCategoryMembership ) ) {
1460 $defaultPreferences['watchlisthidecategorization'] = [
1461 'type' => 'toggle',
1462 'section' => 'watchlist/changeswatchlist',
1463 'label-message' => 'tog-watchlisthidecategorization',
1464 ];
1465 }
1466
1467 if ( $user->useRCPatrol() ) {
1468 $defaultPreferences['watchlisthidepatrolled'] = [
1469 'type' => 'toggle',
1470 'section' => 'watchlist/changeswatchlist',
1471 'label-message' => 'tog-watchlisthidepatrolled',
1472 ];
1473 }
1474
1475 $watchTypes = [
1476 'edit' => 'watchdefault',
1477 'move' => 'watchmoves',
1478 ];
1479
1480 // Kinda hacky
1481 if ( $user->isAllowedAny( 'createpage', 'createtalk' ) ) {
1482 $watchTypes['read'] = 'watchcreations';
1483 }
1484
1485 // Move uncommon actions to end of list
1486 $watchTypes += [
1487 'rollback' => 'watchrollback',
1488 'upload' => 'watchuploads',
1489 'delete' => 'watchdeletion',
1490 ];
1491
1492 foreach ( $watchTypes as $action => $pref ) {
1493 if ( $user->isAllowed( $action ) ) {
1494 // Messages:
1495 // tog-watchdefault, tog-watchmoves, tog-watchdeletion, tog-watchcreations, tog-watchuploads
1496 // tog-watchrollback
1497 $defaultPreferences[$pref] = [
1498 'type' => 'toggle',
1499 'section' => 'watchlist/pageswatchlist',
1500 'label-message' => "tog-$pref",
1501 ];
1502 }
1503 }
1504
1505 $defaultPreferences['watchlisttoken'] = [
1506 'type' => 'api',
1507 ];
1508
1509 $tokenButton = new ButtonWidget( [
1510 'href' => SpecialPage::getTitleFor( 'ResetTokens' )->getLinkURL( [
1511 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
1512 ] ),
1513 'label' => $context->msg( 'prefs-watchlist-managetokens' )->text(),
1514 ] );
1515 $defaultPreferences['watchlisttoken-info'] = [
1516 'type' => 'info',
1517 'section' => 'watchlist/tokenwatchlist',
1518 'label-message' => 'prefs-watchlist-token',
1519 'help-message' => 'prefs-help-tokenmanagement',
1520 'raw' => true,
1521 'default' => (string)$tokenButton,
1522 ];
1523
1524 $defaultPreferences['wlenhancedfilters-disable'] = [
1525 'type' => 'toggle',
1526 'section' => 'watchlist/advancedwatchlist',
1527 'label-message' => 'rcfilters-watchlist-preference-label',
1528 'help-message' => 'rcfilters-watchlist-preference-help',
1529 ];
1530 }
1531
1536 protected function searchPreferences( $context, &$defaultPreferences ) {
1537 $defaultPreferences['search-special-page'] = [
1538 'type' => 'api',
1539 ];
1540
1541 foreach ( $this->nsInfo->getValidNamespaces() as $n ) {
1542 $defaultPreferences['searchNs' . $n] = [
1543 'type' => 'api',
1544 ];
1545 }
1546
1547 if ( $this->options->get( MainConfigNames::SearchMatchRedirectPreference ) ) {
1548 $defaultPreferences['search-match-redirect'] = [
1549 'type' => 'toggle',
1550 'section' => 'searchoptions/searchmisc',
1551 'label-message' => 'search-match-redirect-label',
1552 'help-message' => 'search-match-redirect-help',
1553 ];
1554 } else {
1555 $defaultPreferences['search-match-redirect'] = [
1556 'type' => 'api',
1557 ];
1558 }
1559
1560 $defaultPreferences['searchlimit'] = [
1561 'type' => 'int',
1562 'min' => 1,
1563 'max' => 500,
1564 'section' => 'searchoptions/searchmisc',
1565 'label-message' => 'searchlimit-label',
1566 'help-message' => $context->msg( 'searchlimit-help', 500 ),
1567 'filter' => IntvalFilter::class,
1568 ];
1569
1570 // show a preference for thumbnails from namespaces other than NS_FILE,
1571 // only when there they're actually configured to be served
1572 $thumbNamespaces = $this->options->get( MainConfigNames::ThumbnailNamespaces );
1573 $thumbNamespacesFormatted = array_combine(
1574 $thumbNamespaces,
1575 array_map(
1576 static function ( $namespaceId ) use ( $context ) {
1577 return $namespaceId === NS_MAIN
1578 ? $context->msg( 'blanknamespace' )->escaped()
1579 : $context->getLanguage()->getFormattedNsText( $namespaceId );
1580 },
1581 $thumbNamespaces
1582 )
1583 );
1584 $defaultThumbNamespacesFormatted =
1585 array_intersect_key( $thumbNamespacesFormatted, [ NS_FILE => 1 ] ) ?? [];
1586 $extraThumbNamespacesFormatted =
1587 array_diff_key( $thumbNamespacesFormatted, [ NS_FILE => 1 ] );
1588 if ( $extraThumbNamespacesFormatted ) {
1589 $defaultPreferences['search-thumbnail-extra-namespaces'] = [
1590 'type' => 'toggle',
1591 'section' => 'searchoptions/searchmisc',
1592 'label-message' => 'search-thumbnail-extra-namespaces-label',
1593 'help-message' => $context->msg(
1594 'search-thumbnail-extra-namespaces-message',
1595 $context->getLanguage()->listToText( $extraThumbNamespacesFormatted ),
1596 count( $extraThumbNamespacesFormatted ),
1597 $context->getLanguage()->listToText( $defaultThumbNamespacesFormatted ),
1598 count( $defaultThumbNamespacesFormatted )
1599 ),
1600 ];
1601 }
1602 }
1603
1604 /*
1605 * Custom skin string comparison function that takes into account current and preferred skins.
1606 *
1607 * @param string $a
1608 * @param string $b
1609 * @param string $currentSkin
1610 * @param array $preferredSkins
1611 * @return int
1612 */
1613 private static function sortSkinNames( $a, $b, $currentSkin, $preferredSkins ) {
1614 // Display the current skin first in the list
1615 if ( strcasecmp( $a, $currentSkin ) === 0 ) {
1616 return -1;
1617 }
1618 if ( strcasecmp( $b, $currentSkin ) === 0 ) {
1619 return 1;
1620 }
1621 // Display preferred skins over other skins
1622 if ( count( $preferredSkins ) ) {
1623 $aPreferred = array_search( $a, $preferredSkins );
1624 $bPreferred = array_search( $b, $preferredSkins );
1625 // Cannot use ! operator because array_search returns the
1626 // index of the array item if found (i.e. 0) and false otherwise
1627 if ( $aPreferred !== false && $bPreferred === false ) {
1628 return -1;
1629 }
1630 if ( $aPreferred === false && $bPreferred !== false ) {
1631 return 1;
1632 }
1633 // When both skins are preferred, default to the ordering
1634 // specified by the preferred skins config array
1635 if ( $aPreferred !== false && $bPreferred !== false ) {
1636 return strcasecmp( $aPreferred, $bPreferred );
1637 }
1638 }
1639 // Use normal string comparison if both strings are not preferred
1640 return strcasecmp( $a, $b );
1641 }
1642
1651 private function getValidSkinNames( User $user, IContextSource $context ) {
1652 // Only show skins that aren't disabled
1653 $validSkinNames = $this->skinFactory->getAllowedSkins();
1654 $allInstalledSkins = $this->skinFactory->getInstalledSkins();
1655
1656 // Display the installed skin the user has specifically requested via useskin=….
1657 $useSkin = $context->getRequest()->getRawVal( 'useskin' );
1658 if ( isset( $allInstalledSkins[$useSkin] )
1659 && $context->msg( "skinname-$useSkin" )->exists()
1660 ) {
1661 $validSkinNames[$useSkin] = $useSkin;
1662 }
1663
1664 // Display the skin if the user has set it as a preference already before it was hidden.
1665 $currentUserSkin = $this->userOptionsManager->getOption( $user, 'skin' );
1666 if ( isset( $allInstalledSkins[$currentUserSkin] )
1667 && $context->msg( "skinname-$currentUserSkin" )->exists()
1668 ) {
1669 $validSkinNames[$currentUserSkin] = $currentUserSkin;
1670 }
1671
1672 foreach ( $validSkinNames as $skinkey => &$skinname ) {
1673 $msg = $context->msg( "skinname-{$skinkey}" );
1674 if ( $msg->exists() ) {
1675 $skinname = htmlspecialchars( $msg->text() );
1676 }
1677 }
1678
1679 $preferredSkins = $this->options->get( MainConfigNames::SkinsPreferred );
1680 // Sort by the internal name, so that the ordering is the same for each display language,
1681 // especially if some skin names are translated to use a different alphabet and some are not.
1682 uksort( $validSkinNames, function ( $a, $b ) use ( $currentUserSkin, $preferredSkins ) {
1683 return $this->sortSkinNames( $a, $b, $currentUserSkin, $preferredSkins );
1684 } );
1685
1686 return $validSkinNames;
1687 }
1688
1695 protected function generateSkinOptions( User $user, IContextSource $context, array $validSkinNames ) {
1696 $ret = [];
1697
1698 $mptitle = Title::newMainPage();
1699 $previewtext = $context->msg( 'skin-preview' )->escaped();
1700 $defaultSkin = $this->options->get( MainConfigNames::DefaultSkin );
1701 $allowUserCss = $this->options->get( MainConfigNames::AllowUserCss );
1702 $allowUserJs = $this->options->get( MainConfigNames::AllowUserJs );
1703 $safeMode = $this->userOptionsManager->getOption( $user, 'forcesafemode' );
1704 $foundDefault = false;
1705 foreach ( $validSkinNames as $skinkey => $sn ) {
1706 $linkTools = [];
1707
1708 // Mark the default skin
1709 if ( strcasecmp( $skinkey, $defaultSkin ) === 0 ) {
1710 $linkTools[] = $context->msg( 'default' )->escaped();
1711 $foundDefault = true;
1712 }
1713
1714 // Create talk page link if relevant message exists.
1715 $talkPageMsg = $context->msg( "$skinkey-prefs-talkpage" );
1716 if ( $talkPageMsg->exists() ) {
1717 $linkTools[] = $talkPageMsg->parse();
1718 }
1719
1720 // Create preview link
1721 $mplink = htmlspecialchars( $mptitle->getLocalURL( [ 'useskin' => $skinkey ] ) );
1722 $linkTools[] = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
1723
1724 if ( !$safeMode ) {
1725 // Create links to user CSS/JS pages
1726 // @todo Refactor this and the similar code in skinPreferences().
1727 if ( $allowUserCss ) {
1728 $cssPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.css' );
1729 $cssLinkText = $context->msg( 'prefs-custom-css' )->text();
1730 $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
1731 }
1732
1733 if ( $allowUserJs ) {
1734 $jsPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.js' );
1735 $jsLinkText = $context->msg( 'prefs-custom-js' )->text();
1736 $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
1737 }
1738 }
1739
1740 $display = $sn . ' ' . $context->msg( 'parentheses' )
1741 ->rawParams( $context->getLanguage()->pipeList( $linkTools ) )
1742 ->escaped();
1743 $ret[$display] = $skinkey;
1744 }
1745
1746 if ( !$foundDefault ) {
1747 // If the default skin is not available, things are going to break horribly because the
1748 // default value for skin selector will not be a valid value. Let's just not show it then.
1749 return [];
1750 }
1751
1752 return $ret;
1753 }
1754
1759 protected function getDateOptions( IContextSource $context ) {
1760 $lang = $context->getLanguage();
1761 $dateopts = $lang->getDatePreferences();
1762
1763 $ret = [];
1764
1765 if ( $dateopts ) {
1766 if ( !in_array( 'default', $dateopts ) ) {
1767 $dateopts[] = 'default'; // Make sure default is always valid T21237
1768 }
1769
1770 // FIXME KLUGE: site default might not be valid for user language
1771 global $wgDefaultUserOptions;
1772 if ( !in_array( $wgDefaultUserOptions['date'], $dateopts ) ) {
1773 $wgDefaultUserOptions['date'] = 'default';
1774 }
1775
1776 $epoch = wfTimestampNow();
1777 foreach ( $dateopts as $key ) {
1778 if ( $key == 'default' ) {
1779 $formatted = $context->msg( 'datedefault' )->escaped();
1780 } else {
1781 $formatted = htmlspecialchars( $lang->timeanddate( $epoch, false, $key ) );
1782 }
1783 $ret[$formatted] = $key;
1784 }
1785 }
1786 return $ret;
1787 }
1788
1793 protected function getImageSizes( MessageLocalizer $l10n ) {
1794 $ret = [];
1795 $pixels = $l10n->msg( 'unit-pixel' )->text();
1796
1797 foreach ( $this->options->get( MainConfigNames::ImageLimits ) as $index => $limits ) {
1798 // Note: A left-to-right marker (U+200E) is inserted, see T144386
1799 $display = "{$limits[0]}\u{200E}×{$limits[1]}$pixels";
1800 $ret[$display] = $index;
1801 }
1802
1803 return $ret;
1804 }
1805
1810 protected function getThumbSizes( MessageLocalizer $l10n ) {
1811 $ret = [];
1812 $pixels = $l10n->msg( 'unit-pixel' )->text();
1813
1814 foreach ( $this->options->get( MainConfigNames::ThumbLimits ) as $index => $size ) {
1815 $display = $size . $pixels;
1816 $ret[$display] = $index;
1817 }
1818
1819 return $ret;
1820 }
1821
1828 protected function validateSignature( $signature, $alldata, HTMLForm $form ) {
1829 $sigValidation = $this->options->get( MainConfigNames::SignatureValidation );
1830 $maxSigChars = $this->options->get( MainConfigNames::MaxSigChars );
1831 if ( is_string( $signature ) && mb_strlen( $signature ) > $maxSigChars ) {
1832 return $form->msg( 'badsiglength' )->numParams( $maxSigChars )->escaped();
1833 }
1834
1835 if ( $signature === null || $signature === '' ) {
1836 // Make sure leaving the field empty is valid, since that's used as the default (T288151).
1837 // Code using this preference in Parser::getUserSig() handles this case specially.
1838 return true;
1839 }
1840
1841 // Remaining checks only apply to fancy signatures
1842 if ( !( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) ) {
1843 return true;
1844 }
1845
1846 // HERE BE DRAGONS:
1847 //
1848 // If this value is already saved as the user's signature, treat it as valid, even if it
1849 // would be invalid to save now, and even if $wgSignatureValidation is set to 'disallow'.
1850 //
1851 // It can become invalid when we introduce new validation, or when the value just transcludes
1852 // some page containing the real signature and that page is edited (which we can't validate),
1853 // or when someone's username is changed.
1854 //
1855 // Otherwise it would be completely removed when the user opens their preferences page, which
1856 // would be very unfriendly.
1857 $user = $form->getUser();
1858 if (
1859 $signature === $this->userOptionsManager->getOption( $user, 'nickname' ) &&
1860 (bool)$alldata['fancysig'] === $this->userOptionsManager->getBoolOption( $user, 'fancysig' )
1861 ) {
1862 return true;
1863 }
1864
1865 if ( $sigValidation === 'new' || $sigValidation === 'disallow' ) {
1866 // Validate everything
1867 $parserOpts = ParserOptions::newFromContext( $form->getContext() );
1868 $validator = $this->signatureValidatorFactory
1869 ->newSignatureValidator( $user, $form->getContext(), $parserOpts );
1870 $errors = $validator->validateSignature( $signature );
1871 if ( $errors ) {
1872 return $errors;
1873 }
1874 }
1875
1876 // Quick check for mismatched HTML tags in the input.
1877 // Note that this is easily fooled by wikitext templates or bold/italic markup.
1878 // We're only keeping this until Parsoid is integrated and guaranteed to be available.
1879 if ( $this->parserFactory->getInstance()->validateSig( $signature ) === false ) {
1880 return $form->msg( 'badsig' )->escaped();
1881 }
1882
1883 return true;
1884 }
1885
1892 protected function cleanSignature( $signature, $alldata, HTMLForm $form ) {
1893 if ( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) {
1894 $signature = $this->parserFactory->getInstance()->cleanSig( $signature );
1895 } else {
1896 // When no fancy sig used, make sure ~{3,5} get removed.
1897 $signature = Parser::cleanSigInSig( $signature );
1898 }
1899
1900 return $signature;
1901 }
1902
1910 public function getForm(
1911 User $user,
1912 IContextSource $context,
1913 $formClass = PreferencesFormOOUI::class,
1914 array $remove = []
1915 ) {
1916 // We use ButtonWidgets in some of the getPreferences() functions
1917 $context->getOutput()->enableOOUI();
1918
1919 // Note that the $user parameter of getFormDescriptor() is deprecated.
1920 $formDescriptor = $this->getFormDescriptor( $user, $context );
1921 if ( count( $remove ) ) {
1922 $removeKeys = array_fill_keys( $remove, true );
1923 $formDescriptor = array_diff_key( $formDescriptor, $removeKeys );
1924 }
1925
1926 // Remove type=api preferences. They are not intended for rendering in the form.
1927 foreach ( $formDescriptor as $name => $info ) {
1928 if ( isset( $info['type'] ) && $info['type'] === 'api' ) {
1929 unset( $formDescriptor[$name] );
1930 }
1931 }
1932
1936 $htmlForm = new $formClass( $formDescriptor, $context, 'prefs' );
1937
1938 // This allows users to opt-in to hidden skins. While this should be discouraged and is not
1939 // discoverable, this allows users to still use hidden skins while preventing new users from
1940 // adopting unsupported skins. If no useskin=… parameter was provided, it will not show up
1941 // in the resulting URL.
1942 $htmlForm->setAction( $context->getTitle()->getLocalURL( [
1943 'useskin' => $context->getRequest()->getRawVal( 'useskin' )
1944 ] ) );
1945
1946 $htmlForm->setModifiedUser( $user );
1947 $htmlForm->setOptionsEditable( $user->isAllowed( 'editmyoptions' ) );
1948 $htmlForm->setPrivateInfoEditable( $user->isAllowed( 'editmyprivateinfo' ) );
1949 $htmlForm->setId( 'mw-prefs-form' );
1950 $htmlForm->setAutocomplete( 'off' );
1951 $htmlForm->setSubmitTextMsg( 'saveprefs' );
1952 // Used message keys: 'accesskey-preferences-save', 'tooltip-preferences-save'
1953 $htmlForm->setSubmitTooltip( 'preferences-save' );
1954 $htmlForm->setSubmitID( 'prefcontrol' );
1955 $htmlForm->setSubmitCallback(
1956 function ( array $formData, PreferencesFormOOUI $form ) use ( $formDescriptor ) {
1957 return $this->submitForm( $formData, $form, $formDescriptor );
1958 }
1959 );
1960
1961 return $htmlForm;
1962 }
1963
1972 protected function saveFormData( $formData, PreferencesFormOOUI $form, array $formDescriptor ) {
1973 $user = $form->getModifiedUser();
1974 $hiddenPrefs = $this->options->get( MainConfigNames::HiddenPrefs );
1975 $result = true;
1976
1977 if ( !$user->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
1978 return Status::newFatal( 'mypreferencesprotected' );
1979 }
1980
1981 // Filter input
1982 $this->applyFilters( $formData, $formDescriptor, 'filterFromForm' );
1983
1984 // Fortunately, the realname field is MUCH simpler
1985 // (not really "private", but still shouldn't be edited without permission)
1986
1987 if ( !in_array( 'realname', $hiddenPrefs )
1988 && $user->isAllowed( 'editmyprivateinfo' )
1989 && array_key_exists( 'realname', $formData )
1990 ) {
1991 $realName = $formData['realname'];
1992 $user->setRealName( $realName );
1993 }
1994
1995 if ( $user->isAllowed( 'editmyoptions' ) ) {
1996 $oldUserOptions = $this->userOptionsManager->getOptions( $user );
1997
1998 foreach ( $this->getSaveBlacklist() as $b ) {
1999 unset( $formData[$b] );
2000 }
2001
2002 // If users have saved a value for a preference which has subsequently been disabled
2003 // via $wgHiddenPrefs, we don't want to destroy that setting in case the preference
2004 // is subsequently re-enabled
2005 foreach ( $hiddenPrefs as $pref ) {
2006 // If the user has not set a non-default value here, the default will be returned
2007 // and subsequently discarded
2008 $formData[$pref] = $this->userOptionsManager->getOption( $user, $pref, null, true );
2009 }
2010
2011 // If the user changed the rclimit preference, also change the rcfilters-rclimit preference
2012 if (
2013 isset( $formData['rclimit'] ) &&
2014 intval( $formData[ 'rclimit' ] ) !== $this->userOptionsManager->getIntOption( $user, 'rclimit' )
2015 ) {
2016 $formData['rcfilters-limit'] = $formData['rclimit'];
2017 }
2018
2019 // Keep old preferences from interfering due to back-compat code, etc.
2020 $optionsToReset = $this->getOptionNamesForReset( $user, $form->getContext(), 'unused' );
2021 $this->userOptionsManager->resetOptionsByName( $user, $optionsToReset );
2022
2023 foreach ( $formData as $key => $value ) {
2024 // If we're creating a new local override, we need to explicitly pass
2025 // GLOBAL_OVERRIDE to setOption(), otherwise the update would be ignored
2026 // due to the conflicting global option.
2027 $except = !empty( $formData[$key . UserOptionsLookup::LOCAL_EXCEPTION_SUFFIX] );
2028 $this->userOptionsManager->setOption( $user, $key, $value,
2029 $except ? UserOptionsManager::GLOBAL_OVERRIDE : UserOptionsManager::GLOBAL_IGNORE );
2030 }
2031
2032 $this->hookRunner->onPreferencesFormPreSave(
2033 $formData, $form, $user, $result, $oldUserOptions );
2034 }
2035
2036 $user->saveSettings();
2037
2038 return $result;
2039 }
2040
2049 protected function applyFilters( array &$preferences, array $formDescriptor, $verb ) {
2050 foreach ( $formDescriptor as $preference => $desc ) {
2051 if ( !isset( $desc['filter'] ) || !isset( $preferences[$preference] ) ) {
2052 continue;
2053 }
2054 $filterDesc = $desc['filter'];
2055 if ( $filterDesc instanceof Filter ) {
2056 $filter = $filterDesc;
2057 } elseif ( class_exists( $filterDesc ) ) {
2058 $filter = new $filterDesc();
2059 } elseif ( is_callable( $filterDesc ) ) {
2060 $filter = $filterDesc();
2061 } else {
2062 throw new UnexpectedValueException(
2063 "Unrecognized filter type for preference '$preference'"
2064 );
2065 }
2066 $preferences[$preference] = $filter->$verb( $preferences[$preference] );
2067 }
2068 }
2069
2078 protected function submitForm(
2079 array $formData,
2080 PreferencesFormOOUI $form,
2081 array $formDescriptor
2082 ) {
2083 $res = $this->saveFormData( $formData, $form, $formDescriptor );
2084
2085 if ( $res === true ) {
2086 $context = $form->getContext();
2087 $urlOptions = [];
2088
2089 $urlOptions += $form->getExtraSuccessRedirectParameters();
2090
2091 $url = $form->getTitle()->getFullURL( $urlOptions );
2092
2093 // Set session data for the success message
2094 $context->getRequest()->getSession()->set( 'specialPreferencesSaveSuccess', 1 );
2095
2096 $context->getOutput()->redirect( $url );
2097 }
2098
2099 return ( $res === true ? Status::newGood() : $res );
2100 }
2101
2102 public function getResetKinds(
2103 User $user, IContextSource $context, $options = null
2104 ): array {
2105 if ( $options === null ) {
2106 $options = $this->userOptionsManager->loadUserOptions( $user );
2107 }
2108
2109 $prefs = $this->getFormDescriptor( $user, $context );
2110 $mapping = [];
2111
2112 // Pull out the "special" options, so they don't get converted as
2113 // multiselect or checkmatrix.
2114 $specialOptions = array_fill_keys( $this->getSaveBlacklist(), true );
2115 foreach ( $specialOptions as $name => $value ) {
2116 unset( $prefs[$name] );
2117 }
2118
2119 // Multiselect and checkmatrix options are stored in the database with
2120 // one key per option, each having a boolean value. Extract those keys.
2121 $multiselectOptions = [];
2122 foreach ( $prefs as $name => $info ) {
2123 if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
2124 // Checking old alias for compatibility with unchanged extensions
2125 ( isset( $info['class'] ) && $info['class'] === \HTMLMultiSelectField::class ) ||
2126 ( isset( $info['class'] ) && $info['class'] === HTMLMultiSelectField::class )
2127 ) {
2128 $opts = HTMLFormField::flattenOptions( $info['options'] ?? $info['options-messages'] );
2129 $prefix = $info['prefix'] ?? $name;
2130
2131 foreach ( $opts as $value ) {
2132 $multiselectOptions["$prefix$value"] = true;
2133 }
2134
2135 unset( $prefs[$name] );
2136 }
2137 }
2138 $checkmatrixOptions = [];
2139 foreach ( $prefs as $name => $info ) {
2140 if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
2141 // Checking old alias for compatibility with unchanged extensions
2142 ( isset( $info['class'] ) && $info['class'] === \HTMLCheckMatrix::class ) ||
2143 ( isset( $info['class'] ) && $info['class'] === HTMLCheckMatrix::class )
2144 ) {
2145 $columns = HTMLFormField::flattenOptions( $info['columns'] );
2146 $rows = HTMLFormField::flattenOptions( $info['rows'] );
2147 $prefix = $info['prefix'] ?? $name;
2148
2149 foreach ( $columns as $column ) {
2150 foreach ( $rows as $row ) {
2151 $checkmatrixOptions["$prefix$column-$row"] = true;
2152 }
2153 }
2154
2155 unset( $prefs[$name] );
2156 }
2157 }
2158
2159 // $value is ignored
2160 foreach ( $options as $key => $value ) {
2161 if ( isset( $prefs[$key] ) ) {
2162 $mapping[$key] = 'registered';
2163 } elseif ( isset( $multiselectOptions[$key] ) ) {
2164 $mapping[$key] = 'registered-multiselect';
2165 } elseif ( isset( $checkmatrixOptions[$key] ) ) {
2166 $mapping[$key] = 'registered-checkmatrix';
2167 } elseif ( isset( $specialOptions[$key] ) ) {
2168 $mapping[$key] = 'special';
2169 } elseif ( str_starts_with( $key, 'userjs-' ) ) {
2170 $mapping[$key] = 'userjs';
2171 } elseif ( str_starts_with( $key, UserOptionsLookup::LOCAL_EXCEPTION_SUFFIX ) ) {
2172 $mapping[$key] = 'local-exception';
2173 } else {
2174 $mapping[$key] = 'unused';
2175 }
2176 }
2177
2178 return $mapping;
2179 }
2180
2181 public function listResetKinds() {
2182 return [
2183 'registered',
2184 'registered-multiselect',
2185 'registered-checkmatrix',
2186 'userjs',
2187 'special',
2188 'unused'
2189 ];
2190 }
2191
2192 public function getOptionNamesForReset( User $user, IContextSource $context, $kinds ) {
2193 $oldOptions = $this->userOptionsManager->loadUserOptions( $user, IDBAccessObject::READ_LATEST );
2194
2195 if ( !is_array( $kinds ) ) {
2196 $kinds = [ $kinds ];
2197 }
2198
2199 if ( in_array( 'all', $kinds ) ) {
2200 return array_keys( $oldOptions );
2201 } else {
2202 $optionKinds = $this->getResetKinds( $user, $context );
2203 $kinds = array_intersect( $kinds, $this->listResetKinds() );
2204 $optionNames = [];
2205
2206 foreach ( $oldOptions as $key => $value ) {
2207 if ( in_array( $optionKinds[$key], $kinds ) ) {
2208 $optionNames[] = $key;
2209 }
2210 }
2211 return $optionNames;
2212 }
2213 }
2214}
const NS_USER
Definition Defines.php:67
const NS_FILE
Definition Defines.php:71
const NS_MAIN
Definition Defines.php:65
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
array $params
The job parameters.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Methods for dealing with language codes.
Base class for multi-variant language conversion.
Base class for language-specific code.
Definition Language.php:66
This serves as the entry point to the authentication system.
This is a value object for authentication requests with a username and password.
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
getContext()
Get the base IContextSource object.
A checkbox matrix Operates similarly to HTMLMultiSelectField, but instead of using an array of option...
An information field (text blob), not a proper input.
The parent class to generate form fields.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:208
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition Html.php:216
An interface for creating language converters.
A service that provides utilities to do with language names and codes.
Class that generates HTML for internal links.
A class containing constants representing the names of configuration variables.
const HiddenPrefs
Name constant for the HiddenPrefs setting, for use with Config::get()
const ForceHTTPS
Name constant for the ForceHTTPS setting, for use with Config::get()
const EnotifWatchlist
Name constant for the EnotifWatchlist setting, for use with Config::get()
const MaxSigChars
Name constant for the MaxSigChars setting, for use with Config::get()
const RCMaxAge
Name constant for the RCMaxAge setting, for use with Config::get()
const DefaultSkin
Name constant for the DefaultSkin setting, for use with Config::get()
const EnableUserEmailMuteList
Name constant for the EnableUserEmailMuteList setting, for use with Config::get()
const EnotifRevealEditorAddress
Name constant for the EnotifRevealEditorAddress setting, for use with Config::get()
const EnableUserEmail
Name constant for the EnableUserEmail setting, for use with Config::get()
const SkinsPreferred
Name constant for the SkinsPreferred setting, for use with Config::get()
const EnableEditRecovery
Name constant for the EnableEditRecovery setting, for use with Config::get()
const EmailConfirmToEdit
Name constant for the EmailConfirmToEdit setting, for use with Config::get()
const EnableEmail
Name constant for the EnableEmail setting, for use with Config::get()
const LocalTZoffset
Name constant for the LocalTZoffset setting, for use with Config::get()
const RCShowWatchingUsers
Name constant for the RCShowWatchingUsers setting, for use with Config::get()
const EnotifUserTalk
Name constant for the EnotifUserTalk setting, for use with Config::get()
const AllowUserJs
Name constant for the AllowUserJs setting, for use with Config::get()
const ImageLimits
Name constant for the ImageLimits setting, for use with Config::get()
const SearchMatchRedirectPreference
Name constant for the SearchMatchRedirectPreference setting, for use with Config::get()
const EnotifMinorEdits
Name constant for the EnotifMinorEdits setting, for use with Config::get()
const ScriptPath
Name constant for the ScriptPath setting, for use with Config::get()
const AllowUserCss
Name constant for the AllowUserCss setting, for use with Config::get()
const AllowRequiringEmailForResets
Name constant for the AllowRequiringEmailForResets setting, for use with Config::get()
const ThumbLimits
Name constant for the ThumbLimits setting, for use with Config::get()
const SecureLogin
Name constant for the SecureLogin setting, for use with Config::get()
const LanguageCode
Name constant for the LanguageCode setting, for use with Config::get()
const SignatureValidation
Name constant for the SignatureValidation setting, for use with Config::get()
const AllowUserCssPrefs
Name constant for the AllowUserCssPrefs setting, for use with Config::get()
const RCWatchCategoryMembership
Name constant for the RCWatchCategoryMembership setting, for use with Config::get()
const ThumbnailNamespaces
Name constant for the ThumbnailNamespaces setting, for use with Config::get()
const EmailAuthentication
Name constant for the EmailAuthentication setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:158
This is one of the Core classes and should be read at least once by any new developers.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:155
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
This is the default implementation of PreferencesFactory.
validateSignature( $signature, $alldata, HTMLForm $form)
rcPreferences(User $user, MessageLocalizer $l10n, &$defaultPreferences)
watchlistPreferences(User $user, IContextSource $context, &$defaultPreferences)
listResetKinds()
Return a list of the types of user options currently returned by getResetKinds().
profilePreferences(User $user, IContextSource $context, &$defaultPreferences)
renderingPreferences(User $user, MessageLocalizer $l10n, &$defaultPreferences)
getForm(User $user, IContextSource $context, $formClass=PreferencesFormOOUI::class, array $remove=[])
getOptionNamesForReset(User $user, IContextSource $context, $kinds)
Get the list of option names which have been saved by the user, thus having non-default values,...
skinPreferences(User $user, IContextSource $context, &$defaultPreferences)
__construct(ServiceOptions $options, Language $contLang, AuthManager $authManager, LinkRenderer $linkRenderer, NamespaceInfo $nsInfo, PermissionManager $permissionManager, ILanguageConverter $languageConverter, LanguageNameUtils $languageNameUtils, HookContainer $hookContainer, UserOptionsLookup $userOptionsLookup, LanguageConverterFactory $languageConverterFactory=null, ParserFactory $parserFactory=null, SkinFactory $skinFactory=null, UserGroupManager $userGroupManager=null, SignatureValidatorFactory $signatureValidatorFactory=null)
generateSkinOptions(User $user, IContextSource $context, array $validSkinNames)
static simplifyFormDescriptor(array $descriptor)
Simplify form descriptor for validation or something similar.
getResetKinds(User $user, IContextSource $context, $options=null)
Return an associative array mapping preferences keys to the kind of a preference they're used for.
editingPreferences(User $user, MessageLocalizer $l10n, &$defaultPreferences)
getOptionFromUser( $name, $info, array $userOptions)
Pull option from a user account.
datetimePreferences(User $user, IContextSource $context, &$defaultPreferences)
cleanSignature( $signature, $alldata, HTMLForm $form)
getSaveBlacklist()
Get the names of preferences that should never be saved (such as 'realname' and 'emailaddress')....
applyFilters(array &$preferences, array $formDescriptor, $verb)
Applies filters to preferences either before or after form usage.
static getPreferenceForField( $name, HTMLFormField $field, array $userOptions)
Get preference values for the 'default' param of html form descriptor, compatible with nested fields.
filesPreferences(IContextSource $context, &$defaultPreferences)
submitForm(array $formData, PreferencesFormOOUI $form, array $formDescriptor)
Save the form data and reload the page.
saveFormData( $formData, PreferencesFormOOUI $form, array $formDescriptor)
Handle the form submission if everything validated properly.
Parent class for all special pages.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
A special page that lists last changes made to the wiki, limited to user-defined list of titles.
static checkStructuredFilterUiEnabled(UserIdentity $user)
Static method to check whether StructuredFilter UI is enabled for the given user.1....
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Represents a title within MediaWiki.
Definition Title.php:79
Provides access to user options.
A service class to control user options.
Represents a "user group membership" – a specific instance of a user belonging to a group.
Utility class to parse the TimeCorrection string value.
internal since 1.36
Definition User.php:93
getRegistration()
Get the timestamp of account creation.
Definition User.php:3090
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition User.php:2198
isAllowedAny(... $permissions)
Checks whether this authority has any of the given permissions in general.
Definition User.php:2182
getEditCount()
Get the user's edit count.
Definition User.php:2119
getRealName()
Get the user's real name.
Definition User.php:2000
getTitleKey()
Get the user's name escaped by underscores.
Definition User.php:1677
getEmailAuthenticationTimestamp()
Get the timestamp of the user's e-mail authentication.
Definition User.php:1916
isAllowed(string $permission, PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
Definition User.php:2190
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition User.php:2208
getEmail()
Get the user's e-mail address.
Definition User.php:1903
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:1568
Module of static functions for generating XML.
Definition Xml.php:37
Set options of the Parser.
Form to edit user preferences.
getExtraSuccessRedirectParameters()
Get extra parameters for the query string when redirecting after successful save.
Factory class to create Skin objects.
$wgDefaultUserOptions
Config variable stub for the DefaultUserOptions setting, for use by phpdoc and IDEs.
Interface for database access objects.
The shared interface for all language converters.
Interface for objects which can provide a MediaWiki context on request.
Base interface for user preference filters that work as a middleware between storage and interface.
Definition Filter.php:27
A PreferencesFactory is a MediaWiki service that provides the definitions of preferences for a given ...
Interface for localizing messages in MediaWiki.
msg( $key,... $params)
This is the method for getting translated interface messages.
element(SerializerNode $parent, SerializerNode $node, $contents)