MediaWiki REL1_34
DefaultPreferencesFactory.php
Go to the documentation of this file.
1<?php
22
23use DateTime;
24use DateTimeZone;
25use Exception;
26use Hooks;
27use Html;
28use HTMLForm;
31use Language;
32use LanguageCode;
41use MWException;
42use MWTimestamp;
44use OutputPage;
45use Parser;
48use Psr\Log\LoggerAwareTrait;
49use Psr\Log\NullLogger;
50use Skin;
51use SpecialPage;
52use Status;
53use Title;
54use UnexpectedValueException;
55use User;
57use Xml;
58
63 use LoggerAwareTrait;
64
66 protected $options;
67
69 protected $contLang;
70
72 protected $authManager;
73
75 protected $linkRenderer;
76
78 protected $nsInfo;
79
82
87 public const CONSTRUCTOR_OPTIONS = [
88 'AllowRequiringEmailForResets',
89 'AllowUserCss',
90 'AllowUserCssPrefs',
91 'AllowUserJs',
92 'DefaultSkin',
93 'DisableLangConversion',
94 'EmailAuthentication',
95 'EmailConfirmToEdit',
96 'EnableEmail',
97 'EnableUserEmail',
98 'EnableUserEmailBlacklist',
99 'EnotifMinorEdits',
100 'EnotifRevealEditorAddress',
101 'EnotifUserTalk',
102 'EnotifWatchlist',
103 'ForceHTTPS',
104 'HiddenPrefs',
105 'ImageLimits',
106 'LanguageCode',
107 'LocalTZoffset',
108 'MaxSigChars',
109 'RCMaxAge',
110 'RCShowWatchingUsers',
111 'RCWatchCategoryMembership',
112 'SecureLogin',
113 'ThumbLimits',
114 ];
115
126 public function __construct(
133 ) {
134 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
135
136 $this->options = $options;
137 $this->contLang = $contLang;
138 $this->authManager = $authManager;
139 $this->linkRenderer = $linkRenderer;
140 $this->nsInfo = $nsInfo;
141 $this->permissionManager = $permissionManager;
142 $this->logger = new NullLogger();
143 }
144
148 public function getSaveBlacklist() {
149 return [
150 'realname',
151 'emailaddress',
152 ];
153 }
154
161 public function getFormDescriptor( User $user, IContextSource $context ) {
162 $preferences = [];
163
164 OutputPage::setupOOUI(
165 strtolower( $context->getSkin()->getSkinName() ),
166 $context->getLanguage()->getDir()
167 );
168
169 $canIPUseHTTPS = wfCanIPUseHTTPS( $context->getRequest()->getIP() );
170 $this->profilePreferences( $user, $context, $preferences, $canIPUseHTTPS );
171 $this->skinPreferences( $user, $context, $preferences );
172 $this->datetimePreferences( $user, $context, $preferences );
173 $this->filesPreferences( $context, $preferences );
174 $this->renderingPreferences( $user, $context, $preferences );
175 $this->editingPreferences( $user, $context, $preferences );
176 $this->rcPreferences( $user, $context, $preferences );
177 $this->watchlistPreferences( $user, $context, $preferences );
178 $this->searchPreferences( $preferences );
179
180 Hooks::run( 'GetPreferences', [ $user, &$preferences ] );
181
182 $this->loadPreferenceValues( $user, $context, $preferences );
183 $this->logger->debug( "Created form descriptor for user '{$user->getName()}'" );
184 return $preferences;
185 }
186
195 private function loadPreferenceValues(
196 User $user, IContextSource $context, &$defaultPreferences
197 ) {
198 # # Remove preferences that wikis don't want to use
199 foreach ( $this->options->get( 'HiddenPrefs' ) as $pref ) {
200 if ( isset( $defaultPreferences[$pref] ) ) {
201 unset( $defaultPreferences[$pref] );
202 }
203 }
204
205 # # Make sure that form fields have their parent set. See T43337.
206 $dummyForm = new HTMLForm( [], $context );
207
208 $disable = !$this->permissionManager->userHasRight( $user, 'editmyoptions' );
209
210 $defaultOptions = User::getDefaultOptions();
211 $userOptions = $user->getOptions();
212 $this->applyFilters( $userOptions, $defaultPreferences, 'filterForForm' );
213 # # Prod in defaults from the user
214 foreach ( $defaultPreferences as $name => &$info ) {
215 $prefFromUser = $this->getOptionFromUser( $name, $info, $userOptions );
216 if ( $disable && !in_array( $name, $this->getSaveBlacklist() ) ) {
217 $info['disabled'] = 'disabled';
218 }
219 $field = HTMLForm::loadInputFromParameters( $name, $info, $dummyForm ); // For validation
220 $globalDefault = $defaultOptions[$name] ?? null;
221
222 // If it validates, set it as the default
223 if ( isset( $info['default'] ) ) {
224 // Already set, no problem
225 continue;
226 } elseif ( !is_null( $prefFromUser ) && // Make sure we're not just pulling nothing
227 $field->validate( $prefFromUser, $user->getOptions() ) === true ) {
228 $info['default'] = $prefFromUser;
229 } elseif ( $field->validate( $globalDefault, $user->getOptions() ) === true ) {
230 $info['default'] = $globalDefault;
231 } else {
232 $globalDefault = json_encode( $globalDefault );
233 throw new MWException(
234 "Default '$globalDefault' is invalid for preference $name of user $user"
235 );
236 }
237 }
238
239 return $defaultPreferences;
240 }
241
250 protected function getOptionFromUser( $name, $info, array $userOptions ) {
251 $val = $userOptions[$name] ?? null;
252
253 // Handling for multiselect preferences
254 if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
255 ( isset( $info['class'] ) && $info['class'] == \HTMLMultiSelectField::class ) ) {
256 $options = HTMLFormField::flattenOptions( $info['options'] );
257 $prefix = $info['prefix'] ?? $name;
258 $val = [];
259
260 foreach ( $options as $value ) {
261 if ( $userOptions["$prefix$value"] ?? false ) {
262 $val[] = $value;
263 }
264 }
265 }
266
267 // Handling for checkmatrix preferences
268 if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
269 ( isset( $info['class'] ) && $info['class'] == \HTMLCheckMatrix::class ) ) {
270 $columns = HTMLFormField::flattenOptions( $info['columns'] );
271 $rows = HTMLFormField::flattenOptions( $info['rows'] );
272 $prefix = $info['prefix'] ?? $name;
273 $val = [];
274
275 foreach ( $columns as $column ) {
276 foreach ( $rows as $row ) {
277 if ( $userOptions["$prefix$column-$row"] ?? false ) {
278 $val[] = "$column-$row";
279 }
280 }
281 }
282 }
283
284 return $val;
285 }
286
296 protected function profilePreferences(
297 User $user, IContextSource $context, &$defaultPreferences, $canIPUseHTTPS
298 ) {
299 // retrieving user name for GENDER and misc.
300 $userName = $user->getName();
301
302 # # User info #####################################
303 // Information panel
304 $defaultPreferences['username'] = [
305 'type' => 'info',
306 'label-message' => [ 'username', $userName ],
307 'default' => $userName,
308 'section' => 'personal/info',
309 ];
310
311 $lang = $context->getLanguage();
312
313 # Get groups to which the user belongs
314 $userEffectiveGroups = $user->getEffectiveGroups();
315 $userGroupMemberships = $user->getGroupMemberships();
316 $userGroups = $userMembers = $userTempGroups = $userTempMembers = [];
317 foreach ( $userEffectiveGroups as $ueg ) {
318 if ( $ueg == '*' ) {
319 // Skip the default * group, seems useless here
320 continue;
321 }
322
323 $groupStringOrObject = $userGroupMemberships[$ueg] ?? $ueg;
324
325 $userG = UserGroupMembership::getLink( $groupStringOrObject, $context, 'html' );
326 $userM = UserGroupMembership::getLink( $groupStringOrObject, $context, 'html',
327 $userName );
328
329 // Store expiring groups separately, so we can place them before non-expiring
330 // groups in the list. This is to avoid the ambiguity of something like
331 // "administrator, bureaucrat (until X date)" -- users might wonder whether the
332 // expiry date applies to both groups, or just the last one
333 if ( $groupStringOrObject instanceof UserGroupMembership &&
334 $groupStringOrObject->getExpiry()
335 ) {
336 $userTempGroups[] = $userG;
337 $userTempMembers[] = $userM;
338 } else {
339 $userGroups[] = $userG;
340 $userMembers[] = $userM;
341 }
342 }
343 sort( $userGroups );
344 sort( $userMembers );
345 sort( $userTempGroups );
346 sort( $userTempMembers );
347 $userGroups = array_merge( $userTempGroups, $userGroups );
348 $userMembers = array_merge( $userTempMembers, $userMembers );
349
350 $defaultPreferences['usergroups'] = [
351 'type' => 'info',
352 'label' => $context->msg( 'prefs-memberingroups' )->numParams(
353 count( $userGroups ) )->params( $userName )->parse(),
354 'default' => $context->msg( 'prefs-memberingroups-type' )
355 ->rawParams( $lang->commaList( $userGroups ), $lang->commaList( $userMembers ) )
356 ->escaped(),
357 'raw' => true,
358 'section' => 'personal/info',
359 ];
360
361 $contribTitle = SpecialPage::getTitleFor( "Contributions", $userName );
362 $formattedEditCount = $lang->formatNum( $user->getEditCount() );
363 $editCount = $this->linkRenderer->makeLink( $contribTitle, $formattedEditCount );
364
365 $defaultPreferences['editcount'] = [
366 'type' => 'info',
367 'raw' => true,
368 'label-message' => 'prefs-edits',
369 'default' => $editCount,
370 'section' => 'personal/info',
371 ];
372
373 if ( $user->getRegistration() ) {
374 $displayUser = $context->getUser();
375 $userRegistration = $user->getRegistration();
376 $defaultPreferences['registrationdate'] = [
377 'type' => 'info',
378 'label-message' => 'prefs-registration',
379 'default' => $context->msg(
380 'prefs-registration-date-time',
381 $lang->userTimeAndDate( $userRegistration, $displayUser ),
382 $lang->userDate( $userRegistration, $displayUser ),
383 $lang->userTime( $userRegistration, $displayUser )
384 )->text(),
385 'section' => 'personal/info',
386 ];
387 }
388
389 $canViewPrivateInfo = $this->permissionManager->userHasRight( $user, 'viewmyprivateinfo' );
390 $canEditPrivateInfo = $this->permissionManager->userHasRight( $user, 'editmyprivateinfo' );
391
392 // Actually changeable stuff
393 $defaultPreferences['realname'] = [
394 // (not really "private", but still shouldn't be edited without permission)
395 'type' => $canEditPrivateInfo && $this->authManager->allowsPropertyChange( 'realname' )
396 ? 'text' : 'info',
397 'default' => $user->getRealName(),
398 'section' => 'personal/info',
399 'label-message' => 'yourrealname',
400 'help-message' => 'prefs-help-realname',
401 ];
402
403 if ( $canEditPrivateInfo && $this->authManager->allowsAuthenticationDataChange(
404 new PasswordAuthenticationRequest(), false )->isGood()
405 ) {
406 $defaultPreferences['password'] = [
407 'type' => 'info',
408 'raw' => true,
409 'default' => (string)new \OOUI\ButtonWidget( [
410 'href' => SpecialPage::getTitleFor( 'ChangePassword' )->getLinkURL( [
411 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
412 ] ),
413 'label' => $context->msg( 'prefs-resetpass' )->text(),
414 ] ),
415 'label-message' => 'yourpassword',
416 'section' => 'personal/info',
417 ];
418 }
419 // Only show prefershttps if secure login is turned on
420 if ( !$this->options->get( 'ForceHTTPS' )
421 && $this->options->get( 'SecureLogin' )
422 && $canIPUseHTTPS
423 ) {
424 $defaultPreferences['prefershttps'] = [
425 'type' => 'toggle',
426 'label-message' => 'tog-prefershttps',
427 'help-message' => 'prefs-help-prefershttps',
428 'section' => 'personal/info'
429 ];
430 }
431
432 $languages = Language::fetchLanguageNames( null, 'mwfile' );
433 $languageCode = $this->options->get( 'LanguageCode' );
434 if ( !array_key_exists( $languageCode, $languages ) ) {
435 $languages[$languageCode] = $languageCode;
436 // Sort the array again
437 ksort( $languages );
438 }
439
440 $options = [];
441 foreach ( $languages as $code => $name ) {
442 $display = LanguageCode::bcp47( $code ) . ' - ' . $name;
443 $options[$display] = $code;
444 }
445 $defaultPreferences['language'] = [
446 'type' => 'select',
447 'section' => 'personal/i18n',
448 'options' => $options,
449 'label-message' => 'yourlanguage',
450 ];
451
452 $defaultPreferences['gender'] = [
453 'type' => 'radio',
454 'section' => 'personal/i18n',
455 'options' => [
456 $context->msg( 'parentheses' )
457 ->params( $context->msg( 'gender-unknown' )->plain() )
458 ->escaped() => 'unknown',
459 $context->msg( 'gender-female' )->escaped() => 'female',
460 $context->msg( 'gender-male' )->escaped() => 'male',
461 ],
462 'label-message' => 'yourgender',
463 'help-message' => 'prefs-help-gender',
464 ];
465
466 // see if there are multiple language variants to choose from
467 if ( !$this->options->get( 'DisableLangConversion' ) ) {
468 foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
469 if ( $langCode == $this->contLang->getCode() ) {
470 if ( !$this->contLang->hasVariants() ) {
471 continue;
472 }
473
474 $variants = $this->contLang->getVariants();
475 $variantArray = [];
476 foreach ( $variants as $v ) {
477 $v = str_replace( '_', '-', strtolower( $v ) );
478 $variantArray[$v] = $lang->getVariantname( $v, false );
479 }
480
481 $options = [];
482 foreach ( $variantArray as $code => $name ) {
483 $display = LanguageCode::bcp47( $code ) . ' - ' . $name;
484 $options[$display] = $code;
485 }
486
487 $defaultPreferences['variant'] = [
488 'label-message' => 'yourvariant',
489 'type' => 'select',
490 'options' => $options,
491 'section' => 'personal/i18n',
492 'help-message' => 'prefs-help-variant',
493 ];
494 } else {
495 $defaultPreferences["variant-$langCode"] = [
496 'type' => 'api',
497 ];
498 }
499 }
500 }
501
502 // show a preview of the old signature first
503 $oldsigWikiText = MediaWikiServices::getInstance()->getParser()->preSaveTransform(
504 '~~~',
505 $context->getTitle(),
506 $user,
507 ParserOptions::newFromContext( $context )
508 );
509 $oldsigHTML = Parser::stripOuterParagraph(
510 $context->getOutput()->parseAsContent( $oldsigWikiText )
511 );
512 $defaultPreferences['oldsig'] = [
513 'type' => 'info',
514 'raw' => true,
515 'label-message' => 'tog-oldsig',
516 'default' => $oldsigHTML,
517 'section' => 'personal/signature',
518 ];
519 $defaultPreferences['nickname'] = [
520 'type' => $this->authManager->allowsPropertyChange( 'nickname' ) ? 'text' : 'info',
521 'maxlength' => $this->options->get( 'MaxSigChars' ),
522 'label-message' => 'yournick',
523 'validation-callback' => function ( $signature, $alldata, HTMLForm $form ) {
524 return $this->validateSignature( $signature, $alldata, $form );
525 },
526 'section' => 'personal/signature',
527 'filter-callback' => function ( $signature, array $alldata, HTMLForm $form ) {
528 return $this->cleanSignature( $signature, $alldata, $form );
529 },
530 ];
531 $defaultPreferences['fancysig'] = [
532 'type' => 'toggle',
533 'label-message' => 'tog-fancysig',
534 // show general help about signature at the bottom of the section
535 'help-message' => 'prefs-help-signature',
536 'section' => 'personal/signature'
537 ];
538
539 # # Email stuff
540
541 if ( $this->options->get( 'EnableEmail' ) ) {
542 if ( $canViewPrivateInfo ) {
543 $helpMessages = [];
544 $helpMessages[] = $this->options->get( 'EmailConfirmToEdit' )
545 ? 'prefs-help-email-required'
546 : 'prefs-help-email';
547
548 if ( $this->options->get( 'EnableUserEmail' ) ) {
549 // additional messages when users can send email to each other
550 $helpMessages[] = 'prefs-help-email-others';
551 }
552
553 $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
554 if ( $canEditPrivateInfo && $this->authManager->allowsPropertyChange( 'emailaddress' ) ) {
555 $button = new \OOUI\ButtonWidget( [
556 'href' => SpecialPage::getTitleFor( 'ChangeEmail' )->getLinkURL( [
557 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
558 ] ),
559 'label' =>
560 $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(),
561 ] );
562
563 $emailAddress .= $emailAddress == '' ? $button : ( '<br />' . $button );
564 }
565
566 $defaultPreferences['emailaddress'] = [
567 'type' => 'info',
568 'raw' => true,
569 'default' => $emailAddress,
570 'label-message' => 'youremail',
571 'section' => 'personal/email',
572 'help-messages' => $helpMessages,
573 # 'cssclass' chosen below
574 ];
575 }
576
577 $disableEmailPrefs = false;
578
579 if ( $this->options->get( 'EmailAuthentication' ) ) {
580 $emailauthenticationclass = 'mw-email-not-authenticated';
581 if ( $user->getEmail() ) {
582 if ( $user->getEmailAuthenticationTimestamp() ) {
583 // date and time are separate parameters to facilitate localisation.
584 // $time is kept for backward compat reasons.
585 // 'emailauthenticated' is also used in SpecialConfirmemail.php
586 $displayUser = $context->getUser();
587 $emailTimestamp = $user->getEmailAuthenticationTimestamp();
588 $time = $lang->userTimeAndDate( $emailTimestamp, $displayUser );
589 $d = $lang->userDate( $emailTimestamp, $displayUser );
590 $t = $lang->userTime( $emailTimestamp, $displayUser );
591 $emailauthenticated = $context->msg( 'emailauthenticated',
592 $time, $d, $t )->parse() . '<br />';
593 $disableEmailPrefs = false;
594 $emailauthenticationclass = 'mw-email-authenticated';
595 } else {
596 $disableEmailPrefs = true;
597 $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
598 new \OOUI\ButtonWidget( [
599 'href' => SpecialPage::getTitleFor( 'Confirmemail' )->getLinkURL(),
600 'label' => $context->msg( 'emailconfirmlink' )->text(),
601 ] );
602 $emailauthenticationclass = "mw-email-not-authenticated";
603 }
604 } else {
605 $disableEmailPrefs = true;
606 $emailauthenticated = $context->msg( 'noemailprefs' )->escaped();
607 $emailauthenticationclass = 'mw-email-none';
608 }
609
610 if ( $canViewPrivateInfo ) {
611 $defaultPreferences['emailauthentication'] = [
612 'type' => 'info',
613 'raw' => true,
614 'section' => 'personal/email',
615 'label-message' => 'prefs-emailconfirm-label',
616 'default' => $emailauthenticated,
617 # Apply the same CSS class used on the input to the message:
618 'cssclass' => $emailauthenticationclass,
619 ];
620 }
621 }
622
623 if ( $this->options->get( 'AllowRequiringEmailForResets' ) ) {
624 $defaultPreferences['requireemail'] = [
625 'type' => 'toggle',
626 'label-message' => 'tog-requireemail',
627 'help-message' => 'prefs-help-requireemail',
628 'section' => 'personal/email',
629 'disabled' => $disableEmailPrefs,
630 ];
631 }
632
633 if ( $this->options->get( 'EnableUserEmail' ) &&
634 $this->permissionManager->userHasRight( $user, 'sendemail' )
635 ) {
636 $defaultPreferences['disablemail'] = [
637 'id' => 'wpAllowEmail',
638 'type' => 'toggle',
639 'invert' => true,
640 'section' => 'personal/email',
641 'label-message' => 'allowemail',
642 'disabled' => $disableEmailPrefs,
643 ];
644
645 $defaultPreferences['email-allow-new-users'] = [
646 'id' => 'wpAllowEmailFromNewUsers',
647 'type' => 'toggle',
648 'section' => 'personal/email',
649 'label-message' => 'email-allow-new-users-label',
650 'disabled' => $disableEmailPrefs,
651 ];
652
653 $defaultPreferences['ccmeonemails'] = [
654 'type' => 'toggle',
655 'section' => 'personal/email',
656 'label-message' => 'tog-ccmeonemails',
657 'disabled' => $disableEmailPrefs,
658 ];
659
660 if ( $this->options->get( 'EnableUserEmailBlacklist' ) ) {
661 $defaultPreferences['email-blacklist'] = [
662 'type' => 'usersmultiselect',
663 'label-message' => 'email-blacklist-label',
664 'section' => 'personal/email',
665 'disabled' => $disableEmailPrefs,
666 'filter' => MultiUsernameFilter::class,
667 ];
668 }
669 }
670
671 if ( $this->options->get( 'EnotifWatchlist' ) ) {
672 $defaultPreferences['enotifwatchlistpages'] = [
673 'type' => 'toggle',
674 'section' => 'personal/email',
675 'label-message' => 'tog-enotifwatchlistpages',
676 'disabled' => $disableEmailPrefs,
677 ];
678 }
679 if ( $this->options->get( 'EnotifUserTalk' ) ) {
680 $defaultPreferences['enotifusertalkpages'] = [
681 'type' => 'toggle',
682 'section' => 'personal/email',
683 'label-message' => 'tog-enotifusertalkpages',
684 'disabled' => $disableEmailPrefs,
685 ];
686 }
687 if ( $this->options->get( 'EnotifUserTalk' ) ||
688 $this->options->get( 'EnotifWatchlist' ) ) {
689 if ( $this->options->get( 'EnotifMinorEdits' ) ) {
690 $defaultPreferences['enotifminoredits'] = [
691 'type' => 'toggle',
692 'section' => 'personal/email',
693 'label-message' => 'tog-enotifminoredits',
694 'disabled' => $disableEmailPrefs,
695 ];
696 }
697
698 if ( $this->options->get( 'EnotifRevealEditorAddress' ) ) {
699 $defaultPreferences['enotifrevealaddr'] = [
700 'type' => 'toggle',
701 'section' => 'personal/email',
702 'label-message' => 'tog-enotifrevealaddr',
703 'disabled' => $disableEmailPrefs,
704 ];
705 }
706 }
707 }
708 }
709
716 protected function skinPreferences( User $user, IContextSource $context, &$defaultPreferences ) {
717 # # Skin #####################################
718
719 // Skin selector, if there is at least one valid skin
720 $skinOptions = $this->generateSkinOptions( $user, $context );
721 if ( $skinOptions ) {
722 $defaultPreferences['skin'] = [
723 'type' => 'radio',
724 'options' => $skinOptions,
725 'section' => 'rendering/skin',
726 ];
727 }
728
729 $allowUserCss = $this->options->get( 'AllowUserCss' );
730 $allowUserJs = $this->options->get( 'AllowUserJs' );
731 # Create links to user CSS/JS pages for all skins
732 # This code is basically copied from generateSkinOptions(). It'd
733 # be nice to somehow merge this back in there to avoid redundancy.
734 if ( $allowUserCss || $allowUserJs ) {
735 $linkTools = [];
736 $userName = $user->getName();
737
738 if ( $allowUserCss ) {
739 $cssPage = Title::makeTitleSafe( NS_USER, $userName . '/common.css' );
740 $cssLinkText = $context->msg( 'prefs-custom-css' )->text();
741 $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
742 }
743
744 if ( $allowUserJs ) {
745 $jsPage = Title::makeTitleSafe( NS_USER, $userName . '/common.js' );
746 $jsLinkText = $context->msg( 'prefs-custom-js' )->text();
747 $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
748 }
749
750 $defaultPreferences['commoncssjs'] = [
751 'type' => 'info',
752 'raw' => true,
753 'default' => $context->getLanguage()->pipeList( $linkTools ),
754 'label-message' => 'prefs-common-config',
755 'section' => 'rendering/skin',
756 ];
757 }
758 }
759
764 protected function filesPreferences( IContextSource $context, &$defaultPreferences ) {
765 # # Files #####################################
766 $defaultPreferences['imagesize'] = [
767 'type' => 'select',
768 'options' => $this->getImageSizes( $context ),
769 'label-message' => 'imagemaxsize',
770 'section' => 'rendering/files',
771 ];
772 $defaultPreferences['thumbsize'] = [
773 'type' => 'select',
774 'options' => $this->getThumbSizes( $context ),
775 'label-message' => 'thumbsize',
776 'section' => 'rendering/files',
777 ];
778 }
779
786 protected function datetimePreferences( $user, IContextSource $context, &$defaultPreferences ) {
787 # # Date and time #####################################
788 $dateOptions = $this->getDateOptions( $context );
789 if ( $dateOptions ) {
790 $defaultPreferences['date'] = [
791 'type' => 'radio',
792 'options' => $dateOptions,
793 'section' => 'rendering/dateformat',
794 ];
795 }
796
797 // Info
798 $now = wfTimestampNow();
799 $lang = $context->getLanguage();
800 $nowlocal = Xml::element( 'span', [ 'id' => 'wpLocalTime' ],
801 $lang->userTime( $now, $user ) );
802 $nowserver = $lang->userTime( $now, $user,
803 [ 'format' => false, 'timecorrection' => false ] ) .
804 Html::hidden( 'wpServerTime', (int)substr( $now, 8, 2 ) * 60 + (int)substr( $now, 10, 2 ) );
805
806 $defaultPreferences['nowserver'] = [
807 'type' => 'info',
808 'raw' => 1,
809 'label-message' => 'servertime',
810 'default' => $nowserver,
811 'section' => 'rendering/timeoffset',
812 ];
813
814 $defaultPreferences['nowlocal'] = [
815 'type' => 'info',
816 'raw' => 1,
817 'label-message' => 'localtime',
818 'default' => $nowlocal,
819 'section' => 'rendering/timeoffset',
820 ];
821
822 // Grab existing pref.
823 $tzOffset = $user->getOption( 'timecorrection' );
824 $tz = explode( '|', $tzOffset, 3 );
825
826 $tzOptions = $this->getTimezoneOptions( $context );
827
828 $tzSetting = $tzOffset;
829 if ( count( $tz ) > 1 && $tz[0] == 'ZoneInfo' &&
830 !in_array( $tzOffset, HTMLFormField::flattenOptions( $tzOptions ) )
831 ) {
832 // Timezone offset can vary with DST
833 try {
834 $userTZ = new DateTimeZone( $tz[2] );
835 $minDiff = floor( $userTZ->getOffset( new DateTime( 'now' ) ) / 60 );
836 $tzSetting = "ZoneInfo|$minDiff|{$tz[2]}";
837 } catch ( Exception $e ) {
838 // User has an invalid time zone set. Fall back to just using the offset
839 $tz[0] = 'Offset';
840 }
841 }
842 if ( count( $tz ) > 1 && $tz[0] == 'Offset' ) {
843 $minDiff = $tz[1];
844 $tzSetting = sprintf( '%+03d:%02d', floor( $minDiff / 60 ), abs( $minDiff ) % 60 );
845 }
846
847 $defaultPreferences['timecorrection'] = [
848 'class' => \HTMLSelectOrOtherField::class,
849 'label-message' => 'timezonelegend',
850 'options' => $tzOptions,
851 'default' => $tzSetting,
852 'size' => 20,
853 'section' => 'rendering/timeoffset',
854 'id' => 'wpTimeCorrection',
855 'filter' => TimezoneFilter::class,
856 'placeholder-message' => 'timezone-useoffset-placeholder',
857 ];
858 }
859
865 protected function renderingPreferences(
866 User $user,
867 MessageLocalizer $l10n,
868 &$defaultPreferences
869 ) {
870 # # Diffs ####################################
871 $defaultPreferences['diffonly'] = [
872 'type' => 'toggle',
873 'section' => 'rendering/diffs',
874 'label-message' => 'tog-diffonly',
875 ];
876 $defaultPreferences['norollbackdiff'] = [
877 'type' => 'toggle',
878 'section' => 'rendering/diffs',
879 'label-message' => 'tog-norollbackdiff',
880 ];
881
882 # # Page Rendering ##############################
883 if ( $this->options->get( 'AllowUserCssPrefs' ) ) {
884 $defaultPreferences['underline'] = [
885 'type' => 'select',
886 'options' => [
887 $l10n->msg( 'underline-never' )->text() => 0,
888 $l10n->msg( 'underline-always' )->text() => 1,
889 $l10n->msg( 'underline-default' )->text() => 2,
890 ],
891 'label-message' => 'tog-underline',
892 'section' => 'rendering/advancedrendering',
893 ];
894 }
895
896 $stubThresholdValues = [ 50, 100, 500, 1000, 2000, 5000, 10000 ];
897 $stubThresholdOptions = [ $l10n->msg( 'stub-threshold-disabled' )->text() => 0 ];
898 foreach ( $stubThresholdValues as $value ) {
899 $stubThresholdOptions[$l10n->msg( 'size-bytes', $value )->text()] = $value;
900 }
901
902 $defaultPreferences['stubthreshold'] = [
903 'type' => 'select',
904 'section' => 'rendering/advancedrendering',
905 'options' => $stubThresholdOptions,
906 // This is not a raw HTML message; label-raw is needed for the manual <a></a>
907 'label-raw' => $l10n->msg( 'stub-threshold' )->rawParams(
908 '<a class="stub">' .
909 $l10n->msg( 'stub-threshold-sample-link' )->parse() .
910 '</a>' )->parse(),
911 ];
912
913 $defaultPreferences['showhiddencats'] = [
914 'type' => 'toggle',
915 'section' => 'rendering/advancedrendering',
916 'label-message' => 'tog-showhiddencats'
917 ];
918
919 $defaultPreferences['numberheadings'] = [
920 'type' => 'toggle',
921 'section' => 'rendering/advancedrendering',
922 'label-message' => 'tog-numberheadings',
923 ];
924
925 if ( $this->permissionManager->userHasRight( $user, 'rollback' ) ) {
926 $defaultPreferences['showrollbackconfirmation'] = [
927 'type' => 'toggle',
928 'section' => 'rendering/advancedrendering',
929 'label-message' => 'tog-showrollbackconfirmation',
930 ];
931 }
932 }
933
939 protected function editingPreferences( User $user, MessageLocalizer $l10n, &$defaultPreferences ) {
940 # # Editing #####################################
941 $defaultPreferences['editsectiononrightclick'] = [
942 'type' => 'toggle',
943 'section' => 'editing/advancedediting',
944 'label-message' => 'tog-editsectiononrightclick',
945 ];
946 $defaultPreferences['editondblclick'] = [
947 'type' => 'toggle',
948 'section' => 'editing/advancedediting',
949 'label-message' => 'tog-editondblclick',
950 ];
951
952 if ( $this->options->get( 'AllowUserCssPrefs' ) ) {
953 $defaultPreferences['editfont'] = [
954 'type' => 'select',
955 'section' => 'editing/editor',
956 'label-message' => 'editfont-style',
957 'options' => [
958 $l10n->msg( 'editfont-monospace' )->text() => 'monospace',
959 $l10n->msg( 'editfont-sansserif' )->text() => 'sans-serif',
960 $l10n->msg( 'editfont-serif' )->text() => 'serif',
961 ]
962 ];
963 }
964
965 if ( $this->permissionManager->userHasRight( $user, 'minoredit' ) ) {
966 $defaultPreferences['minordefault'] = [
967 'type' => 'toggle',
968 'section' => 'editing/editor',
969 'label-message' => 'tog-minordefault',
970 ];
971 }
972
973 $defaultPreferences['forceeditsummary'] = [
974 'type' => 'toggle',
975 'section' => 'editing/editor',
976 'label-message' => 'tog-forceeditsummary',
977 ];
978 $defaultPreferences['useeditwarning'] = [
979 'type' => 'toggle',
980 'section' => 'editing/editor',
981 'label-message' => 'tog-useeditwarning',
982 ];
983
984 $defaultPreferences['previewonfirst'] = [
985 'type' => 'toggle',
986 'section' => 'editing/preview',
987 'label-message' => 'tog-previewonfirst',
988 ];
989 $defaultPreferences['previewontop'] = [
990 'type' => 'toggle',
991 'section' => 'editing/preview',
992 'label-message' => 'tog-previewontop',
993 ];
994 $defaultPreferences['uselivepreview'] = [
995 'type' => 'toggle',
996 'section' => 'editing/preview',
997 'label-message' => 'tog-uselivepreview',
998 ];
999 }
1000
1006 protected function rcPreferences( User $user, MessageLocalizer $l10n, &$defaultPreferences ) {
1007 $rcMaxAge = $this->options->get( 'RCMaxAge' );
1008 # # RecentChanges #####################################
1009 $defaultPreferences['rcdays'] = [
1010 'type' => 'float',
1011 'label-message' => 'recentchangesdays',
1012 'section' => 'rc/displayrc',
1013 'min' => 1 / 24,
1014 'max' => ceil( $rcMaxAge / ( 3600 * 24 ) ),
1015 'help' => $l10n->msg( 'recentchangesdays-max' )->numParams(
1016 ceil( $rcMaxAge / ( 3600 * 24 ) ) )->escaped()
1017 ];
1018 $defaultPreferences['rclimit'] = [
1019 'type' => 'int',
1020 'min' => 1,
1021 'max' => 1000,
1022 'label-message' => 'recentchangescount',
1023 'help-message' => 'prefs-help-recentchangescount',
1024 'section' => 'rc/displayrc',
1025 'filter' => IntvalFilter::class,
1026 ];
1027 $defaultPreferences['usenewrc'] = [
1028 'type' => 'toggle',
1029 'label-message' => 'tog-usenewrc',
1030 'section' => 'rc/advancedrc',
1031 ];
1032 $defaultPreferences['hideminor'] = [
1033 'type' => 'toggle',
1034 'label-message' => 'tog-hideminor',
1035 'section' => 'rc/changesrc',
1036 ];
1037 $defaultPreferences['rcfilters-rc-collapsed'] = [
1038 'type' => 'api',
1039 ];
1040 $defaultPreferences['rcfilters-wl-collapsed'] = [
1041 'type' => 'api',
1042 ];
1043 $defaultPreferences['rcfilters-saved-queries'] = [
1044 'type' => 'api',
1045 ];
1046 $defaultPreferences['rcfilters-wl-saved-queries'] = [
1047 'type' => 'api',
1048 ];
1049 // Override RCFilters preferences for RecentChanges 'limit'
1050 $defaultPreferences['rcfilters-limit'] = [
1051 'type' => 'api',
1052 ];
1053 $defaultPreferences['rcfilters-saved-queries-versionbackup'] = [
1054 'type' => 'api',
1055 ];
1056 $defaultPreferences['rcfilters-wl-saved-queries-versionbackup'] = [
1057 'type' => 'api',
1058 ];
1059
1060 if ( $this->options->get( 'RCWatchCategoryMembership' ) ) {
1061 $defaultPreferences['hidecategorization'] = [
1062 'type' => 'toggle',
1063 'label-message' => 'tog-hidecategorization',
1064 'section' => 'rc/changesrc',
1065 ];
1066 }
1067
1068 if ( $user->useRCPatrol() ) {
1069 $defaultPreferences['hidepatrolled'] = [
1070 'type' => 'toggle',
1071 'section' => 'rc/changesrc',
1072 'label-message' => 'tog-hidepatrolled',
1073 ];
1074 }
1075
1076 if ( $user->useNPPatrol() ) {
1077 $defaultPreferences['newpageshidepatrolled'] = [
1078 'type' => 'toggle',
1079 'section' => 'rc/changesrc',
1080 'label-message' => 'tog-newpageshidepatrolled',
1081 ];
1082 }
1083
1084 if ( $this->options->get( 'RCShowWatchingUsers' ) ) {
1085 $defaultPreferences['shownumberswatching'] = [
1086 'type' => 'toggle',
1087 'section' => 'rc/advancedrc',
1088 'label-message' => 'tog-shownumberswatching',
1089 ];
1090 }
1091
1092 $defaultPreferences['rcenhancedfilters-disable'] = [
1093 'type' => 'toggle',
1094 'section' => 'rc/advancedrc',
1095 'label-message' => 'rcfilters-preference-label',
1096 'help-message' => 'rcfilters-preference-help',
1097 ];
1098 }
1099
1105 protected function watchlistPreferences(
1106 User $user, IContextSource $context, &$defaultPreferences
1107 ) {
1108 $watchlistdaysMax = ceil( $this->options->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
1109
1110 # # Watchlist #####################################
1111 if ( $this->permissionManager->userHasRight( $user, 'editmywatchlist' ) ) {
1112 $editWatchlistLinks = '';
1113 $editWatchlistModes = [
1114 'edit' => [ 'subpage' => false, 'flags' => [] ],
1115 'raw' => [ 'subpage' => 'raw', 'flags' => [] ],
1116 'clear' => [ 'subpage' => 'clear', 'flags' => [ 'destructive' ] ],
1117 ];
1118 foreach ( $editWatchlistModes as $mode => $options ) {
1119 // Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear
1120 $editWatchlistLinks .=
1121 new \OOUI\ButtonWidget( [
1122 'href' => SpecialPage::getTitleFor( 'EditWatchlist', $options['subpage'] )->getLinkURL(),
1123 'flags' => $options[ 'flags' ],
1124 'label' => new \OOUI\HtmlSnippet(
1125 $context->msg( "prefs-editwatchlist-{$mode}" )->parse()
1126 ),
1127 ] );
1128 }
1129
1130 $defaultPreferences['editwatchlist'] = [
1131 'type' => 'info',
1132 'raw' => true,
1133 'default' => $editWatchlistLinks,
1134 'label-message' => 'prefs-editwatchlist-label',
1135 'section' => 'watchlist/editwatchlist',
1136 ];
1137 }
1138
1139 $defaultPreferences['watchlistdays'] = [
1140 'type' => 'float',
1141 'min' => 1 / 24,
1142 'max' => $watchlistdaysMax,
1143 'section' => 'watchlist/displaywatchlist',
1144 'help' => $context->msg( 'prefs-watchlist-days-max' )->numParams(
1145 $watchlistdaysMax )->escaped(),
1146 'label-message' => 'prefs-watchlist-days',
1147 ];
1148 $defaultPreferences['wllimit'] = [
1149 'type' => 'int',
1150 'min' => 1,
1151 'max' => 1000,
1152 'label-message' => 'prefs-watchlist-edits',
1153 'help' => $context->msg( 'prefs-watchlist-edits-max' )->escaped(),
1154 'section' => 'watchlist/displaywatchlist',
1155 'filter' => IntvalFilter::class,
1156 ];
1157 $defaultPreferences['extendwatchlist'] = [
1158 'type' => 'toggle',
1159 'section' => 'watchlist/advancedwatchlist',
1160 'label-message' => 'tog-extendwatchlist',
1161 ];
1162 $defaultPreferences['watchlisthideminor'] = [
1163 'type' => 'toggle',
1164 'section' => 'watchlist/changeswatchlist',
1165 'label-message' => 'tog-watchlisthideminor',
1166 ];
1167 $defaultPreferences['watchlisthidebots'] = [
1168 'type' => 'toggle',
1169 'section' => 'watchlist/changeswatchlist',
1170 'label-message' => 'tog-watchlisthidebots',
1171 ];
1172 $defaultPreferences['watchlisthideown'] = [
1173 'type' => 'toggle',
1174 'section' => 'watchlist/changeswatchlist',
1175 'label-message' => 'tog-watchlisthideown',
1176 ];
1177 $defaultPreferences['watchlisthideanons'] = [
1178 'type' => 'toggle',
1179 'section' => 'watchlist/changeswatchlist',
1180 'label-message' => 'tog-watchlisthideanons',
1181 ];
1182 $defaultPreferences['watchlisthideliu'] = [
1183 'type' => 'toggle',
1184 'section' => 'watchlist/changeswatchlist',
1185 'label-message' => 'tog-watchlisthideliu',
1186 ];
1187
1189 $defaultPreferences['watchlistreloadautomatically'] = [
1190 'type' => 'toggle',
1191 'section' => 'watchlist/advancedwatchlist',
1192 'label-message' => 'tog-watchlistreloadautomatically',
1193 ];
1194 }
1195
1196 $defaultPreferences['watchlistunwatchlinks'] = [
1197 'type' => 'toggle',
1198 'section' => 'watchlist/advancedwatchlist',
1199 'label-message' => 'tog-watchlistunwatchlinks',
1200 ];
1201
1202 if ( $this->options->get( 'RCWatchCategoryMembership' ) ) {
1203 $defaultPreferences['watchlisthidecategorization'] = [
1204 'type' => 'toggle',
1205 'section' => 'watchlist/changeswatchlist',
1206 'label-message' => 'tog-watchlisthidecategorization',
1207 ];
1208 }
1209
1210 if ( $user->useRCPatrol() ) {
1211 $defaultPreferences['watchlisthidepatrolled'] = [
1212 'type' => 'toggle',
1213 'section' => 'watchlist/changeswatchlist',
1214 'label-message' => 'tog-watchlisthidepatrolled',
1215 ];
1216 }
1217
1218 $watchTypes = [
1219 'edit' => 'watchdefault',
1220 'move' => 'watchmoves',
1221 'delete' => 'watchdeletion'
1222 ];
1223
1224 // Kinda hacky
1225 if ( $this->permissionManager->userHasAnyRight( $user, 'createpage', 'createtalk' ) ) {
1226 $watchTypes['read'] = 'watchcreations';
1227 }
1228
1229 if ( $this->permissionManager->userHasRight( $user, 'rollback' ) ) {
1230 $watchTypes['rollback'] = 'watchrollback';
1231 }
1232
1233 if ( $this->permissionManager->userHasRight( $user, 'upload' ) ) {
1234 $watchTypes['upload'] = 'watchuploads';
1235 }
1236
1237 foreach ( $watchTypes as $action => $pref ) {
1238 if ( $this->permissionManager->userHasRight( $user, $action ) ) {
1239 // Messages:
1240 // tog-watchdefault, tog-watchmoves, tog-watchdeletion, tog-watchcreations, tog-watchuploads
1241 // tog-watchrollback
1242 $defaultPreferences[$pref] = [
1243 'type' => 'toggle',
1244 'section' => 'watchlist/pageswatchlist',
1245 'label-message' => "tog-$pref",
1246 ];
1247 }
1248 }
1249
1250 $defaultPreferences['watchlisttoken'] = [
1251 'type' => 'api',
1252 ];
1253
1254 $tokenButton = new \OOUI\ButtonWidget( [
1255 'href' => SpecialPage::getTitleFor( 'ResetTokens' )->getLinkURL( [
1256 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
1257 ] ),
1258 'label' => $context->msg( 'prefs-watchlist-managetokens' )->text(),
1259 ] );
1260 $defaultPreferences['watchlisttoken-info'] = [
1261 'type' => 'info',
1262 'section' => 'watchlist/tokenwatchlist',
1263 'label-message' => 'prefs-watchlist-token',
1264 'help-message' => 'prefs-help-tokenmanagement',
1265 'raw' => true,
1266 'default' => (string)$tokenButton,
1267 ];
1268
1269 $defaultPreferences['wlenhancedfilters-disable'] = [
1270 'type' => 'toggle',
1271 'section' => 'watchlist/advancedwatchlist',
1272 'label-message' => 'rcfilters-watchlist-preference-label',
1273 'help-message' => 'rcfilters-watchlist-preference-help',
1274 ];
1275 }
1276
1280 protected function searchPreferences( &$defaultPreferences ) {
1281 foreach ( $this->nsInfo->getValidNamespaces() as $n ) {
1282 $defaultPreferences['searchNs' . $n] = [
1283 'type' => 'api',
1284 ];
1285 }
1286 }
1287
1293 protected function generateSkinOptions( User $user, IContextSource $context ) {
1294 $ret = [];
1295
1296 $mptitle = Title::newMainPage();
1297 $previewtext = $context->msg( 'skin-preview' )->escaped();
1298
1299 # Only show skins that aren't disabled in $wgSkipSkins
1300 $validSkinNames = Skin::getAllowedSkins();
1301 $allInstalledSkins = Skin::getSkinNames();
1302
1303 // Display the installed skin the user has specifically requested via useskin=….
1304 $useSkin = $context->getRequest()->getRawVal( 'useskin' );
1305 if ( isset( $allInstalledSkins[$useSkin] )
1306 && $context->msg( "skinname-$useSkin" )->exists()
1307 ) {
1308 $validSkinNames[$useSkin] = $useSkin;
1309 }
1310
1311 // Display the skin if the user has set it as a preference already before it was hidden.
1312 $currentUserSkin = $user->getOption( 'skin' );
1313 if ( isset( $allInstalledSkins[$currentUserSkin] )
1314 && $context->msg( "skinname-$currentUserSkin" )->exists()
1315 ) {
1316 $validSkinNames[$currentUserSkin] = $currentUserSkin;
1317 }
1318
1319 foreach ( $validSkinNames as $skinkey => &$skinname ) {
1320 $msg = $context->msg( "skinname-{$skinkey}" );
1321 if ( $msg->exists() ) {
1322 $skinname = htmlspecialchars( $msg->text() );
1323 }
1324 }
1325
1326 $defaultSkin = $this->options->get( 'DefaultSkin' );
1327 $allowUserCss = $this->options->get( 'AllowUserCss' );
1328 $allowUserJs = $this->options->get( 'AllowUserJs' );
1329
1330 # Sort by the internal name, so that the ordering is the same for each display language,
1331 # especially if some skin names are translated to use a different alphabet and some are not.
1332 uksort( $validSkinNames, function ( $a, $b ) use ( $defaultSkin ) {
1333 # Display the default first in the list by comparing it as lesser than any other.
1334 if ( strcasecmp( $a, $defaultSkin ) === 0 ) {
1335 return -1;
1336 }
1337 if ( strcasecmp( $b, $defaultSkin ) === 0 ) {
1338 return 1;
1339 }
1340 return strcasecmp( $a, $b );
1341 } );
1342
1343 $foundDefault = false;
1344 foreach ( $validSkinNames as $skinkey => $sn ) {
1345 $linkTools = [];
1346
1347 # Mark the default skin
1348 if ( strcasecmp( $skinkey, $defaultSkin ) === 0 ) {
1349 $linkTools[] = $context->msg( 'default' )->escaped();
1350 $foundDefault = true;
1351 }
1352
1353 # Create preview link
1354 $mplink = htmlspecialchars( $mptitle->getLocalURL( [ 'useskin' => $skinkey ] ) );
1355 $linkTools[] = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
1356
1357 # Create links to user CSS/JS pages
1358 if ( $allowUserCss ) {
1359 $cssPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.css' );
1360 $cssLinkText = $context->msg( 'prefs-custom-css' )->text();
1361 $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
1362 }
1363
1364 if ( $allowUserJs ) {
1365 $jsPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.js' );
1366 $jsLinkText = $context->msg( 'prefs-custom-js' )->text();
1367 $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
1368 }
1369
1370 $display = $sn . ' ' . $context->msg( 'parentheses' )
1371 ->rawParams( $context->getLanguage()->pipeList( $linkTools ) )
1372 ->escaped();
1373 $ret[$display] = $skinkey;
1374 }
1375
1376 if ( !$foundDefault ) {
1377 // If the default skin is not available, things are going to break horribly because the
1378 // default value for skin selector will not be a valid value. Let's just not show it then.
1379 return [];
1380 }
1381
1382 return $ret;
1383 }
1384
1390 $lang = $context->getLanguage();
1391 $dateopts = $lang->getDatePreferences();
1392
1393 $ret = [];
1394
1395 if ( $dateopts ) {
1396 if ( !in_array( 'default', $dateopts ) ) {
1397 $dateopts[] = 'default'; // Make sure default is always valid T21237
1398 }
1399
1400 // FIXME KLUGE: site default might not be valid for user language
1401 global $wgDefaultUserOptions;
1402 if ( !in_array( $wgDefaultUserOptions['date'], $dateopts ) ) {
1403 $wgDefaultUserOptions['date'] = 'default';
1404 }
1405
1406 $epoch = wfTimestampNow();
1407 foreach ( $dateopts as $key ) {
1408 if ( $key == 'default' ) {
1409 $formatted = $context->msg( 'datedefault' )->escaped();
1410 } else {
1411 $formatted = htmlspecialchars( $lang->timeanddate( $epoch, false, $key ) );
1412 }
1413 $ret[$formatted] = $key;
1414 }
1415 }
1416 return $ret;
1417 }
1418
1423 protected function getImageSizes( MessageLocalizer $l10n ) {
1424 $ret = [];
1425 $pixels = $l10n->msg( 'unit-pixel' )->text();
1426
1427 foreach ( $this->options->get( 'ImageLimits' ) as $index => $limits ) {
1428 // Note: A left-to-right marker (U+200E) is inserted, see T144386
1429 $display = "{$limits[0]}\u{200E}×{$limits[1]}$pixels";
1430 $ret[$display] = $index;
1431 }
1432
1433 return $ret;
1434 }
1435
1440 protected function getThumbSizes( MessageLocalizer $l10n ) {
1441 $ret = [];
1442 $pixels = $l10n->msg( 'unit-pixel' )->text();
1443
1444 foreach ( $this->options->get( 'ThumbLimits' ) as $index => $size ) {
1445 $display = $size . $pixels;
1446 $ret[$display] = $index;
1447 }
1448
1449 return $ret;
1450 }
1451
1458 protected function validateSignature( $signature, $alldata, HTMLForm $form ) {
1459 $maxSigChars = $this->options->get( 'MaxSigChars' );
1460 if ( mb_strlen( $signature ) > $maxSigChars ) {
1461 return Xml::element( 'span', [ 'class' => 'error' ],
1462 $form->msg( 'badsiglength' )->numParams( $maxSigChars )->text() );
1463 } elseif ( isset( $alldata['fancysig'] ) &&
1464 $alldata['fancysig'] &&
1465 MediaWikiServices::getInstance()->getParser()->validateSig( $signature ) === false
1466 ) {
1467 return Xml::element(
1468 'span',
1469 [ 'class' => 'error' ],
1470 $form->msg( 'badsig' )->text()
1471 );
1472 } else {
1473 return true;
1474 }
1475 }
1476
1483 protected function cleanSignature( $signature, $alldata, HTMLForm $form ) {
1484 $parser = MediaWikiServices::getInstance()->getParser();
1485 if ( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) {
1486 $signature = $parser->cleanSig( $signature );
1487 } else {
1488 // When no fancy sig used, make sure ~{3,5} get removed.
1489 $signature = Parser::cleanSigInSig( $signature );
1490 }
1491
1492 return $signature;
1493 }
1494
1502 public function getForm(
1503 User $user,
1505 $formClass = PreferencesFormOOUI::class,
1506 array $remove = []
1507 ) {
1508 // We use ButtonWidgets in some of the getPreferences() functions
1509 $context->getOutput()->enableOOUI();
1510
1511 $formDescriptor = $this->getFormDescriptor( $user, $context );
1512 if ( count( $remove ) ) {
1513 $removeKeys = array_flip( $remove );
1514 $formDescriptor = array_diff_key( $formDescriptor, $removeKeys );
1515 }
1516
1517 // Remove type=api preferences. They are not intended for rendering in the form.
1518 foreach ( $formDescriptor as $name => $info ) {
1519 if ( isset( $info['type'] ) && $info['type'] === 'api' ) {
1520 unset( $formDescriptor[$name] );
1521 }
1522 }
1523
1527 $htmlForm = new $formClass( $formDescriptor, $context, 'prefs' );
1528
1529 // This allows users to opt-in to hidden skins. While this should be discouraged and is not
1530 // discoverable, this allows users to still use hidden skins while preventing new users from
1531 // adopting unsupported skins. If no useskin=… parameter was provided, it will not show up
1532 // in the resulting URL.
1533 $htmlForm->setAction( $context->getTitle()->getLocalURL( [
1534 'useskin' => $context->getRequest()->getRawVal( 'useskin' )
1535 ] ) );
1536
1537 $htmlForm->setModifiedUser( $user );
1538 $htmlForm->setOptionsEditable( $this->permissionManager
1539 ->userHasRight( $user, 'editmyoptions' ) );
1540 $htmlForm->setPrivateInfoEditable( $this->permissionManager
1541 ->userHasRight( $user, 'editmyprivateinfo' ) );
1542 $htmlForm->setId( 'mw-prefs-form' );
1543 $htmlForm->setAutocomplete( 'off' );
1544 $htmlForm->setSubmitText( $context->msg( 'saveprefs' )->text() );
1545 # Used message keys: 'accesskey-preferences-save', 'tooltip-preferences-save'
1546 $htmlForm->setSubmitTooltip( 'preferences-save' );
1547 $htmlForm->setSubmitID( 'prefcontrol' );
1548 $htmlForm->setSubmitCallback(
1549 function ( array $formData, PreferencesFormOOUI $form ) use ( $formDescriptor ) {
1550 return $this->submitForm( $formData, $form, $formDescriptor );
1551 }
1552 );
1553
1554 return $htmlForm;
1555 }
1556
1562 $opt = [];
1563
1564 $localTZoffset = $this->options->get( 'LocalTZoffset' );
1565 $timeZoneList = $this->getTimeZoneList( $context->getLanguage() );
1566
1567 $timestamp = MWTimestamp::getLocalInstance();
1568 // Check that the LocalTZoffset is the same as the local time zone offset
1569 if ( $localTZoffset == $timestamp->format( 'Z' ) / 60 ) {
1570 $timezoneName = $timestamp->getTimezone()->getName();
1571 // Localize timezone
1572 if ( isset( $timeZoneList[$timezoneName] ) ) {
1573 $timezoneName = $timeZoneList[$timezoneName]['name'];
1574 }
1575 $server_tz_msg = $context->msg(
1576 'timezoneuseserverdefault',
1577 $timezoneName
1578 )->text();
1579 } else {
1580 $tzstring = sprintf(
1581 '%+03d:%02d',
1582 floor( $localTZoffset / 60 ),
1583 abs( $localTZoffset ) % 60
1584 );
1585 $server_tz_msg = $context->msg( 'timezoneuseserverdefault', $tzstring )->text();
1586 }
1587 $opt[$server_tz_msg] = "System|$localTZoffset";
1588 $opt[$context->msg( 'timezoneuseoffset' )->text()] = 'other';
1589 $opt[$context->msg( 'guesstimezone' )->text()] = 'guess';
1590
1591 foreach ( $timeZoneList as $timeZoneInfo ) {
1592 $region = $timeZoneInfo['region'];
1593 if ( !isset( $opt[$region] ) ) {
1594 $opt[$region] = [];
1595 }
1596 $opt[$region][$timeZoneInfo['name']] = $timeZoneInfo['timecorrection'];
1597 }
1598 return $opt;
1599 }
1600
1609 protected function saveFormData( $formData, PreferencesFormOOUI $form, array $formDescriptor ) {
1610 $user = $form->getModifiedUser();
1611 $hiddenPrefs = $this->options->get( 'HiddenPrefs' );
1612 $result = true;
1613
1614 if ( !$this->permissionManager
1615 ->userHasAnyRight( $user, 'editmyprivateinfo', 'editmyoptions' )
1616 ) {
1617 return Status::newFatal( 'mypreferencesprotected' );
1618 }
1619
1620 // Filter input
1621 $this->applyFilters( $formData, $formDescriptor, 'filterFromForm' );
1622
1623 // Fortunately, the realname field is MUCH simpler
1624 // (not really "private", but still shouldn't be edited without permission)
1625
1626 if ( !in_array( 'realname', $hiddenPrefs )
1627 && $this->permissionManager->userHasRight( $user, 'editmyprivateinfo' )
1628 && array_key_exists( 'realname', $formData )
1629 ) {
1630 $realName = $formData['realname'];
1631 $user->setRealName( $realName );
1632 }
1633
1634 if ( $this->permissionManager->userHasRight( $user, 'editmyoptions' ) ) {
1635 $oldUserOptions = $user->getOptions();
1636
1637 foreach ( $this->getSaveBlacklist() as $b ) {
1638 unset( $formData[$b] );
1639 }
1640
1641 # If users have saved a value for a preference which has subsequently been disabled
1642 # via $wgHiddenPrefs, we don't want to destroy that setting in case the preference
1643 # is subsequently re-enabled
1644 foreach ( $hiddenPrefs as $pref ) {
1645 # If the user has not set a non-default value here, the default will be returned
1646 # and subsequently discarded
1647 $formData[$pref] = $user->getOption( $pref, null, true );
1648 }
1649
1650 // If the user changed the rclimit preference, also change the rcfilters-rclimit preference
1651 if (
1652 isset( $formData['rclimit'] ) &&
1653 intval( $formData[ 'rclimit' ] ) !== $user->getIntOption( 'rclimit' )
1654 ) {
1655 $formData['rcfilters-limit'] = $formData['rclimit'];
1656 }
1657
1658 // Keep old preferences from interfering due to back-compat code, etc.
1659 $user->resetOptions( 'unused', $form->getContext() );
1660
1661 foreach ( $formData as $key => $value ) {
1662 $user->setOption( $key, $value );
1663 }
1664
1665 Hooks::run(
1666 'PreferencesFormPreSave',
1667 [ $formData, $form, $user, &$result, $oldUserOptions ]
1668 );
1669 }
1670
1671 $user->saveSettings();
1672
1673 return $result;
1674 }
1675
1684 protected function applyFilters( array &$preferences, array $formDescriptor, $verb ) {
1685 foreach ( $formDescriptor as $preference => $desc ) {
1686 if ( !isset( $desc['filter'] ) || !isset( $preferences[$preference] ) ) {
1687 continue;
1688 }
1689 $filterDesc = $desc['filter'];
1690 if ( $filterDesc instanceof Filter ) {
1691 $filter = $filterDesc;
1692 } elseif ( class_exists( $filterDesc ) ) {
1693 $filter = new $filterDesc();
1694 } elseif ( is_callable( $filterDesc ) ) {
1695 $filter = $filterDesc();
1696 } else {
1697 throw new UnexpectedValueException(
1698 "Unrecognized filter type for preference '$preference'"
1699 );
1700 }
1701 $preferences[$preference] = $filter->$verb( $preferences[$preference] );
1702 }
1703 }
1704
1713 protected function submitForm(
1714 array $formData,
1715 PreferencesFormOOUI $form,
1716 array $formDescriptor
1717 ) {
1718 $res = $this->saveFormData( $formData, $form, $formDescriptor );
1719
1720 if ( $res === true ) {
1721 $context = $form->getContext();
1722 $urlOptions = [];
1723
1724 if ( $res === 'eauth' ) {
1725 $urlOptions['eauth'] = 1;
1726 }
1727
1728 $urlOptions += $form->getExtraSuccessRedirectParameters();
1729
1730 $url = $form->getTitle()->getFullURL( $urlOptions );
1731
1732 // Set session data for the success message
1733 $context->getRequest()->getSession()->set( 'specialPreferencesSaveSuccess', 1 );
1734
1735 $context->getOutput()->redirect( $url );
1736 }
1737
1738 return ( $res === true ? Status::newGood() : $res );
1739 }
1740
1749 protected function getTimeZoneList( Language $language ) {
1750 $identifiers = DateTimeZone::listIdentifiers();
1751 // @phan-suppress-next-line PhanTypeComparisonFromArray See phan issue #3162
1752 if ( $identifiers === false ) {
1753 return [];
1754 }
1755 sort( $identifiers );
1756
1757 $tzRegions = [
1758 'Africa' => wfMessage( 'timezoneregion-africa' )->inLanguage( $language )->text(),
1759 'America' => wfMessage( 'timezoneregion-america' )->inLanguage( $language )->text(),
1760 'Antarctica' => wfMessage( 'timezoneregion-antarctica' )->inLanguage( $language )->text(),
1761 'Arctic' => wfMessage( 'timezoneregion-arctic' )->inLanguage( $language )->text(),
1762 'Asia' => wfMessage( 'timezoneregion-asia' )->inLanguage( $language )->text(),
1763 'Atlantic' => wfMessage( 'timezoneregion-atlantic' )->inLanguage( $language )->text(),
1764 'Australia' => wfMessage( 'timezoneregion-australia' )->inLanguage( $language )->text(),
1765 'Europe' => wfMessage( 'timezoneregion-europe' )->inLanguage( $language )->text(),
1766 'Indian' => wfMessage( 'timezoneregion-indian' )->inLanguage( $language )->text(),
1767 'Pacific' => wfMessage( 'timezoneregion-pacific' )->inLanguage( $language )->text(),
1768 ];
1769 asort( $tzRegions );
1770
1771 $timeZoneList = [];
1772
1773 $now = new DateTime();
1774
1775 foreach ( $identifiers as $identifier ) {
1776 $parts = explode( '/', $identifier, 2 );
1777
1778 // DateTimeZone::listIdentifiers() returns a number of
1779 // backwards-compatibility entries. This filters them out of the
1780 // list presented to the user.
1781 if ( count( $parts ) !== 2 || !array_key_exists( $parts[0], $tzRegions ) ) {
1782 continue;
1783 }
1784
1785 // Localize region
1786 $parts[0] = $tzRegions[$parts[0]];
1787
1788 $dateTimeZone = new DateTimeZone( $identifier );
1789 $minDiff = floor( $dateTimeZone->getOffset( $now ) / 60 );
1790
1791 $display = str_replace( '_', ' ', $parts[0] . '/' . $parts[1] );
1792 $value = "ZoneInfo|$minDiff|$identifier";
1793
1794 $timeZoneList[$identifier] = [
1795 'name' => $display,
1796 'timecorrection' => $value,
1797 'region' => $parts[0],
1798 ];
1799 }
1800
1801 return $timeZoneList;
1802 }
1803}
$wgDefaultUserOptions
Settings added to this array will override the default globals for the user preferences used by anony...
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfCanIPUseHTTPS( $ip)
Determine whether the client at a given source IP is likely to be able to access the wiki via HTTPS.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
getContext()
Get the base IContextSource object.
The parent class to generate form fields.
static flattenOptions( $options)
flatten an array of options to a single array, for instance, a set of "<options>" inside "<optgroups>...
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:131
getTitle()
Get the title.
Hooks class.
Definition Hooks.php:34
This class is a collection of static functions that serve two purposes:
Definition Html.php:49
Methods for dealing with language codes.
static bcp47( $code)
Get the normalised IETF language tag See unit test for examples.
Base class for language conversion.
static array $languagesWithVariants
languages supporting variants
Internationalisation code.
Definition Language.php:37
MediaWiki exception.
Library for creating and parsing MW-style timestamps.
static getLocalInstance( $ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
This serves as the entry point to the authentication system.
This is a value object for authentication requests with a username and password.
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
Class that generates HTML links for pages.
MediaWikiServices is the service locator for the application scope of MediaWiki.
static getInstance()
Returns the global default instance of the top level service locator.
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
This is the default implementation of PreferencesFactory.
validateSignature( $signature, $alldata, HTMLForm $form)
getTimeZoneList(Language $language)
Get a list of all time zones.
rcPreferences(User $user, MessageLocalizer $l10n, &$defaultPreferences)
profilePreferences(User $user, IContextSource $context, &$defaultPreferences, $canIPUseHTTPS)
watchlistPreferences(User $user, IContextSource $context, &$defaultPreferences)
__construct(ServiceOptions $options, Language $contLang, AuthManager $authManager, LinkRenderer $linkRenderer, NamespaceInfo $nsInfo, PermissionManager $permissionManager)
Do not call this directly.
renderingPreferences(User $user, MessageLocalizer $l10n, &$defaultPreferences)
getForm(User $user, IContextSource $context, $formClass=PreferencesFormOOUI::class, array $remove=[])
skinPreferences(User $user, IContextSource $context, &$defaultPreferences)
loadPreferenceValues(User $user, IContextSource $context, &$defaultPreferences)
Loads existing values for a given array of preferences.
editingPreferences(User $user, MessageLocalizer $l10n, &$defaultPreferences)
datetimePreferences( $user, IContextSource $context, &$defaultPreferences)
getOptionFromUser( $name, $info, array $userOptions)
Pull option from a user account.
cleanSignature( $signature, $alldata, HTMLForm $form)
getSaveBlacklist()
Get the names of preferences that should never be saved (such as 'realname' and 'emailaddress')....
applyFilters(array &$preferences, array $formDescriptor, $verb)
Applies filters to preferences either before or after form usage.
filesPreferences(IContextSource $context, &$defaultPreferences)
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.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
This is one of the Core classes and should be read at least once by any new developers.
Set options of the Parser.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:74
static stripOuterParagraph( $html)
Strip outer.
Definition Parser.php:6797
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
Definition Parser.php:5096
Form to edit user preferences.
getExtraSuccessRedirectParameters()
Get extra parameters for the query string when redirecting after successful save.
The main skin class which provides methods and properties for all other skins.
Definition Skin.php:38
static getAllowedSkins()
Fetch the list of user-selectable skins in regards to $wgSkipSkins.
Definition Skin.php:83
static getSkinNames()
Fetch the set of available skins.
Definition Skin.php:57
Parent class for all special pages.
static checkStructuredFilterUiEnabled( $user)
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:40
Represents a title within MediaWiki.
Definition Title.php:42
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:51
getOptions( $flags=0)
Get all user's options.
Definition User.php:3050
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2364
getEmailAuthenticationTimestamp()
Get the timestamp of the user's e-mail authentication.
Definition User.php:2915
getRealName()
Get the user's real name.
Definition User.php:2995
getRegistration()
Get the timestamp of account creation.
Definition User.php:4781
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition User.php:3712
getOption( $oname, $defaultOverride=null, $ignoreHidden=false)
Get the user's current setting for a given option.
Definition User.php:3022
static getDefaultOptions()
Combine the language default options with any site-specific options and add the default language vari...
Definition User.php:1692
getEffectiveGroups( $recache=false)
Get the list of implicit group memberships this user has.
Definition User.php:3442
getGroupMemberships()
Get the list of explicit group memberships this user has, stored as UserGroupMembership objects.
Definition User.php:3429
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition User.php:3703
getEditCount()
Get the user's edit count.
Definition User.php:3518
getEmail()
Get the user's e-mail address.
Definition User.php:2905
Module of static functions for generating XML.
Definition Xml.php:28
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition Xml.php:41
const NS_USER
Definition Defines.php:71
Interface for objects which can provide a MediaWiki context on request.
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.
$context
Definition load.php:45
$filter
if(!isset( $args[0])) $lang
switch( $options['output']) $languages
Definition transstat.php:76