MediaWiki 1.43.1
DefaultPreferencesFactory.php
Go to the documentation of this file.
1<?php
22
64use OOUI\ButtonWidget;
65use OOUI\FieldLayout;
66use OOUI\HtmlSnippet;
67use OOUI\LabelWidget;
69use Psr\Log\LoggerAwareTrait;
70use Psr\Log\NullLogger;
71use SkinFactory;
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' => $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 'default' => Html::warningBox( $context->msg( 'prefs-custom-cssjs-safemode' )->parse() ),
997 'section' => 'rendering/skin',
998 ];
999 } else {
1000 $linkTools = [];
1001 $userName = $user->getName();
1002
1003 if ( $allowUserCss ) {
1004 $cssPage = Title::makeTitleSafe( NS_USER, $userName . '/common.css' );
1005 $cssLinkText = $context->msg( 'prefs-custom-css' )->text();
1006 $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
1007 }
1008
1009 if ( $allowUserJs ) {
1010 $jsPage = Title::makeTitleSafe( NS_USER, $userName . '/common.js' );
1011 $jsLinkText = $context->msg( 'prefs-custom-js' )->text();
1012 $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
1013 }
1014
1015 $defaultPreferences['commoncssjs'] = [
1016 'type' => 'info',
1017 'raw' => true,
1018 'default' => $context->getLanguage()->pipeList( $linkTools ),
1019 'label-message' => 'prefs-common-config',
1020 'section' => 'rendering/skin',
1021 ];
1022 }
1023 }
1024 }
1025
1030 protected function filesPreferences( IContextSource $context, &$defaultPreferences ) {
1031 $defaultPreferences['imagesize'] = [
1032 'type' => 'select',
1033 'options' => $this->getImageSizes( $context ),
1034 'label-message' => 'imagemaxsize',
1035 'section' => 'rendering/files',
1036 ];
1037 $defaultPreferences['thumbsize'] = [
1038 'type' => 'select',
1039 'options' => $this->getThumbSizes( $context ),
1040 'label-message' => 'thumbsize',
1041 'section' => 'rendering/files',
1042 ];
1043 }
1044
1051 protected function datetimePreferences(
1052 User $user, IContextSource $context, &$defaultPreferences
1053 ) {
1054 $dateOptions = $this->getDateOptions( $context );
1055 if ( $dateOptions ) {
1056 $defaultPreferences['date'] = [
1057 'type' => 'radio',
1058 'options' => $dateOptions,
1059 'section' => 'rendering/dateformat',
1060 ];
1061 }
1062
1063 // Info
1064 $now = wfTimestampNow();
1065 $lang = $context->getLanguage();
1066 $nowlocal = Xml::element( 'span', [ 'id' => 'wpLocalTime' ],
1067 $lang->userTime( $now, $user ) );
1068 $nowserver = $lang->userTime( $now, $user,
1069 [ 'format' => false, 'timecorrection' => false ] ) .
1070 Html::hidden( 'wpServerTime', (int)substr( $now, 8, 2 ) * 60 + (int)substr( $now, 10, 2 ) );
1071
1072 $defaultPreferences['nowserver'] = [
1073 'type' => 'info',
1074 'raw' => 1,
1075 'label-message' => 'servertime',
1076 'default' => $nowserver,
1077 'section' => 'rendering/timeoffset',
1078 ];
1079
1080 $defaultPreferences['nowlocal'] = [
1081 'type' => 'info',
1082 'raw' => 1,
1083 'label-message' => 'localtime',
1084 'default' => $nowlocal,
1085 'section' => 'rendering/timeoffset',
1086 ];
1087
1088 $userTimeCorrection = (string)$this->userOptionsManager->getOption( $user, 'timecorrection' );
1089 // This value should already be normalized by UserTimeCorrection, so it should always be valid and not
1090 // in the legacy format. However, let's be sure about that and normalize it again.
1091 // Also, recompute the offset because it can change with DST.
1092 $userTimeCorrectionObj = new UserTimeCorrection(
1093 $userTimeCorrection,
1094 null,
1095 $this->options->get( MainConfigNames::LocalTZoffset )
1096 );
1097
1098 if ( $userTimeCorrectionObj->getCorrectionType() === UserTimeCorrection::OFFSET ) {
1099 $tzDefault = UserTimeCorrection::formatTimezoneOffset( $userTimeCorrectionObj->getTimeOffset() );
1100 } else {
1101 $tzDefault = $userTimeCorrectionObj->toString();
1102 }
1103
1104 $defaultPreferences['timecorrection'] = [
1105 'type' => 'timezone',
1106 'label-message' => 'timezonelegend',
1107 'default' => $tzDefault,
1108 'size' => 20,
1109 'section' => 'rendering/timeoffset',
1110 'id' => 'wpTimeCorrection',
1111 'filter' => TimezoneFilter::class,
1112 ];
1113 }
1114
1120 protected function renderingPreferences(
1121 User $user,
1122 MessageLocalizer $l10n,
1123 &$defaultPreferences
1124 ) {
1125 // Diffs
1126 $defaultPreferences['diffonly'] = [
1127 'type' => 'toggle',
1128 'section' => 'rendering/diffs',
1129 'label-message' => 'tog-diffonly',
1130 ];
1131 $defaultPreferences['norollbackdiff'] = [
1132 'type' => 'toggle',
1133 'section' => 'rendering/diffs',
1134 'label-message' => 'tog-norollbackdiff',
1135 ];
1136 $defaultPreferences['diff-type'] = [
1137 'type' => 'api',
1138 ];
1139
1140 // Page Rendering
1141 if ( $this->options->get( MainConfigNames::AllowUserCssPrefs ) ) {
1142 $defaultPreferences['underline'] = [
1143 'type' => 'select',
1144 'options' => [
1145 $l10n->msg( 'underline-never' )->text() => 0,
1146 $l10n->msg( 'underline-always' )->text() => 1,
1147 $l10n->msg( 'underline-default' )->text() => 2,
1148 ],
1149 'label-message' => 'tog-underline',
1150 'section' => 'rendering/advancedrendering',
1151 ];
1152 }
1153
1154 $defaultPreferences['showhiddencats'] = [
1155 'type' => 'toggle',
1156 'section' => 'rendering/advancedrendering',
1157 'label-message' => 'tog-showhiddencats'
1158 ];
1159
1160 if ( $user->isAllowed( 'rollback' ) ) {
1161 $defaultPreferences['showrollbackconfirmation'] = [
1162 'type' => 'toggle',
1163 'section' => 'rendering/advancedrendering',
1164 'label-message' => 'tog-showrollbackconfirmation',
1165 ];
1166 }
1167
1168 $defaultPreferences['forcesafemode'] = [
1169 'type' => 'toggle',
1170 'section' => 'rendering/advancedrendering',
1171 'label-message' => 'tog-forcesafemode',
1172 'help-message' => 'prefs-help-forcesafemode'
1173 ];
1174 }
1175
1181 protected function editingPreferences( User $user, MessageLocalizer $l10n, &$defaultPreferences ) {
1182 $defaultPreferences['editsectiononrightclick'] = [
1183 'type' => 'toggle',
1184 'section' => 'editing/advancedediting',
1185 'label-message' => 'tog-editsectiononrightclick',
1186 ];
1187 $defaultPreferences['editondblclick'] = [
1188 'type' => 'toggle',
1189 'section' => 'editing/advancedediting',
1190 'label-message' => 'tog-editondblclick',
1191 ];
1192
1193 if ( $this->options->get( MainConfigNames::AllowUserCssPrefs ) ) {
1194 $defaultPreferences['editfont'] = [
1195 'type' => 'select',
1196 'section' => 'editing/editor',
1197 'label-message' => 'editfont-style',
1198 'options' => [
1199 $l10n->msg( 'editfont-monospace' )->text() => 'monospace',
1200 $l10n->msg( 'editfont-sansserif' )->text() => 'sans-serif',
1201 $l10n->msg( 'editfont-serif' )->text() => 'serif',
1202 ]
1203 ];
1204 }
1205
1206 if ( $user->isAllowed( 'minoredit' ) ) {
1207 $defaultPreferences['minordefault'] = [
1208 'type' => 'toggle',
1209 'section' => 'editing/editor',
1210 'label-message' => 'tog-minordefault',
1211 ];
1212 }
1213
1214 $defaultPreferences['forceeditsummary'] = [
1215 'type' => 'toggle',
1216 'section' => 'editing/editor',
1217 'label-message' => 'tog-forceeditsummary',
1218 ];
1219
1220 // T350653
1221 if ( $this->options->get( MainConfigNames::EnableEditRecovery ) ) {
1222 $defaultPreferences['editrecovery'] = [
1223 'type' => 'toggle',
1224 'section' => 'editing/editor',
1225 'label-message' => 'tog-editrecovery',
1226 'help-message' => [
1227 'tog-editrecovery-help',
1228 'https://meta.wikimedia.org/wiki/Talk:Community_Wishlist_Survey_2023/Edit-recovery_feature',
1229 ],
1230 ];
1231 }
1232
1233 $defaultPreferences['useeditwarning'] = [
1234 'type' => 'toggle',
1235 'section' => 'editing/editor',
1236 'label-message' => 'tog-useeditwarning',
1237 ];
1238
1239 $defaultPreferences['previewonfirst'] = [
1240 'type' => 'toggle',
1241 'section' => 'editing/preview',
1242 'label-message' => 'tog-previewonfirst',
1243 ];
1244 $defaultPreferences['previewontop'] = [
1245 'type' => 'toggle',
1246 'section' => 'editing/preview',
1247 'label-message' => 'tog-previewontop',
1248 ];
1249 $defaultPreferences['uselivepreview'] = [
1250 'type' => 'toggle',
1251 'section' => 'editing/preview',
1252 'label-message' => 'tog-uselivepreview',
1253 ];
1254 }
1255
1261 protected function rcPreferences( User $user, MessageLocalizer $l10n, &$defaultPreferences ) {
1262 $rcMaxAge = $this->options->get( MainConfigNames::RCMaxAge );
1263 $rcMax = ceil( $rcMaxAge / ( 3600 * 24 ) );
1264 $defaultPreferences['rcdays'] = [
1265 'type' => 'float',
1266 'label-message' => 'recentchangesdays',
1267 'section' => 'rc/displayrc',
1268 'min' => 1 / 24,
1269 'max' => $rcMax,
1270 'help-message' => [ 'recentchangesdays-max', Message::numParam( $rcMax ) ],
1271 ];
1272 $defaultPreferences['rclimit'] = [
1273 'type' => 'int',
1274 'min' => 1,
1275 'max' => 1000,
1276 'label-message' => 'recentchangescount',
1277 'help-message' => 'prefs-help-recentchangescount',
1278 'section' => 'rc/displayrc',
1279 'filter' => IntvalFilter::class,
1280 ];
1281 $defaultPreferences['usenewrc'] = [
1282 'type' => 'toggle',
1283 'label-message' => 'tog-usenewrc',
1284 'section' => 'rc/advancedrc',
1285 ];
1286 $defaultPreferences['hideminor'] = [
1287 'type' => 'toggle',
1288 'label-message' => 'tog-hideminor',
1289 'section' => 'rc/changesrc',
1290 ];
1291 $defaultPreferences['pst-cssjs'] = [
1292 'type' => 'api',
1293 ];
1294 $defaultPreferences['rcfilters-rc-collapsed'] = [
1295 'type' => 'api',
1296 ];
1297 $defaultPreferences['rcfilters-wl-collapsed'] = [
1298 'type' => 'api',
1299 ];
1300 $defaultPreferences['rcfilters-saved-queries'] = [
1301 'type' => 'api',
1302 ];
1303 $defaultPreferences['rcfilters-wl-saved-queries'] = [
1304 'type' => 'api',
1305 ];
1306 // Override RCFilters preferences for RecentChanges 'limit'
1307 $defaultPreferences['rcfilters-limit'] = [
1308 'type' => 'api',
1309 ];
1310 $defaultPreferences['rcfilters-saved-queries-versionbackup'] = [
1311 'type' => 'api',
1312 ];
1313 $defaultPreferences['rcfilters-wl-saved-queries-versionbackup'] = [
1314 'type' => 'api',
1315 ];
1316
1317 if ( $this->options->get( MainConfigNames::RCWatchCategoryMembership ) ) {
1318 $defaultPreferences['hidecategorization'] = [
1319 'type' => 'toggle',
1320 'label-message' => 'tog-hidecategorization',
1321 'section' => 'rc/changesrc',
1322 ];
1323 }
1324
1325 if ( $user->useRCPatrol() ) {
1326 $defaultPreferences['hidepatrolled'] = [
1327 'type' => 'toggle',
1328 'section' => 'rc/changesrc',
1329 'label-message' => 'tog-hidepatrolled',
1330 ];
1331 }
1332
1333 if ( $user->useNPPatrol() ) {
1334 $defaultPreferences['newpageshidepatrolled'] = [
1335 'type' => 'toggle',
1336 'section' => 'rc/changesrc',
1337 'label-message' => 'tog-newpageshidepatrolled',
1338 ];
1339 }
1340
1341 if ( $this->options->get( MainConfigNames::RCShowWatchingUsers ) ) {
1342 $defaultPreferences['shownumberswatching'] = [
1343 'type' => 'toggle',
1344 'section' => 'rc/advancedrc',
1345 'label-message' => 'tog-shownumberswatching',
1346 ];
1347 }
1348
1349 $defaultPreferences['rcenhancedfilters-disable'] = [
1350 'type' => 'toggle',
1351 'section' => 'rc/advancedrc',
1352 'label-message' => 'rcfilters-preference-label',
1353 'help-message' => 'rcfilters-preference-help',
1354 ];
1355 }
1356
1362 protected function watchlistPreferences(
1363 User $user, IContextSource $context, &$defaultPreferences
1364 ) {
1365 $watchlistdaysMax = ceil( $this->options->get( MainConfigNames::RCMaxAge ) / ( 3600 * 24 ) );
1366
1367 if ( $user->isAllowed( 'editmywatchlist' ) ) {
1368 $editWatchlistLinks = '';
1369 $editWatchlistModes = [
1370 'edit' => [ 'subpage' => false, 'flags' => [] ],
1371 'raw' => [ 'subpage' => 'raw', 'flags' => [] ],
1372 'clear' => [ 'subpage' => 'clear', 'flags' => [ 'destructive' ] ],
1373 ];
1374 foreach ( $editWatchlistModes as $mode => $options ) {
1375 // Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear
1376 $editWatchlistLinks .=
1377 new ButtonWidget( [
1378 'href' => SpecialPage::getTitleFor( 'EditWatchlist', $options['subpage'] )->getLinkURL(),
1379 'flags' => $options[ 'flags' ],
1380 'label' => new HtmlSnippet(
1381 $context->msg( "prefs-editwatchlist-{$mode}" )->parse()
1382 ),
1383 ] );
1384 }
1385
1386 $defaultPreferences['editwatchlist'] = [
1387 'type' => 'info',
1388 'raw' => true,
1389 'default' => $editWatchlistLinks,
1390 'label-message' => 'prefs-editwatchlist-label',
1391 'section' => 'watchlist/editwatchlist',
1392 ];
1393 }
1394
1395 $defaultPreferences['watchlistdays'] = [
1396 'type' => 'float',
1397 'min' => 1 / 24,
1398 'max' => $watchlistdaysMax,
1399 'section' => 'watchlist/displaywatchlist',
1400 'help-message' => [ 'prefs-watchlist-days-max', Message::numParam( $watchlistdaysMax ) ],
1401 'label-message' => 'prefs-watchlist-days',
1402 ];
1403 $defaultPreferences['wllimit'] = [
1404 'type' => 'int',
1405 'min' => 1,
1406 'max' => 1000,
1407 'label-message' => 'prefs-watchlist-edits',
1408 'help-message' => 'prefs-watchlist-edits-max',
1409 'section' => 'watchlist/displaywatchlist',
1410 'filter' => IntvalFilter::class,
1411 ];
1412 $defaultPreferences['extendwatchlist'] = [
1413 'type' => 'toggle',
1414 'section' => 'watchlist/advancedwatchlist',
1415 'label-message' => 'tog-extendwatchlist',
1416 ];
1417 $defaultPreferences['watchlisthideminor'] = [
1418 'type' => 'toggle',
1419 'section' => 'watchlist/changeswatchlist',
1420 'label-message' => 'tog-watchlisthideminor',
1421 ];
1422 $defaultPreferences['watchlisthidebots'] = [
1423 'type' => 'toggle',
1424 'section' => 'watchlist/changeswatchlist',
1425 'label-message' => 'tog-watchlisthidebots',
1426 ];
1427 $defaultPreferences['watchlisthideown'] = [
1428 'type' => 'toggle',
1429 'section' => 'watchlist/changeswatchlist',
1430 'label-message' => 'tog-watchlisthideown',
1431 ];
1432 $defaultPreferences['watchlisthideanons'] = [
1433 'type' => 'toggle',
1434 'section' => 'watchlist/changeswatchlist',
1435 'label-message' => 'tog-watchlisthideanons',
1436 ];
1437 $defaultPreferences['watchlisthideliu'] = [
1438 'type' => 'toggle',
1439 'section' => 'watchlist/changeswatchlist',
1440 'label-message' => 'tog-watchlisthideliu',
1441 ];
1442
1444 $defaultPreferences['watchlistreloadautomatically'] = [
1445 'type' => 'toggle',
1446 'section' => 'watchlist/advancedwatchlist',
1447 'label-message' => 'tog-watchlistreloadautomatically',
1448 ];
1449 }
1450
1451 $defaultPreferences['watchlistunwatchlinks'] = [
1452 'type' => 'toggle',
1453 'section' => 'watchlist/advancedwatchlist',
1454 'label-message' => 'tog-watchlistunwatchlinks',
1455 ];
1456
1457 if ( $this->options->get( MainConfigNames::RCWatchCategoryMembership ) ) {
1458 $defaultPreferences['watchlisthidecategorization'] = [
1459 'type' => 'toggle',
1460 'section' => 'watchlist/changeswatchlist',
1461 'label-message' => 'tog-watchlisthidecategorization',
1462 ];
1463 }
1464
1465 if ( $user->useRCPatrol() ) {
1466 $defaultPreferences['watchlisthidepatrolled'] = [
1467 'type' => 'toggle',
1468 'section' => 'watchlist/changeswatchlist',
1469 'label-message' => 'tog-watchlisthidepatrolled',
1470 ];
1471 }
1472
1473 $watchTypes = [
1474 'edit' => 'watchdefault',
1475 'move' => 'watchmoves',
1476 ];
1477
1478 // Kinda hacky
1479 if ( $user->isAllowedAny( 'createpage', 'createtalk' ) ) {
1480 $watchTypes['read'] = 'watchcreations';
1481 }
1482
1483 // Move uncommon actions to end of list
1484 $watchTypes += [
1485 'rollback' => 'watchrollback',
1486 'upload' => 'watchuploads',
1487 'delete' => 'watchdeletion',
1488 ];
1489
1490 foreach ( $watchTypes as $action => $pref ) {
1491 if ( $user->isAllowed( $action ) ) {
1492 // Messages:
1493 // tog-watchdefault, tog-watchmoves, tog-watchdeletion, tog-watchcreations, tog-watchuploads
1494 // tog-watchrollback
1495 $defaultPreferences[$pref] = [
1496 'type' => 'toggle',
1497 'section' => 'watchlist/pageswatchlist',
1498 'label-message' => "tog-$pref",
1499 ];
1500 }
1501 }
1502
1503 $defaultPreferences['watchlisttoken'] = [
1504 'type' => 'api',
1505 ];
1506
1507 $tokenButton = new ButtonWidget( [
1508 'href' => SpecialPage::getTitleFor( 'ResetTokens' )->getLinkURL( [
1509 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
1510 ] ),
1511 'label' => $context->msg( 'prefs-watchlist-managetokens' )->text(),
1512 ] );
1513 $defaultPreferences['watchlisttoken-info'] = [
1514 'type' => 'info',
1515 'section' => 'watchlist/tokenwatchlist',
1516 'label-message' => 'prefs-watchlist-token',
1517 'help-message' => 'prefs-help-tokenmanagement',
1518 'raw' => true,
1519 'default' => (string)$tokenButton,
1520 ];
1521
1522 $defaultPreferences['wlenhancedfilters-disable'] = [
1523 'type' => 'toggle',
1524 'section' => 'watchlist/advancedwatchlist',
1525 'label-message' => 'rcfilters-watchlist-preference-label',
1526 'help-message' => 'rcfilters-watchlist-preference-help',
1527 ];
1528 }
1529
1534 protected function searchPreferences( $context, &$defaultPreferences ) {
1535 $defaultPreferences['search-special-page'] = [
1536 'type' => 'api',
1537 ];
1538
1539 foreach ( $this->nsInfo->getValidNamespaces() as $n ) {
1540 $defaultPreferences['searchNs' . $n] = [
1541 'type' => 'api',
1542 ];
1543 }
1544
1545 if ( $this->options->get( MainConfigNames::SearchMatchRedirectPreference ) ) {
1546 $defaultPreferences['search-match-redirect'] = [
1547 'type' => 'toggle',
1548 'section' => 'searchoptions/searchmisc',
1549 'label-message' => 'search-match-redirect-label',
1550 'help-message' => 'search-match-redirect-help',
1551 ];
1552 } else {
1553 $defaultPreferences['search-match-redirect'] = [
1554 'type' => 'api',
1555 ];
1556 }
1557
1558 $defaultPreferences['searchlimit'] = [
1559 'type' => 'int',
1560 'min' => 1,
1561 'max' => 500,
1562 'section' => 'searchoptions/searchmisc',
1563 'label-message' => 'searchlimit-label',
1564 'help-message' => $context->msg( 'searchlimit-help', 500 ),
1565 'filter' => IntvalFilter::class,
1566 ];
1567
1568 // show a preference for thumbnails from namespaces other than NS_FILE,
1569 // only when there they're actually configured to be served
1570 $thumbNamespaces = $this->options->get( MainConfigNames::ThumbnailNamespaces );
1571 $thumbNamespacesFormatted = array_combine(
1572 $thumbNamespaces,
1573 array_map(
1574 static function ( $namespaceId ) use ( $context ) {
1575 return $namespaceId === NS_MAIN
1576 ? $context->msg( 'blanknamespace' )->escaped()
1577 : $context->getLanguage()->getFormattedNsText( $namespaceId );
1578 },
1579 $thumbNamespaces
1580 )
1581 );
1582 $defaultThumbNamespacesFormatted =
1583 array_intersect_key( $thumbNamespacesFormatted, [ NS_FILE => 1 ] ) ?? [];
1584 $extraThumbNamespacesFormatted =
1585 array_diff_key( $thumbNamespacesFormatted, [ NS_FILE => 1 ] );
1586 if ( $extraThumbNamespacesFormatted ) {
1587 $defaultPreferences['search-thumbnail-extra-namespaces'] = [
1588 'type' => 'toggle',
1589 'section' => 'searchoptions/searchmisc',
1590 'label-message' => 'search-thumbnail-extra-namespaces-label',
1591 'help-message' => $context->msg(
1592 'search-thumbnail-extra-namespaces-message',
1593 $context->getLanguage()->listToText( $extraThumbNamespacesFormatted ),
1594 count( $extraThumbNamespacesFormatted ),
1595 $context->getLanguage()->listToText( $defaultThumbNamespacesFormatted ),
1596 count( $defaultThumbNamespacesFormatted )
1597 ),
1598 ];
1599 }
1600 }
1601
1602 /*
1603 * Custom skin string comparison function that takes into account current and preferred skins.
1604 *
1605 * @param string $a
1606 * @param string $b
1607 * @param string $currentSkin
1608 * @param array $preferredSkins
1609 * @return int
1610 */
1611 private static function sortSkinNames( $a, $b, $currentSkin, $preferredSkins ) {
1612 // Display the current skin first in the list
1613 if ( strcasecmp( $a, $currentSkin ) === 0 ) {
1614 return -1;
1615 }
1616 if ( strcasecmp( $b, $currentSkin ) === 0 ) {
1617 return 1;
1618 }
1619 // Display preferred skins over other skins
1620 if ( count( $preferredSkins ) ) {
1621 $aPreferred = array_search( $a, $preferredSkins );
1622 $bPreferred = array_search( $b, $preferredSkins );
1623 // Cannot use ! operator because array_search returns the
1624 // index of the array item if found (i.e. 0) and false otherwise
1625 if ( $aPreferred !== false && $bPreferred === false ) {
1626 return -1;
1627 }
1628 if ( $aPreferred === false && $bPreferred !== false ) {
1629 return 1;
1630 }
1631 // When both skins are preferred, default to the ordering
1632 // specified by the preferred skins config array
1633 if ( $aPreferred !== false && $bPreferred !== false ) {
1634 return strcasecmp( $aPreferred, $bPreferred );
1635 }
1636 }
1637 // Use normal string comparison if both strings are not preferred
1638 return strcasecmp( $a, $b );
1639 }
1640
1649 private function getValidSkinNames( User $user, IContextSource $context ) {
1650 // Only show skins that aren't disabled
1651 $validSkinNames = $this->skinFactory->getAllowedSkins();
1652 $allInstalledSkins = $this->skinFactory->getInstalledSkins();
1653
1654 // Display the installed skin the user has specifically requested via useskin=….
1655 $useSkin = $context->getRequest()->getRawVal( 'useskin' );
1656 if ( $useSkin !== null && isset( $allInstalledSkins[$useSkin] )
1657 && $context->msg( "skinname-$useSkin" )->exists()
1658 ) {
1659 $validSkinNames[$useSkin] = $useSkin;
1660 }
1661
1662 // Display the skin if the user has set it as a preference already before it was hidden.
1663 $currentUserSkin = $this->userOptionsManager->getOption( $user, 'skin' );
1664 if ( isset( $allInstalledSkins[$currentUserSkin] )
1665 && $context->msg( "skinname-$currentUserSkin" )->exists()
1666 ) {
1667 $validSkinNames[$currentUserSkin] = $currentUserSkin;
1668 }
1669
1670 foreach ( $validSkinNames as $skinkey => &$skinname ) {
1671 $msg = $context->msg( "skinname-{$skinkey}" );
1672 if ( $msg->exists() ) {
1673 $skinname = htmlspecialchars( $msg->text() );
1674 }
1675 }
1676
1677 $preferredSkins = $this->options->get( MainConfigNames::SkinsPreferred );
1678 // Sort by the internal name, so that the ordering is the same for each display language,
1679 // especially if some skin names are translated to use a different alphabet and some are not.
1680 uksort( $validSkinNames, function ( $a, $b ) use ( $currentUserSkin, $preferredSkins ) {
1681 return $this->sortSkinNames( $a, $b, $currentUserSkin, $preferredSkins );
1682 } );
1683
1684 return $validSkinNames;
1685 }
1686
1693 protected function generateSkinOptions( User $user, IContextSource $context, array $validSkinNames ) {
1694 $ret = [];
1695
1696 $mptitle = Title::newMainPage();
1697 $previewtext = $context->msg( 'skin-preview' )->escaped();
1698 $defaultSkin = $this->options->get( MainConfigNames::DefaultSkin );
1699 $allowUserCss = $this->options->get( MainConfigNames::AllowUserCss );
1700 $allowUserJs = $this->options->get( MainConfigNames::AllowUserJs );
1701 $safeMode = $this->userOptionsManager->getOption( $user, 'forcesafemode' );
1702 $foundDefault = false;
1703 foreach ( $validSkinNames as $skinkey => $sn ) {
1704 $linkTools = [];
1705
1706 // Mark the default skin
1707 if ( strcasecmp( $skinkey, $defaultSkin ) === 0 ) {
1708 $linkTools[] = $context->msg( 'default' )->escaped();
1709 $foundDefault = true;
1710 }
1711
1712 // Create talk page link if relevant message exists.
1713 $talkPageMsg = $context->msg( "$skinkey-prefs-talkpage" );
1714 if ( $talkPageMsg->exists() ) {
1715 $linkTools[] = $talkPageMsg->parse();
1716 }
1717
1718 // Create preview link
1719 $mplink = htmlspecialchars( $mptitle->getLocalURL( [ 'useskin' => $skinkey ] ) );
1720 $linkTools[] = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
1721
1722 if ( !$safeMode ) {
1723 // Create links to user CSS/JS pages
1724 // @todo Refactor this and the similar code in skinPreferences().
1725 if ( $allowUserCss ) {
1726 $cssPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.css' );
1727 $cssLinkText = $context->msg( 'prefs-custom-css' )->text();
1728 $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
1729 }
1730
1731 if ( $allowUserJs ) {
1732 $jsPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.js' );
1733 $jsLinkText = $context->msg( 'prefs-custom-js' )->text();
1734 $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
1735 }
1736 }
1737
1738 $display = $sn . ' ' . $context->msg( 'parentheses' )
1739 ->rawParams( $context->getLanguage()->pipeList( $linkTools ) )
1740 ->escaped();
1741 $ret[$display] = $skinkey;
1742 }
1743
1744 if ( !$foundDefault ) {
1745 // If the default skin is not available, things are going to break horribly because the
1746 // default value for skin selector will not be a valid value. Let's just not show it then.
1747 return [];
1748 }
1749
1750 return $ret;
1751 }
1752
1757 protected function getDateOptions( IContextSource $context ) {
1758 $lang = $context->getLanguage();
1759 $dateopts = $lang->getDatePreferences();
1760
1761 $ret = [];
1762
1763 if ( $dateopts ) {
1764 if ( !in_array( 'default', $dateopts ) ) {
1765 $dateopts[] = 'default'; // Make sure default is always valid T21237
1766 }
1767
1768 // FIXME KLUGE: site default might not be valid for user language
1769 global $wgDefaultUserOptions;
1770 if ( !in_array( $wgDefaultUserOptions['date'], $dateopts ) ) {
1771 $wgDefaultUserOptions['date'] = 'default';
1772 }
1773
1774 $epoch = wfTimestampNow();
1775 foreach ( $dateopts as $key ) {
1776 if ( $key == 'default' ) {
1777 $formatted = $context->msg( 'datedefault' )->escaped();
1778 } else {
1779 $formatted = htmlspecialchars( $lang->timeanddate( $epoch, false, $key ) );
1780 }
1781 $ret[$formatted] = $key;
1782 }
1783 }
1784 return $ret;
1785 }
1786
1791 protected function getImageSizes( MessageLocalizer $l10n ) {
1792 $ret = [];
1793 $pixels = $l10n->msg( 'unit-pixel' )->text();
1794
1795 foreach ( $this->options->get( MainConfigNames::ImageLimits ) as $index => $limits ) {
1796 // Note: A left-to-right marker (U+200E) is inserted, see T144386
1797 $display = "{$limits[0]}\u{200E}×{$limits[1]}$pixels";
1798 $ret[$display] = $index;
1799 }
1800
1801 return $ret;
1802 }
1803
1808 protected function getThumbSizes( MessageLocalizer $l10n ) {
1809 $ret = [];
1810 $pixels = $l10n->msg( 'unit-pixel' )->text();
1811
1812 foreach ( $this->options->get( MainConfigNames::ThumbLimits ) as $index => $size ) {
1813 $display = $size . $pixels;
1814 $ret[$display] = $index;
1815 }
1816
1817 return $ret;
1818 }
1819
1826 protected function validateSignature( $signature, $alldata, HTMLForm $form ) {
1827 $sigValidation = $this->options->get( MainConfigNames::SignatureValidation );
1828 $maxSigChars = $this->options->get( MainConfigNames::MaxSigChars );
1829 if ( is_string( $signature ) && mb_strlen( $signature ) > $maxSigChars ) {
1830 return $form->msg( 'badsiglength' )->numParams( $maxSigChars )->escaped();
1831 }
1832
1833 if ( $signature === null || $signature === '' ) {
1834 // Make sure leaving the field empty is valid, since that's used as the default (T288151).
1835 // Code using this preference in Parser::getUserSig() handles this case specially.
1836 return true;
1837 }
1838
1839 // Remaining checks only apply to fancy signatures
1840 if ( !( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) ) {
1841 return true;
1842 }
1843
1844 // HERE BE DRAGONS:
1845 //
1846 // If this value is already saved as the user's signature, treat it as valid, even if it
1847 // would be invalid to save now, and even if $wgSignatureValidation is set to 'disallow'.
1848 //
1849 // It can become invalid when we introduce new validation, or when the value just transcludes
1850 // some page containing the real signature and that page is edited (which we can't validate),
1851 // or when someone's username is changed.
1852 //
1853 // Otherwise it would be completely removed when the user opens their preferences page, which
1854 // would be very unfriendly.
1855 $user = $form->getUser();
1856 if (
1857 $signature === $this->userOptionsManager->getOption( $user, 'nickname' ) &&
1858 (bool)$alldata['fancysig'] === $this->userOptionsManager->getBoolOption( $user, 'fancysig' )
1859 ) {
1860 return true;
1861 }
1862
1863 if ( $sigValidation === 'new' || $sigValidation === 'disallow' ) {
1864 // Validate everything
1865 $parserOpts = ParserOptions::newFromContext( $form->getContext() );
1866 $validator = $this->signatureValidatorFactory
1867 ->newSignatureValidator( $user, $form->getContext(), $parserOpts );
1868 $errors = $validator->validateSignature( $signature );
1869 if ( $errors ) {
1870 return $errors;
1871 }
1872 }
1873
1874 // Quick check for mismatched HTML tags in the input.
1875 // Note that this is easily fooled by wikitext templates or bold/italic markup.
1876 // We're only keeping this until Parsoid is integrated and guaranteed to be available.
1877 if ( $this->parserFactory->getInstance()->validateSig( $signature ) === false ) {
1878 return $form->msg( 'badsig' )->escaped();
1879 }
1880
1881 return true;
1882 }
1883
1890 protected function cleanSignature( $signature, $alldata, HTMLForm $form ) {
1891 if ( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) {
1892 $signature = $this->parserFactory->getInstance()->cleanSig( $signature );
1893 } else {
1894 // When no fancy sig used, make sure ~{3,5} get removed.
1895 $signature = Parser::cleanSigInSig( $signature );
1896 }
1897
1898 return $signature;
1899 }
1900
1908 public function getForm(
1909 User $user,
1910 IContextSource $context,
1911 $formClass = PreferencesFormOOUI::class,
1912 array $remove = []
1913 ) {
1914 // We use ButtonWidgets in some of the getPreferences() functions
1915 $context->getOutput()->enableOOUI();
1916
1917 // Note that the $user parameter of getFormDescriptor() is deprecated.
1918 $formDescriptor = $this->getFormDescriptor( $user, $context );
1919 if ( count( $remove ) ) {
1920 $removeKeys = array_fill_keys( $remove, true );
1921 $formDescriptor = array_diff_key( $formDescriptor, $removeKeys );
1922 }
1923
1924 // Remove type=api preferences. They are not intended for rendering in the form.
1925 foreach ( $formDescriptor as $name => $info ) {
1926 if ( isset( $info['type'] ) && $info['type'] === 'api' ) {
1927 unset( $formDescriptor[$name] );
1928 }
1929 }
1930
1934 $htmlForm = new $formClass( $formDescriptor, $context, 'prefs' );
1935
1936 // This allows users to opt-in to hidden skins. While this should be discouraged and is not
1937 // discoverable, this allows users to still use hidden skins while preventing new users from
1938 // adopting unsupported skins. If no useskin=… parameter was provided, it will not show up
1939 // in the resulting URL.
1940 $htmlForm->setAction( $context->getTitle()->getLocalURL( [
1941 'useskin' => $context->getRequest()->getRawVal( 'useskin' )
1942 ] ) );
1943
1944 $htmlForm->setModifiedUser( $user );
1945 $htmlForm->setOptionsEditable( $user->isAllowed( 'editmyoptions' ) );
1946 $htmlForm->setPrivateInfoEditable( $user->isAllowed( 'editmyprivateinfo' ) );
1947 $htmlForm->setId( 'mw-prefs-form' );
1948 $htmlForm->setAutocomplete( 'off' );
1949 $htmlForm->setSubmitTextMsg( 'saveprefs' );
1950 // Used message keys: 'accesskey-preferences-save', 'tooltip-preferences-save'
1951 $htmlForm->setSubmitTooltip( 'preferences-save' );
1952 $htmlForm->setSubmitID( 'prefcontrol' );
1953 $htmlForm->setSubmitCallback(
1954 function ( array $formData, PreferencesFormOOUI $form ) use ( $formDescriptor ) {
1955 return $this->submitForm( $formData, $form, $formDescriptor );
1956 }
1957 );
1958
1959 return $htmlForm;
1960 }
1961
1970 protected function saveFormData( $formData, PreferencesFormOOUI $form, array $formDescriptor ) {
1971 $user = $form->getModifiedUser();
1972 $hiddenPrefs = $this->options->get( MainConfigNames::HiddenPrefs );
1973 $result = true;
1974
1975 if ( !$user->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
1976 return Status::newFatal( 'mypreferencesprotected' );
1977 }
1978
1979 // Filter input
1980 $this->applyFilters( $formData, $formDescriptor, 'filterFromForm' );
1981
1982 // Fortunately, the realname field is MUCH simpler
1983 // (not really "private", but still shouldn't be edited without permission)
1984
1985 if ( !in_array( 'realname', $hiddenPrefs )
1986 && $user->isAllowed( 'editmyprivateinfo' )
1987 && array_key_exists( 'realname', $formData )
1988 ) {
1989 $realName = $formData['realname'];
1990 $user->setRealName( $realName );
1991 }
1992
1993 if ( $user->isAllowed( 'editmyoptions' ) ) {
1994 $oldUserOptions = $this->userOptionsManager->getOptions( $user );
1995
1996 foreach ( $this->getSaveBlacklist() as $b ) {
1997 unset( $formData[$b] );
1998 }
1999
2000 // If users have saved a value for a preference which has subsequently been disabled
2001 // via $wgHiddenPrefs, we don't want to destroy that setting in case the preference
2002 // is subsequently re-enabled
2003 foreach ( $hiddenPrefs as $pref ) {
2004 // If the user has not set a non-default value here, the default will be returned
2005 // and subsequently discarded
2006 $formData[$pref] = $this->userOptionsManager->getOption( $user, $pref, null, true );
2007 }
2008
2009 // If the user changed the rclimit preference, also change the rcfilters-rclimit preference
2010 if (
2011 isset( $formData['rclimit'] ) &&
2012 intval( $formData[ 'rclimit' ] ) !== $this->userOptionsManager->getIntOption( $user, 'rclimit' )
2013 ) {
2014 $formData['rcfilters-limit'] = $formData['rclimit'];
2015 }
2016
2017 // Keep old preferences from interfering due to back-compat code, etc.
2018 $optionsToReset = $this->getOptionNamesForReset( $user, $form->getContext(), 'unused' );
2019 $this->userOptionsManager->resetOptionsByName( $user, $optionsToReset );
2020
2021 foreach ( $formData as $key => $value ) {
2022 // If we're creating a new local override, we need to explicitly pass
2023 // GLOBAL_OVERRIDE to setOption(), otherwise the update would be ignored
2024 // due to the conflicting global option.
2025 $except = !empty( $formData[$key . UserOptionsLookup::LOCAL_EXCEPTION_SUFFIX] );
2026 $this->userOptionsManager->setOption( $user, $key, $value,
2027 $except ? UserOptionsManager::GLOBAL_OVERRIDE : UserOptionsManager::GLOBAL_IGNORE );
2028 }
2029
2030 $this->hookRunner->onPreferencesFormPreSave(
2031 $formData, $form, $user, $result, $oldUserOptions );
2032 }
2033
2034 $user->saveSettings();
2035
2036 return $result;
2037 }
2038
2047 protected function applyFilters( array &$preferences, array $formDescriptor, $verb ) {
2048 foreach ( $formDescriptor as $preference => $desc ) {
2049 if ( !isset( $desc['filter'] ) || !isset( $preferences[$preference] ) ) {
2050 continue;
2051 }
2052 $filterDesc = $desc['filter'];
2053 if ( $filterDesc instanceof Filter ) {
2054 $filter = $filterDesc;
2055 } elseif ( class_exists( $filterDesc ) ) {
2056 $filter = new $filterDesc();
2057 } elseif ( is_callable( $filterDesc ) ) {
2058 $filter = $filterDesc();
2059 } else {
2060 throw new UnexpectedValueException(
2061 "Unrecognized filter type for preference '$preference'"
2062 );
2063 }
2064 $preferences[$preference] = $filter->$verb( $preferences[$preference] );
2065 }
2066 }
2067
2076 protected function submitForm(
2077 array $formData,
2078 PreferencesFormOOUI $form,
2079 array $formDescriptor
2080 ) {
2081 $res = $this->saveFormData( $formData, $form, $formDescriptor );
2082
2083 if ( $res === true ) {
2084 $context = $form->getContext();
2085 $urlOptions = [];
2086
2087 $urlOptions += $form->getExtraSuccessRedirectParameters();
2088
2089 $url = $form->getTitle()->getFullURL( $urlOptions );
2090
2091 // Set session data for the success message
2092 $context->getRequest()->getSession()->set( 'specialPreferencesSaveSuccess', 1 );
2093
2094 $context->getOutput()->redirect( $url );
2095 }
2096
2097 return ( $res === true ? Status::newGood() : $res );
2098 }
2099
2100 public function getResetKinds(
2101 User $user, IContextSource $context, $options = null
2102 ): array {
2103 $options ??= $this->userOptionsManager->loadUserOptions( $user );
2104
2105 $prefs = $this->getFormDescriptor( $user, $context );
2106 $mapping = [];
2107
2108 // Pull out the "special" options, so they don't get converted as
2109 // multiselect or checkmatrix.
2110 $specialOptions = array_fill_keys( $this->getSaveBlacklist(), true );
2111 foreach ( $specialOptions as $name => $value ) {
2112 unset( $prefs[$name] );
2113 }
2114
2115 // Multiselect and checkmatrix options are stored in the database with
2116 // one key per option, each having a boolean value. Extract those keys.
2117 $multiselectOptions = [];
2118 foreach ( $prefs as $name => $info ) {
2119 if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
2120 // Checking old alias for compatibility with unchanged extensions
2121 ( isset( $info['class'] ) && $info['class'] === \HTMLMultiSelectField::class ) ||
2122 ( isset( $info['class'] ) && $info['class'] === HTMLMultiSelectField::class )
2123 ) {
2124 $opts = HTMLFormField::flattenOptions( $info['options'] ?? $info['options-messages'] );
2125 $prefix = $info['prefix'] ?? $name;
2126
2127 foreach ( $opts as $value ) {
2128 $multiselectOptions["$prefix$value"] = true;
2129 }
2130
2131 unset( $prefs[$name] );
2132 }
2133 }
2134 $checkmatrixOptions = [];
2135 foreach ( $prefs as $name => $info ) {
2136 if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
2137 // Checking old alias for compatibility with unchanged extensions
2138 ( isset( $info['class'] ) && $info['class'] === \HTMLCheckMatrix::class ) ||
2139 ( isset( $info['class'] ) && $info['class'] === HTMLCheckMatrix::class )
2140 ) {
2141 $columns = HTMLFormField::flattenOptions( $info['columns'] );
2142 $rows = HTMLFormField::flattenOptions( $info['rows'] );
2143 $prefix = $info['prefix'] ?? $name;
2144
2145 foreach ( $columns as $column ) {
2146 foreach ( $rows as $row ) {
2147 $checkmatrixOptions["$prefix$column-$row"] = true;
2148 }
2149 }
2150
2151 unset( $prefs[$name] );
2152 }
2153 }
2154
2155 // $value is ignored
2156 foreach ( $options as $key => $value ) {
2157 if ( isset( $prefs[$key] ) ) {
2158 $mapping[$key] = 'registered';
2159 } elseif ( isset( $multiselectOptions[$key] ) ) {
2160 $mapping[$key] = 'registered-multiselect';
2161 } elseif ( isset( $checkmatrixOptions[$key] ) ) {
2162 $mapping[$key] = 'registered-checkmatrix';
2163 } elseif ( isset( $specialOptions[$key] ) ) {
2164 $mapping[$key] = 'special';
2165 } elseif ( str_starts_with( $key, 'userjs-' ) ) {
2166 $mapping[$key] = 'userjs';
2167 } elseif ( str_starts_with( $key, UserOptionsLookup::LOCAL_EXCEPTION_SUFFIX ) ) {
2168 $mapping[$key] = 'local-exception';
2169 } else {
2170 $mapping[$key] = 'unused';
2171 }
2172 }
2173
2174 return $mapping;
2175 }
2176
2177 public function listResetKinds() {
2178 return [
2179 'registered',
2180 'registered-multiselect',
2181 'registered-checkmatrix',
2182 'userjs',
2183 'special',
2184 'unused'
2185 ];
2186 }
2187
2188 public function getOptionNamesForReset( User $user, IContextSource $context, $kinds ) {
2189 $oldOptions = $this->userOptionsManager->loadUserOptions( $user, IDBAccessObject::READ_LATEST );
2190
2191 if ( !is_array( $kinds ) ) {
2192 $kinds = [ $kinds ];
2193 }
2194
2195 if ( in_array( 'all', $kinds ) ) {
2196 return array_keys( $oldOptions );
2197 } else {
2198 $optionKinds = $this->getResetKinds( $user, $context );
2199 $kinds = array_intersect( $kinds, $this->listResetKinds() );
2200 $optionNames = [];
2201
2202 foreach ( $oldOptions as $key => $value ) {
2203 if ( in_array( $optionKinds[$key], $kinds ) ) {
2204 $optionNames[] = $key;
2205 }
2206 }
2207 return $optionNames;
2208 }
2209 }
2210}
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:57
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition Html.php:218
Methods for dealing with language codes.
Base class for multi-variant language conversion.
Base class for language-specific code.
Definition Language.php:79
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:156
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:143
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:94
isAllowed(string $permission, ?PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
Definition User.php:2181
getRegistration()
Get the timestamp of account creation.
Definition User.php:3089
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition User.php:2189
isAllowedAny(... $permissions)
Checks whether this authority has any of the given permissions in general.
Definition User.php:2173
getEditCount()
Get the user's edit count.
Definition User.php:2110
getRealName()
Get the user's real name.
Definition User.php:1991
getTitleKey()
Get the user's name escaped by underscores.
Definition User.php:1668
getEmailAuthenticationTimestamp()
Get the timestamp of the user's e-mail authentication.
Definition User.php:1907
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition User.php:2199
getEmail()
Get the user's e-mail address.
Definition User.php:1894
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:1576
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)