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