MediaWiki REL1_31
DefaultPreferencesFactory.php
Go to the documentation of this file.
1<?php
22
24use Config;
25use DateTime;
26use DateTimeZone;
27use Exception;
28use Hooks;
29use Html;
30use HtmlArmor;
31use HTMLForm;
34use Language;
35use LanguageCode;
42use MWException;
43use MWNamespace;
44use MWTimestamp;
45use Parser;
48use Psr\Log\LoggerAwareTrait;
49use Psr\Log\NullLogger;
50use Skin;
51use SpecialPage;
52use Status;
53use Title;
54use User;
56use Xml;
57
62 use LoggerAwareTrait;
63
65 protected $config;
66
68 protected $contLang;
69
71 protected $authManager;
72
74 protected $linkRenderer;
75
82 public function __construct(
87 ) {
88 $this->config = $config;
89 $this->contLang = $contLang;
90 $this->authManager = $authManager;
91 $this->linkRenderer = $linkRenderer;
92 $this->logger = new NullLogger();
93 }
94
98 protected function getSaveFilters() {
99 // Wrap intval() so that we can pass it multiple parameters and treat all filters the same.
100 $intvalFilter = function ( $value, $alldata ) {
101 return intval( $value );
102 };
103 return [
104 'timecorrection' => [ $this, 'filterTimezoneInput' ],
105 'rclimit' => $intvalFilter,
106 'wllimit' => $intvalFilter,
107 'searchlimit' => $intvalFilter,
108 ];
109 }
110
114 public function getSaveBlacklist() {
115 return [
116 'realname',
117 'emailaddress',
118 ];
119 }
120
127 public function getFormDescriptor( User $user, IContextSource $context ) {
128 $preferences = [];
129
130 $canIPUseHTTPS = wfCanIPUseHTTPS( $context->getRequest()->getIP() );
131 $this->profilePreferences( $user, $context, $preferences, $canIPUseHTTPS );
132 $this->skinPreferences( $user, $context, $preferences );
133 $this->datetimePreferences( $user, $context, $preferences );
134 $this->filesPreferences( $context, $preferences );
135 $this->renderingPreferences( $context, $preferences );
136 $this->editingPreferences( $user, $context, $preferences );
137 $this->rcPreferences( $user, $context, $preferences );
138 $this->watchlistPreferences( $user, $context, $preferences );
139 $this->searchPreferences( $preferences );
140
141 Hooks::run( 'GetPreferences', [ $user, &$preferences ] );
142
143 $this->loadPreferenceValues( $user, $context, $preferences );
144 $this->logger->debug( "Created form descriptor for user '{$user->getName()}'" );
145 return $preferences;
146 }
147
156 private function loadPreferenceValues(
157 User $user, IContextSource $context, &$defaultPreferences
158 ) {
159 # # Remove preferences that wikis don't want to use
160 foreach ( $this->config->get( 'HiddenPrefs' ) as $pref ) {
161 if ( isset( $defaultPreferences[$pref] ) ) {
162 unset( $defaultPreferences[$pref] );
163 }
164 }
165
166 # # Make sure that form fields have their parent set. See T43337.
167 $dummyForm = new HTMLForm( [], $context );
168
169 $disable = !$user->isAllowed( 'editmyoptions' );
170
171 $defaultOptions = User::getDefaultOptions();
172 # # Prod in defaults from the user
173 foreach ( $defaultPreferences as $name => &$info ) {
174 $prefFromUser = $this->getOptionFromUser( $name, $info, $user );
175 if ( $disable && !in_array( $name, $this->getSaveBlacklist() ) ) {
176 $info['disabled'] = 'disabled';
177 }
178 $field = HTMLForm::loadInputFromParameters( $name, $info, $dummyForm ); // For validation
179 $globalDefault = isset( $defaultOptions[$name] )
180 ? $defaultOptions[$name]
181 : null;
182
183 // If it validates, set it as the default
184 if ( isset( $info['default'] ) ) {
185 // Already set, no problem
186 continue;
187 } elseif ( !is_null( $prefFromUser ) && // Make sure we're not just pulling nothing
188 $field->validate( $prefFromUser, $user->getOptions() ) === true ) {
189 $info['default'] = $prefFromUser;
190 } elseif ( $field->validate( $globalDefault, $user->getOptions() ) === true ) {
191 $info['default'] = $globalDefault;
192 } else {
193 throw new MWException( "Global default '$globalDefault' is invalid for field $name" );
194 }
195 }
196
197 return $defaultPreferences;
198 }
199
208 protected function getOptionFromUser( $name, $info, User $user ) {
209 $val = $user->getOption( $name );
210
211 // Handling for multiselect preferences
212 if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
213 ( isset( $info['class'] ) && $info['class'] == \HTMLMultiSelectField::class ) ) {
214 $options = HTMLFormField::flattenOptions( $info['options'] );
215 $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
216 $val = [];
217
218 foreach ( $options as $value ) {
219 if ( $user->getOption( "$prefix$value" ) ) {
220 $val[] = $value;
221 }
222 }
223 }
224
225 // Handling for checkmatrix preferences
226 if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
227 ( isset( $info['class'] ) && $info['class'] == \HTMLCheckMatrix::class ) ) {
228 $columns = HTMLFormField::flattenOptions( $info['columns'] );
229 $rows = HTMLFormField::flattenOptions( $info['rows'] );
230 $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
231 $val = [];
232
233 foreach ( $columns as $column ) {
234 foreach ( $rows as $row ) {
235 if ( $user->getOption( "$prefix$column-$row" ) ) {
236 $val[] = "$column-$row";
237 }
238 }
239 }
240 }
241
242 return $val;
243 }
244
254 protected function profilePreferences(
255 User $user, IContextSource $context, &$defaultPreferences, $canIPUseHTTPS
256 ) {
257 // retrieving user name for GENDER and misc.
258 $userName = $user->getName();
259
260 # # User info #####################################
261 // Information panel
262 $defaultPreferences['username'] = [
263 'type' => 'info',
264 'label-message' => [ 'username', $userName ],
265 'default' => $userName,
266 'section' => 'personal/info',
267 ];
268
269 $lang = $context->getLanguage();
270
271 # Get groups to which the user belongs
272 $userEffectiveGroups = $user->getEffectiveGroups();
273 $userGroupMemberships = $user->getGroupMemberships();
274 $userGroups = $userMembers = $userTempGroups = $userTempMembers = [];
275 foreach ( $userEffectiveGroups as $ueg ) {
276 if ( $ueg == '*' ) {
277 // Skip the default * group, seems useless here
278 continue;
279 }
280
281 if ( isset( $userGroupMemberships[$ueg] ) ) {
282 $groupStringOrObject = $userGroupMemberships[$ueg];
283 } else {
284 $groupStringOrObject = $ueg;
285 }
286
287 $userG = UserGroupMembership::getLink( $groupStringOrObject, $context, 'html' );
288 $userM = UserGroupMembership::getLink( $groupStringOrObject, $context, 'html',
289 $userName );
290
291 // Store expiring groups separately, so we can place them before non-expiring
292 // groups in the list. This is to avoid the ambiguity of something like
293 // "administrator, bureaucrat (until X date)" -- users might wonder whether the
294 // expiry date applies to both groups, or just the last one
295 if ( $groupStringOrObject instanceof UserGroupMembership &&
296 $groupStringOrObject->getExpiry()
297 ) {
298 $userTempGroups[] = $userG;
299 $userTempMembers[] = $userM;
300 } else {
301 $userGroups[] = $userG;
302 $userMembers[] = $userM;
303 }
304 }
305 sort( $userGroups );
306 sort( $userMembers );
307 sort( $userTempGroups );
308 sort( $userTempMembers );
309 $userGroups = array_merge( $userTempGroups, $userGroups );
310 $userMembers = array_merge( $userTempMembers, $userMembers );
311
312 $defaultPreferences['usergroups'] = [
313 'type' => 'info',
314 'label' => $context->msg( 'prefs-memberingroups' )->numParams(
315 count( $userGroups ) )->params( $userName )->parse(),
316 'default' => $context->msg( 'prefs-memberingroups-type' )
317 ->rawParams( $lang->commaList( $userGroups ), $lang->commaList( $userMembers ) )
318 ->escaped(),
319 'raw' => true,
320 'section' => 'personal/info',
321 ];
322
323 $contribTitle = SpecialPage::getTitleFor( "Contributions", $userName );
324 $formattedEditCount = $lang->formatNum( $user->getEditCount() );
325 $editCount = $this->linkRenderer->makeLink( $contribTitle, $formattedEditCount );
326
327 $defaultPreferences['editcount'] = [
328 'type' => 'info',
329 'raw' => true,
330 'label-message' => 'prefs-edits',
331 'default' => $editCount,
332 'section' => 'personal/info',
333 ];
334
335 if ( $user->getRegistration() ) {
336 $displayUser = $context->getUser();
337 $userRegistration = $user->getRegistration();
338 $defaultPreferences['registrationdate'] = [
339 'type' => 'info',
340 'label-message' => 'prefs-registration',
341 'default' => $context->msg(
342 'prefs-registration-date-time',
343 $lang->userTimeAndDate( $userRegistration, $displayUser ),
344 $lang->userDate( $userRegistration, $displayUser ),
345 $lang->userTime( $userRegistration, $displayUser )
346 )->parse(),
347 'section' => 'personal/info',
348 ];
349 }
350
351 $canViewPrivateInfo = $user->isAllowed( 'viewmyprivateinfo' );
352 $canEditPrivateInfo = $user->isAllowed( 'editmyprivateinfo' );
353
354 // Actually changeable stuff
355 $defaultPreferences['realname'] = [
356 // (not really "private", but still shouldn't be edited without permission)
357 'type' => $canEditPrivateInfo && $this->authManager->allowsPropertyChange( 'realname' )
358 ? 'text' : 'info',
359 'default' => $user->getRealName(),
360 'section' => 'personal/info',
361 'label-message' => 'yourrealname',
362 'help-message' => 'prefs-help-realname',
363 ];
364
365 if ( $canEditPrivateInfo && $this->authManager->allowsAuthenticationDataChange(
366 new PasswordAuthenticationRequest(), false )->isGood()
367 ) {
368 $link = $this->linkRenderer->makeLink( SpecialPage::getTitleFor( 'ChangePassword' ),
369 $context->msg( 'prefs-resetpass' )->text(), [],
370 [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
371 $defaultPreferences['password'] = [
372 'type' => 'info',
373 'raw' => true,
374 'default' => $link,
375 'label-message' => 'yourpassword',
376 'section' => 'personal/info',
377 ];
378 }
379 // Only show prefershttps if secure login is turned on
380 if ( !$this->config->get( 'ForceHTTPS' )
381 && $this->config->get( 'SecureLogin' )
382 && $canIPUseHTTPS
383 ) {
384 $defaultPreferences['prefershttps'] = [
385 'type' => 'toggle',
386 'label-message' => 'tog-prefershttps',
387 'help-message' => 'prefs-help-prefershttps',
388 'section' => 'personal/info'
389 ];
390 }
391
392 // Language
393 $languages = Language::fetchLanguageNames( null, 'mw' );
394 $languageCode = $this->config->get( 'LanguageCode' );
395 if ( !array_key_exists( $languageCode, $languages ) ) {
396 $languages[$languageCode] = $languageCode;
397 }
398 ksort( $languages );
399
400 $options = [];
401 foreach ( $languages as $code => $name ) {
402 $display = LanguageCode::bcp47( $code ) . ' - ' . $name;
403 $options[$display] = $code;
404 }
405 $defaultPreferences['language'] = [
406 'type' => 'select',
407 'section' => 'personal/i18n',
408 'options' => $options,
409 'label-message' => 'yourlanguage',
410 ];
411
412 $defaultPreferences['gender'] = [
413 'type' => 'radio',
414 'section' => 'personal/i18n',
415 'options' => [
416 $context->msg( 'parentheses' )
417 ->params( $context->msg( 'gender-unknown' )->plain() )
418 ->escaped() => 'unknown',
419 $context->msg( 'gender-female' )->escaped() => 'female',
420 $context->msg( 'gender-male' )->escaped() => 'male',
421 ],
422 'label-message' => 'yourgender',
423 'help-message' => 'prefs-help-gender',
424 ];
425
426 // see if there are multiple language variants to choose from
427 if ( !$this->config->get( 'DisableLangConversion' ) ) {
428 foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
429 if ( $langCode == $this->contLang->getCode() ) {
430 $variants = $this->contLang->getVariants();
431
432 if ( count( $variants ) <= 1 ) {
433 continue;
434 }
435
436 $variantArray = [];
437 foreach ( $variants as $v ) {
438 $v = str_replace( '_', '-', strtolower( $v ) );
439 $variantArray[$v] = $lang->getVariantname( $v, false );
440 }
441
442 $options = [];
443 foreach ( $variantArray as $code => $name ) {
444 $display = LanguageCode::bcp47( $code ) . ' - ' . $name;
445 $options[$display] = $code;
446 }
447
448 $defaultPreferences['variant'] = [
449 'label-message' => 'yourvariant',
450 'type' => 'select',
451 'options' => $options,
452 'section' => 'personal/i18n',
453 'help-message' => 'prefs-help-variant',
454 ];
455 } else {
456 $defaultPreferences["variant-$langCode"] = [
457 'type' => 'api',
458 ];
459 }
460 }
461 }
462
463 // Stuff from Language::getExtraUserToggles()
464 // FIXME is this dead code? $extraUserToggles doesn't seem to be defined for any language
465 $toggles = $this->contLang->getExtraUserToggles();
466
467 foreach ( $toggles as $toggle ) {
468 $defaultPreferences[$toggle] = [
469 'type' => 'toggle',
470 'section' => 'personal/i18n',
471 'label-message' => "tog-$toggle",
472 ];
473 }
474
475 // show a preview of the old signature first
476 $oldsigWikiText = MediaWikiServices::getInstance()->getParser()->preSaveTransform(
477 '~~~',
478 $context->getTitle(),
479 $user,
480 ParserOptions::newFromContext( $context )
481 );
482 $oldsigHTML = $context->getOutput()->parseInline( $oldsigWikiText, true, true );
483 $defaultPreferences['oldsig'] = [
484 'type' => 'info',
485 'raw' => true,
486 'label-message' => 'tog-oldsig',
487 'default' => $oldsigHTML,
488 'section' => 'personal/signature',
489 ];
490 $defaultPreferences['nickname'] = [
491 'type' => $this->authManager->allowsPropertyChange( 'nickname' ) ? 'text' : 'info',
492 'maxlength' => $this->config->get( 'MaxSigChars' ),
493 'label-message' => 'yournick',
494 'validation-callback' => function ( $signature, $alldata, HTMLForm $form ) {
495 return $this->validateSignature( $signature, $alldata, $form );
496 },
497 'section' => 'personal/signature',
498 'filter-callback' => function ( $signature, array $alldata, HTMLForm $form ) {
499 return $this->cleanSignature( $signature, $alldata, $form );
500 },
501 ];
502 $defaultPreferences['fancysig'] = [
503 'type' => 'toggle',
504 'label-message' => 'tog-fancysig',
505 // show general help about signature at the bottom of the section
506 'help-message' => 'prefs-help-signature',
507 'section' => 'personal/signature'
508 ];
509
510 # # Email stuff
511
512 if ( $this->config->get( 'EnableEmail' ) ) {
513 if ( $canViewPrivateInfo ) {
514 $helpMessages[] = $this->config->get( 'EmailConfirmToEdit' )
515 ? 'prefs-help-email-required'
516 : 'prefs-help-email';
517
518 if ( $this->config->get( 'EnableUserEmail' ) ) {
519 // additional messages when users can send email to each other
520 $helpMessages[] = 'prefs-help-email-others';
521 }
522
523 $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
524 if ( $canEditPrivateInfo && $this->authManager->allowsPropertyChange( 'emailaddress' ) ) {
525 $link = $this->linkRenderer->makeLink(
526 SpecialPage::getTitleFor( 'ChangeEmail' ),
527 $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(),
528 [],
529 [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
530
531 $emailAddress .= $emailAddress == '' ? $link : (
532 $context->msg( 'word-separator' )->escaped()
533 . $context->msg( 'parentheses' )->rawParams( $link )->escaped()
534 );
535 }
536
537 $defaultPreferences['emailaddress'] = [
538 'type' => 'info',
539 'raw' => true,
540 'default' => $emailAddress,
541 'label-message' => 'youremail',
542 'section' => 'personal/email',
543 'help-messages' => $helpMessages,
544 # 'cssclass' chosen below
545 ];
546 }
547
548 $disableEmailPrefs = false;
549
550 if ( $this->config->get( 'EmailAuthentication' ) ) {
551 $emailauthenticationclass = 'mw-email-not-authenticated';
552 if ( $user->getEmail() ) {
553 if ( $user->getEmailAuthenticationTimestamp() ) {
554 // date and time are separate parameters to facilitate localisation.
555 // $time is kept for backward compat reasons.
556 // 'emailauthenticated' is also used in SpecialConfirmemail.php
557 $displayUser = $context->getUser();
558 $emailTimestamp = $user->getEmailAuthenticationTimestamp();
559 $time = $lang->userTimeAndDate( $emailTimestamp, $displayUser );
560 $d = $lang->userDate( $emailTimestamp, $displayUser );
561 $t = $lang->userTime( $emailTimestamp, $displayUser );
562 $emailauthenticated = $context->msg( 'emailauthenticated',
563 $time, $d, $t )->parse() . '<br />';
564 $disableEmailPrefs = false;
565 $emailauthenticationclass = 'mw-email-authenticated';
566 } else {
567 $disableEmailPrefs = true;
568 $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
569 $this->linkRenderer->makeKnownLink(
570 SpecialPage::getTitleFor( 'Confirmemail' ),
571 $context->msg( 'emailconfirmlink' )->text()
572 ) . '<br />';
573 $emailauthenticationclass = "mw-email-not-authenticated";
574 }
575 } else {
576 $disableEmailPrefs = true;
577 $emailauthenticated = $context->msg( 'noemailprefs' )->escaped();
578 $emailauthenticationclass = 'mw-email-none';
579 }
580
581 if ( $canViewPrivateInfo ) {
582 $defaultPreferences['emailauthentication'] = [
583 'type' => 'info',
584 'raw' => true,
585 'section' => 'personal/email',
586 'label-message' => 'prefs-emailconfirm-label',
587 'default' => $emailauthenticated,
588 # Apply the same CSS class used on the input to the message:
589 'cssclass' => $emailauthenticationclass,
590 ];
591 }
592 }
593
594 if ( $this->config->get( 'EnableUserEmail' ) && $user->isAllowed( 'sendemail' ) ) {
595 $defaultPreferences['disablemail'] = [
596 'id' => 'wpAllowEmail',
597 'type' => 'toggle',
598 'invert' => true,
599 'section' => 'personal/email',
600 'label-message' => 'allowemail',
601 'disabled' => $disableEmailPrefs,
602 ];
603
604 $defaultPreferences['email-allow-new-users'] = [
605 'id' => 'wpAllowEmailFromNewUsers',
606 'type' => 'toggle',
607 'section' => 'personal/email',
608 'label-message' => 'email-allow-new-users-label',
609 'disabled' => $disableEmailPrefs,
610 ];
611
612 $defaultPreferences['ccmeonemails'] = [
613 'type' => 'toggle',
614 'section' => 'personal/email',
615 'label-message' => 'tog-ccmeonemails',
616 'disabled' => $disableEmailPrefs,
617 ];
618
619 if ( $this->config->get( 'EnableUserEmailBlacklist' ) ) {
620 $lookup = CentralIdLookup::factory();
621 $ids = $user->getOption( 'email-blacklist', [] );
622 $names = $ids ? $lookup->namesFromCentralIds( $ids, $user ) : [];
623
624 $defaultPreferences['email-blacklist'] = [
625 'type' => 'usersmultiselect',
626 'label-message' => 'email-blacklist-label',
627 'section' => 'personal/email',
628 'default' => implode( "\n", $names ),
629 'disabled' => $disableEmailPrefs,
630 ];
631 }
632 }
633
634 if ( $this->config->get( 'EnotifWatchlist' ) ) {
635 $defaultPreferences['enotifwatchlistpages'] = [
636 'type' => 'toggle',
637 'section' => 'personal/email',
638 'label-message' => 'tog-enotifwatchlistpages',
639 'disabled' => $disableEmailPrefs,
640 ];
641 }
642 if ( $this->config->get( 'EnotifUserTalk' ) ) {
643 $defaultPreferences['enotifusertalkpages'] = [
644 'type' => 'toggle',
645 'section' => 'personal/email',
646 'label-message' => 'tog-enotifusertalkpages',
647 'disabled' => $disableEmailPrefs,
648 ];
649 }
650 if ( $this->config->get( 'EnotifUserTalk' ) || $this->config->get( 'EnotifWatchlist' ) ) {
651 if ( $this->config->get( 'EnotifMinorEdits' ) ) {
652 $defaultPreferences['enotifminoredits'] = [
653 'type' => 'toggle',
654 'section' => 'personal/email',
655 'label-message' => 'tog-enotifminoredits',
656 'disabled' => $disableEmailPrefs,
657 ];
658 }
659
660 if ( $this->config->get( 'EnotifRevealEditorAddress' ) ) {
661 $defaultPreferences['enotifrevealaddr'] = [
662 'type' => 'toggle',
663 'section' => 'personal/email',
664 'label-message' => 'tog-enotifrevealaddr',
665 'disabled' => $disableEmailPrefs,
666 ];
667 }
668 }
669 }
670 }
671
678 protected function skinPreferences( User $user, IContextSource $context, &$defaultPreferences ) {
679 # # Skin #####################################
680
681 // Skin selector, if there is at least one valid skin
682 $skinOptions = $this->generateSkinOptions( $user, $context );
683 if ( $skinOptions ) {
684 $defaultPreferences['skin'] = [
685 'type' => 'radio',
686 'options' => $skinOptions,
687 'section' => 'rendering/skin',
688 ];
689 }
690
691 $allowUserCss = $this->config->get( 'AllowUserCss' );
692 $allowUserJs = $this->config->get( 'AllowUserJs' );
693 # Create links to user CSS/JS pages for all skins
694 # This code is basically copied from generateSkinOptions(). It'd
695 # be nice to somehow merge this back in there to avoid redundancy.
696 if ( $allowUserCss || $allowUserJs ) {
697 $linkTools = [];
698 $userName = $user->getName();
699
700 if ( $allowUserCss ) {
701 $cssPage = Title::makeTitleSafe( NS_USER, $userName . '/common.css' );
702 $cssLinkText = $context->msg( 'prefs-custom-css' )->text();
703 $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
704 }
705
706 if ( $allowUserJs ) {
707 $jsPage = Title::makeTitleSafe( NS_USER, $userName . '/common.js' );
708 $jsLinkText = $context->msg( 'prefs-custom-js' )->text();
709 $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
710 }
711
712 $defaultPreferences['commoncssjs'] = [
713 'type' => 'info',
714 'raw' => true,
715 'default' => $context->getLanguage()->pipeList( $linkTools ),
716 'label-message' => 'prefs-common-config',
717 'section' => 'rendering/skin',
718 ];
719 }
720 }
721
726 protected function filesPreferences( IContextSource $context, &$defaultPreferences ) {
727 # # Files #####################################
728 $defaultPreferences['imagesize'] = [
729 'type' => 'select',
730 'options' => $this->getImageSizes( $context ),
731 'label-message' => 'imagemaxsize',
732 'section' => 'rendering/files',
733 ];
734 $defaultPreferences['thumbsize'] = [
735 'type' => 'select',
736 'options' => $this->getThumbSizes( $context ),
737 'label-message' => 'thumbsize',
738 'section' => 'rendering/files',
739 ];
740 }
741
748 protected function datetimePreferences( $user, IContextSource $context, &$defaultPreferences ) {
749 # # Date and time #####################################
750 $dateOptions = $this->getDateOptions( $context );
751 if ( $dateOptions ) {
752 $defaultPreferences['date'] = [
753 'type' => 'radio',
754 'options' => $dateOptions,
755 'section' => 'rendering/dateformat',
756 ];
757 }
758
759 // Info
760 $now = wfTimestampNow();
761 $lang = $context->getLanguage();
762 $nowlocal = Xml::element( 'span', [ 'id' => 'wpLocalTime' ],
763 $lang->userTime( $now, $user ) );
764 $nowserver = $lang->userTime( $now, $user,
765 [ 'format' => false, 'timecorrection' => false ] ) .
766 Html::hidden( 'wpServerTime', (int)substr( $now, 8, 2 ) * 60 + (int)substr( $now, 10, 2 ) );
767
768 $defaultPreferences['nowserver'] = [
769 'type' => 'info',
770 'raw' => 1,
771 'label-message' => 'servertime',
772 'default' => $nowserver,
773 'section' => 'rendering/timeoffset',
774 ];
775
776 $defaultPreferences['nowlocal'] = [
777 'type' => 'info',
778 'raw' => 1,
779 'label-message' => 'localtime',
780 'default' => $nowlocal,
781 'section' => 'rendering/timeoffset',
782 ];
783
784 // Grab existing pref.
785 $tzOffset = $user->getOption( 'timecorrection' );
786 $tz = explode( '|', $tzOffset, 3 );
787
788 $tzOptions = $this->getTimezoneOptions( $context );
789
790 $tzSetting = $tzOffset;
791 if ( count( $tz ) > 1 && $tz[0] == 'ZoneInfo' &&
792 !in_array( $tzOffset, HTMLFormField::flattenOptions( $tzOptions ) )
793 ) {
794 // Timezone offset can vary with DST
795 try {
796 $userTZ = new DateTimeZone( $tz[2] );
797 $minDiff = floor( $userTZ->getOffset( new DateTime( 'now' ) ) / 60 );
798 $tzSetting = "ZoneInfo|$minDiff|{$tz[2]}";
799 } catch ( Exception $e ) {
800 // User has an invalid time zone set. Fall back to just using the offset
801 $tz[0] = 'Offset';
802 }
803 }
804 if ( count( $tz ) > 1 && $tz[0] == 'Offset' ) {
805 $minDiff = $tz[1];
806 $tzSetting = sprintf( '%+03d:%02d', floor( $minDiff / 60 ), abs( $minDiff ) % 60 );
807 }
808
809 $defaultPreferences['timecorrection'] = [
810 'class' => \HTMLSelectOrOtherField::class,
811 'label-message' => 'timezonelegend',
812 'options' => $tzOptions,
813 'default' => $tzSetting,
814 'size' => 20,
815 'section' => 'rendering/timeoffset',
816 ];
817 }
818
823 protected function renderingPreferences( MessageLocalizer $l10n, &$defaultPreferences ) {
824 # # Diffs ####################################
825 $defaultPreferences['diffonly'] = [
826 'type' => 'toggle',
827 'section' => 'rendering/diffs',
828 'label-message' => 'tog-diffonly',
829 ];
830 $defaultPreferences['norollbackdiff'] = [
831 'type' => 'toggle',
832 'section' => 'rendering/diffs',
833 'label-message' => 'tog-norollbackdiff',
834 ];
835
836 # # Page Rendering ##############################
837 if ( $this->config->get( 'AllowUserCssPrefs' ) ) {
838 $defaultPreferences['underline'] = [
839 'type' => 'select',
840 'options' => [
841 $l10n->msg( 'underline-never' )->text() => 0,
842 $l10n->msg( 'underline-always' )->text() => 1,
843 $l10n->msg( 'underline-default' )->text() => 2,
844 ],
845 'label-message' => 'tog-underline',
846 'section' => 'rendering/advancedrendering',
847 ];
848 }
849
850 $stubThresholdValues = [ 50, 100, 500, 1000, 2000, 5000, 10000 ];
851 $stubThresholdOptions = [ $l10n->msg( 'stub-threshold-disabled' )->text() => 0 ];
852 foreach ( $stubThresholdValues as $value ) {
853 $stubThresholdOptions[$l10n->msg( 'size-bytes', $value )->text()] = $value;
854 }
855
856 $defaultPreferences['stubthreshold'] = [
857 'type' => 'select',
858 'section' => 'rendering/advancedrendering',
859 'options' => $stubThresholdOptions,
860 // This is not a raw HTML message; label-raw is needed for the manual <a></a>
861 'label-raw' => $l10n->msg( 'stub-threshold' )->rawParams(
862 '<a href="#" class="stub">' .
863 $l10n->msg( 'stub-threshold-sample-link' )->parse() .
864 '</a>' )->parse(),
865 ];
866
867 $defaultPreferences['showhiddencats'] = [
868 'type' => 'toggle',
869 'section' => 'rendering/advancedrendering',
870 'label-message' => 'tog-showhiddencats'
871 ];
872
873 $defaultPreferences['numberheadings'] = [
874 'type' => 'toggle',
875 'section' => 'rendering/advancedrendering',
876 'label-message' => 'tog-numberheadings',
877 ];
878 }
879
885 protected function editingPreferences( User $user, MessageLocalizer $l10n, &$defaultPreferences ) {
886 # # Editing #####################################
887 $defaultPreferences['editsectiononrightclick'] = [
888 'type' => 'toggle',
889 'section' => 'editing/advancedediting',
890 'label-message' => 'tog-editsectiononrightclick',
891 ];
892 $defaultPreferences['editondblclick'] = [
893 'type' => 'toggle',
894 'section' => 'editing/advancedediting',
895 'label-message' => 'tog-editondblclick',
896 ];
897
898 if ( $this->config->get( 'AllowUserCssPrefs' ) ) {
899 $defaultPreferences['editfont'] = [
900 'type' => 'select',
901 'section' => 'editing/editor',
902 'label-message' => 'editfont-style',
903 'options' => [
904 $l10n->msg( 'editfont-monospace' )->text() => 'monospace',
905 $l10n->msg( 'editfont-sansserif' )->text() => 'sans-serif',
906 $l10n->msg( 'editfont-serif' )->text() => 'serif',
907 ]
908 ];
909 }
910
911 if ( $user->isAllowed( 'minoredit' ) ) {
912 $defaultPreferences['minordefault'] = [
913 'type' => 'toggle',
914 'section' => 'editing/editor',
915 'label-message' => 'tog-minordefault',
916 ];
917 }
918
919 $defaultPreferences['forceeditsummary'] = [
920 'type' => 'toggle',
921 'section' => 'editing/editor',
922 'label-message' => 'tog-forceeditsummary',
923 ];
924 $defaultPreferences['useeditwarning'] = [
925 'type' => 'toggle',
926 'section' => 'editing/editor',
927 'label-message' => 'tog-useeditwarning',
928 ];
929 $defaultPreferences['showtoolbar'] = [
930 'type' => 'toggle',
931 'section' => 'editing/editor',
932 'label-message' => 'tog-showtoolbar',
933 ];
934
935 $defaultPreferences['previewonfirst'] = [
936 'type' => 'toggle',
937 'section' => 'editing/preview',
938 'label-message' => 'tog-previewonfirst',
939 ];
940 $defaultPreferences['previewontop'] = [
941 'type' => 'toggle',
942 'section' => 'editing/preview',
943 'label-message' => 'tog-previewontop',
944 ];
945 $defaultPreferences['uselivepreview'] = [
946 'type' => 'toggle',
947 'section' => 'editing/preview',
948 'label-message' => 'tog-uselivepreview',
949 ];
950 }
951
957 protected function rcPreferences( User $user, MessageLocalizer $l10n, &$defaultPreferences ) {
958 $rcMaxAge = $this->config->get( 'RCMaxAge' );
959 # # RecentChanges #####################################
960 $defaultPreferences['rcdays'] = [
961 'type' => 'float',
962 'label-message' => 'recentchangesdays',
963 'section' => 'rc/displayrc',
964 'min' => 1,
965 'max' => ceil( $rcMaxAge / ( 3600 * 24 ) ),
966 'help' => $l10n->msg( 'recentchangesdays-max' )->numParams(
967 ceil( $rcMaxAge / ( 3600 * 24 ) ) )->escaped()
968 ];
969 $defaultPreferences['rclimit'] = [
970 'type' => 'int',
971 'min' => 0,
972 'max' => 1000,
973 'label-message' => 'recentchangescount',
974 'help-message' => 'prefs-help-recentchangescount',
975 'section' => 'rc/displayrc',
976 ];
977 $defaultPreferences['usenewrc'] = [
978 'type' => 'toggle',
979 'label-message' => 'tog-usenewrc',
980 'section' => 'rc/advancedrc',
981 ];
982 $defaultPreferences['hideminor'] = [
983 'type' => 'toggle',
984 'label-message' => 'tog-hideminor',
985 'section' => 'rc/advancedrc',
986 ];
987 $defaultPreferences['rcfilters-saved-queries'] = [
988 'type' => 'api',
989 ];
990 $defaultPreferences['rcfilters-wl-saved-queries'] = [
991 'type' => 'api',
992 ];
993 // Override RCFilters preferences for RecentChanges 'limit'
994 $defaultPreferences['rcfilters-limit'] = [
995 'type' => 'api',
996 ];
997 $defaultPreferences['rcfilters-saved-queries-versionbackup'] = [
998 'type' => 'api',
999 ];
1000 $defaultPreferences['rcfilters-wl-saved-queries-versionbackup'] = [
1001 'type' => 'api',
1002 ];
1003
1004 if ( $this->config->get( 'RCWatchCategoryMembership' ) ) {
1005 $defaultPreferences['hidecategorization'] = [
1006 'type' => 'toggle',
1007 'label-message' => 'tog-hidecategorization',
1008 'section' => 'rc/advancedrc',
1009 ];
1010 }
1011
1012 if ( $user->useRCPatrol() ) {
1013 $defaultPreferences['hidepatrolled'] = [
1014 'type' => 'toggle',
1015 'section' => 'rc/advancedrc',
1016 'label-message' => 'tog-hidepatrolled',
1017 ];
1018 }
1019
1020 if ( $user->useNPPatrol() ) {
1021 $defaultPreferences['newpageshidepatrolled'] = [
1022 'type' => 'toggle',
1023 'section' => 'rc/advancedrc',
1024 'label-message' => 'tog-newpageshidepatrolled',
1025 ];
1026 }
1027
1028 if ( $this->config->get( 'RCShowWatchingUsers' ) ) {
1029 $defaultPreferences['shownumberswatching'] = [
1030 'type' => 'toggle',
1031 'section' => 'rc/advancedrc',
1032 'label-message' => 'tog-shownumberswatching',
1033 ];
1034 }
1035
1036 if ( $this->config->get( 'StructuredChangeFiltersShowPreference' ) ) {
1037 $defaultPreferences['rcenhancedfilters-disable'] = [
1038 'type' => 'toggle',
1039 'section' => 'rc/opt-out',
1040 'label-message' => 'rcfilters-preference-label',
1041 'help-message' => 'rcfilters-preference-help',
1042 ];
1043 }
1044 }
1045
1051 protected function watchlistPreferences(
1052 User $user, IContextSource $context, &$defaultPreferences
1053 ) {
1054 $watchlistdaysMax = ceil( $this->config->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
1055
1056 # # Watchlist #####################################
1057 if ( $user->isAllowed( 'editmywatchlist' ) ) {
1058 $editWatchlistLinks = [];
1059 $editWatchlistModes = [
1060 'edit' => [ 'EditWatchlist', false ],
1061 'raw' => [ 'EditWatchlist', 'raw' ],
1062 'clear' => [ 'EditWatchlist', 'clear' ],
1063 ];
1064 foreach ( $editWatchlistModes as $editWatchlistMode => $mode ) {
1065 // Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear
1066 $editWatchlistLinks[] = $this->linkRenderer->makeKnownLink(
1067 SpecialPage::getTitleFor( $mode[0], $mode[1] ),
1068 new HtmlArmor( $context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse() )
1069 );
1070 }
1071
1072 $defaultPreferences['editwatchlist'] = [
1073 'type' => 'info',
1074 'raw' => true,
1075 'default' => $context->getLanguage()->pipeList( $editWatchlistLinks ),
1076 'label-message' => 'prefs-editwatchlist-label',
1077 'section' => 'watchlist/editwatchlist',
1078 ];
1079 }
1080
1081 $defaultPreferences['watchlistdays'] = [
1082 'type' => 'float',
1083 'min' => 0,
1084 'max' => $watchlistdaysMax,
1085 'section' => 'watchlist/displaywatchlist',
1086 'help' => $context->msg( 'prefs-watchlist-days-max' )->numParams(
1087 $watchlistdaysMax )->escaped(),
1088 'label-message' => 'prefs-watchlist-days',
1089 ];
1090 $defaultPreferences['wllimit'] = [
1091 'type' => 'int',
1092 'min' => 0,
1093 'max' => 1000,
1094 'label-message' => 'prefs-watchlist-edits',
1095 'help' => $context->msg( 'prefs-watchlist-edits-max' )->escaped(),
1096 'section' => 'watchlist/displaywatchlist',
1097 ];
1098 $defaultPreferences['extendwatchlist'] = [
1099 'type' => 'toggle',
1100 'section' => 'watchlist/advancedwatchlist',
1101 'label-message' => 'tog-extendwatchlist',
1102 ];
1103 $defaultPreferences['watchlisthideminor'] = [
1104 'type' => 'toggle',
1105 'section' => 'watchlist/advancedwatchlist',
1106 'label-message' => 'tog-watchlisthideminor',
1107 ];
1108 $defaultPreferences['watchlisthidebots'] = [
1109 'type' => 'toggle',
1110 'section' => 'watchlist/advancedwatchlist',
1111 'label-message' => 'tog-watchlisthidebots',
1112 ];
1113 $defaultPreferences['watchlisthideown'] = [
1114 'type' => 'toggle',
1115 'section' => 'watchlist/advancedwatchlist',
1116 'label-message' => 'tog-watchlisthideown',
1117 ];
1118 $defaultPreferences['watchlisthideanons'] = [
1119 'type' => 'toggle',
1120 'section' => 'watchlist/advancedwatchlist',
1121 'label-message' => 'tog-watchlisthideanons',
1122 ];
1123 $defaultPreferences['watchlisthideliu'] = [
1124 'type' => 'toggle',
1125 'section' => 'watchlist/advancedwatchlist',
1126 'label-message' => 'tog-watchlisthideliu',
1127 ];
1128
1130 $this->config,
1131 $user
1132 ) ) {
1133 $defaultPreferences['watchlistreloadautomatically'] = [
1134 'type' => 'toggle',
1135 'section' => 'watchlist/advancedwatchlist',
1136 'label-message' => 'tog-watchlistreloadautomatically',
1137 ];
1138 }
1139
1140 $defaultPreferences['watchlistunwatchlinks'] = [
1141 'type' => 'toggle',
1142 'section' => 'watchlist/advancedwatchlist',
1143 'label-message' => 'tog-watchlistunwatchlinks',
1144 ];
1145
1146 if ( $this->config->get( 'RCWatchCategoryMembership' ) ) {
1147 $defaultPreferences['watchlisthidecategorization'] = [
1148 'type' => 'toggle',
1149 'section' => 'watchlist/advancedwatchlist',
1150 'label-message' => 'tog-watchlisthidecategorization',
1151 ];
1152 }
1153
1154 if ( $user->useRCPatrol() ) {
1155 $defaultPreferences['watchlisthidepatrolled'] = [
1156 'type' => 'toggle',
1157 'section' => 'watchlist/advancedwatchlist',
1158 'label-message' => 'tog-watchlisthidepatrolled',
1159 ];
1160 }
1161
1162 $watchTypes = [
1163 'edit' => 'watchdefault',
1164 'move' => 'watchmoves',
1165 'delete' => 'watchdeletion'
1166 ];
1167
1168 // Kinda hacky
1169 if ( $user->isAllowed( 'createpage' ) || $user->isAllowed( 'createtalk' ) ) {
1170 $watchTypes['read'] = 'watchcreations';
1171 }
1172
1173 if ( $user->isAllowed( 'rollback' ) ) {
1174 $watchTypes['rollback'] = 'watchrollback';
1175 }
1176
1177 if ( $user->isAllowed( 'upload' ) ) {
1178 $watchTypes['upload'] = 'watchuploads';
1179 }
1180
1181 foreach ( $watchTypes as $action => $pref ) {
1182 if ( $user->isAllowed( $action ) ) {
1183 // Messages:
1184 // tog-watchdefault, tog-watchmoves, tog-watchdeletion, tog-watchcreations, tog-watchuploads
1185 // tog-watchrollback
1186 $defaultPreferences[$pref] = [
1187 'type' => 'toggle',
1188 'section' => 'watchlist/advancedwatchlist',
1189 'label-message' => "tog-$pref",
1190 ];
1191 }
1192 }
1193
1194 if ( $this->config->get( 'EnableAPI' ) ) {
1195 $defaultPreferences['watchlisttoken'] = [
1196 'type' => 'api',
1197 ];
1198 $defaultPreferences['watchlisttoken-info'] = [
1199 'type' => 'info',
1200 'section' => 'watchlist/tokenwatchlist',
1201 'label-message' => 'prefs-watchlist-token',
1202 'default' => $user->getTokenFromOption( 'watchlisttoken' ),
1203 'help-message' => 'prefs-help-watchlist-token2',
1204 ];
1205 }
1206 }
1207
1211 protected function searchPreferences( &$defaultPreferences ) {
1212 foreach ( MWNamespace::getValidNamespaces() as $n ) {
1213 $defaultPreferences['searchNs' . $n] = [
1214 'type' => 'api',
1215 ];
1216 }
1217 }
1218
1224 protected function generateSkinOptions( User $user, IContextSource $context ) {
1225 $ret = [];
1226
1227 $mptitle = Title::newMainPage();
1228 $previewtext = $context->msg( 'skin-preview' )->escaped();
1229
1230 # Only show skins that aren't disabled in $wgSkipSkins
1231 $validSkinNames = Skin::getAllowedSkins();
1232
1233 foreach ( $validSkinNames as $skinkey => &$skinname ) {
1234 $msg = $context->msg( "skinname-{$skinkey}" );
1235 if ( $msg->exists() ) {
1236 $skinname = htmlspecialchars( $msg->text() );
1237 }
1238 }
1239
1240 $defaultSkin = $this->config->get( 'DefaultSkin' );
1241 $allowUserCss = $this->config->get( 'AllowUserCss' );
1242 $allowUserJs = $this->config->get( 'AllowUserJs' );
1243
1244 # Sort by the internal name, so that the ordering is the same for each display language,
1245 # especially if some skin names are translated to use a different alphabet and some are not.
1246 uksort( $validSkinNames, function ( $a, $b ) use ( $defaultSkin ) {
1247 # Display the default first in the list by comparing it as lesser than any other.
1248 if ( strcasecmp( $a, $defaultSkin ) === 0 ) {
1249 return -1;
1250 }
1251 if ( strcasecmp( $b, $defaultSkin ) === 0 ) {
1252 return 1;
1253 }
1254 return strcasecmp( $a, $b );
1255 } );
1256
1257 $foundDefault = false;
1258 foreach ( $validSkinNames as $skinkey => $sn ) {
1259 $linkTools = [];
1260
1261 # Mark the default skin
1262 if ( strcasecmp( $skinkey, $defaultSkin ) === 0 ) {
1263 $linkTools[] = $context->msg( 'default' )->escaped();
1264 $foundDefault = true;
1265 }
1266
1267 # Create preview link
1268 $mplink = htmlspecialchars( $mptitle->getLocalURL( [ 'useskin' => $skinkey ] ) );
1269 $linkTools[] = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
1270
1271 # Create links to user CSS/JS pages
1272 if ( $allowUserCss ) {
1273 $cssPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.css' );
1274 $cssLinkText = $context->msg( 'prefs-custom-css' )->text();
1275 $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
1276 }
1277
1278 if ( $allowUserJs ) {
1279 $jsPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.js' );
1280 $jsLinkText = $context->msg( 'prefs-custom-js' )->text();
1281 $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
1282 }
1283
1284 $display = $sn . ' ' . $context->msg( 'parentheses' )
1285 ->rawParams( $context->getLanguage()->pipeList( $linkTools ) )
1286 ->escaped();
1287 $ret[$display] = $skinkey;
1288 }
1289
1290 if ( !$foundDefault ) {
1291 // If the default skin is not available, things are going to break horribly because the
1292 // default value for skin selector will not be a valid value. Let's just not show it then.
1293 return [];
1294 }
1295
1296 return $ret;
1297 }
1298
1304 $lang = $context->getLanguage();
1305 $dateopts = $lang->getDatePreferences();
1306
1307 $ret = [];
1308
1309 if ( $dateopts ) {
1310 if ( !in_array( 'default', $dateopts ) ) {
1311 $dateopts[] = 'default'; // Make sure default is always valid T21237
1312 }
1313
1314 // FIXME KLUGE: site default might not be valid for user language
1315 global $wgDefaultUserOptions;
1316 if ( !in_array( $wgDefaultUserOptions['date'], $dateopts ) ) {
1317 $wgDefaultUserOptions['date'] = 'default';
1318 }
1319
1320 $epoch = wfTimestampNow();
1321 foreach ( $dateopts as $key ) {
1322 if ( $key == 'default' ) {
1323 $formatted = $context->msg( 'datedefault' )->escaped();
1324 } else {
1325 $formatted = htmlspecialchars( $lang->timeanddate( $epoch, false, $key ) );
1326 }
1327 $ret[$formatted] = $key;
1328 }
1329 }
1330 return $ret;
1331 }
1332
1337 protected function getImageSizes( MessageLocalizer $l10n ) {
1338 $ret = [];
1339 $pixels = $l10n->msg( 'unit-pixel' )->text();
1340
1341 foreach ( $this->config->get( 'ImageLimits' ) as $index => $limits ) {
1342 // Note: A left-to-right marker (\u200e) is inserted, see T144386
1343 $display = "{$limits[0]}" . json_decode( '"\u200e"' ) . "×{$limits[1]}" . $pixels;
1344 $ret[$display] = $index;
1345 }
1346
1347 return $ret;
1348 }
1349
1354 protected function getThumbSizes( MessageLocalizer $l10n ) {
1355 $ret = [];
1356 $pixels = $l10n->msg( 'unit-pixel' )->text();
1357
1358 foreach ( $this->config->get( 'ThumbLimits' ) as $index => $size ) {
1359 $display = $size . $pixels;
1360 $ret[$display] = $index;
1361 }
1362
1363 return $ret;
1364 }
1365
1372 protected function validateSignature( $signature, $alldata, HTMLForm $form ) {
1373 $maxSigChars = $this->config->get( 'MaxSigChars' );
1374 if ( mb_strlen( $signature ) > $maxSigChars ) {
1375 return Xml::element( 'span', [ 'class' => 'error' ],
1376 $form->msg( 'badsiglength' )->numParams( $maxSigChars )->text() );
1377 } elseif ( isset( $alldata['fancysig'] ) &&
1378 $alldata['fancysig'] &&
1379 MediaWikiServices::getInstance()->getParser()->validateSig( $signature ) === false
1380 ) {
1381 return Xml::element(
1382 'span',
1383 [ 'class' => 'error' ],
1384 $form->msg( 'badsig' )->text()
1385 );
1386 } else {
1387 return true;
1388 }
1389 }
1390
1397 protected function cleanSignature( $signature, $alldata, HTMLForm $form ) {
1398 $parser = MediaWikiServices::getInstance()->getParser();
1399 if ( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) {
1400 $signature = $parser->cleanSig( $signature );
1401 } else {
1402 // When no fancy sig used, make sure ~{3,5} get removed.
1403 $signature = Parser::cleanSigInSig( $signature );
1404 }
1405
1406 return $signature;
1407 }
1408
1416 public function getForm(
1417 User $user,
1419 $formClass = PreferencesForm::class,
1420 array $remove = []
1421 ) {
1422 $formDescriptor = $this->getFormDescriptor( $user, $context );
1423 if ( count( $remove ) ) {
1424 $removeKeys = array_flip( $remove );
1425 $formDescriptor = array_diff_key( $formDescriptor, $removeKeys );
1426 }
1427
1428 // Remove type=api preferences. They are not intended for rendering in the form.
1429 foreach ( $formDescriptor as $name => $info ) {
1430 if ( isset( $info['type'] ) && $info['type'] === 'api' ) {
1431 unset( $formDescriptor[$name] );
1432 }
1433 }
1434
1438 $htmlForm = new $formClass( $formDescriptor, $context, 'prefs' );
1439
1440 $htmlForm->setModifiedUser( $user );
1441 $htmlForm->setId( 'mw-prefs-form' );
1442 $htmlForm->setAutocomplete( 'off' );
1443 $htmlForm->setSubmitText( $context->msg( 'saveprefs' )->text() );
1444 # Used message keys: 'accesskey-preferences-save', 'tooltip-preferences-save'
1445 $htmlForm->setSubmitTooltip( 'preferences-save' );
1446 $htmlForm->setSubmitID( 'prefcontrol' );
1447 $htmlForm->setSubmitCallback( function ( array $formData, PreferencesForm $form ) {
1448 return $this->submitForm( $formData, $form );
1449 } );
1450
1451 return $htmlForm;
1452 }
1453
1459 $opt = [];
1460
1461 $localTZoffset = $this->config->get( 'LocalTZoffset' );
1462 $timeZoneList = $this->getTimeZoneList( $context->getLanguage() );
1463
1464 $timestamp = MWTimestamp::getLocalInstance();
1465 // Check that the LocalTZoffset is the same as the local time zone offset
1466 if ( $localTZoffset == $timestamp->format( 'Z' ) / 60 ) {
1467 $timezoneName = $timestamp->getTimezone()->getName();
1468 // Localize timezone
1469 if ( isset( $timeZoneList[$timezoneName] ) ) {
1470 $timezoneName = $timeZoneList[$timezoneName]['name'];
1471 }
1472 $server_tz_msg = $context->msg(
1473 'timezoneuseserverdefault',
1474 $timezoneName
1475 )->text();
1476 } else {
1477 $tzstring = sprintf(
1478 '%+03d:%02d',
1479 floor( $localTZoffset / 60 ),
1480 abs( $localTZoffset ) % 60
1481 );
1482 $server_tz_msg = $context->msg( 'timezoneuseserverdefault', $tzstring )->text();
1483 }
1484 $opt[$server_tz_msg] = "System|$localTZoffset";
1485 $opt[$context->msg( 'timezoneuseoffset' )->text()] = 'other';
1486 $opt[$context->msg( 'guesstimezone' )->text()] = 'guess';
1487
1488 foreach ( $timeZoneList as $timeZoneInfo ) {
1489 $region = $timeZoneInfo['region'];
1490 if ( !isset( $opt[$region] ) ) {
1491 $opt[$region] = [];
1492 }
1493 $opt[$region][$timeZoneInfo['name']] = $timeZoneInfo['timecorrection'];
1494 }
1495 return $opt;
1496 }
1497
1503 protected function filterTimezoneInput( $tz, array $alldata ) {
1504 $data = explode( '|', $tz, 3 );
1505 switch ( $data[0] ) {
1506 case 'ZoneInfo':
1507 $valid = false;
1508
1509 if ( count( $data ) === 3 ) {
1510 // Make sure this timezone exists
1511 try {
1512 new DateTimeZone( $data[2] );
1513 // If the constructor didn't throw, we know it's valid
1514 $valid = true;
1515 } catch ( Exception $e ) {
1516 // Not a valid timezone
1517 }
1518 }
1519
1520 if ( !$valid ) {
1521 // If the supplied timezone doesn't exist, fall back to the encoded offset
1522 return 'Offset|' . intval( $tz[1] );
1523 }
1524 return $tz;
1525 case 'System':
1526 return $tz;
1527 default:
1528 $data = explode( ':', $tz, 2 );
1529 if ( count( $data ) == 2 ) {
1530 $data[0] = intval( $data[0] );
1531 $data[1] = intval( $data[1] );
1532 $minDiff = abs( $data[0] ) * 60 + $data[1];
1533 if ( $data[0] < 0 ) {
1534 $minDiff = - $minDiff;
1535 }
1536 } else {
1537 $minDiff = intval( $data[0] ) * 60;
1538 }
1539
1540 # Max is +14:00 and min is -12:00, see:
1541 # https://en.wikipedia.org/wiki/Timezone
1542 $minDiff = min( $minDiff, 840 ); # 14:00
1543 $minDiff = max( $minDiff, -720 ); # -12:00
1544 return 'Offset|' . $minDiff;
1545 }
1546 }
1547
1555 protected function saveFormData( $formData, PreferencesForm $form ) {
1556 $user = $form->getModifiedUser();
1557 $hiddenPrefs = $this->config->get( 'HiddenPrefs' );
1558 $result = true;
1559
1560 if ( !$user->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
1561 return Status::newFatal( 'mypreferencesprotected' );
1562 }
1563
1564 // Filter input
1565 foreach ( array_keys( $formData ) as $name ) {
1566 $filters = $this->getSaveFilters();
1567 if ( isset( $filters[$name] ) ) {
1568 $formData[$name] = call_user_func( $filters[$name], $formData[$name], $formData );
1569 }
1570 }
1571
1572 // Fortunately, the realname field is MUCH simpler
1573 // (not really "private", but still shouldn't be edited without permission)
1574
1575 if ( !in_array( 'realname', $hiddenPrefs )
1576 && $user->isAllowed( 'editmyprivateinfo' )
1577 && array_key_exists( 'realname', $formData )
1578 ) {
1579 $realName = $formData['realname'];
1580 $user->setRealName( $realName );
1581 }
1582
1583 if ( $user->isAllowed( 'editmyoptions' ) ) {
1584 $oldUserOptions = $user->getOptions();
1585
1586 foreach ( $this->getSaveBlacklist() as $b ) {
1587 unset( $formData[$b] );
1588 }
1589
1590 # If users have saved a value for a preference which has subsequently been disabled
1591 # via $wgHiddenPrefs, we don't want to destroy that setting in case the preference
1592 # is subsequently re-enabled
1593 foreach ( $hiddenPrefs as $pref ) {
1594 # If the user has not set a non-default value here, the default will be returned
1595 # and subsequently discarded
1596 $formData[$pref] = $user->getOption( $pref, null, true );
1597 }
1598
1599 // If the user changed the rclimit preference, also change the rcfilters-rclimit preference
1600 if (
1601 isset( $formData['rclimit'] ) &&
1602 intval( $formData[ 'rclimit' ] ) !== $user->getIntOption( 'rclimit' )
1603 ) {
1604 $formData['rcfilters-limit'] = $formData['rclimit'];
1605 }
1606
1607 // Keep old preferences from interfering due to back-compat code, etc.
1608 $user->resetOptions( 'unused', $form->getContext() );
1609
1610 foreach ( $formData as $key => $value ) {
1611 $user->setOption( $key, $value );
1612 }
1613
1614 Hooks::run(
1615 'PreferencesFormPreSave',
1616 [ $formData, $form, $user, &$result, $oldUserOptions ]
1617 );
1618 }
1619
1620 AuthManager::callLegacyAuthPlugin( 'updateExternalDB', [ $user ] );
1621 $user->saveSettings();
1622
1623 return $result;
1624 }
1625
1635 public function legacySaveFormData( $formData, PreferencesForm $form ) {
1636 return $this->saveFormData( $formData, $form );
1637 }
1638
1646 protected function submitForm( array $formData, PreferencesForm $form ) {
1647 $res = $this->saveFormData( $formData, $form );
1648
1649 if ( $res ) {
1650 $urlOptions = [];
1651
1652 if ( $res === 'eauth' ) {
1653 $urlOptions['eauth'] = 1;
1654 }
1655
1656 $urlOptions += $form->getExtraSuccessRedirectParameters();
1657
1658 $url = $form->getTitle()->getFullURL( $urlOptions );
1659
1660 $context = $form->getContext();
1661 // Set session data for the success message
1662 $context->getRequest()->getSession()->set( 'specialPreferencesSaveSuccess', 1 );
1663
1664 $context->getOutput()->redirect( $url );
1665 }
1666
1667 return Status::newGood();
1668 }
1669
1679 public function legacySubmitForm( array $formData, PreferencesForm $form ) {
1680 return $this->submitForm( $formData, $form );
1681 }
1682
1691 protected function getTimeZoneList( Language $language ) {
1692 $identifiers = DateTimeZone::listIdentifiers();
1693 if ( $identifiers === false ) {
1694 return [];
1695 }
1696 sort( $identifiers );
1697
1698 $tzRegions = [
1699 'Africa' => wfMessage( 'timezoneregion-africa' )->inLanguage( $language )->text(),
1700 'America' => wfMessage( 'timezoneregion-america' )->inLanguage( $language )->text(),
1701 'Antarctica' => wfMessage( 'timezoneregion-antarctica' )->inLanguage( $language )->text(),
1702 'Arctic' => wfMessage( 'timezoneregion-arctic' )->inLanguage( $language )->text(),
1703 'Asia' => wfMessage( 'timezoneregion-asia' )->inLanguage( $language )->text(),
1704 'Atlantic' => wfMessage( 'timezoneregion-atlantic' )->inLanguage( $language )->text(),
1705 'Australia' => wfMessage( 'timezoneregion-australia' )->inLanguage( $language )->text(),
1706 'Europe' => wfMessage( 'timezoneregion-europe' )->inLanguage( $language )->text(),
1707 'Indian' => wfMessage( 'timezoneregion-indian' )->inLanguage( $language )->text(),
1708 'Pacific' => wfMessage( 'timezoneregion-pacific' )->inLanguage( $language )->text(),
1709 ];
1710 asort( $tzRegions );
1711
1712 $timeZoneList = [];
1713
1714 $now = new DateTime();
1715
1716 foreach ( $identifiers as $identifier ) {
1717 $parts = explode( '/', $identifier, 2 );
1718
1719 // DateTimeZone::listIdentifiers() returns a number of
1720 // backwards-compatibility entries. This filters them out of the
1721 // list presented to the user.
1722 if ( count( $parts ) !== 2 || !array_key_exists( $parts[0], $tzRegions ) ) {
1723 continue;
1724 }
1725
1726 // Localize region
1727 $parts[0] = $tzRegions[$parts[0]];
1728
1729 $dateTimeZone = new DateTimeZone( $identifier );
1730 $minDiff = floor( $dateTimeZone->getOffset( $now ) / 60 );
1731
1732 $display = str_replace( '_', ' ', $parts[0] . '/' . $parts[1] );
1733 $value = "ZoneInfo|$minDiff|$identifier";
1734
1735 $timeZoneList[$identifier] = [
1736 'name' => $display,
1737 'timecorrection' => $value,
1738 'region' => $parts[0],
1739 ];
1740 }
1741
1742 return $timeZoneList;
1743 }
1744}
$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.
The CentralIdLookup service allows for connecting local users with cluster-wide IDs.
static factory( $providerId=null)
Fetch a CentralIdLookup.
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.
Definition HTMLForm.php:130
Hooks class.
Definition Hooks.php:34
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:28
This class is a collection of static functions that serve two purposes:
Definition Html.php:48
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:35
MediaWiki exception.
This is a utility class with only static functions for dealing with namespaces that encodes all the "...
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.
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.
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)
skinPreferences(User $user, IContextSource $context, &$defaultPreferences)
renderingPreferences(MessageLocalizer $l10n, &$defaultPreferences)
getOptionFromUser( $name, $info, User $user)
Pull option from a user account.
loadPreferenceValues(User $user, IContextSource $context, &$defaultPreferences)
Loads existing values for a given array of preferences.
saveFormData( $formData, PreferencesForm $form)
Handle the form submission if everything validated properly.
editingPreferences(User $user, MessageLocalizer $l10n, &$defaultPreferences)
legacySaveFormData( $formData, PreferencesForm $form)
DO NOT USE.
datetimePreferences( $user, IContextSource $context, &$defaultPreferences)
submitForm(array $formData, PreferencesForm $form)
Save the form data and reload the page.
getForm(User $user, IContextSource $context, $formClass=PreferencesForm::class, array $remove=[])
cleanSignature( $signature, $alldata, HTMLForm $form)
Language $contLang
The wiki's content language, equivalent to $wgContLang.
filesPreferences(IContextSource $context, &$defaultPreferences)
legacySubmitForm(array $formData, PreferencesForm $form)
DO NOT USE.
__construct(Config $config, Language $contLang, AuthManager $authManager, LinkRenderer $linkRenderer)
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:70
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
Definition Parser.php:4658
Form to edit user preferences.
The main skin class which provides methods and properties for all other skins.
Definition Skin.php:36
static getAllowedSkins()
Fetch the list of user-selectable skins in regards to $wgSkipSkins.
Definition Skin.php:78
Parent class for all special pages.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
static checkStructuredFilterUiEnabled(Config $config, User $user)
Static method to check whether StructuredFilter UI is enabled for the given 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:39
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:53
static getDefaultOptions()
Combine the language default options with any site-specific options and add the default language vari...
Definition User.php:1722
Module of static functions for generating XML.
Definition Xml.php:26
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition Xml.php:39
$res
Definition database.txt:21
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add text
Definition design.txt:18
namespace being checked & $result
Definition hooks.txt:2323
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction $rows
Definition hooks.txt:2783
do that in ParserLimitReportFormat instead $parser
Definition hooks.txt:2603
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition hooks.txt:1795
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:2001
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable & $code
Definition hooks.txt:865
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on and they can depend only on the ResourceLoaderContext $context
Definition hooks.txt:2811
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt;div ...>$1&lt;/div>"). - flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException':Called before an exception(or PHP error) is logged. This is meant for integration with external error aggregation services
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition hooks.txt:2005
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition hooks.txt:3021
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
returning false will NOT prevent logging $e
Definition hooks.txt:2176
Interface for configuration instances.
Definition Config.php:28
Interface for objects which can provide a MediaWiki context on request.
A PreferencesFactory is a MediaWiki service that provides the definitions of preferences for a given ...
Interface for localizing messages in MediaWiki.
if(!isset( $args[0])) $lang
switch( $options['output']) $languages
Definition transstat.php:76