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' => $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 '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 = Xml::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.
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: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: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:80
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:145
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.
Represents a "user group membership" – a specific instance of a user belonging to a group.
Utility class to parse the TimeCorrection string value.
internal since 1.36
Definition User.php:93
isAllowed(string $permission, ?PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
Definition User.php:2171
getRegistration()
Get the timestamp of account creation.
Definition User.php:3081
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition User.php:2179
isAllowedAny(... $permissions)
Checks whether this authority has any of the given permissions in general.
Definition User.php:2163
getEditCount()
Get the user's edit count.
Definition User.php:2100
getRealName()
Get the user's real name.
Definition User.php:1981
getTitleKey()
Get the user's name escaped by underscores.
Definition User.php:1658
getEmailAuthenticationTimestamp()
Get the timestamp of the user's e-mail authentication.
Definition User.php:1897
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition User.php:2189
getEmail()
Get the user's e-mail address.
Definition User.php:1884
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:1566
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)