jenkins-bot has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/370152 )

Change subject: Switch to using PreferencesFactory, and improve UI
......................................................................


Switch to using PreferencesFactory, and improve UI

This refactors the extension to use the new PreferencesFactory
service and moves each preference's 'enable this globally?'
toggle to the side of the preferences form to make it easier to
see what's enabled and what's not. It also introduces a
'select-all' toggle for each tab of the preferences form.

Bug: T173476
Bug: T68869
Change-Id: I3c10dfeacf02367e90f84a3e572ecf3f4048e02a
---
M .stylelintrc.json
M extension.json
M i18n/en.json
M i18n/qqq.json
D includes/GlobalPreferences.php
A includes/GlobalPreferencesFactory.php
A includes/GlobalPreferencesForm.php
M includes/Hooks.php
M includes/SpecialGlobalPreferences.php
A includes/Storage.php
M resources/ext.GlobalPreferences.special.css
M resources/ext.GlobalPreferences.special.js
M resources/ext.GlobalPreferences.special.nojs.css
A tests/phpunit/GlobalPreferencesTest.php
14 files changed, 781 insertions(+), 363 deletions(-)

Approvals:
  MaxSem: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/.stylelintrc.json b/.stylelintrc.json
index 2c90730..d691e9d 100644
--- a/.stylelintrc.json
+++ b/.stylelintrc.json
@@ -1,3 +1,6 @@
 {
-       "extends": "stylelint-config-wikimedia"
+       "extends": "stylelint-config-wikimedia",
+       "rules": {
+               "selector-no-id": null
+       }
 }
diff --git a/extension.json b/extension.json
index 8245339..4824a25 100644
--- a/extension.json
+++ b/extension.json
@@ -23,9 +23,11 @@
                "GlobalPreferencesAlias": "GlobalPreferences.alias.php"
        },
        "AutoloadClasses": {
-               "GlobalPreferences\\GlobalPreferences": 
"includes/GlobalPreferences.php",
                "GlobalPreferences\\Hooks": "includes/Hooks.php",
-               "GlobalPreferences\\SpecialGlobalPreferences": 
"includes/SpecialGlobalPreferences.php"
+               "GlobalPreferences\\SpecialGlobalPreferences": 
"includes/SpecialGlobalPreferences.php",
+               "GlobalPreferences\\GlobalPreferencesFactory": 
"includes/GlobalPreferencesFactory.php",
+               "GlobalPreferences\\GlobalPreferencesForm": 
"includes/GlobalPreferencesForm.php",
+               "GlobalPreferences\\Storage": "includes/Storage.php"
        },
        "Hooks": {
                "UserLoadOptions": [
@@ -39,11 +41,11 @@
                ],
                "LoadExtensionSchemaUpdates": [
                        "GlobalPreferences\\Hooks::onLoadExtensionSchemaUpdates"
+               ],
+               "MediaWikiServices": [
+                       "GlobalPreferences\\Hooks::onMediaWikiServices"
                ]
        },
-       "ExtensionFunctions": [
-               "GlobalPreferences\\Hooks::onExtensionFunctions"
-       ],
        "ResourceFileModulePaths": {
                "localBasePath": "resources",
                "remoteExtPath": "GlobalPreferences/resources"
@@ -51,7 +53,8 @@
        "ResourceModules": {
                "ext.GlobalPreferences.special": {
                        "styles": "ext.GlobalPreferences.special.css",
-                       "scripts": "ext.GlobalPreferences.special.js"
+                       "scripts": "ext.GlobalPreferences.special.js",
+                       "messages": [ "globalprefs-select-all" ]
                },
                "ext.GlobalPreferences.special.nojs": {
                        "styles": "ext.GlobalPreferences.special.nojs.css"
diff --git a/i18n/en.json b/i18n/en.json
index 0e37692..119902e 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -5,13 +5,14 @@
                ]
        },
        "globalprefs-desc": "Allows users to set global preferences",
-       "globalprefs-set-globally": "This preference has been set globally and 
must be modified through [[Special:GlobalPreferences|your global 
preferences]].",
-       "globalprefs-check-label": "Use this preference on all wikis",
+       "globalprefs-set-globally": "This preference has been set globally and 
must be modified through [[Special:GlobalPreferences#$1|your global 
preferences]].",
+       "tooltip-globalprefs-check-label": "Make this setting global",
        "globalprefs-error-header": "Error",
        "globalprefs-notglobal": "Your account is not a global account and 
cannot set global preferences.",
        "globalpreferences": "Global preferences",
        "globalprefs-info-label": "Global preferences:",
        "globalprefs-info-link": "Set your global preferences",
        "globalprefs-reset-intro": "You can use this page to disable all global 
preferences and return to your local preferences.\nThis cannot be undone.",
-       "globalprefs-restoreprefs": "Remove all global preferences (in all 
sections)"
+       "globalprefs-restoreprefs": "Remove all global preferences (in all 
sections)",
+       "globalprefs-select-all": "Select all"
 }
diff --git a/i18n/qqq.json b/i18n/qqq.json
index f158572..d2c1323 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -7,12 +7,13 @@
        },
        "globalprefs-desc": "{{desc|name=Global 
Preferences|url=https://www.mediawiki.org/wiki/Extension:GlobalPreferences}}";,
        "globalprefs-set-globally": "Help message below a disabled preference 
option instructing the user to change it at their global preferences page.",
-       "globalprefs-check-label": "Label for a checkbox that enables the user 
to save that (the above) preference globally.",
+       "tooltip-globalprefs-check-label": "The tooltip and the label text for 
the checkbox to enable a preference globally.",
        "globalprefs-error-header": "Page title for error 
message.\n{{Identical|Error}}",
-       "globalprefs-notglobal": "Error message a user sees if they don not 
have a global account.",
+       "globalprefs-notglobal": "Error message a user sees if they do not have 
a global account.",
        "globalpreferences": "{{doc-special|GlobalPreferences}}",
        "globalprefs-info-label": "Label for link in [[Special:Preferences]] to 
go set your global preferences.",
        "globalprefs-info-link": "Link text to [[Special:GlobalPreferences]].",
        "globalprefs-reset-intro": "Used in 
[[Special:GlobalPreferences/reset]].",
-       "globalprefs-restoreprefs": "Used as link text in 
[[Special:GlobalPreferences]]. The link points to 
[[Special:GlobalPreferences/reset]] which shows the \"Remove all global 
preferences\" form.\n\nAlso used as label for the Submit button in 
[[Special:GlobalPreferences/reset]]."
+       "globalprefs-restoreprefs": "Used as link text in 
[[Special:GlobalPreferences]]. The link points to 
[[Special:GlobalPreferences/reset]] which shows the \"Remove all global 
preferences\" form.\n\nAlso used as label for the Submit button in 
[[Special:GlobalPreferences/reset]].",
+       "globalprefs-select-all": "Label for the checkbox for selecting all of 
a section's preferences."
 }
