65use Psr\Log\LoggerAwareTrait;
66use Psr\Log\NullLogger;
68use UnexpectedValueException;
99 private $languageConverter;
108 private $languageConverterFactory;
111 private $parserFactory;
114 private $skinFactory;
117 private $userGroupManager;
120 private $signatureValidatorFactory;
206 $this->logger =
new NullLogger();
207 $this->languageConverter = $languageConverter;
209 $this->hookRunner =
new HookRunner( $hookContainer );
213 $services =
static function () {
219 : $services()->getUserOptionsManager();
220 $this->languageConverterFactory = $languageConverterFactory ?? $services()->getLanguageConverterFactory();
222 $this->parserFactory = $parserFactory ?? $services()->getParserFactory();
223 $this->skinFactory = $skinFactory ?? $services()->getSkinFactory();
224 $this->userGroupManager = $userGroupManager ?? $services()->getUserGroupManager();
225 $this->signatureValidatorFactory = $signatureValidatorFactory
226 ?? $services()->getSignatureValidatorFactory();
247 OutputPage::setupOOUI(
248 strtolower( $context->
getSkin()->getSkinName() ),
262 $this->hookRunner->onGetPreferences( $user, $preferences );
264 $this->loadPreferenceValues( $user, $context, $preferences );
265 $this->logger->debug(
"Created form descriptor for user '{$user->getName()}'" );
276 foreach ( $descriptor as $name => &
$params ) {
279 if ( ( isset(
$params[
'type'] ) &&
$params[
'type'] ===
'info' ) ||
280 ( isset(
$params[
'class'] ) &&
$params[
'class'] === \HTMLInfoField::class )
282 unset( $descriptor[$name] );
287 foreach (
$params as $key => $value ) {
290 case 'options-message':
293 case 'options-messages':
298 if ( preg_match(
'/-messages?$/', $key ) ) {
315 private function loadPreferenceValues(
User $user,
IContextSource $context, &$defaultPreferences ) {
318 unset( $defaultPreferences[$pref] );
323 $form =
new HTMLForm( $simplified, $context );
325 $disable = !$user->
isAllowed(
'editmyoptions' );
327 $defaultOptions = $this->userOptionsManager->getDefaultOptions( $user );
328 $userOptions = $this->userOptionsManager->getOptions( $user );
329 $this->
applyFilters( $userOptions, $defaultPreferences,
'filterForForm' );
331 foreach ( $simplified as $name => $_ ) {
332 $info = &$defaultPreferences[$name];
334 $info[
'disabled'] =
'disabled';
336 if ( isset( $info[
'default'] ) ) {
340 $field = $form->getField( $name );
341 $globalDefault = $defaultOptions[$name] ??
null;
342 $prefFromUser = static::getPreferenceForField( $name, $field, $userOptions );
347 if ( $prefFromUser !==
null &&
348 $field->validate( $prefFromUser, $this->userOptionsManager->getOptions( $user ) ) ===
true ) {
349 $info[
'default'] = $prefFromUser;
350 } elseif ( $field->validate( $globalDefault, $this->userOptionsManager->getOptions( $user ) ) ===
true ) {
351 $info[
'default'] = $globalDefault;
353 $globalDefault = json_encode( $globalDefault );
354 throw new UnexpectedValueException(
355 "Default '$globalDefault' is invalid for preference $name of user " . $user->
getName()
360 return $defaultPreferences;
374 $val = $userOptions[$name] ??
null;
378 $prefix = $field->mParams[
'prefix'] ?? $name;
380 $keys = array_keys( $field->filterDataForSubmit( [] ) );
381 foreach ( $keys as $key ) {
382 if ( $userOptions[$prefix . $key] ??
false ) {
401 $val = $userOptions[$name] ??
null;
404 if ( ( isset( $info[
'type'] ) && $info[
'type'] ==
'multiselect' ) ||
405 ( isset( $info[
'class'] ) && $info[
'class'] == \HTMLMultiSelectField::class ) ) {
406 $options = HTMLFormField::flattenOptions( $info[
'options-messages'] ?? $info[
'options'] );
407 $prefix = $info[
'prefix'] ?? $name;
411 if ( $userOptions[
"$prefix$value"] ??
false ) {
418 if ( ( isset( $info[
'type'] ) && $info[
'type'] ==
'checkmatrix' ) ||
419 ( isset( $info[
'class'] ) && $info[
'class'] == \HTMLCheckMatrix::class ) ) {
420 $columns = HTMLFormField::flattenOptions( $info[
'columns'] );
421 $rows = HTMLFormField::flattenOptions( $info[
'rows'] );
422 $prefix = $info[
'prefix'] ?? $name;
425 foreach ( $columns as $column ) {
426 foreach ( $rows as $row ) {
427 if ( $userOptions[
"$prefix$column-$row"] ??
false ) {
428 $val[] =
"$column-$row";
451 $defaultPreferences[
'username'] = [
453 'label-message' => [
'username', $userName ],
454 'default' => $userName,
455 'section' =>
'personal/info',
461 $userEffectiveGroups = array_diff(
462 $this->userGroupManager->getUserEffectiveGroups( $user ),
465 $defaultPreferences[
'usergroups'] = [
467 'label-message' => [
'prefs-memberingroups',
468 \Message::numParam( count( $userEffectiveGroups ) ), $userName ],
469 'default' =>
function () use ( $user, $userEffectiveGroups, $context, $lang, $userName ) {
470 $userGroupMemberships = $this->userGroupManager->getUserGroupMemberships( $user );
471 $userGroups = $userMembers = $userTempGroups = $userTempMembers = [];
472 foreach ( $userEffectiveGroups as $ueg ) {
473 $groupStringOrObject = $userGroupMemberships[$ueg] ?? $ueg;
475 $userG = UserGroupMembership::getLinkHTML( $groupStringOrObject, $context );
476 $userM = UserGroupMembership::getLinkHTML( $groupStringOrObject, $context, $userName );
485 $userTempGroups[] = $userG;
486 $userTempMembers[] = $userM;
488 $userGroups[] = $userG;
489 $userMembers[] = $userM;
493 sort( $userMembers );
494 sort( $userTempGroups );
495 sort( $userTempMembers );
496 $userGroups = array_merge( $userTempGroups, $userGroups );
497 $userMembers = array_merge( $userTempMembers, $userMembers );
498 return $context->
msg(
'prefs-memberingroups-type' )
499 ->rawParams( $lang->commaList( $userGroups ), $lang->commaList( $userMembers ) )
503 'section' =>
'personal/info',
507 $formattedEditCount = $lang->formatNum( $user->
getEditCount() );
508 $editCount = $this->linkRenderer->makeLink( $contribTitle, $formattedEditCount );
510 $defaultPreferences[
'editcount'] = [
513 'label-message' =>
'prefs-edits',
514 'default' => $editCount,
515 'section' =>
'personal/info',
519 $displayUser = $context->
getUser();
521 $defaultPreferences[
'registrationdate'] = [
523 'label-message' =>
'prefs-registration',
524 'default' => $context->
msg(
525 'prefs-registration-date-time',
526 $lang->userTimeAndDate( $userRegistration, $displayUser ),
527 $lang->userDate( $userRegistration, $displayUser ),
528 $lang->userTime( $userRegistration, $displayUser )
530 'section' =>
'personal/info',
534 $canViewPrivateInfo = $user->
isAllowed(
'viewmyprivateinfo' );
535 $canEditPrivateInfo = $user->
isAllowed(
'editmyprivateinfo' );
538 $defaultPreferences[
'realname'] = [
540 'type' => $canEditPrivateInfo && $this->authManager->allowsPropertyChange(
'realname' )
543 'section' =>
'personal/info',
544 'label-message' =>
'yourrealname',
545 'help-message' =>
'prefs-help-realname',
548 if ( $canEditPrivateInfo && $this->authManager->allowsAuthenticationDataChange(
551 $defaultPreferences[
'password'] = [
554 'default' => (string)
new ButtonWidget( [
558 'label' => $context->
msg(
'prefs-resetpass' )->text(),
560 'label-message' =>
'yourpassword',
564 ? $context->
msg(
'prefs-help-yourpassword',
565 '[[#mw-prefsection-personal-email|{{int:prefs-email}}]]' )->parse()
567 'section' =>
'personal/info',
574 $defaultPreferences[
'prefershttps'] = [
576 'label-message' =>
'tog-prefershttps',
577 'help-message' =>
'prefs-help-prefershttps',
578 'section' =>
'personal/info'
582 $defaultPreferences[
'downloaduserdata'] = [
585 'label-message' =>
'prefs-user-downloaddata-label',
590 '/api.php?action=query&meta=userinfo&uiprop=*&formatversion=2',
592 $context->
msg(
'prefs-user-downloaddata-info' )->text()
594 'help-message' => [
'prefs-user-downloaddata-help-message', urlencode( $user->
getTitleKey() ) ],
595 'section' =>
'personal/info',
598 $defaultPreferences[
'restoreprefs'] = [
601 'label-message' =>
'prefs-user-restoreprefs-label',
606 ->getSubpage(
'reset' )->getLocalURL()
608 $context->
msg(
'prefs-user-restoreprefs-info' )->text()
610 'section' =>
'personal/info',
613 $languages = $this->languageNameUtils->getLanguageNames(
614 LanguageNameUtils::AUTONYMS,
615 LanguageNameUtils::SUPPORTED
618 if ( !array_key_exists( $languageCode, $languages ) ) {
619 $languages[$languageCode] = $languageCode;
625 foreach ( $languages as $code => $name ) {
626 $display = LanguageCode::bcp47( $code ) .
' - ' . $name;
629 $defaultPreferences[
'language'] = [
631 'section' =>
'personal/i18n',
633 'label-message' =>
'yourlanguage',
636 $neutralGenderMessage = $context->
msg(
'gender-notknown' )->escaped() . (
637 !$context->
msg(
'gender-unknown' )->isDisabled()
638 ?
"<br>" . $context->
msg(
'parentheses' )
639 ->params( $context->
msg(
'gender-unknown' )->plain() )
644 $defaultPreferences[
'gender'] = [
646 'section' =>
'personal/i18n',
648 $neutralGenderMessage =>
'unknown',
649 $context->
msg(
'gender-female' )->escaped() =>
'female',
650 $context->
msg(
'gender-male' )->escaped() =>
'male',
652 'label-message' =>
'yourgender',
653 'help-message' =>
'prefs-help-gender',
657 if ( !$this->languageConverterFactory->isConversionDisabled() ) {
659 foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
660 if ( $langCode == $this->contLang->getCode() ) {
661 if ( !$this->languageConverter->hasVariants() ) {
665 $variants = $this->languageConverter->getVariants();
667 foreach ( $variants as $v ) {
668 $v = str_replace(
'_',
'-', strtolower( $v ) );
669 $variantArray[$v] = $lang->getVariantname( $v,
false );
673 foreach ( $variantArray as $code => $name ) {
674 $display = LanguageCode::bcp47( $code ) .
' - ' . $name;
678 $defaultPreferences[
'variant'] = [
679 'label-message' =>
'yourvariant',
682 'section' =>
'personal/i18n',
683 'help-message' =>
'prefs-help-variant',
686 $defaultPreferences[
"variant-$langCode"] = [
694 $oldsigWikiText = $this->parserFactory->getInstance()->preSaveTransform(
698 ParserOptions::newFromContext( $context )
700 $oldsigHTML = Parser::stripOuterParagraph(
701 $context->
getOutput()->parseAsContent( $oldsigWikiText )
703 $signatureFieldConfig = [];
705 $signature = $this->userOptionsManager->getOption( $user,
'nickname' );
706 $useFancySig = $this->userOptionsManager->getBoolOption( $user,
'fancysig' );
707 if ( $useFancySig && $signature !==
'' ) {
708 $parserOpts = ParserOptions::newFromContext( $context );
709 $validator = $this->signatureValidatorFactory
710 ->newSignatureValidator( $user, $context, $parserOpts );
711 $signatureErrors = $validator->validateSignature( $signature );
712 if ( $signatureErrors ) {
714 $oldsigHTML .=
'<p><strong>' .
719 $context->
msg(
"prefs-signature-invalid-$sigValidation" )->parse() .
724 foreach ( $signatureErrors as &$sigError ) {
725 $sigError =
new HtmlSnippet( $sigError );
728 $signatureFieldConfig = [
729 'warnings' => $sigValidation !==
'disallow' ? $signatureErrors :
null,
730 'errors' => $sigValidation ===
'disallow' ? $signatureErrors :
null,
735 $defaultPreferences[
'oldsig'] = [
740 'default' =>
new FieldLayout(
742 'label' =>
new HtmlSnippet( $oldsigHTML ),
746 'label' =>
new HtmlSnippet( $context->
msg(
'tog-oldsig' )->parse() )
747 ] + $signatureFieldConfig
749 'section' =>
'personal/signature',
751 $defaultPreferences[
'nickname'] = [
752 'type' => $this->authManager->allowsPropertyChange(
'nickname' ) ?
'text' :
'info',
754 'label-message' =>
'yournick',
755 'validation-callback' =>
function ( $signature, $alldata,
HTMLForm $form ) {
758 'section' =>
'personal/signature',
759 'filter-callback' =>
function ( $signature, array $alldata,
HTMLForm $form ) {
763 $defaultPreferences[
'fancysig'] = [
765 'label-message' =>
'tog-fancysig',
767 'help-message' =>
'prefs-help-signature',
768 'section' =>
'personal/signature'
773 if ( $canViewPrivateInfo ) {
776 ?
'prefs-help-email-required'
777 :
'prefs-help-email';
781 $helpMessages[] =
'prefs-help-email-others';
784 $emailAddress = $user->
getEmail() ? htmlspecialchars( $user->
getEmail() ) :
'';
785 if ( $canEditPrivateInfo && $this->authManager->allowsPropertyChange(
'emailaddress' ) ) {
786 $button =
new ButtonWidget( [
791 $context->
msg( $user->
getEmail() ?
'prefs-changeemail' :
'prefs-setemail' )->text(),
794 $emailAddress .= $emailAddress ==
'' ? $button : (
'<br />' . $button );
797 $defaultPreferences[
'emailaddress'] = [
800 'default' => $emailAddress,
801 'label-message' =>
'youremail',
802 'section' =>
'personal/email',
803 'help-messages' => $helpMessages,
808 $disableEmailPrefs =
false;
811 $defaultPreferences[
'requireemail'] = [
813 'label-message' =>
'tog-requireemail',
814 'help-message' =>
'prefs-help-requireemail',
815 'section' =>
'personal/email',
826 $displayUser = $context->
getUser();
828 $time = $lang->userTimeAndDate( $emailTimestamp, $displayUser );
829 $d = $lang->userDate( $emailTimestamp, $displayUser );
830 $t = $lang->userTime( $emailTimestamp, $displayUser );
831 $emailauthenticated = $context->
msg(
'emailauthenticated',
832 $time, $d, $t )->parse() .
'<br />';
833 $emailauthenticationclass =
'mw-email-authenticated';
835 $disableEmailPrefs =
true;
836 $emailauthenticated = $context->
msg(
'emailnotauthenticated' )->parse() .
'<br />' .
839 'label' => $context->
msg(
'emailconfirmlink' )->text(),
841 $emailauthenticationclass =
"mw-email-not-authenticated";
844 $disableEmailPrefs =
true;
845 $emailauthenticated = $context->
msg(
'noemailprefs' )->escaped();
846 $emailauthenticationclass =
'mw-email-none';
849 if ( $canViewPrivateInfo ) {
850 $defaultPreferences[
'emailauthentication'] = [
853 'section' =>
'personal/email',
854 'label-message' =>
'prefs-emailconfirm-label',
855 'default' => $emailauthenticated,
857 'cssclass' => $emailauthenticationclass,
865 $defaultPreferences[
'disablemail'] = [
866 'id' =>
'wpAllowEmail',
869 'section' =>
'personal/email',
870 'label-message' =>
'allowemail',
871 'disabled' => $disableEmailPrefs,
874 $defaultPreferences[
'email-allow-new-users'] = [
875 'id' =>
'wpAllowEmailFromNewUsers',
877 'section' =>
'personal/email',
878 'label-message' =>
'email-allow-new-users-label',
879 'disabled' => $disableEmailPrefs,
880 'disable-if' => [
'!==',
'disablemail',
'1' ],
883 $defaultPreferences[
'ccmeonemails'] = [
885 'section' =>
'personal/email',
886 'label-message' =>
'tog-ccmeonemails',
887 'disabled' => $disableEmailPrefs,
891 $defaultPreferences[
'email-blacklist'] = [
892 'type' =>
'usersmultiselect',
893 'label-message' =>
'email-mutelist-label',
894 'section' =>
'personal/email',
895 'disabled' => $disableEmailPrefs,
896 'filter' => MultiUsernameFilter::class,
902 $defaultPreferences[
'enotifwatchlistpages'] = [
904 'section' =>
'personal/email',
905 'label-message' =>
'tog-enotifwatchlistpages',
906 'disabled' => $disableEmailPrefs,
910 $defaultPreferences[
'enotifusertalkpages'] = [
912 'section' =>
'personal/email',
913 'label-message' =>
'tog-enotifusertalkpages',
914 'disabled' => $disableEmailPrefs,
920 $defaultPreferences[
'enotifminoredits'] = [
922 'section' =>
'personal/email',
923 'label-message' =>
'tog-enotifminoredits',
924 'disabled' => $disableEmailPrefs,
929 $defaultPreferences[
'enotifrevealaddr'] = [
931 'section' =>
'personal/email',
932 'label-message' =>
'tog-enotifrevealaddr',
933 'disabled' => $disableEmailPrefs,
948 $validSkinNames = $this->getValidSkinNames( $user, $context );
949 if ( $validSkinNames ) {
950 $defaultPreferences[
'skin'] = [
954 'section' =>
'rendering/skin',
956 $hideCond = [
'AND' ];
957 foreach ( $validSkinNames as $skinName => $_ ) {
958 $options = $this->skinFactory->getSkinOptions( $skinName );
959 if (
$options[
'responsive'] ??
false ) {
960 $hideCond[] = [
'!==',
'skin', $skinName ];
963 if ( $hideCond === [
'AND' ] ) {
966 $defaultPreferences[
'skin-responsive'] = [
968 'label-message' =>
'prefs-skin-responsive',
969 'section' =>
'rendering/skin/skin-prefs',
970 'help-message' =>
'prefs-help-skin-responsive',
971 'hide-if' => $hideCond,
977 $safeMode = $this->userOptionsManager->getOption( $user,
'forcesafemode' );
981 if ( $allowUserCss || $allowUserJs ) {
983 $defaultPreferences[
'customcssjs-safemode'] = [
986 'default' => Html::warningBox( $context->
msg(
'prefs-custom-cssjs-safemode' )->parse() ),
987 'section' =>
'rendering/skin',
993 if ( $allowUserCss ) {
994 $cssPage = Title::makeTitleSafe(
NS_USER, $userName .
'/common.css' );
995 $cssLinkText = $context->
msg(
'prefs-custom-css' )->text();
996 $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
999 if ( $allowUserJs ) {
1000 $jsPage = Title::makeTitleSafe(
NS_USER, $userName .
'/common.js' );
1001 $jsLinkText = $context->
msg(
'prefs-custom-js' )->text();
1002 $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
1005 $defaultPreferences[
'commoncssjs'] = [
1008 'default' => $context->
getLanguage()->pipeList( $linkTools ),
1009 'label-message' =>
'prefs-common-config',
1010 'section' =>
'rendering/skin',
1021 $defaultPreferences[
'imagesize'] = [
1024 'label-message' =>
'imagemaxsize',
1025 'section' =>
'rendering/files',
1027 $defaultPreferences[
'thumbsize'] = [
1030 'label-message' =>
'thumbsize',
1031 'section' =>
'rendering/files',
1045 if ( $dateOptions ) {
1046 $defaultPreferences[
'date'] = [
1048 'options' => $dateOptions,
1049 'section' =>
'rendering/dateformat',
1056 $nowlocal = Xml::element(
'span', [
'id' =>
'wpLocalTime' ],
1057 $lang->userTime( $now, $user ) );
1058 $nowserver = $lang->userTime( $now, $user,
1059 [
'format' =>
false,
'timecorrection' =>
false ] ) .
1060 Html::hidden(
'wpServerTime', (
int)substr( $now, 8, 2 ) * 60 + (
int)substr( $now, 10, 2 ) );
1062 $defaultPreferences[
'nowserver'] = [
1065 'label-message' =>
'servertime',
1066 'default' => $nowserver,
1067 'section' =>
'rendering/timeoffset',
1070 $defaultPreferences[
'nowlocal'] = [
1073 'label-message' =>
'localtime',
1074 'default' => $nowlocal,
1075 'section' =>
'rendering/timeoffset',
1078 $userTimeCorrection = (string)$this->userOptionsManager->getOption( $user,
'timecorrection' );
1083 $userTimeCorrection,
1088 if ( $userTimeCorrectionObj->getCorrectionType() === UserTimeCorrection::OFFSET ) {
1089 $tzDefault = UserTimeCorrection::formatTimezoneOffset( $userTimeCorrectionObj->getTimeOffset() );
1091 $tzDefault = $userTimeCorrectionObj->toString();
1094 $defaultPreferences[
'timecorrection'] = [
1095 'type' =>
'timezone',
1096 'label-message' =>
'timezonelegend',
1097 'default' => $tzDefault,
1099 'section' =>
'rendering/timeoffset',
1100 'id' =>
'wpTimeCorrection',
1101 'filter' => TimezoneFilter::class,
1113 &$defaultPreferences
1116 $defaultPreferences[
'diffonly'] = [
1118 'section' =>
'rendering/diffs',
1119 'label-message' =>
'tog-diffonly',
1121 $defaultPreferences[
'norollbackdiff'] = [
1123 'section' =>
'rendering/diffs',
1124 'label-message' =>
'tog-norollbackdiff',
1126 $defaultPreferences[
'diff-type'] = [
1132 $defaultPreferences[
'underline'] = [
1135 $l10n->
msg(
'underline-never' )->text() => 0,
1136 $l10n->
msg(
'underline-always' )->text() => 1,
1137 $l10n->
msg(
'underline-default' )->text() => 2,
1139 'label-message' =>
'tog-underline',
1140 'section' =>
'rendering/advancedrendering',
1144 $defaultPreferences[
'showhiddencats'] = [
1146 'section' =>
'rendering/advancedrendering',
1147 'label-message' =>
'tog-showhiddencats'
1151 $defaultPreferences[
'showrollbackconfirmation'] = [
1153 'section' =>
'rendering/advancedrendering',
1154 'label-message' =>
'tog-showrollbackconfirmation',
1158 $defaultPreferences[
'forcesafemode'] = [
1160 'section' =>
'rendering/advancedrendering',
1161 'label-message' =>
'tog-forcesafemode',
1162 'help-message' =>
'prefs-help-forcesafemode'
1172 $defaultPreferences[
'editsectiononrightclick'] = [
1174 'section' =>
'editing/advancedediting',
1175 'label-message' =>
'tog-editsectiononrightclick',
1177 $defaultPreferences[
'editondblclick'] = [
1179 'section' =>
'editing/advancedediting',
1180 'label-message' =>
'tog-editondblclick',
1184 $defaultPreferences[
'editfont'] = [
1186 'section' =>
'editing/editor',
1187 'label-message' =>
'editfont-style',
1189 $l10n->
msg(
'editfont-monospace' )->text() =>
'monospace',
1190 $l10n->
msg(
'editfont-sansserif' )->text() =>
'sans-serif',
1191 $l10n->
msg(
'editfont-serif' )->text() =>
'serif',
1196 if ( $user->
isAllowed(
'minoredit' ) ) {
1197 $defaultPreferences[
'minordefault'] = [
1199 'section' =>
'editing/editor',
1200 'label-message' =>
'tog-minordefault',
1204 $defaultPreferences[
'forceeditsummary'] = [
1206 'section' =>
'editing/editor',
1207 'label-message' =>
'tog-forceeditsummary',
1212 $defaultPreferences[
'editrecovery'] = [
1214 'section' =>
'editing/editor',
1215 'label-message' =>
'tog-editrecovery',
1217 'tog-editrecovery-help',
1218 'https://meta.wikimedia.org/wiki/Talk:Community_Wishlist_Survey_2023/Edit-recovery_feature',
1223 $defaultPreferences[
'useeditwarning'] = [
1225 'section' =>
'editing/editor',
1226 'label-message' =>
'tog-useeditwarning',
1229 $defaultPreferences[
'previewonfirst'] = [
1231 'section' =>
'editing/preview',
1232 'label-message' =>
'tog-previewonfirst',
1234 $defaultPreferences[
'previewontop'] = [
1236 'section' =>
'editing/preview',
1237 'label-message' =>
'tog-previewontop',
1239 $defaultPreferences[
'uselivepreview'] = [
1241 'section' =>
'editing/preview',
1242 'label-message' =>
'tog-uselivepreview',
1253 $rcMax = ceil( $rcMaxAge / ( 3600 * 24 ) );
1254 $defaultPreferences[
'rcdays'] = [
1256 'label-message' =>
'recentchangesdays',
1257 'section' =>
'rc/displayrc',
1262 $defaultPreferences[
'rclimit'] = [
1266 'label-message' =>
'recentchangescount',
1267 'help-message' =>
'prefs-help-recentchangescount',
1268 'section' =>
'rc/displayrc',
1269 'filter' => IntvalFilter::class,
1271 $defaultPreferences[
'usenewrc'] = [
1273 'label-message' =>
'tog-usenewrc',
1274 'section' =>
'rc/advancedrc',
1276 $defaultPreferences[
'hideminor'] = [
1278 'label-message' =>
'tog-hideminor',
1279 'section' =>
'rc/changesrc',
1281 $defaultPreferences[
'pst-cssjs'] = [
1284 $defaultPreferences[
'rcfilters-rc-collapsed'] = [
1287 $defaultPreferences[
'rcfilters-wl-collapsed'] = [
1290 $defaultPreferences[
'rcfilters-saved-queries'] = [
1293 $defaultPreferences[
'rcfilters-wl-saved-queries'] = [
1297 $defaultPreferences[
'rcfilters-limit'] = [
1300 $defaultPreferences[
'rcfilters-saved-queries-versionbackup'] = [
1303 $defaultPreferences[
'rcfilters-wl-saved-queries-versionbackup'] = [
1308 $defaultPreferences[
'hidecategorization'] = [
1310 'label-message' =>
'tog-hidecategorization',
1311 'section' =>
'rc/changesrc',
1316 $defaultPreferences[
'hidepatrolled'] = [
1318 'section' =>
'rc/changesrc',
1319 'label-message' =>
'tog-hidepatrolled',
1324 $defaultPreferences[
'newpageshidepatrolled'] = [
1326 'section' =>
'rc/changesrc',
1327 'label-message' =>
'tog-newpageshidepatrolled',
1332 $defaultPreferences[
'shownumberswatching'] = [
1334 'section' =>
'rc/advancedrc',
1335 'label-message' =>
'tog-shownumberswatching',
1339 $defaultPreferences[
'rcenhancedfilters-disable'] = [
1341 'section' =>
'rc/advancedrc',
1342 'label-message' =>
'rcfilters-preference-label',
1343 'help-message' =>
'rcfilters-preference-help',
1357 if ( $user->
isAllowed(
'editmywatchlist' ) ) {
1358 $editWatchlistLinks =
'';
1359 $editWatchlistModes = [
1360 'edit' => [
'subpage' =>
false,
'flags' => [] ],
1361 'raw' => [
'subpage' =>
'raw',
'flags' => [] ],
1362 'clear' => [
'subpage' =>
'clear',
'flags' => [
'destructive' ] ],
1364 foreach ( $editWatchlistModes as $mode =>
$options ) {
1366 $editWatchlistLinks .=
1370 'label' =>
new HtmlSnippet(
1371 $context->
msg(
"prefs-editwatchlist-{$mode}" )->parse()
1376 $defaultPreferences[
'editwatchlist'] = [
1379 'default' => $editWatchlistLinks,
1380 'label-message' =>
'prefs-editwatchlist-label',
1381 'section' =>
'watchlist/editwatchlist',
1385 $defaultPreferences[
'watchlistdays'] = [
1388 'max' => $watchlistdaysMax,
1389 'section' =>
'watchlist/displaywatchlist',
1390 'help-message' => [
'prefs-watchlist-days-max',
Message::numParam( $watchlistdaysMax ) ],
1391 'label-message' =>
'prefs-watchlist-days',
1393 $defaultPreferences[
'wllimit'] = [
1397 'label-message' =>
'prefs-watchlist-edits',
1398 'help-message' =>
'prefs-watchlist-edits-max',
1399 'section' =>
'watchlist/displaywatchlist',
1400 'filter' => IntvalFilter::class,
1402 $defaultPreferences[
'extendwatchlist'] = [
1404 'section' =>
'watchlist/advancedwatchlist',
1405 'label-message' =>
'tog-extendwatchlist',
1407 $defaultPreferences[
'watchlisthideminor'] = [
1409 'section' =>
'watchlist/changeswatchlist',
1410 'label-message' =>
'tog-watchlisthideminor',
1412 $defaultPreferences[
'watchlisthidebots'] = [
1414 'section' =>
'watchlist/changeswatchlist',
1415 'label-message' =>
'tog-watchlisthidebots',
1417 $defaultPreferences[
'watchlisthideown'] = [
1419 'section' =>
'watchlist/changeswatchlist',
1420 'label-message' =>
'tog-watchlisthideown',
1422 $defaultPreferences[
'watchlisthideanons'] = [
1424 'section' =>
'watchlist/changeswatchlist',
1425 'label-message' =>
'tog-watchlisthideanons',
1427 $defaultPreferences[
'watchlisthideliu'] = [
1429 'section' =>
'watchlist/changeswatchlist',
1430 'label-message' =>
'tog-watchlisthideliu',
1434 $defaultPreferences[
'watchlistreloadautomatically'] = [
1436 'section' =>
'watchlist/advancedwatchlist',
1437 'label-message' =>
'tog-watchlistreloadautomatically',
1441 $defaultPreferences[
'watchlistunwatchlinks'] = [
1443 'section' =>
'watchlist/advancedwatchlist',
1444 'label-message' =>
'tog-watchlistunwatchlinks',
1448 $defaultPreferences[
'watchlisthidecategorization'] = [
1450 'section' =>
'watchlist/changeswatchlist',
1451 'label-message' =>
'tog-watchlisthidecategorization',
1456 $defaultPreferences[
'watchlisthidepatrolled'] = [
1458 'section' =>
'watchlist/changeswatchlist',
1459 'label-message' =>
'tog-watchlisthidepatrolled',
1464 'edit' =>
'watchdefault',
1465 'move' =>
'watchmoves',
1469 if ( $user->
isAllowedAny(
'createpage',
'createtalk' ) ) {
1470 $watchTypes[
'read'] =
'watchcreations';
1475 'rollback' =>
'watchrollback',
1476 'upload' =>
'watchuploads',
1477 'delete' =>
'watchdeletion',
1480 foreach ( $watchTypes as $action => $pref ) {
1485 $defaultPreferences[$pref] = [
1487 'section' =>
'watchlist/pageswatchlist',
1488 'label-message' =>
"tog-$pref",
1493 $defaultPreferences[
'watchlisttoken'] = [
1497 $tokenButton =
new ButtonWidget( [
1501 'label' => $context->
msg(
'prefs-watchlist-managetokens' )->text(),
1503 $defaultPreferences[
'watchlisttoken-info'] = [
1505 'section' =>
'watchlist/tokenwatchlist',
1506 'label-message' =>
'prefs-watchlist-token',
1507 'help-message' =>
'prefs-help-tokenmanagement',
1509 'default' => (string)$tokenButton,
1512 $defaultPreferences[
'wlenhancedfilters-disable'] = [
1514 'section' =>
'watchlist/advancedwatchlist',
1515 'label-message' =>
'rcfilters-watchlist-preference-label',
1516 'help-message' =>
'rcfilters-watchlist-preference-help',
1525 $defaultPreferences[
'search-special-page'] = [
1529 foreach ( $this->nsInfo->getValidNamespaces() as $n ) {
1530 $defaultPreferences[
'searchNs' . $n] = [
1536 $defaultPreferences[
'search-match-redirect'] = [
1538 'section' =>
'searchoptions/searchmisc',
1539 'label-message' =>
'search-match-redirect-label',
1540 'help-message' =>
'search-match-redirect-help',
1543 $defaultPreferences[
'search-match-redirect'] = [
1548 $defaultPreferences[
'searchlimit'] = [
1552 'section' =>
'searchoptions/searchmisc',
1553 'label-message' =>
'searchlimit-label',
1554 'help-message' => $context->
msg(
'searchlimit-help', 500 ),
1555 'filter' => IntvalFilter::class,
1561 $thumbNamespacesFormatted = array_combine(
1564 static function ( $namespaceId ) use ( $context ) {
1565 return $namespaceId ===
NS_MAIN
1566 ? $context->
msg(
'blanknamespace' )->escaped()
1567 : $context->
getLanguage()->getFormattedNsText( $namespaceId );
1572 $defaultThumbNamespacesFormatted =
1573 array_intersect_key( $thumbNamespacesFormatted, [
NS_FILE => 1 ] ) ?? [];
1574 $extraThumbNamespacesFormatted =
1575 array_diff_key( $thumbNamespacesFormatted, [
NS_FILE => 1 ] );
1576 if ( $extraThumbNamespacesFormatted ) {
1577 $defaultPreferences[
'search-thumbnail-extra-namespaces'] = [
1579 'section' =>
'searchoptions/searchmisc',
1580 'label-message' =>
'search-thumbnail-extra-namespaces-label',
1581 'help-message' => $context->
msg(
1582 'search-thumbnail-extra-namespaces-message',
1583 $context->
getLanguage()->listToText( $extraThumbNamespacesFormatted ),
1584 count( $extraThumbNamespacesFormatted ),
1585 $context->
getLanguage()->listToText( $defaultThumbNamespacesFormatted ),
1586 count( $defaultThumbNamespacesFormatted )
1601 private static function sortSkinNames( $a, $b, $currentSkin, $preferredSkins ) {
1603 if ( strcasecmp( $a, $currentSkin ) === 0 ) {
1606 if ( strcasecmp( $b, $currentSkin ) === 0 ) {
1610 if ( count( $preferredSkins ) ) {
1611 $aPreferred = array_search( $a, $preferredSkins );
1612 $bPreferred = array_search( $b, $preferredSkins );
1615 if ( $aPreferred !==
false && $bPreferred ===
false ) {
1618 if ( $aPreferred ===
false && $bPreferred !==
false ) {
1623 if ( $aPreferred !==
false && $bPreferred !==
false ) {
1624 return strcasecmp( $aPreferred, $bPreferred );
1628 return strcasecmp( $a, $b );
1639 private function getValidSkinNames( User $user, IContextSource $context ) {
1641 $validSkinNames = $this->skinFactory->getAllowedSkins();
1642 $allInstalledSkins = $this->skinFactory->getInstalledSkins();
1645 $useSkin = $context->getRequest()->getRawVal(
'useskin' );
1646 if ( isset( $allInstalledSkins[$useSkin] )
1647 && $context->msg(
"skinname-$useSkin" )->exists()
1649 $validSkinNames[$useSkin] = $useSkin;
1653 $currentUserSkin = $this->userOptionsManager->getOption( $user,
'skin' );
1654 if ( isset( $allInstalledSkins[$currentUserSkin] )
1655 && $context->msg(
"skinname-$currentUserSkin" )->exists()
1657 $validSkinNames[$currentUserSkin] = $currentUserSkin;
1660 foreach ( $validSkinNames as $skinkey => &$skinname ) {
1661 $msg = $context->msg(
"skinname-{$skinkey}" );
1662 if ( $msg->exists() ) {
1663 $skinname = htmlspecialchars( $msg->text() );
1670 uksort( $validSkinNames,
function ( $a, $b ) use ( $currentUserSkin, $preferredSkins ) {
1671 return $this->sortSkinNames( $a, $b, $currentUserSkin, $preferredSkins );
1674 return $validSkinNames;
1686 $mptitle = Title::newMainPage();
1687 $previewtext = $context->
msg(
'skin-preview' )->escaped();
1691 $safeMode = $this->userOptionsManager->getOption( $user,
'forcesafemode' );
1692 $foundDefault =
false;
1693 foreach ( $validSkinNames as $skinkey => $sn ) {
1697 if ( strcasecmp( $skinkey, $defaultSkin ) === 0 ) {
1698 $linkTools[] = $context->
msg(
'default' )->escaped();
1699 $foundDefault =
true;
1703 $talkPageMsg = $context->
msg(
"$skinkey-prefs-talkpage" );
1704 if ( $talkPageMsg->exists() ) {
1705 $linkTools[] = $talkPageMsg->parse();
1709 $mplink = htmlspecialchars( $mptitle->getLocalURL( [
'useskin' => $skinkey ] ) );
1710 $linkTools[] =
"<a target='_blank' href=\"$mplink\">$previewtext</a>";
1715 if ( $allowUserCss ) {
1716 $cssPage = Title::makeTitleSafe(
NS_USER, $user->
getName() .
'/' . $skinkey .
'.css' );
1717 $cssLinkText = $context->
msg(
'prefs-custom-css' )->text();
1718 $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
1721 if ( $allowUserJs ) {
1722 $jsPage = Title::makeTitleSafe(
NS_USER, $user->
getName() .
'/' . $skinkey .
'.js' );
1723 $jsLinkText = $context->
msg(
'prefs-custom-js' )->text();
1724 $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
1728 $display = $sn .
' ' . $context->
msg(
'parentheses' )
1729 ->rawParams( $context->
getLanguage()->pipeList( $linkTools ) )
1731 $ret[$display] = $skinkey;
1734 if ( !$foundDefault ) {
1749 $dateopts = $lang->getDatePreferences();
1754 if ( !in_array(
'default', $dateopts ) ) {
1755 $dateopts[] =
'default';
1765 foreach ( $dateopts as $key ) {
1766 if ( $key ==
'default' ) {
1767 $formatted = $context->
msg(
'datedefault' )->escaped();
1769 $formatted = htmlspecialchars( $lang->timeanddate( $epoch,
false, $key ) );
1771 $ret[$formatted] = $key;
1783 $pixels = $l10n->
msg(
'unit-pixel' )->text();
1787 $display =
"{$limits[0]}\u{200E}×{$limits[1]}$pixels";
1788 $ret[$display] = $index;
1800 $pixels = $l10n->
msg(
'unit-pixel' )->text();
1803 $display = $size . $pixels;
1804 $ret[$display] = $index;
1819 if ( is_string( $signature ) && mb_strlen( $signature ) > $maxSigChars ) {
1820 return $form->
msg(
'badsiglength' )->numParams( $maxSigChars )->escaped();
1823 if ( $signature ===
null || $signature ===
'' ) {
1830 if ( !( isset( $alldata[
'fancysig'] ) && $alldata[
'fancysig'] ) ) {
1847 $signature === $this->userOptionsManager->getOption( $user,
'nickname' ) &&
1848 (
bool)$alldata[
'fancysig'] === $this->userOptionsManager->getBoolOption( $user,
'fancysig' )
1853 if ( $sigValidation ===
'new' || $sigValidation ===
'disallow' ) {
1855 $parserOpts = ParserOptions::newFromContext( $form->
getContext() );
1856 $validator = $this->signatureValidatorFactory
1857 ->newSignatureValidator( $user, $form->
getContext(), $parserOpts );
1858 $errors = $validator->validateSignature( $signature );
1867 if ( $this->parserFactory->getInstance()->validateSig( $signature ) ===
false ) {
1868 return $form->
msg(
'badsig' )->escaped();
1881 if ( isset( $alldata[
'fancysig'] ) && $alldata[
'fancysig'] ) {
1882 $signature = $this->parserFactory->getInstance()->cleanSig( $signature );
1885 $signature = Parser::cleanSigInSig( $signature );
1901 $formClass = PreferencesFormOOUI::class,
1909 if ( count( $remove ) ) {
1910 $removeKeys = array_fill_keys( $remove,
true );
1911 $formDescriptor = array_diff_key( $formDescriptor, $removeKeys );
1915 foreach ( $formDescriptor as $name => $info ) {
1916 if ( isset( $info[
'type'] ) && $info[
'type'] ===
'api' ) {
1917 unset( $formDescriptor[$name] );
1924 $htmlForm =
new $formClass( $formDescriptor, $context,
'prefs' );
1930 $htmlForm->setAction( $context->
getTitle()->getLocalURL( [
1931 'useskin' => $context->
getRequest()->getRawVal(
'useskin' )
1934 $htmlForm->setModifiedUser( $user );
1935 $htmlForm->setOptionsEditable( $user->
isAllowed(
'editmyoptions' ) );
1936 $htmlForm->setPrivateInfoEditable( $user->
isAllowed(
'editmyprivateinfo' ) );
1937 $htmlForm->setId(
'mw-prefs-form' );
1938 $htmlForm->setAutocomplete(
'off' );
1939 $htmlForm->setSubmitTextMsg(
'saveprefs' );
1941 $htmlForm->setSubmitTooltip(
'preferences-save' );
1942 $htmlForm->setSubmitID(
'prefcontrol' );
1943 $htmlForm->setSubmitCallback(
1945 return $this->
submitForm( $formData, $form, $formDescriptor );
1965 if ( !$user->isAllowedAny(
'editmyprivateinfo',
'editmyoptions' ) ) {
1966 return Status::newFatal(
'mypreferencesprotected' );
1970 $this->
applyFilters( $formData, $formDescriptor,
'filterFromForm' );
1975 if ( !in_array(
'realname', $hiddenPrefs )
1976 && $user->isAllowed(
'editmyprivateinfo' )
1977 && array_key_exists(
'realname', $formData )
1979 $realName = $formData[
'realname'];
1980 $user->setRealName( $realName );
1983 if ( $user->isAllowed(
'editmyoptions' ) ) {
1984 $oldUserOptions = $this->userOptionsManager->getOptions( $user );
1987 unset( $formData[$b] );
1993 foreach ( $hiddenPrefs as $pref ) {
1996 $formData[$pref] = $this->userOptionsManager->getOption( $user, $pref,
null,
true );
2001 isset( $formData[
'rclimit'] ) &&
2002 intval( $formData[
'rclimit' ] ) !== $this->userOptionsManager->getIntOption( $user,
'rclimit' )
2004 $formData[
'rcfilters-limit'] = $formData[
'rclimit'];
2008 $this->userOptionsManager->resetOptions( $user, $form->
getContext(),
'unused' );
2010 foreach ( $formData as $key => $value ) {
2011 $this->userOptionsManager->setOption( $user, $key, $value );
2014 $this->hookRunner->onPreferencesFormPreSave(
2015 $formData, $form, $user, $result, $oldUserOptions );
2018 $user->saveSettings();
2031 protected function applyFilters( array &$preferences, array $formDescriptor, $verb ) {
2032 foreach ( $formDescriptor as $preference => $desc ) {
2033 if ( !isset( $desc[
'filter'] ) || !isset( $preferences[$preference] ) ) {
2036 $filterDesc = $desc[
'filter'];
2037 if ( $filterDesc instanceof
Filter ) {
2038 $filter = $filterDesc;
2039 } elseif ( class_exists( $filterDesc ) ) {
2040 $filter =
new $filterDesc();
2041 } elseif ( is_callable( $filterDesc ) ) {
2042 $filter = $filterDesc();
2044 throw new UnexpectedValueException(
2045 "Unrecognized filter type for preference '$preference'"
2048 $preferences[$preference] = $filter->$verb( $preferences[$preference] );
2063 array $formDescriptor
2065 $res = $this->
saveFormData( $formData, $form, $formDescriptor );
2067 if ( $res ===
true ) {
2073 $url = $form->
getTitle()->getFullURL( $urlOptions );
2076 $context->getRequest()->getSession()->set(
'specialPreferencesSaveSuccess', 1 );
2078 $context->getOutput()->redirect( $url );
2081 return ( $res ===
true ? Status::newGood() : $res );
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
array $params
The job parameters.
Methods for dealing with language codes.
Base class for multi-variant language conversion.
Base class for language-specific code.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
getContext()
Get the base IContextSource object.
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 AllowRequiringEmailForResets
Name constant for the AllowRequiringEmailForResets 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()
This is one of the Core classes and should be read at least once by any new developers.
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,...
Set options of the Parser.
Factory class to create Skin objects.
Module of static functions for generating XML.
$wgDefaultUserOptions
Config variable stub for the DefaultUserOptions setting, for use by phpdoc and IDEs.
The shared interface for all language converters.
Interface for objects which can provide a MediaWiki context on request.
Interface for localizing messages in MediaWiki.
msg( $key,... $params)
This is the method for getting translated interface messages.