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