MediaWiki master
DefaultPreferencesFactory.php
Go to the documentation of this file.
1<?php
22
64use OOUI\ButtonWidget;
65use OOUI\FieldLayout;
66use OOUI\HtmlSnippet;
67use OOUI\LabelWidget;
68use OOUI\MessageWidget;
70use Psr\Log\LoggerAwareTrait;
71use Psr\Log\NullLogger;
72use SkinFactory;
73use UnexpectedValueException;
75
80 use LoggerAwareTrait;
81
83 protected $options;
84
86 protected $contLang;
87
90
92 protected $authManager;
93
95 protected $linkRenderer;
96
98 protected $nsInfo;
99
102
104 private $languageConverter;
105
107 private $hookRunner;
108
111
113 private $languageConverterFactory;
114
116 private $parserFactory;
117
119 private $skinFactory;
120
122 private $userGroupManager;
123
125 private $signatureValidatorFactory;
126
130 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-raw' => $user->getEmail()
575 ? $context->msg( 'prefs-help-yourpassword',
576 '[[#mw-prefsection-personal-email|{{int:prefs-email}}]]' )->parse()
577 : '',
578 'section' => 'personal/info',
579 ];
580 }
581 // Only show prefershttps if secure login is turned on
582 if ( !$this->options->get( MainConfigNames::ForceHTTPS )
583 && $this->options->get( MainConfigNames::SecureLogin )
584 ) {
585 $defaultPreferences['prefershttps'] = [
586 'type' => 'toggle',
587 'label-message' => 'tog-prefershttps',
588 'help-message' => 'prefs-help-prefershttps',
589 'section' => 'personal/info'
590 ];
591 }
592
593 $defaultPreferences['downloaduserdata'] = [
594 'type' => 'info',
595 'raw' => true,
596 'label-message' => 'prefs-user-downloaddata-label',
597 'default' => Html::element(
598 'a',
599 [
600 'href' => $this->options->get( MainConfigNames::ScriptPath ) .
601 '/api.php?action=query&meta=userinfo&uiprop=*&formatversion=2',
602 ],
603 $context->msg( 'prefs-user-downloaddata-info' )->text()
604 ),
605 'help-message' => [ 'prefs-user-downloaddata-help-message', urlencode( $user->getTitleKey() ) ],
606 'section' => 'personal/info',
607 ];
608
609 $defaultPreferences['restoreprefs'] = [
610 'type' => 'info',
611 'raw' => true,
612 'label-message' => 'prefs-user-restoreprefs-label',
613 'default' => Html::element(
614 'a',
615 [
616 'href' => SpecialPage::getTitleFor( 'Preferences' )
617 ->getSubpage( 'reset' )->getLocalURL()
618 ],
619 $context->msg( 'prefs-user-restoreprefs-info' )->text()
620 ),
621 'section' => 'personal/info',
622 ];
623
624 $languages = $this->languageNameUtils->getLanguageNames(
625 LanguageNameUtils::AUTONYMS,
626 LanguageNameUtils::SUPPORTED
627 );
628 $languageCode = $this->options->get( MainConfigNames::LanguageCode );
629 if ( !array_key_exists( $languageCode, $languages ) ) {
630 $languages[$languageCode] = $languageCode;
631 // Sort the array again
632 ksort( $languages );
633 }
634
635 $options = [];
636 foreach ( $languages as $code => $name ) {
637 $display = LanguageCode::bcp47( $code ) . ' - ' . $name;
638 $options[$display] = $code;
639 }
640 $defaultPreferences['language'] = [
641 'type' => 'select',
642 'section' => 'personal/i18n',
643 'options' => $options,
644 'label-message' => 'yourlanguage',
645 ];
646
647 $neutralGenderMessage = $context->msg( 'gender-notknown' )->escaped() . (
648 !$context->msg( 'gender-unknown' )->isDisabled()
649 ? "<br>" . $context->msg( 'parentheses' )
650 ->params( $context->msg( 'gender-unknown' )->plain() )
651 ->escaped()
652 : ''
653 );
654
655 $defaultPreferences['gender'] = [
656 'type' => 'radio',
657 'section' => 'personal/i18n',
658 'options' => [
659 $neutralGenderMessage => 'unknown',
660 $context->msg( 'gender-female' )->escaped() => 'female',
661 $context->msg( 'gender-male' )->escaped() => 'male',
662 ],
663 'label-message' => 'yourgender',
664 'help-message' => 'prefs-help-gender',
665 ];
666
667 // see if there are multiple language variants to choose from
668 if ( !$this->languageConverterFactory->isConversionDisabled() ) {
669
670 foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
671 if ( $langCode == $this->contLang->getCode() ) {
672 if ( !$this->languageConverter->hasVariants() ) {
673 continue;
674 }
675
676 $variants = $this->languageConverter->getVariants();
677 $variantArray = [];
678 foreach ( $variants as $v ) {
679 $v = str_replace( '_', '-', strtolower( $v ) );
680 $variantArray[$v] = $lang->getVariantname( $v, false );
681 }
682
683 $options = [];
684 foreach ( $variantArray as $code => $name ) {
685 $display = LanguageCode::bcp47( $code ) . ' - ' . $name;
686 $options[$display] = $code;
687 }
688
689 $defaultPreferences['variant'] = [
690 'label-message' => 'yourvariant',
691 'type' => 'select',
692 'options' => $options,
693 'section' => 'personal/i18n',
694 'help-message' => 'prefs-help-variant',
695 ];
696 } else {
697 $defaultPreferences["variant-$langCode"] = [
698 'type' => 'api',
699 ];
700 }
701 }
702 }
703
704 // show a preview of the old signature first
705 $oldsigWikiText = $this->parserFactory->getInstance()->preSaveTransform(
706 '~~~',
707 $context->getTitle(),
708 $user,
709 ParserOptions::newFromContext( $context )
710 );
711 $oldsigHTML = Parser::stripOuterParagraph(
712 $context->getOutput()->parseAsContent( $oldsigWikiText )
713 );
714 $signatureFieldConfig = [];
715 // Validate existing signature and show a message about it
716 $signature = $this->userOptionsManager->getOption( $user, 'nickname' );
717 $useFancySig = $this->userOptionsManager->getBoolOption( $user, 'fancysig' );
718 if ( $useFancySig && $signature !== '' ) {
719 $parserOpts = ParserOptions::newFromContext( $context );
720 $validator = $this->signatureValidatorFactory
721 ->newSignatureValidator( $user, $context, $parserOpts );
722 $signatureErrors = $validator->validateSignature( $signature );
723 if ( $signatureErrors ) {
724 $sigValidation = $this->options->get( MainConfigNames::SignatureValidation );
725 $oldsigHTML .= '<p><strong>' .
726 // Messages used here:
727 // * prefs-signature-invalid-warning
728 // * prefs-signature-invalid-new
729 // * prefs-signature-invalid-disallow
730 $context->msg( "prefs-signature-invalid-$sigValidation" )->parse() .
731 '</strong></p>';
732
733 // On initial page load, show the warnings as well
734 // (when posting, you get normal validation errors instead)
735 foreach ( $signatureErrors as &$sigError ) {
736 $sigError = new HtmlSnippet( $sigError );
737 }
738 if ( !$context->getRequest()->wasPosted() ) {
739 $signatureFieldConfig = [
740 'warnings' => $sigValidation !== 'disallow' ? $signatureErrors : null,
741 'errors' => $sigValidation === 'disallow' ? $signatureErrors : null,
742 ];
743 }
744 }
745 }
746 $defaultPreferences['oldsig'] = [
747 'type' => 'info',
748 // Normally HTMLFormFields do not display warnings, so we need to use 'rawrow'
749 // and provide the entire OOUI\FieldLayout here
750 'rawrow' => true,
751 'default' => new FieldLayout(
752 new LabelWidget( [
753 'label' => new HtmlSnippet( $oldsigHTML ),
754 ] ),
755 [
756 'align' => 'top',
757 'label' => new HtmlSnippet( $context->msg( 'tog-oldsig' )->parse() )
758 ] + $signatureFieldConfig
759 ),
760 'section' => 'personal/signature',
761 ];
762 $defaultPreferences['nickname'] = [
763 'type' => $this->authManager->allowsPropertyChange( 'nickname' ) ? 'text' : 'info',
764 'maxlength' => $this->options->get( MainConfigNames::MaxSigChars ),
765 'label-message' => 'yournick',
766 'validation-callback' => function ( $signature, $alldata, HTMLForm $form ) {
767 return $this->validateSignature( $signature, $alldata, $form );
768 },
769 'section' => 'personal/signature',
770 'filter-callback' => function ( $signature, array $alldata, HTMLForm $form ) {
771 return $this->cleanSignature( $signature, $alldata, $form );
772 },
773 ];
774 $defaultPreferences['fancysig'] = [
775 'type' => 'toggle',
776 'label-message' => 'tog-fancysig',
777 // show general help about signature at the bottom of the section
778 'help-message' => 'prefs-help-signature',
779 'section' => 'personal/signature'
780 ];
781
782 // Email preferences
783 if ( $this->options->get( MainConfigNames::EnableEmail ) ) {
784 if ( $canViewPrivateInfo ) {
785 $helpMessages = [];
786 $helpMessages[] = $this->options->get( MainConfigNames::EmailConfirmToEdit )
787 ? 'prefs-help-email-required'
788 : 'prefs-help-email';
789
790 if ( $this->options->get( MainConfigNames::EnableUserEmail ) ) {
791 // additional messages when users can send email to each other
792 $helpMessages[] = 'prefs-help-email-others';
793 }
794
795 $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
796 if ( $canEditPrivateInfo && $this->authManager->allowsPropertyChange( 'emailaddress' ) ) {
797 $button = new ButtonWidget( [
798 'href' => SpecialPage::getTitleFor( 'ChangeEmail' )->getLinkURL( [
799 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
800 ] ),
801 'label' =>
802 $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(),
803 ] );
804
805 $emailAddress .= $emailAddress == '' ? $button : ( '<br />' . $button );
806 }
807
808 $defaultPreferences['emailaddress'] = [
809 'type' => 'info',
810 'raw' => true,
811 'default' => $emailAddress,
812 'label-message' => 'youremail',
813 'section' => 'personal/email',
814 'help-messages' => $helpMessages,
815 // 'cssclass' chosen below
816 ];
817 }
818
819 $disableEmailPrefs = false;
820
821 $defaultPreferences['requireemail'] = [
822 'type' => 'toggle',
823 'label-message' => 'tog-requireemail',
824 'help-message' => 'prefs-help-requireemail',
825 'section' => 'personal/email',
826 'disabled' => !$user->getEmail(),
827 ];
828
829 if ( $this->options->get( MainConfigNames::EmailAuthentication ) ) {
830 if ( $user->getEmail() ) {
831 if ( $user->getEmailAuthenticationTimestamp() ) {
832 // date and time are separate parameters to facilitate localisation.
833 // $time is kept for backward compat reasons.
834 // 'emailauthenticated' is also used in SpecialConfirmemail.php
835 $displayUser = $context->getUser();
836 $emailTimestamp = $user->getEmailAuthenticationTimestamp();
837 $time = $lang->userTimeAndDate( $emailTimestamp, $displayUser );
838 $d = $lang->userDate( $emailTimestamp, $displayUser );
839 $t = $lang->userTime( $emailTimestamp, $displayUser );
840 $emailauthenticated = $context->msg( 'emailauthenticated',
841 $time, $d, $t )->parse() . '<br />';
842 $emailauthenticationclass = 'mw-email-authenticated';
843 } else {
844 $disableEmailPrefs = true;
845 $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
846 new ButtonWidget( [
847 'href' => SpecialPage::getTitleFor( 'Confirmemail' )->getLinkURL(),
848 'label' => $context->msg( 'emailconfirmlink' )->text(),
849 ] );
850 $emailauthenticationclass = "mw-email-not-authenticated";
851 }
852 } else {
853 $disableEmailPrefs = true;
854 $emailauthenticated = $context->msg( 'noemailprefs' )->escaped();
855 $emailauthenticationclass = 'mw-email-none';
856 }
857
858 if ( $canViewPrivateInfo ) {
859 $defaultPreferences['emailauthentication'] = [
860 'type' => 'info',
861 'raw' => true,
862 'section' => 'personal/email',
863 'label-message' => 'prefs-emailconfirm-label',
864 'default' => $emailauthenticated,
865 // Apply the same CSS class used on the input to the message:
866 'cssclass' => $emailauthenticationclass,
867 ];
868 }
869 }
870
871 if ( $this->options->get( MainConfigNames::EnableUserEmail ) &&
872 $user->isAllowed( 'sendemail' )
873 ) {
874 $defaultPreferences['disablemail'] = [
875 'id' => 'wpAllowEmail',
876 'type' => 'toggle',
877 'invert' => true,
878 'section' => 'personal/email',
879 'label-message' => 'allowemail',
880 'disabled' => $disableEmailPrefs,
881 ];
882
883 $defaultPreferences['email-allow-new-users'] = [
884 'id' => 'wpAllowEmailFromNewUsers',
885 'type' => 'toggle',
886 'section' => 'personal/email',
887 'label-message' => 'email-allow-new-users-label',
888 'help-message' => 'prefs-help-email-allow-new-users',
889 'disabled' => $disableEmailPrefs,
890 'disable-if' => [ '!==', 'disablemail', '1' ],
891 ];
892
893 $defaultPreferences['ccmeonemails'] = [
894 'type' => 'toggle',
895 'section' => 'personal/email',
896 'label-message' => 'tog-ccmeonemails',
897 'disabled' => $disableEmailPrefs,
898 ];
899
900 if ( $this->options->get( MainConfigNames::EnableUserEmailMuteList ) ) {
901 $defaultPreferences['email-blacklist'] = [
902 'type' => 'usersmultiselect',
903 'label-message' => 'email-mutelist-label',
904 'section' => 'personal/email',
905 'disabled' => $disableEmailPrefs,
906 'filter' => MultiUsernameFilter::class,
907 'excludetemp' => true,
908 ];
909 }
910 }
911
912 if ( $this->options->get( MainConfigNames::EnotifWatchlist ) ) {
913 $defaultPreferences['enotifwatchlistpages'] = [
914 'type' => 'toggle',
915 'section' => 'personal/email',
916 'label-message' => 'tog-enotifwatchlistpages',
917 'disabled' => $disableEmailPrefs,
918 ];
919 }
920 if ( $this->options->get( MainConfigNames::EnotifUserTalk ) ) {
921 $defaultPreferences['enotifusertalkpages'] = [
922 'type' => 'toggle',
923 'section' => 'personal/email',
924 'label-message' => 'tog-enotifusertalkpages',
925 'disabled' => $disableEmailPrefs,
926 ];
927 }
928 if ( $this->options->get( MainConfigNames::EnotifUserTalk ) ||
929 $this->options->get( MainConfigNames::EnotifWatchlist ) ) {
930 if ( $this->options->get( MainConfigNames::EnotifMinorEdits ) ) {
931 $defaultPreferences['enotifminoredits'] = [
932 'type' => 'toggle',
933 'section' => 'personal/email',
934 'label-message' => 'tog-enotifminoredits',
935 'disabled' => $disableEmailPrefs,
936 ];
937 }
938
939 if ( $this->options->get( MainConfigNames::EnotifRevealEditorAddress ) ) {
940 $defaultPreferences['enotifrevealaddr'] = [
941 'type' => 'toggle',
942 'section' => 'personal/email',
943 'label-message' => 'tog-enotifrevealaddr',
944 'disabled' => $disableEmailPrefs,
945 ];
946 }
947 }
948 }
949 }
950
957 protected function skinPreferences( User $user, IContextSource $context, &$defaultPreferences ) {
958 // Skin selector, if there is at least one valid skin
959 $validSkinNames = $this->getValidSkinNames( $user, $context );
960 if ( $validSkinNames ) {
961 $defaultPreferences['skin'] = [
962 // @phan-suppress-next-line SecurityCheck-XSS False +ve, label is escaped in generateSkinOptions()
963 'type' => 'radio',
964 'options' => $this->generateSkinOptions( $user, $context, $validSkinNames ),
965 'section' => 'rendering/skin',
966 ];
967 $hideCond = [ 'AND' ];
968 foreach ( $validSkinNames as $skinName => $_ ) {
969 $options = $this->skinFactory->getSkinOptions( $skinName );
970 if ( $options['responsive'] ?? false ) {
971 $hideCond[] = [ '!==', 'skin', $skinName ];
972 }
973 }
974 if ( $hideCond === [ 'AND' ] ) {
975 $hideCond = [];
976 }
977 $defaultPreferences['skin-responsive'] = [
978 'type' => 'check',
979 'label-message' => 'prefs-skin-responsive',
980 'section' => 'rendering/skin/skin-prefs',
981 'help-message' => 'prefs-help-skin-responsive',
982 'hide-if' => $hideCond,
983 ];
984 }
985
986 $allowUserCss = $this->options->get( MainConfigNames::AllowUserCss );
987 $allowUserJs = $this->options->get( MainConfigNames::AllowUserJs );
988 $safeMode = $this->userOptionsManager->getOption( $user, 'forcesafemode' );
989 // Create links to user CSS/JS pages for all skins.
990 // This code is basically copied from generateSkinOptions().
991 // @todo Refactor this and the similar code in generateSkinOptions().
992 if ( $allowUserCss || $allowUserJs ) {
993 if ( $safeMode ) {
994 $defaultPreferences['customcssjs-safemode'] = [
995 'type' => 'info',
996 'raw' => true,
997 'rawrow' => true,
998 'section' => 'rendering/skin',
999 'default' => new FieldLayout(
1000 new MessageWidget( [
1001 'label' => new HtmlSnippet( $context->msg( 'prefs-custom-cssjs-safemode' )->parse() ),
1002 'type' => 'warning',
1003 ] )
1004 ),
1005 ];
1006 } else {
1007 $linkTools = [];
1008 $userName = $user->getName();
1009
1010 if ( $allowUserCss ) {
1011 $cssPage = Title::makeTitleSafe( NS_USER, $userName . '/common.css' );
1012 $cssLinkText = $context->msg( 'prefs-custom-css' )->text();
1013 $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
1014 }
1015
1016 if ( $allowUserJs ) {
1017 $jsPage = Title::makeTitleSafe( NS_USER, $userName . '/common.js' );
1018 $jsLinkText = $context->msg( 'prefs-custom-js' )->text();
1019 $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
1020 }
1021
1022 $defaultPreferences['commoncssjs'] = [
1023 'type' => 'info',
1024 'raw' => true,
1025 'default' => $context->getLanguage()->pipeList( $linkTools ),
1026 'label-message' => 'prefs-common-config',
1027 'section' => 'rendering/skin',
1028 ];
1029 }
1030 }
1031 }
1032
1037 protected function filesPreferences( IContextSource $context, &$defaultPreferences ) {
1038 $defaultPreferences['imagesize'] = [
1039 'type' => 'select',
1040 'options' => $this->getImageSizes( $context ),
1041 'label-message' => 'imagemaxsize',
1042 'section' => 'rendering/files',
1043 ];
1044 $defaultPreferences['thumbsize'] = [
1045 'type' => 'select',
1046 'options' => $this->getThumbSizes( $context ),
1047 'label-message' => 'thumbsize',
1048 'section' => 'rendering/files',
1049 ];
1050 }
1051
1058 protected function datetimePreferences(
1059 User $user, IContextSource $context, &$defaultPreferences
1060 ) {
1061 $dateOptions = $this->getDateOptions( $context );
1062 if ( $dateOptions ) {
1063 $defaultPreferences['date'] = [
1064 'type' => 'radio',
1065 'options' => $dateOptions,
1066 'section' => 'rendering/dateformat',
1067 ];
1068 }
1069
1070 // Info
1071 $now = wfTimestampNow();
1072 $lang = $context->getLanguage();
1073 $nowlocal = Xml::element( 'span', [ 'id' => 'wpLocalTime' ],
1074 $lang->userTime( $now, $user ) );
1075 $nowserver = $lang->userTime( $now, $user,
1076 [ 'format' => false, 'timecorrection' => false ] ) .
1077 Html::hidden( 'wpServerTime', (int)substr( $now, 8, 2 ) * 60 + (int)substr( $now, 10, 2 ) );
1078
1079 $defaultPreferences['nowserver'] = [
1080 'type' => 'info',
1081 'raw' => 1,
1082 'label-message' => 'servertime',
1083 'default' => $nowserver,
1084 'section' => 'rendering/timeoffset',
1085 ];
1086
1087 $defaultPreferences['nowlocal'] = [
1088 'type' => 'info',
1089 'raw' => 1,
1090 'label-message' => 'localtime',
1091 'default' => $nowlocal,
1092 'section' => 'rendering/timeoffset',
1093 ];
1094
1095 $userTimeCorrection = (string)$this->userOptionsManager->getOption( $user, 'timecorrection' );
1096 // This value should already be normalized by UserTimeCorrection, so it should always be valid and not
1097 // in the legacy format. However, let's be sure about that and normalize it again.
1098 // Also, recompute the offset because it can change with DST.
1099 $userTimeCorrectionObj = new UserTimeCorrection(
1100 $userTimeCorrection,
1101 null,
1102 $this->options->get( MainConfigNames::LocalTZoffset )
1103 );
1104
1105 if ( $userTimeCorrectionObj->getCorrectionType() === UserTimeCorrection::OFFSET ) {
1106 $tzDefault = UserTimeCorrection::formatTimezoneOffset( $userTimeCorrectionObj->getTimeOffset() );
1107 } else {
1108 $tzDefault = $userTimeCorrectionObj->toString();
1109 }
1110
1111 $defaultPreferences['timecorrection'] = [
1112 'type' => 'timezone',
1113 'label-message' => 'timezonelegend',
1114 'default' => $tzDefault,
1115 'size' => 20,
1116 'section' => 'rendering/timeoffset',
1117 'id' => 'wpTimeCorrection',
1118 'filter' => TimezoneFilter::class,
1119 ];
1120 }
1121
1127 protected function renderingPreferences(
1128 User $user,
1129 MessageLocalizer $l10n,
1130 &$defaultPreferences
1131 ) {
1132 // Diffs
1133 $defaultPreferences['diffonly'] = [
1134 'type' => 'toggle',
1135 'section' => 'rendering/diffs',
1136 'label-message' => 'tog-diffonly',
1137 ];
1138 $defaultPreferences['norollbackdiff'] = [
1139 'type' => 'toggle',
1140 'section' => 'rendering/diffs',
1141 'label-message' => 'tog-norollbackdiff',
1142 ];
1143 $defaultPreferences['diff-type'] = [
1144 'type' => 'api',
1145 ];
1146
1147 // Page Rendering
1148 if ( $this->options->get( MainConfigNames::AllowUserCssPrefs ) ) {
1149 $defaultPreferences['underline'] = [
1150 'type' => 'select',
1151 'options' => [
1152 $l10n->msg( 'underline-never' )->text() => 0,
1153 $l10n->msg( 'underline-always' )->text() => 1,
1154 $l10n->msg( 'underline-default' )->text() => 2,
1155 ],
1156 'label-message' => 'tog-underline',
1157 'section' => 'rendering/advancedrendering',
1158 ];
1159 }
1160
1161 $defaultPreferences['showhiddencats'] = [
1162 'type' => 'toggle',
1163 'section' => 'rendering/advancedrendering',
1164 'label-message' => 'tog-showhiddencats'
1165 ];
1166
1167 if ( $user->isAllowed( 'rollback' ) ) {
1168 $defaultPreferences['showrollbackconfirmation'] = [
1169 'type' => 'toggle',
1170 'section' => 'rendering/advancedrendering',
1171 'label-message' => 'tog-showrollbackconfirmation',
1172 ];
1173 }
1174
1175 $defaultPreferences['forcesafemode'] = [
1176 'type' => 'toggle',
1177 'section' => 'rendering/advancedrendering',
1178 'label-message' => 'tog-forcesafemode',
1179 'help-message' => 'prefs-help-forcesafemode'
1180 ];
1181 }
1182
1188 protected function editingPreferences( User $user, MessageLocalizer $l10n, &$defaultPreferences ) {
1189 $defaultPreferences['editsectiononrightclick'] = [
1190 'type' => 'toggle',
1191 'section' => 'editing/advancedediting',
1192 'label-message' => 'tog-editsectiononrightclick',
1193 ];
1194 $defaultPreferences['editondblclick'] = [
1195 'type' => 'toggle',
1196 'section' => 'editing/advancedediting',
1197 'label-message' => 'tog-editondblclick',
1198 ];
1199
1200 if ( $this->options->get( MainConfigNames::AllowUserCssPrefs ) ) {
1201 $defaultPreferences['editfont'] = [
1202 'type' => 'select',
1203 'section' => 'editing/editor',
1204 'label-message' => 'editfont-style',
1205 'options' => [
1206 $l10n->msg( 'editfont-monospace' )->text() => 'monospace',
1207 $l10n->msg( 'editfont-sansserif' )->text() => 'sans-serif',
1208 $l10n->msg( 'editfont-serif' )->text() => 'serif',
1209 ]
1210 ];
1211 }
1212
1213 if ( $user->isAllowed( 'minoredit' ) ) {
1214 $defaultPreferences['minordefault'] = [
1215 'type' => 'toggle',
1216 'section' => 'editing/editor',
1217 'label-message' => 'tog-minordefault',
1218 ];
1219 }
1220
1221 $defaultPreferences['forceeditsummary'] = [
1222 'type' => 'toggle',
1223 'section' => 'editing/editor',
1224 'label-message' => 'tog-forceeditsummary',
1225 ];
1226
1227 // T350653
1228 if ( $this->options->get( MainConfigNames::EnableEditRecovery ) ) {
1229 $defaultPreferences['editrecovery'] = [
1230 'type' => 'toggle',
1231 'section' => 'editing/editor',
1232 'label-message' => 'tog-editrecovery',
1233 'help-message' => [
1234 'tog-editrecovery-help',
1235 'https://meta.wikimedia.org/wiki/Talk:Community_Wishlist_Survey_2023/Edit-recovery_feature',
1236 ],
1237 ];
1238 }
1239
1240 $defaultPreferences['useeditwarning'] = [
1241 'type' => 'toggle',
1242 'section' => 'editing/editor',
1243 'label-message' => 'tog-useeditwarning',
1244 ];
1245
1246 $defaultPreferences['previewonfirst'] = [
1247 'type' => 'toggle',
1248 'section' => 'editing/preview',
1249 'label-message' => 'tog-previewonfirst',
1250 ];
1251 $defaultPreferences['previewontop'] = [
1252 'type' => 'toggle',
1253 'section' => 'editing/preview',
1254 'label-message' => 'tog-previewontop',
1255 ];
1256 $defaultPreferences['uselivepreview'] = [
1257 'type' => 'toggle',
1258 'section' => 'editing/preview',
1259 'label-message' => 'tog-uselivepreview',
1260 ];
1261 }
1262
1268 protected function rcPreferences( User $user, MessageLocalizer $l10n, &$defaultPreferences ) {
1269 $rcMaxAge = $this->options->get( MainConfigNames::RCMaxAge );
1270 $rcMax = ceil( $rcMaxAge / ( 3600 * 24 ) );
1271 $defaultPreferences['rcdays'] = [
1272 'type' => 'float',
1273 'label-message' => 'recentchangesdays',
1274 'section' => 'rc/displayrc',
1275 'min' => 1 / 24,
1276 'max' => $rcMax,
1277 'help-message' => [ 'recentchangesdays-max', Message::numParam( $rcMax ) ],
1278 ];
1279 $defaultPreferences['rclimit'] = [
1280 'type' => 'int',
1281 'min' => 1,
1282 'max' => 1000,
1283 'label-message' => 'recentchangescount',
1284 'help-message' => 'prefs-help-recentchangescount',
1285 'section' => 'rc/displayrc',
1286 'filter' => IntvalFilter::class,
1287 ];
1288 $defaultPreferences['usenewrc'] = [
1289 'type' => 'toggle',
1290 'label-message' => 'tog-usenewrc',
1291 'section' => 'rc/advancedrc',
1292 ];
1293 $defaultPreferences['hideminor'] = [
1294 'type' => 'toggle',
1295 'label-message' => 'tog-hideminor',
1296 'section' => 'rc/changesrc',
1297 ];
1298 $defaultPreferences['pst-cssjs'] = [
1299 'type' => 'api',
1300 ];
1301 $defaultPreferences['rcfilters-rc-collapsed'] = [
1302 'type' => 'api',
1303 ];
1304 $defaultPreferences['rcfilters-wl-collapsed'] = [
1305 'type' => 'api',
1306 ];
1307 $defaultPreferences['rcfilters-saved-queries'] = [
1308 'type' => 'api',
1309 ];
1310 $defaultPreferences['rcfilters-wl-saved-queries'] = [
1311 'type' => 'api',
1312 ];
1313 // Override RCFilters preferences for RecentChanges 'limit'
1314 $defaultPreferences['rcfilters-limit'] = [
1315 'type' => 'api',
1316 ];
1317 $defaultPreferences['rcfilters-saved-queries-versionbackup'] = [
1318 'type' => 'api',
1319 ];
1320 $defaultPreferences['rcfilters-wl-saved-queries-versionbackup'] = [
1321 'type' => 'api',
1322 ];
1323
1324 if ( $this->options->get( MainConfigNames::RCWatchCategoryMembership ) ) {
1325 $defaultPreferences['hidecategorization'] = [
1326 'type' => 'toggle',
1327 'label-message' => 'tog-hidecategorization',
1328 'section' => 'rc/changesrc',
1329 ];
1330 }
1331
1332 if ( $user->useRCPatrol() ) {
1333 $defaultPreferences['hidepatrolled'] = [
1334 'type' => 'toggle',
1335 'section' => 'rc/changesrc',
1336 'label-message' => 'tog-hidepatrolled',
1337 ];
1338 }
1339
1340 if ( $user->useNPPatrol() ) {
1341 $defaultPreferences['newpageshidepatrolled'] = [
1342 'type' => 'toggle',
1343 'section' => 'rc/changesrc',
1344 'label-message' => 'tog-newpageshidepatrolled',
1345 ];
1346 }
1347
1348 if ( $this->options->get( MainConfigNames::RCShowWatchingUsers ) ) {
1349 $defaultPreferences['shownumberswatching'] = [
1350 'type' => 'toggle',
1351 'section' => 'rc/advancedrc',
1352 'label-message' => 'tog-shownumberswatching',
1353 ];
1354 }
1355
1356 $defaultPreferences['rcenhancedfilters-disable'] = [
1357 'type' => 'toggle',
1358 'section' => 'rc/advancedrc',
1359 'label-message' => 'rcfilters-preference-label',
1360 'help-message' => 'rcfilters-preference-help',
1361 ];
1362 }
1363
1369 protected function watchlistPreferences(
1370 User $user, IContextSource $context, &$defaultPreferences
1371 ) {
1372 $watchlistdaysMax = ceil( $this->options->get( MainConfigNames::RCMaxAge ) / ( 3600 * 24 ) );
1373
1374 if ( $user->isAllowed( 'editmywatchlist' ) ) {
1375 $editWatchlistLinks = '';
1376 $editWatchlistModes = [
1377 'edit' => [ 'subpage' => false, 'flags' => [] ],
1378 'raw' => [ 'subpage' => 'raw', 'flags' => [] ],
1379 'clear' => [ 'subpage' => 'clear', 'flags' => [ 'destructive' ] ],
1380 ];
1381 foreach ( $editWatchlistModes as $mode => $options ) {
1382 // Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear
1383 $editWatchlistLinks .=
1384 new ButtonWidget( [
1385 'href' => SpecialPage::getTitleFor( 'EditWatchlist', $options['subpage'] )->getLinkURL(),
1386 'flags' => $options[ 'flags' ],
1387 'label' => new HtmlSnippet(
1388 $context->msg( "prefs-editwatchlist-{$mode}" )->parse()
1389 ),
1390 ] );
1391 }
1392
1393 $defaultPreferences['editwatchlist'] = [
1394 'type' => 'info',
1395 'raw' => true,
1396 'default' => $editWatchlistLinks,
1397 'label-message' => 'prefs-editwatchlist-label',
1398 'section' => 'watchlist/editwatchlist',
1399 ];
1400 }
1401
1402 $defaultPreferences['watchlistdays'] = [
1403 'type' => 'float',
1404 'min' => 1 / 24,
1405 'max' => $watchlistdaysMax,
1406 'section' => 'watchlist/displaywatchlist',
1407 'help-message' => [ 'prefs-watchlist-days-max', Message::numParam( $watchlistdaysMax ) ],
1408 'label-message' => 'prefs-watchlist-days',
1409 ];
1410 $defaultPreferences['wllimit'] = [
1411 'type' => 'int',
1412 'min' => 1,
1413 'max' => 1000,
1414 'label-message' => 'prefs-watchlist-edits',
1415 'help-message' => 'prefs-watchlist-edits-max',
1416 'section' => 'watchlist/displaywatchlist',
1417 'filter' => IntvalFilter::class,
1418 ];
1419 $defaultPreferences['extendwatchlist'] = [
1420 'type' => 'toggle',
1421 'section' => 'watchlist/advancedwatchlist',
1422 'label-message' => 'tog-extendwatchlist',
1423 ];
1424 $defaultPreferences['watchlisthideminor'] = [
1425 'type' => 'toggle',
1426 'section' => 'watchlist/changeswatchlist',
1427 'label-message' => 'tog-watchlisthideminor',
1428 ];
1429 $defaultPreferences['watchlisthidebots'] = [
1430 'type' => 'toggle',
1431 'section' => 'watchlist/changeswatchlist',
1432 'label-message' => 'tog-watchlisthidebots',
1433 ];
1434 $defaultPreferences['watchlisthideown'] = [
1435 'type' => 'toggle',
1436 'section' => 'watchlist/changeswatchlist',
1437 'label-message' => 'tog-watchlisthideown',
1438 ];
1439 $defaultPreferences['watchlisthideanons'] = [
1440 'type' => 'toggle',
1441 'section' => 'watchlist/changeswatchlist',
1442 'label-message' => 'tog-watchlisthideanons',
1443 ];
1444 $defaultPreferences['watchlisthideliu'] = [
1445 'type' => 'toggle',
1446 'section' => 'watchlist/changeswatchlist',
1447 'label-message' => 'tog-watchlisthideliu',
1448 ];
1449
1451 $defaultPreferences['watchlistreloadautomatically'] = [
1452 'type' => 'toggle',
1453 'section' => 'watchlist/advancedwatchlist',
1454 'label-message' => 'tog-watchlistreloadautomatically',
1455 ];
1456 }
1457
1458 $defaultPreferences['watchlistunwatchlinks'] = [
1459 'type' => 'toggle',
1460 'section' => 'watchlist/advancedwatchlist',
1461 'label-message' => 'tog-watchlistunwatchlinks',
1462 ];
1463
1464 if ( $this->options->get( MainConfigNames::RCWatchCategoryMembership ) ) {
1465 $defaultPreferences['watchlisthidecategorization'] = [
1466 'type' => 'toggle',
1467 'section' => 'watchlist/changeswatchlist',
1468 'label-message' => 'tog-watchlisthidecategorization',
1469 ];
1470 }
1471
1472 if ( $user->useRCPatrol() ) {
1473 $defaultPreferences['watchlisthidepatrolled'] = [
1474 'type' => 'toggle',
1475 'section' => 'watchlist/changeswatchlist',
1476 'label-message' => 'tog-watchlisthidepatrolled',
1477 ];
1478 }
1479
1480 $watchTypes = [
1481 'edit' => 'watchdefault',
1482 'move' => 'watchmoves',
1483 ];
1484
1485 // Kinda hacky
1486 if ( $user->isAllowedAny( 'createpage', 'createtalk' ) ) {
1487 $watchTypes['read'] = 'watchcreations';
1488 }
1489
1490 // Move uncommon actions to end of list
1491 $watchTypes += [
1492 'rollback' => 'watchrollback',
1493 'upload' => 'watchuploads',
1494 'delete' => 'watchdeletion',
1495 ];
1496
1497 foreach ( $watchTypes as $action => $pref ) {
1498 if ( $user->isAllowed( $action ) ) {
1499 // Messages:
1500 // tog-watchdefault, tog-watchmoves, tog-watchdeletion, tog-watchcreations, tog-watchuploads
1501 // tog-watchrollback
1502 $defaultPreferences[$pref] = [
1503 'type' => 'toggle',
1504 'section' => 'watchlist/pageswatchlist',
1505 'label-message' => "tog-$pref",
1506 ];
1507 }
1508 }
1509
1510 $defaultPreferences['watchlisttoken'] = [
1511 'type' => 'api',
1512 ];
1513
1514 $tokenButton = new ButtonWidget( [
1515 'href' => SpecialPage::getTitleFor( 'ResetTokens' )->getLinkURL( [
1516 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
1517 ] ),
1518 'label' => $context->msg( 'prefs-watchlist-managetokens' )->text(),
1519 ] );
1520 $defaultPreferences['watchlisttoken-info'] = [
1521 'type' => 'info',
1522 'section' => 'watchlist/tokenwatchlist',
1523 'label-message' => 'prefs-watchlist-token',
1524 'help-message' => 'prefs-help-tokenmanagement',
1525 'raw' => true,
1526 'default' => (string)$tokenButton,
1527 ];
1528
1529 $defaultPreferences['wlenhancedfilters-disable'] = [
1530 'type' => 'toggle',
1531 'section' => 'watchlist/advancedwatchlist',
1532 'label-message' => 'rcfilters-watchlist-preference-label',
1533 'help-message' => 'rcfilters-watchlist-preference-help',
1534 ];
1535 }
1536
1541 protected function searchPreferences( $context, &$defaultPreferences ) {
1542 $defaultPreferences['search-special-page'] = [
1543 'type' => 'api',
1544 ];
1545
1546 foreach ( $this->nsInfo->getValidNamespaces() as $n ) {
1547 $defaultPreferences['searchNs' . $n] = [
1548 'type' => 'api',
1549 ];
1550 }
1551
1552 if ( $this->options->get( MainConfigNames::SearchMatchRedirectPreference ) ) {
1553 $defaultPreferences['search-match-redirect'] = [
1554 'type' => 'toggle',
1555 'section' => 'searchoptions/searchmisc',
1556 'label-message' => 'search-match-redirect-label',
1557 'help-message' => 'search-match-redirect-help',
1558 ];
1559 } else {
1560 $defaultPreferences['search-match-redirect'] = [
1561 'type' => 'api',
1562 ];
1563 }
1564
1565 $defaultPreferences['searchlimit'] = [
1566 'type' => 'int',
1567 'min' => 1,
1568 'max' => 500,
1569 'section' => 'searchoptions/searchmisc',
1570 'label-message' => 'searchlimit-label',
1571 'help-message' => $context->msg( 'searchlimit-help', 500 ),
1572 'filter' => IntvalFilter::class,
1573 ];
1574
1575 // show a preference for thumbnails from namespaces other than NS_FILE,
1576 // only when there they're actually configured to be served
1577 $thumbNamespaces = $this->options->get( MainConfigNames::ThumbnailNamespaces );
1578 $thumbNamespacesFormatted = array_combine(
1579 $thumbNamespaces,
1580 array_map(
1581 static function ( $namespaceId ) use ( $context ) {
1582 return $namespaceId === NS_MAIN
1583 ? $context->msg( 'blanknamespace' )->escaped()
1584 : $context->getLanguage()->getFormattedNsText( $namespaceId );
1585 },
1586 $thumbNamespaces
1587 )
1588 );
1589 $defaultThumbNamespacesFormatted =
1590 array_intersect_key( $thumbNamespacesFormatted, [ NS_FILE => 1 ] ) ?? [];
1591 $extraThumbNamespacesFormatted =
1592 array_diff_key( $thumbNamespacesFormatted, [ NS_FILE => 1 ] );
1593 if ( $extraThumbNamespacesFormatted ) {
1594 $defaultPreferences['search-thumbnail-extra-namespaces'] = [
1595 'type' => 'toggle',
1596 'section' => 'searchoptions/searchmisc',
1597 'label-message' => 'search-thumbnail-extra-namespaces-label',
1598 'help-message' => $context->msg(
1599 'search-thumbnail-extra-namespaces-message',
1600 $context->getLanguage()->listToText( $extraThumbNamespacesFormatted ),
1601 count( $extraThumbNamespacesFormatted ),
1602 $context->getLanguage()->listToText( $defaultThumbNamespacesFormatted ),
1603 count( $defaultThumbNamespacesFormatted )
1604 ),
1605 ];
1606 }
1607 }
1608
1609 /*
1610 * Custom skin string comparison function that takes into account current and preferred skins.
1611 *
1612 * @param string $a
1613 * @param string $b
1614 * @param string $currentSkin
1615 * @param array $preferredSkins
1616 * @return int
1617 */
1618 private static function sortSkinNames( $a, $b, $currentSkin, $preferredSkins ) {
1619 // Display the current skin first in the list
1620 if ( strcasecmp( $a, $currentSkin ) === 0 ) {
1621 return -1;
1622 }
1623 if ( strcasecmp( $b, $currentSkin ) === 0 ) {
1624 return 1;
1625 }
1626 // Display preferred skins over other skins
1627 if ( count( $preferredSkins ) ) {
1628 $aPreferred = array_search( $a, $preferredSkins );
1629 $bPreferred = array_search( $b, $preferredSkins );
1630 // Cannot use ! operator because array_search returns the
1631 // index of the array item if found (i.e. 0) and false otherwise
1632 if ( $aPreferred !== false && $bPreferred === false ) {
1633 return -1;
1634 }
1635 if ( $aPreferred === false && $bPreferred !== false ) {
1636 return 1;
1637 }
1638 // When both skins are preferred, default to the ordering
1639 // specified by the preferred skins config array
1640 if ( $aPreferred !== false && $bPreferred !== false ) {
1641 return strcasecmp( $aPreferred, $bPreferred );
1642 }
1643 }
1644 // Use normal string comparison if both strings are not preferred
1645 return strcasecmp( $a, $b );
1646 }
1647
1656 private function getValidSkinNames( User $user, IContextSource $context ) {
1657 // Only show skins that aren't disabled
1658 $validSkinNames = $this->skinFactory->getAllowedSkins();
1659 $allInstalledSkins = $this->skinFactory->getInstalledSkins();
1660
1661 // Display the installed skin the user has specifically requested via useskin=….
1662 $useSkin = $context->getRequest()->getRawVal( 'useskin' );
1663 if ( $useSkin !== null && isset( $allInstalledSkins[$useSkin] )
1664 && $context->msg( "skinname-$useSkin" )->exists()
1665 ) {
1666 $validSkinNames[$useSkin] = $useSkin;
1667 }
1668
1669 // Display the skin if the user has set it as a preference already before it was hidden.
1670 $currentUserSkin = $this->userOptionsManager->getOption( $user, 'skin' );
1671 if ( isset( $allInstalledSkins[$currentUserSkin] )
1672 && $context->msg( "skinname-$currentUserSkin" )->exists()
1673 ) {
1674 $validSkinNames[$currentUserSkin] = $currentUserSkin;
1675 }
1676
1677 foreach ( $validSkinNames as $skinkey => &$skinname ) {
1678 $msg = $context->msg( "skinname-{$skinkey}" );
1679 if ( $msg->exists() ) {
1680 $skinname = htmlspecialchars( $msg->text() );
1681 }
1682 }
1683
1684 $preferredSkins = $this->options->get( MainConfigNames::SkinsPreferred );
1685 // Sort by the internal name, so that the ordering is the same for each display language,
1686 // especially if some skin names are translated to use a different alphabet and some are not.
1687 uksort( $validSkinNames, function ( $a, $b ) use ( $currentUserSkin, $preferredSkins ) {
1688 return $this->sortSkinNames( $a, $b, $currentUserSkin, $preferredSkins );
1689 } );
1690
1691 return $validSkinNames;
1692 }
1693
1700 protected function generateSkinOptions( User $user, IContextSource $context, array $validSkinNames ) {
1701 $ret = [];
1702
1703 $mptitle = Title::newMainPage();
1704 $previewtext = $context->msg( 'skin-preview' )->escaped();
1705 $defaultSkin = $this->options->get( MainConfigNames::DefaultSkin );
1706 $allowUserCss = $this->options->get( MainConfigNames::AllowUserCss );
1707 $allowUserJs = $this->options->get( MainConfigNames::AllowUserJs );
1708 $safeMode = $this->userOptionsManager->getOption( $user, 'forcesafemode' );
1709 $foundDefault = false;
1710 foreach ( $validSkinNames as $skinkey => $sn ) {
1711 $linkTools = [];
1712
1713 // Mark the default skin
1714 if ( strcasecmp( $skinkey, $defaultSkin ) === 0 ) {
1715 $linkTools[] = $context->msg( 'default' )->escaped();
1716 $foundDefault = true;
1717 }
1718
1719 // Create talk page link if relevant message exists.
1720 $talkPageMsg = $context->msg( "$skinkey-prefs-talkpage" );
1721 if ( $talkPageMsg->exists() ) {
1722 $linkTools[] = $talkPageMsg->parse();
1723 }
1724
1725 // Create preview link
1726 $mplink = htmlspecialchars( $mptitle->getLocalURL( [ 'useskin' => $skinkey ] ) );
1727 $linkTools[] = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
1728
1729 if ( !$safeMode ) {
1730 // Create links to user CSS/JS pages
1731 // @todo Refactor this and the similar code in skinPreferences().
1732 if ( $allowUserCss ) {
1733 $cssPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.css' );
1734 $cssLinkText = $context->msg( 'prefs-custom-css' )->text();
1735 $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
1736 }
1737
1738 if ( $allowUserJs ) {
1739 $jsPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.js' );
1740 $jsLinkText = $context->msg( 'prefs-custom-js' )->text();
1741 $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
1742 }
1743 }
1744
1745 $display = $sn . ' ' . $context->msg( 'parentheses' )
1746 ->rawParams( $context->getLanguage()->pipeList( $linkTools ) )
1747 ->escaped();
1748 $ret[$display] = $skinkey;
1749 }
1750
1751 if ( !$foundDefault ) {
1752 // If the default skin is not available, things are going to break horribly because the
1753 // default value for skin selector will not be a valid value. Let's just not show it then.
1754 return [];
1755 }
1756
1757 return $ret;
1758 }
1759
1764 protected function getDateOptions( IContextSource $context ) {
1765 $lang = $context->getLanguage();
1766 $dateopts = $lang->getDatePreferences();
1767
1768 $ret = [];
1769
1770 if ( $dateopts ) {
1771 if ( !in_array( 'default', $dateopts ) ) {
1772 $dateopts[] = 'default'; // Make sure default is always valid T21237
1773 }
1774
1775 // FIXME KLUGE: site default might not be valid for user language
1776 global $wgDefaultUserOptions;
1777 if ( !in_array( $wgDefaultUserOptions['date'], $dateopts ) ) {
1778 $wgDefaultUserOptions['date'] = 'default';
1779 }
1780
1781 $epoch = wfTimestampNow();
1782 foreach ( $dateopts as $key ) {
1783 if ( $key == 'default' ) {
1784 $formatted = $context->msg( 'datedefault' )->escaped();
1785 } else {
1786 $formatted = htmlspecialchars( $lang->timeanddate( $epoch, false, $key ) );
1787 }
1788 $ret[$formatted] = $key;
1789 }
1790 }
1791 return $ret;
1792 }
1793
1798 protected function getImageSizes( MessageLocalizer $l10n ) {
1799 $ret = [];
1800 $pixels = $l10n->msg( 'unit-pixel' )->text();
1801
1802 foreach ( $this->options->get( MainConfigNames::ImageLimits ) as $index => $limits ) {
1803 // Note: A left-to-right marker (U+200E) is inserted, see T144386
1804 $display = "{$limits[0]}\u{200E}×{$limits[1]}$pixels";
1805 $ret[$display] = $index;
1806 }
1807
1808 return $ret;
1809 }
1810
1815 protected function getThumbSizes( MessageLocalizer $l10n ) {
1816 $ret = [];
1817 $pixels = $l10n->msg( 'unit-pixel' )->text();
1818
1819 foreach ( $this->options->get( MainConfigNames::ThumbLimits ) as $index => $size ) {
1820 $display = $size . $pixels;
1821 $ret[$display] = $index;
1822 }
1823
1824 return $ret;
1825 }
1826
1833 protected function validateSignature( $signature, $alldata, HTMLForm $form ) {
1834 $sigValidation = $this->options->get( MainConfigNames::SignatureValidation );
1835 $maxSigChars = $this->options->get( MainConfigNames::MaxSigChars );
1836 if ( is_string( $signature ) && mb_strlen( $signature ) > $maxSigChars ) {
1837 return $form->msg( 'badsiglength' )->numParams( $maxSigChars )->escaped();
1838 }
1839
1840 if ( $signature === null || $signature === '' ) {
1841 // Make sure leaving the field empty is valid, since that's used as the default (T288151).
1842 // Code using this preference in Parser::getUserSig() handles this case specially.
1843 return true;
1844 }
1845
1846 // Remaining checks only apply to fancy signatures
1847 if ( !( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) ) {
1848 return true;
1849 }
1850
1851 // HERE BE DRAGONS:
1852 //
1853 // If this value is already saved as the user's signature, treat it as valid, even if it
1854 // would be invalid to save now, and even if $wgSignatureValidation is set to 'disallow'.
1855 //
1856 // It can become invalid when we introduce new validation, or when the value just transcludes
1857 // some page containing the real signature and that page is edited (which we can't validate),
1858 // or when someone's username is changed.
1859 //
1860 // Otherwise it would be completely removed when the user opens their preferences page, which
1861 // would be very unfriendly.
1862 $user = $form->getUser();
1863 if (
1864 $signature === $this->userOptionsManager->getOption( $user, 'nickname' ) &&
1865 (bool)$alldata['fancysig'] === $this->userOptionsManager->getBoolOption( $user, 'fancysig' )
1866 ) {
1867 return true;
1868 }
1869
1870 if ( $sigValidation === 'new' || $sigValidation === 'disallow' ) {
1871 // Validate everything
1872 $parserOpts = ParserOptions::newFromContext( $form->getContext() );
1873 $validator = $this->signatureValidatorFactory
1874 ->newSignatureValidator( $user, $form->getContext(), $parserOpts );
1875 $errors = $validator->validateSignature( $signature );
1876 if ( $errors ) {
1877 return $errors;
1878 }
1879 }
1880
1881 // Quick check for mismatched HTML tags in the input.
1882 // Note that this is easily fooled by wikitext templates or bold/italic markup.
1883 // We're only keeping this until Parsoid is integrated and guaranteed to be available.
1884 if ( $this->parserFactory->getInstance()->validateSig( $signature ) === false ) {
1885 return $form->msg( 'badsig' )->escaped();
1886 }
1887
1888 return true;
1889 }
1890
1897 protected function cleanSignature( $signature, $alldata, HTMLForm $form ) {
1898 if ( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) {
1899 $signature = $this->parserFactory->getInstance()->cleanSig( $signature );
1900 } else {
1901 // When no fancy sig used, make sure ~{3,5} get removed.
1902 $signature = Parser::cleanSigInSig( $signature );
1903 }
1904
1905 return $signature;
1906 }
1907
1915 public function getForm(
1916 User $user,
1917 IContextSource $context,
1918 $formClass = PreferencesFormOOUI::class,
1919 array $remove = []
1920 ) {
1921 // We use ButtonWidgets in some of the getPreferences() functions
1922 $context->getOutput()->enableOOUI();
1923
1924 // Note that the $user parameter of getFormDescriptor() is deprecated.
1925 $formDescriptor = $this->getFormDescriptor( $user, $context );
1926 if ( count( $remove ) ) {
1927 $removeKeys = array_fill_keys( $remove, true );
1928 $formDescriptor = array_diff_key( $formDescriptor, $removeKeys );
1929 }
1930
1931 // Remove type=api preferences. They are not intended for rendering in the form.
1932 foreach ( $formDescriptor as $name => $info ) {
1933 if ( isset( $info['type'] ) && $info['type'] === 'api' ) {
1934 unset( $formDescriptor[$name] );
1935 }
1936 }
1937
1941 $htmlForm = new $formClass( $formDescriptor, $context, 'prefs' );
1942
1943 // This allows users to opt-in to hidden skins. While this should be discouraged and is not
1944 // discoverable, this allows users to still use hidden skins while preventing new users from
1945 // adopting unsupported skins. If no useskin=… parameter was provided, it will not show up
1946 // in the resulting URL.
1947 $htmlForm->setAction( $context->getTitle()->getLocalURL( [
1948 'useskin' => $context->getRequest()->getRawVal( 'useskin' )
1949 ] ) );
1950
1951 $htmlForm->setModifiedUser( $user );
1952 $htmlForm->setOptionsEditable( $user->isAllowed( 'editmyoptions' ) );
1953 $htmlForm->setPrivateInfoEditable( $user->isAllowed( 'editmyprivateinfo' ) );
1954 $htmlForm->setId( 'mw-prefs-form' );
1955 $htmlForm->setAutocomplete( 'off' );
1956 $htmlForm->setSubmitTextMsg( 'saveprefs' );
1957 // Used message keys: 'accesskey-preferences-save', 'tooltip-preferences-save'
1958 $htmlForm->setSubmitTooltip( 'preferences-save' );
1959 $htmlForm->setSubmitID( 'prefcontrol' );
1960 $htmlForm->setSubmitCallback(
1961 function ( array $formData, PreferencesFormOOUI $form ) use ( $formDescriptor ) {
1962 return $this->submitForm( $formData, $form, $formDescriptor );
1963 }
1964 );
1965
1966 return $htmlForm;
1967 }
1968
1977 protected function saveFormData( $formData, PreferencesFormOOUI $form, array $formDescriptor ) {
1978 $user = $form->getModifiedUser();
1979 $hiddenPrefs = $this->options->get( MainConfigNames::HiddenPrefs );
1980 $result = true;
1981
1982 if ( !$user->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
1983 return Status::newFatal( 'mypreferencesprotected' );
1984 }
1985
1986 // Filter input
1987 $this->applyFilters( $formData, $formDescriptor, 'filterFromForm' );
1988
1989 // Fortunately, the realname field is MUCH simpler
1990 // (not really "private", but still shouldn't be edited without permission)
1991
1992 if ( !in_array( 'realname', $hiddenPrefs )
1993 && $user->isAllowed( 'editmyprivateinfo' )
1994 && array_key_exists( 'realname', $formData )
1995 ) {
1996 $realName = $formData['realname'];
1997 $user->setRealName( $realName );
1998 }
1999
2000 if ( $user->isAllowed( 'editmyoptions' ) ) {
2001 $oldUserOptions = $this->userOptionsManager->getOptions( $user );
2002
2003 foreach ( $this->getSaveBlacklist() as $b ) {
2004 unset( $formData[$b] );
2005 }
2006
2007 // If users have saved a value for a preference which has subsequently been disabled
2008 // via $wgHiddenPrefs, we don't want to destroy that setting in case the preference
2009 // is subsequently re-enabled
2010 foreach ( $hiddenPrefs as $pref ) {
2011 // If the user has not set a non-default value here, the default will be returned
2012 // and subsequently discarded
2013 $formData[$pref] = $this->userOptionsManager->getOption( $user, $pref, null, true );
2014 }
2015
2016 // If the user changed the rclimit preference, also change the rcfilters-rclimit preference
2017 if (
2018 isset( $formData['rclimit'] ) &&
2019 intval( $formData[ 'rclimit' ] ) !== $this->userOptionsManager->getIntOption( $user, 'rclimit' )
2020 ) {
2021 $formData['rcfilters-limit'] = $formData['rclimit'];
2022 }
2023
2024 // Keep old preferences from interfering due to back-compat code, etc.
2025 $optionsToReset = $this->getOptionNamesForReset( $user, $form->getContext(), 'unused' );
2026 $this->userOptionsManager->resetOptionsByName( $user, $optionsToReset );
2027
2028 foreach ( $formData as $key => $value ) {
2029 // If we're creating a new local override, we need to explicitly pass
2030 // GLOBAL_OVERRIDE to setOption(), otherwise the update would be ignored
2031 // due to the conflicting global option.
2032 $except = !empty( $formData[$key . UserOptionsLookup::LOCAL_EXCEPTION_SUFFIX] );
2033 $this->userOptionsManager->setOption( $user, $key, $value,
2034 $except ? UserOptionsManager::GLOBAL_OVERRIDE : UserOptionsManager::GLOBAL_IGNORE );
2035 }
2036
2037 $this->hookRunner->onPreferencesFormPreSave(
2038 $formData, $form, $user, $result, $oldUserOptions );
2039 }
2040
2041 $user->saveSettings();
2042
2043 return $result;
2044 }
2045
2054 protected function applyFilters( array &$preferences, array $formDescriptor, $verb ) {
2055 foreach ( $formDescriptor as $preference => $desc ) {
2056 if ( !isset( $desc['filter'] ) || !isset( $preferences[$preference] ) ) {
2057 continue;
2058 }
2059 $filterDesc = $desc['filter'];
2060 if ( $filterDesc instanceof Filter ) {
2061 $filter = $filterDesc;
2062 } elseif ( class_exists( $filterDesc ) ) {
2063 $filter = new $filterDesc();
2064 } elseif ( is_callable( $filterDesc ) ) {
2065 $filter = $filterDesc();
2066 } else {
2067 throw new UnexpectedValueException(
2068 "Unrecognized filter type for preference '$preference'"
2069 );
2070 }
2071 $preferences[$preference] = $filter->$verb( $preferences[$preference] );
2072 }
2073 }
2074
2083 protected function submitForm(
2084 array $formData,
2085 PreferencesFormOOUI $form,
2086 array $formDescriptor
2087 ) {
2088 $res = $this->saveFormData( $formData, $form, $formDescriptor );
2089
2090 if ( $res === true ) {
2091 $context = $form->getContext();
2092 $urlOptions = [];
2093
2094 $urlOptions += $form->getExtraSuccessRedirectParameters();
2095
2096 $url = $form->getTitle()->getFullURL( $urlOptions );
2097
2098 // Set session data for the success message
2099 $context->getRequest()->getSession()->set( 'specialPreferencesSaveSuccess', 1 );
2100
2101 $context->getOutput()->redirect( $url );
2102 }
2103
2104 return ( $res === true ? Status::newGood() : $res );
2105 }
2106
2107 public function getResetKinds(
2108 User $user, IContextSource $context, $options = null
2109 ): array {
2110 $options ??= $this->userOptionsManager->loadUserOptions( $user );
2111
2112 $prefs = $this->getFormDescriptor( $user, $context );
2113 $mapping = [];
2114
2115 // Pull out the "special" options, so they don't get converted as
2116 // multiselect or checkmatrix.
2117 $specialOptions = array_fill_keys( $this->getSaveBlacklist(), true );
2118 foreach ( $specialOptions as $name => $value ) {
2119 unset( $prefs[$name] );
2120 }
2121
2122 // Multiselect and checkmatrix options are stored in the database with
2123 // one key per option, each having a boolean value. Extract those keys.
2124 $multiselectOptions = [];
2125 foreach ( $prefs as $name => $info ) {
2126 if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
2127 // Checking old alias for compatibility with unchanged extensions
2128 ( isset( $info['class'] ) && $info['class'] === \HTMLMultiSelectField::class ) ||
2129 ( isset( $info['class'] ) && $info['class'] === HTMLMultiSelectField::class )
2130 ) {
2131 $opts = HTMLFormField::flattenOptions( $info['options'] ?? $info['options-messages'] );
2132 $prefix = $info['prefix'] ?? $name;
2133
2134 foreach ( $opts as $value ) {
2135 $multiselectOptions["$prefix$value"] = true;
2136 }
2137
2138 unset( $prefs[$name] );
2139 }
2140 }
2141 $checkmatrixOptions = [];
2142 foreach ( $prefs as $name => $info ) {
2143 if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
2144 // Checking old alias for compatibility with unchanged extensions
2145 ( isset( $info['class'] ) && $info['class'] === \HTMLCheckMatrix::class ) ||
2146 ( isset( $info['class'] ) && $info['class'] === HTMLCheckMatrix::class )
2147 ) {
2148 $columns = HTMLFormField::flattenOptions( $info['columns'] );
2149 $rows = HTMLFormField::flattenOptions( $info['rows'] );
2150 $prefix = $info['prefix'] ?? $name;
2151
2152 foreach ( $columns as $column ) {
2153 foreach ( $rows as $row ) {
2154 $checkmatrixOptions["$prefix$column-$row"] = true;
2155 }
2156 }
2157
2158 unset( $prefs[$name] );
2159 }
2160 }
2161
2162 // $value is ignored
2163 foreach ( $options as $key => $value ) {
2164 if ( isset( $prefs[$key] ) ) {
2165 $mapping[$key] = 'registered';
2166 } elseif ( isset( $multiselectOptions[$key] ) ) {
2167 $mapping[$key] = 'registered-multiselect';
2168 } elseif ( isset( $checkmatrixOptions[$key] ) ) {
2169 $mapping[$key] = 'registered-checkmatrix';
2170 } elseif ( isset( $specialOptions[$key] ) ) {
2171 $mapping[$key] = 'special';
2172 } elseif ( str_starts_with( $key, 'userjs-' ) ) {
2173 $mapping[$key] = 'userjs';
2174 } elseif ( str_starts_with( $key, UserOptionsLookup::LOCAL_EXCEPTION_SUFFIX ) ) {
2175 $mapping[$key] = 'local-exception';
2176 } else {
2177 $mapping[$key] = 'unused';
2178 }
2179 }
2180
2181 return $mapping;
2182 }
2183
2184 public function listResetKinds() {
2185 return [
2186 'registered',
2187 'registered-multiselect',
2188 'registered-checkmatrix',
2189 'userjs',
2190 'special',
2191 'unused'
2192 ];
2193 }
2194
2195 public function getOptionNamesForReset( User $user, IContextSource $context, $kinds ) {
2196 $oldOptions = $this->userOptionsManager->loadUserOptions( $user, IDBAccessObject::READ_LATEST );
2197
2198 if ( !is_array( $kinds ) ) {
2199 $kinds = [ $kinds ];
2200 }
2201
2202 if ( in_array( 'all', $kinds ) ) {
2203 return array_keys( $oldOptions );
2204 } else {
2205 $optionKinds = $this->getResetKinds( $user, $context );
2206 $kinds = array_intersect( $kinds, $this->listResetKinds() );
2207 $optionNames = [];
2208
2209 foreach ( $oldOptions as $key => $value ) {
2210 if ( in_array( $optionKinds[$key], $kinds ) ) {
2211 $optionNames[] = $key;
2212 }
2213 }
2214 return $optionNames;
2215 }
2216 }
2217}
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.
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:210
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
Methods for dealing with language codes.
Base class for multi-variant language conversion.
Base class for language-specific code.
Definition Language.php:82
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 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:155
This is one of the Core classes and should be read at least once by any new developers.
Set options of the Parser.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:147
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:78
Provides access to user options.
A service class to control user options.
Manage user group memberships.
Represents the membership of one user in one user group.
Utility class to parse the TimeCorrection string value.
User class for the MediaWiki software.
Definition User.php:120
isAllowed(string $permission, ?PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
Definition User.php:2213
getRegistration()
Get the timestamp of account creation.
Definition User.php:3121
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition User.php:2221
isAllowedAny(... $permissions)
Checks whether this authority has any of the given permissions in general.
Definition User.php:2205
getEditCount()
Get the user's edit count.
Definition User.php:2142
getRealName()
Get the user's real name.
Definition User.php:2023
getTitleKey()
Get the user's name escaped by underscores.
Definition User.php:1700
getEmailAuthenticationTimestamp()
Get the timestamp of the user's e-mail authentication.
Definition User.php:1939
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition User.php:2231
getEmail()
Get the user's e-mail address.
Definition User.php:1926
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:1608
Module of static functions for generating XML.
Definition Xml.php:37
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 objects which can provide a MediaWiki context on request.
The shared interface for all language converters.
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.
Interface for database access objects.
element(SerializerNode $parent, SerializerNode $node, $contents)