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