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