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