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