Preferences.php 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650
  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. use MediaWiki\Auth\AuthManager;
  21. use MediaWiki\Auth\PasswordAuthenticationRequest;
  22. use MediaWiki\MediaWikiServices;
  23. /**
  24. * We're now using the HTMLForm object with some customisation to generate the
  25. * Preferences form. This object handles generic submission, CSRF protection,
  26. * layout and other logic in a reusable manner. We subclass it as a PreferencesForm
  27. * to make some minor customisations.
  28. *
  29. * In order to generate the form, the HTMLForm object needs an array structure
  30. * detailing the form fields available, and that's what this class is for. Each
  31. * element of the array is a basic property-list, including the type of field,
  32. * the label it is to be given in the form, callbacks for validation and
  33. * 'filtering', and other pertinent information. Note that the 'default' field
  34. * is named for generic forms, and does not represent the preference's default
  35. * (which is stored in $wgDefaultUserOptions), but the default for the form
  36. * field, which should be whatever the user has set for that preference. There
  37. * is no need to override it unless you have some special storage logic (for
  38. * instance, those not presently stored as options, but which are best set from
  39. * the user preferences view).
  40. *
  41. * Field types are implemented as subclasses of the generic HTMLFormField
  42. * object, and typically implement at least getInputHTML, which generates the
  43. * HTML for the input field to be placed in the table.
  44. *
  45. * Once fields have been retrieved and validated, submission logic is handed
  46. * over to the tryUISubmit static method of this class.
  47. */
  48. class Preferences {
  49. /** @var array */
  50. protected static $defaultPreferences = null;
  51. /** @var array */
  52. protected static $saveFilters = [
  53. 'timecorrection' => [ 'Preferences', 'filterTimezoneInput' ],
  54. 'rclimit' => [ 'Preferences', 'filterIntval' ],
  55. 'wllimit' => [ 'Preferences', 'filterIntval' ],
  56. 'searchlimit' => [ 'Preferences', 'filterIntval' ],
  57. ];
  58. // Stuff that shouldn't be saved as a preference.
  59. private static $saveBlacklist = [
  60. 'realname',
  61. 'emailaddress',
  62. ];
  63. /**
  64. * @return array
  65. */
  66. static function getSaveBlacklist() {
  67. return self::$saveBlacklist;
  68. }
  69. /**
  70. * @throws MWException
  71. * @param User $user
  72. * @param IContextSource $context
  73. * @return array|null
  74. */
  75. static function getPreferences( $user, IContextSource $context ) {
  76. if ( self::$defaultPreferences ) {
  77. return self::$defaultPreferences;
  78. }
  79. $defaultPreferences = [];
  80. self::profilePreferences( $user, $context, $defaultPreferences );
  81. self::skinPreferences( $user, $context, $defaultPreferences );
  82. self::datetimePreferences( $user, $context, $defaultPreferences );
  83. self::filesPreferences( $user, $context, $defaultPreferences );
  84. self::renderingPreferences( $user, $context, $defaultPreferences );
  85. self::editingPreferences( $user, $context, $defaultPreferences );
  86. self::rcPreferences( $user, $context, $defaultPreferences );
  87. self::watchlistPreferences( $user, $context, $defaultPreferences );
  88. self::searchPreferences( $user, $context, $defaultPreferences );
  89. self::miscPreferences( $user, $context, $defaultPreferences );
  90. Hooks::run( 'GetPreferences', [ $user, &$defaultPreferences ] );
  91. self::loadPreferenceValues( $user, $context, $defaultPreferences );
  92. self::$defaultPreferences = $defaultPreferences;
  93. return $defaultPreferences;
  94. }
  95. /**
  96. * Loads existing values for a given array of preferences
  97. * @throws MWException
  98. * @param User $user
  99. * @param IContextSource $context
  100. * @param array &$defaultPreferences Array to load values for
  101. * @return array|null
  102. */
  103. static function loadPreferenceValues( $user, $context, &$defaultPreferences ) {
  104. # # Remove preferences that wikis don't want to use
  105. foreach ( $context->getConfig()->get( 'HiddenPrefs' ) as $pref ) {
  106. if ( isset( $defaultPreferences[$pref] ) ) {
  107. unset( $defaultPreferences[$pref] );
  108. }
  109. }
  110. # # Make sure that form fields have their parent set. See T43337.
  111. $dummyForm = new HTMLForm( [], $context );
  112. $disable = !$user->isAllowed( 'editmyoptions' );
  113. $defaultOptions = User::getDefaultOptions();
  114. # # Prod in defaults from the user
  115. foreach ( $defaultPreferences as $name => &$info ) {
  116. $prefFromUser = self::getOptionFromUser( $name, $info, $user );
  117. if ( $disable && !in_array( $name, self::$saveBlacklist ) ) {
  118. $info['disabled'] = 'disabled';
  119. }
  120. $field = HTMLForm::loadInputFromParameters( $name, $info, $dummyForm ); // For validation
  121. $globalDefault = isset( $defaultOptions[$name] )
  122. ? $defaultOptions[$name]
  123. : null;
  124. // If it validates, set it as the default
  125. if ( isset( $info['default'] ) ) {
  126. // Already set, no problem
  127. continue;
  128. } elseif ( !is_null( $prefFromUser ) && // Make sure we're not just pulling nothing
  129. $field->validate( $prefFromUser, $user->getOptions() ) === true ) {
  130. $info['default'] = $prefFromUser;
  131. } elseif ( $field->validate( $globalDefault, $user->getOptions() ) === true ) {
  132. $info['default'] = $globalDefault;
  133. } else {
  134. throw new MWException( "Global default '$globalDefault' is invalid for field $name" );
  135. }
  136. }
  137. return $defaultPreferences;
  138. }
  139. /**
  140. * Pull option from a user account. Handles stuff like array-type preferences.
  141. *
  142. * @param string $name
  143. * @param array $info
  144. * @param User $user
  145. * @return array|string
  146. */
  147. static function getOptionFromUser( $name, $info, $user ) {
  148. $val = $user->getOption( $name );
  149. // Handling for multiselect preferences
  150. if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
  151. ( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) {
  152. $options = HTMLFormField::flattenOptions( $info['options'] );
  153. $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
  154. $val = [];
  155. foreach ( $options as $value ) {
  156. if ( $user->getOption( "$prefix$value" ) ) {
  157. $val[] = $value;
  158. }
  159. }
  160. }
  161. // Handling for checkmatrix preferences
  162. if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
  163. ( isset( $info['class'] ) && $info['class'] == 'HTMLCheckMatrix' ) ) {
  164. $columns = HTMLFormField::flattenOptions( $info['columns'] );
  165. $rows = HTMLFormField::flattenOptions( $info['rows'] );
  166. $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
  167. $val = [];
  168. foreach ( $columns as $column ) {
  169. foreach ( $rows as $row ) {
  170. if ( $user->getOption( "$prefix$column-$row" ) ) {
  171. $val[] = "$column-$row";
  172. }
  173. }
  174. }
  175. }
  176. return $val;
  177. }
  178. /**
  179. * @param User $user
  180. * @param IContextSource $context
  181. * @param array &$defaultPreferences
  182. * @return void
  183. */
  184. static function profilePreferences( $user, IContextSource $context, &$defaultPreferences ) {
  185. global $wgContLang, $wgParser;
  186. $authManager = AuthManager::singleton();
  187. $config = $context->getConfig();
  188. // retrieving user name for GENDER and misc.
  189. $userName = $user->getName();
  190. # # User info #####################################
  191. // Information panel
  192. $defaultPreferences['username'] = [
  193. 'type' => 'info',
  194. 'label-message' => [ 'username', $userName ],
  195. 'default' => $userName,
  196. 'section' => 'personal/info',
  197. ];
  198. $lang = $context->getLanguage();
  199. # Get groups to which the user belongs
  200. $userEffectiveGroups = $user->getEffectiveGroups();
  201. $userGroupMemberships = $user->getGroupMemberships();
  202. $userGroups = $userMembers = $userTempGroups = $userTempMembers = [];
  203. foreach ( $userEffectiveGroups as $ueg ) {
  204. if ( $ueg == '*' ) {
  205. // Skip the default * group, seems useless here
  206. continue;
  207. }
  208. if ( isset( $userGroupMemberships[$ueg] ) ) {
  209. $groupStringOrObject = $userGroupMemberships[$ueg];
  210. } else {
  211. $groupStringOrObject = $ueg;
  212. }
  213. $userG = UserGroupMembership::getLink( $groupStringOrObject, $context, 'html' );
  214. $userM = UserGroupMembership::getLink( $groupStringOrObject, $context, 'html',
  215. $userName );
  216. // Store expiring groups separately, so we can place them before non-expiring
  217. // groups in the list. This is to avoid the ambiguity of something like
  218. // "administrator, bureaucrat (until X date)" -- users might wonder whether the
  219. // expiry date applies to both groups, or just the last one
  220. if ( $groupStringOrObject instanceof UserGroupMembership &&
  221. $groupStringOrObject->getExpiry()
  222. ) {
  223. $userTempGroups[] = $userG;
  224. $userTempMembers[] = $userM;
  225. } else {
  226. $userGroups[] = $userG;
  227. $userMembers[] = $userM;
  228. }
  229. }
  230. sort( $userGroups );
  231. sort( $userMembers );
  232. sort( $userTempGroups );
  233. sort( $userTempMembers );
  234. $userGroups = array_merge( $userTempGroups, $userGroups );
  235. $userMembers = array_merge( $userTempMembers, $userMembers );
  236. $defaultPreferences['usergroups'] = [
  237. 'type' => 'info',
  238. 'label' => $context->msg( 'prefs-memberingroups' )->numParams(
  239. count( $userGroups ) )->params( $userName )->parse(),
  240. 'default' => $context->msg( 'prefs-memberingroups-type' )
  241. ->rawParams( $lang->commaList( $userGroups ), $lang->commaList( $userMembers ) )
  242. ->escaped(),
  243. 'raw' => true,
  244. 'section' => 'personal/info',
  245. ];
  246. $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
  247. $editCount = $linkRenderer->makeLink( SpecialPage::getTitleFor( "Contributions", $userName ),
  248. $lang->formatNum( $user->getEditCount() ) );
  249. $defaultPreferences['editcount'] = [
  250. 'type' => 'info',
  251. 'raw' => true,
  252. 'label-message' => 'prefs-edits',
  253. 'default' => $editCount,
  254. 'section' => 'personal/info',
  255. ];
  256. if ( $user->getRegistration() ) {
  257. $displayUser = $context->getUser();
  258. $userRegistration = $user->getRegistration();
  259. $defaultPreferences['registrationdate'] = [
  260. 'type' => 'info',
  261. 'label-message' => 'prefs-registration',
  262. 'default' => $context->msg(
  263. 'prefs-registration-date-time',
  264. $lang->userTimeAndDate( $userRegistration, $displayUser ),
  265. $lang->userDate( $userRegistration, $displayUser ),
  266. $lang->userTime( $userRegistration, $displayUser )
  267. )->parse(),
  268. 'section' => 'personal/info',
  269. ];
  270. }
  271. $canViewPrivateInfo = $user->isAllowed( 'viewmyprivateinfo' );
  272. $canEditPrivateInfo = $user->isAllowed( 'editmyprivateinfo' );
  273. // Actually changeable stuff
  274. $defaultPreferences['realname'] = [
  275. // (not really "private", but still shouldn't be edited without permission)
  276. 'type' => $canEditPrivateInfo && $authManager->allowsPropertyChange( 'realname' )
  277. ? 'text' : 'info',
  278. 'default' => $user->getRealName(),
  279. 'section' => 'personal/info',
  280. 'label-message' => 'yourrealname',
  281. 'help-message' => 'prefs-help-realname',
  282. ];
  283. if ( $canEditPrivateInfo && $authManager->allowsAuthenticationDataChange(
  284. new PasswordAuthenticationRequest(), false )->isGood()
  285. ) {
  286. $link = $linkRenderer->makeLink( SpecialPage::getTitleFor( 'ChangePassword' ),
  287. $context->msg( 'prefs-resetpass' )->text(), [],
  288. [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
  289. $defaultPreferences['password'] = [
  290. 'type' => 'info',
  291. 'raw' => true,
  292. 'default' => $link,
  293. 'label-message' => 'yourpassword',
  294. 'section' => 'personal/info',
  295. ];
  296. }
  297. // Only show prefershttps if secure login is turned on
  298. if ( $config->get( 'SecureLogin' ) && wfCanIPUseHTTPS( $context->getRequest()->getIP() ) ) {
  299. $defaultPreferences['prefershttps'] = [
  300. 'type' => 'toggle',
  301. 'label-message' => 'tog-prefershttps',
  302. 'help-message' => 'prefs-help-prefershttps',
  303. 'section' => 'personal/info'
  304. ];
  305. }
  306. // Language
  307. $languages = Language::fetchLanguageNames( null, 'mw' );
  308. $languageCode = $config->get( 'LanguageCode' );
  309. if ( !array_key_exists( $languageCode, $languages ) ) {
  310. $languages[$languageCode] = $languageCode;
  311. }
  312. ksort( $languages );
  313. $options = [];
  314. foreach ( $languages as $code => $name ) {
  315. $display = wfBCP47( $code ) . ' - ' . $name;
  316. $options[$display] = $code;
  317. }
  318. $defaultPreferences['language'] = [
  319. 'type' => 'select',
  320. 'section' => 'personal/i18n',
  321. 'options' => $options,
  322. 'label-message' => 'yourlanguage',
  323. ];
  324. $defaultPreferences['gender'] = [
  325. 'type' => 'radio',
  326. 'section' => 'personal/i18n',
  327. 'options' => [
  328. $context->msg( 'parentheses' )
  329. ->params( $context->msg( 'gender-unknown' )->plain() )
  330. ->escaped() => 'unknown',
  331. $context->msg( 'gender-female' )->escaped() => 'female',
  332. $context->msg( 'gender-male' )->escaped() => 'male',
  333. ],
  334. 'label-message' => 'yourgender',
  335. 'help-message' => 'prefs-help-gender',
  336. ];
  337. // see if there are multiple language variants to choose from
  338. if ( !$config->get( 'DisableLangConversion' ) ) {
  339. foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
  340. if ( $langCode == $wgContLang->getCode() ) {
  341. $variants = $wgContLang->getVariants();
  342. if ( count( $variants ) <= 1 ) {
  343. continue;
  344. }
  345. $variantArray = [];
  346. foreach ( $variants as $v ) {
  347. $v = str_replace( '_', '-', strtolower( $v ) );
  348. $variantArray[$v] = $lang->getVariantname( $v, false );
  349. }
  350. $options = [];
  351. foreach ( $variantArray as $code => $name ) {
  352. $display = wfBCP47( $code ) . ' - ' . $name;
  353. $options[$display] = $code;
  354. }
  355. $defaultPreferences['variant'] = [
  356. 'label-message' => 'yourvariant',
  357. 'type' => 'select',
  358. 'options' => $options,
  359. 'section' => 'personal/i18n',
  360. 'help-message' => 'prefs-help-variant',
  361. ];
  362. } else {
  363. $defaultPreferences["variant-$langCode"] = [
  364. 'type' => 'api',
  365. ];
  366. }
  367. }
  368. }
  369. // Stuff from Language::getExtraUserToggles()
  370. // FIXME is this dead code? $extraUserToggles doesn't seem to be defined for any language
  371. $toggles = $wgContLang->getExtraUserToggles();
  372. foreach ( $toggles as $toggle ) {
  373. $defaultPreferences[$toggle] = [
  374. 'type' => 'toggle',
  375. 'section' => 'personal/i18n',
  376. 'label-message' => "tog-$toggle",
  377. ];
  378. }
  379. // show a preview of the old signature first
  380. $oldsigWikiText = $wgParser->preSaveTransform(
  381. '~~~',
  382. $context->getTitle(),
  383. $user,
  384. ParserOptions::newFromContext( $context )
  385. );
  386. $oldsigHTML = $context->getOutput()->parseInline( $oldsigWikiText, true, true );
  387. $defaultPreferences['oldsig'] = [
  388. 'type' => 'info',
  389. 'raw' => true,
  390. 'label-message' => 'tog-oldsig',
  391. 'default' => $oldsigHTML,
  392. 'section' => 'personal/signature',
  393. ];
  394. $defaultPreferences['nickname'] = [
  395. 'type' => $authManager->allowsPropertyChange( 'nickname' ) ? 'text' : 'info',
  396. 'maxlength' => $config->get( 'MaxSigChars' ),
  397. 'label-message' => 'yournick',
  398. 'validation-callback' => [ 'Preferences', 'validateSignature' ],
  399. 'section' => 'personal/signature',
  400. 'filter-callback' => [ 'Preferences', 'cleanSignature' ],
  401. ];
  402. $defaultPreferences['fancysig'] = [
  403. 'type' => 'toggle',
  404. 'label-message' => 'tog-fancysig',
  405. // show general help about signature at the bottom of the section
  406. 'help-message' => 'prefs-help-signature',
  407. 'section' => 'personal/signature'
  408. ];
  409. # # Email stuff
  410. if ( $config->get( 'EnableEmail' ) ) {
  411. if ( $canViewPrivateInfo ) {
  412. $helpMessages[] = $config->get( 'EmailConfirmToEdit' )
  413. ? 'prefs-help-email-required'
  414. : 'prefs-help-email';
  415. if ( $config->get( 'EnableUserEmail' ) ) {
  416. // additional messages when users can send email to each other
  417. $helpMessages[] = 'prefs-help-email-others';
  418. }
  419. $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
  420. if ( $canEditPrivateInfo && $authManager->allowsPropertyChange( 'emailaddress' ) ) {
  421. $link = $linkRenderer->makeLink(
  422. SpecialPage::getTitleFor( 'ChangeEmail' ),
  423. $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(),
  424. [],
  425. [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
  426. $emailAddress .= $emailAddress == '' ? $link : (
  427. $context->msg( 'word-separator' )->escaped()
  428. . $context->msg( 'parentheses' )->rawParams( $link )->escaped()
  429. );
  430. }
  431. $defaultPreferences['emailaddress'] = [
  432. 'type' => 'info',
  433. 'raw' => true,
  434. 'default' => $emailAddress,
  435. 'label-message' => 'youremail',
  436. 'section' => 'personal/email',
  437. 'help-messages' => $helpMessages,
  438. # 'cssclass' chosen below
  439. ];
  440. }
  441. $disableEmailPrefs = false;
  442. if ( $config->get( 'EmailAuthentication' ) ) {
  443. $emailauthenticationclass = 'mw-email-not-authenticated';
  444. if ( $user->getEmail() ) {
  445. if ( $user->getEmailAuthenticationTimestamp() ) {
  446. // date and time are separate parameters to facilitate localisation.
  447. // $time is kept for backward compat reasons.
  448. // 'emailauthenticated' is also used in SpecialConfirmemail.php
  449. $displayUser = $context->getUser();
  450. $emailTimestamp = $user->getEmailAuthenticationTimestamp();
  451. $time = $lang->userTimeAndDate( $emailTimestamp, $displayUser );
  452. $d = $lang->userDate( $emailTimestamp, $displayUser );
  453. $t = $lang->userTime( $emailTimestamp, $displayUser );
  454. $emailauthenticated = $context->msg( 'emailauthenticated',
  455. $time, $d, $t )->parse() . '<br />';
  456. $disableEmailPrefs = false;
  457. $emailauthenticationclass = 'mw-email-authenticated';
  458. } else {
  459. $disableEmailPrefs = true;
  460. $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
  461. $linkRenderer->makeKnownLink(
  462. SpecialPage::getTitleFor( 'Confirmemail' ),
  463. $context->msg( 'emailconfirmlink' )->text()
  464. ) . '<br />';
  465. $emailauthenticationclass = "mw-email-not-authenticated";
  466. }
  467. } else {
  468. $disableEmailPrefs = true;
  469. $emailauthenticated = $context->msg( 'noemailprefs' )->escaped();
  470. $emailauthenticationclass = 'mw-email-none';
  471. }
  472. if ( $canViewPrivateInfo ) {
  473. $defaultPreferences['emailauthentication'] = [
  474. 'type' => 'info',
  475. 'raw' => true,
  476. 'section' => 'personal/email',
  477. 'label-message' => 'prefs-emailconfirm-label',
  478. 'default' => $emailauthenticated,
  479. # Apply the same CSS class used on the input to the message:
  480. 'cssclass' => $emailauthenticationclass,
  481. ];
  482. }
  483. }
  484. if ( $config->get( 'EnableUserEmail' ) && $user->isAllowed( 'sendemail' ) ) {
  485. $defaultPreferences['disablemail'] = [
  486. 'type' => 'toggle',
  487. 'invert' => true,
  488. 'section' => 'personal/email',
  489. 'label-message' => 'allowemail',
  490. 'disabled' => $disableEmailPrefs,
  491. ];
  492. $defaultPreferences['ccmeonemails'] = [
  493. 'type' => 'toggle',
  494. 'section' => 'personal/email',
  495. 'label-message' => 'tog-ccmeonemails',
  496. 'disabled' => $disableEmailPrefs,
  497. ];
  498. if ( $config->get( 'EnableUserEmailBlacklist' )
  499. && !$disableEmailPrefs
  500. && !(bool)$user->getOption( 'disablemail' )
  501. ) {
  502. $lookup = CentralIdLookup::factory();
  503. $ids = $user->getOption( 'email-blacklist', [] );
  504. $names = $ids ? $lookup->namesFromCentralIds( $ids, $user ) : [];
  505. $defaultPreferences['email-blacklist'] = [
  506. 'type' => 'usersmultiselect',
  507. 'label-message' => 'email-blacklist-label',
  508. 'section' => 'personal/email',
  509. 'default' => implode( "\n", $names ),
  510. ];
  511. }
  512. }
  513. if ( $config->get( 'EnotifWatchlist' ) ) {
  514. $defaultPreferences['enotifwatchlistpages'] = [
  515. 'type' => 'toggle',
  516. 'section' => 'personal/email',
  517. 'label-message' => 'tog-enotifwatchlistpages',
  518. 'disabled' => $disableEmailPrefs,
  519. ];
  520. }
  521. if ( $config->get( 'EnotifUserTalk' ) ) {
  522. $defaultPreferences['enotifusertalkpages'] = [
  523. 'type' => 'toggle',
  524. 'section' => 'personal/email',
  525. 'label-message' => 'tog-enotifusertalkpages',
  526. 'disabled' => $disableEmailPrefs,
  527. ];
  528. }
  529. if ( $config->get( 'EnotifUserTalk' ) || $config->get( 'EnotifWatchlist' ) ) {
  530. if ( $config->get( 'EnotifMinorEdits' ) ) {
  531. $defaultPreferences['enotifminoredits'] = [
  532. 'type' => 'toggle',
  533. 'section' => 'personal/email',
  534. 'label-message' => 'tog-enotifminoredits',
  535. 'disabled' => $disableEmailPrefs,
  536. ];
  537. }
  538. if ( $config->get( 'EnotifRevealEditorAddress' ) ) {
  539. $defaultPreferences['enotifrevealaddr'] = [
  540. 'type' => 'toggle',
  541. 'section' => 'personal/email',
  542. 'label-message' => 'tog-enotifrevealaddr',
  543. 'disabled' => $disableEmailPrefs,
  544. ];
  545. }
  546. }
  547. }
  548. }
  549. /**
  550. * @param User $user
  551. * @param IContextSource $context
  552. * @param array &$defaultPreferences
  553. * @return void
  554. */
  555. static function skinPreferences( $user, IContextSource $context, &$defaultPreferences ) {
  556. # # Skin #####################################
  557. // Skin selector, if there is at least one valid skin
  558. $skinOptions = self::generateSkinOptions( $user, $context );
  559. if ( $skinOptions ) {
  560. $defaultPreferences['skin'] = [
  561. 'type' => 'radio',
  562. 'options' => $skinOptions,
  563. 'section' => 'rendering/skin',
  564. ];
  565. }
  566. $config = $context->getConfig();
  567. $allowUserCss = $config->get( 'AllowUserCss' );
  568. $allowUserJs = $config->get( 'AllowUserJs' );
  569. # Create links to user CSS/JS pages for all skins
  570. # This code is basically copied from generateSkinOptions(). It'd
  571. # be nice to somehow merge this back in there to avoid redundancy.
  572. if ( $allowUserCss || $allowUserJs ) {
  573. $linkTools = [];
  574. $userName = $user->getName();
  575. $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
  576. if ( $allowUserCss ) {
  577. $cssPage = Title::makeTitleSafe( NS_USER, $userName . '/common.css' );
  578. $linkTools[] = $linkRenderer->makeLink( $cssPage, $context->msg( 'prefs-custom-css' )->text() );
  579. }
  580. if ( $allowUserJs ) {
  581. $jsPage = Title::makeTitleSafe( NS_USER, $userName . '/common.js' );
  582. $linkTools[] = $linkRenderer->makeLink( $jsPage, $context->msg( 'prefs-custom-js' )->text() );
  583. }
  584. $defaultPreferences['commoncssjs'] = [
  585. 'type' => 'info',
  586. 'raw' => true,
  587. 'default' => $context->getLanguage()->pipeList( $linkTools ),
  588. 'label-message' => 'prefs-common-css-js',
  589. 'section' => 'rendering/skin',
  590. ];
  591. }
  592. }
  593. /**
  594. * @param User $user
  595. * @param IContextSource $context
  596. * @param array &$defaultPreferences
  597. */
  598. static function filesPreferences( $user, IContextSource $context, &$defaultPreferences ) {
  599. # # Files #####################################
  600. $defaultPreferences['imagesize'] = [
  601. 'type' => 'select',
  602. 'options' => self::getImageSizes( $context ),
  603. 'label-message' => 'imagemaxsize',
  604. 'section' => 'rendering/files',
  605. ];
  606. $defaultPreferences['thumbsize'] = [
  607. 'type' => 'select',
  608. 'options' => self::getThumbSizes( $context ),
  609. 'label-message' => 'thumbsize',
  610. 'section' => 'rendering/files',
  611. ];
  612. }
  613. /**
  614. * @param User $user
  615. * @param IContextSource $context
  616. * @param array &$defaultPreferences
  617. * @return void
  618. */
  619. static function datetimePreferences( $user, IContextSource $context, &$defaultPreferences ) {
  620. # # Date and time #####################################
  621. $dateOptions = self::getDateOptions( $context );
  622. if ( $dateOptions ) {
  623. $defaultPreferences['date'] = [
  624. 'type' => 'radio',
  625. 'options' => $dateOptions,
  626. 'section' => 'rendering/dateformat',
  627. ];
  628. }
  629. // Info
  630. $now = wfTimestampNow();
  631. $lang = $context->getLanguage();
  632. $nowlocal = Xml::element( 'span', [ 'id' => 'wpLocalTime' ],
  633. $lang->userTime( $now, $user ) );
  634. $nowserver = $lang->userTime( $now, $user,
  635. [ 'format' => false, 'timecorrection' => false ] ) .
  636. Html::hidden( 'wpServerTime', (int)substr( $now, 8, 2 ) * 60 + (int)substr( $now, 10, 2 ) );
  637. $defaultPreferences['nowserver'] = [
  638. 'type' => 'info',
  639. 'raw' => 1,
  640. 'label-message' => 'servertime',
  641. 'default' => $nowserver,
  642. 'section' => 'rendering/timeoffset',
  643. ];
  644. $defaultPreferences['nowlocal'] = [
  645. 'type' => 'info',
  646. 'raw' => 1,
  647. 'label-message' => 'localtime',
  648. 'default' => $nowlocal,
  649. 'section' => 'rendering/timeoffset',
  650. ];
  651. // Grab existing pref.
  652. $tzOffset = $user->getOption( 'timecorrection' );
  653. $tz = explode( '|', $tzOffset, 3 );
  654. $tzOptions = self::getTimezoneOptions( $context );
  655. $tzSetting = $tzOffset;
  656. if ( count( $tz ) > 1 && $tz[0] == 'ZoneInfo' &&
  657. !in_array( $tzOffset, HTMLFormField::flattenOptions( $tzOptions ) )
  658. ) {
  659. // Timezone offset can vary with DST
  660. try {
  661. $userTZ = new DateTimeZone( $tz[2] );
  662. $minDiff = floor( $userTZ->getOffset( new DateTime( 'now' ) ) / 60 );
  663. $tzSetting = "ZoneInfo|$minDiff|{$tz[2]}";
  664. } catch ( Exception $e ) {
  665. // User has an invalid time zone set. Fall back to just using the offset
  666. $tz[0] = 'Offset';
  667. }
  668. }
  669. if ( count( $tz ) > 1 && $tz[0] == 'Offset' ) {
  670. $minDiff = $tz[1];
  671. $tzSetting = sprintf( '%+03d:%02d', floor( $minDiff / 60 ), abs( $minDiff ) % 60 );
  672. }
  673. $defaultPreferences['timecorrection'] = [
  674. 'class' => 'HTMLSelectOrOtherField',
  675. 'label-message' => 'timezonelegend',
  676. 'options' => $tzOptions,
  677. 'default' => $tzSetting,
  678. 'size' => 20,
  679. 'section' => 'rendering/timeoffset',
  680. ];
  681. }
  682. /**
  683. * @param User $user
  684. * @param IContextSource $context
  685. * @param array &$defaultPreferences
  686. */
  687. static function renderingPreferences( $user, IContextSource $context, &$defaultPreferences ) {
  688. # # Diffs ####################################
  689. $defaultPreferences['diffonly'] = [
  690. 'type' => 'toggle',
  691. 'section' => 'rendering/diffs',
  692. 'label-message' => 'tog-diffonly',
  693. ];
  694. $defaultPreferences['norollbackdiff'] = [
  695. 'type' => 'toggle',
  696. 'section' => 'rendering/diffs',
  697. 'label-message' => 'tog-norollbackdiff',
  698. ];
  699. # # Page Rendering ##############################
  700. if ( $context->getConfig()->get( 'AllowUserCssPrefs' ) ) {
  701. $defaultPreferences['underline'] = [
  702. 'type' => 'select',
  703. 'options' => [
  704. $context->msg( 'underline-never' )->text() => 0,
  705. $context->msg( 'underline-always' )->text() => 1,
  706. $context->msg( 'underline-default' )->text() => 2,
  707. ],
  708. 'label-message' => 'tog-underline',
  709. 'section' => 'rendering/advancedrendering',
  710. ];
  711. }
  712. $stubThresholdValues = [ 50, 100, 500, 1000, 2000, 5000, 10000 ];
  713. $stubThresholdOptions = [ $context->msg( 'stub-threshold-disabled' )->text() => 0 ];
  714. foreach ( $stubThresholdValues as $value ) {
  715. $stubThresholdOptions[$context->msg( 'size-bytes', $value )->text()] = $value;
  716. }
  717. $defaultPreferences['stubthreshold'] = [
  718. 'type' => 'select',
  719. 'section' => 'rendering/advancedrendering',
  720. 'options' => $stubThresholdOptions,
  721. // This is not a raw HTML message; label-raw is needed for the manual <a></a>
  722. 'label-raw' => $context->msg( 'stub-threshold' )->rawParams(
  723. '<a href="#" class="stub">' .
  724. $context->msg( 'stub-threshold-sample-link' )->parse() .
  725. '</a>' )->parse(),
  726. ];
  727. $defaultPreferences['showhiddencats'] = [
  728. 'type' => 'toggle',
  729. 'section' => 'rendering/advancedrendering',
  730. 'label-message' => 'tog-showhiddencats'
  731. ];
  732. $defaultPreferences['numberheadings'] = [
  733. 'type' => 'toggle',
  734. 'section' => 'rendering/advancedrendering',
  735. 'label-message' => 'tog-numberheadings',
  736. ];
  737. }
  738. /**
  739. * @param User $user
  740. * @param IContextSource $context
  741. * @param array &$defaultPreferences
  742. */
  743. static function editingPreferences( $user, IContextSource $context, &$defaultPreferences ) {
  744. # # Editing #####################################
  745. $defaultPreferences['editsectiononrightclick'] = [
  746. 'type' => 'toggle',
  747. 'section' => 'editing/advancedediting',
  748. 'label-message' => 'tog-editsectiononrightclick',
  749. ];
  750. $defaultPreferences['editondblclick'] = [
  751. 'type' => 'toggle',
  752. 'section' => 'editing/advancedediting',
  753. 'label-message' => 'tog-editondblclick',
  754. ];
  755. if ( $context->getConfig()->get( 'AllowUserCssPrefs' ) ) {
  756. $defaultPreferences['editfont'] = [
  757. 'type' => 'select',
  758. 'section' => 'editing/editor',
  759. 'label-message' => 'editfont-style',
  760. 'options' => [
  761. $context->msg( 'editfont-monospace' )->text() => 'monospace',
  762. $context->msg( 'editfont-sansserif' )->text() => 'sans-serif',
  763. $context->msg( 'editfont-serif' )->text() => 'serif',
  764. $context->msg( 'editfont-default' )->text() => 'default',
  765. ]
  766. ];
  767. }
  768. if ( $user->isAllowed( 'minoredit' ) ) {
  769. $defaultPreferences['minordefault'] = [
  770. 'type' => 'toggle',
  771. 'section' => 'editing/editor',
  772. 'label-message' => 'tog-minordefault',
  773. ];
  774. }
  775. $defaultPreferences['forceeditsummary'] = [
  776. 'type' => 'toggle',
  777. 'section' => 'editing/editor',
  778. 'label-message' => 'tog-forceeditsummary',
  779. ];
  780. $defaultPreferences['useeditwarning'] = [
  781. 'type' => 'toggle',
  782. 'section' => 'editing/editor',
  783. 'label-message' => 'tog-useeditwarning',
  784. ];
  785. $defaultPreferences['showtoolbar'] = [
  786. 'type' => 'toggle',
  787. 'section' => 'editing/editor',
  788. 'label-message' => 'tog-showtoolbar',
  789. ];
  790. $defaultPreferences['previewonfirst'] = [
  791. 'type' => 'toggle',
  792. 'section' => 'editing/preview',
  793. 'label-message' => 'tog-previewonfirst',
  794. ];
  795. $defaultPreferences['previewontop'] = [
  796. 'type' => 'toggle',
  797. 'section' => 'editing/preview',
  798. 'label-message' => 'tog-previewontop',
  799. ];
  800. $defaultPreferences['uselivepreview'] = [
  801. 'type' => 'toggle',
  802. 'section' => 'editing/preview',
  803. 'label-message' => 'tog-uselivepreview',
  804. ];
  805. }
  806. /**
  807. * @param User $user
  808. * @param IContextSource $context
  809. * @param array &$defaultPreferences
  810. */
  811. static function rcPreferences( $user, IContextSource $context, &$defaultPreferences ) {
  812. $config = $context->getConfig();
  813. $rcMaxAge = $config->get( 'RCMaxAge' );
  814. # # RecentChanges #####################################
  815. $defaultPreferences['rcdays'] = [
  816. 'type' => 'float',
  817. 'label-message' => 'recentchangesdays',
  818. 'section' => 'rc/displayrc',
  819. 'min' => 1,
  820. 'max' => ceil( $rcMaxAge / ( 3600 * 24 ) ),
  821. 'help' => $context->msg( 'recentchangesdays-max' )->numParams(
  822. ceil( $rcMaxAge / ( 3600 * 24 ) ) )->escaped()
  823. ];
  824. $defaultPreferences['rclimit'] = [
  825. 'type' => 'int',
  826. 'min' => 0,
  827. 'max' => 1000,
  828. 'label-message' => 'recentchangescount',
  829. 'help-message' => 'prefs-help-recentchangescount',
  830. 'section' => 'rc/displayrc',
  831. ];
  832. $defaultPreferences['usenewrc'] = [
  833. 'type' => 'toggle',
  834. 'label-message' => 'tog-usenewrc',
  835. 'section' => 'rc/advancedrc',
  836. ];
  837. $defaultPreferences['hideminor'] = [
  838. 'type' => 'toggle',
  839. 'label-message' => 'tog-hideminor',
  840. 'section' => 'rc/advancedrc',
  841. ];
  842. $defaultPreferences['rcfilters-saved-queries'] = [
  843. 'type' => 'api',
  844. ];
  845. $defaultPreferences['rcfilters-wl-saved-queries'] = [
  846. 'type' => 'api',
  847. ];
  848. $defaultPreferences['rcfilters-rclimit'] = [
  849. 'type' => 'api',
  850. ];
  851. if ( $config->get( 'RCWatchCategoryMembership' ) ) {
  852. $defaultPreferences['hidecategorization'] = [
  853. 'type' => 'toggle',
  854. 'label-message' => 'tog-hidecategorization',
  855. 'section' => 'rc/advancedrc',
  856. ];
  857. }
  858. if ( $user->useRCPatrol() ) {
  859. $defaultPreferences['hidepatrolled'] = [
  860. 'type' => 'toggle',
  861. 'section' => 'rc/advancedrc',
  862. 'label-message' => 'tog-hidepatrolled',
  863. ];
  864. }
  865. if ( $user->useNPPatrol() ) {
  866. $defaultPreferences['newpageshidepatrolled'] = [
  867. 'type' => 'toggle',
  868. 'section' => 'rc/advancedrc',
  869. 'label-message' => 'tog-newpageshidepatrolled',
  870. ];
  871. }
  872. if ( $config->get( 'RCShowWatchingUsers' ) ) {
  873. $defaultPreferences['shownumberswatching'] = [
  874. 'type' => 'toggle',
  875. 'section' => 'rc/advancedrc',
  876. 'label-message' => 'tog-shownumberswatching',
  877. ];
  878. }
  879. if ( $config->get( 'StructuredChangeFiltersShowPreference' ) ) {
  880. $defaultPreferences['rcenhancedfilters-disable'] = [
  881. 'type' => 'toggle',
  882. 'section' => 'rc/opt-out',
  883. 'label-message' => 'rcfilters-preference-label',
  884. 'help-message' => 'rcfilters-preference-help',
  885. ];
  886. }
  887. }
  888. /**
  889. * @param User $user
  890. * @param IContextSource $context
  891. * @param array &$defaultPreferences
  892. */
  893. static function watchlistPreferences( $user, IContextSource $context, &$defaultPreferences ) {
  894. $config = $context->getConfig();
  895. $watchlistdaysMax = ceil( $config->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
  896. # # Watchlist #####################################
  897. if ( $user->isAllowed( 'editmywatchlist' ) ) {
  898. $editWatchlistLinks = [];
  899. $editWatchlistModes = [
  900. 'edit' => [ 'EditWatchlist', false ],
  901. 'raw' => [ 'EditWatchlist', 'raw' ],
  902. 'clear' => [ 'EditWatchlist', 'clear' ],
  903. ];
  904. $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
  905. foreach ( $editWatchlistModes as $editWatchlistMode => $mode ) {
  906. // Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear
  907. $editWatchlistLinks[] = $linkRenderer->makeKnownLink(
  908. SpecialPage::getTitleFor( $mode[0], $mode[1] ),
  909. new HtmlArmor( $context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse() )
  910. );
  911. }
  912. $defaultPreferences['editwatchlist'] = [
  913. 'type' => 'info',
  914. 'raw' => true,
  915. 'default' => $context->getLanguage()->pipeList( $editWatchlistLinks ),
  916. 'label-message' => 'prefs-editwatchlist-label',
  917. 'section' => 'watchlist/editwatchlist',
  918. ];
  919. }
  920. $defaultPreferences['watchlistdays'] = [
  921. 'type' => 'float',
  922. 'min' => 0,
  923. 'max' => $watchlistdaysMax,
  924. 'section' => 'watchlist/displaywatchlist',
  925. 'help' => $context->msg( 'prefs-watchlist-days-max' )->numParams(
  926. $watchlistdaysMax )->escaped(),
  927. 'label-message' => 'prefs-watchlist-days',
  928. ];
  929. $defaultPreferences['wllimit'] = [
  930. 'type' => 'int',
  931. 'min' => 0,
  932. 'max' => 1000,
  933. 'label-message' => 'prefs-watchlist-edits',
  934. 'help' => $context->msg( 'prefs-watchlist-edits-max' )->escaped(),
  935. 'section' => 'watchlist/displaywatchlist',
  936. ];
  937. $defaultPreferences['extendwatchlist'] = [
  938. 'type' => 'toggle',
  939. 'section' => 'watchlist/advancedwatchlist',
  940. 'label-message' => 'tog-extendwatchlist',
  941. ];
  942. $defaultPreferences['watchlisthideminor'] = [
  943. 'type' => 'toggle',
  944. 'section' => 'watchlist/advancedwatchlist',
  945. 'label-message' => 'tog-watchlisthideminor',
  946. ];
  947. $defaultPreferences['watchlisthidebots'] = [
  948. 'type' => 'toggle',
  949. 'section' => 'watchlist/advancedwatchlist',
  950. 'label-message' => 'tog-watchlisthidebots',
  951. ];
  952. $defaultPreferences['watchlisthideown'] = [
  953. 'type' => 'toggle',
  954. 'section' => 'watchlist/advancedwatchlist',
  955. 'label-message' => 'tog-watchlisthideown',
  956. ];
  957. $defaultPreferences['watchlisthideanons'] = [
  958. 'type' => 'toggle',
  959. 'section' => 'watchlist/advancedwatchlist',
  960. 'label-message' => 'tog-watchlisthideanons',
  961. ];
  962. $defaultPreferences['watchlisthideliu'] = [
  963. 'type' => 'toggle',
  964. 'section' => 'watchlist/advancedwatchlist',
  965. 'label-message' => 'tog-watchlisthideliu',
  966. ];
  967. $defaultPreferences['watchlistreloadautomatically'] = [
  968. 'type' => 'toggle',
  969. 'section' => 'watchlist/advancedwatchlist',
  970. 'label-message' => 'tog-watchlistreloadautomatically',
  971. ];
  972. $defaultPreferences['watchlistunwatchlinks'] = [
  973. 'type' => 'toggle',
  974. 'section' => 'watchlist/advancedwatchlist',
  975. 'label-message' => 'tog-watchlistunwatchlinks',
  976. ];
  977. if ( $config->get( 'RCWatchCategoryMembership' ) ) {
  978. $defaultPreferences['watchlisthidecategorization'] = [
  979. 'type' => 'toggle',
  980. 'section' => 'watchlist/advancedwatchlist',
  981. 'label-message' => 'tog-watchlisthidecategorization',
  982. ];
  983. }
  984. if ( $user->useRCPatrol() ) {
  985. $defaultPreferences['watchlisthidepatrolled'] = [
  986. 'type' => 'toggle',
  987. 'section' => 'watchlist/advancedwatchlist',
  988. 'label-message' => 'tog-watchlisthidepatrolled',
  989. ];
  990. }
  991. $watchTypes = [
  992. 'edit' => 'watchdefault',
  993. 'move' => 'watchmoves',
  994. 'delete' => 'watchdeletion'
  995. ];
  996. // Kinda hacky
  997. if ( $user->isAllowed( 'createpage' ) || $user->isAllowed( 'createtalk' ) ) {
  998. $watchTypes['read'] = 'watchcreations';
  999. }
  1000. if ( $user->isAllowed( 'rollback' ) ) {
  1001. $watchTypes['rollback'] = 'watchrollback';
  1002. }
  1003. if ( $user->isAllowed( 'upload' ) ) {
  1004. $watchTypes['upload'] = 'watchuploads';
  1005. }
  1006. foreach ( $watchTypes as $action => $pref ) {
  1007. if ( $user->isAllowed( $action ) ) {
  1008. // Messages:
  1009. // tog-watchdefault, tog-watchmoves, tog-watchdeletion, tog-watchcreations, tog-watchuploads
  1010. // tog-watchrollback
  1011. $defaultPreferences[$pref] = [
  1012. 'type' => 'toggle',
  1013. 'section' => 'watchlist/advancedwatchlist',
  1014. 'label-message' => "tog-$pref",
  1015. ];
  1016. }
  1017. }
  1018. if ( $config->get( 'EnableAPI' ) ) {
  1019. $defaultPreferences['watchlisttoken'] = [
  1020. 'type' => 'api',
  1021. ];
  1022. $defaultPreferences['watchlisttoken-info'] = [
  1023. 'type' => 'info',
  1024. 'section' => 'watchlist/tokenwatchlist',
  1025. 'label-message' => 'prefs-watchlist-token',
  1026. 'default' => $user->getTokenFromOption( 'watchlisttoken' ),
  1027. 'help-message' => 'prefs-help-watchlist-token2',
  1028. ];
  1029. }
  1030. }
  1031. /**
  1032. * @param User $user
  1033. * @param IContextSource $context
  1034. * @param array &$defaultPreferences
  1035. */
  1036. static function searchPreferences( $user, IContextSource $context, &$defaultPreferences ) {
  1037. foreach ( MWNamespace::getValidNamespaces() as $n ) {
  1038. $defaultPreferences['searchNs' . $n] = [
  1039. 'type' => 'api',
  1040. ];
  1041. }
  1042. }
  1043. /**
  1044. * Dummy, kept for backwards-compatibility.
  1045. * @param User $user
  1046. * @param IContextSource $context
  1047. * @param array &$defaultPreferences
  1048. */
  1049. static function miscPreferences( $user, IContextSource $context, &$defaultPreferences ) {
  1050. }
  1051. /**
  1052. * @param User $user The User object
  1053. * @param IContextSource $context
  1054. * @return array Text/links to display as key; $skinkey as value
  1055. */
  1056. static function generateSkinOptions( $user, IContextSource $context ) {
  1057. $ret = [];
  1058. $mptitle = Title::newMainPage();
  1059. $previewtext = $context->msg( 'skin-preview' )->escaped();
  1060. $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
  1061. # Only show skins that aren't disabled in $wgSkipSkins
  1062. $validSkinNames = Skin::getAllowedSkins();
  1063. # Sort by UI skin name. First though need to update validSkinNames as sometimes
  1064. # the skinkey & UI skinname differ (e.g. "standard" skinkey is "Classic" in the UI).
  1065. foreach ( $validSkinNames as $skinkey => &$skinname ) {
  1066. $msg = $context->msg( "skinname-{$skinkey}" );
  1067. if ( $msg->exists() ) {
  1068. $skinname = htmlspecialchars( $msg->text() );
  1069. }
  1070. }
  1071. asort( $validSkinNames );
  1072. $config = $context->getConfig();
  1073. $defaultSkin = $config->get( 'DefaultSkin' );
  1074. $allowUserCss = $config->get( 'AllowUserCss' );
  1075. $allowUserJs = $config->get( 'AllowUserJs' );
  1076. $foundDefault = false;
  1077. foreach ( $validSkinNames as $skinkey => $sn ) {
  1078. $linkTools = [];
  1079. # Mark the default skin
  1080. if ( strcasecmp( $skinkey, $defaultSkin ) === 0 ) {
  1081. $linkTools[] = $context->msg( 'default' )->escaped();
  1082. $foundDefault = true;
  1083. }
  1084. # Create preview link
  1085. $mplink = htmlspecialchars( $mptitle->getLocalURL( [ 'useskin' => $skinkey ] ) );
  1086. $linkTools[] = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
  1087. # Create links to user CSS/JS pages
  1088. if ( $allowUserCss ) {
  1089. $cssPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.css' );
  1090. $linkTools[] = $linkRenderer->makeLink( $cssPage, $context->msg( 'prefs-custom-css' )->text() );
  1091. }
  1092. if ( $allowUserJs ) {
  1093. $jsPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.js' );
  1094. $linkTools[] = $linkRenderer->makeLink( $jsPage, $context->msg( 'prefs-custom-js' )->text() );
  1095. }
  1096. $display = $sn . ' ' . $context->msg( 'parentheses' )
  1097. ->rawParams( $context->getLanguage()->pipeList( $linkTools ) )
  1098. ->escaped();
  1099. $ret[$display] = $skinkey;
  1100. }
  1101. if ( !$foundDefault ) {
  1102. // If the default skin is not available, things are going to break horribly because the
  1103. // default value for skin selector will not be a valid value. Let's just not show it then.
  1104. return [];
  1105. }
  1106. return $ret;
  1107. }
  1108. /**
  1109. * @param IContextSource $context
  1110. * @return array
  1111. */
  1112. static function getDateOptions( IContextSource $context ) {
  1113. $lang = $context->getLanguage();
  1114. $dateopts = $lang->getDatePreferences();
  1115. $ret = [];
  1116. if ( $dateopts ) {
  1117. if ( !in_array( 'default', $dateopts ) ) {
  1118. $dateopts[] = 'default'; // Make sure default is always valid T21237
  1119. }
  1120. // FIXME KLUGE: site default might not be valid for user language
  1121. global $wgDefaultUserOptions;
  1122. if ( !in_array( $wgDefaultUserOptions['date'], $dateopts ) ) {
  1123. $wgDefaultUserOptions['date'] = 'default';
  1124. }
  1125. $epoch = wfTimestampNow();
  1126. foreach ( $dateopts as $key ) {
  1127. if ( $key == 'default' ) {
  1128. $formatted = $context->msg( 'datedefault' )->escaped();
  1129. } else {
  1130. $formatted = htmlspecialchars( $lang->timeanddate( $epoch, false, $key ) );
  1131. }
  1132. $ret[$formatted] = $key;
  1133. }
  1134. }
  1135. return $ret;
  1136. }
  1137. /**
  1138. * @param IContextSource $context
  1139. * @return array
  1140. */
  1141. static function getImageSizes( IContextSource $context ) {
  1142. $ret = [];
  1143. $pixels = $context->msg( 'unit-pixel' )->text();
  1144. foreach ( $context->getConfig()->get( 'ImageLimits' ) as $index => $limits ) {
  1145. // Note: A left-to-right marker (\u200e) is inserted, see T144386
  1146. $display = "{$limits[0]}" . json_decode( '"\u200e"' ) . "×{$limits[1]}" . $pixels;
  1147. $ret[$display] = $index;
  1148. }
  1149. return $ret;
  1150. }
  1151. /**
  1152. * @param IContextSource $context
  1153. * @return array
  1154. */
  1155. static function getThumbSizes( IContextSource $context ) {
  1156. $ret = [];
  1157. $pixels = $context->msg( 'unit-pixel' )->text();
  1158. foreach ( $context->getConfig()->get( 'ThumbLimits' ) as $index => $size ) {
  1159. $display = $size . $pixels;
  1160. $ret[$display] = $index;
  1161. }
  1162. return $ret;
  1163. }
  1164. /**
  1165. * @param string $signature
  1166. * @param array $alldata
  1167. * @param HTMLForm $form
  1168. * @return bool|string
  1169. */
  1170. static function validateSignature( $signature, $alldata, $form ) {
  1171. global $wgParser;
  1172. $maxSigChars = $form->getConfig()->get( 'MaxSigChars' );
  1173. if ( mb_strlen( $signature ) > $maxSigChars ) {
  1174. return Xml::element( 'span', [ 'class' => 'error' ],
  1175. $form->msg( 'badsiglength' )->numParams( $maxSigChars )->text() );
  1176. } elseif ( isset( $alldata['fancysig'] ) &&
  1177. $alldata['fancysig'] &&
  1178. $wgParser->validateSig( $signature ) === false
  1179. ) {
  1180. return Xml::element(
  1181. 'span',
  1182. [ 'class' => 'error' ],
  1183. $form->msg( 'badsig' )->text()
  1184. );
  1185. } else {
  1186. return true;
  1187. }
  1188. }
  1189. /**
  1190. * @param string $signature
  1191. * @param array $alldata
  1192. * @param HTMLForm $form
  1193. * @return string
  1194. */
  1195. static function cleanSignature( $signature, $alldata, $form ) {
  1196. if ( isset( $alldata['fancysig'] ) && $alldata['fancysig'] ) {
  1197. global $wgParser;
  1198. $signature = $wgParser->cleanSig( $signature );
  1199. } else {
  1200. // When no fancy sig used, make sure ~{3,5} get removed.
  1201. $signature = Parser::cleanSigInSig( $signature );
  1202. }
  1203. return $signature;
  1204. }
  1205. /**
  1206. * @param User $user
  1207. * @param IContextSource $context
  1208. * @param string $formClass
  1209. * @param array $remove Array of items to remove
  1210. * @return PreferencesForm|HtmlForm
  1211. */
  1212. static function getFormObject(
  1213. $user,
  1214. IContextSource $context,
  1215. $formClass = 'PreferencesForm',
  1216. array $remove = []
  1217. ) {
  1218. $formDescriptor = self::getPreferences( $user, $context );
  1219. if ( count( $remove ) ) {
  1220. $removeKeys = array_flip( $remove );
  1221. $formDescriptor = array_diff_key( $formDescriptor, $removeKeys );
  1222. }
  1223. // Remove type=api preferences. They are not intended for rendering in the form.
  1224. foreach ( $formDescriptor as $name => $info ) {
  1225. if ( isset( $info['type'] ) && $info['type'] === 'api' ) {
  1226. unset( $formDescriptor[$name] );
  1227. }
  1228. }
  1229. /**
  1230. * @var $htmlForm PreferencesForm
  1231. */
  1232. $htmlForm = new $formClass( $formDescriptor, $context, 'prefs' );
  1233. $htmlForm->setModifiedUser( $user );
  1234. $htmlForm->setId( 'mw-prefs-form' );
  1235. $htmlForm->setAutocomplete( 'off' );
  1236. $htmlForm->setSubmitText( $context->msg( 'saveprefs' )->text() );
  1237. # Used message keys: 'accesskey-preferences-save', 'tooltip-preferences-save'
  1238. $htmlForm->setSubmitTooltip( 'preferences-save' );
  1239. $htmlForm->setSubmitID( 'prefcontrol' );
  1240. $htmlForm->setSubmitCallback( [ 'Preferences', 'tryFormSubmit' ] );
  1241. return $htmlForm;
  1242. }
  1243. /**
  1244. * @param IContextSource $context
  1245. * @return array
  1246. */
  1247. static function getTimezoneOptions( IContextSource $context ) {
  1248. $opt = [];
  1249. $localTZoffset = $context->getConfig()->get( 'LocalTZoffset' );
  1250. $timeZoneList = self::getTimeZoneList( $context->getLanguage() );
  1251. $timestamp = MWTimestamp::getLocalInstance();
  1252. // Check that the LocalTZoffset is the same as the local time zone offset
  1253. if ( $localTZoffset == $timestamp->format( 'Z' ) / 60 ) {
  1254. $timezoneName = $timestamp->getTimezone()->getName();
  1255. // Localize timezone
  1256. if ( isset( $timeZoneList[$timezoneName] ) ) {
  1257. $timezoneName = $timeZoneList[$timezoneName]['name'];
  1258. }
  1259. $server_tz_msg = $context->msg(
  1260. 'timezoneuseserverdefault',
  1261. $timezoneName
  1262. )->text();
  1263. } else {
  1264. $tzstring = sprintf(
  1265. '%+03d:%02d',
  1266. floor( $localTZoffset / 60 ),
  1267. abs( $localTZoffset ) % 60
  1268. );
  1269. $server_tz_msg = $context->msg( 'timezoneuseserverdefault', $tzstring )->text();
  1270. }
  1271. $opt[$server_tz_msg] = "System|$localTZoffset";
  1272. $opt[$context->msg( 'timezoneuseoffset' )->text()] = 'other';
  1273. $opt[$context->msg( 'guesstimezone' )->text()] = 'guess';
  1274. foreach ( $timeZoneList as $timeZoneInfo ) {
  1275. $region = $timeZoneInfo['region'];
  1276. if ( !isset( $opt[$region] ) ) {
  1277. $opt[$region] = [];
  1278. }
  1279. $opt[$region][$timeZoneInfo['name']] = $timeZoneInfo['timecorrection'];
  1280. }
  1281. return $opt;
  1282. }
  1283. /**
  1284. * @param string $value
  1285. * @param array $alldata
  1286. * @return int
  1287. */
  1288. static function filterIntval( $value, $alldata ) {
  1289. return intval( $value );
  1290. }
  1291. /**
  1292. * @param string $tz
  1293. * @param array $alldata
  1294. * @return string
  1295. */
  1296. static function filterTimezoneInput( $tz, $alldata ) {
  1297. $data = explode( '|', $tz, 3 );
  1298. switch ( $data[0] ) {
  1299. case 'ZoneInfo':
  1300. $valid = false;
  1301. if ( count( $data ) === 3 ) {
  1302. // Make sure this timezone exists
  1303. try {
  1304. new DateTimeZone( $data[2] );
  1305. // If the constructor didn't throw, we know it's valid
  1306. $valid = true;
  1307. } catch ( Exception $e ) {
  1308. // Not a valid timezone
  1309. }
  1310. }
  1311. if ( !$valid ) {
  1312. // If the supplied timezone doesn't exist, fall back to the encoded offset
  1313. return 'Offset|' . intval( $tz[1] );
  1314. }
  1315. return $tz;
  1316. case 'System':
  1317. return $tz;
  1318. default:
  1319. $data = explode( ':', $tz, 2 );
  1320. if ( count( $data ) == 2 ) {
  1321. $data[0] = intval( $data[0] );
  1322. $data[1] = intval( $data[1] );
  1323. $minDiff = abs( $data[0] ) * 60 + $data[1];
  1324. if ( $data[0] < 0 ) {
  1325. $minDiff = - $minDiff;
  1326. }
  1327. } else {
  1328. $minDiff = intval( $data[0] ) * 60;
  1329. }
  1330. # Max is +14:00 and min is -12:00, see:
  1331. # https://en.wikipedia.org/wiki/Timezone
  1332. $minDiff = min( $minDiff, 840 ); # 14:00
  1333. $minDiff = max( $minDiff, -720 ); # -12:00
  1334. return 'Offset|' . $minDiff;
  1335. }
  1336. }
  1337. /**
  1338. * Handle the form submission if everything validated properly
  1339. *
  1340. * @param array $formData
  1341. * @param PreferencesForm $form
  1342. * @return bool|Status|string
  1343. */
  1344. static function tryFormSubmit( $formData, $form ) {
  1345. $user = $form->getModifiedUser();
  1346. $hiddenPrefs = $form->getConfig()->get( 'HiddenPrefs' );
  1347. $result = true;
  1348. if ( !$user->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
  1349. return Status::newFatal( 'mypreferencesprotected' );
  1350. }
  1351. // Filter input
  1352. foreach ( array_keys( $formData ) as $name ) {
  1353. if ( isset( self::$saveFilters[$name] ) ) {
  1354. $formData[$name] =
  1355. call_user_func( self::$saveFilters[$name], $formData[$name], $formData );
  1356. }
  1357. }
  1358. // Fortunately, the realname field is MUCH simpler
  1359. // (not really "private", but still shouldn't be edited without permission)
  1360. if ( !in_array( 'realname', $hiddenPrefs )
  1361. && $user->isAllowed( 'editmyprivateinfo' )
  1362. && array_key_exists( 'realname', $formData )
  1363. ) {
  1364. $realName = $formData['realname'];
  1365. $user->setRealName( $realName );
  1366. }
  1367. if ( $user->isAllowed( 'editmyoptions' ) ) {
  1368. $oldUserOptions = $user->getOptions();
  1369. foreach ( self::$saveBlacklist as $b ) {
  1370. unset( $formData[$b] );
  1371. }
  1372. # If users have saved a value for a preference which has subsequently been disabled
  1373. # via $wgHiddenPrefs, we don't want to destroy that setting in case the preference
  1374. # is subsequently re-enabled
  1375. foreach ( $hiddenPrefs as $pref ) {
  1376. # If the user has not set a non-default value here, the default will be returned
  1377. # and subsequently discarded
  1378. $formData[$pref] = $user->getOption( $pref, null, true );
  1379. }
  1380. // Keep old preferences from interfering due to back-compat code, etc.
  1381. $user->resetOptions( 'unused', $form->getContext() );
  1382. foreach ( $formData as $key => $value ) {
  1383. $user->setOption( $key, $value );
  1384. }
  1385. Hooks::run(
  1386. 'PreferencesFormPreSave',
  1387. [ $formData, $form, $user, &$result, $oldUserOptions ]
  1388. );
  1389. }
  1390. MediaWiki\Auth\AuthManager::callLegacyAuthPlugin( 'updateExternalDB', [ $user ] );
  1391. $user->saveSettings();
  1392. return $result;
  1393. }
  1394. /**
  1395. * @param array $formData
  1396. * @param PreferencesForm $form
  1397. * @return Status
  1398. */
  1399. public static function tryUISubmit( $formData, $form ) {
  1400. $res = self::tryFormSubmit( $formData, $form );
  1401. if ( $res ) {
  1402. $urlOptions = [];
  1403. if ( $res === 'eauth' ) {
  1404. $urlOptions['eauth'] = 1;
  1405. }
  1406. $urlOptions += $form->getExtraSuccessRedirectParameters();
  1407. $url = $form->getTitle()->getFullURL( $urlOptions );
  1408. $context = $form->getContext();
  1409. // Set session data for the success message
  1410. $context->getRequest()->getSession()->set( 'specialPreferencesSaveSuccess', 1 );
  1411. $context->getOutput()->redirect( $url );
  1412. }
  1413. return Status::newGood();
  1414. }
  1415. /**
  1416. * Get a list of all time zones
  1417. * @param Language $language Language used for the localized names
  1418. * @return array A list of all time zones. The system name of the time zone is used as key and
  1419. * the value is an array which contains localized name, the timecorrection value used for
  1420. * preferences and the region
  1421. * @since 1.26
  1422. */
  1423. public static function getTimeZoneList( Language $language ) {
  1424. $identifiers = DateTimeZone::listIdentifiers();
  1425. if ( $identifiers === false ) {
  1426. return [];
  1427. }
  1428. sort( $identifiers );
  1429. $tzRegions = [
  1430. 'Africa' => wfMessage( 'timezoneregion-africa' )->inLanguage( $language )->text(),
  1431. 'America' => wfMessage( 'timezoneregion-america' )->inLanguage( $language )->text(),
  1432. 'Antarctica' => wfMessage( 'timezoneregion-antarctica' )->inLanguage( $language )->text(),
  1433. 'Arctic' => wfMessage( 'timezoneregion-arctic' )->inLanguage( $language )->text(),
  1434. 'Asia' => wfMessage( 'timezoneregion-asia' )->inLanguage( $language )->text(),
  1435. 'Atlantic' => wfMessage( 'timezoneregion-atlantic' )->inLanguage( $language )->text(),
  1436. 'Australia' => wfMessage( 'timezoneregion-australia' )->inLanguage( $language )->text(),
  1437. 'Europe' => wfMessage( 'timezoneregion-europe' )->inLanguage( $language )->text(),
  1438. 'Indian' => wfMessage( 'timezoneregion-indian' )->inLanguage( $language )->text(),
  1439. 'Pacific' => wfMessage( 'timezoneregion-pacific' )->inLanguage( $language )->text(),
  1440. ];
  1441. asort( $tzRegions );
  1442. $timeZoneList = [];
  1443. $now = new DateTime();
  1444. foreach ( $identifiers as $identifier ) {
  1445. $parts = explode( '/', $identifier, 2 );
  1446. // DateTimeZone::listIdentifiers() returns a number of
  1447. // backwards-compatibility entries. This filters them out of the
  1448. // list presented to the user.
  1449. if ( count( $parts ) !== 2 || !array_key_exists( $parts[0], $tzRegions ) ) {
  1450. continue;
  1451. }
  1452. // Localize region
  1453. $parts[0] = $tzRegions[$parts[0]];
  1454. $dateTimeZone = new DateTimeZone( $identifier );
  1455. $minDiff = floor( $dateTimeZone->getOffset( $now ) / 60 );
  1456. $display = str_replace( '_', ' ', $parts[0] . '/' . $parts[1] );
  1457. $value = "ZoneInfo|$minDiff|$identifier";
  1458. $timeZoneList[$identifier] = [
  1459. 'name' => $display,
  1460. 'timecorrection' => $value,
  1461. 'region' => $parts[0],
  1462. ];
  1463. }
  1464. return $timeZoneList;
  1465. }
  1466. }