MediaWiki  master
DefaultPreferencesFactory.php
Go to the documentation of this file.
1 <?php
22 
23 use Config;
24 use HTMLForm;
25 use HTMLFormField;
26 use IContextSource;
28 use Language;
29 use LanguageCode;
48 use Message;
50 use MWException;
51 use NamespaceInfo;
52 use OutputPage;
53 use Parser;
54 use ParserOptions;
56 use Psr\Log\LoggerAwareTrait;
57 use Psr\Log\NullLogger;
58 use SkinFactory;
59 use SpecialPage;
60 use Status;
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(),
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  ];
1054  }
1055 
1061  protected function renderingPreferences(
1062  User $user,
1063  MessageLocalizer $l10n,
1064  &$defaultPreferences
1065  ) {
1066  // Diffs
1067  $defaultPreferences['diffonly'] = [
1068  'type' => 'toggle',
1069  'section' => 'rendering/diffs',
1070  'label-message' => 'tog-diffonly',
1071  ];
1072  $defaultPreferences['norollbackdiff'] = [
1073  'type' => 'toggle',
1074  'section' => 'rendering/diffs',
1075  'label-message' => 'tog-norollbackdiff',
1076  ];
1077 
1078  // Page Rendering
1079  if ( $this->options->get( MainConfigNames::AllowUserCssPrefs ) ) {
1080  $defaultPreferences['underline'] = [
1081  'type' => 'select',
1082  'options' => [
1083  $l10n->msg( 'underline-never' )->text() => 0,
1084  $l10n->msg( 'underline-always' )->text() => 1,
1085  $l10n->msg( 'underline-default' )->text() => 2,
1086  ],
1087  'label-message' => 'tog-underline',
1088  'section' => 'rendering/advancedrendering',
1089  ];
1090  }
1091 
1092  $defaultPreferences['showhiddencats'] = [
1093  'type' => 'toggle',
1094  'section' => 'rendering/advancedrendering',
1095  'label-message' => 'tog-showhiddencats'
1096  ];
1097 
1098  if ( $user->isAllowed( 'rollback' ) ) {
1099  $defaultPreferences['showrollbackconfirmation'] = [
1100  'type' => 'toggle',
1101  'section' => 'rendering/advancedrendering',
1102  'label-message' => 'tog-showrollbackconfirmation',
1103  ];
1104  }
1105  }
1106 
1112  protected function editingPreferences( User $user, MessageLocalizer $l10n, &$defaultPreferences ) {
1113  $defaultPreferences['editsectiononrightclick'] = [
1114  'type' => 'toggle',
1115  'section' => 'editing/advancedediting',
1116  'label-message' => 'tog-editsectiononrightclick',
1117  ];
1118  $defaultPreferences['editondblclick'] = [
1119  'type' => 'toggle',
1120  'section' => 'editing/advancedediting',
1121  'label-message' => 'tog-editondblclick',
1122  ];
1123 
1124  if ( $this->options->get( MainConfigNames::AllowUserCssPrefs ) ) {
1125  $defaultPreferences['editfont'] = [
1126  'type' => 'select',
1127  'section' => 'editing/editor',
1128  'label-message' => 'editfont-style',
1129  'options' => [
1130  $l10n->msg( 'editfont-monospace' )->text() => 'monospace',
1131  $l10n->msg( 'editfont-sansserif' )->text() => 'sans-serif',
1132  $l10n->msg( 'editfont-serif' )->text() => 'serif',
1133  ]
1134  ];
1135  }
1136 
1137  if ( $user->isAllowed( 'minoredit' ) ) {
1138  $defaultPreferences['minordefault'] = [
1139  'type' => 'toggle',
1140  'section' => 'editing/editor',
1141  'label-message' => 'tog-minordefault',
1142  ];
1143  }
1144 
1145  $defaultPreferences['forceeditsummary'] = [
1146  'type' => 'toggle',
1147  'section' => 'editing/editor',
1148  'label-message' => 'tog-forceeditsummary',
1149  ];
1150  $defaultPreferences['useeditwarning'] = [
1151  'type' => 'toggle',
1152  'section' => 'editing/editor',
1153  'label-message' => 'tog-useeditwarning',
1154  ];
1155 
1156  $defaultPreferences['previewonfirst'] = [
1157  'type' => 'toggle',
1158  'section' => 'editing/preview',
1159  'label-message' => 'tog-previewonfirst',
1160  ];
1161  $defaultPreferences['previewontop'] = [
1162  'type' => 'toggle',
1163  'section' => 'editing/preview',
1164  'label-message' => 'tog-previewontop',
1165  ];
1166  $defaultPreferences['uselivepreview'] = [
1167  'type' => 'toggle',
1168  'section' => 'editing/preview',
1169  'label-message' => 'tog-uselivepreview',
1170  ];
1171  }
1172 
1178  protected function rcPreferences( User $user, MessageLocalizer $l10n, &$defaultPreferences ) {
1179  $rcMaxAge = $this->options->get( MainConfigNames::RCMaxAge );
1180  $rcMax = ceil( $rcMaxAge / ( 3600 * 24 ) );
1181  $defaultPreferences['rcdays'] = [
1182  'type' => 'float',
1183  'label-message' => 'recentchangesdays',
1184  'section' => 'rc/displayrc',
1185  'min' => 1 / 24,
1186  'max' => $rcMax,
1187  'help-message' => [ 'recentchangesdays-max', Message::numParam( $rcMax ) ],
1188  ];
1189  $defaultPreferences['rclimit'] = [
1190  'type' => 'int',
1191  'min' => 1,
1192  'max' => 1000,
1193  'label-message' => 'recentchangescount',
1194  'help-message' => 'prefs-help-recentchangescount',
1195  'section' => 'rc/displayrc',
1196  'filter' => IntvalFilter::class,
1197  ];
1198  $defaultPreferences['usenewrc'] = [
1199  'type' => 'toggle',
1200  'label-message' => 'tog-usenewrc',
1201  'section' => 'rc/advancedrc',
1202  ];
1203  $defaultPreferences['hideminor'] = [
1204  'type' => 'toggle',
1205  'label-message' => 'tog-hideminor',
1206  'section' => 'rc/changesrc',
1207  ];
1208  $defaultPreferences['pst-cssjs'] = [
1209  'type' => 'api',
1210  ];
1211  $defaultPreferences['rcfilters-rc-collapsed'] = [
1212  'type' => 'api',
1213  ];
1214  $defaultPreferences['rcfilters-wl-collapsed'] = [
1215  'type' => 'api',
1216  ];
1217  $defaultPreferences['rcfilters-saved-queries'] = [
1218  'type' => 'api',
1219  ];
1220  $defaultPreferences['rcfilters-wl-saved-queries'] = [
1221  'type' => 'api',
1222  ];
1223  // Override RCFilters preferences for RecentChanges 'limit'
1224  $defaultPreferences['rcfilters-limit'] = [
1225  'type' => 'api',
1226  ];
1227  $defaultPreferences['rcfilters-saved-queries-versionbackup'] = [
1228  'type' => 'api',
1229  ];
1230  $defaultPreferences['rcfilters-wl-saved-queries-versionbackup'] = [
1231  'type' => 'api',
1232  ];
1233 
1234  if ( $this->options->get( MainConfigNames::RCWatchCategoryMembership ) ) {
1235  $defaultPreferences['hidecategorization'] = [
1236  'type' => 'toggle',
1237  'label-message' => 'tog-hidecategorization',
1238  'section' => 'rc/changesrc',
1239  ];
1240  }
1241 
1242  if ( $user->useRCPatrol() ) {
1243  $defaultPreferences['hidepatrolled'] = [
1244  'type' => 'toggle',
1245  'section' => 'rc/changesrc',
1246  'label-message' => 'tog-hidepatrolled',
1247  ];
1248  }
1249 
1250  if ( $user->useNPPatrol() ) {
1251  $defaultPreferences['newpageshidepatrolled'] = [
1252  'type' => 'toggle',
1253  'section' => 'rc/changesrc',
1254  'label-message' => 'tog-newpageshidepatrolled',
1255  ];
1256  }
1257 
1258  if ( $this->options->get( MainConfigNames::RCShowWatchingUsers ) ) {
1259  $defaultPreferences['shownumberswatching'] = [
1260  'type' => 'toggle',
1261  'section' => 'rc/advancedrc',
1262  'label-message' => 'tog-shownumberswatching',
1263  ];
1264  }
1265 
1266  $defaultPreferences['rcenhancedfilters-disable'] = [
1267  'type' => 'toggle',
1268  'section' => 'rc/advancedrc',
1269  'label-message' => 'rcfilters-preference-label',
1270  'help-message' => 'rcfilters-preference-help',
1271  ];
1272  }
1273 
1279  protected function watchlistPreferences(
1280  User $user, IContextSource $context, &$defaultPreferences
1281  ) {
1282  $watchlistdaysMax = ceil( $this->options->get( MainConfigNames::RCMaxAge ) / ( 3600 * 24 ) );
1283 
1284  if ( $user->isAllowed( 'editmywatchlist' ) ) {
1285  $editWatchlistLinks = '';
1286  $editWatchlistModes = [
1287  'edit' => [ 'subpage' => false, 'flags' => [] ],
1288  'raw' => [ 'subpage' => 'raw', 'flags' => [] ],
1289  'clear' => [ 'subpage' => 'clear', 'flags' => [ 'destructive' ] ],
1290  ];
1291  foreach ( $editWatchlistModes as $mode => $options ) {
1292  // Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear
1293  $editWatchlistLinks .=
1294  new \OOUI\ButtonWidget( [
1295  'href' => SpecialPage::getTitleFor( 'EditWatchlist', $options['subpage'] )->getLinkURL(),
1296  'flags' => $options[ 'flags' ],
1297  'label' => new \OOUI\HtmlSnippet(
1298  $context->msg( "prefs-editwatchlist-{$mode}" )->parse()
1299  ),
1300  ] );
1301  }
1302 
1303  $defaultPreferences['editwatchlist'] = [
1304  'type' => 'info',
1305  'raw' => true,
1306  'default' => $editWatchlistLinks,
1307  'label-message' => 'prefs-editwatchlist-label',
1308  'section' => 'watchlist/editwatchlist',
1309  ];
1310  }
1311 
1312  $defaultPreferences['watchlistdays'] = [
1313  'type' => 'float',
1314  'min' => 1 / 24,
1315  'max' => $watchlistdaysMax,
1316  'section' => 'watchlist/displaywatchlist',
1317  'help-message' => [ 'prefs-watchlist-days-max', Message::numParam( $watchlistdaysMax ) ],
1318  'label-message' => 'prefs-watchlist-days',
1319  ];
1320  $defaultPreferences['wllimit'] = [
1321  'type' => 'int',
1322  'min' => 1,
1323  'max' => 1000,
1324  'label-message' => 'prefs-watchlist-edits',
1325  'help-message' => 'prefs-watchlist-edits-max',
1326  'section' => 'watchlist/displaywatchlist',
1327  'filter' => IntvalFilter::class,
1328  ];
1329  $defaultPreferences['extendwatchlist'] = [
1330  'type' => 'toggle',
1331  'section' => 'watchlist/advancedwatchlist',
1332  'label-message' => 'tog-extendwatchlist',
1333  ];
1334  $defaultPreferences['watchlisthideminor'] = [
1335  'type' => 'toggle',
1336  'section' => 'watchlist/changeswatchlist',
1337  'label-message' => 'tog-watchlisthideminor',
1338  ];
1339  $defaultPreferences['watchlisthidebots'] = [
1340  'type' => 'toggle',
1341  'section' => 'watchlist/changeswatchlist',
1342  'label-message' => 'tog-watchlisthidebots',
1343  ];
1344  $defaultPreferences['watchlisthideown'] = [
1345  'type' => 'toggle',
1346  'section' => 'watchlist/changeswatchlist',
1347  'label-message' => 'tog-watchlisthideown',
1348  ];
1349  $defaultPreferences['watchlisthideanons'] = [
1350  'type' => 'toggle',
1351  'section' => 'watchlist/changeswatchlist',
1352  'label-message' => 'tog-watchlisthideanons',
1353  ];
1354  $defaultPreferences['watchlisthideliu'] = [
1355  'type' => 'toggle',
1356  'section' => 'watchlist/changeswatchlist',
1357  'label-message' => 'tog-watchlisthideliu',
1358  ];
1359 
1361  $defaultPreferences['watchlistreloadautomatically'] = [
1362  'type' => 'toggle',
1363  'section' => 'watchlist/advancedwatchlist',
1364  'label-message' => 'tog-watchlistreloadautomatically',
1365  ];
1366  }
1367 
1368  $defaultPreferences['watchlistunwatchlinks'] = [
1369  'type' => 'toggle',
1370  'section' => 'watchlist/advancedwatchlist',
1371  'label-message' => 'tog-watchlistunwatchlinks',
1372  ];
1373 
1374  if ( $this->options->get( MainConfigNames::RCWatchCategoryMembership ) ) {
1375  $defaultPreferences['watchlisthidecategorization'] = [
1376  'type' => 'toggle',
1377  'section' => 'watchlist/changeswatchlist',
1378  'label-message' => 'tog-watchlisthidecategorization',
1379  ];
1380  }
1381 
1382  if ( $user->useRCPatrol() ) {
1383  $defaultPreferences['watchlisthidepatrolled'] = [
1384  'type' => 'toggle',
1385  'section' => 'watchlist/changeswatchlist',
1386  'label-message' => 'tog-watchlisthidepatrolled',
1387  ];
1388  }
1389 
1390  $watchTypes = [
1391  'edit' => 'watchdefault',
1392  'move' => 'watchmoves',
1393  'delete' => 'watchdeletion'
1394  ];
1395 
1396  // Kinda hacky
1397  if ( $user->isAllowedAny( 'createpage', 'createtalk' ) ) {
1398  $watchTypes['read'] = 'watchcreations';
1399  }
1400 
1401  if ( $user->isAllowed( 'rollback' ) ) {
1402  $watchTypes['rollback'] = 'watchrollback';
1403  }
1404 
1405  if ( $user->isAllowed( 'upload' ) ) {
1406  $watchTypes['upload'] = 'watchuploads';
1407  }
1408 
1409  foreach ( $watchTypes as $action => $pref ) {
1410  if ( $user->isAllowed( $action ) ) {
1411  // Messages:
1412  // tog-watchdefault, tog-watchmoves, tog-watchdeletion, tog-watchcreations, tog-watchuploads
1413  // tog-watchrollback
1414  $defaultPreferences[$pref] = [
1415  'type' => 'toggle',
1416  'section' => 'watchlist/pageswatchlist',
1417  'label-message' => "tog-$pref",
1418  ];
1419  }
1420  }
1421 
1422  $defaultPreferences['watchlisttoken'] = [
1423  'type' => 'api',
1424  ];
1425 
1426  $tokenButton = new \OOUI\ButtonWidget( [
1427  'href' => SpecialPage::getTitleFor( 'ResetTokens' )->getLinkURL( [
1428  'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
1429  ] ),
1430  'label' => $context->msg( 'prefs-watchlist-managetokens' )->text(),
1431  ] );
1432  $defaultPreferences['watchlisttoken-info'] = [
1433  'type' => 'info',
1434  'section' => 'watchlist/tokenwatchlist',
1435  'label-message' => 'prefs-watchlist-token',
1436  'help-message' => 'prefs-help-tokenmanagement',
1437  'raw' => true,
1438  'default' => (string)$tokenButton,
1439  ];
1440 
1441  $defaultPreferences['wlenhancedfilters-disable'] = [
1442  'type' => 'toggle',
1443  'section' => 'watchlist/advancedwatchlist',
1444  'label-message' => 'rcfilters-watchlist-preference-label',
1445  'help-message' => 'rcfilters-watchlist-preference-help',
1446  ];
1447  }
1448 
1453  protected function searchPreferences( $context, &$defaultPreferences ) {
1454  $defaultPreferences['search-special-page'] = [
1455  'type' => 'api',
1456  ];
1457 
1458  foreach ( $this->nsInfo->getValidNamespaces() as $n ) {
1459  $defaultPreferences['searchNs' . $n] = [
1460  'type' => 'api',
1461  ];
1462  }
1463 
1464  if ( $this->options->get( MainConfigNames::SearchMatchRedirectPreference ) ) {
1465  $defaultPreferences['search-match-redirect'] = [
1466  'type' => 'toggle',
1467  'section' => 'searchoptions/searchmisc',
1468  'label-message' => 'search-match-redirect-label',
1469  'help-message' => 'search-match-redirect-help',
1470  ];
1471  } else {
1472  $defaultPreferences['search-match-redirect'] = [
1473  'type' => 'api',
1474  ];
1475  }
1476 
1477  $defaultPreferences['searchlimit'] = [
1478  'type' => 'int',
1479  'min' => 1,
1480  'max' => 500,
1481  'section' => 'searchoptions/searchmisc',
1482  'label-message' => 'searchlimit-label',
1483  'help-message' => $context->msg( 'searchlimit-help', 500 ),
1484  'filter' => IntvalFilter::class,
1485  ];
1486 
1487  // show a preference for thumbnails from namespaces other than NS_FILE,
1488  // only when there they're actually configured to be served
1489  $thumbNamespaces = $this->config->get( 'ThumbnailNamespaces' );
1490  $thumbNamespacesFormatted = array_combine(
1491  $thumbNamespaces,
1492  array_map(
1493  static function ( $namespaceId ) use ( $context ) {
1494  return $namespaceId === NS_MAIN
1495  ? $context->msg( 'blanknamespace' )->escaped()
1496  : $context->getLanguage()->getFormattedNsText( $namespaceId );
1497  },
1498  $thumbNamespaces
1499  )
1500  );
1501  $defaultThumbNamespacesFormatted =
1502  array_intersect_key( $thumbNamespacesFormatted, [ NS_FILE => 1 ] ) ?? [];
1503  $extraThumbNamespacesFormatted =
1504  array_diff_key( $thumbNamespacesFormatted, [ NS_FILE => 1 ] );
1505  if ( $extraThumbNamespacesFormatted ) {
1506  $defaultPreferences['search-thumbnail-extra-namespaces'] = [
1507  'type' => 'toggle',
1508  'section' => 'searchoptions/searchmisc',
1509  'label-message' => 'search-thumbnail-extra-namespaces-label',
1510  'help-message' => $context->msg(
1511  'search-thumbnail-extra-namespaces-message',
1512  $context->getLanguage()->listToText( $extraThumbNamespacesFormatted ),
1513  count( $extraThumbNamespacesFormatted ),
1514  $context->getLanguage()->listToText( $defaultThumbNamespacesFormatted ),
1515  count( $defaultThumbNamespacesFormatted )
1516  ),
1517  ];
1518  }
1519  }
1520 
1521  /*
1522  * Custom skin string comparison function that takes into account current and preferred skins.
1523  *
1524  * @param string $a
1525  * @param string $b
1526  * @param string $currentSkin
1527  * @param array $preferredSkins
1528  * @return int
1529  */
1530  private static function sortSkinNames( $a, $b, $currentSkin, $preferredSkins ) {
1531  // Display the current skin first in the list
1532  if ( strcasecmp( $a, $currentSkin ) === 0 ) {
1533  return -1;
1534  }
1535  if ( strcasecmp( $b, $currentSkin ) === 0 ) {
1536  return 1;
1537  }
1538  // Display preferred skins over other skins
1539  if ( count( $preferredSkins ) ) {
1540  $aPreferred = array_search( $a, $preferredSkins );
1541  $bPreferred = array_search( $b, $preferredSkins );
1542  // Cannot use ! operator because array_search returns the
1543  // index of the array item if found (i.e. 0) and false otherwise
1544  if ( $aPreferred !== false && $bPreferred === false ) {
1545  return -1;
1546  }
1547  if ( $aPreferred === false && $bPreferred !== false ) {
1548  return 1;
1549  }
1550  // When both skins are preferred, default to the ordering
1551  // specified by the preferred skins config array
1552  if ( $aPreferred !== false && $bPreferred !== false ) {
1553  return strcasecmp( $aPreferred, $bPreferred );
1554  }
1555  }
1556  // Use normal string comparison if both strings are not preferred
1557  return strcasecmp( $a, $b );
1558  }
1559 
1565  protected function generateSkinOptions( User $user, IContextSource $context ) {
1566  $ret = [];
1567 
1568  $mptitle = Title::newMainPage();
1569  $previewtext = $context->msg( 'skin-preview' )->escaped();
1570 
1571  // Only show skins that aren't disabled
1572  $validSkinNames = $this->skinFactory->getAllowedSkins();
1573  $allInstalledSkins = $this->skinFactory->getInstalledSkins();
1574 
1575  // Display the installed skin the user has specifically requested via useskin=….
1576  $useSkin = $context->getRequest()->getRawVal( 'useskin' );
1577  if ( isset( $allInstalledSkins[$useSkin] )
1578  && $context->msg( "skinname-$useSkin" )->exists()
1579  ) {
1580  $validSkinNames[$useSkin] = $useSkin;
1581  }
1582 
1583  // Display the skin if the user has set it as a preference already before it was hidden.
1584  $currentUserSkin = $this->userOptionsManager->getOption( $user, 'skin' );
1585  if ( isset( $allInstalledSkins[$currentUserSkin] )
1586  && $context->msg( "skinname-$currentUserSkin" )->exists()
1587  ) {
1588  $validSkinNames[$currentUserSkin] = $currentUserSkin;
1589  }
1590 
1591  foreach ( $validSkinNames as $skinkey => &$skinname ) {
1592  $msg = $context->msg( "skinname-{$skinkey}" );
1593  if ( $msg->exists() ) {
1594  $skinname = htmlspecialchars( $msg->text() );
1595  }
1596  }
1597 
1598  $preferredSkins = $this->config->get( MainConfigNames::SkinsPreferred );
1599  // Sort by the internal name, so that the ordering is the same for each display language,
1600  // especially if some skin names are translated to use a different alphabet and some are not.
1601  uksort( $validSkinNames, function ( $a, $b ) use ( $currentUserSkin, $preferredSkins ) {
1602  return $this->sortSkinNames( $a, $b, $currentUserSkin, $preferredSkins );
1603  } );
1604 
1605  $defaultSkin = $this->options->get( MainConfigNames::DefaultSkin );
1606  $allowUserCss = $this->options->get( MainConfigNames::AllowUserCss );
1607  $allowUserJs = $this->options->get( MainConfigNames::AllowUserJs );
1608  $foundDefault = false;
1609  foreach ( $validSkinNames as $skinkey => $sn ) {
1610  $linkTools = [];
1611 
1612  // Mark the default skin
1613  if ( strcasecmp( $skinkey, $defaultSkin ) === 0 ) {
1614  $linkTools[] = $context->msg( 'default' )->escaped();
1615  $foundDefault = true;
1616  }
1617 
1618  // Create talk page link if relevant message exists.
1619  $talkPageMsg = $context->msg( "$skinkey-prefs-talkpage" );
1620  if ( $talkPageMsg->exists() ) {
1621  $linkTools[] = $talkPageMsg->parse();
1622  }
1623 
1624  // Create preview link
1625  $mplink = htmlspecialchars( $mptitle->getLocalURL( [ 'useskin' => $skinkey ] ) );
1626  $linkTools[] = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
1627 
1628  // Create links to user CSS/JS pages
1629  // @todo Refactor this and the similar code in skinPreferences().
1630  if ( $allowUserCss ) {
1631  $cssPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.css' );
1632  $cssLinkText = $context->msg( 'prefs-custom-css' )->text();
1633  $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
1634  }
1635 
1636  if ( $allowUserJs ) {
1637  $jsPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.js' );
1638  $jsLinkText = $context->msg( 'prefs-custom-js' )->text();
1639  $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
1640  }
1641 
1642  $display = $sn . ' ' . $context->msg( 'parentheses' )
1643  ->rawParams( $context->getLanguage()->pipeList( $linkTools ) )
1644  ->escaped();
1645  $ret[$display] = $skinkey;
1646  }
1647 
1648  if ( !$foundDefault ) {
1649  // If the default skin is not available, things are going to break horribly because the
1650  // default value for skin selector will not be a valid value. Let's just not show it then.
1651  return [];
1652  }
1653 
1654  return $ret;
1655  }
1656 
1661  protected function getDateOptions( IContextSource $context ) {
1662  $lang = $context->getLanguage();
1663  $dateopts = $lang->getDatePreferences();
1664 
1665  $ret = [];
1666 
1667  if ( $dateopts ) {
1668  if ( !in_array( 'default', $dateopts ) ) {
1669  $dateopts[] = 'default'; // Make sure default is always valid T21237
1670  }
1671 
1672  // FIXME KLUGE: site default might not be valid for user language
1673  global $wgDefaultUserOptions;
1674  if ( !in_array( $wgDefaultUserOptions['date'], $dateopts ) ) {
1675  $wgDefaultUserOptions['date'] = 'default';
1676  }
1677 
1678  $epoch = wfTimestampNow();
1679  foreach ( $dateopts as $key ) {
1680  if ( $key == 'default' ) {
1681  $formatted = $context->msg( 'datedefault' )->escaped();
1682  } else {
1683  $formatted = htmlspecialchars( $lang->timeanddate( $epoch, false, $key ) );
1684  }
1685  $ret[$formatted] = $key;
1686  }
1687  }
1688  return $ret;
1689  }
1690 
1695  protected function getImageSizes( MessageLocalizer $l10n ) {
1696  $ret = [];
1697  $pixels = $l10n->msg( 'unit-pixel' )->text();
1698 
1699  foreach ( $this->options->get( MainConfigNames::ImageLimits ) as $index => $limits ) {
1700  // Note: A left-to-right marker (U+200E) is inserted, see T144386
1701  $display = "{$limits[0]}\u{200E}×{$limits[1]}$pixels";
1702  $ret[$display] = $index;
1703  }
1704 
1705  return $ret;
1706  }
1707 
1712  protected function getThumbSizes( MessageLocalizer $l10n ) {
1713  $ret = [];
1714  $pixels = $l10n->msg( 'unit-pixel' )->text();
1715 
1716  foreach ( $this->options->get( MainConfigNames::ThumbLimits ) as $index => $size ) {
1717  $display = $size . $pixels;
1718  $ret[$display] = $index;
1719  }
1720 
1721  return $ret;
1722  }
1723 
1730  protected function validateSignature( $signature, $alldata, HTMLForm $form ) {
1731  $sigValidation = $this->options->get( MainConfigNames::SignatureValidation );
1732  $maxSigChars = $this->options->get( MainConfigNames::MaxSigChars );
1733  if ( is_string( $signature ) && mb_strlen( $signature ) > $maxSigChars ) {
1734  return $form->msg( 'badsiglength' )->numParams( $maxSigChars )->escaped();
1735  }
1736 
1737  if ( $signature === null || $signature === '' ) {
1738  // Make sure leaving the field empty is valid, since that's used as the default (T288151).
1739  // Code using this preference in Parser::getUserSig() handles this case specially.
1740  return true;
1741  }
1742 
1743  // Remaining checks only apply to fancy signatures
1744  if ( !( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) ) {
1745  return true;
1746  }
1747 
1748  // HERE BE DRAGONS:
1749  //
1750  // If this value is already saved as the user's signature, treat it as valid, even if it
1751  // would be invalid to save now, and even if $wgSignatureValidation is set to 'disallow'.
1752  //
1753  // It can become invalid when we introduce new validation, or when the value just transcludes
1754  // some page containing the real signature and that page is edited (which we can't validate),
1755  // or when someone's username is changed.
1756  //
1757  // Otherwise it would be completely removed when the user opens their preferences page, which
1758  // would be very unfriendly.
1759  $user = $form->getUser();
1760  if (
1761  $signature === $this->userOptionsManager->getOption( $user, 'nickname' ) &&
1762  (bool)$alldata['fancysig'] === $this->userOptionsManager->getBoolOption( $user, 'fancysig' )
1763  ) {
1764  return true;
1765  }
1766 
1767  if ( $sigValidation === 'new' || $sigValidation === 'disallow' ) {
1768  // Validate everything
1769  $parserOpts = ParserOptions::newFromContext( $form->getContext() );
1770  $validator = $this->signatureValidatorFactory
1771  ->newSignatureValidator( $user, $form->getContext(), $parserOpts );
1772  $errors = $validator->validateSignature( $signature );
1773  if ( $errors ) {
1774  return $errors;
1775  }
1776  }
1777 
1778  // Quick check for mismatched HTML tags in the input.
1779  // Note that this is easily fooled by wikitext templates or bold/italic markup.
1780  // We're only keeping this until Parsoid is integrated and guaranteed to be available.
1781  if ( $this->parser->validateSig( $signature ) === false ) {
1782  return $form->msg( 'badsig' )->escaped();
1783  }
1784 
1785  return true;
1786  }
1787 
1794  protected function cleanSignature( $signature, $alldata, HTMLForm $form ) {
1795  if ( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) {
1796  $signature = $this->parser->cleanSig( $signature );
1797  } else {
1798  // When no fancy sig used, make sure ~{3,5} get removed.
1799  $signature = Parser::cleanSigInSig( $signature );
1800  }
1801 
1802  return $signature;
1803  }
1804 
1812  public function getForm(
1813  User $user,
1814  IContextSource $context,
1815  $formClass = PreferencesFormOOUI::class,
1816  array $remove = []
1817  ) {
1818  // We use ButtonWidgets in some of the getPreferences() functions
1819  $context->getOutput()->enableOOUI();
1820 
1821  // Note that the $user parameter of getFormDescriptor() is deprecated.
1822  $formDescriptor = $this->getFormDescriptor( $user, $context );
1823  if ( count( $remove ) ) {
1824  $removeKeys = array_fill_keys( $remove, true );
1825  $formDescriptor = array_diff_key( $formDescriptor, $removeKeys );
1826  }
1827 
1828  // Remove type=api preferences. They are not intended for rendering in the form.
1829  foreach ( $formDescriptor as $name => $info ) {
1830  if ( isset( $info['type'] ) && $info['type'] === 'api' ) {
1831  unset( $formDescriptor[$name] );
1832  }
1833  }
1834 
1838  $htmlForm = new $formClass( $formDescriptor, $context, 'prefs' );
1839 
1840  // This allows users to opt-in to hidden skins. While this should be discouraged and is not
1841  // discoverable, this allows users to still use hidden skins while preventing new users from
1842  // adopting unsupported skins. If no useskin=… parameter was provided, it will not show up
1843  // in the resulting URL.
1844  $htmlForm->setAction( $context->getTitle()->getLocalURL( [
1845  'useskin' => $context->getRequest()->getRawVal( 'useskin' )
1846  ] ) );
1847 
1848  $htmlForm->setModifiedUser( $user );
1849  $htmlForm->setOptionsEditable( $user->isAllowed( 'editmyoptions' ) );
1850  $htmlForm->setPrivateInfoEditable( $user->isAllowed( 'editmyprivateinfo' ) );
1851  $htmlForm->setId( 'mw-prefs-form' );
1852  $htmlForm->setAutocomplete( 'off' );
1853  $htmlForm->setSubmitTextMsg( 'saveprefs' );
1854  // Used message keys: 'accesskey-preferences-save', 'tooltip-preferences-save'
1855  $htmlForm->setSubmitTooltip( 'preferences-save' );
1856  $htmlForm->setSubmitID( 'prefcontrol' );
1857  $htmlForm->setSubmitCallback(
1858  function ( array $formData, PreferencesFormOOUI $form ) use ( $formDescriptor ) {
1859  return $this->submitForm( $formData, $form, $formDescriptor );
1860  }
1861  );
1862 
1863  return $htmlForm;
1864  }
1865 
1874  protected function saveFormData( $formData, PreferencesFormOOUI $form, array $formDescriptor ) {
1875  $user = $form->getModifiedUser();
1876  $hiddenPrefs = $this->options->get( MainConfigNames::HiddenPrefs );
1877  $result = true;
1878 
1879  if ( !$user->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' )
1880  ) {
1881  return Status::newFatal( 'mypreferencesprotected' );
1882  }
1883 
1884  // Filter input
1885  $this->applyFilters( $formData, $formDescriptor, 'filterFromForm' );
1886 
1887  // Fortunately, the realname field is MUCH simpler
1888  // (not really "private", but still shouldn't be edited without permission)
1889 
1890  if ( !in_array( 'realname', $hiddenPrefs )
1891  && $user->isAllowed( 'editmyprivateinfo' )
1892  && array_key_exists( 'realname', $formData )
1893  ) {
1894  $realName = $formData['realname'];
1895  $user->setRealName( $realName );
1896  }
1897 
1898  if ( $user->isAllowed( 'editmyoptions' ) ) {
1899  $oldUserOptions = $this->userOptionsManager->getOptions( $user );
1900 
1901  foreach ( $this->getSaveBlacklist() as $b ) {
1902  unset( $formData[$b] );
1903  }
1904 
1905  // If users have saved a value for a preference which has subsequently been disabled
1906  // via $wgHiddenPrefs, we don't want to destroy that setting in case the preference
1907  // is subsequently re-enabled
1908  foreach ( $hiddenPrefs as $pref ) {
1909  // If the user has not set a non-default value here, the default will be returned
1910  // and subsequently discarded
1911  $formData[$pref] = $this->userOptionsManager->getOption( $user, $pref, null, true );
1912  }
1913 
1914  // If the user changed the rclimit preference, also change the rcfilters-rclimit preference
1915  if (
1916  isset( $formData['rclimit'] ) &&
1917  intval( $formData[ 'rclimit' ] ) !== $this->userOptionsManager->getIntOption( $user, 'rclimit' )
1918  ) {
1919  $formData['rcfilters-limit'] = $formData['rclimit'];
1920  }
1921 
1922  // Keep old preferences from interfering due to back-compat code, etc.
1923  $this->userOptionsManager->resetOptions( $user, $form->getContext(), 'unused' );
1924 
1925  foreach ( $formData as $key => $value ) {
1926  $this->userOptionsManager->setOption( $user, $key, $value );
1927  }
1928 
1929  $this->hookRunner->onPreferencesFormPreSave(
1930  $formData, $form, $user, $result, $oldUserOptions );
1931  }
1932 
1933  $user->saveSettings();
1934 
1935  return $result;
1936  }
1937 
1946  protected function applyFilters( array &$preferences, array $formDescriptor, $verb ) {
1947  foreach ( $formDescriptor as $preference => $desc ) {
1948  if ( !isset( $desc['filter'] ) || !isset( $preferences[$preference] ) ) {
1949  continue;
1950  }
1951  $filterDesc = $desc['filter'];
1952  if ( $filterDesc instanceof Filter ) {
1953  $filter = $filterDesc;
1954  } elseif ( class_exists( $filterDesc ) ) {
1955  $filter = new $filterDesc();
1956  } elseif ( is_callable( $filterDesc ) ) {
1957  $filter = $filterDesc();
1958  } else {
1959  throw new UnexpectedValueException(
1960  "Unrecognized filter type for preference '$preference'"
1961  );
1962  }
1963  $preferences[$preference] = $filter->$verb( $preferences[$preference] );
1964  }
1965  }
1966 
1975  protected function submitForm(
1976  array $formData,
1977  PreferencesFormOOUI $form,
1978  array $formDescriptor
1979  ) {
1980  $res = $this->saveFormData( $formData, $form, $formDescriptor );
1981 
1982  if ( $res === true ) {
1983  $context = $form->getContext();
1984  $urlOptions = [];
1985 
1986  $urlOptions += $form->getExtraSuccessRedirectParameters();
1987 
1988  $url = $form->getTitle()->getFullURL( $urlOptions );
1989 
1990  // Set session data for the success message
1991  $context->getRequest()->getSession()->set( 'specialPreferencesSaveSuccess', 1 );
1992 
1993  $context->getOutput()->redirect( $url );
1994  }
1995 
1996  return ( $res === true ? Status::newGood() : $res );
1997  }
1998 }
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:153
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 string[] $languagesWithVariants
languages supporting variants
Base class for language-specific code.
Definition: Language.php:56
MediaWiki exception.
Definition: MWException.php:32
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:568
This class is a collection of static functions that serve two purposes:
Definition: Html.php:55
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:860
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:241
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 for internal links.
A class containing constants representing the names of configuration variables.
const HiddenPrefs
Name constant for the HiddenPrefs setting, for use with Config::get()
const ForceHTTPS
Name constant for the ForceHTTPS setting, for use with Config::get()
const EnotifWatchlist
Name constant for the EnotifWatchlist setting, for use with Config::get()
const MaxSigChars
Name constant for the MaxSigChars setting, for use with Config::get()
const RCMaxAge
Name constant for the RCMaxAge setting, for use with Config::get()
const DefaultSkin
Name constant for the DefaultSkin setting, for use with Config::get()
const EnableUserEmailMuteList
Name constant for the EnableUserEmailMuteList setting, for use with Config::get()
const EnotifRevealEditorAddress
Name constant for the EnotifRevealEditorAddress setting, for use with Config::get()
const EnableUserEmail
Name constant for the EnableUserEmail setting, for use with Config::get()
const SkinsPreferred
Name constant for the SkinsPreferred setting, for use with Config::get()
const 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)
Represents a title within MediaWiki.
Definition: Title.php:82
static newMainPage(MessageLocalizer $localizer=null)
Create a new Title for the Main Page.
Definition: Title.php:733
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:697
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:144
static numParam( $num)
Definition: Message.php:1146
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:60
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:107
static stripOuterParagraph( $html)
Strip outer.
Definition: Parser.php:6455
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
Definition: Parser.php:4780
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:45
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:46
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:71
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:1680
getEmailAuthenticationTimestamp()
Get the timestamp of the user's e-mail authentication.
Definition: User.php:2011
getRealName()
Get the user's real name.
Definition: User.php:2095
getRegistration()
Get the timestamp of account creation.
Definition: User.php:3256
isAllowedAny(... $permissions)
Checks whether this authority has any of the given permissions in general.
Definition: User.php:2359
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition: User.php:2385
setRealName(string $str)
Set the user's real name.
Definition: User.php:2107
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition: User.php:2375
getEditCount()
Get the user's edit count.
Definition: User.php:2264
getTitleKey()
Get the user's name escaped by underscores.
Definition: User.php:1772
saveSettings()
Save this user's settings into the database.
Definition: User.php:2551
getEmail()
Get the user's e-mail address.
Definition: User.php:1998
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
Definition: User.php:2367
Module of static functions for generating XML.
Definition: Xml.php:31
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:44
$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