Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
30.99% covered (danger)
30.99%
22 / 71
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
30.99% covered (danger)
30.99%
22 / 71
0.00% covered (danger)
0.00%
0 / 8
325.84
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 onBeforePageDisplay
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 onLoadUserOptions
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
90
 onSaveUserOptions
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 onPreferencesFormPreSave
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 localPreferencesFormPreSave
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 onDeleteUnknownPreferences
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onApiOptions
95.65% covered (success)
95.65%
22 / 23
0.00% covered (danger)
0.00%
0 / 1
8
1<?php
2
3namespace GlobalPreferences;
4
5use ApiOptions;
6use HTMLForm;
7use MediaWiki\Api\Hook\ApiOptionsHook;
8use MediaWiki\Config\Config;
9use MediaWiki\Hook\BeforePageDisplayHook;
10use MediaWiki\Hook\DeleteUnknownPreferencesHook;
11use MediaWiki\Logger\LoggerFactory;
12use MediaWiki\Output\OutputPage;
13use MediaWiki\Preferences\Hook\PreferencesFormPreSaveHook;
14use MediaWiki\Preferences\PreferencesFactory;
15use MediaWiki\User\Options\Hook\LoadUserOptionsHook;
16use MediaWiki\User\Options\Hook\SaveUserOptionsHook;
17use MediaWiki\User\Options\UserOptionsLookup;
18use MediaWiki\User\Options\UserOptionsManager;
19use MediaWiki\User\User;
20use MediaWiki\User\UserIdentity;
21use Message;
22use Skin;
23use Wikimedia\Rdbms\IDatabase;
24
25class Hooks implements
26    BeforePageDisplayHook,
27    LoadUserOptionsHook,
28    SaveUserOptionsHook,
29    PreferencesFormPreSaveHook,
30    DeleteUnknownPreferencesHook,
31    ApiOptionsHook
32    {
33
34    /** @var GlobalPreferencesFactory */
35    private $preferencesFactory;
36
37    /** @var UserOptionsManager */
38    private $userOptionsManager;
39
40    /** @var UserOptionsLookup */
41    private $userOptionsLookup;
42
43    /** @var Config */
44    private $config;
45
46    /**
47     * @param PreferencesFactory $preferencesFactory
48     * @param UserOptionsManager $userOptionsManager
49     * @param UserOptionsLookup $userOptionsLookup
50     * @param Config $config
51     */
52    public function __construct(
53        PreferencesFactory $preferencesFactory,
54        UserOptionsManager $userOptionsManager,
55        UserOptionsLookup $userOptionsLookup,
56        Config $config
57    ) {
58        $this->preferencesFactory = $preferencesFactory;
59        $this->userOptionsManager = $userOptionsManager;
60        $this->userOptionsLookup = $userOptionsLookup;
61        $this->config = $config;
62    }
63
64    /**
65     * Allows last minute changes to the output page, e.g. adding of CSS or JavaScript by extensions.
66     * @link https://www.mediawiki.org/wiki/Manual:Hooks/BeforePageDisplay
67     * @param OutputPage $out The output page.
68     * @param Skin $skin The skin. Not used.
69     */
70    public function onBeforePageDisplay( $out, $skin ): void {
71        if ( $out->getTitle()->isSpecial( 'Preferences' ) ) {
72            // Add module styles and scripts separately
73            // so non-JS users get the styles quicker and to avoid a FOUC.
74            $out->addModuleStyles( 'ext.GlobalPreferences.local-nojs' );
75            $out->addModules( 'ext.GlobalPreferences.local' );
76        }
77    }
78
79    /**
80     * Load global preferences.
81     * @link https://www.mediawiki.org/wiki/Manual:Hooks/LoadUserOptions
82     * @param UserIdentity $user The user for whom options are being loaded.
83     * @param array &$options The user's options; can be modified.
84     */
85    public function onLoadUserOptions( UserIdentity $user, array &$options ): void {
86        if ( !$this->preferencesFactory->isUserGlobalized( $user ) ) {
87            // Not a global user.
88            return;
89        }
90
91        $logger = LoggerFactory::getInstance( 'preferences' );
92        $logger->debug(
93            'Loading global options for user \'{user}\'',
94            [ 'user' => $user->getName() ]
95        );
96        // Overwrite all options that have a global counterpart.
97        $globalPrefs = $this->preferencesFactory->getGlobalPreferencesValues( $user );
98        if ( $globalPrefs === false ) {
99            return;
100        }
101        foreach ( $globalPrefs as $optName => $globalValue ) {
102            // Don't overwrite if it has a local exception.
103            $localExceptionName = $optName . GlobalPreferencesFactory::LOCAL_EXCEPTION_SUFFIX;
104            if ( isset( $options[ $localExceptionName ] ) && $options[ $localExceptionName ] ) {
105                continue;
106            }
107
108            // FIXME: temporary plug for T201340: DB might have rows for deglobalized
109            // Echo notifications. Don't allow these through if the main checkbox is not checked.
110            if ( !( $globalPrefs['echo-subscriptions'] ?? false )
111                && strpos( $optName, 'echo-subscriptions-' ) === 0
112            ) {
113                continue;
114            }
115
116            // Convert '0' to 0. PHP's boolean conversion considers them both false,
117            // but e.g. JavaScript considers the former as true.
118            if ( $globalValue === '0' ) {
119                $globalValue = 0;
120            }
121            $options[ $optName ] = $globalValue;
122        }
123    }
124
125    /**
126     * When saving a user's options, remove any global ones and never save any on the Global
127     * Preferences page. Global options are saved separately, in the PreferencesFormPreSave hook.
128     *
129     * @see https://www.mediawiki.org/wiki/Manual:Hooks/SaveUserOptions
130     * @param UserIdentity $user The user.
131     * @param string[] &$modifiedOptions The user's options that were modified.
132     * @param string[] $originalOptions The original options.
133     * @return bool False if nothing changed, true otherwise.
134     */
135    public function onSaveUserOptions( UserIdentity $user, array &$modifiedOptions, array $originalOptions ) {
136        if ( $this->preferencesFactory->onGlobalPrefsPage() ) {
137            // It shouldn't be possible to save local options here,
138            // but never save on this page anyways.
139            return false;
140        }
141
142        $this->preferencesFactory->handleLocalPreferencesChange( $user, $modifiedOptions, $originalOptions );
143
144        return true;
145    }
146
147    /**
148     * @link https://www.mediawiki.org/wiki/Manual:Hooks/PreferencesFormPreSave
149     * @param array $formData An associative array containing the data from the preferences form.
150     * @param HTMLForm $form The HTMLForm object that represents the preferences form.
151     * @param User $user The User object that can be used to change the user's preferences.
152     * @param bool &$result The boolean return value of the Preferences::tryFormSubmit method.
153     * @param array $oldUserOptions Array with user's old options (before save)
154     * @return bool|void True or no return value to continue or false to abort
155     */
156    public function onPreferencesFormPreSave( $formData, $form, $user, &$result, $oldUserOptions ) {
157        if ( !$this->preferencesFactory->onGlobalPrefsPage( $form ) ) {
158            return $this->localPreferencesFormPreSave( $formData, $user );
159        }
160        return true;
161    }
162
163    /**
164     * Process PreferencesFormPreSave for Special:Preferences
165     * Handles CheckMatrix
166     *
167     * @param array $formData Associative array of [ preference name => value ]
168     * @param User $user Current user
169     * @return bool Hook return value
170     */
171    private function localPreferencesFormPreSave( array $formData, User $user ): bool {
172        foreach ( $formData as $pref => $value ) {
173            if ( !GlobalPreferencesFactory::isLocalPrefName( $pref ) ) {
174                continue;
175            }
176            // Determine the real name of the preference.
177            $suffixLen = strlen( GlobalPreferencesFactory::LOCAL_EXCEPTION_SUFFIX );
178            $realName = substr( $pref, 0, -$suffixLen );
179            if ( isset( $formData[$realName] ) ) {
180                // Not a CheckMatrix field
181                continue;
182            }
183            $checkMatrix = preg_grep( "/^$realName-/", array_keys( $formData ) );
184            foreach ( $checkMatrix as $check ) {
185                $localExceptionName = $check . GlobalPreferencesFactory::LOCAL_EXCEPTION_SUFFIX;
186                $this->userOptionsManager->setOption( $user, $localExceptionName, $value );
187            }
188        }
189        return true;
190    }
191
192    /**
193     * Prevent local exception preferences from being cleaned up.
194     * @link https://www.mediawiki.org/wiki/Manual:Hooks/DeleteUnknownPreferences
195     * @param string[] &$where Array of where clause conditions to add to.
196     * @param IDatabase $db
197     */
198    public function onDeleteUnknownPreferences( &$where, $db ) {
199        $like = $db->buildLike( $db->anyString(), GlobalPreferencesFactory::LOCAL_EXCEPTION_SUFFIX );
200        $where[] = "up_property NOT $like";
201    }
202
203    /**
204     * @param ApiOptions $apiModule Calling ApiOptions object
205     * @param User $user User object whose preferences are being changed
206     * @param array $changes Associative array of preference name => value
207     * @param string[] $resetKinds Array of strings specifying which options kinds to reset
208     *   See User::resetOptions() and User::getOptionKinds() for possible values.
209     * @return bool|void True or no return value to continue or false to abort
210     */
211    public function onApiOptions( $apiModule, $user, $changes, $resetKinds ) {
212        // Only hook to the core module but not to our code that inherits from it
213        if ( $apiModule->getModuleName() !== 'options' ) {
214            return;
215        }
216
217        $globalPrefs = $this->preferencesFactory->getGlobalPreferencesValues( $user );
218
219        $toWarn = [];
220        foreach ( array_keys( $changes ) as $preference ) {
221            if ( GlobalPreferencesFactory::isLocalPrefName( $preference ) ) {
222                continue;
223            }
224            $exceptionName = $preference . GlobalPreferencesFactory::LOCAL_EXCEPTION_SUFFIX;
225            if ( !$this->userOptionsLookup->getOption( $user, $exceptionName ) ) {
226                if ( $globalPrefs && array_key_exists( $preference, $globalPrefs ) ) {
227                    $toWarn[] = $preference;
228                }
229            }
230        }
231        if ( $toWarn ) {
232            $toWarn = array_map( static function ( $str ) {
233                return wfEscapeWikiText( "`$str`" );
234            }, $toWarn );
235            $apiModule->addWarning(
236                [
237                    'apiwarn-globally-overridden',
238                    Message::listParam( $toWarn ),
239                    count( $toWarn ),
240                ],
241                'globally-overridden'
242            );
243        }
244    }
245}