This came up as a result of working on T218265: Mute: Add links to disable email and mute specific user to emails sent via Special:EmailUser but a generalized solution can help other products that need to update preferences in similar ways.
The problem
GlobalPreferences can only be saved from the Special:GlobalPreferences page. While the extension intercepts all option saves from the onUserSaveOptions hook, it only updates the global preferences specifically, from the Special:GlobalPreferences page. This was an intentional product decision when the extension was created.
The main idea is the GlobalPreferences allows for setting/updating the global preferences specifically through an API, and the decision on whether a preference should be updated locally or globally should be done by the product, and not as an overarching decision by the GlobalPreferences extension.
This works well for extensions and gadgets, both of which can directly call the GlobalPreferences-specific API, but it has some flaws:
- It does not work for any product in core, since core does not know about GlobalPreferences extension. Any code-driven preference change in core, therefore, is only updating local preferences, even if that local preference is overridden by core, or has a local override by GlobalPreferences itself.
- Products that want to update a preference regardless of where it is saved (whether it is global, local, or locally overridden by the user) must do these checks themselves if they want to. Those checks are doable, but add complexity to the code that is repetitive and since each product needs to do that for themselves, it can quickly get non standard.
Proposed solution
There should be a way to tell GlobalPreferences that certain preference options should be saved based on its own logic, wherever they are enabled.
The code that tells core to save preference should not have to deal with whether GlobalPreferences exists, is enabled, and then whether the specific option's state is globalized, local, or a local override. Code in products should "just" save the option and GlobalPreferences should handle this logic. However, products should decide whether this is wanted or not, since there are multiple cases (especially in core) where options that are saved in the code are meant to be local (for example, RecentChangesFilters' saved queries).
Technical implementation
To accomplish this, we can implement the following:
- Create a wg variable that stores an array of local option names that should be handled by GlobalPreferences. If a product wants GlobalPreferences to decide where the best to save it, the product should add their preference names to this global variables in config.
- In the onUserSaveOptions code, the GlobalPreferences extension will evaluate the options given and compare each to the wg variable.
- If any of the given options is in the wg variable, GlobalPreferences will handle it itself (and take it out of the options array when giving it back through the hook)
- For each option that GlobalPreferences needs to handle, it will do the following:
- If this option is local for the user (the user does not have that preference enabled in Special:GlobalPreferences) then the value will be saved locally.
- If this option is globalized (the user enabled the preference in Special:GlobalPreferences) then the value will be stored in the global preference.
- If this option is globalized, but has a local override (the user has the option enabled in Special:GlobalPreferences, but in the specific wiki also enabled a local override) then the value will be saved in the override.
This will mean, essentially, that for all preference values given to GlobalPreferences to handle, it will save the given value where it is actually set at the moment of saving, and will result in the least overriding of other values as possible.
This also means that if a preference was changed through some code through the method above, and the user, after that, enabled/disabled or changed the state of what preference is "on" (enabled or disabled global preference or override, etc) then the value will change from the value that the code has set. That, however ,will be specifically done by the user and in either Special:Preferences or Special:GlobalPreferences where they can see what is active and what is overridden.