MediaWiki REL1_39
DefaultPreferencesFactory.php
Go to the documentation of this file.
1<?php
22
23use DateTime;
24use DateTimeZone;
25use Exception;
26use Html;
27use HTMLForm;
31use Language;
32use LanguageCode;
48use Message;
50use MWException;
51use MWTimestamp;
53use OutputPage;
54use Parser;
57use Psr\Log\LoggerAwareTrait;
58use Psr\Log\NullLogger;
59use SkinFactory;
60use SpecialPage;
61use Status;
62use Title;
63use UnexpectedValueException;
64use User;
66use Wikimedia\RequestTimeout\TimeoutException;
67use Xml;
68
73 use LoggerAwareTrait;
74
76 protected $options;
77
79 protected $contLang;
80
83
85 protected $authManager;
86
88 protected $linkRenderer;
89
91 protected $nsInfo;
92
95
97 private $languageConverter;
98
100 private $hookRunner;
101
103 private $userOptionsManager;
104
106 private $languageConverterFactory;
107
109 private $parser;
110
112 private $skinFactory;
113
115 private $userGroupManager;
116
118 private $signatureValidatorFactory;
119
123 public const CONSTRUCTOR_OPTIONS = [
152 ];
153
171 public function __construct(
178 ILanguageConverter $languageConverter,
180 HookContainer $hookContainer,
181 UserOptionsLookup $userOptionsLookup,
182 LanguageConverterFactory $languageConverterFactory = null,
183 Parser $parser = null,
184 SkinFactory $skinFactory = null,
185 UserGroupManager $userGroupManager = null,
186 SignatureValidatorFactory $signatureValidatorFactory = null
187 ) {
188 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
189
190 $this->options = $options;
191 $this->contLang = $contLang;
192 $this->authManager = $authManager;
193 $this->linkRenderer = $linkRenderer;
194 $this->nsInfo = $nsInfo;
195
196 // We don't use the PermissionManager anymore, but we need to be careful
197 // removing the parameter since this class is extended by GlobalPreferencesFactory
198 // in the GlobalPreferences extension, and that class uses it
199 $this->permissionManager = $permissionManager;
200
201 $this->logger = new NullLogger();
202 $this->languageConverter = $languageConverter;
203 $this->languageNameUtils = $languageNameUtils;
204 $this->hookRunner = new HookRunner( $hookContainer );
205
206 // Don't break GlobalPreferences, fall back to global state if missing services
207 // or if passed a UserOptionsLookup that isn't UserOptionsManager
208 $services = static function () {
209 // BC hack. Use a closure so this can be unit-tested.
211 };
212 $this->userOptionsManager = ( $userOptionsLookup instanceof UserOptionsManager )
213 ? $userOptionsLookup
214 : $services()->getUserOptionsManager();
215 $this->languageConverterFactory = $languageConverterFactory ?? $services()->getLanguageConverterFactory();
216
217 $this->parser = $parser ?? $services()->getParser();
218 $this->skinFactory = $skinFactory ?? $services()->getSkinFactory();
219 $this->userGroupManager = $userGroupManager ?? $services()->getUserGroupManager();
220 $this->signatureValidatorFactory = $signatureValidatorFactory
221 ?? $services()->getSignatureValidatorFactory();
222 }
223
227 public function getSaveBlacklist() {
228 return [
229 'realname',
230 'emailaddress',
231 ];
232 }
233
240 public function getFormDescriptor( User $user, IContextSource $context ) {
241 $preferences = [];
242
243 OutputPage::setupOOUI(
244 strtolower( $context->getSkin()->getSkinName() ),
245 $context->getLanguage()->getDir()
246 );
247
248 $this->profilePreferences( $user, $context, $preferences );
249 $this->skinPreferences( $user, $context, $preferences );
250 $this->datetimePreferences( $user, $context, $preferences );
251 $this->filesPreferences( $context, $preferences );
252 $this->renderingPreferences( $user, $context, $preferences );
253 $this->editingPreferences( $user, $context, $preferences );
254 $this->rcPreferences( $user, $context, $preferences );
255 $this->watchlistPreferences( $user, $context, $preferences );
256 $this->searchPreferences( $context, $preferences );
257
258 $this->hookRunner->onGetPreferences( $user, $preferences );
259
260 $this->loadPreferenceValues( $user, $context, $preferences );
261 $this->logger->debug( "Created form descriptor for user '{$user->getName()}'" );
262 return $preferences;
263 }
264
271 public static function simplifyFormDescriptor( array $descriptor ) {
272 foreach ( $descriptor as $name => &$params ) {
273 // Info fields are useless and can use complicated closure to provide
274 // text, skip all of them.
275 if ( ( isset( $params['type'] ) && $params['type'] === 'info' ) ||
276 ( isset( $params['class'] ) && $params['class'] === \HTMLInfoField::class )
277 ) {
278 unset( $descriptor[$name] );
279 continue;
280 }
281 // Message parsing is the heaviest load when constructing the field,
282 // but we just want to validate data.
283 foreach ( $params as $key => $value ) {
284 switch ( $key ) {
285 // Special case, should be kept.
286 case 'options-message':
287 break;
288 // Special case, should be transferred.
289 case 'options-messages':
290 unset( $params[$key] );
291 $params['options'] = $value;
292 break;
293 default:
294 if ( preg_match( '/-messages?$/', $key ) ) {
295 // Unwanted.
296 unset( $params[$key] );
297 }
298 }
299 }
300 }
301 return $descriptor;
302 }
303
312 private function loadPreferenceValues( User $user, IContextSource $context, &$defaultPreferences ) {
313 // Remove preferences that wikis don't want to use
314 foreach ( $this->options->get( MainConfigNames::HiddenPrefs ) as $pref ) {
315 unset( $defaultPreferences[$pref] );
316 }
317
318 // For validation.
319 $simplified = self::simplifyFormDescriptor( $defaultPreferences );
320 $form = new HTMLForm( $simplified, $context );
321
322 $disable = !$user->isAllowed( 'editmyoptions' );
323
324 $defaultOptions = $this->userOptionsManager->getDefaultOptions();
325 $userOptions = $this->userOptionsManager->getOptions( $user );
326 $this->applyFilters( $userOptions, $defaultPreferences, 'filterForForm' );
327 // Add in defaults from the user
328 foreach ( $simplified as $name => $_ ) {
329 $info = &$defaultPreferences[$name];
330 if ( $disable && !in_array( $name, $this->getSaveBlacklist() ) ) {
331 $info['disabled'] = 'disabled';
332 }
333 if ( isset( $info['default'] ) ) {
334 // Already set, no problem
335 continue;
336 }
337 $field = $form->getField( $name );
338 $globalDefault = $defaultOptions[$name] ?? null;
339 $prefFromUser = $this->getOptionFromUser( $name, $info, $userOptions );
340
341 // If it validates, set it as the default
342 // FIXME: That's not how the validate() function works! Values of nested fields
343 // (e.g. CheckMatix) would be missing.
344 if ( $prefFromUser !== null && // Make sure we're not just pulling nothing
345 $field->validate( $prefFromUser, $this->userOptionsManager->getOptions( $user ) ) === true ) {
346 $info['default'] = $prefFromUser;
347 } elseif ( $field->validate( $globalDefault, $this->userOptionsManager->getOptions( $user ) ) === true ) {
348 $info['default'] = $globalDefault;
349 } else {
350 $globalDefault = json_encode( $globalDefault );
351 throw new MWException(
352 "Default '$globalDefault' is invalid for preference $name of user " . $user->getName()
353 );
354 }
355 }
356
357 return $defaultPreferences;
358 }
359
368 protected function getOptionFromUser( $name, $info, array $userOptions ) {
369 $val = $userOptions[$name] ?? null;
370
371 // Handling for multiselect preferences
372 if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
373 ( isset( $info['class'] ) && $info['class'] == \HTMLMultiSelectField::class ) ) {
374 $options = HTMLFormField::flattenOptions( $info['options-messages'] ?? $info['options'] );
375 $prefix = $info['prefix'] ?? $name;
376 $val = [];
377
378 foreach ( $options as $value ) {
379 if ( $userOptions["$prefix$value"] ?? false ) {
380 $val[] = $value;
381 }
382 }
383 }
384
385 // Handling for checkmatrix preferences
386 if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
387 ( isset( $info['class'] ) && $info['class'] == \HTMLCheckMatrix::class ) ) {
388 $columns = HTMLFormField::flattenOptions( $info['columns'] );
389 $rows = HTMLFormField::flattenOptions( $info['rows'] );
390 $prefix = $info['prefix'] ?? $name;
391 $val = [];
392
393 foreach ( $columns as $column ) {
394 foreach ( $rows as $row ) {
395 if ( $userOptions["$prefix$column-$row"] ?? false ) {
396 $val[] = "$column-$row";
397 }
398 }
399 }
400 }
401
402 return $val;
403 }
404
412 protected function profilePreferences(
413 User $user, IContextSource $context, &$defaultPreferences
414 ) {
415 // retrieving user name for GENDER and misc.
416 $userName = $user->getName();
417
418 // Information panel
419 $defaultPreferences['username'] = [
420 'type' => 'info',
421 'label-message' => [ 'username', $userName ],
422 'default' => $userName,
423 'section' => 'personal/info',
424 ];
425
426 $lang = $context->getLanguage();
427
428 // Get groups to which the user belongs, Skip the default * group, seems useless here
429 $userEffectiveGroups = array_diff(
430 $this->userGroupManager->getUserEffectiveGroups( $user ),
431 [ '*' ]
432 );
433 $defaultPreferences['usergroups'] = [
434 'type' => 'info',
435 'label-message' => [ 'prefs-memberingroups',
436 \Message::numParam( count( $userEffectiveGroups ) ), $userName ],
437 'default' => function () use ( $user, $userEffectiveGroups, $context, $lang, $userName ) {
438 $userGroupMemberships = $this->userGroupManager->getUserGroupMemberships( $user );
439 $userGroups = $userMembers = $userTempGroups = $userTempMembers = [];
440 foreach ( $userEffectiveGroups as $ueg ) {
441 $groupStringOrObject = $userGroupMemberships[$ueg] ?? $ueg;
442
443 $userG = UserGroupMembership::getLink( $groupStringOrObject, $context, 'html' );
444 $userM = UserGroupMembership::getLink( $groupStringOrObject, $context, 'html',
445 $userName );
446
447 // Store expiring groups separately, so we can place them before non-expiring
448 // groups in the list. This is to avoid the ambiguity of something like
449 // "administrator, bureaucrat (until X date)" -- users might wonder whether the
450 // expiry date applies to both groups, or just the last one
451 if ( $groupStringOrObject instanceof UserGroupMembership &&
452 $groupStringOrObject->getExpiry()
453 ) {
454 $userTempGroups[] = $userG;
455 $userTempMembers[] = $userM;
456 } else {
457 $userGroups[] = $userG;
458 $userMembers[] = $userM;
459 }
460 }
461 sort( $userGroups );
462 sort( $userMembers );
463 sort( $userTempGroups );
464 sort( $userTempMembers );
465 $userGroups = array_merge( $userTempGroups, $userGroups );
466 $userMembers = array_merge( $userTempMembers, $userMembers );
467 return $context->msg( 'prefs-memberingroups-type' )
468 ->rawParams( $lang->commaList( $userGroups ), $lang->commaList( $userMembers ) )
469 ->escaped();
470 },
471 'raw' => true,
472 'section' => 'personal/info',
473 ];
474
475 $contribTitle = SpecialPage::getTitleFor( "Contributions", $userName );
476 $formattedEditCount = $lang->formatNum( $user->getEditCount() );
477 $editCount = $this->linkRenderer->makeLink( $contribTitle, $formattedEditCount );
478
479 $defaultPreferences['editcount'] = [
480 'type' => 'info',
481 'raw' => true,
482 'label-message' => 'prefs-edits',
483 'default' => $editCount,
484 'section' => 'personal/info',
485 ];
486
487 if ( $user->getRegistration() ) {
488 $displayUser = $context->getUser();
489 $userRegistration = $user->getRegistration();
490 $defaultPreferences['registrationdate'] = [
491 'type' => 'info',
492 'label-message' => 'prefs-registration',
493 'default' => $context->msg(
494 'prefs-registration-date-time',
495 $lang->userTimeAndDate( $userRegistration, $displayUser ),
496 $lang->userDate( $userRegistration, $displayUser ),
497 $lang->userTime( $userRegistration, $displayUser )
498 )->text(),
499 'section' => 'personal/info',
500 ];
501 }
502
503 $canViewPrivateInfo = $user->isAllowed( 'viewmyprivateinfo' );
504 $canEditPrivateInfo = $user->isAllowed( 'editmyprivateinfo' );
505
506 // Actually changeable stuff
507 $defaultPreferences['realname'] = [
508 // (not really "private", but still shouldn't be edited without permission)
509 'type' => $canEditPrivateInfo && $this->authManager->allowsPropertyChange( 'realname' )
510 ? 'text' : 'info',
511 'default' => $user->getRealName(),
512 'section' => 'personal/info',
513 'label-message' => 'yourrealname',
514 'help-message' => 'prefs-help-realname',
515 ];
516
517 if ( $canEditPrivateInfo && $this->authManager->allowsAuthenticationDataChange(
518 new PasswordAuthenticationRequest(), false )->isGood()
519 ) {
520 $defaultPreferences['password'] = [
521 'type' => 'info',
522 'raw' => true,
523 'default' => (string)new \OOUI\ButtonWidget( [
524 'href' => SpecialPage::getTitleFor( 'ChangePassword' )->getLinkURL( [
525 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
526 ] ),
527 'label' => $context->msg( 'prefs-resetpass' )->text(),
528 ] ),
529 'label-message' => 'yourpassword',
530 // email password reset feature only works for users that have an email set up
531 'help' => $this->options->get( MainConfigNames::AllowRequiringEmailForResets ) &&
532 $user->getEmail()
533 ? $context->msg( 'prefs-help-yourpassword',
534 '[[#mw-prefsection-personal-email|{{int:prefs-email}}]]' )->parse()
535 : '',
536 'section' => 'personal/info',
537 ];
538 }
539 // Only show prefershttps if secure login is turned on
540 if ( !$this->options->get( MainConfigNames::ForceHTTPS )
541 && $this->options->get( MainConfigNames::SecureLogin )
542 ) {
543 $defaultPreferences['prefershttps'] = [
544 'type' => 'toggle',
545 'label-message' => 'tog-prefershttps',
546 'help-message' => 'prefs-help-prefershttps',
547 'section' => 'personal/info'
548 ];
549 }
550
551 $defaultPreferences['downloaduserdata'] = [
552 'type' => 'info',
553 'raw' => true,
554 'label-message' => 'prefs-user-downloaddata-label',
555 'default' => Html::element(
556 'a',
557 [
558 'href' => $this->options->get( MainConfigNames::ScriptPath ) .
559 '/api.php?action=query&meta=userinfo&uiprop=*',
560 ],
561 $context->msg( 'prefs-user-downloaddata-info' )->text()
562 ),
563 'help-message' => [ 'prefs-user-downloaddata-help-message', urlencode( $user->getTitleKey() ) ],
564 'section' => 'personal/info',
565 ];
566
567 $defaultPreferences['restoreprefs'] = [
568 'type' => 'info',
569 'raw' => true,
570 'label-message' => 'prefs-user-restoreprefs-label',
571 'default' => Html::element(
572 'a',
573 [
574 'href' => SpecialPage::getTitleFor( 'Preferences' )
575 ->getSubpage( 'reset' )->getLocalURL()
576 ],
577 $context->msg( 'prefs-user-restoreprefs-info' )->text()
578 ),
579 'section' => 'personal/info',
580 ];
581
582 $languages = $this->languageNameUtils->getLanguageNames(
583 LanguageNameUtils::AUTONYMS,
584 LanguageNameUtils::SUPPORTED
585 );
586 $languageCode = $this->options->get( MainConfigNames::LanguageCode );
587 if ( !array_key_exists( $languageCode, $languages ) ) {
588 $languages[$languageCode] = $languageCode;
589 // Sort the array again
590 ksort( $languages );
591 }
592
593 $options = [];
594 foreach ( $languages as $code => $name ) {
595 $display = LanguageCode::bcp47( $code ) . ' - ' . $name;
596 $options[$display] = $code;
597 }
598 $defaultPreferences['language'] = [
599 'type' => 'select',
600 'section' => 'personal/i18n',
601 'options' => $options,
602 'label-message' => 'yourlanguage',
603 ];
604
605 $neutralGenderMessage = $context->msg( 'gender-notknown' )->escaped() . (
606 !$context->msg( 'gender-unknown' )->isDisabled()
607 ? "<br>" . $context->msg( 'parentheses' )
608 ->params( $context->msg( 'gender-unknown' )->plain() )
609 ->escaped()
610 : ''
611 );
612
613 $defaultPreferences['gender'] = [
614 'type' => 'radio',
615 'section' => 'personal/i18n',
616 'options' => [
617 $neutralGenderMessage => 'unknown',
618 $context->msg( 'gender-female' )->escaped() => 'female',
619 $context->msg( 'gender-male' )->escaped() => 'male',
620 ],
621 'label-message' => 'yourgender',
622 'help-message' => 'prefs-help-gender',
623 ];
624
625 // see if there are multiple language variants to choose from
626 if ( !$this->languageConverterFactory->isConversionDisabled() ) {
627
628 foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
629 if ( $langCode == $this->contLang->getCode() ) {
630 if ( !$this->languageConverter->hasVariants() ) {
631 continue;
632 }
633
634 $variants = $this->languageConverter->getVariants();
635 $variantArray = [];
636 foreach ( $variants as $v ) {
637 $v = str_replace( '_', '-', strtolower( $v ) );
638 $variantArray[$v] = $lang->getVariantname( $v, false );
639 }
640
641 $options = [];
642 foreach ( $variantArray as $code => $name ) {
643 $display = LanguageCode::bcp47( $code ) . ' - ' . $name;
644 $options[$display] = $code;
645 }
646
647 $defaultPreferences['variant'] = [
648 'label-message' => 'yourvariant',
649 'type' => 'select',
650 'options' => $options,
651 'section' => 'personal/i18n',
652 'help-message' => 'prefs-help-variant',
653 ];
654 } else {
655 $defaultPreferences["variant-$langCode"] = [
656 'type' => 'api',
657 ];
658 }
659 }
660 }
661
662 // show a preview of the old signature first
663 $oldsigWikiText = $this->parser->preSaveTransform(
664 '~~~',
665 $context->getTitle(),
666 $user,
667 ParserOptions::newFromContext( $context )
668 );
669 $oldsigHTML = Parser::stripOuterParagraph(
670 $context->getOutput()->parseAsContent( $oldsigWikiText )
671 );
672 $signatureFieldConfig = [];
673 // Validate existing signature and show a message about it
674 $signature = $this->userOptionsManager->getOption( $user, 'nickname' );
675 $useFancySig = $this->userOptionsManager->getBoolOption( $user, 'fancysig' );
676 if ( $useFancySig && $signature !== '' ) {
677 $parserOpts = ParserOptions::newFromContext( $context );
678 $validator = $this->signatureValidatorFactory
679 ->newSignatureValidator( $user, $context, $parserOpts );
680 $signatureErrors = $validator->validateSignature( $signature );
681 if ( $signatureErrors ) {
682 $sigValidation = $this->options->get( MainConfigNames::SignatureValidation );
683 $oldsigHTML .= '<p><strong>' .
684 // Messages used here:
685 // * prefs-signature-invalid-warning
686 // * prefs-signature-invalid-new
687 // * prefs-signature-invalid-disallow
688 $context->msg( "prefs-signature-invalid-$sigValidation" )->parse() .
689 '</strong></p>';
690
691 // On initial page load, show the warnings as well
692 // (when posting, you get normal validation errors instead)
693 foreach ( $signatureErrors as &$sigError ) {
694 $sigError = new \OOUI\HtmlSnippet( $sigError );
695 }
696 if ( !$context->getRequest()->wasPosted() ) {
697 $signatureFieldConfig = [
698 'warnings' => $sigValidation !== 'disallow' ? $signatureErrors : null,
699 'errors' => $sigValidation === 'disallow' ? $signatureErrors : null,
700 ];
701 }
702 }
703 }
704 $defaultPreferences['oldsig'] = [
705 'type' => 'info',
706 // Normally HTMLFormFields do not display warnings, so we need to use 'rawrow'
707 // and provide the entire OOUI\FieldLayout here
708 'rawrow' => true,
709 'default' => new \OOUI\FieldLayout(
710 new \OOUI\LabelWidget( [
711 'label' => new \OOUI\HtmlSnippet( $oldsigHTML ),
712 ] ),
713 [
714 'align' => 'top',
715 'label' => new \OOUI\HtmlSnippet( $context->msg( 'tog-oldsig' )->parse() )
716 ] + $signatureFieldConfig
717 ),
718 'section' => 'personal/signature',
719 ];
720 $defaultPreferences['nickname'] = [
721 'type' => $this->authManager->allowsPropertyChange( 'nickname' ) ? 'text' : 'info',
722 'maxlength' => $this->options->get( MainConfigNames::MaxSigChars ),
723 'label-message' => 'yournick',
724 'validation-callback' => function ( $signature, $alldata, HTMLForm $form ) {
725 return $this->validateSignature( $signature, $alldata, $form );
726 },
727 'section' => 'personal/signature',
728 'filter-callback' => function ( $signature, array $alldata, HTMLForm $form ) {
729 return $this->cleanSignature( $signature, $alldata, $form );
730 },
731 ];
732 $defaultPreferences['fancysig'] = [
733 'type' => 'toggle',
734 'label-message' => 'tog-fancysig',
735 // show general help about signature at the bottom of the section
736 'help-message' => 'prefs-help-signature',
737 'section' => 'personal/signature'
738 ];
739
740 // Email preferences
741 if ( $this->options->get( MainConfigNames::EnableEmail ) ) {
742 if ( $canViewPrivateInfo ) {
743 $helpMessages = [];
744 $helpMessages[] = $this->options->get( MainConfigNames::EmailConfirmToEdit )
745 ? 'prefs-help-email-required'
746 : 'prefs-help-email';
747
748 if ( $this->options->get( MainConfigNames::EnableUserEmail ) ) {
749 // additional messages when users can send email to each other
750 $helpMessages[] = 'prefs-help-email-others';
751 }
752
753 $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
754 if ( $canEditPrivateInfo && $this->authManager->allowsPropertyChange( 'emailaddress' ) ) {
755 $button = new \OOUI\ButtonWidget( [
756 'href' => SpecialPage::getTitleFor( 'ChangeEmail' )->getLinkURL( [
757 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
758 ] ),
759 'label' =>
760 $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(),
761 ] );
762
763 $emailAddress .= $emailAddress == '' ? $button : ( '<br />' . $button );
764 }
765
766 $defaultPreferences['emailaddress'] = [
767 'type' => 'info',
768 'raw' => true,
769 'default' => $emailAddress,
770 'label-message' => 'youremail',
771 'section' => 'personal/email',
772 'help-messages' => $helpMessages,
773 // 'cssclass' chosen below
774 ];
775 }
776
777 $disableEmailPrefs = false;
778
779 if ( $this->options->get( MainConfigNames::AllowRequiringEmailForResets ) ) {
780 $defaultPreferences['requireemail'] = [
781 'type' => 'toggle',
782 'label-message' => 'tog-requireemail',
783 'help-message' => 'prefs-help-requireemail',
784 'section' => 'personal/email',
785 'disabled' => $user->getEmail() ? false : true,
786 ];
787 }
788
789 if ( $this->options->get( MainConfigNames::EmailAuthentication ) ) {
790 if ( $user->getEmail() ) {
791 if ( $user->getEmailAuthenticationTimestamp() ) {
792 // date and time are separate parameters to facilitate localisation.
793 // $time is kept for backward compat reasons.
794 // 'emailauthenticated' is also used in SpecialConfirmemail.php
795 $displayUser = $context->getUser();
796 $emailTimestamp = $user->getEmailAuthenticationTimestamp();
797 $time = $lang->userTimeAndDate( $emailTimestamp, $displayUser );
798 $d = $lang->userDate( $emailTimestamp, $displayUser );
799 $t = $lang->userTime( $emailTimestamp, $displayUser );
800 $emailauthenticated = $context->msg( 'emailauthenticated',
801 $time, $d, $t )->parse() . '<br />';
802 $emailauthenticationclass = 'mw-email-authenticated';
803 } else {
804 $disableEmailPrefs = true;
805 $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
806 new \OOUI\ButtonWidget( [
807 'href' => SpecialPage::getTitleFor( 'Confirmemail' )->getLinkURL(),
808 'label' => $context->msg( 'emailconfirmlink' )->text(),
809 ] );
810 $emailauthenticationclass = "mw-email-not-authenticated";
811 }
812 } else {
813 $disableEmailPrefs = true;
814 $emailauthenticated = $context->msg( 'noemailprefs' )->escaped();
815 $emailauthenticationclass = 'mw-email-none';
816 }
817
818 if ( $canViewPrivateInfo ) {
819 $defaultPreferences['emailauthentication'] = [
820 'type' => 'info',
821 'raw' => true,
822 'section' => 'personal/email',
823 'label-message' => 'prefs-emailconfirm-label',
824 'default' => $emailauthenticated,
825 // Apply the same CSS class used on the input to the message:
826 'cssclass' => $emailauthenticationclass,
827 ];
828 }
829 }
830
831 if ( $this->options->get( MainConfigNames::EnableUserEmail ) &&
832 $user->isAllowed( 'sendemail' )
833 ) {
834 $defaultPreferences['disablemail'] = [
835 'id' => 'wpAllowEmail',
836 'type' => 'toggle',
837 'invert' => true,
838 'section' => 'personal/email',
839 'label-message' => 'allowemail',
840 'disabled' => $disableEmailPrefs,
841 ];
842
843 $defaultPreferences['email-allow-new-users'] = [
844 'id' => 'wpAllowEmailFromNewUsers',
845 'type' => 'toggle',
846 'section' => 'personal/email',
847 'label-message' => 'email-allow-new-users-label',
848 'disabled' => $disableEmailPrefs,
849 'disable-if' => [ '!==', 'disablemail', '1' ],
850 ];
851
852 $defaultPreferences['ccmeonemails'] = [
853 'type' => 'toggle',
854 'section' => 'personal/email',
855 'label-message' => 'tog-ccmeonemails',
856 'disabled' => $disableEmailPrefs,
857 ];
858
859 if ( $this->options->get( MainConfigNames::EnableUserEmailMuteList ) ) {
860 $defaultPreferences['email-blacklist'] = [
861 'type' => 'usersmultiselect',
862 'label-message' => 'email-mutelist-label',
863 'section' => 'personal/email',
864 'disabled' => $disableEmailPrefs,
865 'filter' => MultiUsernameFilter::class,
866 ];
867 }
868 }
869
870 if ( $this->options->get( MainConfigNames::EnotifWatchlist ) ) {
871 $defaultPreferences['enotifwatchlistpages'] = [
872 'type' => 'toggle',
873 'section' => 'personal/email',
874 'label-message' => 'tog-enotifwatchlistpages',
875 'disabled' => $disableEmailPrefs,
876 ];
877 }
878 if ( $this->options->get( MainConfigNames::EnotifUserTalk ) ) {
879 $defaultPreferences['enotifusertalkpages'] = [
880 'type' => 'toggle',
881 'section' => 'personal/email',
882 'label-message' => 'tog-enotifusertalkpages',
883 'disabled' => $disableEmailPrefs,
884 ];
885 }
886 if ( $this->options->get( MainConfigNames::EnotifUserTalk ) ||
887 $this->options->get( MainConfigNames::EnotifWatchlist ) ) {
888 if ( $this->options->get( MainConfigNames::EnotifMinorEdits ) ) {
889 $defaultPreferences['enotifminoredits'] = [
890 'type' => 'toggle',
891 'section' => 'personal/email',
892 'label-message' => 'tog-enotifminoredits',
893 'disabled' => $disableEmailPrefs,
894 ];
895 }
896
897 if ( $this->options->get( MainConfigNames::EnotifRevealEditorAddress ) ) {
898 $defaultPreferences['enotifrevealaddr'] = [
899 'type' => 'toggle',
900 'section' => 'personal/email',
901 'label-message' => 'tog-enotifrevealaddr',
902 'disabled' => $disableEmailPrefs,
903 ];
904 }
905 }
906 }
907 }
908
915 protected function skinPreferences( User $user, IContextSource $context, &$defaultPreferences ) {
916 // Skin selector, if there is at least one valid skin
917 $skinOptions = $this->generateSkinOptions( $user, $context );
918 if ( $skinOptions ) {
919 $defaultPreferences['skin'] = [
920 // @phan-suppress-next-line SecurityCheck-XSS False positive, key is escaped
921 'type' => 'radio',
922 'options' => $skinOptions,
923 'section' => 'rendering/skin',
924 ];
925 $defaultPreferences['skin-responsive'] = [
926 'type' => 'check',
927 'label-message' => 'prefs-skin-responsive',
928 'section' => 'rendering/skin/skin-prefs',
929 'help-message' => 'prefs-help-skin-responsive',
930 ];
931 }
932
933 $allowUserCss = $this->options->get( MainConfigNames::AllowUserCss );
934 $allowUserJs = $this->options->get( MainConfigNames::AllowUserJs );
935 // Create links to user CSS/JS pages for all skins.
936 // This code is basically copied from generateSkinOptions().
937 // @todo Refactor this and the similar code in generateSkinOptions().
938 if ( $allowUserCss || $allowUserJs ) {
939 $linkTools = [];
940 $userName = $user->getName();
941
942 if ( $allowUserCss ) {
943 $cssPage = Title::makeTitleSafe( NS_USER, $userName . '/common.css' );
944 $cssLinkText = $context->msg( 'prefs-custom-css' )->text();
945 $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
946 }
947
948 if ( $allowUserJs ) {
949 $jsPage = Title::makeTitleSafe( NS_USER, $userName . '/common.js' );
950 $jsLinkText = $context->msg( 'prefs-custom-js' )->text();
951 $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
952 }
953
954 $defaultPreferences['commoncssjs'] = [
955 'type' => 'info',
956 'raw' => true,
957 'default' => $context->getLanguage()->pipeList( $linkTools ),
958 'label-message' => 'prefs-common-config',
959 'section' => 'rendering/skin',
960 ];
961 }
962 }
963
968 protected function filesPreferences( IContextSource $context, &$defaultPreferences ) {
969 $defaultPreferences['imagesize'] = [
970 'type' => 'select',
971 'options' => $this->getImageSizes( $context ),
972 'label-message' => 'imagemaxsize',
973 'section' => 'rendering/files',
974 ];
975 $defaultPreferences['thumbsize'] = [
976 'type' => 'select',
977 'options' => $this->getThumbSizes( $context ),
978 'label-message' => 'thumbsize',
979 'section' => 'rendering/files',
980 ];
981 }
982
989 protected function datetimePreferences(
990 User $user, IContextSource $context, &$defaultPreferences
991 ) {
992 $dateOptions = $this->getDateOptions( $context );
993 if ( $dateOptions ) {
994 $defaultPreferences['date'] = [
995 'type' => 'radio',
996 'options' => $dateOptions,
997 'section' => 'rendering/dateformat',
998 ];
999 }
1000
1001 // Info
1002 $now = wfTimestampNow();
1003 $lang = $context->getLanguage();
1004 $nowlocal = Xml::element( 'span', [ 'id' => 'wpLocalTime' ],
1005 $lang->userTime( $now, $user ) );
1006 $nowserver = $lang->userTime( $now, $user,
1007 [ 'format' => false, 'timecorrection' => false ] ) .
1008 Html::hidden( 'wpServerTime', (int)substr( $now, 8, 2 ) * 60 + (int)substr( $now, 10, 2 ) );
1009
1010 $defaultPreferences['nowserver'] = [
1011 'type' => 'info',
1012 'raw' => 1,
1013 'label-message' => 'servertime',
1014 'default' => $nowserver,
1015 'section' => 'rendering/timeoffset',
1016 ];
1017
1018 $defaultPreferences['nowlocal'] = [
1019 'type' => 'info',
1020 'raw' => 1,
1021 'label-message' => 'localtime',
1022 'default' => $nowlocal,
1023 'section' => 'rendering/timeoffset',
1024 ];
1025
1026 // Grab existing pref.
1027 $tzOffset = $this->userOptionsManager->getOption( $user, 'timecorrection' );
1028 $tz = explode( '|', $tzOffset, 3 );
1029
1030 $tzOptions = $this->getTimezoneOptions( $context );
1031
1032 $tzSetting = $tzOffset;
1033 if ( count( $tz ) > 1 && $tz[0] == 'ZoneInfo' &&
1034 !in_array( $tzOffset, HTMLFormField::flattenOptions( $tzOptions ) )
1035 ) {
1036 // Timezone offset can vary with DST
1037 try {
1038 $userTZ = new DateTimeZone( $tz[2] );
1039 $minDiff = floor( $userTZ->getOffset( new DateTime( 'now' ) ) / 60 );
1040 $tzSetting = "ZoneInfo|$minDiff|{$tz[2]}";
1041 } catch ( TimeoutException $e ) {
1042 throw $e;
1043 } catch ( Exception $e ) {
1044 // User has an invalid time zone set. Fall back to just using the offset
1045 $tz[0] = 'Offset';
1046 }
1047 }
1048 if ( count( $tz ) > 1 && $tz[0] == 'Offset' ) {
1049 $minDiff = (int)$tz[1];
1050 $tzSetting = sprintf( '%+03d:%02d', floor( $minDiff / 60 ), abs( $minDiff ) % 60 );
1051 }
1052
1053 $defaultPreferences['timecorrection'] = [
1054 'class' => \HTMLSelectOrOtherField::class,
1055 'label-message' => 'timezonelegend',
1056 'options' => $tzOptions,
1057 'default' => $tzSetting,
1058 'size' => 20,
1059 'section' => 'rendering/timeoffset',
1060 'id' => 'wpTimeCorrection',
1061 'filter' => TimezoneFilter::class,
1062 'placeholder-message' => 'timezone-useoffset-placeholder',
1063 ];
1064 }
1065
1071 protected function renderingPreferences(
1072 User $user,
1073 MessageLocalizer $l10n,
1074 &$defaultPreferences
1075 ) {
1076 // Diffs
1077 $defaultPreferences['diffonly'] = [
1078 'type' => 'toggle',
1079 'section' => 'rendering/diffs',
1080 'label-message' => 'tog-diffonly',
1081 ];
1082 $defaultPreferences['norollbackdiff'] = [
1083 'type' => 'toggle',
1084 'section' => 'rendering/diffs',
1085 'label-message' => 'tog-norollbackdiff',
1086 ];
1087
1088 // Page Rendering
1089 if ( $this->options->get( MainConfigNames::AllowUserCssPrefs ) ) {
1090 $defaultPreferences['underline'] = [
1091 'type' => 'select',
1092 'options' => [
1093 $l10n->msg( 'underline-never' )->text() => 0,
1094 $l10n->msg( 'underline-always' )->text() => 1,
1095 $l10n->msg( 'underline-default' )->text() => 2,
1096 ],
1097 'label-message' => 'tog-underline',
1098 'section' => 'rendering/advancedrendering',
1099 ];
1100 }
1101
1102 $defaultPreferences['showhiddencats'] = [
1103 'type' => 'toggle',
1104 'section' => 'rendering/advancedrendering',
1105 'label-message' => 'tog-showhiddencats'
1106 ];
1107
1108 if ( $user->isAllowed( 'rollback' ) ) {
1109 $defaultPreferences['showrollbackconfirmation'] = [
1110 'type' => 'toggle',
1111 'section' => 'rendering/advancedrendering',
1112 'label-message' => 'tog-showrollbackconfirmation',
1113 ];
1114 }
1115 }
1116
1122 protected function editingPreferences( User $user, MessageLocalizer $l10n, &$defaultPreferences ) {
1123 $defaultPreferences['editsectiononrightclick'] = [
1124 'type' => 'toggle',
1125 'section' => 'editing/advancedediting',
1126 'label-message' => 'tog-editsectiononrightclick',
1127 ];
1128 $defaultPreferences['editondblclick'] = [
1129 'type' => 'toggle',
1130 'section' => 'editing/advancedediting',
1131 'label-message' => 'tog-editondblclick',
1132 ];
1133
1134 if ( $this->options->get( MainConfigNames::AllowUserCssPrefs ) ) {
1135 $defaultPreferences['editfont'] = [
1136 'type' => 'select',
1137 'section' => 'editing/editor',
1138 'label-message' => 'editfont-style',
1139 'options' => [
1140 $l10n->msg( 'editfont-monospace' )->text() => 'monospace',
1141 $l10n->msg( 'editfont-sansserif' )->text() => 'sans-serif',
1142 $l10n->msg( 'editfont-serif' )->text() => 'serif',
1143 ]
1144 ];
1145 }
1146
1147 if ( $user->isAllowed( 'minoredit' ) ) {
1148 $defaultPreferences['minordefault'] = [
1149 'type' => 'toggle',
1150 'section' => 'editing/editor',
1151 'label-message' => 'tog-minordefault',
1152 ];
1153 }
1154
1155 $defaultPreferences['forceeditsummary'] = [
1156 'type' => 'toggle',
1157 'section' => 'editing/editor',
1158 'label-message' => 'tog-forceeditsummary',
1159 ];
1160 $defaultPreferences['useeditwarning'] = [
1161 'type' => 'toggle',
1162 'section' => 'editing/editor',
1163 'label-message' => 'tog-useeditwarning',
1164 ];
1165
1166 $defaultPreferences['previewonfirst'] = [
1167 'type' => 'toggle',
1168 'section' => 'editing/preview',
1169 'label-message' => 'tog-previewonfirst',
1170 ];
1171 $defaultPreferences['previewontop'] = [
1172 'type' => 'toggle',
1173 'section' => 'editing/preview',
1174 'label-message' => 'tog-previewontop',
1175 ];
1176 $defaultPreferences['uselivepreview'] = [
1177 'type' => 'toggle',
1178 'section' => 'editing/preview',
1179 'label-message' => 'tog-uselivepreview',
1180 ];
1181 }
1182
1188 protected function rcPreferences( User $user, MessageLocalizer $l10n, &$defaultPreferences ) {
1189 $rcMaxAge = $this->options->get( MainConfigNames::RCMaxAge );
1190 $rcMax = ceil( $rcMaxAge / ( 3600 * 24 ) );
1191 $defaultPreferences['rcdays'] = [
1192 'type' => 'float',
1193 'label-message' => 'recentchangesdays',
1194 'section' => 'rc/displayrc',
1195 'min' => 1 / 24,
1196 'max' => $rcMax,
1197 'help-message' => [ 'recentchangesdays-max', Message::numParam( $rcMax ) ],
1198 ];
1199 $defaultPreferences['rclimit'] = [
1200 'type' => 'int',
1201 'min' => 1,
1202 'max' => 1000,
1203 'label-message' => 'recentchangescount',
1204 'help-message' => 'prefs-help-recentchangescount',
1205 'section' => 'rc/displayrc',
1206 'filter' => IntvalFilter::class,
1207 ];
1208 $defaultPreferences['usenewrc'] = [
1209 'type' => 'toggle',
1210 'label-message' => 'tog-usenewrc',
1211 'section' => 'rc/advancedrc',
1212 ];
1213 $defaultPreferences['hideminor'] = [
1214 'type' => 'toggle',
1215 'label-message' => 'tog-hideminor',
1216 'section' => 'rc/changesrc',
1217 ];
1218 $defaultPreferences['pst-cssjs'] = [
1219 'type' => 'api',
1220 ];
1221 $defaultPreferences['rcfilters-rc-collapsed'] = [
1222 'type' => 'api',
1223 ];
1224 $defaultPreferences['rcfilters-wl-collapsed'] = [
1225 'type' => 'api',
1226 ];
1227 $defaultPreferences['rcfilters-saved-queries'] = [
1228 'type' => 'api',
1229 ];
1230 $defaultPreferences['rcfilters-wl-saved-queries'] = [
1231 'type' => 'api',
1232 ];
1233 // Override RCFilters preferences for RecentChanges 'limit'
1234 $defaultPreferences['rcfilters-limit'] = [
1235 'type' => 'api',
1236 ];
1237 $defaultPreferences['rcfilters-saved-queries-versionbackup'] = [
1238 'type' => 'api',
1239 ];
1240 $defaultPreferences['rcfilters-wl-saved-queries-versionbackup'] = [
1241 'type' => 'api',
1242 ];
1243
1244 if ( $this->options->get( MainConfigNames::RCWatchCategoryMembership ) ) {
1245 $defaultPreferences['hidecategorization'] = [
1246 'type' => 'toggle',
1247 'label-message' => 'tog-hidecategorization',
1248 'section' => 'rc/changesrc',
1249 ];
1250 }
1251
1252 if ( $user->useRCPatrol() ) {
1253 $defaultPreferences['hidepatrolled'] = [
1254 'type' => 'toggle',
1255 'section' => 'rc/changesrc',
1256 'label-message' => 'tog-hidepatrolled',
1257 ];
1258 }
1259
1260 if ( $user->useNPPatrol() ) {
1261 $defaultPreferences['newpageshidepatrolled'] = [
1262 'type' => 'toggle',
1263 'section' => 'rc/changesrc',
1264 'label-message' => 'tog-newpageshidepatrolled',
1265 ];
1266 }
1267
1268 if ( $this->options->get( MainConfigNames::RCShowWatchingUsers ) ) {
1269 $defaultPreferences['shownumberswatching'] = [
1270 'type' => 'toggle',
1271 'section' => 'rc/advancedrc',
1272 'label-message' => 'tog-shownumberswatching',
1273 ];
1274 }
1275
1276 $defaultPreferences['rcenhancedfilters-disable'] = [
1277 'type' => 'toggle',
1278 'section' => 'rc/advancedrc',
1279 'label-message' => 'rcfilters-preference-label',
1280 'help-message' => 'rcfilters-preference-help',
1281 ];
1282 }
1283
1289 protected function watchlistPreferences(
1290 User $user, IContextSource $context, &$defaultPreferences
1291 ) {
1292 $watchlistdaysMax = ceil( $this->options->get( MainConfigNames::RCMaxAge ) / ( 3600 * 24 ) );
1293
1294 if ( $user->isAllowed( 'editmywatchlist' ) ) {
1295 $editWatchlistLinks = '';
1296 $editWatchlistModes = [
1297 'edit' => [ 'subpage' => false, 'flags' => [] ],
1298 'raw' => [ 'subpage' => 'raw', 'flags' => [] ],
1299 'clear' => [ 'subpage' => 'clear', 'flags' => [ 'destructive' ] ],
1300 ];
1301 foreach ( $editWatchlistModes as $mode => $options ) {
1302 // Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear
1303 $editWatchlistLinks .=
1304 new \OOUI\ButtonWidget( [
1305 'href' => SpecialPage::getTitleFor( 'EditWatchlist', $options['subpage'] )->getLinkURL(),
1306 'flags' => $options[ 'flags' ],
1307 'label' => new \OOUI\HtmlSnippet(
1308 $context->msg( "prefs-editwatchlist-{$mode}" )->parse()
1309 ),
1310 ] );
1311 }
1312
1313 $defaultPreferences['editwatchlist'] = [
1314 'type' => 'info',
1315 'raw' => true,
1316 'default' => $editWatchlistLinks,
1317 'label-message' => 'prefs-editwatchlist-label',
1318 'section' => 'watchlist/editwatchlist',
1319 ];
1320 }
1321
1322 $defaultPreferences['watchlistdays'] = [
1323 'type' => 'float',
1324 'min' => 1 / 24,
1325 'max' => $watchlistdaysMax,
1326 'section' => 'watchlist/displaywatchlist',
1327 'help-message' => [ 'prefs-watchlist-days-max', Message::numParam( $watchlistdaysMax ) ],
1328 'label-message' => 'prefs-watchlist-days',
1329 ];
1330 $defaultPreferences['wllimit'] = [
1331 'type' => 'int',
1332 'min' => 1,
1333 'max' => 1000,
1334 'label-message' => 'prefs-watchlist-edits',
1335 'help-message' => 'prefs-watchlist-edits-max',
1336 'section' => 'watchlist/displaywatchlist',
1337 'filter' => IntvalFilter::class,
1338 ];
1339 $defaultPreferences['extendwatchlist'] = [
1340 'type' => 'toggle',
1341 'section' => 'watchlist/advancedwatchlist',
1342 'label-message' => 'tog-extendwatchlist',
1343 ];
1344 $defaultPreferences['watchlisthideminor'] = [
1345 'type' => 'toggle',
1346 'section' => 'watchlist/changeswatchlist',
1347 'label-message' => 'tog-watchlisthideminor',
1348 ];
1349 $defaultPreferences['watchlisthidebots'] = [
1350 'type' => 'toggle',
1351 'section' => 'watchlist/changeswatchlist',
1352 'label-message' => 'tog-watchlisthidebots',
1353 ];
1354 $defaultPreferences['watchlisthideown'] = [
1355 'type' => 'toggle',
1356 'section' => 'watchlist/changeswatchlist',
1357 'label-message' => 'tog-watchlisthideown',
1358 ];
1359 $defaultPreferences['watchlisthideanons'] = [
1360 'type' => 'toggle',
1361 'section' => 'watchlist/changeswatchlist',
1362 'label-message' => 'tog-watchlisthideanons',
1363 ];
1364 $defaultPreferences['watchlisthideliu'] = [
1365 'type' => 'toggle',
1366 'section' => 'watchlist/changeswatchlist',
1367 'label-message' => 'tog-watchlisthideliu',
1368 ];
1369
1371 $defaultPreferences['watchlistreloadautomatically'] = [
1372 'type' => 'toggle',
1373 'section' => 'watchlist/advancedwatchlist',
1374 'label-message' => 'tog-watchlistreloadautomatically',
1375 ];
1376 }
1377
1378 $defaultPreferences['watchlistunwatchlinks'] = [
1379 'type' => 'toggle',
1380 'section' => 'watchlist/advancedwatchlist',
1381 'label-message' => 'tog-watchlistunwatchlinks',
1382 ];
1383
1384 if ( $this->options->get( MainConfigNames::RCWatchCategoryMembership ) ) {
1385 $defaultPreferences['watchlisthidecategorization'] = [
1386 'type' => 'toggle',
1387 'section' => 'watchlist/changeswatchlist',
1388 'label-message' => 'tog-watchlisthidecategorization',
1389 ];
1390 }
1391
1392 if ( $user->useRCPatrol() ) {
1393 $defaultPreferences['watchlisthidepatrolled'] = [
1394 'type' => 'toggle',
1395 'section' => 'watchlist/changeswatchlist',
1396 'label-message' => 'tog-watchlisthidepatrolled',
1397 ];
1398 }
1399
1400 $watchTypes = [
1401 'edit' => 'watchdefault',
1402 'move' => 'watchmoves',
1403 'delete' => 'watchdeletion'
1404 ];
1405
1406 // Kinda hacky
1407 if ( $user->isAllowedAny( 'createpage', 'createtalk' ) ) {
1408 $watchTypes['read'] = 'watchcreations';
1409 }
1410
1411 if ( $user->isAllowed( 'rollback' ) ) {
1412 $watchTypes['rollback'] = 'watchrollback';
1413 }
1414
1415 if ( $user->isAllowed( 'upload' ) ) {
1416 $watchTypes['upload'] = 'watchuploads';
1417 }
1418
1419 foreach ( $watchTypes as $action => $pref ) {
1420 if ( $user->isAllowed( $action ) ) {
1421 // Messages:
1422 // tog-watchdefault, tog-watchmoves, tog-watchdeletion, tog-watchcreations, tog-watchuploads
1423 // tog-watchrollback
1424 $defaultPreferences[$pref] = [
1425 'type' => 'toggle',
1426 'section' => 'watchlist/pageswatchlist',
1427 'label-message' => "tog-$pref",
1428 ];
1429 }
1430 }
1431
1432 $defaultPreferences['watchlisttoken'] = [
1433 'type' => 'api',
1434 ];
1435
1436 $tokenButton = new \OOUI\ButtonWidget( [
1437 'href' => SpecialPage::getTitleFor( 'ResetTokens' )->getLinkURL( [
1438 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
1439 ] ),
1440 'label' => $context->msg( 'prefs-watchlist-managetokens' )->text(),
1441 ] );
1442 $defaultPreferences['watchlisttoken-info'] = [
1443 'type' => 'info',
1444 'section' => 'watchlist/tokenwatchlist',
1445 'label-message' => 'prefs-watchlist-token',
1446 'help-message' => 'prefs-help-tokenmanagement',
1447 'raw' => true,
1448 'default' => (string)$tokenButton,
1449 ];
1450
1451 $defaultPreferences['wlenhancedfilters-disable'] = [
1452 'type' => 'toggle',
1453 'section' => 'watchlist/advancedwatchlist',
1454 'label-message' => 'rcfilters-watchlist-preference-label',
1455 'help-message' => 'rcfilters-watchlist-preference-help',
1456 ];
1457 }
1458
1463 protected function searchPreferences( $context, &$defaultPreferences ) {
1464 $defaultPreferences['search-special-page'] = [
1465 'type' => 'api',
1466 ];
1467
1468 foreach ( $this->nsInfo->getValidNamespaces() as $n ) {
1469 $defaultPreferences['searchNs' . $n] = [
1470 'type' => 'api',
1471 ];
1472 }
1473
1474 if ( $this->options->get( MainConfigNames::SearchMatchRedirectPreference ) ) {
1475 $defaultPreferences['search-match-redirect'] = [
1476 'type' => 'toggle',
1477 'section' => 'searchoptions/searchmisc',
1478 'label-message' => 'search-match-redirect-label',
1479 'help-message' => 'search-match-redirect-help',
1480 ];
1481 } else {
1482 $defaultPreferences['search-match-redirect'] = [
1483 'type' => 'api',
1484 ];
1485 }
1486
1487 $defaultPreferences['searchlimit'] = [
1488 'type' => 'int',
1489 'min' => 1,
1490 'max' => 500,
1491 'section' => 'searchoptions/searchmisc',
1492 'label-message' => 'searchlimit-label',
1493 'help-message' => $context->msg( 'searchlimit-help', 500 ),
1494 'filter' => IntvalFilter::class,
1495 ];
1496 }
1497
1498 /*
1499 * Custom skin string comparison function that takes into account current and preferred skins.
1500 *
1501 * @param string $a
1502 * @param string $b
1503 * @param string $currentSkin
1504 * @param array $preferredSkins
1505 * @return int
1506 */
1507 private static function sortSkinNames( $a, $b, $currentSkin, $preferredSkins ) {
1508 // Display the current skin first in the list
1509 if ( strcasecmp( $a, $currentSkin ) === 0 ) {
1510 return -1;
1511 }
1512 if ( strcasecmp( $b, $currentSkin ) === 0 ) {
1513 return 1;
1514 }
1515 // Display preferred skins over other skins
1516 if ( count( $preferredSkins ) ) {
1517 $aPreferred = array_search( $a, $preferredSkins );
1518 $bPreferred = array_search( $b, $preferredSkins );
1519 // Cannot use ! operator because array_search returns the
1520 // index of the array item if found (i.e. 0) and false otherwise
1521 if ( $aPreferred !== false && $bPreferred === false ) {
1522 return -1;
1523 }
1524 if ( $aPreferred === false && $bPreferred !== false ) {
1525 return 1;
1526 }
1527 // When both skins are preferred, default to the ordering
1528 // specified by the preferred skins config array
1529 if ( $aPreferred !== false && $bPreferred !== false ) {
1530 return strcasecmp( $aPreferred, $bPreferred );
1531 }
1532 }
1533 // Use normal string comparison if both strings are not preferred
1534 return strcasecmp( $a, $b );
1535 }
1536
1542 protected function generateSkinOptions( User $user, IContextSource $context ) {
1543 $ret = [];
1544
1545 $mptitle = Title::newMainPage();
1546 $previewtext = $context->msg( 'skin-preview' )->escaped();
1547
1548 // Only show skins that aren't disabled
1549 $validSkinNames = $this->skinFactory->getAllowedSkins();
1550 $allInstalledSkins = $this->skinFactory->getInstalledSkins();
1551
1552 // Display the installed skin the user has specifically requested via useskin=….
1553 $useSkin = $context->getRequest()->getRawVal( 'useskin' );
1554 if ( isset( $allInstalledSkins[$useSkin] )
1555 && $context->msg( "skinname-$useSkin" )->exists()
1556 ) {
1557 $validSkinNames[$useSkin] = $useSkin;
1558 }
1559
1560 // Display the skin if the user has set it as a preference already before it was hidden.
1561 $currentUserSkin = $this->userOptionsManager->getOption( $user, 'skin' );
1562 if ( isset( $allInstalledSkins[$currentUserSkin] )
1563 && $context->msg( "skinname-$currentUserSkin" )->exists()
1564 ) {
1565 $validSkinNames[$currentUserSkin] = $currentUserSkin;
1566 }
1567
1568 foreach ( $validSkinNames as $skinkey => &$skinname ) {
1569 $msg = $context->msg( "skinname-{$skinkey}" );
1570 if ( $msg->exists() ) {
1571 $skinname = htmlspecialchars( $msg->text() );
1572 }
1573 }
1574
1575 $preferredSkins = MediaWikiServices::getInstance()->getMainConfig()->get(
1577 // Sort by the internal name, so that the ordering is the same for each display language,
1578 // especially if some skin names are translated to use a different alphabet and some are not.
1579 uksort( $validSkinNames, function ( $a, $b ) use ( $currentUserSkin, $preferredSkins ) {
1580 return $this->sortSkinNames( $a, $b, $currentUserSkin, $preferredSkins );
1581 } );
1582
1583 $defaultSkin = $this->options->get( MainConfigNames::DefaultSkin );
1584 $allowUserCss = $this->options->get( MainConfigNames::AllowUserCss );
1585 $allowUserJs = $this->options->get( MainConfigNames::AllowUserJs );
1586 $foundDefault = false;
1587 foreach ( $validSkinNames as $skinkey => $sn ) {
1588 $linkTools = [];
1589
1590 // Mark the default skin
1591 if ( strcasecmp( $skinkey, $defaultSkin ) === 0 ) {
1592 $linkTools[] = $context->msg( 'default' )->escaped();
1593 $foundDefault = true;
1594 }
1595
1596 // Create talk page link if relevant message exists.
1597 $talkPageMsg = $context->msg( "$skinkey-prefs-talkpage" );
1598 if ( $talkPageMsg->exists() ) {
1599 $linkTools[] = $talkPageMsg->parse();
1600 }
1601
1602 // Create preview link
1603 $mplink = htmlspecialchars( $mptitle->getLocalURL( [ 'useskin' => $skinkey ] ) );
1604 $linkTools[] = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
1605
1606 // Create links to user CSS/JS pages
1607 // @todo Refactor this and the similar code in skinPreferences().
1608 if ( $allowUserCss ) {
1609 $cssPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.css' );
1610 $cssLinkText = $context->msg( 'prefs-custom-css' )->text();
1611 $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
1612 }
1613
1614 if ( $allowUserJs ) {
1615 $jsPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.js' );
1616 $jsLinkText = $context->msg( 'prefs-custom-js' )->text();
1617 $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
1618 }
1619
1620 $display = $sn . ' ' . $context->msg( 'parentheses' )
1621 ->rawParams( $context->getLanguage()->pipeList( $linkTools ) )
1622 ->escaped();
1623 $ret[$display] = $skinkey;
1624 }
1625
1626 if ( !$foundDefault ) {
1627 // If the default skin is not available, things are going to break horribly because the
1628 // default value for skin selector will not be a valid value. Let's just not show it then.
1629 return [];
1630 }
1631
1632 return $ret;
1633 }
1634
1639 protected function getDateOptions( IContextSource $context ) {
1640 $lang = $context->getLanguage();
1641 $dateopts = $lang->getDatePreferences();
1642
1643 $ret = [];
1644
1645 if ( $dateopts ) {
1646 if ( !in_array( 'default', $dateopts ) ) {
1647 $dateopts[] = 'default'; // Make sure default is always valid T21237
1648 }
1649
1650 // FIXME KLUGE: site default might not be valid for user language
1651 global $wgDefaultUserOptions;
1652 if ( !in_array( $wgDefaultUserOptions['date'], $dateopts ) ) {
1653 $wgDefaultUserOptions['date'] = 'default';
1654 }
1655
1656 $epoch = wfTimestampNow();
1657 foreach ( $dateopts as $key ) {
1658 if ( $key == 'default' ) {
1659 $formatted = $context->msg( 'datedefault' )->escaped();
1660 } else {
1661 $formatted = htmlspecialchars( $lang->timeanddate( $epoch, false, $key ) );
1662 }
1663 $ret[$formatted] = $key;
1664 }
1665 }
1666 return $ret;
1667 }
1668
1673 protected function getImageSizes( MessageLocalizer $l10n ) {
1674 $ret = [];
1675 $pixels = $l10n->msg( 'unit-pixel' )->text();
1676
1677 foreach ( $this->options->get( MainConfigNames::ImageLimits ) as $index => $limits ) {
1678 // Note: A left-to-right marker (U+200E) is inserted, see T144386
1679 $display = "{$limits[0]}\u{200E}×{$limits[1]}$pixels";
1680 $ret[$display] = $index;
1681 }
1682
1683 return $ret;
1684 }
1685
1690 protected function getThumbSizes( MessageLocalizer $l10n ) {
1691 $ret = [];
1692 $pixels = $l10n->msg( 'unit-pixel' )->text();
1693
1694 foreach ( $this->options->get( MainConfigNames::ThumbLimits ) as $index => $size ) {
1695 $display = $size . $pixels;
1696 $ret[$display] = $index;
1697 }
1698
1699 return $ret;
1700 }
1701
1708 protected function validateSignature( $signature, $alldata, HTMLForm $form ) {
1709 $sigValidation = $this->options->get( MainConfigNames::SignatureValidation );
1710 $maxSigChars = $this->options->get( MainConfigNames::MaxSigChars );
1711 if ( is_string( $signature ) && mb_strlen( $signature ) > $maxSigChars ) {
1712 return $form->msg( 'badsiglength' )->numParams( $maxSigChars )->escaped();
1713 }
1714
1715 if ( $signature === null || $signature === '' ) {
1716 // Make sure leaving the field empty is valid, since that's used as the default (T288151).
1717 // Code using this preference in Parser::getUserSig() handles this case specially.
1718 return true;
1719 }
1720
1721 // Remaining checks only apply to fancy signatures
1722 if ( !( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) ) {
1723 return true;
1724 }
1725
1726 // HERE BE DRAGONS:
1727 //
1728 // If this value is already saved as the user's signature, treat it as valid, even if it
1729 // would be invalid to save now, and even if $wgSignatureValidation is set to 'disallow'.
1730 //
1731 // It can become invalid when we introduce new validation, or when the value just transcludes
1732 // some page containing the real signature and that page is edited (which we can't validate),
1733 // or when someone's username is changed.
1734 //
1735 // Otherwise it would be completely removed when the user opens their preferences page, which
1736 // would be very unfriendly.
1737 $user = $form->getUser();
1738 if (
1739 $signature === $this->userOptionsManager->getOption( $user, 'nickname' ) &&
1740 (bool)$alldata['fancysig'] === $this->userOptionsManager->getBoolOption( $user, 'fancysig' )
1741 ) {
1742 return true;
1743 }
1744
1745 if ( $sigValidation === 'new' || $sigValidation === 'disallow' ) {
1746 // Validate everything
1747 $parserOpts = ParserOptions::newFromContext( $form->getContext() );
1748 $validator = $this->signatureValidatorFactory
1749 ->newSignatureValidator( $user, $form->getContext(), $parserOpts );
1750 $errors = $validator->validateSignature( $signature );
1751 if ( $errors ) {
1752 return $errors;
1753 }
1754 }
1755
1756 // Quick check for mismatched HTML tags in the input.
1757 // Note that this is easily fooled by wikitext templates or bold/italic markup.
1758 // We're only keeping this until Parsoid is integrated and guaranteed to be available.
1759 if ( $this->parser->validateSig( $signature ) === false ) {
1760 return $form->msg( 'badsig' )->escaped();
1761 }
1762
1763 return true;
1764 }
1765
1772 protected function cleanSignature( $signature, $alldata, HTMLForm $form ) {
1773 if ( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) {
1774 $signature = $this->parser->cleanSig( $signature );
1775 } else {
1776 // When no fancy sig used, make sure ~{3,5} get removed.
1777 $signature = Parser::cleanSigInSig( $signature );
1778 }
1779
1780 return $signature;
1781 }
1782
1790 public function getForm(
1791 User $user,
1792 IContextSource $context,
1793 $formClass = PreferencesFormOOUI::class,
1794 array $remove = []
1795 ) {
1796 // We use ButtonWidgets in some of the getPreferences() functions
1797 $context->getOutput()->enableOOUI();
1798
1799 // Note that the $user parameter of getFormDescriptor() is deprecated.
1800 $formDescriptor = $this->getFormDescriptor( $user, $context );
1801 if ( count( $remove ) ) {
1802 $removeKeys = array_fill_keys( $remove, true );
1803 $formDescriptor = array_diff_key( $formDescriptor, $removeKeys );
1804 }
1805
1806 // Remove type=api preferences. They are not intended for rendering in the form.
1807 foreach ( $formDescriptor as $name => $info ) {
1808 if ( isset( $info['type'] ) && $info['type'] === 'api' ) {
1809 unset( $formDescriptor[$name] );
1810 }
1811 }
1812
1816 $htmlForm = new $formClass( $formDescriptor, $context, 'prefs' );
1817
1818 // This allows users to opt-in to hidden skins. While this should be discouraged and is not
1819 // discoverable, this allows users to still use hidden skins while preventing new users from
1820 // adopting unsupported skins. If no useskin=… parameter was provided, it will not show up
1821 // in the resulting URL.
1822 $htmlForm->setAction( $context->getTitle()->getLocalURL( [
1823 'useskin' => $context->getRequest()->getRawVal( 'useskin' )
1824 ] ) );
1825
1826 $htmlForm->setModifiedUser( $user );
1827 $htmlForm->setOptionsEditable( $user->isAllowed( 'editmyoptions' ) );
1828 $htmlForm->setPrivateInfoEditable( $user->isAllowed( 'editmyprivateinfo' ) );
1829 $htmlForm->setId( 'mw-prefs-form' );
1830 $htmlForm->setAutocomplete( 'off' );
1831 $htmlForm->setSubmitTextMsg( 'saveprefs' );
1832 // Used message keys: 'accesskey-preferences-save', 'tooltip-preferences-save'
1833 $htmlForm->setSubmitTooltip( 'preferences-save' );
1834 $htmlForm->setSubmitID( 'prefcontrol' );
1835 $htmlForm->setSubmitCallback(
1836 function ( array $formData, PreferencesFormOOUI $form ) use ( $formDescriptor ) {
1837 return $this->submitForm( $formData, $form, $formDescriptor );
1838 }
1839 );
1840
1841 return $htmlForm;
1842 }
1843
1848 protected function getTimezoneOptions( IContextSource $context ) {
1849 $opt = [];
1850
1851 $localTZoffset = $this->options->get( MainConfigNames::LocalTZoffset );
1852 $timeZoneList = $this->getTimeZoneList( $context->getLanguage() );
1853
1854 $timestamp = MWTimestamp::getLocalInstance();
1855 // Check that the LocalTZoffset is the same as the local time zone offset
1856 if ( $localTZoffset === (int)$timestamp->format( 'Z' ) / 60 ) {
1857 $timezoneName = $timestamp->getTimezone()->getName();
1858 // Localize timezone
1859 if ( isset( $timeZoneList[$timezoneName] ) ) {
1860 $timezoneName = $timeZoneList[$timezoneName]['name'];
1861 }
1862 $server_tz_msg = $context->msg(
1863 'timezoneuseserverdefault',
1864 $timezoneName
1865 )->text();
1866 } else {
1867 $tzstring = sprintf(
1868 '%+03d:%02d',
1869 floor( $localTZoffset / 60 ),
1870 abs( $localTZoffset ) % 60
1871 );
1872 $server_tz_msg = $context->msg( 'timezoneuseserverdefault', $tzstring )->text();
1873 }
1874 $opt[$server_tz_msg] = "System|$localTZoffset";
1875 $opt[$context->msg( 'timezoneuseoffset' )->text()] = 'other';
1876 $opt[$context->msg( 'guesstimezone' )->text()] = 'guess';
1877
1878 foreach ( $timeZoneList as $timeZoneInfo ) {
1879 $region = $timeZoneInfo['region'];
1880 if ( !isset( $opt[$region] ) ) {
1881 $opt[$region] = [];
1882 }
1883 $opt[$region][$timeZoneInfo['name']] = $timeZoneInfo['timecorrection'];
1884 }
1885 return $opt;
1886 }
1887
1896 protected function saveFormData( $formData, PreferencesFormOOUI $form, array $formDescriptor ) {
1897 $user = $form->getModifiedUser();
1898 $hiddenPrefs = $this->options->get( MainConfigNames::HiddenPrefs );
1899 $result = true;
1900
1901 if ( !$user->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' )
1902 ) {
1903 return Status::newFatal( 'mypreferencesprotected' );
1904 }
1905
1906 // Filter input
1907 $this->applyFilters( $formData, $formDescriptor, 'filterFromForm' );
1908
1909 // Fortunately, the realname field is MUCH simpler
1910 // (not really "private", but still shouldn't be edited without permission)
1911
1912 if ( !in_array( 'realname', $hiddenPrefs )
1913 && $user->isAllowed( 'editmyprivateinfo' )
1914 && array_key_exists( 'realname', $formData )
1915 ) {
1916 $realName = $formData['realname'];
1917 $user->setRealName( $realName );
1918 }
1919
1920 if ( $user->isAllowed( 'editmyoptions' ) ) {
1921 $oldUserOptions = $this->userOptionsManager->getOptions( $user );
1922
1923 foreach ( $this->getSaveBlacklist() as $b ) {
1924 unset( $formData[$b] );
1925 }
1926
1927 // If users have saved a value for a preference which has subsequently been disabled
1928 // via $wgHiddenPrefs, we don't want to destroy that setting in case the preference
1929 // is subsequently re-enabled
1930 foreach ( $hiddenPrefs as $pref ) {
1931 // If the user has not set a non-default value here, the default will be returned
1932 // and subsequently discarded
1933 $formData[$pref] = $this->userOptionsManager->getOption( $user, $pref, null, true );
1934 }
1935
1936 // If the user changed the rclimit preference, also change the rcfilters-rclimit preference
1937 if (
1938 isset( $formData['rclimit'] ) &&
1939 intval( $formData[ 'rclimit' ] ) !== $this->userOptionsManager->getIntOption( $user, 'rclimit' )
1940 ) {
1941 $formData['rcfilters-limit'] = $formData['rclimit'];
1942 }
1943
1944 // Keep old preferences from interfering due to back-compat code, etc.
1945 $this->userOptionsManager->resetOptions( $user, $form->getContext(), 'unused' );
1946
1947 foreach ( $formData as $key => $value ) {
1948 $this->userOptionsManager->setOption( $user, $key, $value );
1949 }
1950
1951 $this->hookRunner->onPreferencesFormPreSave(
1952 $formData, $form, $user, $result, $oldUserOptions );
1953 }
1954
1955 $user->saveSettings();
1956
1957 return $result;
1958 }
1959
1968 protected function applyFilters( array &$preferences, array $formDescriptor, $verb ) {
1969 foreach ( $formDescriptor as $preference => $desc ) {
1970 if ( !isset( $desc['filter'] ) || !isset( $preferences[$preference] ) ) {
1971 continue;
1972 }
1973 $filterDesc = $desc['filter'];
1974 if ( $filterDesc instanceof Filter ) {
1975 $filter = $filterDesc;
1976 } elseif ( class_exists( $filterDesc ) ) {
1977 $filter = new $filterDesc();
1978 } elseif ( is_callable( $filterDesc ) ) {
1979 $filter = $filterDesc();
1980 } else {
1981 throw new UnexpectedValueException(
1982 "Unrecognized filter type for preference '$preference'"
1983 );
1984 }
1985 $preferences[$preference] = $filter->$verb( $preferences[$preference] );
1986 }
1987 }
1988
1997 protected function submitForm(
1998 array $formData,
1999 PreferencesFormOOUI $form,
2000 array $formDescriptor
2001 ) {
2002 $res = $this->saveFormData( $formData, $form, $formDescriptor );
2003
2004 if ( $res === true ) {
2005 $context = $form->getContext();
2006 $urlOptions = [];
2007
2008 $urlOptions += $form->getExtraSuccessRedirectParameters();
2009
2010 $url = $form->getTitle()->getFullURL( $urlOptions );
2011
2012 // Set session data for the success message
2013 $context->getRequest()->getSession()->set( 'specialPreferencesSaveSuccess', 1 );
2014
2015 $context->getOutput()->redirect( $url );
2016 }
2017
2018 return ( $res === true ? Status::newGood() : $res );
2019 }
2020
2029 protected function getTimeZoneList( Language $language ) {
2030 $identifiers = DateTimeZone::listIdentifiers();
2031 '@phan-var array|false $identifiers'; // See phan issue #3162
2032 if ( $identifiers === false ) {
2033 return [];
2034 }
2035 sort( $identifiers );
2036
2037 $tzRegions = [
2038 'Africa' => wfMessage( 'timezoneregion-africa' )->inLanguage( $language )->text(),
2039 'America' => wfMessage( 'timezoneregion-america' )->inLanguage( $language )->text(),
2040 'Antarctica' => wfMessage( 'timezoneregion-antarctica' )->inLanguage( $language )->text(),
2041 'Arctic' => wfMessage( 'timezoneregion-arctic' )->inLanguage( $language )->text(),
2042 'Asia' => wfMessage( 'timezoneregion-asia' )->inLanguage( $language )->text(),
2043 'Atlantic' => wfMessage( 'timezoneregion-atlantic' )->inLanguage( $language )->text(),
2044 'Australia' => wfMessage( 'timezoneregion-australia' )->inLanguage( $language )->text(),
2045 'Europe' => wfMessage( 'timezoneregion-europe' )->inLanguage( $language )->text(),
2046 'Indian' => wfMessage( 'timezoneregion-indian' )->inLanguage( $language )->text(),
2047 'Pacific' => wfMessage( 'timezoneregion-pacific' )->inLanguage( $language )->text(),
2048 ];
2049 asort( $tzRegions );
2050
2051 $timeZoneList = [];
2052
2053 $now = new DateTime();
2054
2055 foreach ( $identifiers as $identifier ) {
2056 $parts = explode( '/', $identifier, 2 );
2057
2058 // DateTimeZone::listIdentifiers() returns a number of
2059 // backwards-compatibility entries. This filters them out of the
2060 // list presented to the user.
2061 if ( count( $parts ) !== 2 || !array_key_exists( $parts[0], $tzRegions ) ) {
2062 continue;
2063 }
2064
2065 // Localize region
2066 $parts[0] = $tzRegions[$parts[0]];
2067
2068 $dateTimeZone = new DateTimeZone( $identifier );
2069 $minDiff = floor( $dateTimeZone->getOffset( $now ) / 60 );
2070
2071 $display = str_replace( '_', ' ', $parts[0] . '/' . $parts[1] );
2072 $value = "ZoneInfo|$minDiff|$identifier";
2073
2074 $timeZoneList[$identifier] = [
2075 'name' => $display,
2076 'timecorrection' => $value,
2077 'region' => $parts[0],
2078 ];
2079 }
2080
2081 return $timeZoneList;
2082 }
2083}
const NS_USER
Definition Defines.php:66
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
getContext()
Get the base IContextSource object.
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:150
This class is a collection of static functions that serve two purposes:
Definition Html.php:51
Methods for dealing with language codes.
Base class for multi-variant language conversion.
Base class for language-specific code.
Definition Language.php:53
MediaWiki exception.
Library for creating and parsing MW-style timestamps.
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,...
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
An interface for creating language converters.
A service that provides utilities to do with language names and codes.
Class that generates HTML anchor link elements for pages.
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 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 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.
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)
getTimeZoneList(Language $language)
Get a list of all time zones.
rcPreferences(User $user, MessageLocalizer $l10n, &$defaultPreferences)
watchlistPreferences(User $user, IContextSource $context, &$defaultPreferences)
profilePreferences(User $user, IContextSource $context, &$defaultPreferences)
renderingPreferences(User $user, MessageLocalizer $l10n, &$defaultPreferences)
getForm(User $user, IContextSource $context, $formClass=PreferencesFormOOUI::class, array $remove=[])
skinPreferences(User $user, IContextSource $context, &$defaultPreferences)
static simplifyFormDescriptor(array $descriptor)
Simplify form descriptor for validation or something similar.
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.
filesPreferences(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, Parser $parser=null, SkinFactory $skinFactory=null, UserGroupManager $userGroupManager=null, SignatureValidatorFactory $signatureValidatorFactory=null)
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.
Provides access to user options.
A service class to control user options.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:140
static numParam( $num)
Definition Message.php:1145
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
This is one of the Core classes and should be read at least once by any new developers.
Set options of the Parser.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:96
static stripOuterParagraph( $html)
Strip outer.
Definition Parser.php:6499
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
Definition Parser.php:4829
Form to edit user preferences.
getExtraSuccessRedirectParameters()
Get extra parameters for the query string when redirecting after successful save.
Factory class to create Skin objects.
Parent class for all special pages.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
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:44
Represents a title within MediaWiki.
Definition Title.php:49
Represents a "user group membership" – a specific instance of a user belonging to a group.
internal since 1.36
Definition User.php:70
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:1679
getEmailAuthenticationTimestamp()
Get the timestamp of the user's e-mail authentication.
Definition User.php:2010
getRealName()
Get the user's real name.
Definition User.php:2094
getRegistration()
Get the timestamp of account creation.
Definition User.php:3262
isAllowedAny(... $permissions)
Checks whether this authority has any of the given permissions in general.
Definition User.php:2358
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition User.php:2384
setRealName(string $str)
Set the user's real name.
Definition User.php:2106
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition User.php:2374
getEditCount()
Get the user's edit count.
Definition User.php:2263
getTitleKey()
Get the user's name escaped by underscores.
Definition User.php:1771
saveSettings()
Save this user's settings into the database.
Definition User.php:2555
getEmail()
Get the user's e-mail address.
Definition User.php:1997
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
Definition User.php:2366
Module of static functions for generating XML.
Definition Xml.php:30
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition Xml.php:43
$wgDefaultUserOptions
Config variable stub for the DefaultUserOptions setting, for use by phpdoc and IDEs.
Interface for objects which can provide a MediaWiki context on request.
The shared interface for all language converters.
Base interface for user preference filters that work as a middleware between storage and interface.
Definition Filter.php:27
A PreferencesFactory is a MediaWiki service that provides the definitions of preferences for a given ...
Interface for localizing messages in MediaWiki.
msg( $key,... $params)
This is the method for getting translated interface messages.
if(!isset( $args[0])) $lang