Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
63.01% |
138 / 219 |
|
36.84% |
7 / 19 |
CRAP | |
0.00% |
0 / 1 |
GlobalPreferencesFactory | |
63.01% |
138 / 219 |
|
36.84% |
7 / 19 |
422.21 | |
0.00% |
0 / 1 |
setAutoGlobals | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFormDescriptor | |
45.45% |
5 / 11 |
|
0.00% |
0 / 1 |
6.60 | |||
getPreferencesLocal | |
93.75% |
45 / 48 |
|
0.00% |
0 / 1 |
6.01 | |||
getPreferencesGlobal | |
0.00% |
0 / 51 |
|
0.00% |
0 / 1 |
90 | |||
saveFormData | |
88.57% |
31 / 35 |
|
0.00% |
0 / 1 |
15.34 | |||
findCheckMatrices | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
42 | |||
getSectionFragmentId | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
isGlobalizablePreference | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
11.04 | |||
isGlobalPrefName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isLocalPrefName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isUserGlobalized | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getUserID | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getGlobalPreferencesValues | |
50.00% |
4 / 8 |
|
0.00% |
0 / 1 |
2.50 | |||
setGlobalPreferences | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
2.01 | |||
resetGlobalUserSettings | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onGlobalPrefsPage | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
3 | |||
onLocalPrefsPage | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
12 | |||
handleLocalPreferencesChange | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
11 | |||
makeStorage | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 |
1 | <?php |
2 | /** |
3 | * Implements global preferences for MediaWiki |
4 | * |
5 | * @author Kunal Mehta <legoktm@gmail.com> |
6 | * @license http://www.gnu.org/copyleft/gpl.html GPL-2.0-or-later |
7 | * @file |
8 | * @ingroup Extensions |
9 | * |
10 | * Partially based off of work by Werdna |
11 | * https://www.mediawiki.org/wiki/Special:Code/MediaWiki/49790 |
12 | */ |
13 | |
14 | namespace GlobalPreferences; |
15 | |
16 | use IContextSource; |
17 | use LogicException; |
18 | use MediaWiki\Html\Html; |
19 | use MediaWiki\Language\RawMessage; |
20 | use MediaWiki\MediaWikiServices; |
21 | use MediaWiki\Preferences\DefaultPreferencesFactory; |
22 | use MediaWiki\SpecialPage\SpecialPage; |
23 | use MediaWiki\Status\Status; |
24 | use MediaWiki\User\CentralId\CentralIdLookup; |
25 | use MediaWiki\User\User; |
26 | use MediaWiki\User\UserIdentity; |
27 | use OOUI\ButtonWidget; |
28 | use RequestContext; |
29 | use RuntimeException; |
30 | |
31 | /** |
32 | * Global preferences. |
33 | * @package GlobalPreferences |
34 | */ |
35 | class GlobalPreferencesFactory extends DefaultPreferencesFactory { |
36 | |
37 | /** |
38 | * The suffix appended to preference names |
39 | * for the associated preference that tracks whether they have a local override. |
40 | */ |
41 | public const LOCAL_EXCEPTION_SUFFIX = '-local-exception'; |
42 | |
43 | /** |
44 | * The suffix appended to preference names for their global counterparts. |
45 | */ |
46 | public const GLOBAL_EXCEPTION_SUFFIX = '-global'; |
47 | |
48 | /** |
49 | * @var string[] Names of autoglobal options |
50 | */ |
51 | protected $autoGlobals = []; |
52 | |
53 | /** |
54 | * "bad" preferences that we should remove from |
55 | * Special:GlobalPrefs |
56 | * @var array |
57 | */ |
58 | protected $disallowedPreferences = [ |
59 | // Stored in user table, doesn't work yet |
60 | 'realname', |
61 | // @todo Show CA user id / shared user table id? |
62 | 'userid', |
63 | // @todo Show CA global groups instead? |
64 | 'usergroups', |
65 | // @todo Should global edit count instead? |
66 | 'editcount', |
67 | 'registrationdate', |
68 | // Signature could be global, but links in it are too likely to break. |
69 | 'nickname', |
70 | 'fancysig', |
71 | ]; |
72 | |
73 | /** |
74 | * Preference types that we should not add a checkbox for |
75 | * @var array |
76 | */ |
77 | protected $typesPrevented = [ |
78 | 'info', |
79 | 'hidden', |
80 | 'api', |
81 | ]; |
82 | |
83 | /** |
84 | * Preference classes that are allowed to be global |
85 | * @var array |
86 | */ |
87 | protected $allowedClasses = [ |
88 | \HTMLSelectOrOtherField::class, |
89 | \MediaWiki\Extension\BetaFeatures\HTMLFeatureField::class, |
90 | \HTMLCheckMatrix::class, |
91 | ]; |
92 | |
93 | /** |
94 | * Sets the list of options for which setting the local value should transparently update |
95 | * the global value. |
96 | * |
97 | * @param string[] $list |
98 | */ |
99 | public function setAutoGlobals( array $list ) { |
100 | $this->autoGlobals = $list; |
101 | } |
102 | |
103 | /** |
104 | * Get all user preferences. |
105 | * @param User $user |
106 | * @param IContextSource $context The current request context |
107 | * @return array|null |
108 | */ |
109 | public function getFormDescriptor( User $user, IContextSource $context ) { |
110 | $globalPrefs = $this->getGlobalPreferencesValues( $user, Storage::SKIP_CACHE ); |
111 | // The above function can return false |
112 | $globalPrefNames = $globalPrefs ? array_keys( $globalPrefs ) : []; |
113 | $preferences = parent::getFormDescriptor( $user, $context ); |
114 | if ( $this->onGlobalPrefsPage( $context ) ) { |
115 | if ( $globalPrefs === false ) { |
116 | throw new RuntimeException( |
117 | "Attempted to load global preferences page for {$user->getName()} whose " |
118 | . 'preference values failed to load' |
119 | ); |
120 | } |
121 | return $this->getPreferencesGlobal( $user, $preferences, $globalPrefs, $context ); |
122 | } |
123 | return $this->getPreferencesLocal( $user, $preferences, $globalPrefNames, $context ); |
124 | } |
125 | |
126 | /** |
127 | * Add help-text to the local preferences where they're globalized, |
128 | * and add the link to Special:GlobalPreferences to the personal preferences tab. |
129 | * @param User $user |
130 | * @param mixed[][] $preferences The preferences array. |
131 | * @param string[] $globalPrefNames The names of those preferences that are already global. |
132 | * @param IContextSource $context The current request context |
133 | * @return mixed[][] |
134 | */ |
135 | protected function getPreferencesLocal( |
136 | User $user, |
137 | array $preferences, |
138 | array $globalPrefNames, |
139 | IContextSource $context |
140 | ) { |
141 | $this->logger->debug( "Creating local preferences array for '{$user->getName()}'" ); |
142 | $modifiedPrefs = []; |
143 | $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup(); |
144 | foreach ( $preferences as $name => $def ) { |
145 | $modifiedPrefs[$name] = $def; |
146 | // If this is not globalizable, or hasn't been set globally. |
147 | if ( !isset( $def['section'] ) |
148 | || !in_array( $name, $globalPrefNames ) |
149 | || !$this->isGlobalizablePreference( $name, $def ) |
150 | ) { |
151 | continue; |
152 | } |
153 | $localExName = $name . static::LOCAL_EXCEPTION_SUFFIX; |
154 | $localExValueUser = $userOptionsLookup->getBoolOption( $user, $localExName ); |
155 | |
156 | // Add a new local exception preference after this one. |
157 | $cssClasses = [ |
158 | 'mw-globalprefs-local-exception', |
159 | 'mw-globalprefs-local-exception-for-' . $name, |
160 | 'mw-prefs-search-noindex', |
161 | ]; |
162 | $section = $def['section']; |
163 | $secFragment = static::getSectionFragmentId( $section ); |
164 | $labelMsg = $context->msg( 'globalprefs-set-local-exception', [ $secFragment ] ); |
165 | $modifiedPrefs[$localExName] = [ |
166 | 'type' => 'toggle', |
167 | 'label-raw' => $labelMsg->parse(), |
168 | 'default' => $localExValueUser, |
169 | 'section' => $section, |
170 | 'cssclass' => implode( ' ', $cssClasses ), |
171 | 'hide-if' => $def['hide-if'] ?? false, |
172 | 'disable-if' => $def['disable-if'] ?? false, |
173 | ]; |
174 | if ( isset( $def['disable-if'] ) ) { |
175 | $modifiedPrefs[$name]['disable-if'] = [ 'OR', $def['disable-if'], |
176 | [ '!==', $localExName, '1' ] |
177 | ]; |
178 | } else { |
179 | $modifiedPrefs[$name]['disable-if'] = [ '!==', $localExName, '1' ]; |
180 | } |
181 | } |
182 | $preferences = $modifiedPrefs; |
183 | |
184 | // Add a link to GlobalPreferences to the local preferences form. |
185 | $linkObject = new ButtonWidget( [ |
186 | 'href' => SpecialPage::getTitleFor( 'GlobalPreferences' )->getLinkURL(), |
187 | 'label' => $context->msg( 'globalprefs-info-link' )->text(), |
188 | ] ); |
189 | $link = $linkObject->toString(); |
190 | |
191 | $preferences['global-info'] = [ |
192 | 'type' => 'info', |
193 | 'section' => 'personal/info', |
194 | 'label-message' => 'globalprefs-info-label', |
195 | 'raw' => true, |
196 | 'default' => $link, |
197 | 'help-message' => 'globalprefs-info-help', |
198 | ]; |
199 | |
200 | return $preferences; |
201 | } |
202 | |
203 | /** |
204 | * Add the '-global' counterparts to all preferences, and override the local exception. |
205 | * @param User $user |
206 | * @param mixed[][] $preferences The preferences array. |
207 | * @param mixed[] $globalPrefs The array of global preferences. |
208 | * @param IContextSource $context The current request context |
209 | * @return mixed[][] |
210 | */ |
211 | protected function getPreferencesGlobal( |
212 | User $user, |
213 | array $preferences, |
214 | array $globalPrefs, |
215 | IContextSource $context |
216 | ) { |
217 | $allPrefs = []; |
218 | $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup(); |
219 | |
220 | // Add the "Restore all default preferences" link like on Special:Preferences |
221 | // (the normal preference entry is not globalizable) |
222 | $allPrefs['restoreprefs'] = [ |
223 | 'type' => 'info', |
224 | 'raw' => true, |
225 | 'label-message' => 'prefs-user-restoreprefs-label', |
226 | 'default' => Html::element( |
227 | 'a', |
228 | [ |
229 | 'href' => SpecialPage::getTitleFor( 'GlobalPreferences' ) |
230 | ->getSubpage( 'reset' )->getLocalURL() |
231 | ], |
232 | $context->msg( 'globalprefs-restoreprefs' )->text() |
233 | ), |
234 | 'section' => 'personal/info', |
235 | ]; |
236 | |
237 | // Add all corresponding new global fields. |
238 | foreach ( $preferences as $pref => $def ) { |
239 | // Ignore unwanted preferences. |
240 | if ( !$this->isGlobalizablePreference( $pref, $def ) ) { |
241 | continue; |
242 | } |
243 | // Create the new preference. |
244 | $isGlobal = isset( $globalPrefs[$pref] ); |
245 | $allPrefs[$pref . static::GLOBAL_EXCEPTION_SUFFIX] = [ |
246 | 'type' => 'toggle', |
247 | // Make the tooltip and the label the same, because the label is normally hidden. |
248 | 'tooltip' => 'globalprefs-check-label', |
249 | 'label-message' => 'tooltip-globalprefs-check-label', |
250 | 'default' => $isGlobal, |
251 | 'section' => $def['section'], |
252 | 'cssclass' => 'mw-globalprefs-global-check mw-globalprefs-checkbox-for-' . $pref, |
253 | 'hide-if' => $def['hide-if'] ?? false, |
254 | 'disable-if' => $def['disable-if'] ?? false, |
255 | ]; |
256 | if ( isset( $def['disable-if'] ) ) { |
257 | $def['disable-if'] = [ 'OR', $def['disable-if'], |
258 | [ '!==', $pref . static::GLOBAL_EXCEPTION_SUFFIX, '1' ] |
259 | ]; |
260 | } else { |
261 | $def['disable-if'] = [ '!==', $pref . static::GLOBAL_EXCEPTION_SUFFIX, '1' ]; |
262 | } |
263 | // If this has a local exception, override it and append a help message to say so. |
264 | if ( $isGlobal && $userOptionsLookup->getBoolOption( $user, $pref . static::LOCAL_EXCEPTION_SUFFIX ) ) { |
265 | $def['default'] = $this->getOptionFromUser( $pref, $def, $globalPrefs ); |
266 | // Create a link to the relevant section of GlobalPreferences. |
267 | $secFragment = static::getSectionFragmentId( $def['section'] ); |
268 | $helpMsg = [ 'globalprefs-has-local-exception', $secFragment ]; |
269 | // Merge the help messages. |
270 | if ( isset( $def['help'] ) ) { |
271 | $def['help-messages'] = [ new RawMessage( $def['help'] . '<br />' ), $helpMsg ]; |
272 | unset( $def['help'] ); |
273 | } elseif ( isset( $def['help-message'] ) ) { |
274 | $def['help-messages'] = [ $def['help-message'], new RawMessage( '<br />' ), $helpMsg ]; |
275 | unset( $def['help-message'] ); |
276 | } elseif ( isset( $def['help-messages'] ) ) { |
277 | $def['help-messages'][] = new RawMessage( '<br />' ); |
278 | $def['help-messages'][] = $helpMsg; |
279 | } else { |
280 | $def['help-message'] = $helpMsg; |
281 | } |
282 | } |
283 | |
284 | $allPrefs[$pref] = $def; |
285 | } |
286 | |
287 | return $allPrefs; |
288 | } |
289 | |
290 | /** |
291 | * @inheritDoc |
292 | */ |
293 | protected function saveFormData( $formData, \PreferencesFormOOUI $form, array $formDescriptor ) { |
294 | if ( !$this->onGlobalPrefsPage( $form ) ) { |
295 | return parent::saveFormData( $formData, $form, $formDescriptor ); |
296 | } |
297 | '@phan-var GlobalPreferencesFormOOUI $form'; |
298 | |
299 | $user = $form->getModifiedUser(); |
300 | |
301 | // Difference from parent: removed 'editmyprivateinfo' |
302 | if ( !$this->permissionManager->userHasRight( $user, 'editmyoptions' ) ) { |
303 | return Status::newFatal( 'mypreferencesprotected' ); |
304 | } |
305 | |
306 | // Filter input |
307 | $this->applyFilters( $formData, $formDescriptor, 'filterFromForm' ); |
308 | |
309 | // In the parent, we remove 'realname', but this is unnecessary |
310 | // here because GlobalPreferences removes this elsewhere, so |
311 | // the field will not even appear in this form |
312 | |
313 | // Difference from parent: We are not collecting old user settings |
314 | |
315 | foreach ( $this->getSaveBlacklist() as $b ) { |
316 | unset( $formData[$b] ); |
317 | } |
318 | |
319 | # If users have saved a value for a preference which has subsequently been disabled |
320 | # via $wgHiddenPrefs, we don't want to destroy that setting in case the preference |
321 | # is subsequently re-enabled |
322 | $hiddenPrefs = $this->options->get( 'HiddenPrefs' ); |
323 | $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup(); |
324 | foreach ( $hiddenPrefs as $pref ) { |
325 | # If the user has not set a non-default value here, the default will be returned |
326 | # and subsequently discarded |
327 | $formData[$pref] = $userOptionsLookup->getOption( $user, $pref, null, true ); |
328 | } |
329 | |
330 | // Difference from parent: We are ignoring RClimit preference; the parent |
331 | // checks for changes in that preference to update a hidden one, but the |
332 | // RCFilters product is okay with having that be localized |
333 | |
334 | // We are also not resetting unused preferences in the global context. |
335 | // Otherwise, users could lose data by editing their global preferences |
336 | // on a wiki where some of the preferences don't exist. However, this means |
337 | // that preferences for undeployed extensions or removed code are never |
338 | // removed from the database... |
339 | |
340 | // Setting the actual preference values: |
341 | $prefs = []; |
342 | $suffixLen = strlen( self::GLOBAL_EXCEPTION_SUFFIX ); |
343 | foreach ( $formData as $name => $value ) { |
344 | // If this is the '-global' counterpart to a preference. |
345 | if ( self::isGlobalPrefName( $name ) && $value === true ) { |
346 | // Determine the real name of the preference. |
347 | $realName = substr( $name, 0, -$suffixLen ); |
348 | if ( array_key_exists( $realName, $formData ) ) { |
349 | $prefs[$realName] = $formData[$realName]; |
350 | if ( $prefs[$realName] === null ) { |
351 | // Special case: null means don't save this row, which can keep the previous value |
352 | $prefs[$realName] = ''; |
353 | } |
354 | } |
355 | } |
356 | } |
357 | |
358 | $matricesToClear = []; |
359 | // Now special processing for CheckMatrices |
360 | foreach ( $this->findCheckMatrices( $formDescriptor ) as $name ) { |
361 | $globalName = $name . self::GLOBAL_EXCEPTION_SUFFIX; |
362 | // Find all separate controls for this CheckMatrix |
363 | $checkMatrix = preg_grep( '/^' . preg_quote( $name ) . '/', array_keys( $formData ) ); |
364 | if ( array_key_exists( $globalName, $formData ) && $formData[$globalName] ) { |
365 | // Setting is global, copy the checkmatrices |
366 | foreach ( $checkMatrix as $input ) { |
367 | $prefs[$input] = $formData[$input]; |
368 | } |
369 | $prefs[$name] = true; |
370 | } else { |
371 | // Remove all the rows for this CheckMatrix |
372 | foreach ( $checkMatrix as $input ) { |
373 | unset( $prefs[$input] ); |
374 | } |
375 | $matricesToClear[] = $name; |
376 | } |
377 | unset( $prefs[$globalName] ); |
378 | } |
379 | $this->setGlobalPreferences( $user, $prefs, $form->getContext(), $matricesToClear ); |
380 | |
381 | return true; |
382 | } |
383 | |
384 | /** |
385 | * Finds CheckMatrix inputs in a form descriptor |
386 | * |
387 | * @param array $formDescriptor |
388 | * @return string[] Names of CheckMatrix options (parent only, not sub-checkboxes) |
389 | */ |
390 | private function findCheckMatrices( array $formDescriptor ) { |
391 | $result = []; |
392 | foreach ( $formDescriptor as $name => $info ) { |
393 | if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) || |
394 | ( isset( $info['class'] ) && $info['class'] == \HTMLCheckMatrix::class ) |
395 | ) { |
396 | $result[] = $name; |
397 | } |
398 | } |
399 | |
400 | return $result; |
401 | } |
402 | |
403 | /** |
404 | * Get the HTML fragment identifier for a given preferences section. This is the leading part |
405 | * of the provided section name, up to a slash (if there is one). |
406 | * @param string $section A section name, as used in a preference definition. |
407 | * @return string |
408 | */ |
409 | public static function getSectionFragmentId( $section ) { |
410 | $sectionId = preg_replace( '#/.*$#', '', $section ); |
411 | return 'mw-prefsection-' . $sectionId; |
412 | } |
413 | |
414 | /** |
415 | * Checks whether the given preference is globalizable. |
416 | * |
417 | * @param string $name Preference name |
418 | * @param mixed[] &$info Preference description, by reference to avoid unnecessary cloning |
419 | * @return bool |
420 | */ |
421 | protected function isGlobalizablePreference( $name, &$info ) { |
422 | // Preferences can opt out of being globalized by setting the 'noglobal' flag. |
423 | if ( isset( $info['noglobal'] ) && $info['noglobal'] === true ) { |
424 | return false; |
425 | } |
426 | |
427 | // Ignore "is global" checkboxes |
428 | if ( static::isGlobalPrefName( $name ) ) { |
429 | return false; |
430 | } |
431 | |
432 | // If a setting can't be changed, don't bother globalizing it |
433 | if ( isset( $info['disabled'] ) && $info['disabled'] ) { |
434 | return false; |
435 | } |
436 | |
437 | // Allow explicitly define if a preference can be globalized, |
438 | // especially useful for custom field classes. |
439 | // TODO: Deprecate 'noglobal' in favour of this param. |
440 | if ( isset( $info['canglobal'] ) ) { |
441 | return (bool)$info['canglobal']; |
442 | } |
443 | |
444 | $isAllowedType = isset( $info['type'] ) |
445 | && !in_array( $info['type'], $this->typesPrevented ) |
446 | && !in_array( $name, $this->disallowedPreferences ); |
447 | |
448 | $isAllowedClass = isset( $info['class'] ) |
449 | && in_array( $info['class'], $this->allowedClasses ); |
450 | |
451 | return $isAllowedType || $isAllowedClass; |
452 | } |
453 | |
454 | /** |
455 | * A convenience function to check if a preference name is for a global one. |
456 | * @param string $name The name to check. |
457 | * @return bool |
458 | */ |
459 | public static function isGlobalPrefName( $name ) { |
460 | return str_ends_with( $name, static::GLOBAL_EXCEPTION_SUFFIX ); |
461 | } |
462 | |
463 | /** |
464 | * A convenience function to check if a preference name is for a local-exception preference. |
465 | * @param string $name The name to check. |
466 | * @return bool |
467 | */ |
468 | public static function isLocalPrefName( $name ) { |
469 | return str_ends_with( $name, static::LOCAL_EXCEPTION_SUFFIX ); |
470 | } |
471 | |
472 | /** |
473 | * Checks if the user is globalized. |
474 | * @param UserIdentity $user |
475 | * @return bool |
476 | */ |
477 | public function isUserGlobalized( UserIdentity $user ) { |
478 | $utils = MediaWikiServices::getInstance()->getUserIdentityUtils(); |
479 | return $utils->isNamed( $user ) && $this->getUserID( $user ) !== 0; |
480 | } |
481 | |
482 | /** |
483 | * Gets the user's ID that we're using in the table |
484 | * Returns 0 if the user is not global |
485 | * @param UserIdentity $user |
486 | * @return int |
487 | */ |
488 | public function getUserID( UserIdentity $user ) { |
489 | return MediaWikiServices::getInstance()->getCentralIdLookupFactory()->getLookup() |
490 | ->centralIdFromLocalUser( $user, CentralIdLookup::AUDIENCE_RAW ); |
491 | } |
492 | |
493 | /** |
494 | * Get the user's global preferences. |
495 | * @param UserIdentity $user |
496 | * @param bool $skipCache Whether the preferences should be loaded strictly from DB |
497 | * @return false|string[] Array keyed by preference name, or false if not found. |
498 | */ |
499 | public function getGlobalPreferencesValues( UserIdentity $user, $skipCache = false ) { |
500 | $id = $this->getUserID( $user ); |
501 | if ( !$id ) { |
502 | $this->logger->warning( "Couldn't find a global ID for user {user}", |
503 | [ 'user' => $user->getName() ] |
504 | ); |
505 | return false; |
506 | } |
507 | $storage = $this->makeStorage( $user ); |
508 | return $storage->load( $skipCache ); |
509 | } |
510 | |
511 | /** |
512 | * Save the user's global preferences. |
513 | * @param User $user |
514 | * @param array $newGlobalPrefs Array keyed by preference name. |
515 | * @param IContextSource $context The request context. |
516 | * @param string[] $checkMatricesToClear List of check matrix controls that |
517 | * need their rows purged |
518 | * @return bool True on success, false if the user isn't global. |
519 | */ |
520 | public function setGlobalPreferences( |
521 | User $user, |
522 | $newGlobalPrefs, |
523 | IContextSource $context, |
524 | array $checkMatricesToClear = [] |
525 | ) { |
526 | $id = $this->getUserID( $user ); |
527 | if ( !$id ) { |
528 | return false; |
529 | } |
530 | |
531 | // Use a new instance of the current user to fetch the form descriptor because that way |
532 | // we're working with the previous user options and not those that are currently in the |
533 | // process of being saved (we only want the option names here, so don't care what the |
534 | // values are). |
535 | $userForDescriptor = User::newFromId( $user->getId() ); |
536 | |
537 | // Save the global options. |
538 | $storage = $this->makeStorage( $user ); |
539 | $knownPrefs = array_keys( $this->getFormDescriptor( $userForDescriptor, $context ) ); |
540 | |
541 | $storage->save( $newGlobalPrefs, $knownPrefs, $checkMatricesToClear ); |
542 | |
543 | $user->clearInstanceCache(); |
544 | return true; |
545 | } |
546 | |
547 | /** |
548 | * Deletes all of a user's global preferences. |
549 | * Assumes that the user is globalized. |
550 | * @param User $user |
551 | */ |
552 | public function resetGlobalUserSettings( User $user ) { |
553 | $this->makeStorage( $user )->delete(); |
554 | } |
555 | |
556 | /** |
557 | * Convenience function to check if we're on the global prefs page. |
558 | * @param IContextSource|null $context The context to use; if not set main request context is used. |
559 | * @return bool |
560 | */ |
561 | public function onGlobalPrefsPage( $context = null ) { |
562 | $context = $context ?: RequestContext::getMain(); |
563 | return $context->getTitle() && $context->getTitle()->isSpecial( 'GlobalPreferences' ); |
564 | } |
565 | |
566 | /** |
567 | * Convenience function to check if we're on the local prefs page. |
568 | * |
569 | * @param IContextSource|null $context The context to use; if not set main request context is used. |
570 | * @return bool |
571 | */ |
572 | public function onLocalPrefsPage( $context = null ) { |
573 | $context = $context ?: RequestContext::getMain(); |
574 | return $context->getTitle() && $context->getTitle()->isSpecial( 'Preferences' ); |
575 | } |
576 | |
577 | /** |
578 | * Processes local user options before they're saved |
579 | * |
580 | * @param UserIdentity $user |
581 | * @param array &$modifiedOptions |
582 | * @param array $originalOptions |
583 | */ |
584 | public function handleLocalPreferencesChange( |
585 | UserIdentity $user, |
586 | array &$modifiedOptions, |
587 | array $originalOptions |
588 | ) { |
589 | $shouldModify = []; |
590 | $mergedOptions = array_merge( $originalOptions, $modifiedOptions ); |
591 | foreach ( $this->autoGlobals as $optName ) { |
592 | // $modifiedOptions can contains options that not actually modified, filter out them |
593 | if ( array_key_exists( $optName, $modifiedOptions ) && |
594 | ( !array_key_exists( $optName, $originalOptions ) || |
595 | $modifiedOptions[$optName] !== $originalOptions[$optName] ) && |
596 | // And skip options that have local exceptions |
597 | !( $mergedOptions[$optName . static::LOCAL_EXCEPTION_SUFFIX] ?? false ) |
598 | ) { |
599 | $shouldModify[$optName] = $modifiedOptions[$optName]; |
600 | } |
601 | } |
602 | // No auto-global options are modified |
603 | if ( !$shouldModify ) { |
604 | return; |
605 | } |
606 | |
607 | $preferencesChanged = false; |
608 | $globals = $this->getGlobalPreferencesValues( $user, true ); |
609 | |
610 | if ( $globals ) { |
611 | foreach ( $shouldModify as $optName => $optVal ) { |
612 | if ( array_key_exists( $optName, $globals ) ) { |
613 | $globals[$optName] = $optVal; |
614 | $preferencesChanged = true; |
615 | } |
616 | } |
617 | |
618 | if ( $preferencesChanged ) { |
619 | $this->makeStorage( $user )->save( $globals, array_keys( $globals ) ); |
620 | } |
621 | } |
622 | } |
623 | |
624 | /** |
625 | * Factory for preference storage |
626 | * |
627 | * @param UserIdentity $user |
628 | * @return Storage |
629 | */ |
630 | protected function makeStorage( UserIdentity $user ) { |
631 | $id = $this->getUserID( $user ); |
632 | if ( !$id ) { |
633 | throw new LogicException( 'User not set or is not global on call to ' . __METHOD__ ); |
634 | } |
635 | return new Storage( $id ); |
636 | } |
637 | } |