diff --git a/includes/GlobalPreferences.php b/includes/GlobalPreferences.php
deleted file mode 100644
index 351f7f5..0000000
--- a/includes/GlobalPreferences.php
+++ /dev/null
@@ -1,103 +0,0 @@
-<?php
-/**
- * Implements global preferences for MediaWiki
- *
- * @author Kunal Mehta <lego...@gmail.com>
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 
2.0 or later
- * @file
- * @ingroup Extensions
- *
- * Partially based off of work by Werdna
- * https://www.mediawiki.org/wiki/Special:Code/MediaWiki/49790
- */
-
-namespace GlobalPreferences;
-
-use CentralIdLookup;
-use IContextSource;
-use RequestContext;
-use User;
-use Wikimedia\Rdbms\Database;
-
-class GlobalPreferences {
-
-       /**
-        * @param int $type one of the DB_* constants
-        * @return Database
-        */
-       public static function getPrefsDB( $type = DB_REPLICA ) {
-               global $wgGlobalPreferencesDB;
-               if ( $wgGlobalPreferencesDB ) {
-                       return wfGetDB( $type, [], $wgGlobalPreferencesDB );
-               } else {
-                       return wfGetDB( $type );
-               }
-       }
-
-       /**
-        * Checks if the user is globalized
-        * @param User $user The user
-        * @return bool
-        */
-       public static function isUserGlobalized( User $user ) {
-               if ( $user->isAnon() ) {
-                       // No prefs for anons, sorry :(
-                       return false;
-               }
-
-               return self::getUserID( $user ) !== 0;
-       }
-
-       /**
-        * Gets the user's ID that we're using in the table
-        * Returns 0 if the user is not global
-        * @param User $user The user for whom to get the ID.
-        * @return int
-        */
-       public static function getUserID( User $user ) {
-               $lookup = CentralIdLookup::factory();
-               return $lookup->centralIdFromLocalUser( $user, 
CentralIdLookup::AUDIENCE_RAW );
-       }
-
-       /**
-        * Deletes all of a user's global prefs
-        * Assumes that the user is globalized
-        * @param User $user The user.
-        */
-       public static function resetGlobalUserSettings( User $user ) {
-               if ( !isset( $user->mGlobalPrefs ) ) {
-                       // Triggers User::loadOptions.
-                       $user->getOption( '' );
-               }
-               if ( count( $user->mGlobalPrefs ) ) {
-                       self::getPrefsDB( DB_MASTER )->delete(
-                               'global_preferences',
-                               [ 'gp_user' => self::getUserID( $user ) ],
-                               __METHOD__
-                       );
-               }
-       }
-
-       /**
-        * Convenience function to check if we're on the global prefs page.
-        * @param IContextSource $context The context to use; if not set main 
request context is used.
-        * @return bool
-        */
-       public static function onGlobalPrefsPage( $context = null ) {
-               $context = $context ?: RequestContext::getMain();
-               return $context->getTitle() && $context->getTitle()->isSpecial( 
'GlobalPreferences' );
-       }
-
-       /**
-        * Convenience function to check if we're on the local
-        * prefs page
-        *
-        * @param IContextSource $context The context to use; if not set main 
request context is used.
-        * @return bool
-        */
-       public static function onLocalPrefsPage( $context = null ) {
-               $context = $context ?: RequestContext::getMain();
-               return $context->getTitle()
-               && $context->getTitle()->isSpecial( 'Preferences' );
-       }
-}
diff --git a/includes/GlobalPreferencesFactory.php 
b/includes/GlobalPreferencesFactory.php
new file mode 100644
index 0000000..edc1b90
--- /dev/null
+++ b/includes/GlobalPreferencesFactory.php
@@ -0,0 +1,284 @@
+<?php
+/**
+ * Implements global preferences for MediaWiki
+ *
+ * @author Kunal Mehta <lego...@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 
2.0 or later
+ * @file
+ * @ingroup Extensions
+ *
+ * Partially based off of work by Werdna
+ * https://www.mediawiki.org/wiki/Special:Code/MediaWiki/49790
+ */
+
+namespace GlobalPreferences;
+
+use CentralIdLookup;
+use IContextSource;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Preferences\DefaultPreferencesFactory;
+use RequestContext;
+use SpecialPage;
+use User;
+
+/**
+ * Global preferences.
+ * @package GlobalPreferences
+ */
+class GlobalPreferencesFactory extends DefaultPreferencesFactory {
+
+       /** @var User */
+       protected $user;
+
+       /**
+        * "bad" preferences that we should remove from
+        * Special:GlobalPrefs
+        * @var array
+        */
+       protected $prefsBlacklist = [
+               // Stored in user table, doesn't work yet
+               'realname',
+               // @todo Show CA user id / shared user table id?
+               'userid',
+               // @todo Show CA global groups instead?
+               'usergroups',
+               // @todo Should global edit count instead?
+               'editcount',
+               'registrationdate',
+               // Signature could be global, but links in it are too likely to 
break.
+               'nickname',
+               'fancysig',
+       ];
+
+       /**
+        * Preference types that we should not add a checkbox for
+        * @var array
+        */
+       protected $typeBlacklist = [
+               'info',
+               'hidden',
+               'api',
+       ];
+
+       /**
+        * Preference classes that are allowed to be global
+        * @var array
+        */
+       protected $classWhitelist = [
+               'HTMLSelectOrOtherField',
+               'CirrusSearch\HTMLCompletionProfileSettings',
+               'NewHTMLCheckField',
+               'HTMLFeatureField',
+       ];
+
+       /**
+        * Set the preferences user.
+        * Note that not many of this class's methods use this, and you have to 
pass $user again.
+        * @TODO This should really be higher up the class hierarchy.
+        * @param User $user The user.
+        */
+       public function setUser( User $user ) {
+               $this->user = $user;
+       }
+
+       /**
+        * Get all user preferences.
+        * @param User $user The user.
+        * @param IContextSource $context The preferences page.
+        * @return array|null
+        */
+       public function getFormDescriptor( User $user, IContextSource $context 
) {
+               $this->setUser( $user );
+               $globalPrefNames = array_keys( 
$this->getGlobalPreferencesValues() );
+               $preferences = parent::getFormDescriptor( $user, $context );
+               if ( $this->onGlobalPrefsPage() ) {
+                       return $this->getPreferencesGlobal( $preferences, 
$globalPrefNames );
+               }
+               return $this->getPreferencesLocal( $preferences, 
$globalPrefNames );
+       }
+
+       /**
+        * Add help-text to the local preferences where they're globalized,
+        * and add the link to Special:GlobalPreferences to the personal 
preferences tab.
+        * @param mixed[][] $preferences The preferences array.
+        * @param string[] $globalPrefNames The names of those preferences that 
are already global.
+        * @return mixed[][]
+        */
+       protected function getPreferencesLocal( $preferences, $globalPrefNames 
) {
+               foreach ( $preferences as $name => $def ) {
+                       // If this has been set globally.
+                       if ( in_array( $name, $globalPrefNames ) ) {
+                               // Disable this preference.
+                               $preferences[$name]['disabled'] = true;
+
+                               // Append a help message.
+                               $help = '';
+                               if ( isset( $preferences[$name]['help-message'] 
) ) {
+                                       $help .= wfMessage( 
$preferences[$name]['help-message'] )->parse() . '<br />';
+                               } elseif ( isset( $preferences[$name]['help'] ) 
) {
+                                       $help .= $preferences[$name]['help'] . 
'<br />';
+                               }
+
+                               // Create a link to the relevant section of 
GlobalPreferences.
+                               $section = substr( $def['section'], 0, strpos( 
$def['section'], '/' ) );
+                               $secFragment = 'mw-prefsection-' . $section;
+
+                               // Set the new full help text.
+                               $help .= wfMessage( 'globalprefs-set-globally', 
[ $secFragment ] )->parse();
+                               $preferences[$name]['help'] = $help;
+                               unset( $preferences[$name]['help-message'] );
+                       }
+               }
+
+               // Add a link to GlobalPreferences to the local preferences 
form.
+               $linkRenderer = 
MediaWikiServices::getInstance()->getLinkRenderer();
+               $preferences['global-info'] = [
+                       'type' => 'info',
+                       'section' => 'personal/info',
+                       'label-message' => 'globalprefs-info-label',
+                       'raw' => true,
+                       'default' => $linkRenderer->makeKnownLink(
+                               SpecialPage::getTitleFor( 'GlobalPreferences' ),
+                               wfMessage( 'globalprefs-info-link' )->escaped()
+                       ),
+               ];
+
+               return $preferences;
+       }
+
+       /**
+        * Add the '-global' counterparts to all preferences.
+        * @param mixed[][] $preferences The preferences array.
+        * @param string[] $globalPrefNames The names of those preferences that 
are already global.
+        * @return mixed[][]
+        */
+       protected function getPreferencesGlobal( $preferences, $globalPrefNames 
) {
+               // Add all corresponding new global fields.
+               $allPrefs = [];
+               foreach ( $preferences as $pref => $def ) {
+                       // Ignore unwanted preferences.
+                       if ( !$this->isGlobalizablePreference( $pref, $def ) ) {
+                               continue;
+                       }
+                       // Create the new preference.
+                       $allPrefs[$pref.'-global'] = [
+                               'type' => 'toggle',
+                               // Make the tooltip and the label the same, 
because the label is normally hidden.
+                               'tooltip' => 'globalprefs-check-label',
+                               'label-message' => 
'tooltip-globalprefs-check-label',
+                               'default' => in_array( $pref, $globalPrefNames 
),
+                               'section' => $def['section'],
+                               'cssclass' => 'mw-globalprefs-global-check 
mw-globalprefs-checkbox-for-' . $pref,
+                       ];
+
+                       $allPrefs[$pref] = $def;
+               }
+               return $allPrefs;
+       }
+
+       /**
+        * Checks whether the given preference is globalizable.
+        *
+        * @param string $name Preference name
+        * @param mixed[] &$info Preference description, by reference to avoid 
unnecessary cloning
+        * @return bool
+        */
+       protected function isGlobalizablePreference( $name, &$info ) {
+               // Preferences can opt out of being globalized by setting the 
'noglobal' flag.
+               $hasOptedOut = ( isset( $info['noglobal'] ) && 
$info['noglobal'] === true );
+
+               $isAllowedType = isset( $info['type'] )
+                                                && !in_array( $info['type'], 
$this->typeBlacklist )
+                                                && !in_array( $name, 
$this->prefsBlacklist );
+
+               $isAllowedClass = isset( $info['class'] )
+                                                 && in_array( $info['class'], 
$this->classWhitelist );
+
+               $endsInGlobal = ( substr( $name, -strlen( '-global' ) ) === 
'-global' );
+
+               return !$hasOptedOut && !$endsInGlobal && ( $isAllowedType || 
$isAllowedClass );
+       }
+
+       /**
+        * Checks if the user is globalized.
+        * @return bool
+        */
+       public function isUserGlobalized() {
+               if ( $this->user->isAnon() ) {
+                       // No prefs for anons, sorry :(
+                       return false;
+               }
+               return $this->getUserID() !== 0;
+       }
+
+       /**
+        * Gets the user's ID that we're using in the table
+        * Returns 0 if the user is not global
+        * @return int
+        */
+       public function getUserID() {
+               $lookup = CentralIdLookup::factory();
+               return $lookup->centralIdFromLocalUser( $this->user, 
CentralIdLookup::AUDIENCE_RAW );
+       }
+
+       /**
+        * Get the user's global preferences.
+        * @return string[]|bool Array keyed by preference name, or false if 
not found.
+        */
+       public function getGlobalPreferencesValues() {
+               $id = $this->getUserID();
+               if ( !$id ) {
+                       return false;
+               }
+               $storage = new Storage( $id );
+               return $storage->load();
+       }
+
+       /**
+        * Save the user's global preferences.
+        * @param array $newGlobalPrefs Array keyed by preference name.
+        * @return bool True on success, false if the user isn't global.
+        */
+       public function setGlobalPreferences( $newGlobalPrefs ) {
+               $id = $this->getUserID();
+               if ( !$id ) {
+                       return false;
+               }
+               $storage = new Storage( $this->getUserID() );
+               $storage->save( $newGlobalPrefs );
+               $this->user->clearInstanceCache();
+               return true;
+       }
+
+       /**
+        * Deletes all of a user's global preferences.
+        * Assumes that the user is globalized.
+        */
+       public function resetGlobalUserSettings() {
+               $storage = new Storage( $this->getUserID() );
+               $storage->delete();
+       }
+
+       /**
+        * Convenience function to check if we're on the global prefs page.
+        * @param IContextSource $context The context to use; if not set main 
request context is used.
+        * @return bool
+        */
+       public function onGlobalPrefsPage( $context = null ) {
+               $context = $context ?: RequestContext::getMain();
+               return $context->getTitle() && $context->getTitle()->isSpecial( 
'GlobalPreferences' );
+       }
+
+       /**
+        * Convenience function to check if we're on the local
+        * prefs page
+        *
+        * @param IContextSource $context The context to use; if not set main 
request context is used.
+        * @return bool
+        */
+       public function onLocalPrefsPage( $context = null ) {
+               $context = $context ?: RequestContext::getMain();
+               return $context->getTitle()
+               && $context->getTitle()->isSpecial( 'Preferences' );
+       }
+}
diff --git a/includes/GlobalPreferencesForm.php 
b/includes/GlobalPreferencesForm.php
new file mode 100644
index 0000000..5c36867
--- /dev/null
+++ b/includes/GlobalPreferencesForm.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace GlobalPreferences;
+
+use Html;
+use IContextSource;
+use PreferencesForm;
+
+/**
+ * The GlobalPreferencesForm changes the display format, and adds section 
headers linking back to
+ * the local-preferences form.
+ *
+ * @package GlobalPreferences
+ */
+class GlobalPreferencesForm extends PreferencesForm {
+
+       /**
+        * Build a new GlobalPreferencesForm from an array of field attributes, 
and force it to be
+        * have a 'div' display format.
+        *
+        * @param array $descriptor Array of Field constructs, as described 
above.
+        * @param IContextSource $context The context of the form.
+        * @param string $messagePrefix A prefix to go in front of default 
messages.
+        */
+       public function __construct( $descriptor, IContextSource $context = 
null, $messagePrefix = '' ) {
+               parent::__construct( $descriptor, $context, $messagePrefix );
+               $this->setDisplayFormat( 'div' );
+       }
+
+       /**
+        * Get the whole body of the form, adding the global preferences header 
text to the top of each
+        * section. Javascript will later add the 'select all' checkbox to this 
header.
+        * @return string
+        */
+       function getBody() {
+               // Add help text to the top of every section.
+               foreach ( $this->getPreferenceSections() as $section ) {
+                       $colHeaderText = Html::element(
+                               'span',
+                               [ 'class' => 'col-header' ],
+                               $this->getMessage( 
'tooltip-globalprefs-check-label' )
+                       );
+                       $secHeader = Html::rawElement(
+                               'div',
+                               [ 'class' => 'globalprefs-section-header' ],
+                               $colHeaderText
+                       );
+                       $this->addHeaderText( $secHeader, $section );
+               }
+               return parent::getBody();
+       }
+}
diff --git a/includes/Hooks.php b/includes/Hooks.php
index 5a461fb..7f04f90 100644
--- a/includes/Hooks.php
+++ b/includes/Hooks.php
@@ -3,118 +3,56 @@
 namespace GlobalPreferences;
 
 use DatabaseUpdater;
