Merge "maintenance: Document secondary purpose of --server"
[lhc/web/wiklou.git] / includes / preferences / DefaultPreferencesFactory.php
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21 namespace MediaWiki\Preferences;
22
23 use CentralIdLookup;
24 use Config;
25 use DateTime;
26 use DateTimeZone;
27 use Exception;
28 use Hooks;
29 use Html;
30 use HtmlArmor;
31 use HTMLForm;
32 use HTMLFormField;
33 use IContextSource;
34 use Language;
35 use LanguageCode;
36 use LanguageConverter;
37 use MediaWiki\Auth\AuthManager;
38 use MediaWiki\Auth\PasswordAuthenticationRequest;
39 use MediaWiki\Linker\LinkRenderer;
40 use MediaWiki\MediaWikiServices;
41 use MessageLocalizer;
42 use MWException;
43 use MWNamespace;
44 use MWTimestamp;
45 use Parser;
46 use ParserOptions;
47 use PreferencesForm;
48 use Psr\Log\LoggerAwareTrait;
49 use Psr\Log\NullLogger;
50 use Skin;
51 use SpecialPage;
52 use Status;
53 use Title;
54 use User;
55 use UserGroupMembership;
56 use Xml;
57
58 /**
59 * This is the default implementation of PreferencesFactory.
60 */
61 class DefaultPreferencesFactory implements PreferencesFactory {
62 use LoggerAwareTrait;
63
64 /** @var Config */
65 protected $config;
66
67 /** @var Language The wiki's content language, equivalent to $wgContLang. */
68 protected $contLang;
69
70 /** @var AuthManager */
71 protected $authManager;
72
73 /** @var LinkRenderer */
74 protected $linkRenderer;
75
76 /**
77 * @param Config $config
78 * @param Language $contLang
79 * @param AuthManager $authManager
80 * @param LinkRenderer $linkRenderer
81 */
82 public function __construct(
83 Config $config,
84 Language $contLang,
85 AuthManager $authManager,
86 LinkRenderer $linkRenderer
87 ) {
88 $this->config = $config;
89 $this->contLang = $contLang;
90 $this->authManager = $authManager;
91 $this->linkRenderer = $linkRenderer;
92 $this->logger = new NullLogger();
93 }
94
95 /**
96 * @return callable[]
97 */
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
111 /**
112 * @inheritDoc
113 */
114 public function getSaveBlacklist() {
115 return [
116 'realname',
117 'emailaddress',
118 ];
119 }
120
121 /**
122 * @throws MWException
123 * @param User $user
124 * @param IContextSource $context
125 * @return array|null
126 */
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
148 /**
149 * Loads existing values for a given array of preferences
150 * @throws MWException
151 * @param User $user
152 * @param IContextSource $context
153 * @param array &$defaultPreferences Array to load values for
154 * @return array|null
155 */
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
200 /**
201 * Pull option from a user account. Handles stuff like array-type preferences.
202 *
203 * @param string $name
204 * @param array $info
205 * @param User $user
206 * @return array|string
207 */
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
245 /**
246 * @todo Inject user Language instead of using context.
247 * @param User $user
248 * @param IContextSource $context
249 * @param array &$defaultPreferences
250 * @param bool $canIPUseHTTPS Whether the user's IP is likely to be able to access the wiki
251 * via HTTPS.
252 * @return void
253 */
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( 'SecureLogin' ) && $canIPUseHTTPS ) {
381 $defaultPreferences['prefershttps'] = [
382 'type' => 'toggle',
383 'label-message' => 'tog-prefershttps',
384 'help-message' => 'prefs-help-prefershttps',
385 'section' => 'personal/info'
386 ];
387 }
388
389 // Language
390 $languages = Language::fetchLanguageNames( null, 'mw' );
391 $languageCode = $this->config->get( 'LanguageCode' );
392 if ( !array_key_exists( $languageCode, $languages ) ) {
393 $languages[$languageCode] = $languageCode;
394 }
395 ksort( $languages );
396
397 $options = [];
398 foreach ( $languages as $code => $name ) {
399 $display = LanguageCode::bcp47( $code ) . ' - ' . $name;
400 $options[$display] = $code;
401 }
402 $defaultPreferences['language'] = [
403 'type' => 'select',
404 'section' => 'personal/i18n',
405 'options' => $options,
406 'label-message' => 'yourlanguage',
407 ];
408
409 $defaultPreferences['gender'] = [
410 'type' => 'radio',
411 'section' => 'personal/i18n',
412 'options' => [
413 $context->msg( 'parentheses' )
414 ->params( $context->msg( 'gender-unknown' )->plain() )
415 ->escaped() => 'unknown',
416 $context->msg( 'gender-female' )->escaped() => 'female',
417 $context->msg( 'gender-male' )->escaped() => 'male',
418 ],
419 'label-message' => 'yourgender',
420 'help-message' => 'prefs-help-gender',
421 ];
422
423 // see if there are multiple language variants to choose from
424 if ( !$this->config->get( 'DisableLangConversion' ) ) {
425 foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
426 if ( $langCode == $this->contLang->getCode() ) {
427 $variants = $this->contLang->getVariants();
428
429 if ( count( $variants ) <= 1 ) {
430 continue;
431 }
432
433 $variantArray = [];
434 foreach ( $variants as $v ) {
435 $v = str_replace( '_', '-', strtolower( $v ) );
436 $variantArray[$v] = $lang->getVariantname( $v, false );
437 }
438
439 $options = [];
440 foreach ( $variantArray as $code => $name ) {
441 $display = LanguageCode::bcp47( $code ) . ' - ' . $name;
442 $options[$display] = $code;
443 }
444
445 $defaultPreferences['variant'] = [
446 'label-message' => 'yourvariant',
447 'type' => 'select',
448 'options' => $options,
449 'section' => 'personal/i18n',
450 'help-message' => 'prefs-help-variant',
451 ];
452 } else {
453 $defaultPreferences["variant-$langCode"] = [
454 'type' => 'api',
455 ];
456 }
457 }
458 }
459
460 // Stuff from Language::getExtraUserToggles()
461 // FIXME is this dead code? $extraUserToggles doesn't seem to be defined for any language
462 $toggles = $this->contLang->getExtraUserToggles();
463
464 foreach ( $toggles as $toggle ) {
465 $defaultPreferences[$toggle] = [
466 'type' => 'toggle',
467 'section' => 'personal/i18n',
468 'label-message' => "tog-$toggle",
469 ];
470 }
471
472 // show a preview of the old signature first
473 $oldsigWikiText = MediaWikiServices::getInstance()->getParser()->preSaveTransform(
474 '~~~',
475 $context->getTitle(),
476 $user,
477 ParserOptions::newFromContext( $context )
478 );
479 $oldsigHTML = $context->getOutput()->parseInline( $oldsigWikiText, true, true );
480 $defaultPreferences['oldsig'] = [
481 'type' => 'info',
482 'raw' => true,
483 'label-message' => 'tog-oldsig',
484 'default' => $oldsigHTML,
485 'section' => 'personal/signature',
486 ];
487 $defaultPreferences['nickname'] = [
488 'type' => $this->authManager->allowsPropertyChange( 'nickname' ) ? 'text' : 'info',
489 'maxlength' => $this->config->get( 'MaxSigChars' ),
490 'label-message' => 'yournick',
491 'validation-callback' => function ( $signature, $alldata, HTMLForm $form ) {
492 return $this->validateSignature( $signature, $alldata, $form );
493 },
494 'section' => 'personal/signature',
495 'filter-callback' => function ( $signature, array $alldata, HTMLForm $form ) {
496 return $this->cleanSignature( $signature, $alldata, $form );
497 },
498 ];
499 $defaultPreferences['fancysig'] = [
500 'type' => 'toggle',
501 'label-message' => 'tog-fancysig',
502 // show general help about signature at the bottom of the section
503 'help-message' => 'prefs-help-signature',
504 'section' => 'personal/signature'
505 ];
506
507 # # Email stuff
508
509 if ( $this->config->get( 'EnableEmail' ) ) {
510 if ( $canViewPrivateInfo ) {
511 $helpMessages[] = $this->config->get( 'EmailConfirmToEdit' )
512 ? 'prefs-help-email-required'
513 : 'prefs-help-email';
514
515 if ( $this->config->get( 'EnableUserEmail' ) ) {
516 // additional messages when users can send email to each other
517 $helpMessages[] = 'prefs-help-email-others';
518 }
519
520 $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
521 if ( $canEditPrivateInfo && $this->authManager->allowsPropertyChange( 'emailaddress' ) ) {
522 $link = $this->linkRenderer->makeLink(
523 SpecialPage::getTitleFor( 'ChangeEmail' ),
524 $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(),
525 [],
526 [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
527
528 $emailAddress .= $emailAddress == '' ? $link : (
529 $context->msg( 'word-separator' )->escaped()
530 . $context->msg( 'parentheses' )->rawParams( $link )->escaped()
531 );
532 }
533
534 $defaultPreferences['emailaddress'] = [
535 'type' => 'info',
536 'raw' => true,
537 'default' => $emailAddress,
538 'label-message' => 'youremail',
539 'section' => 'personal/email',
540 'help-messages' => $helpMessages,
541 # 'cssclass' chosen below
542 ];
543 }
544
545 $disableEmailPrefs = false;
546
547 if ( $this->config->get( 'EmailAuthentication' ) ) {
548 $emailauthenticationclass = 'mw-email-not-authenticated';
549 if ( $user->getEmail() ) {
550 if ( $user->getEmailAuthenticationTimestamp() ) {
551 // date and time are separate parameters to facilitate localisation.
552 // $time is kept for backward compat reasons.
553 // 'emailauthenticated' is also used in SpecialConfirmemail.php
554 $displayUser = $context->getUser();
555 $emailTimestamp = $user->getEmailAuthenticationTimestamp();
556 $time = $lang->userTimeAndDate( $emailTimestamp, $displayUser );
557 $d = $lang->userDate( $emailTimestamp, $displayUser );
558 $t = $lang->userTime( $emailTimestamp, $displayUser );
559 $emailauthenticated = $context->msg( 'emailauthenticated',
560 $time, $d, $t )->parse() . '<br />';
561 $disableEmailPrefs = false;
562 $emailauthenticationclass = 'mw-email-authenticated';
563 } else {
564 $disableEmailPrefs = true;
565 $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
566 $this->linkRenderer->makeKnownLink(
567 SpecialPage::getTitleFor( 'Confirmemail' ),
568 $context->msg( 'emailconfirmlink' )->text()
569 ) . '<br />';
570 $emailauthenticationclass = "mw-email-not-authenticated";
571 }
572 } else {
573 $disableEmailPrefs = true;
574 $emailauthenticated = $context->msg( 'noemailprefs' )->escaped();
575 $emailauthenticationclass = 'mw-email-none';
576 }
577
578 if ( $canViewPrivateInfo ) {
579 $defaultPreferences['emailauthentication'] = [
580 'type' => 'info',
581 'raw' => true,
582 'section' => 'personal/email',
583 'label-message' => 'prefs-emailconfirm-label',
584 'default' => $emailauthenticated,
585 # Apply the same CSS class used on the input to the message:
586 'cssclass' => $emailauthenticationclass,
587 ];
588 }
589 }
590
591 if ( $this->config->get( 'EnableUserEmail' ) && $user->isAllowed( 'sendemail' ) ) {
592 $defaultPreferences['disablemail'] = [
593 'id' => 'wpAllowEmail',
594 'type' => 'toggle',
595 'invert' => true,
596 'section' => 'personal/email',
597 'label-message' => 'allowemail',
598 'disabled' => $disableEmailPrefs,
599 ];
600
601 $defaultPreferences['email-allow-new-users'] = [
602 'id' => 'wpAllowEmailFromNewUsers',
603 'type' => 'toggle',
604 'section' => 'personal/email',
605 'label-message' => 'email-allow-new-users-label',
606 'disabled' => $disableEmailPrefs,
607 ];
608
609 $defaultPreferences['ccmeonemails'] = [
610 'type' => 'toggle',
611 'section' => 'personal/email',
612 'label-message' => 'tog-ccmeonemails',
613 'disabled' => $disableEmailPrefs,
614 ];
615
616 if ( $this->config->get( 'EnableUserEmailBlacklist' ) ) {
617 $lookup = CentralIdLookup::factory();
618 $ids = $user->getOption( 'email-blacklist', [] );
619 $names = $ids ? $lookup->namesFromCentralIds( $ids, $user ) : [];
620
621 $defaultPreferences['email-blacklist'] = [
622 'type' => 'usersmultiselect',
623 'label-message' => 'email-blacklist-label',
624 'section' => 'personal/email',
625 'default' => implode( "\n", $names ),
626 'disabled' => $disableEmailPrefs,
627 ];
628 }
629 }
630
631 if ( $this->config->get( 'EnotifWatchlist' ) ) {
632 $defaultPreferences['enotifwatchlistpages'] = [
633 'type' => 'toggle',
634 'section' => 'personal/email',
635 'label-message' => 'tog-enotifwatchlistpages',
636 'disabled' => $disableEmailPrefs,
637 ];
638 }
639 if ( $this->config->get( 'EnotifUserTalk' ) ) {
640 $defaultPreferences['enotifusertalkpages'] = [
641 'type' => 'toggle',
642 'section' => 'personal/email',
643 'label-message' => 'tog-enotifusertalkpages',
644 'disabled' => $disableEmailPrefs,
645 ];
646 }
647 if ( $this->config->get( 'EnotifUserTalk' ) || $this->config->get( 'EnotifWatchlist' ) ) {
648 if ( $this->config->get( 'EnotifMinorEdits' ) ) {
649 $defaultPreferences['enotifminoredits'] = [
650 'type' => 'toggle',
651 'section' => 'personal/email',
652 'label-message' => 'tog-enotifminoredits',
653 'disabled' => $disableEmailPrefs,
654 ];
655 }
656
657 if ( $this->config->get( 'EnotifRevealEditorAddress' ) ) {
658 $defaultPreferences['enotifrevealaddr'] = [
659 'type' => 'toggle',
660 'section' => 'personal/email',
661 'label-message' => 'tog-enotifrevealaddr',
662 'disabled' => $disableEmailPrefs,
663 ];
664 }
665 }
666 }
667 }
668
669 /**
670 * @param User $user
671 * @param IContextSource $context
672 * @param array &$defaultPreferences
673 * @return void
674 */
675 protected function skinPreferences( User $user, IContextSource $context, &$defaultPreferences ) {
676 # # Skin #####################################
677
678 // Skin selector, if there is at least one valid skin
679 $skinOptions = $this->generateSkinOptions( $user, $context );
680 if ( $skinOptions ) {
681 $defaultPreferences['skin'] = [
682 'type' => 'radio',
683 'options' => $skinOptions,
684 'section' => 'rendering/skin',
685 ];
686 }
687
688 $allowUserCss = $this->config->get( 'AllowUserCss' );
689 $allowUserJs = $this->config->get( 'AllowUserJs' );
690 # Create links to user CSS/JS pages for all skins
691 # This code is basically copied from generateSkinOptions(). It'd
692 # be nice to somehow merge this back in there to avoid redundancy.
693 if ( $allowUserCss || $allowUserJs ) {
694 $linkTools = [];
695 $userName = $user->getName();
696
697 if ( $allowUserCss ) {
698 $cssPage = Title::makeTitleSafe( NS_USER, $userName . '/common.css' );
699 $cssLinkText = $context->msg( 'prefs-custom-css' )->text();
700 $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
701 }
702
703 if ( $allowUserJs ) {
704 $jsPage = Title::makeTitleSafe( NS_USER, $userName . '/common.js' );
705 $jsLinkText = $context->msg( 'prefs-custom-js' )->text();
706 $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
707 }
708
709 $defaultPreferences['commoncssjs'] = [
710 'type' => 'info',
711 'raw' => true,
712 'default' => $context->getLanguage()->pipeList( $linkTools ),
713 'label-message' => 'prefs-common-config',
714 'section' => 'rendering/skin',
715 ];
716 }
717 }
718
719 /**
720 * @param IContextSource $context
721 * @param array &$defaultPreferences
722 */
723 protected function filesPreferences( IContextSource $context, &$defaultPreferences ) {
724 # # Files #####################################
725 $defaultPreferences['imagesize'] = [
726 'type' => 'select',
727 'options' => $this->getImageSizes( $context ),
728 'label-message' => 'imagemaxsize',
729 'section' => 'rendering/files',
730 ];
731 $defaultPreferences['thumbsize'] = [
732 'type' => 'select',
733 'options' => $this->getThumbSizes( $context ),
734 'label-message' => 'thumbsize',
735 'section' => 'rendering/files',
736 ];
737 }
738
739 /**
740 * @param User $user
741 * @param IContextSource $context
742 * @param array &$defaultPreferences
743 * @return void
744 */
745 protected function datetimePreferences( $user, IContextSource $context, &$defaultPreferences ) {
746 # # Date and time #####################################
747 $dateOptions = $this->getDateOptions( $context );
748 if ( $dateOptions ) {
749 $defaultPreferences['date'] = [
750 'type' => 'radio',
751 'options' => $dateOptions,
752 'section' => 'rendering/dateformat',
753 ];
754 }
755
756 // Info
757 $now = wfTimestampNow();
758 $lang = $context->getLanguage();
759 $nowlocal = Xml::element( 'span', [ 'id' => 'wpLocalTime' ],
760 $lang->userTime( $now, $user ) );
761 $nowserver = $lang->userTime( $now, $user,
762 [ 'format' => false, 'timecorrection' => false ] ) .
763 Html::hidden( 'wpServerTime', (int)substr( $now, 8, 2 ) * 60 + (int)substr( $now, 10, 2 ) );
764
765 $defaultPreferences['nowserver'] = [
766 'type' => 'info',
767 'raw' => 1,
768 'label-message' => 'servertime',
769 'default' => $nowserver,
770 'section' => 'rendering/timeoffset',
771 ];
772
773 $defaultPreferences['nowlocal'] = [
774 'type' => 'info',
775 'raw' => 1,
776 'label-message' => 'localtime',
777 'default' => $nowlocal,
778 'section' => 'rendering/timeoffset',
779 ];
780
781 // Grab existing pref.
782 $tzOffset = $user->getOption( 'timecorrection' );
783 $tz = explode( '|', $tzOffset, 3 );
784
785 $tzOptions = $this->getTimezoneOptions( $context );
786
787 $tzSetting = $tzOffset;
788 if ( count( $tz ) > 1 && $tz[0] == 'ZoneInfo' &&
789 !in_array( $tzOffset, HTMLFormField::flattenOptions( $tzOptions ) )
790 ) {
791 // Timezone offset can vary with DST
792 try {
793 $userTZ = new DateTimeZone( $tz[2] );
794 $minDiff = floor( $userTZ->getOffset( new DateTime( 'now' ) ) / 60 );
795 $tzSetting = "ZoneInfo|$minDiff|{$tz[2]}";
796 } catch ( Exception $e ) {
797 // User has an invalid time zone set. Fall back to just using the offset
798 $tz[0] = 'Offset';
799 }
800 }
801 if ( count( $tz ) > 1 && $tz[0] == 'Offset' ) {
802 $minDiff = $tz[1];
803 $tzSetting = sprintf( '%+03d:%02d', floor( $minDiff / 60 ), abs( $minDiff ) % 60 );
804 }
805
806 $defaultPreferences['timecorrection'] = [
807 'class' => \HTMLSelectOrOtherField::class,
808 'label-message' => 'timezonelegend',
809 'options' => $tzOptions,
810 'default' => $tzSetting,
811 'size' => 20,
812 'section' => 'rendering/timeoffset',
813 ];
814 }
815
816 /**
817 * @param MessageLocalizer $l10n
818 * @param array &$defaultPreferences
819 */
820 protected function renderingPreferences( MessageLocalizer $l10n, &$defaultPreferences ) {
821 # # Diffs ####################################
822 $defaultPreferences['diffonly'] = [
823 'type' => 'toggle',
824 'section' => 'rendering/diffs',
825 'label-message' => 'tog-diffonly',
826 ];
827 $defaultPreferences['norollbackdiff'] = [
828 'type' => 'toggle',
829 'section' => 'rendering/diffs',
830 'label-message' => 'tog-norollbackdiff',
831 ];
832
833 # # Page Rendering ##############################
834 if ( $this->config->get( 'AllowUserCssPrefs' ) ) {
835 $defaultPreferences['underline'] = [
836 'type' => 'select',
837 'options' => [
838 $l10n->msg( 'underline-never' )->text() => 0,
839 $l10n->msg( 'underline-always' )->text() => 1,
840 $l10n->msg( 'underline-default' )->text() => 2,
841 ],
842 'label-message' => 'tog-underline',
843 'section' => 'rendering/advancedrendering',
844 ];
845 }
846
847 $stubThresholdValues = [ 50, 100, 500, 1000, 2000, 5000, 10000 ];
848 $stubThresholdOptions = [ $l10n->msg( 'stub-threshold-disabled' )->text() => 0 ];
849 foreach ( $stubThresholdValues as $value ) {
850 $stubThresholdOptions[$l10n->msg( 'size-bytes', $value )->text()] = $value;
851 }
852
853 $defaultPreferences['stubthreshold'] = [
854 'type' => 'select',
855 'section' => 'rendering/advancedrendering',
856 'options' => $stubThresholdOptions,
857 // This is not a raw HTML message; label-raw is needed for the manual <a></a>
858 'label-raw' => $l10n->msg( 'stub-threshold' )->rawParams(
859 '<a href="#" class="stub">' .
860 $l10n->msg( 'stub-threshold-sample-link' )->parse() .
861 '</a>' )->parse(),
862 ];
863
864 $defaultPreferences['showhiddencats'] = [
865 'type' => 'toggle',
866 'section' => 'rendering/advancedrendering',
867 'label-message' => 'tog-showhiddencats'
868 ];
869
870 $defaultPreferences['numberheadings'] = [
871 'type' => 'toggle',
872 'section' => 'rendering/advancedrendering',
873 'label-message' => 'tog-numberheadings',
874 ];
875 }
876
877 /**
878 * @param User $user
879 * @param MessageLocalizer $l10n
880 * @param array &$defaultPreferences
881 */
882 protected function editingPreferences( User $user, MessageLocalizer $l10n, &$defaultPreferences ) {
883 # # Editing #####################################
884 $defaultPreferences['editsectiononrightclick'] = [
885 'type' => 'toggle',
886 'section' => 'editing/advancedediting',
887 'label-message' => 'tog-editsectiononrightclick',
888 ];
889 $defaultPreferences['editondblclick'] = [
890 'type' => 'toggle',
891 'section' => 'editing/advancedediting',
892 'label-message' => 'tog-editondblclick',
893 ];
894
895 if ( $this->config->get( 'AllowUserCssPrefs' ) ) {
896 $defaultPreferences['editfont'] = [
897 'type' => 'select',
898 'section' => 'editing/editor',
899 'label-message' => 'editfont-style',
900 'options' => [
901 $l10n->msg( 'editfont-monospace' )->text() => 'monospace',
902 $l10n->msg( 'editfont-sansserif' )->text() => 'sans-serif',
903 $l10n->msg( 'editfont-serif' )->text() => 'serif',
904 ]
905 ];
906 }
907
908 if ( $user->isAllowed( 'minoredit' ) ) {
909 $defaultPreferences['minordefault'] = [
910 'type' => 'toggle',
911 'section' => 'editing/editor',
912 'label-message' => 'tog-minordefault',
913 ];
914 }
915
916 $defaultPreferences['forceeditsummary'] = [
917 'type' => 'toggle',
918 'section' => 'editing/editor',
919 'label-message' => 'tog-forceeditsummary',
920 ];
921 $defaultPreferences['useeditwarning'] = [
922 'type' => 'toggle',
923 'section' => 'editing/editor',
924 'label-message' => 'tog-useeditwarning',
925 ];
926 $defaultPreferences['showtoolbar'] = [
927 'type' => 'toggle',
928 'section' => 'editing/editor',
929 'label-message' => 'tog-showtoolbar',
930 ];
931
932 $defaultPreferences['previewonfirst'] = [
933 'type' => 'toggle',
934 'section' => 'editing/preview',
935 'label-message' => 'tog-previewonfirst',
936 ];
937 $defaultPreferences['previewontop'] = [
938 'type' => 'toggle',
939 'section' => 'editing/preview',
940 'label-message' => 'tog-previewontop',
941 ];
942 $defaultPreferences['uselivepreview'] = [
943 'type' => 'toggle',
944 'section' => 'editing/preview',
945 'label-message' => 'tog-uselivepreview',
946 ];
947 }
948
949 /**
950 * @param User $user
951 * @param MessageLocalizer $l10n
952 * @param array &$defaultPreferences
953 */
954 protected function rcPreferences( User $user, MessageLocalizer $l10n, &$defaultPreferences ) {
955 $rcMaxAge = $this->config->get( 'RCMaxAge' );
956 # # RecentChanges #####################################
957 $defaultPreferences['rcdays'] = [
958 'type' => 'float',
959 'label-message' => 'recentchangesdays',
960 'section' => 'rc/displayrc',
961 'min' => 1,
962 'max' => ceil( $rcMaxAge / ( 3600 * 24 ) ),
963 'help' => $l10n->msg( 'recentchangesdays-max' )->numParams(
964 ceil( $rcMaxAge / ( 3600 * 24 ) ) )->escaped()
965 ];
966 $defaultPreferences['rclimit'] = [
967 'type' => 'int',
968 'min' => 0,
969 'max' => 1000,
970 'label-message' => 'recentchangescount',
971 'help-message' => 'prefs-help-recentchangescount',
972 'section' => 'rc/displayrc',
973 ];
974 $defaultPreferences['usenewrc'] = [
975 'type' => 'toggle',
976 'label-message' => 'tog-usenewrc',
977 'section' => 'rc/advancedrc',
978 ];
979 $defaultPreferences['hideminor'] = [
980 'type' => 'toggle',
981 'label-message' => 'tog-hideminor',
982 'section' => 'rc/advancedrc',
983 ];
984 $defaultPreferences['rcfilters-saved-queries'] = [
985 'type' => 'api',
986 ];
987 $defaultPreferences['rcfilters-wl-saved-queries'] = [
988 'type' => 'api',
989 ];
990 // Override RCFilters preferences for RecentChanges 'limit'
991 $defaultPreferences['rcfilters-limit'] = [
992 'type' => 'api',
993 ];
994 $defaultPreferences['rcfilters-saved-queries-versionbackup'] = [
995 'type' => 'api',
996 ];
997 $defaultPreferences['rcfilters-wl-saved-queries-versionbackup'] = [
998 'type' => 'api',
999 ];
1000
1001 if ( $this->config->get( 'RCWatchCategoryMembership' ) ) {
1002 $defaultPreferences['hidecategorization'] = [
1003 'type' => 'toggle',
1004 'label-message' => 'tog-hidecategorization',
1005 'section' => 'rc/advancedrc',
1006 ];
1007 }
1008
1009 if ( $user->useRCPatrol() ) {
1010 $defaultPreferences['hidepatrolled'] = [
1011 'type' => 'toggle',
1012 'section' => 'rc/advancedrc',
1013 'label-message' => 'tog-hidepatrolled',
1014 ];
1015 }
1016
1017 if ( $user->useNPPatrol() ) {
1018 $defaultPreferences['newpageshidepatrolled'] = [
1019 'type' => 'toggle',
1020 'section' => 'rc/advancedrc',
1021 'label-message' => 'tog-newpageshidepatrolled',
1022 ];
1023 }
1024
1025 if ( $this->config->get( 'RCShowWatchingUsers' ) ) {
1026 $defaultPreferences['shownumberswatching'] = [
1027 'type' => 'toggle',
1028 'section' => 'rc/advancedrc',
1029 'label-message' => 'tog-shownumberswatching',
1030 ];
1031 }
1032
1033 if ( $this->config->get( 'StructuredChangeFiltersShowPreference' ) ) {
1034 $defaultPreferences['rcenhancedfilters-disable'] = [
1035 'type' => 'toggle',
1036 'section' => 'rc/opt-out',
1037 'label-message' => 'rcfilters-preference-label',
1038 'help-message' => 'rcfilters-preference-help',
1039 ];
1040 }
1041 }
1042
1043 /**
1044 * @param User $user
1045 * @param IContextSource $context
1046 * @param array &$defaultPreferences
1047 */
1048 protected function watchlistPreferences(
1049 User $user, IContextSource $context, &$defaultPreferences
1050 ) {
1051 $watchlistdaysMax = ceil( $this->config->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
1052
1053 # # Watchlist #####################################
1054 if ( $user->isAllowed( 'editmywatchlist' ) ) {
1055 $editWatchlistLinks = [];
1056 $editWatchlistModes = [
1057 'edit' => [ 'EditWatchlist', false ],
1058 'raw' => [ 'EditWatchlist', 'raw' ],
1059 'clear' => [ 'EditWatchlist', 'clear' ],
1060 ];
1061 foreach ( $editWatchlistModes as $editWatchlistMode => $mode ) {
1062 // Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear
1063 $editWatchlistLinks[] = $this->linkRenderer->makeKnownLink(
1064 SpecialPage::getTitleFor( $mode[0], $mode[1] ),
1065 new HtmlArmor( $context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse() )
1066 );
1067 }
1068
1069 $defaultPreferences['editwatchlist'] = [
1070 'type' => 'info',
1071 'raw' => true,
1072 'default' => $context->getLanguage()->pipeList( $editWatchlistLinks ),
1073 'label-message' => 'prefs-editwatchlist-label',
1074 'section' => 'watchlist/editwatchlist',
1075 ];
1076 }
1077
1078 $defaultPreferences['watchlistdays'] = [
1079 'type' => 'float',
1080 'min' => 0,
1081 'max' => $watchlistdaysMax,
1082 'section' => 'watchlist/displaywatchlist',
1083 'help' => $context->msg( 'prefs-watchlist-days-max' )->numParams(
1084 $watchlistdaysMax )->escaped(),
1085 'label-message' => 'prefs-watchlist-days',
1086 ];
1087 $defaultPreferences['wllimit'] = [
1088 'type' => 'int',
1089 'min' => 0,
1090 'max' => 1000,
1091 'label-message' => 'prefs-watchlist-edits',
1092 'help' => $context->msg( 'prefs-watchlist-edits-max' )->escaped(),
1093 'section' => 'watchlist/displaywatchlist',
1094 ];
1095 $defaultPreferences['extendwatchlist'] = [
1096 'type' => 'toggle',
1097 'section' => 'watchlist/advancedwatchlist',
1098 'label-message' => 'tog-extendwatchlist',
1099 ];
1100 $defaultPreferences['watchlisthideminor'] = [
1101 'type' => 'toggle',
1102 'section' => 'watchlist/advancedwatchlist',
1103 'label-message' => 'tog-watchlisthideminor',
1104 ];
1105 $defaultPreferences['watchlisthidebots'] = [
1106 'type' => 'toggle',
1107 'section' => 'watchlist/advancedwatchlist',
1108 'label-message' => 'tog-watchlisthidebots',
1109 ];
1110 $defaultPreferences['watchlisthideown'] = [
1111 'type' => 'toggle',
1112 'section' => 'watchlist/advancedwatchlist',
1113 'label-message' => 'tog-watchlisthideown',
1114 ];
1115 $defaultPreferences['watchlisthideanons'] = [
1116 'type' => 'toggle',
1117 'section' => 'watchlist/advancedwatchlist',
1118 'label-message' => 'tog-watchlisthideanons',
1119 ];
1120 $defaultPreferences['watchlisthideliu'] = [
1121 'type' => 'toggle',
1122 'section' => 'watchlist/advancedwatchlist',
1123 'label-message' => 'tog-watchlisthideliu',
1124 ];
1125
1126 if ( !\SpecialWatchlist::checkStructuredFilterUiEnabled(
1127 $this->config,
1128 $user
1129 ) ) {
1130 $defaultPreferences['watchlistreloadautomatically'] = [
1131 'type' => 'toggle',
1132 'section' => 'watchlist/advancedwatchlist',
1133 'label-message' => 'tog-watchlistreloadautomatically',
1134 ];
1135 }
1136
1137 $defaultPreferences['watchlistunwatchlinks'] = [
1138 'type' => 'toggle',
1139 'section' => 'watchlist/advancedwatchlist',
1140 'label-message' => 'tog-watchlistunwatchlinks',
1141 ];
1142
1143 if ( $this->config->get( 'RCWatchCategoryMembership' ) ) {
1144 $defaultPreferences['watchlisthidecategorization'] = [
1145 'type' => 'toggle',
1146 'section' => 'watchlist/advancedwatchlist',
1147 'label-message' => 'tog-watchlisthidecategorization',
1148 ];
1149 }
1150
1151 if ( $user->useRCPatrol() ) {
1152 $defaultPreferences['watchlisthidepatrolled'] = [
1153 'type' => 'toggle',
1154 'section' => 'watchlist/advancedwatchlist',
1155 'label-message' => 'tog-watchlisthidepatrolled',
1156 ];
1157 }
1158
1159 $watchTypes = [
1160 'edit' => 'watchdefault',
1161 'move' => 'watchmoves',
1162 'delete' => 'watchdeletion'
1163 ];
1164
1165 // Kinda hacky
1166 if ( $user->isAllowed( 'createpage' ) || $user->isAllowed( 'createtalk' ) ) {
1167 $watchTypes['read'] = 'watchcreations';
1168 }
1169
1170 if ( $user->isAllowed( 'rollback' ) ) {
1171 $watchTypes['rollback'] = 'watchrollback';
1172 }
1173
1174 if ( $user->isAllowed( 'upload' ) ) {
1175 $watchTypes['upload'] = 'watchuploads';
1176 }
1177
1178 foreach ( $watchTypes as $action => $pref ) {
1179 if ( $user->isAllowed( $action ) ) {
1180 // Messages:
1181 // tog-watchdefault, tog-watchmoves, tog-watchdeletion, tog-watchcreations, tog-watchuploads
1182 // tog-watchrollback
1183 $defaultPreferences[$pref] = [
1184 'type' => 'toggle',
1185 'section' => 'watchlist/advancedwatchlist',
1186 'label-message' => "tog-$pref",
1187 ];
1188 }
1189 }
1190
1191 if ( $this->config->get( 'EnableAPI' ) ) {
1192 $defaultPreferences['watchlisttoken'] = [
1193 'type' => 'api',
1194 ];
1195 $defaultPreferences['watchlisttoken-info'] = [
1196 'type' => 'info',
1197 'section' => 'watchlist/tokenwatchlist',
1198 'label-message' => 'prefs-watchlist-token',
1199 'default' => $user->getTokenFromOption( 'watchlisttoken' ),
1200 'help-message' => 'prefs-help-watchlist-token2',
1201 ];
1202 }
1203 }
1204
1205 /**
1206 * @param array &$defaultPreferences
1207 */
1208 protected function searchPreferences( &$defaultPreferences ) {
1209 foreach ( MWNamespace::getValidNamespaces() as $n ) {
1210 $defaultPreferences['searchNs' . $n] = [
1211 'type' => 'api',
1212 ];
1213 }
1214 }
1215
1216 /**
1217 * @param User $user The User object
1218 * @param IContextSource $context
1219 * @return array Text/links to display as key; $skinkey as value
1220 */
1221 protected function generateSkinOptions( User $user, IContextSource $context ) {
1222 $ret = [];
1223
1224 $mptitle = Title::newMainPage();
1225 $previewtext = $context->msg( 'skin-preview' )->escaped();
1226
1227 # Only show skins that aren't disabled in $wgSkipSkins
1228 $validSkinNames = Skin::getAllowedSkins();
1229
1230 foreach ( $validSkinNames as $skinkey => &$skinname ) {
1231 $msg = $context->msg( "skinname-{$skinkey}" );
1232 if ( $msg->exists() ) {
1233 $skinname = htmlspecialchars( $msg->text() );
1234 }
1235 }
1236
1237 $defaultSkin = $this->config->get( 'DefaultSkin' );
1238 $allowUserCss = $this->config->get( 'AllowUserCss' );
1239 $allowUserJs = $this->config->get( 'AllowUserJs' );
1240
1241 # Sort by the internal name, so that the ordering is the same for each display language,
1242 # especially if some skin names are translated to use a different alphabet and some are not.
1243 uksort( $validSkinNames, function ( $a, $b ) use ( $defaultSkin ) {
1244 # Display the default first in the list by comparing it as lesser than any other.
1245 if ( strcasecmp( $a, $defaultSkin ) === 0 ) {
1246 return -1;
1247 }
1248 if ( strcasecmp( $b, $defaultSkin ) === 0 ) {
1249 return 1;
1250 }
1251 return strcasecmp( $a, $b );
1252 } );
1253
1254 $foundDefault = false;
1255 foreach ( $validSkinNames as $skinkey => $sn ) {
1256 $linkTools = [];
1257
1258 # Mark the default skin
1259 if ( strcasecmp( $skinkey, $defaultSkin ) === 0 ) {
1260 $linkTools[] = $context->msg( 'default' )->escaped();
1261 $foundDefault = true;
1262 }
1263
1264 # Create preview link
1265 $mplink = htmlspecialchars( $mptitle->getLocalURL( [ 'useskin' => $skinkey ] ) );
1266 $linkTools[] = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
1267
1268 # Create links to user CSS/JS pages
1269 if ( $allowUserCss ) {
1270 $cssPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.css' );
1271 $cssLinkText = $context->msg( 'prefs-custom-css' )->text();
1272 $linkTools[] = $this->linkRenderer->makeLink( $cssPage, $cssLinkText );
1273 }
1274
1275 if ( $allowUserJs ) {
1276 $jsPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.js' );
1277 $jsLinkText = $context->msg( 'prefs-custom-js' )->text();
1278 $linkTools[] = $this->linkRenderer->makeLink( $jsPage, $jsLinkText );
1279 }
1280
1281 $display = $sn . ' ' . $context->msg( 'parentheses' )
1282 ->rawParams( $context->getLanguage()->pipeList( $linkTools ) )
1283 ->escaped();
1284 $ret[$display] = $skinkey;
1285 }
1286
1287 if ( !$foundDefault ) {
1288 // If the default skin is not available, things are going to break horribly because the
1289 // default value for skin selector will not be a valid value. Let's just not show it then.
1290 return [];
1291 }
1292
1293 return $ret;
1294 }
1295
1296 /**
1297 * @param IContextSource $context
1298 * @return array
1299 */
1300 protected function getDateOptions( IContextSource $context ) {
1301 $lang = $context->getLanguage();
1302 $dateopts = $lang->getDatePreferences();
1303
1304 $ret = [];
1305
1306 if ( $dateopts ) {
1307 if ( !in_array( 'default', $dateopts ) ) {
1308 $dateopts[] = 'default'; // Make sure default is always valid T21237
1309 }
1310
1311 // FIXME KLUGE: site default might not be valid for user language
1312 global $wgDefaultUserOptions;
1313 if ( !in_array( $wgDefaultUserOptions['date'], $dateopts ) ) {
1314 $wgDefaultUserOptions['date'] = 'default';
1315 }
1316
1317 $epoch = wfTimestampNow();
1318 foreach ( $dateopts as $key ) {
1319 if ( $key == 'default' ) {
1320 $formatted = $context->msg( 'datedefault' )->escaped();
1321 } else {
1322 $formatted = htmlspecialchars( $lang->timeanddate( $epoch, false, $key ) );
1323 }
1324 $ret[$formatted] = $key;
1325 }
1326 }
1327 return $ret;
1328 }
1329
1330 /**
1331 * @param MessageLocalizer $l10n
1332 * @return array
1333 */
1334 protected function getImageSizes( MessageLocalizer $l10n ) {
1335 $ret = [];
1336 $pixels = $l10n->msg( 'unit-pixel' )->text();
1337
1338 foreach ( $this->config->get( 'ImageLimits' ) as $index => $limits ) {
1339 // Note: A left-to-right marker (\u200e) is inserted, see T144386
1340 $display = "{$limits[0]}" . json_decode( '"\u200e"' ) . "×{$limits[1]}" . $pixels;
1341 $ret[$display] = $index;
1342 }
1343
1344 return $ret;
1345 }
1346
1347 /**
1348 * @param MessageLocalizer $l10n
1349 * @return array
1350 */
1351 protected function getThumbSizes( MessageLocalizer $l10n ) {
1352 $ret = [];
1353 $pixels = $l10n->msg( 'unit-pixel' )->text();
1354
1355 foreach ( $this->config->get( 'ThumbLimits' ) as $index => $size ) {
1356 $display = $size . $pixels;
1357 $ret[$display] = $index;
1358 }
1359
1360 return $ret;
1361 }
1362
1363 /**
1364 * @param string $signature
1365 * @param array $alldata
1366 * @param HTMLForm $form
1367 * @return bool|string
1368 */
1369 protected function validateSignature( $signature, $alldata, HTMLForm $form ) {
1370 $maxSigChars = $this->config->get( 'MaxSigChars' );
1371 if ( mb_strlen( $signature ) > $maxSigChars ) {
1372 return Xml::element( 'span', [ 'class' => 'error' ],
1373 $form->msg( 'badsiglength' )->numParams( $maxSigChars )->text() );
1374 } elseif ( isset( $alldata['fancysig'] ) &&
1375 $alldata['fancysig'] &&
1376 MediaWikiServices::getInstance()->getParser()->validateSig( $signature ) === false
1377 ) {
1378 return Xml::element(
1379 'span',
1380 [ 'class' => 'error' ],
1381 $form->msg( 'badsig' )->text()
1382 );
1383 } else {
1384 return true;
1385 }
1386 }
1387
1388 /**
1389 * @param string $signature
1390 * @param array $alldata
1391 * @param HTMLForm $form
1392 * @return string
1393 */
1394 protected function cleanSignature( $signature, $alldata, HTMLForm $form ) {
1395 $parser = MediaWikiServices::getInstance()->getParser();
1396 if ( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) {
1397 $signature = $parser->cleanSig( $signature );
1398 } else {
1399 // When no fancy sig used, make sure ~{3,5} get removed.
1400 $signature = Parser::cleanSigInSig( $signature );
1401 }
1402
1403 return $signature;
1404 }
1405
1406 /**
1407 * @param User $user
1408 * @param IContextSource $context
1409 * @param string $formClass
1410 * @param array $remove Array of items to remove
1411 * @return PreferencesForm|HTMLForm
1412 */
1413 public function getForm(
1414 User $user,
1415 IContextSource $context,
1416 $formClass = PreferencesForm::class,
1417 array $remove = []
1418 ) {
1419 $formDescriptor = $this->getFormDescriptor( $user, $context );
1420 if ( count( $remove ) ) {
1421 $removeKeys = array_flip( $remove );
1422 $formDescriptor = array_diff_key( $formDescriptor, $removeKeys );
1423 }
1424
1425 // Remove type=api preferences. They are not intended for rendering in the form.
1426 foreach ( $formDescriptor as $name => $info ) {
1427 if ( isset( $info['type'] ) && $info['type'] === 'api' ) {
1428 unset( $formDescriptor[$name] );
1429 }
1430 }
1431
1432 /**
1433 * @var $htmlForm PreferencesForm
1434 */
1435 $htmlForm = new $formClass( $formDescriptor, $context, 'prefs' );
1436
1437 $htmlForm->setModifiedUser( $user );
1438 $htmlForm->setId( 'mw-prefs-form' );
1439 $htmlForm->setAutocomplete( 'off' );
1440 $htmlForm->setSubmitText( $context->msg( 'saveprefs' )->text() );
1441 # Used message keys: 'accesskey-preferences-save', 'tooltip-preferences-save'
1442 $htmlForm->setSubmitTooltip( 'preferences-save' );
1443 $htmlForm->setSubmitID( 'prefcontrol' );
1444 $htmlForm->setSubmitCallback( function ( array $formData, PreferencesForm $form ) {
1445 return $this->submitForm( $formData, $form );
1446 } );
1447
1448 return $htmlForm;
1449 }
1450
1451 /**
1452 * @param IContextSource $context
1453 * @return array
1454 */
1455 protected function getTimezoneOptions( IContextSource $context ) {
1456 $opt = [];
1457
1458 $localTZoffset = $this->config->get( 'LocalTZoffset' );
1459 $timeZoneList = $this->getTimeZoneList( $context->getLanguage() );
1460
1461 $timestamp = MWTimestamp::getLocalInstance();
1462 // Check that the LocalTZoffset is the same as the local time zone offset
1463 if ( $localTZoffset == $timestamp->format( 'Z' ) / 60 ) {
1464 $timezoneName = $timestamp->getTimezone()->getName();
1465 // Localize timezone
1466 if ( isset( $timeZoneList[$timezoneName] ) ) {
1467 $timezoneName = $timeZoneList[$timezoneName]['name'];
1468 }
1469 $server_tz_msg = $context->msg(
1470 'timezoneuseserverdefault',
1471 $timezoneName
1472 )->text();
1473 } else {
1474 $tzstring = sprintf(
1475 '%+03d:%02d',
1476 floor( $localTZoffset / 60 ),
1477 abs( $localTZoffset ) % 60
1478 );
1479 $server_tz_msg = $context->msg( 'timezoneuseserverdefault', $tzstring )->text();
1480 }
1481 $opt[$server_tz_msg] = "System|$localTZoffset";
1482 $opt[$context->msg( 'timezoneuseoffset' )->text()] = 'other';
1483 $opt[$context->msg( 'guesstimezone' )->text()] = 'guess';
1484
1485 foreach ( $timeZoneList as $timeZoneInfo ) {
1486 $region = $timeZoneInfo['region'];
1487 if ( !isset( $opt[$region] ) ) {
1488 $opt[$region] = [];
1489 }
1490 $opt[$region][$timeZoneInfo['name']] = $timeZoneInfo['timecorrection'];
1491 }
1492 return $opt;
1493 }
1494
1495 /**
1496 * @param string $tz
1497 * @param array $alldata
1498 * @return string
1499 */
1500 protected function filterTimezoneInput( $tz, array $alldata ) {
1501 $data = explode( '|', $tz, 3 );
1502 switch ( $data[0] ) {
1503 case 'ZoneInfo':
1504 $valid = false;
1505
1506 if ( count( $data ) === 3 ) {
1507 // Make sure this timezone exists
1508 try {
1509 new DateTimeZone( $data[2] );
1510 // If the constructor didn't throw, we know it's valid
1511 $valid = true;
1512 } catch ( Exception $e ) {
1513 // Not a valid timezone
1514 }
1515 }
1516
1517 if ( !$valid ) {
1518 // If the supplied timezone doesn't exist, fall back to the encoded offset
1519 return 'Offset|' . intval( $tz[1] );
1520 }
1521 return $tz;
1522 case 'System':
1523 return $tz;
1524 default:
1525 $data = explode( ':', $tz, 2 );
1526 if ( count( $data ) == 2 ) {
1527 $data[0] = intval( $data[0] );
1528 $data[1] = intval( $data[1] );
1529 $minDiff = abs( $data[0] ) * 60 + $data[1];
1530 if ( $data[0] < 0 ) {
1531 $minDiff = - $minDiff;
1532 }
1533 } else {
1534 $minDiff = intval( $data[0] ) * 60;
1535 }
1536
1537 # Max is +14:00 and min is -12:00, see:
1538 # https://en.wikipedia.org/wiki/Timezone
1539 $minDiff = min( $minDiff, 840 ); # 14:00
1540 $minDiff = max( $minDiff, -720 ); # -12:00
1541 return 'Offset|' . $minDiff;
1542 }
1543 }
1544
1545 /**
1546 * Handle the form submission if everything validated properly
1547 *
1548 * @param array $formData
1549 * @param PreferencesForm $form
1550 * @return bool|Status|string
1551 */
1552 protected function saveFormData( $formData, PreferencesForm $form ) {
1553 $user = $form->getModifiedUser();
1554 $hiddenPrefs = $this->config->get( 'HiddenPrefs' );
1555 $result = true;
1556
1557 if ( !$user->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
1558 return Status::newFatal( 'mypreferencesprotected' );
1559 }
1560
1561 // Filter input
1562 foreach ( array_keys( $formData ) as $name ) {
1563 $filters = $this->getSaveFilters();
1564 if ( isset( $filters[$name] ) ) {
1565 $formData[$name] = call_user_func( $filters[$name], $formData[$name], $formData );
1566 }
1567 }
1568
1569 // Fortunately, the realname field is MUCH simpler
1570 // (not really "private", but still shouldn't be edited without permission)
1571
1572 if ( !in_array( 'realname', $hiddenPrefs )
1573 && $user->isAllowed( 'editmyprivateinfo' )
1574 && array_key_exists( 'realname', $formData )
1575 ) {
1576 $realName = $formData['realname'];
1577 $user->setRealName( $realName );
1578 }
1579
1580 if ( $user->isAllowed( 'editmyoptions' ) ) {
1581 $oldUserOptions = $user->getOptions();
1582
1583 foreach ( $this->getSaveBlacklist() as $b ) {
1584 unset( $formData[$b] );
1585 }
1586
1587 # If users have saved a value for a preference which has subsequently been disabled
1588 # via $wgHiddenPrefs, we don't want to destroy that setting in case the preference
1589 # is subsequently re-enabled
1590 foreach ( $hiddenPrefs as $pref ) {
1591 # If the user has not set a non-default value here, the default will be returned
1592 # and subsequently discarded
1593 $formData[$pref] = $user->getOption( $pref, null, true );
1594 }
1595
1596 // If the user changed the rclimit preference, also change the rcfilters-rclimit preference
1597 if (
1598 isset( $formData['rclimit'] ) &&
1599 intval( $formData[ 'rclimit' ] ) !== $user->getIntOption( 'rclimit' )
1600 ) {
1601 $formData['rcfilters-limit'] = $formData['rclimit'];
1602 }
1603
1604 // Keep old preferences from interfering due to back-compat code, etc.
1605 $user->resetOptions( 'unused', $form->getContext() );
1606
1607 foreach ( $formData as $key => $value ) {
1608 $user->setOption( $key, $value );
1609 }
1610
1611 Hooks::run(
1612 'PreferencesFormPreSave',
1613 [ $formData, $form, $user, &$result, $oldUserOptions ]
1614 );
1615 }
1616
1617 AuthManager::callLegacyAuthPlugin( 'updateExternalDB', [ $user ] );
1618 $user->saveSettings();
1619
1620 return $result;
1621 }
1622
1623 /**
1624 * DO NOT USE. Temporary function to punch hole for the Preferences class.
1625 *
1626 * @deprecated since 1.31, its inception
1627 *
1628 * @param array $formData
1629 * @param PreferencesForm $form
1630 * @return bool|Status|string
1631 */
1632 public function legacySaveFormData( $formData, PreferencesForm $form ) {
1633 return $this->saveFormData( $formData, $form );
1634 }
1635
1636 /**
1637 * Save the form data and reload the page
1638 *
1639 * @param array $formData
1640 * @param PreferencesForm $form
1641 * @return Status
1642 */
1643 protected function submitForm( array $formData, PreferencesForm $form ) {
1644 $res = $this->saveFormData( $formData, $form );
1645
1646 if ( $res ) {
1647 $urlOptions = [];
1648
1649 if ( $res === 'eauth' ) {
1650 $urlOptions['eauth'] = 1;
1651 }
1652
1653 $urlOptions += $form->getExtraSuccessRedirectParameters();
1654
1655 $url = $form->getTitle()->getFullURL( $urlOptions );
1656
1657 $context = $form->getContext();
1658 // Set session data for the success message
1659 $context->getRequest()->getSession()->set( 'specialPreferencesSaveSuccess', 1 );
1660
1661 $context->getOutput()->redirect( $url );
1662 }
1663
1664 return Status::newGood();
1665 }
1666
1667 /**
1668 * DO NOT USE. Temporary function to punch hole for the Preferences class.
1669 *
1670 * @deprecated since 1.31, its inception
1671 *
1672 * @param array $formData
1673 * @param PreferencesForm $form
1674 * @return Status
1675 */
1676 public function legacySubmitForm( array $formData, PreferencesForm $form ) {
1677 return $this->submitForm( $formData, $form );
1678 }
1679
1680 /**
1681 * Get a list of all time zones
1682 * @param Language $language Language used for the localized names
1683 * @return array A list of all time zones. The system name of the time zone is used as key and
1684 * the value is an array which contains localized name, the timecorrection value used for
1685 * preferences and the region
1686 * @since 1.26
1687 */
1688 protected function getTimeZoneList( Language $language ) {
1689 $identifiers = DateTimeZone::listIdentifiers();
1690 if ( $identifiers === false ) {
1691 return [];
1692 }
1693 sort( $identifiers );
1694
1695 $tzRegions = [
1696 'Africa' => wfMessage( 'timezoneregion-africa' )->inLanguage( $language )->text(),
1697 'America' => wfMessage( 'timezoneregion-america' )->inLanguage( $language )->text(),
1698 'Antarctica' => wfMessage( 'timezoneregion-antarctica' )->inLanguage( $language )->text(),
1699 'Arctic' => wfMessage( 'timezoneregion-arctic' )->inLanguage( $language )->text(),
1700 'Asia' => wfMessage( 'timezoneregion-asia' )->inLanguage( $language )->text(),
1701 'Atlantic' => wfMessage( 'timezoneregion-atlantic' )->inLanguage( $language )->text(),
1702 'Australia' => wfMessage( 'timezoneregion-australia' )->inLanguage( $language )->text(),
1703 'Europe' => wfMessage( 'timezoneregion-europe' )->inLanguage( $language )->text(),
1704 'Indian' => wfMessage( 'timezoneregion-indian' )->inLanguage( $language )->text(),
1705 'Pacific' => wfMessage( 'timezoneregion-pacific' )->inLanguage( $language )->text(),
1706 ];
1707 asort( $tzRegions );
1708
1709 $timeZoneList = [];
1710
1711 $now = new DateTime();
1712
1713 foreach ( $identifiers as $identifier ) {
1714 $parts = explode( '/', $identifier, 2 );
1715
1716 // DateTimeZone::listIdentifiers() returns a number of
1717 // backwards-compatibility entries. This filters them out of the
1718 // list presented to the user.
1719 if ( count( $parts ) !== 2 || !array_key_exists( $parts[0], $tzRegions ) ) {
1720 continue;
1721 }
1722
1723 // Localize region
1724 $parts[0] = $tzRegions[$parts[0]];
1725
1726 $dateTimeZone = new DateTimeZone( $identifier );
1727 $minDiff = floor( $dateTimeZone->getOffset( $now ) / 60 );
1728
1729 $display = str_replace( '_', ' ', $parts[0] . '/' . $parts[1] );
1730 $value = "ZoneInfo|$minDiff|$identifier";
1731
1732 $timeZoneList[$identifier] = [
1733 'name' => $display,
1734 'timecorrection' => $value,
1735 'region' => $parts[0],
1736 ];
1737 }
1738
1739 return $timeZoneList;
1740 }
1741 }