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