(bug 19798) High-level documentation for Preferences.
[lhc/web/wiklou.git] / includes / Preferences.php
1 <?php
2
3 /**
4 General information about this file:
5 We're now using the HTMLForm object with some customisation to generate the Preferences
6 form. This object handles generic submission, CSRF protection, layout and other logic
7 in a reusable manner. We subclass it as a PreferencesForm to make some minor
8 customisations.
9 In order to generate the form, the HTMLForm object needs an array structure detailing the
10 form fields available, and that's what this class is for. Each element of the array is
11 a basic property-list, including the type of field, the label it is to be given in the
12 form, callbacks for validation and 'filtering', and other pertinent information. Note that
13 the 'default' field is named for generic forms, and does not represent the preference's
14 default (which is stored in $wgDefaultUserOptions), but the default for the form field,
15 which should be whatever the user has set for that preference. There is no need to
16 override it unless you have some special storage logic (for instance, those not presently
17 stored as options, but which are best set from the user preferences view).
18 Field types are implemented as subclasses of the generic HTMLFormField object, and
19 typically implement at least getInputHTML, which generates the HTML for the input field
20 to be placed in the table.
21 Once fields have been retrieved and validated, submission logic is handed over to the
22 tryUISubmit static method of this class.
23 */
24
25 class Preferences {
26 static $defaultPreferences = null;
27 static $saveFilters =
28 array(
29 'timecorrection' => array( 'Preferences', 'filterTimezoneInput' ),
30 );
31
32 static function getPreferences( $user ) {
33 if ( self::$defaultPreferences )
34 return self::$defaultPreferences;
35
36 global $wgRCMaxAge;
37
38 $defaultPreferences = array();
39
40 self::profilePreferences( $user, $defaultPreferences );
41 self::skinPreferences( $user, $defaultPreferences );
42 self::filesPreferences( $user, $defaultPreferences );
43 self::mathPreferences( $user, $defaultPreferences );
44 self::datetimePreferences( $user, $defaultPreferences );
45 self::renderingPreferences( $user, $defaultPreferences );
46 self::editingPreferences( $user, $defaultPreferences );
47 self::rcPreferences( $user, $defaultPreferences );
48 self::watchlistPreferences( $user, $defaultPreferences );
49 self::searchPreferences( $user, $defaultPreferences );
50 self::miscPreferences( $user, $defaultPreferences );
51
52 wfRunHooks( 'GetPreferences', array( $user, &$defaultPreferences ) );
53
54 ## Remove preferences that wikis don't want to use
55 global $wgHiddenPrefs;
56 foreach ( $wgHiddenPrefs as $pref ) {
57 if ( isset( $defaultPreferences[$pref] ) ) {
58 unset( $defaultPreferences[$pref] );
59 }
60 }
61
62 ## Prod in defaults from the user
63 global $wgDefaultUserOptions;
64 foreach( $defaultPreferences as $name => &$info ) {
65 $prefFromUser = self::getOptionFromUser( $name, $info, $user );
66 $field = HTMLForm::loadInputFromParameters( $info ); // For validation
67 $defaultOptions = User::getDefaultOptions();
68 $globalDefault = isset( $defaultOptions[$name] )
69 ? $defaultOptions[$name]
70 : null;
71
72 // If it validates, set it as the default
73 if ( isset( $info['default'] ) ) {
74 // Already set, no problem
75 continue;
76 } elseif ( !is_null( $prefFromUser ) && // Make sure we're not just pulling nothing
77 $field->validate( $prefFromUser, $user->mOptions ) === true ) {
78 $info['default'] = $prefFromUser;
79 } elseif( $field->validate( $globalDefault, $user->mOptions ) === true ) {
80 $info['default'] = $globalDefault;
81 } else {
82 throw new MWException( "Global default '$globalDefault' is invalid for field $name" );
83 }
84 }
85
86 self::$defaultPreferences = $defaultPreferences;
87
88 return $defaultPreferences;
89 }
90
91 // Pull option from a user account. Handles stuff like array-type preferences.
92 static function getOptionFromUser( $name, $info, $user ) {
93 $val = $user->getOption( $name );
94
95 // Handling for array-type preferences
96 if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
97 ( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) {
98
99 $options = HTMLFormField::flattenOptions( $info['options'] );
100 $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
101 $val = array();
102
103 foreach( $options as $label => $value ) {
104 if( $user->getOption( "$prefix$value" ) ) {
105 $val[] = $value;
106 }
107 }
108 }
109
110 return $val;
111 }
112
113 static function profilePreferences( $user, &$defaultPreferences ) {
114 global $wgLang;
115 ## User info #####################################
116 // Information panel
117 $defaultPreferences['username'] =
118 array(
119 'type' => 'info',
120 'label-message' => 'username',
121 'default' => $user->getName(),
122 'section' => 'personal/info',
123 );
124
125 $defaultPreferences['userid'] =
126 array(
127 'type' => 'info',
128 'label-message' => 'uid',
129 'default' => $user->getId(),
130 'section' => 'personal/info',
131 );
132
133 # Get groups to which the user belongs
134 $userEffectiveGroups = $user->getEffectiveGroups();
135 $userGroups = $userMembers = array();
136 foreach( $userEffectiveGroups as $ueg ) {
137 if( $ueg == '*' ) {
138 // Skip the default * group, seems useless here
139 continue;
140 }
141 $groupName = User::getGroupName( $ueg );
142 $userGroups[] = User::makeGroupLinkHTML( $ueg, $groupName );
143
144 $memberName = User::getGroupMember( $ueg );
145 $userMembers[] = User::makeGroupLinkHTML( $ueg, $memberName );
146 }
147 asort( $userGroups );
148 asort( $userMembers );
149
150 $defaultPreferences['usergroups'] =
151 array(
152 'type' => 'info',
153 'label' => wfMsgExt( 'prefs-memberingroups', 'parseinline',
154 $wgLang->formatNum( count($userGroups) ) ),
155 'default' => wfMsgExt( 'prefs-memberingroups-type', array(),
156 $wgLang->commaList( $userGroups ),
157 $wgLang->commaList( $userMembers )
158 ),
159 'raw' => true,
160 'section' => 'personal/info',
161 );
162
163 $defaultPreferences['editcount'] =
164 array(
165 'type' => 'info',
166 'label-message' => 'prefs-edits',
167 'default' => $wgLang->formatNum( $user->getEditCount() ),
168 'section' => 'personal/info',
169 );
170
171 if( $user->getRegistration() ) {
172 $defaultPreferences['registrationdate'] =
173 array(
174 'type' => 'info',
175 'label-message' => 'prefs-registration',
176 'default' => wfMsgExt( 'prefs-registration-date-time', 'parsemag',
177 $wgLang->timeanddate( $user->getRegistration(), true ),
178 $wgLang->date( $user->getRegistration(), true ),
179 $wgLang->time( $user->getRegistration(), true ) ),
180 'section' => 'personal/info',
181 );
182 }
183
184 // Actually changeable stuff
185 global $wgAuth;
186 $defaultPreferences['realname'] =
187 array(
188 'type' => $wgAuth->allowPropChange( 'realname' ) ? 'text' : 'info',
189 'default' => $user->getRealName(),
190 'section' => 'personal/info',
191 'label-message' => 'yourrealname',
192 'help-message' => 'prefs-help-realname',
193 );
194
195 $defaultPreferences['gender'] =
196 array(
197 'type' => 'select',
198 'section' => 'personal/info',
199 'options' => array(
200 wfMsg( 'gender-male' ) => 'male',
201 wfMsg( 'gender-female' ) => 'female',
202 wfMsg( 'gender-unknown' ) => 'unknown',
203 ),
204 'label-message' => 'yourgender',
205 'help-message' => 'prefs-help-gender',
206 );
207
208 if( $wgAuth->allowPasswordChange() ) {
209 global $wgUser; // For skin.
210 $link = $wgUser->getSkin()->link( SpecialPage::getTitleFor( 'Resetpass' ),
211 wfMsgHtml( 'prefs-resetpass' ), array(),
212 array( 'returnto' => SpecialPage::getTitleFor( 'Preferences' ) ) );
213
214 $defaultPreferences['password'] =
215 array(
216 'type' => 'info',
217 'raw' => true,
218 'default' => $link,
219 'label-message' => 'yourpassword',
220 'section' => 'personal/info',
221 );
222 }
223
224 $defaultPreferences['rememberpassword'] =
225 array(
226 'type' => 'toggle',
227 'label-message' => 'tog-rememberpassword',
228 'section' => 'personal/info',
229 );
230
231 // Language
232 global $wgContLanguageCode;
233 $languages = array_reverse( Language::getLanguageNames( false ) );
234 if( !array_key_exists( $wgContLanguageCode, $languages ) ) {
235 $languages[$wgContLanguageCode] = $wgContLanguageCode;
236 }
237 ksort( $languages );
238
239 $options = array();
240 foreach( $languages as $code => $name ) {
241 $display = wfBCP47( $code ) . ' - ' . $name;
242 $options[$display] = $code;
243 }
244 $defaultPreferences['language'] =
245 array(
246 'type' => 'select',
247 'section' => 'personal/i18n',
248 'options' => $options,
249 'label-message' => 'yourlanguage',
250 );
251
252 global $wgContLang, $wgDisableLangConversion;
253 global $wgDisableTitleConversion;
254 /* see if there are multiple language variants to choose from*/
255 $variantArray = array();
256 if( !$wgDisableLangConversion ) {
257 $variants = $wgContLang->getVariants();
258
259 $languages = Language::getLanguageNames( true );
260 foreach( $variants as $v ) {
261 $v = str_replace( '_', '-', strtolower( $v ) );
262 if( array_key_exists( $v, $languages ) ) {
263 // If it doesn't have a name, we'll pretend it doesn't exist
264 $variantArray[$v] = $languages[$v];
265 }
266 }
267
268 $options = array();
269 foreach( $variantArray as $code => $name ) {
270 $display = wfBCP47( $code ) . ' - ' . $name;
271 $options[$display] = $code;
272 }
273
274 if( count( $variantArray ) > 1 ) {
275 $defaultPreferences['variant'] =
276 array(
277 'label-message' => 'yourvariant',
278 'type' => 'select',
279 'options' => $options,
280 'section' => 'personal/i18n',
281 );
282 }
283 }
284
285 if( count( $variantArray ) > 1 && !$wgDisableLangConversion && !$wgDisableTitleConversion ) {
286 $defaultPreferences['noconvertlink'] =
287 array(
288 'type' => 'toggle',
289 'section' => 'personal/i18n',
290 'label-message' => 'tog-noconvertlink',
291 );
292 }
293
294 global $wgMaxSigChars;
295 $defaultPreferences['nickname'] =
296 array(
297 'type' => $wgAuth->allowPropChange( 'nickname' ) ? 'text' : 'info',
298 'maxlength' => $wgMaxSigChars,
299 'label-message' => 'yournick',
300 'validation-callback' =>
301 array( 'Preferences', 'validateSignature' ),
302 'section' => 'personal/signature',
303 'filter-callback' => array( 'Preferences', 'cleanSignature' ),
304 );
305 $defaultPreferences['fancysig'] =
306 array(
307 'type' => 'toggle',
308 'label-message' => 'tog-fancysig',
309 'section' => 'personal/signature'
310 );
311
312 ## Email stuff
313
314 global $wgEnableEmail;
315 if ($wgEnableEmail) {
316
317 global $wgEmailConfirmToEdit;
318
319 $defaultPreferences['emailaddress'] =
320 array(
321 'type' => $wgAuth->allowPropChange( 'emailaddress' ) ? 'text' : 'info',
322 'default' => $user->getEmail(),
323 'section' => 'personal/email',
324 'label-message' => 'youremail',
325 'help-message' => $wgEmailConfirmToEdit
326 ? 'prefs-help-email-required'
327 : 'prefs-help-email',
328 'validation-callback' => array( 'Preferences', 'validateEmail' ),
329 );
330
331 global $wgEnableUserEmail, $wgEmailAuthentication;
332
333 $disableEmailPrefs = false;
334
335 if ( $wgEmailAuthentication ) {
336 if ( $user->getEmail() ) {
337 if( $user->getEmailAuthenticationTimestamp() ) {
338 // date and time are separate parameters to facilitate localisation.
339 // $time is kept for backward compat reasons.
340 // 'emailauthenticated' is also used in SpecialConfirmemail.php
341 $time = $wgLang->timeAndDate( $user->getEmailAuthenticationTimestamp(), true );
342 $d = $wgLang->date( $user->getEmailAuthenticationTimestamp(), true );
343 $t = $wgLang->time( $user->getEmailAuthenticationTimestamp(), true );
344 $emailauthenticated = wfMsgExt( 'emailauthenticated', 'parseinline',
345 array($time, $d, $t ) ) . '<br />';
346 $disableEmailPrefs = false;
347 } else {
348 $disableEmailPrefs = true;
349 global $wgUser; // wgUser is okay here, it's for display
350 $skin = $wgUser->getSkin();
351 $emailauthenticated = wfMsgExt( 'emailnotauthenticated', 'parseinline' ) . '<br />' .
352 $skin->link(
353 SpecialPage::getTitleFor( 'Confirmemail' ),
354 wfMsg( 'emailconfirmlink' ),
355 array(),
356 array(),
357 array( 'known', 'noclasses' )
358 ) . '<br />';
359 }
360 } else {
361 $disableEmailPrefs = true;
362 $emailauthenticated = wfMsgHtml( 'noemailprefs' );
363 }
364
365 $defaultPreferences['emailauthentication'] =
366 array(
367 'type' => 'info',
368 'raw' => true,
369 'section' => 'personal/email',
370 'label-message' => 'prefs-emailconfirm-label',
371 'default' => $emailauthenticated,
372 );
373
374 }
375
376 if( $wgEnableUserEmail ) {
377 $defaultPreferences['disablemail'] =
378 array(
379 'type' => 'toggle',
380 'invert' => true,
381 'section' => 'personal/email',
382 'label-message' => 'allowemail',
383 'disabled' => $disableEmailPrefs,
384 );
385 $defaultPreferences['ccmeonemails'] =
386 array(
387 'type' => 'toggle',
388 'section' => 'personal/email',
389 'label-message' => 'tog-ccmeonemails',
390 'disabled' => $disableEmailPrefs,
391 );
392 }
393
394 global $wgEnotifWatchlist;
395 if ( $wgEnotifWatchlist ) {
396 $defaultPreferences['enotifwatchlistpages'] =
397 array(
398 'type' => 'toggle',
399 'section' => 'personal/email',
400 'label-message' => 'tog-enotifwatchlistpages',
401 'disabled' => $disableEmailPrefs,
402 );
403 }
404 global $wgEnotifUserTalk;
405 if( $wgEnotifUserTalk ) {
406 $defaultPreferences['enotifusertalkpages'] =
407 array(
408 'type' => 'toggle',
409 'section' => 'personal/email',
410 'label-message' => 'tog-enotifusertalkpages',
411 'disabled' => $disableEmailPrefs,
412 );
413 }
414 if( $wgEnotifUserTalk || $wgEnotifWatchlist ) {
415 $defaultPreferences['enotifminoredits'] =
416 array(
417 'type' => 'toggle',
418 'section' => 'personal/email',
419 'label-message' => 'tog-enotifminoredits',
420 'disabled' => $disableEmailPrefs,
421 );
422 }
423 $defaultPreferences['enotifrevealaddr'] =
424 array(
425 'type' => 'toggle',
426 'section' => 'personal/email',
427 'label-message' => 'tog-enotifrevealaddr',
428 'disabled' => $disableEmailPrefs,
429 );
430 }
431 }
432
433 static function skinPreferences( $user, &$defaultPreferences ) {
434 ## Skin #####################################
435 $defaultPreferences['skin'] =
436 array(
437 'type' => 'radio',
438 'options' => self::generateSkinOptions( $user ),
439 'label' => '&nbsp;',
440 'section' => 'rendering/skin',
441 );
442
443 $selectedSkin = $user->getOption( 'skin' );
444 if ( in_array( $selectedSkin, array( 'cologneblue', 'standard' ) ) ) {
445 global $wgLang;
446 $settings = array_flip( $wgLang->getQuickbarSettings() );
447
448 $defaultPreferences['quickbar'] =
449 array(
450 'type' => 'radio',
451 'options' => $settings,
452 'section' => 'rendering/skin',
453 'label-message' => 'qbsettings',
454 );
455 }
456 }
457
458 static function mathPreferences( $user, &$defaultPreferences ) {
459 ## Math #####################################
460 global $wgUseTeX, $wgLang;
461 if( $wgUseTeX ) {
462 $defaultPreferences['math'] =
463 array(
464 'type' => 'radio',
465 'options' =>
466 array_flip( array_map( 'wfMsgHtml', $wgLang->getMathNames() ) ),
467 'label' => '&nbsp;',
468 'section' => 'rendering/math',
469 );
470 }
471 }
472
473 static function filesPreferences( $user, &$defaultPreferences ) {
474 ## Files #####################################
475 $defaultPreferences['imagesize'] =
476 array(
477 'type' => 'select',
478 'options' => self::getImageSizes(),
479 'label-message' => 'imagemaxsize',
480 'section' => 'rendering/files',
481 );
482 $defaultPreferences['thumbsize'] =
483 array(
484 'type' => 'select',
485 'options' => self::getThumbSizes(),
486 'label-message' => 'thumbsize',
487 'section' => 'rendering/files',
488 );
489 }
490
491 static function datetimePreferences( $user, &$defaultPreferences ) {
492 global $wgLang;
493
494 ## Date and time #####################################
495 $dateOptions = self::getDateOptions();
496 if( $dateOptions ) {
497 $defaultPreferences['date'] =
498 array(
499 'type' => 'radio',
500 'options' => $dateOptions,
501 'label' => '&nbsp;',
502 'section' => 'datetime/dateformat',
503 );
504 }
505
506 // Info
507 $nowlocal = Xml::element( 'span', array( 'id' => 'wpLocalTime' ),
508 $wgLang->time( $now = wfTimestampNow(), true ) );
509 $nowserver = $wgLang->time( $now, false ) .
510 Xml::hidden( 'wpServerTime', substr( $now, 8, 2 ) * 60 + substr( $now, 10, 2 ) );
511
512 $defaultPreferences['nowserver'] =
513 array(
514 'type' => 'info',
515 'raw' => 1,
516 'label-message' => 'servertime',
517 'default' => $nowserver,
518 'section' => 'datetime/timeoffset',
519 );
520
521 $defaultPreferences['nowlocal'] =
522 array(
523 'type' => 'info',
524 'raw' => 1,
525 'label-message' => 'localtime',
526 'default' => $nowlocal,
527 'section' => 'datetime/timeoffset',
528 );
529
530 // Grab existing pref.
531 $tzOffset = $user->getOption( 'timecorrection' );
532 $tz = explode( '|', $tzOffset, 2 );
533
534 $tzSetting = $tzOffset;
535 if( count( $tz ) > 1 && $tz[0] == 'Offset' ) {
536 $minDiff = $tz[1];
537 $tzSetting = sprintf( '%+03d:%02d', floor( $minDiff/60 ), abs( $minDiff )%60 );
538 }
539
540 $defaultPreferences['timecorrection'] =
541 array(
542 'class' => 'HTMLSelectOrOtherField',
543 'label-message' => 'timezonelegend',
544 'options' => self::getTimezoneOptions(),
545 'default' => $tzSetting,
546 'section' => 'datetime/timeoffset',
547 );
548 }
549
550 static function renderingPreferences( $user, &$defaultPreferences ) {
551 ## Page Rendering ##############################
552 $defaultPreferences['underline'] =
553 array(
554 'type' => 'select',
555 'options' => array(
556 wfMsg( 'underline-never' ) => 0,
557 wfMsg( 'underline-always' ) => 1,
558 wfMsg( 'underline-default' ) => 2,
559 ),
560 'label-message' => 'tog-underline',
561 'section' => 'rendering/advancedrendering',
562 );
563
564 $stubThresholdValues = array( 0, 50, 100, 500, 1000, 2000, 5000, 10000 );
565 $stubThresholdOptions = array();
566 foreach( $stubThresholdValues as $value ) {
567 $stubThresholdOptions[wfMsg( 'size-bytes', $value )] = $value;
568 }
569
570 $defaultPreferences['stubthreshold'] =
571 array(
572 'type' => 'selectorother',
573 'section' => 'rendering/advancedrendering',
574 'options' => $stubThresholdOptions,
575 'label' => wfMsg( 'stub-threshold' ), // Raw HTML message. Yay?
576 );
577 $defaultPreferences['highlightbroken'] =
578 array(
579 'type' => 'toggle',
580 'section' => 'rendering/advancedrendering',
581 'label' => wfMsg( 'tog-highlightbroken' ), // Raw HTML
582 );
583 $defaultPreferences['showtoc'] =
584 array(
585 'type' => 'toggle',
586 'section' => 'rendering/advancedrendering',
587 'label-message' => 'tog-showtoc',
588 );
589 $defaultPreferences['nocache'] =
590 array(
591 'type' => 'toggle',
592 'label-message' => 'tog-nocache',
593 'section' => 'rendering/advancedrendering',
594 );
595 $defaultPreferences['showhiddencats'] =
596 array(
597 'type' => 'toggle',
598 'section' => 'rendering/advancedrendering',
599 'label-message' => 'tog-showhiddencats'
600 );
601 $defaultPreferences['showjumplinks'] =
602 array(
603 'type' => 'toggle',
604 'section' => 'rendering/advancedrendering',
605 'label-message' => 'tog-showjumplinks',
606 );
607 $defaultPreferences['justify'] =
608 array(
609 'type' => 'toggle',
610 'section' => 'rendering/advancedrendering',
611 'label-message' => 'tog-justify',
612 );
613 $defaultPreferences['numberheadings'] =
614 array(
615 'type' => 'toggle',
616 'section' => 'rendering/advancedrendering',
617 'label-message' => 'tog-numberheadings',
618 );
619 }
620
621 static function editingPreferences( $user, &$defaultPreferences ) {
622 global $wgUseExternalEditor, $wgLivePreview;
623
624 ## Editing #####################################
625 $defaultPreferences['cols'] =
626 array(
627 'type' => 'int',
628 'label-message' => 'columns',
629 'section' => 'editing/textboxsize',
630 'min' => 4,
631 'max' => 1000,
632 );
633 $defaultPreferences['rows'] =
634 array(
635 'type' => 'int',
636 'label-message' => 'rows',
637 'section' => 'editing/textboxsize',
638 'min' => 4,
639 'max' => 1000,
640 );
641
642 $defaultPreferences['editfont'] =
643 array(
644 'type' => 'select',
645 'section' => 'editing/advancedediting',
646 'label-message' => 'editfont-style',
647 'options' => array(
648 wfMsg( 'editfont-default' ) => 'default',
649 wfMsg( 'editfont-monospace' ) => 'monospace',
650 wfMsg( 'editfont-sansserif' ) => 'sans-serif',
651 wfMsg( 'editfont-serif' ) => 'serif',
652 )
653 );
654 $defaultPreferences['previewontop'] =
655 array(
656 'type' => 'toggle',
657 'section' => 'editing/advancedediting',
658 'label-message' => 'tog-previewontop',
659 );
660 $defaultPreferences['previewonfirst'] =
661 array(
662 'type' => 'toggle',
663 'section' => 'editing/advancedediting',
664 'label-message' => 'tog-previewonfirst',
665 );
666 $defaultPreferences['editsection'] =
667 array(
668 'type' => 'toggle',
669 'section' => 'editing/advancedediting',
670 'label-message' => 'tog-editsection',
671 );
672 $defaultPreferences['editsectiononrightclick'] =
673 array(
674 'type' => 'toggle',
675 'section' => 'editing/advancedediting',
676 'label-message' => 'tog-editsectiononrightclick',
677 );
678 $defaultPreferences['editondblclick'] =
679 array(
680 'type' => 'toggle',
681 'section' => 'editing/advancedediting',
682 'label-message' => 'tog-editondblclick',
683 );
684 $defaultPreferences['editwidth'] =
685 array(
686 'type' => 'toggle',
687 'section' => 'editing/advancedediting',
688 'label-message' => 'tog-editwidth',
689 );
690 $defaultPreferences['showtoolbar'] =
691 array(
692 'type' => 'toggle',
693 'section' => 'editing/advancedediting',
694 'label-message' => 'tog-showtoolbar',
695 );
696 $defaultPreferences['minordefault'] =
697 array(
698 'type' => 'toggle',
699 'section' => 'editing/advancedediting',
700 'label-message' => 'tog-minordefault',
701 );
702
703 if ( $wgUseExternalEditor ) {
704 $defaultPreferences['externaleditor'] =
705 array(
706 'type' => 'toggle',
707 'section' => 'editing/advancedediting',
708 'label-message' => 'tog-externaleditor',
709 );
710 $defaultPreferences['externaldiff'] =
711 array(
712 'type' => 'toggle',
713 'section' => 'editing/advancedediting',
714 'label-message' => 'tog-externaldiff',
715 );
716 }
717
718 $defaultPreferences['forceeditsummary'] =
719 array(
720 'type' => 'toggle',
721 'section' => 'editing/advancedediting',
722 'label-message' => 'tog-forceeditsummary',
723 );
724 if ( $wgLivePreview ) {
725 $defaultPreferences['uselivepreview'] =
726 array(
727 'type' => 'toggle',
728 'section' => 'editing/advancedediting',
729 'label-message' => 'tog-uselivepreview',
730 );
731 }
732 }
733
734 static function rcPreferences( $user, &$defaultPreferences ) {
735 global $wgRCMaxAge, $wgUseRCPatrol, $wgLang;
736 ## RecentChanges #####################################
737 $defaultPreferences['rcdays'] =
738 array(
739 'type' => 'float',
740 'label-message' => 'recentchangesdays',
741 'section' => 'rc/display',
742 'min' => 1,
743 'max' => ceil( $wgRCMaxAge / ( 3600*24 ) ),
744 'help' => wfMsgExt( 'recentchangesdays-max', array( 'parsemag' ), $wgLang->formatNum( ceil( $wgRCMaxAge / ( 3600*24 ) ) ) ),
745 );
746 $defaultPreferences['rclimit'] =
747 array(
748 'type' => 'int',
749 'label-message' => 'recentchangescount',
750 'help-message' => 'prefs-help-recentchangescount',
751 'section' => 'rc/display',
752 );
753 $defaultPreferences['usenewrc'] =
754 array(
755 'type' => 'toggle',
756 'label-message' => 'tog-usenewrc',
757 'section' => 'rc/advancedrc',
758 );
759 $defaultPreferences['hideminor'] =
760 array(
761 'type' => 'toggle',
762 'label-message' => 'tog-hideminor',
763 'section' => 'rc/advancedrc',
764 );
765
766 global $wgUseRCPatrol;
767 if( $wgUseRCPatrol ) {
768 $defaultPreferences['hidepatrolled'] =
769 array(
770 'type' => 'toggle',
771 'section' => 'rc/advancedrc',
772 'label-message' => 'tog-hidepatrolled',
773 );
774 $defaultPreferences['newpageshidepatrolled'] =
775 array(
776 'type' => 'toggle',
777 'section' => 'rc/advancedrc',
778 'label-message' => 'tog-newpageshidepatrolled',
779 );
780 }
781
782 global $wgRCShowWatchingUsers;
783 if( $wgRCShowWatchingUsers ) {
784 $defaultPreferences['shownumberswatching'] =
785 array(
786 'type' => 'toggle',
787 'section' => 'rc/advancedrc',
788 'label-message' => 'tog-shownumberswatching',
789 );
790 }
791 }
792
793 static function watchlistPreferences( $user, &$defaultPreferences ) {
794 global $wgUseRCPatrol, $wgEnableAPI;
795 ## Watchlist #####################################
796 $defaultPreferences['watchlistdays'] =
797 array(
798 'type' => 'float',
799 'min' => 0,
800 'max' => 7,
801 'section' => 'watchlist/display',
802 'help' => wfMsgHtml( 'prefs-watchlist-days-max' ),
803 'label-message' => 'prefs-watchlist-days',
804 );
805 $defaultPreferences['wllimit'] =
806 array(
807 'type' => 'int',
808 'min' => 0,
809 'max' => 1000,
810 'label-message' => 'prefs-watchlist-edits',
811 'help' => wfMsgHtml( 'prefs-watchlist-edits-max' ),
812 'section' => 'watchlist/display',
813 );
814 $defaultPreferences['extendwatchlist'] =
815 array(
816 'type' => 'toggle',
817 'section' => 'watchlist/advancedwatchlist',
818 'label-message' => 'tog-extendwatchlist',
819 );
820 $defaultPreferences['watchlisthideminor'] =
821 array(
822 'type' => 'toggle',
823 'section' => 'watchlist/advancedwatchlist',
824 'label-message' => 'tog-watchlisthideminor',
825 );
826 $defaultPreferences['watchlisthidebots'] =
827 array(
828 'type' => 'toggle',
829 'section' => 'watchlist/advancedwatchlist',
830 'label-message' => 'tog-watchlisthidebots',
831 );
832 $defaultPreferences['watchlisthideown'] =
833 array(
834 'type' => 'toggle',
835 'section' => 'watchlist/advancedwatchlist',
836 'label-message' => 'tog-watchlisthideown',
837 );
838 $defaultPreferences['watchlisthideanons'] =
839 array(
840 'type' => 'toggle',
841 'section' => 'watchlist/advancedwatchlist',
842 'label-message' => 'tog-watchlisthideanons',
843 );
844 $defaultPreferences['watchlisthideliu'] =
845 array(
846 'type' => 'toggle',
847 'section' => 'watchlist/advancedwatchlist',
848 'label-message' => 'tog-watchlisthideliu',
849 );
850 if ( $wgEnableAPI ) {
851 # Some random gibberish as a proposed default
852 $hash = sha1( mt_rand() . microtime( true ) );
853 $defaultPreferences['watchlisttoken'] =
854 array(
855 'type' => 'text',
856 'section' => 'watchlist/advancedwatchlist',
857 'label-message' => 'prefs-watchlist-token',
858 'help' => wfMsgHtml( 'prefs-help-watchlist-token', $hash )
859 );
860 }
861
862 if ( $wgUseRCPatrol ) {
863 $defaultPreferences['watchlisthidepatrolled'] =
864 array(
865 'type' => 'toggle',
866 'section' => 'watchlist/advancedwatchlist',
867 'label-message' => 'tog-watchlisthidepatrolled',
868 );
869 }
870
871 $watchTypes = array(
872 'edit' => 'watchdefault',
873 'move' => 'watchmoves',
874 'delete' => 'watchdeletion'
875 );
876
877 // Kinda hacky
878 if( $user->isAllowed( 'createpage' ) || $user->isAllowed( 'createtalk' ) ) {
879 $watchTypes['read'] = 'watchcreations';
880 }
881
882 foreach( $watchTypes as $action => $pref ) {
883 if ( $user->isAllowed( $action ) ) {
884 $defaultPreferences[$pref] = array(
885 'type' => 'toggle',
886 'section' => 'watchlist/advancedwatchlist',
887 'label-message' => "tog-$pref",
888 );
889 }
890 }
891 }
892
893 static function searchPreferences( $user, &$defaultPreferences ) {
894 global $wgContLang;
895
896 ## Search #####################################
897 $defaultPreferences['searchlimit'] =
898 array(
899 'type' => 'int',
900 'label-message' => 'resultsperpage',
901 'section' => 'searchoptions/display',
902 'min' => 0,
903 );
904 $defaultPreferences['contextlines'] =
905 array(
906 'type' => 'int',
907 'label-message' => 'contextlines',
908 'section' => 'searchoptions/display',
909 'min' => 0,
910 );
911 $defaultPreferences['contextchars'] =
912 array(
913 'type' => 'int',
914 'label-message' => 'contextchars',
915 'section' => 'searchoptions/display',
916 'min' => 0,
917 );
918 global $wgEnableMWSuggest;
919 if( $wgEnableMWSuggest ) {
920 $defaultPreferences['disablesuggest'] =
921 array(
922 'type' => 'toggle',
923 'label-message' => 'mwsuggest-disable',
924 'section' => 'searchoptions/display',
925 );
926 }
927
928 $defaultPreferences['searcheverything'] =
929 array(
930 'type' => 'toggle',
931 'label-message' => 'searcheverything-enable',
932 'section' => 'searchoptions/advancedsearchoptions',
933 );
934
935 // Searchable namespaces back-compat with old format
936 $searchableNamespaces = SearchEngine::searchableNamespaces();
937
938 $nsOptions = array();
939 foreach( $wgContLang->getNamespaces() as $ns => $name ) {
940 if( $ns < 0 ) continue;
941 $displayNs = str_replace( '_', ' ', $name );
942
943 if( !$displayNs ) $displayNs = wfMsg( 'blanknamespace' );
944
945 $displayNs = htmlspecialchars( $displayNs );
946 $nsOptions[$displayNs] = $ns;
947 }
948
949 $defaultPreferences['searchnamespaces'] =
950 array(
951 'type' => 'multiselect',
952 'label-message' => 'defaultns',
953 'options' => $nsOptions,
954 'section' => 'searchoptions/advancedsearchoptions',
955 'prefix' => 'searchNs',
956 );
957 }
958
959 static function miscPreferences( $user, &$defaultPreferences ) {
960 ## Misc #####################################
961 $defaultPreferences['diffonly'] =
962 array(
963 'type' => 'toggle',
964 'section' => 'misc/diffs',
965 'label-message' => 'tog-diffonly',
966 );
967 $defaultPreferences['norollbackdiff'] =
968 array(
969 'type' => 'toggle',
970 'section' => 'misc/diffs',
971 'label-message' => 'tog-norollbackdiff',
972 );
973
974 // Stuff from Language::getExtraUserToggles()
975 global $wgContLang;
976
977 $toggles = $wgContLang->getExtraUserToggles();
978
979 foreach( $toggles as $toggle ) {
980 $defaultPreferences[$toggle] =
981 array(
982 'type' => 'toggle',
983 'section' => 'personal/i18n',
984 'label-message' => "tog-$toggle",
985 );
986 }
987 }
988
989 static function generateSkinOptions( $user ) {
990 global $wgDefaultSkin;
991 $ret = array();
992
993 $mptitle = Title::newMainPage();
994 $previewtext = wfMsgHtml( 'skin-preview' );
995 # Only show members of Skin::getSkinNames() rather than
996 # $skinNames (skins is all skin names from Language.php)
997 $validSkinNames = Skin::getUsableSkins();
998 # Sort by UI skin name. First though need to update validSkinNames as sometimes
999 # the skinkey & UI skinname differ (e.g. "standard" skinkey is "Classic" in the UI).
1000 foreach ( $validSkinNames as $skinkey => &$skinname ) {
1001 $msgName = "skinname-{$skinkey}";
1002 $localisedSkinName = wfMsg( $msgName );
1003 if ( !wfEmptyMsg( $msgName, $localisedSkinName ) ) {
1004 $skinname = htmlspecialchars( $localisedSkinName );
1005 }
1006 }
1007 asort( $validSkinNames );
1008 $sk = $user->getSkin();
1009
1010 foreach( $validSkinNames as $skinkey => $sn ) {
1011 $mplink = htmlspecialchars( $mptitle->getLocalURL( "useskin=$skinkey" ) );
1012 $previewlink = "(<a target='_blank' href=\"$mplink\">$previewtext</a>)";
1013 $extraLinks = '';
1014 global $wgAllowUserCss, $wgAllowUserJs;
1015 if( $wgAllowUserCss ) {
1016 $cssPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.css' );
1017 $customCSS = $sk->link( $cssPage, wfMsgHtml( 'prefs-custom-css' ) );
1018 $extraLinks .= " ($customCSS)";
1019 }
1020 if( $wgAllowUserJs ) {
1021 $jsPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.js' );
1022 $customJS = $sk->link( $jsPage, wfMsgHtml( 'prefs-custom-js' ) );
1023 $extraLinks .= " ($customJS)";
1024 }
1025 if( $skinkey == $wgDefaultSkin )
1026 $sn .= ' (' . wfMsgHtml( 'default' ) . ')';
1027 $display = "$sn $previewlink{$extraLinks}";
1028 $ret[$display] = $skinkey;
1029 }
1030
1031 return $ret;
1032 }
1033
1034 static function getDateOptions() {
1035 global $wgLang;
1036 $dateopts = $wgLang->getDatePreferences();
1037
1038 $ret = array();
1039
1040 if( $dateopts ) {
1041 if ( !in_array( 'default', $dateopts ) ) {
1042 $dateopts[] = 'default'; // Make sure default is always valid
1043 // Bug 19237
1044 }
1045
1046 $idCnt = 0;
1047 $epoch = '20010115161234'; # Wikipedia day
1048 foreach( $dateopts as $key ) {
1049 if( $key == 'default' ) {
1050 $formatted = wfMsgHtml( 'datedefault' );
1051 } else {
1052 $formatted = htmlspecialchars( $wgLang->timeanddate( $epoch, false, $key ) );
1053 }
1054 $ret[$formatted] = $key;
1055 }
1056 }
1057 return $ret;
1058 }
1059
1060 static function getImageSizes() {
1061 global $wgImageLimits;
1062
1063 $ret = array();
1064
1065 foreach ( $wgImageLimits as $index => $limits ) {
1066 $display = "{$limits[0]}×{$limits[1]}" . wfMsg( 'unit-pixel' );
1067 $ret[$display] = $index;
1068 }
1069
1070 return $ret;
1071 }
1072
1073 static function getThumbSizes() {
1074 global $wgThumbLimits;
1075
1076 $ret = array();
1077
1078 foreach ( $wgThumbLimits as $index => $size ) {
1079 $display = $size . wfMsg( 'unit-pixel' );
1080 $ret[$display] = $index;
1081 }
1082
1083 return $ret;
1084 }
1085
1086 static function validateSignature( $signature, $alldata ) {
1087 global $wgParser, $wgMaxSigChars, $wgLang;
1088 if( mb_strlen( $signature ) > $wgMaxSigChars ) {
1089 return
1090 Xml::element( 'span', array( 'class' => 'error' ),
1091 wfMsgExt( 'badsiglength', 'parsemag',
1092 $wgLang->formatNum( $wgMaxSigChars )
1093 )
1094 );
1095 } elseif( !empty( $alldata['fancysig'] ) &&
1096 false === $wgParser->validateSig( $signature ) ) {
1097 return Xml::element( 'span', array( 'class' => 'error' ), wfMsg( 'badsig' ) );
1098 } else {
1099 return true;
1100 }
1101 }
1102
1103 static function cleanSignature( $signature, $alldata ) {
1104 global $wgParser;
1105 if( $alldata['fancysig'] ) {
1106 $signature = $wgParser->cleanSig( $signature );
1107 } else {
1108 // When no fancy sig used, make sure ~{3,5} get removed.
1109 $signature = $wgParser->cleanSigInSig( $signature );
1110 }
1111
1112 return $signature;
1113 }
1114
1115 static function validateEmail( $email, $alldata ) {
1116 if ( $email && !User::isValidEmailAddr( $email ) ) {
1117 return wfMsgExt( 'invalidemailaddress', 'parseinline' );
1118 }
1119
1120 global $wgEmailConfirmToEdit;
1121 if( $wgEmailConfirmToEdit && !$email ) {
1122 return wfMsgExt( 'noemailtitle', 'parseinline' );
1123 }
1124 return true;
1125 }
1126
1127 static function getFormObject( $user ) {
1128 $formDescriptor = Preferences::getPreferences( $user );
1129 $htmlForm = new PreferencesForm( $formDescriptor, 'prefs' );
1130
1131 $htmlForm->setSubmitText( wfMsg( 'saveprefs' ) );
1132 $htmlForm->setTitle( SpecialPage::getTitleFor( 'Preferences' ) );
1133 $htmlForm->setSubmitID( 'prefsubmit' );
1134 $htmlForm->setSubmitCallback( array( 'Preferences', 'tryFormSubmit' ) );
1135
1136 return $htmlForm;
1137 }
1138
1139 static function getTimezoneOptions() {
1140 $opt = array();
1141
1142 global $wgLocalTZoffset;
1143
1144 $opt[wfMsg( 'timezoneuseserverdefault' )] = "System|$wgLocalTZoffset";
1145 $opt[wfMsg( 'timezoneuseoffset' )] = 'other';
1146 $opt[wfMsg( 'guesstimezone' )] = 'guess';
1147
1148 if ( function_exists( 'timezone_identifiers_list' ) ) {
1149 # Read timezone list
1150 $tzs = timezone_identifiers_list();
1151 sort( $tzs );
1152
1153 $tzRegions = array();
1154 $tzRegions['Africa'] = wfMsg( 'timezoneregion-africa' );
1155 $tzRegions['America'] = wfMsg( 'timezoneregion-america' );
1156 $tzRegions['Antarctica'] = wfMsg( 'timezoneregion-antarctica' );
1157 $tzRegions['Arctic'] = wfMsg( 'timezoneregion-arctic' );
1158 $tzRegions['Asia'] = wfMsg( 'timezoneregion-asia' );
1159 $tzRegions['Atlantic'] = wfMsg( 'timezoneregion-atlantic' );
1160 $tzRegions['Australia'] = wfMsg( 'timezoneregion-australia' );
1161 $tzRegions['Europe'] = wfMsg( 'timezoneregion-europe' );
1162 $tzRegions['Indian'] = wfMsg( 'timezoneregion-indian' );
1163 $tzRegions['Pacific'] = wfMsg( 'timezoneregion-pacific' );
1164 asort( $tzRegions );
1165
1166 $prefill = array_fill_keys( array_values( $tzRegions ), array() );
1167 $opt = array_merge( $opt, $prefill );
1168
1169 $now = date_create( 'now' );
1170
1171 foreach ( $tzs as $tz ) {
1172 $z = explode( '/', $tz, 2 );
1173
1174 # timezone_identifiers_list() returns a number of
1175 # backwards-compatibility entries. This filters them out of the
1176 # list presented to the user.
1177 if ( count( $z ) != 2 || !array_key_exists( $z[0], $tzRegions ) )
1178 continue;
1179
1180 # Localize region
1181 $z[0] = $tzRegions[$z[0]];
1182
1183 $minDiff = floor( timezone_offset_get( timezone_open( $tz ), $now ) / 60 );
1184
1185 $display = str_replace( '_', ' ', $z[0] . '/' . $z[1] );
1186 $value = "ZoneInfo|$minDiff|$tz";
1187
1188 $opt[$z[0]][$display] = $value;
1189 }
1190 }
1191 return $opt;
1192 }
1193
1194 static function filterTimezoneInput( $tz, $alldata ) {
1195 $data = explode( '|', $tz, 3 );
1196 switch ( $data[0] ) {
1197 case 'ZoneInfo':
1198 case 'System':
1199 return $tz;
1200 default:
1201 $data = explode( ':', $tz, 2 );
1202 $minDiff = 0;
1203 if( count( $data ) == 2 ) {
1204 $data[0] = intval( $data[0] );
1205 $data[1] = intval( $data[1] );
1206 $minDiff = abs( $data[0] ) * 60 + $data[1];
1207 if ( $data[0] < 0 ) $minDiff = -$minDiff;
1208 } else {
1209 $minDiff = intval( $data[0] ) * 60;
1210 }
1211
1212 # Max is +14:00 and min is -12:00, see:
1213 # http://en.wikipedia.org/wiki/Timezone
1214 $minDiff = min( $minDiff, 840 ); # 14:00
1215 $minDiff = max( $minDiff, -720 ); # -12:00
1216 return 'Offset|'.$minDiff;
1217 }
1218 }
1219
1220 static function tryFormSubmit( $formData, $entryPoint = 'internal' ) {
1221 global $wgUser, $wgEmailAuthentication, $wgEnableEmail;
1222
1223 $result = true;
1224
1225 // Filter input
1226 foreach( array_keys( $formData ) as $name ) {
1227 if ( isset( self::$saveFilters[$name] ) ) {
1228 $formData[$name] =
1229 call_user_func( self::$saveFilters[$name], $formData[$name], $formData );
1230 }
1231 }
1232
1233 // Stuff that shouldn't be saved as a preference.
1234 $saveBlacklist = array(
1235 'realname',
1236 'emailaddress',
1237 );
1238
1239 if( $wgEnableEmail ) {
1240 $newadr = $formData['emailaddress'];
1241 $oldadr = $wgUser->getEmail();
1242 if( ( $newadr != '' ) && ( $newadr != $oldadr ) ) {
1243 # the user has supplied a new email address on the login page
1244 # new behaviour: set this new emailaddr from login-page into user database record
1245 $wgUser->setEmail( $newadr );
1246 # but flag as "dirty" = unauthenticated
1247 $wgUser->invalidateEmail();
1248 if( $wgEmailAuthentication ) {
1249 # Mail a temporary password to the dirty address.
1250 # User can come back through the confirmation URL to re-enable email.
1251 $result = $wgUser->sendConfirmationMail();
1252 if( WikiError::isError( $result ) ) {
1253 return wfMsg( 'mailerror', htmlspecialchars( $result->getMessage() ) );
1254 } elseif( $entryPoint == 'ui' ) {
1255 $result = 'eauth';
1256 }
1257 }
1258 } else {
1259 $wgUser->setEmail( $newadr );
1260 }
1261 if( $oldadr != $newadr ) {
1262 wfRunHooks( 'PrefsEmailAudit', array( $wgUser, $oldadr, $newadr ) );
1263 }
1264 }
1265
1266 // Fortunately, the realname field is MUCH simpler
1267 global $wgHiddenPrefs;
1268 if ( !in_array( 'realname', $wgHiddenPrefs ) ) {
1269 $realName = $formData['realname'];
1270 $wgUser->setRealName( $realName );
1271 }
1272
1273 foreach( $saveBlacklist as $b )
1274 unset( $formData[$b] );
1275
1276 // Keeps old preferences from interfering due to back-compat
1277 // code, etc.
1278 $wgUser->resetOptions();
1279
1280 foreach( $formData as $key => $value ) {
1281 $wgUser->setOption( $key, $value );
1282 }
1283
1284 $wgUser->saveSettings();
1285
1286 return $result;
1287 }
1288
1289 public static function tryUISubmit( $formData ) {
1290 $res = self::tryFormSubmit( $formData, 'ui' );
1291
1292 if( $res ) {
1293 $urlOptions = array( 'success' );
1294 if( $res === 'eauth' )
1295 $urlOptions[] = 'eauth';
1296
1297 $queryString = implode( '&', $urlOptions );
1298
1299 $url = SpecialPage::getTitleFor( 'Preferences' )->getFullURL( $queryString );
1300 global $wgOut;
1301 $wgOut->redirect( $url );
1302 }
1303
1304 return true;
1305 }
1306
1307 public static function loadOldSearchNs( $user ) {
1308 $searchableNamespaces = SearchEngine::searchableNamespaces();
1309 // Back compat with old format
1310 $arr = array();
1311
1312 foreach( $searchableNamespaces as $ns => $name ) {
1313 if( $user->getOption( 'searchNs' . $ns ) ) {
1314 $arr[] = $ns;
1315 }
1316 }
1317
1318 return $arr;
1319 }
1320 }
1321
1322 /** Some tweaks to allow js prefs to work */
1323 class PreferencesForm extends HTMLForm {
1324
1325 function wrapForm( $html ) {
1326 $html = Xml::tags( 'div', array( 'id' => 'preferences' ), $html );
1327
1328 return parent::wrapForm( $html );
1329 }
1330
1331 function getButtons() {
1332 $html = parent::getButtons();
1333
1334 global $wgUser;
1335
1336 $sk = $wgUser->getSkin();
1337 $t = SpecialPage::getTitleFor( 'Preferences', 'reset' );
1338
1339 $html .= "\n" . $sk->link( $t, wfMsgHtml( 'restoreprefs' ) );
1340
1341 $html = Xml::tags( 'div', array( 'class' => 'mw-prefs-buttons' ), $html );
1342
1343 return $html;
1344 }
1345
1346 function filterDataForSubmit( $data ) {
1347 // Support for separating MultiSelect preferences into multiple preferences
1348 // Due to lack of array support.
1349 foreach( $this->mFlatFields as $fieldname => $field ) {
1350 $info = $field->mParams;
1351 if( $field instanceof HTMLMultiSelectField ) {
1352 $options = HTMLFormField::flattenOptions( $info['options'] );
1353 $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $fieldname;
1354
1355 foreach( $options as $opt ) {
1356 $data["$prefix$opt"] = in_array( $opt, $data[$fieldname] );
1357 }
1358
1359 unset( $data[$fieldname] );
1360 }
1361 }
1362
1363 return $data;
1364 }
1365 }