MediaWiki  master
DefaultPreferencesFactory.php
Go to the documentation of this file.
1 <?php
22 
23 use DateTime;
24 use DateTimeZone;
25 use Exception;
26 use Html;
27 use HTMLForm;
28 use HTMLFormField;
29 use IContextSource;
31 use Language;
32 use LanguageCode;
48 use Message;
50 use MWException;
51 use MWTimestamp;
52 use NamespaceInfo;
53 use OutputPage;
54 use Parser;
55 use ParserOptions;
57 use Psr\Log\LoggerAwareTrait;
58 use Psr\Log\NullLogger;
59 use SkinFactory;
60 use SpecialPage;
61 use Status;
62 use Title;
63 use UnexpectedValueException;
64 use User;
66 use Wikimedia\RequestTimeout\TimeoutException;
67 use Xml;
68 
73  use LoggerAwareTrait;
74 
76  protected $options;
77 
79  protected $contLang;
80 
82  protected $languageNameUtils;
83 
85  protected $authManager;
86 
88  protected $linkRenderer;
89 
91  protected $nsInfo;
92 
94  protected $permissionManager;
95 
98 
100  private $hookRunner;
101 
104 
107 
109  private $parser;
110 
112  private $skinFactory;
113 
116 
119 
123  public const CONSTRUCTOR_OPTIONS = [
152  ];
153 
171  public function __construct(
180  HookContainer $hookContainer,
183  Parser $parser = null,
184  SkinFactory $skinFactory = 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 )
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 
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 
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(
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,
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 ( mb_strlen( $signature ) > $maxSigChars ) {
1712  return $form->msg( 'badsiglength' )->numParams( $maxSigChars )->escaped();
1713  }
1714 
1715  if ( $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,
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 
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-suppress-next-line PhanTypeComparisonFromArray 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 }
UserOptionsLookup $userOptionsLookup
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.
static flattenOptions( $options)
flatten an array of options to a single array, for instance, a set of "<options>" inside "<optgroups>...
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
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:236
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:851
Methods for dealing with language codes.
static bcp47( $code)
Get the normalised IETF language tag See unit test for examples.
Base class for multi-variant language conversion.
static array $languagesWithVariants
languages supporting variants
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition: Language.php:45
MediaWiki exception.
Definition: MWException.php:29
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:39
static getLocalInstance( $ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
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...
Definition: HookRunner.php:562
An interface for creating language converters.
A service that provides utilities to do with language names and codes.
const AUTONYMS
Return autonyms in getLanguageName(s).
const SUPPORTED
Return in getLanguageName(s) only the languages for which we have at least some localisation.
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()
MediaWikiServices is the service locator for the application scope of MediaWiki.
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.
loadPreferenceValues(User $user, IContextSource $context, &$defaultPreferences)
Loads existing values for a given array of preferences.
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')....
getFormDescriptor(User $user, IContextSource $context)
applyFilters(array &$preferences, array $formDescriptor, $verb)
Applies filters to preferences either before or after form usage.
filesPreferences(IContextSource $context, &$defaultPreferences)
static sortSkinNames( $a, $b, $currentSkin, $preferredSkins)
__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.
generateSkinOptions(User $user, IContextSource $context)
Provides access to user options.
A service class to control user options.
string null $action
Cache what action this request is.
Definition: MediaWiki.php:47
IContextSource $context
Definition: MediaWiki.php:42
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition: Message.php:141
static numParam( $num)
Definition: Message.php:1166
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.
Definition: OutputPage.php:54
static setupOOUI( $skinName='default', $dir='ltr')
Helper function to setup the PHP implementation of OOUI to use in this request.
Set options of the Parser.
static newFromContext(IContextSource $context)
Get a ParserOptions object from a IContextSource object.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition: Parser.php:95
static stripOuterParagraph( $html)
Strip outer.
Definition: Parser.php:6469
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.
Definition: SkinFactory.php:31
Parent class for all special pages.
Definition: SpecialPage.php:44
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( $user)
Static method to check whether StructuredFilter UI is enabled for the given user.1....
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
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
static newMainPage(MessageLocalizer $localizer=null)
Create a new Title for the Main Page.
Definition: Title.php:700
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:664
Represents a "user group membership" – a specific instance of a user belonging to a group.
static getLink( $ugm, IContextSource $context, $format, $userName=null)
Gets a link for a user group, possibly including the expiry date if relevant.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:68
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:1922
getEmailAuthenticationTimestamp()
Get the timestamp of the user's e-mail authentication.
Definition: User.php:2253
getRealName()
Get the user's real name.
Definition: User.php:2337
getRegistration()
Get the timestamp of account creation.
Definition: User.php:3496
isAllowedAny(... $permissions)
Checks whether this authority has any of the given permissions in general.
Definition: User.php:2594
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition: User.php:2620
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition: User.php:2610
getEditCount()
Get the user's edit count.
Definition: User.php:2499
getTitleKey()
Get the user's name escaped by underscores.
Definition: User.php:2014
getEmail()
Get the user's e-mail address.
Definition: User.php:2240
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
Definition: User.php:2602
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