-use Linker;
+use Language;
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\MediaWikiServices;
 use PreferencesForm;
-use SpecialPage;
 use User;
 
 class Hooks {
 
        /**
-        * "bad" preferences that we should remove from
-        * Special:GlobalPrefs
-        * @var array
-        */
-       protected static $prefsBlacklist = [
-               // Stored in user table, doesn't work yet
-               'realname',
-               // @todo Show CA user id / shared user table id?
-               'userid',
-               // @todo Show CA global groups instead?
-               'usergroups',
-               // @todo Should global edit count instead?
-               'editcount',
-               'registrationdate',
-       ];
-
-       /**
-        * Preference types that we should not add a checkbox for
-        * @var array
-        */
-       protected static $typeBlacklist = [
-               'info',
-               'hidden',
-               'api',
-       ];
-
-       /**
-        * Preference classes that are allowed to be global
-        * @var array
-        */
-       protected static $classWhitelist = [
-               'HTMLSelectOrOtherField',
-               'CirrusSearch\HTMLCompletionProfileSettings',
-               'NewHTMLCheckField',
-               'HTMLFeatureField',
-       ];
-
-       /**
-        * @FIXME This is terrible
-        */
-       public static function onExtensionFunctions() {
-               global $wgHooks;
-               // Register this as late as possible!
-               $wgHooks['GetPreferences'][] = self::class . 
'::onGetPreferences';
-       }
-
-       /**
-        * Load our global prefs
+        * Load global preferences.
         * @link https://www.mediawiki.org/wiki/Manual:Hooks/UserLoadOptions
         * @param User $user The user for whom options are being loaded.
         * @param array &$options The user's options; can be modified.
-        * @return bool
         */
        public static function onUserLoadOptions( User $user, &$options ) {
-               $id = GlobalPreferences::getUserID( $user );
-               if ( !$id ) {
+               /** @var GlobalPreferencesFactory $globalPreferences */
+               $globalPreferences = 
MediaWikiServices::getInstance()->getPreferencesFactory();
+               $globalPreferences->setUser( $user );
+               if ( !$globalPreferences->isUserGlobalized() ) {
                        // Not a global user.
-                       return true;
+                       return;
                }
 
-               $dbr = GlobalPreferences::getPrefsDB( DB_REPLICA );
-               $res = $dbr->select(
-                       'global_preferences',
-                       [ 'gp_property', 'gp_value' ],
-                       [ 'gp_user' => $id ],
-                       __METHOD__
-               );
-
-               $user->mGlobalPrefs = [];
-               $user->mLocalPrefs = [];
-
-               foreach ( $res as $row ) {
-                       if ( isset( $user->mOptions[$row->gp_property] ) ) {
-                               // Store the local one we will override
-                               $user->mLocalPrefs[$row->gp_property] = 
$user->mOptions[$row->gp_property];
-                       }
-                       $options[$row->gp_property] = $row->gp_value;
-                       $user->mGlobalPrefs[] = $row->gp_property;
+               // Overwrite all options that have a global counterpart.
+               foreach ( $globalPreferences->getGlobalPreferencesValues() as 
$optName => $globalValue ) {
+                       $options[ $optName ] = $globalValue;
                }
-
-               return true;
        }
 
        /**
-        * Don't save global prefs
-        * @link https://www.mediawiki.org/wiki/Manual:Hooks/UserSaveOptions
-        * @param User $user The user for whom options are being saved.
-        * @param array &$options The user's options; can be modified.
-        * @return bool
+        * When saving a user's options, remove any global ones and never save 
any on the Global
+        * Preferences page. Global options are saved separately, in the 
PreferencesFormPreSave hook.
+        * @param User $user The user. Not used.
+        * @param string[] &$options The user's options.
+        * @return bool False if nothing changed, true otherwise.
         */
        public static function onUserSaveOptions( User $user, &$options ) {
-               if ( GlobalPreferences::onGlobalPrefsPage() ) {
+               /** @var GlobalPreferencesFactory $preferencesFactory */
+               $preferencesFactory = 
MediaWikiServices::getInstance()->getPreferencesFactory();
+               $preferencesFactory->setUser( $user );
+               if ( $preferencesFactory->onGlobalPrefsPage() ) {
                        // It shouldn't be possible to save local options here,
                        // but never save on this page anyways.
                        return false;
                }
 
-               foreach ( $user->mGlobalPrefs as $pref ) {
-                       if ( isset( $options[$pref] ) ) {
-                               unset( $options[$pref] );
-                       }
-                       // But also save prefs we might have overrode...
-                       if ( isset( $user->mLocalPrefs[$pref] ) ) {
-                               $options[$pref] = $user->mLocalPrefs[$pref];
+               foreach ( $options as $optName => $optVal ) {
+                       // Ignore if ends in "-global".
+                       if ( substr( $optName, -strlen( '-global' ) ) === 
'-global' ) {
+                               unset( $options[ $optName ] );
                        }
                }
 
@@ -135,17 +73,18 @@
                User $user,
                &$result
        ) {
-               if ( !GlobalPreferences::onGlobalPrefsPage( $form ) ) {
+               /** @var GlobalPreferencesFactory $preferencesFactory */
+               $preferencesFactory = 
MediaWikiServices::getInstance()->getPreferencesFactory();
+               if ( !$preferencesFactory->onGlobalPrefsPage( $form ) ) {
                        // Don't interfere with local preferences
                        return true;
                }
 
-               $rows = [];
                $prefs = [];
                foreach ( $formData as $name => $value ) {
                        if ( substr( $name, -strlen( 'global' ) ) === 'global' 
&& $value === true ) {
                                $realName = substr( $name, 0, -strlen( 
'-global' ) );
-                               if ( isset( $formData[$realName] ) && 
!in_array( $realName, self::$prefsBlacklist ) ) {
+                               if ( isset( $formData[$realName] ) ) {
                                        $prefs[$realName] = 
$formData[$realName];
                                } else {
                                        // FIXME: Handle checkbox matrixes 
properly
@@ -157,27 +96,10 @@
                        }
                }
 
-               $id = GlobalPreferences::getUserID( $user );
-               foreach ( $prefs as $prop => $value ) {
-                       $rows[] = [
-                               'gp_user' => $id,
-                               'gp_property' => $prop,
-                               'gp_value' => $value,
-                       ];
-
-               }
-
-               // Reset preferences, and then save new ones
-               GlobalPreferences::resetGlobalUserSettings( $user );
-               if ( $rows ) {
-                       $dbw = GlobalPreferences::getPrefsDB( DB_MASTER );
-                       $dbw->replace(
-                               'global_preferences',
-                               [ 'gp_user', 'gp_property' ],
-                               $rows,
-                               __METHOD__
-                       );
-               }
+               /** @var GlobalPreferencesFactory $preferencesFactory */
+               $preferencesFactory = 
MediaWikiServices::getInstance()->getPreferencesFactory();
+               $preferencesFactory->setUser( $user );
+               $preferencesFactory->setGlobalPreferences( $prefs );
 
                return false;
        }
@@ -199,104 +121,23 @@
        }
 
        /**
-        * @link https://www.mediawiki.org/wiki/Manual:Hooks/GetPreferences
-        * @param User $user User whose preferences are being modified.
-        * @param array &$prefs Preferences description array, to be fed to an 
HTMLForm object.
-        * @return bool
+        * Replace the PreferencesFactory service with the 
GlobalPreferencesFactory.
+        * @link https://www.mediawiki.org/wiki/Manual:Hooks/MediaWikiServices
+        * @param MediaWikiServices $services The services object to use.
         */
-       public static function onGetPreferences( User $user, &$prefs ) {
-               if ( !GlobalPreferences::isUserGlobalized( $user ) ) {
-                       return true;
-               }
-
-               if ( GlobalPreferences::onGlobalPrefsPage() ) {
-                       if ( !isset( $user->mGlobalPrefs ) ) {
-                               // Just in case the user hasn't been loaded 
yet. Triggers User::loadOptions.
-                               $user->getOption( '' );
-                       }
-                       foreach ( $prefs as $name => $info ) {
-                               // Preferences can opt out of being globalized 
by setting the 'noglobal' flag.
-                               $hasOptedOut = ( isset( $info['noglobal'] ) && 
$info['noglobal'] === true );
-                               if ( $hasOptedOut ) {
-                                       unset( $prefs[ $name ] );
-                                       continue;
-                               }
-
-                               // FIXME: This whole code section sucks
-                               if ( !isset( $prefs["$name-global"] )
-                                       && self::isGlobalizablePreference( 
$name, $info )
-                               ) {
-                                       $prefs = wfArrayInsertAfter( $prefs, [
-                                               "$name-global" => [
-                                                       'type' => 'toggle',
-                                                       'label-message' => 
'globalprefs-check-label',
-                                                       'default' => in_array( 
$name, $user->mGlobalPrefs ),
-                                                       'section' => 
$info['section'],
-                                                       'cssclass' => 
'mw-globalprefs-global-check',
-                                               ]
-                                       ], $name );
-                               } elseif ( in_array( $name, 
self::$prefsBlacklist ) ) {
-                                       $prefs[$name]['type'] = 'hidden';
-                               }
-                       }
-               } elseif ( GlobalPreferences::onLocalPrefsPage() ) {
-                       if ( !isset( $user->mGlobalPrefs ) ) {
-                               // Just in case the user hasn't been loaded 
yet. Triggers User::loadOptions.
-                               $user->getOption( '' );
-                       }
-                       foreach ( $user->mGlobalPrefs as $name ) {
-                               if ( isset( $prefs[$name] ) ) {
-                                       $prefs[$name]['disabled'] = 'disabled';
-                                       // Append a help message.
-                                       $help = '';
-                                       if ( isset( 
$prefs[$name]['help-message'] ) ) {
-                                               $help .= wfMessage( 
$prefs[$name]['help-message'] )->parse() . '<br />';
-                                       } elseif ( isset( $prefs[$name]['help'] 
) ) {
-                                               $help .= $prefs[$name]['help'] 
. '<br />';
-                                       }
-
-                                       $help .= wfMessage( 
'globalprefs-set-globally' )->parse();
-                                       $prefs[$name]['help'] = $help;
-                                       unset( $prefs[$name]['help-message'] );
-
-                               }
-                       }
-               }
-
-               // Provide a link to Special:GlobalPreferences
-               // if we're not on that page.
-               if ( !GlobalPreferences::onGlobalPrefsPage() ) {
-                       $prefs['global-info'] = [
-                               'type' => 'info',
-                               'section' => 'personal/info',
-                               'label-message' => 'globalprefs-info-label',
-                               'raw' => true,
-                               'default' => Linker::link(
-                                       SpecialPage::getTitleFor( 
'GlobalPreferences' ),
-                                       wfMessage( 'globalprefs-info-link' 
)->escaped()
-                               ),
-                       ];
-               }
-
-               return true;
-       }
-
-       /**
-        * Checks whether the given preference is localizable
-        *
-        * @param string $name Preference name
-        * @param array|mixed $info Preference description, by reference to 
avoid unnecessary cloning
-        * @return bool
-        */
-       private static function isGlobalizablePreference( $name, &$info ) {
-               $isAllowedType = isset( $info['type'] )
-                       && !in_array( $info['type'], self::$typeBlacklist )
-                       && !in_array( $name, self::$prefsBlacklist );
-
-               $isAllowedClass = isset( $info['class'] )
-                       && in_array( $info['class'], self::$classWhitelist );
-
-               return substr( $name, -strlen( 'global' ) ) !== 'global'
-                       && ( $isAllowedType || $isAllowedClass );
+       public static function onMediaWikiServices( MediaWikiServices $services 
) {
+               $services->redefineService( 'PreferencesFactory', function ( 
MediaWikiServices $services ) {
+                       global $wgContLang, $wgLanguageCode;
+                       $wgContLang = Language::factory( $wgLanguageCode );
+                       $wgContLang->initContLang();
+                       $authManager = AuthManager::singleton();
+                       $linkRenderer = 
$services->getLinkRendererFactory()->create();
+                       $config = $services->getMainConfig();
+                       return new GlobalPreferencesFactory(
+                               $config, $wgContLang, $authManager, 
$linkRenderer
+                       );
+               } );
+               // Now instantiate the new Preferences, to prevent it being 
overwritten.
+               $services->getPreferencesFactory();
        }
 }
diff --git a/includes/SpecialGlobalPreferences.php 
b/includes/SpecialGlobalPreferences.php
index ec45a86..9cc3d6b 100644
--- a/includes/SpecialGlobalPreferences.php
+++ b/includes/SpecialGlobalPreferences.php
@@ -5,13 +5,18 @@
 use DerivativeContext;
 use ErrorPageError;
 use HTMLForm;
+use IContextSource;
+use MediaWiki\MediaWikiServices;
 use PermissionsError;
+use PreferencesForm;
 use SpecialPage;
 use SpecialPreferences;
+use User;
 use UserNotLoggedIn;
 
 class SpecialGlobalPreferences extends SpecialPreferences {
-       function __construct() {
+
+       public function __construct() {
                SpecialPage::__construct( 'GlobalPreferences' );
        }
 
@@ -34,7 +39,10 @@
                        $this->setHeaders();
                        throw new UserNotLoggedIn();
                }
-               if ( !GlobalPreferences::isUserGlobalized( $this->getUser() ) ) 
{
+               /** @var GlobalPreferencesFactory $globalPreferencesFactory */
+               $globalPreferencesFactory = 
MediaWikiServices::getInstance()->getPreferencesFactory();
+               $globalPreferencesFactory->setUser( $this->getUser() );
+               if ( !$globalPreferencesFactory->isUserGlobalized() ) {
                        $this->setHeaders();
                        throw new ErrorPageError( 'globalprefs-error-header', 
'globalprefs-notglobal' );
                }
@@ -52,6 +60,18 @@
                $this->getOutput()->addModuleStyles( 
'ext.GlobalPreferences.special.nojs' );
                $this->getOutput()->addModules( 'ext.GlobalPreferences.special' 
);
                parent::execute( $par );
+       }
+
+       /**
+        * Get the preferences form to use.
+        * @param User $user The user.
+        * @param IContextSource $context The context.
+        * @return PreferencesForm|HTMLForm
+        */
+       protected function getFormObject( $user, IContextSource $context ) {
+               $preferencesFactory = 
MediaWikiServices::getInstance()->getPreferencesFactory();
+               $form = $preferencesFactory->getForm( $user, $context, 
GlobalPreferencesForm::class );
+               return $form;
        }
 
        /**
@@ -90,6 +110,15 @@
        }
 
        /**
+        * Adds help link with an icon via page indicators.
+        * @param string $to Ignored.
+        * @param bool $overrideBaseUrl Whether $url is a full URL, to avoid 
MW.o.
+        */
+       public function addHelpLink( $to, $overrideBaseUrl = false ) {
+               parent::addHelpLink( 'Help:Extension:GlobalPreferences', 
$overrideBaseUrl );
+       }
+
+       /**
         * Handle reset submission (subpage '/reset').
         * @param string[] $formData The submitted data (not used).
         * @return bool
@@ -101,7 +130,10 @@
                        throw new PermissionsError( 'editmyoptions' );
                }
 
-               GlobalPreferences::resetGlobalUserSettings( $this->getUser() );
+               /** @var GlobalPreferencesFactory $preferencesFactory */
+               $preferencesFactory = 
MediaWikiServices::getInstance()->getPreferencesFactory();
+               $preferencesFactory->setUser( $this->getUser() );
+               $preferencesFactory->resetGlobalUserSettings();
 
                $url = $this->getTitle()->getFullURL( 'success' );
 
@@ -109,5 +141,4 @@
 
                return true;
        }
-
 }
diff --git a/includes/Storage.php b/includes/Storage.php
new file mode 100644
index 0000000..a9c32eb
--- /dev/null
+++ b/includes/Storage.php
@@ -0,0 +1,103 @@
+<?php
+/**
+ * This file contains only the Storage class.
+ * @package GlobalPreferences
+ */
+
+namespace GlobalPreferences;
+
+use Wikimedia\Rdbms\Database;
+
+/**
+ * This class handles all database storage of global preferences.
+ * @package GlobalPreferences
+ */
+class Storage {
+
+       /** The non-prefixed name of the global preferences database table. */
+       const TABLE_NAME = 'global_preferences';
+
+       /** @var int The global user ID. */
+       protected $userId;
+
+       /**
+        * Create a new Global Preferences Storage object for a given user.
+        * @param int $userId The global user ID.
+        */
+       public function __construct( $userId ) {
+               $this->userId = $userId;
+       }
+
+       /**
+        * Get the user's global preferences.
+        * @return string[] Keyed by the preference name.
+        */
+       public function load() {
+               $dbr = $this->getDatabase( DB_REPLICA );
+               $res = $dbr->select(
+                       static::TABLE_NAME,
+                       [ 'gp_property', 'gp_value' ],
+                       [ 'gp_user' => $this->userId ],
+                       __METHOD__
+               );
+               $preferences = [];
+               foreach ( $res as $row ) {
+                       $preferences[$row->gp_property] = $row->gp_value;
+               }
+               return $preferences;
+       }
+
+       /**
+        * Save a set of global preferences. All existing preferences will be 
deleted before the new
+        * ones are saved.
+        * @param string[] $newPrefs Keyed by the preference name.
+        */
+       public function save( $newPrefs ) {
+               // Assemble the records to save.
+               $rows = [];
+               foreach ( $newPrefs as $prop => $value ) {
+                       $rows[] = [
+                               'gp_user' => $this->userId,
+                               'gp_property' => $prop,
+                               'gp_value' => $value,
+                       ];
+               }
+               // Delete all global preferences, and then save new ones.
+               $this->delete();
+               if ( $rows ) {
+                       $dbw = $this->getDatabase( DB_MASTER );
+                       $dbw->replace(
+                               static::TABLE_NAME,
+                               [ 'gp_user', 'gp_property' ],
+                               $rows,
+                               __METHOD__
+                       );
+               }
+       }
+
+       /**
+        * Delete all of this user's global preferences.
+        */
+       public function delete() {
+               $db = $this->getDatabase( DB_MASTER );
+               $db->delete(
+                       static::TABLE_NAME,
+                       [ 'gp_user' => $this->userId ],
+                       __METHOD__
+               );
+       }
+
+       /**
+        * Get the database object pointing to the Global Preferences database.
+        * @param int $type One of the DB_* constants
+        * @return Database
+        */
+       protected function getDatabase( $type = DB_REPLICA ) {
+               global $wgGlobalPreferencesDB;
+               if ( $wgGlobalPreferencesDB ) {
+                       return wfGetDB( $type, [], $wgGlobalPreferencesDB );
+               } else {
+                       return wfGetDB( $type );
+               }
+       }
+}
diff --git a/resources/ext.GlobalPreferences.special.css 
b/resources/ext.GlobalPreferences.special.css
index ef5a087..4d42ba8 100644
--- a/resources/ext.GlobalPreferences.special.css
+++ b/resources/ext.GlobalPreferences.special.css
@@ -1,9 +1,15 @@
-.globalprefs-disabled {
-       opacity: 0.5;
-}
 .globalprefs-hover {
-       background-color: rgba( 255, 224, 97, 0.4 );
+       background-color: #eaecf0;
 }
-.mw-special-GlobalPreferences form.mw-htmlform table {
-       border-collapse: collapse;
+
+/* Style fixes for Skin:Vector. The ID selector is whitelisted in .stylelintrc 
because Vector uses it. */
+body.skin-vector #preferences fieldset.ext-globalpreferences-select-all {
+       padding-bottom: 0;
+       margin-bottom: 0;
+       border: 0;
+}
+
+/* Style fixes for Extension:BetaFeatures. */
+body.skin-vector #preferences #mw-prefsection-betafeatures 
fieldset.ext-globalpreferences-select-all {
+       padding-left: 0;
 }
diff --git a/resources/ext.GlobalPreferences.special.js 
b/resources/ext.GlobalPreferences.special.js
index 59658de..ed1d045 100644
--- a/resources/ext.GlobalPreferences.special.js
+++ b/resources/ext.GlobalPreferences.special.js
@@ -1,7 +1,11 @@
 ( function ( mw, $ ) {
        'use strict';
 
-       $( 'input.mw-globalprefs-global-check' ).on( 'change', function () {
+       /**
+        * When one of the global checkboxes is changed enable or disable its 
matching preference.
+        * Also highlight the relevant preference when hovering on the checkbox.
+        */
+       function onChangeGlobalCheckboxes() {
                var $labels,
 
                        // Find the name (without the '-global' suffix, but 
with the 'wp' prefix).
@@ -11,44 +15,93 @@
                        // Is this preference enabled globally?
                        enabled = $( this ).prop( 'checked' ),
 
-                       // The table rows relating to this preference
-                       // (two or three rows, depending on whether there's a 
help row).
+                       // This selector is required because there's no common 
class on these.
+                       fieldSelector = '[class^="mw-htmlform-field-"]',
+
+                       // The form 'rows' (which are adjacent divs) relating 
to this preference
+                       // (two or three rows, depending on whether there's a 
help row, all contained in $rows).
                        $globalCheckRow,
-                       $labelRow,
-                       $rows;
+                       $mainFieldRow,
+                       $rows,
+
+                       // The current preference's inputs (can be multiple, 
and not all will have the same name).
+                       $inputs = $( ':input[name="' + name + '"]' ).parents( 
'.mw-input' ).find( ':input' );
 
                // All the labels for this preference (not all have for='').
-               $labels = $( 'label[for^=\'mw-input-' + name + '\']' )
-                       .closest( 'tr' )
+               $labels = $inputs
+                       .closest( fieldSelector )
                        .find( 'label' )
-                       .not( '[for$=\'-global\']' );
+                       .not( '[for$="-global"]' );
+
+               // Collect the related rows. The main field row is sometimes 
followed by a help-tip row.
+               $globalCheckRow = $( this ).closest( fieldSelector );
+               $mainFieldRow = $labels.closest( fieldSelector );
+               $rows = $().add( $globalCheckRow ).add( $mainFieldRow );
+               if ( $mainFieldRow.next().hasClass( 'htmlform-tip' ) ) {
+                       $rows = $rows.add( $mainFieldRow.next() );
+               }
 
                // Disable or enable the related preferences inputs.
-               $( ':input[name=\'' + name + '\']' ).prop( 'disabled', !enabled 
);
+               $inputs.prop( 'disabled', !enabled );
                if ( enabled ) {
                        $labels.removeClass( 'globalprefs-disabled' );
                } else {
                        $labels.addClass( 'globalprefs-disabled' );
                }
 
-               // Collect the related rows. The latter two in the $rows array 
will often be the same element.
-               $globalCheckRow = $( this ).closest( 'tr' );
-               $labelRow = $labels.closest( 'tr' );
-               $rows = $( [
-                       $labelRow[ 0 ],
-                       $labelRow.next()[ 0 ],
-                       $globalCheckRow[ 0 ]
-               ] );
-
                // Add a class on hover, to highlight the related rows.
-               $( this ).add( 'label[for=\'' + $( this ).attr( 'id' ) + '\']' 
).hover( function () {
-                       // Hover on.
-                       $rows.addClass( 'globalprefs-hover' );
-               }, function () {
-                       // Hover off.
-                       $rows.removeClass( 'globalprefs-hover' );
+               $( this ).add( 'label[for="' + $( this ).attr( 'id' ) + '"]' 
).on( {
+                       mouseenter: function () {
+                               $rows.addClass( 'globalprefs-hover' );
+                       },
+                       mouseleave: function () {
+                               $rows.removeClass( 'globalprefs-hover' );
+                       }
                } );
+       }
 
-       } ).change();
+       /**
+        * Add select all behaviour to a group of checkboxes.
+        * @param {jQuery} $selectAll The select-all checkbox.
+        * @param {jQuery} $targets The target checkboxes.
+        */
+       function selectAllCheckboxes( $selectAll, $targets ) {
+               // Handle the select-all box changing.
+               $selectAll.on( 'change', function () {
+                       $targets.prop( 'checked', $( this ).prop( 'checked' ) 
).change();
+               } );
+               // Handle any of the targets changing.
+               $targets.on( 'change', function () {
+                       var allSelected = true;
+                       $targets.each( function () {
+                               allSelected = allSelected && $( this ).prop( 
'checked' );
+                       } );
+                       $selectAll.prop( 'checked', allSelected );
+               } );
+       }
 
+       /**
+        * Add the 'select all' checkbox to the form section headers.
+        */
+       function addSelectAllToHeader() {
+               // For each preferences form tab, add a select-all checkbox to 
the header.
+               $( '.globalprefs-section-header' ).each( function () {
+                       var selectAll = mw.message( 'globalprefs-select-all' ),
+                               $checkbox,
+                               $allGlobalCheckboxes;
+                       // Wrap the checkbox in a fieldset so it acts/looks the 
same as all the global checkboxes.
+                       $checkbox = $( '<fieldset 
class="ext-globalpreferences-select-all"><label><input type="checkbox" /> ' + 
selectAll + '</label></fieldset>' );
+                       $( this ).append( $checkbox );
+
+                       // Determine all the matching checkboxes.
+                       $allGlobalCheckboxes = $( this ).parent( 'fieldset' 
).find( '.mw-globalprefs-global-check:checkbox' );
+
+                       // Enable the select-all behaviour.
+                       selectAllCheckboxes( $checkbox.find( ':checkbox' ), 
$allGlobalCheckboxes );
+               } );
+       }
+
+       // Activate the above functions.
+       addSelectAllToHeader();
+       $( 'input.mw-globalprefs-global-check' ).on( 'change', 
onChangeGlobalCheckboxes ).change();
 }( mediaWiki, jQuery ) );
diff --git a/resources/ext.GlobalPreferences.special.nojs.css 
b/resources/ext.GlobalPreferences.special.nojs.css
index 71deae3..e8a0f78 100644
--- a/resources/ext.GlobalPreferences.special.nojs.css
+++ b/resources/ext.GlobalPreferences.special.nojs.css
@@ -1,6 +1,80 @@
-input.mw-globalprefs-global-check {
-       margin-left: 2em;
+.mw-htmlform-nolabel .mw-label {
+       display: none;
 }
-tr.mw-globalprefs-global-check {
+
+.mw-globalprefs-global-check .mw-label,
+.mw-globalprefs-global-check label {
+       display: none;
+}
+
+/* For core fields */
+.mw-htmlform-field-HTMLAutoCompleteSelectField,
+.mw-htmlform-field-HTMLButtonField,
+.mw-htmlform-field-HTMLCheckField,
+.mw-htmlform-field-HTMLCheckMatrix,
+.mw-htmlform-field-HTMLComboboxField,
+.mw-htmlform-field-HTMLDateTimeField,
+.mw-htmlform-field-HTMLEditTools,
+.mw-htmlform-field-HTMLFloatField,
+.mw-htmlform-field-HTMLFormFieldCloner,
+.mw-htmlform-field-HTMLFormFieldWithButton,
+.mw-htmlform-field-HTMLHiddenField,
+.mw-htmlform-field-HTMLInfoField,
+.mw-htmlform-field-HTMLIntField,
+.mw-htmlform-field-HTMLMultiSelectField,
+.mw-htmlform-field-HTMLRadioField,
+.mw-htmlform-field-HTMLRestrictionsField,
+.mw-htmlform-field-HTMLSelectAndOtherField,
+.mw-htmlform-field-HTMLSelectField,
+.mw-htmlform-field-HTMLSelectLimitField,
+.mw-htmlform-field-HTMLSelectNamespace,
+.mw-htmlform-field-HTMLSelectNamespaceWithButton,
+.mw-htmlform-field-HTMLSelectOrOtherField,
+.mw-htmlform-field-HTMLSizeFilterField,
+.mw-htmlform-field-HTMLSubmitField,
+.mw-htmlform-field-HTMLTagFilter,
+.mw-htmlform-field-HTMLTextAreaField,
+.mw-htmlform-field-HTMLTextField,
+.mw-htmlform-field-HTMLTextFieldWithButton,
+.mw-htmlform-field-HTMLTitleTextField,
+.mw-htmlform-field-HTMLUsersMultiselectField,
+.mw-htmlform-field-HTMLUserTextField,
+/* For Extension:BetaFeatures */
+.mw-htmlform-field-NewHTMLCheckField,
+.mw-htmlform-field-HTMLFeatureField,
+/* For Extension:GlobalPrefences */
+.mw-special-GlobalPreferences .htmlform-tip {
+       padding-left: 7%;
+}
+
+.mw-globalprefs-global-check {
        font-size: smaller;
+       padding-left: 0;
+       float: left;
+       width: 5%;
+}
+.mw-globalprefs-global-check input.mw-globalprefs-global-check {
+       float: none;
+       width: auto;
+}
+
+/** Make the column header a bit narrower so it looks more associated with the 
column. */
+.globalprefs-section-header .col-header {
+       display: block;
+       width: 7%;
+       text-align: center;
+}
+
+/* Style fixes for Extension:BetaFeatures. */
+fieldset#mw-prefsection-betafeatures .mw-globalprefs-global-check .mw-input {
+       /* To match .mw-htmlform-field-HTMLFeatureField .mw-input */
+       padding-top: 10px;
+       /* To match .oo-ui-fieldLayout.oo-ui-labelElement > 
.oo-ui-fieldLayout-body > .oo-ui-fieldLayout-header > .oo-ui-labelElement-label 
*/
+       font-size: 24px;
+       margin-top: 0.3em;
+}
+fieldset#mw-prefsection-betafeatures 
.mw-globalprefs-checkbox-for-betafeatures-auto-enroll .mw-input {
+       padding-top: 0;
+       font-size: inherit;
+       margin-top: 0;
 }
diff --git a/tests/phpunit/GlobalPreferencesTest.php 
b/tests/phpunit/GlobalPreferencesTest.php
new file mode 100644
index 0000000..fa43b99
--- /dev/null
+++ b/tests/phpunit/GlobalPreferencesTest.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace GlobalPreferences\Test;
+
+use GlobalPreferences\GlobalPreferencesFactory;
+use GlobalPreferences\Storage;
+use MediaWiki\MediaWikiServices;
+use MediaWikiTestCase;
+
+/**
+ * @group GlobalPreferences
+ */
+class GlobalPreferencesTest extends MediaWikiTestCase {
+
+       public function testService() {
+               $factory = 
MediaWikiServices::getInstance()->getPreferencesFactory();
+               $this->assertInstanceOf( GlobalPreferencesFactory::class, 
$factory );
+       }
+
+       public function testStorage() {
+               $user = $this->getTestUser()->getUser();
+               $gpStorage = new Storage( $user->getId() );
+
+               // No prefs to start with.
+               $this->assertEmpty( $gpStorage->load() );
+
+               // Save one, and retrieve it.
+               $gpStorage->save( [ 'testpref' => 'test' ] );
+               $this->assertCount( 1, $gpStorage->load() );
+
+               // Save different ones, and it should overwrite.
+               $gpStorage->save( [ 'testpref2' => 'test2' ] );
+               $this->assertCount( 1, $gpStorage->load() );
+               $gpStorage->save( [ 'testpref2' => 'test2', 'testpref3' => 
'test3' ] );
+               $this->assertCount( 2, $gpStorage->load() );
+
+               // Delete all
+               $gpStorage->delete();
+               $this->assertEmpty( $gpStorage->load() );
+       }
+
+       public function testUserPreference() {
+               $user = $this->getTestUser()->getUser();
+               /** @var GlobalPreferencesFactory $globalPreferences */
+               $globalPreferences = 
MediaWikiServices::getInstance()->getPreferencesFactory();
+               $globalPreferences->setUser( $user );
+
+               // Confirm the site default.
+               $this->assertEquals( 'en', $user->getOption( 'language' ) );
+
+               // Set a local preference.
+               $user->setOption( 'language', 'bn' );
+               $user->saveSettings();
+               $this->assertEquals( 'bn', $user->getOption( 'language' ) );
+
+               // Set it to be global (with a different value).
+               $globalPreferences->setGlobalPreferences( [ 'language' => 'de' 
] );
+               $this->assertEquals( [ 'language' => 'de' ], 
$globalPreferences->getGlobalPreferencesValues() );
+               $this->assertEquals( 'de', $user->getOption( 'language' ) );
+               $globalPreferences->setGlobalPreferences( [ 'language' => 'ru' 
] );
+               $this->assertEquals( 'ru', $user->getOption( 'language' ) );
+
+               // Then unglobalize it, and it should return to the local value.
+               $globalPreferences->setGlobalPreferences( [] );
+               $this->assertEquals( [], 
$globalPreferences->getGlobalPreferencesValues() );
+               // @TODO Instance caching on User doesn't clear 
User::$mOptionOverrides
+               // $this->assertEquals( 'bn', $user->getOption( 'language' ) );
+       }
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/370152
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I3c10dfeacf02367e90f84a3e572ecf3f4048e02a
Gerrit-PatchSet: 43
Gerrit-Project: mediawiki/extensions/GlobalPreferences
Gerrit-Branch: master
Gerrit-Owner: Samwilson <s...@samwilson.id.au>
Gerrit-Reviewer: Daniel Kinzler <daniel.kinz...@wikimedia.de>
Gerrit-Reviewer: MaxSem <maxsem.w...@gmail.com>
Gerrit-Reviewer: Niharika29 <nko...@wikimedia.org>
Gerrit-Reviewer: Samwilson <s...@samwilson.id.au>
Gerrit-Reviewer: Siebrand <siebr...@kitano.nl>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to