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

Change subject: Major rewrite of GlobalUserrights to unbreak it for 1.29
......................................................................


Major rewrite of GlobalUserrights to unbreak it for 1.29

This extension needed attention as it did not function at all after the
changes made to Special:Userrights with 1.29, which this extension
derives of.
This bumps the version number up to 1.5 to prevent confusion with
ShoutWiki's 1.4-SW release.
Things fixed:
* Support for SQLite
* Support for expiring group membership
* Log formatting is now done with LogFormatter instead of the previous
hack, identical to the user rights log, complete with the advantages
that brings
* User Uid's are now the Central ID. This doesn't change anything for
shared tables setups, but should make the extension easier to support on
setups that have a different Central ID provider
* Global group membership has now been abstracted similar to local group
membership
* A hook handler for UsersPagerDoBatchLookups has been added, because a
previous hook is not in MediaWiki (anymore?)
* Short array syntax used

Bug: T166436
Change-Id: I2eca420b3590ce7680cbc96c3432ff777e091d2a
---
A GlobalRightsLogFormatter.php
A GlobalUserGroupMembership.php
M GlobalUserrights.alias.php
M GlobalUserrightsHooks.php
M GlobalUserrights_body.php
A db_patches/patch-gug_expiry-field.sql
A db_patches/patch-gug_expiry-index.sql
A db_patches/patch-gug_group-field.sql
M extension.json
M global_user_groups.sql
M i18n/en.json
M i18n/qqq.json
12 files changed, 827 insertions(+), 253 deletions(-)

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



diff --git a/GlobalRightsLogFormatter.php b/GlobalRightsLogFormatter.php
new file mode 100644
index 0000000..fab20be
--- /dev/null
+++ b/GlobalRightsLogFormatter.php
@@ -0,0 +1,90 @@
+<?php
+/**
+ * Formatter for global user rights log entries.
+ *
+ * @file
+ * @ingroup Extensions
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 
2.0 or later
+ */
+
+/**
+ * This class formats global rights log entries.
+ */
+class GlobalRightsLogFormatter extends RightsLogFormatter {
+
+       /**
+        * Return the old key here for backwards compatibility.
+        * This preserves old translations and log entries
+        *
+        * @return string message key
+        */
+       protected function getMessageKey() {
+               $key = parent::getMessageKey();
+
+               $params = $this->getMessageParameters();
+               if ( !isset( $params[4] ) ) {
+                       $key = 'gur-rightslog-entry';
+               }
+
+               return $key;
+       }
+
+       protected function getMessageParameters() {
+               // This is hacky but required, because the parent 
RightsLogFormatter's method
+               // must be avoided, otherwise the group expiration date appears 
twice in the logs
+               $params = LogFormatter::getMessageParameters();
+
+               // Old entries do not contain a fourth parameter
+               if ( !isset( $params[4] ) ) {
+                       return $params;
+               }
+
+               $oldGroups = $this->makeGroupArray( $params[3] );
+               $newGroups = $this->makeGroupArray( $params[4] );
+
+               $userName = $this->entry->getTarget()->getText();
+               if ( !$this->plaintext && count( $oldGroups ) ) {
+                       foreach ( $oldGroups as &$group ) {
+                               $group = 
GlobalUserGroupMembership::getGroupMemberName( $group, $userName );
+                       }
+               }
+               if ( !$this->plaintext && count( $newGroups ) ) {
+                       foreach ( $newGroups as &$group ) {
+                               $group = 
GlobalUserGroupMembership::getGroupMemberName( $group, $userName );
+                       }
+               }
+
+               // fetch the metadata about each group membership
+               $allParams = $this->entry->getParameters();
+
+               if ( count( $oldGroups ) ) {
+                       $params[3] = [ 'raw' => $this->formatRightsList( 
$oldGroups,
+                               isset( $allParams['oldmetadata'] ) ? 
$allParams['oldmetadata'] : [] ) ];
+               } else {
+                       $params[3] = $this->msg( 'rightsnone' )->text();
+               }
+
+               if ( count( $newGroups ) ) {
+                       // Array_values is used here because of T44211
+                       // see use of array_unique in 
UserrightsPage::doSaveUserGroups on $newGroups.
+                       $params[4] = [ 'raw' => $this->formatRightsList( 
array_values( $newGroups ),
+                               isset( $allParams['newmetadata'] ) ? 
$allParams['newmetadata'] : [] ) ];
+               } else {
+                       $params[4] = $this->msg( 'rightsnone' )->text();
+               }
+
+               $params[5] = $userName;
+
+               return $params;
+       }
+
+       private function makeGroupArray( $group ) {
+               // Migrate old group params from string to array
+               if ( $group === '' ) {
+                       $group = [];
+               } elseif ( is_string( $group ) ) {
+                       $group = array_map( 'trim', explode( ',', $group ) );
+               }
+               return $group;
+       }
+}
diff --git a/GlobalUserGroupMembership.php b/GlobalUserGroupMembership.php
new file mode 100644
index 0000000..a50594e
--- /dev/null
+++ b/GlobalUserGroupMembership.php
@@ -0,0 +1,363 @@
+<?php
+/**
+ * Represents the membership of a user to a global user group.
+ *
+ * @file
+ * @ingroup Extensions
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 
2.0 or later
+ */
+
+use Wikimedia\Rdbms\IDatabase;
+
+/**
+ * Represents a "global user group membership" -- a specific instance of a 
user belonging
+ * to a global group. For example, the fact that user Mary belongs to the 
global-sysop group is a
+ * global user group membership.
+ *
+ * The class encapsulates rows in the global_user_groups table. The logic is 
low-level and
+ * doesn't run any hooks.
+ *
+ * This class inherits from UserGroupMembership for compatibility and code 
deduplication, although
+ * since UserGroupMembership does not allow proper inheriting, as the member 
variables are
+ * private, this is quite limited.
+ */
+class GlobalUserGroupMembership extends UserGroupMembership {
+       /** @var int The ID of the user who belongs to the global group */
+       private $userId;
+
+       /** @var string */
+       private $group;
+
+       /** @var string|null Timestamp of expiry in TS_MW format, or null if no 
expiry */
+       private $expiry;
+
+       /**
+        * @param int $userId The ID of the user who belongs to the group
+        * @param string $group The internal group name
+        * @param string|null $expiry Timestamp of expiry in TS_MW format, or 
null if no expiry
+        */
+       public function __construct( $userId = 0, $group = null, $expiry = null 
) {
+               parent::__construct( $userId, $group, $expiry );
+
+               $this->userId = $userId;
+               $this->group = $group;
+               $this->expiry = $expiry;
+       }
+
+       /**
+        * @return int
+        */
+       public function getUserId() {
+               return $this->userId;
+       }
+
+       /**
+        * @return string
+        */
+       public function getGroup() {
+               return $this->group;
+       }
+
+       /**
+        * @return string|null Timestamp of expiry in TS_MW format, or null if 
no expiry
+        */
+       public function getExpiry() {
+               return $this->expiry;
+       }
+
+       protected function initFromRow( $row ) {
+               $this->userId = (int)$row->gug_user;
+               $this->group = $row->gug_group;
+               $this->expiry = $row->gug_expiry === null ?
+                       null :
+                       wfTimestamp( TS_MW, $row->gug_expiry );
+       }
+
+       /**
+        * Creates a new GlobalUserGroupMembership object from a database row.
+        *
+        * @param stdClass $row The row from the global_user_groups table
+        * @return GlobalUserGroupMembership
+        */
+       public static function newFromRow( $row ) {
+               $ugm = new self();
+               $ugm->initFromRow( $row );
+               return $ugm;
+       }
+
+       /**
+        * Returns the list of user_groups fields that should be selected to 
create
+        * a new user group membership.
+        * @return array
+        */
+       public static function selectFields() {
+               return [
+                       'gug_user',
+                       'gug_group',
+                       'gug_expiry',
+               ];
+       }
+
+       public function delete( IDatabase $dbw = null ) {
+               if ( wfReadOnly() ) {
+                       return false;
+               }
+
+               if ( $dbw === null ) {
+                       $dbw = wfGetDB( DB_MASTER );
+               }
+
+               $dbw->delete(
+                       'global_user_groups',
+                       [ 'gug_user' => $this->userId, 'gug_group' => 
$this->group ],
+                       __METHOD__ );
+               if ( !$dbw->affectedRows() ) {
+                       return false;
+               }
+
+               return true;
+       }
+
+       /**
+        * Insert a user right membership into the database. When $allowUpdate 
is false,
+        * the function fails if there is a conflicting membership entry (same 
user and
+        * group) already in the table.
+        *
+        * @throws MWException
+        * @param bool $allowUpdate Whether to perform "upsert" instead of 
INSERT
+        * @param IDatabase|null $dbw If you have one available
+        * @return bool Whether or not anything was inserted
+        */
+       public function insert( $allowUpdate = false, IDatabase $dbw = null ) {
+               if ( $dbw === null ) {
+                       $dbw = wfGetDB( DB_MASTER );
+               }
+
+               // Purge old, expired memberships from the DB
+               self::purgeExpired( $dbw );
+
+               // Check that the values make sense
+               if ( $this->group === null ) {
+                       throw new UnexpectedValueException(
+                               'Don\'t try inserting an uninitialized 
GlobalUserGroupMembership object' );
+               } elseif ( $this->userId <= 0 ) {
+                       throw new UnexpectedValueException(
+                               'GlobalUserGroupMembership::insert() needs a 
positive user ID. ' .
+                               'Did you forget to add your User object to the 
database before calling addGroup()?' );
+               }
+
+               $row = $this->getDatabaseArray( $dbw );
+               $dbw->insert( 'global_user_groups', $row, __METHOD__, [ 
'IGNORE' ] );
+               $affected = $dbw->affectedRows();
+
+               // Don't collide with expired user group memberships
+               // Do this after trying to insert, in order to avoid locking
+               if ( !$affected ) {
+                       $conds = [
+                               'gug_user' => $row['gug_user'],
+                               'gug_group' => $row['gug_group'],
+                       ];
+                       // if we're unconditionally updating, check that the 
expiry is not already the
+                       // same as what we are trying to update it to; 
otherwise, only update if
+                       // the expiry date is in the past
+                       if ( $allowUpdate ) {
+                               if ( $this->expiry ) {
+                                       $conds[] = 'gug_expiry IS NULL OR 
gug_expiry != ' .
+                                                  $dbw->addQuotes( 
$dbw->timestamp( $this->expiry ) );
+                               } else {
+                                       $conds[] = 'gug_expiry IS NOT NULL';
+                               }
+                       } else {
+                               $conds[] = 'gug_expiry < ' . $dbw->addQuotes( 
$dbw->timestamp() );
+                       }
+
+                       $row = $dbw->selectRow( 'global_user_groups', 
$this::selectFields(), $conds, __METHOD__ );
+                       if ( $row ) {
+                               $dbw->update(
+                                       'global_user_groups',
+                                       [ 'gug_expiry' => $this->expiry ? 
$dbw->timestamp( $this->expiry ) : null ],
+                                       [ 'gug_user' => $row->gug_user, 
'gug_group' => $row->gug_group ],
+                                       __METHOD__ );
+                               $affected = $dbw->affectedRows();
+                       }
+               }
+
+               return $affected > 0;
+       }
+
+       /**
+        * Get an array suitable for passing to $dbw->insert() or $dbw->update()
+        * @param IDatabase $db
+        * @return array
+        */
+       protected function getDatabaseArray( IDatabase $db ) {
+               return [
+                       'gug_user' => $this->userId,
+                       'gug_group' => $this->group,
+                       'gug_expiry' => $this->expiry ? $db->timestamp( 
$this->expiry ) : null,
+               ];
+       }
+
+       /**
+        * Has the membership expired?
+        * @return bool
+        */
+       public function isExpired() {
+               if ( !$this->expiry ) {
+                       return false;
+               } else {
+                       return wfTimestampNow() > $this->expiry;
+               }
+       }
+
+       /**
+        * Purge expired memberships from the user_groups table
+        *
+        * @param IDatabase|null $dbw
+        */
+       public static function purgeExpired( IDatabase $dbw = null ) {
+               if ( wfReadOnly() ) {
+                       return;
+               }
+
+               if ( $dbw === null ) {
+                       $dbw = wfGetDB( DB_MASTER );
+               }
+
+               DeferredUpdates::addUpdate( new AtomicSectionUpdate(
+                       $dbw,
+                       __METHOD__,
+                       function ( IDatabase $dbw, $fname ) {
+                               $expiryCond = [ 'gug_expiry < ' . 
$dbw->addQuotes( $dbw->timestamp() ) ];
+
+                               // delete 'em all
+                               $dbw->delete( 'global_user_groups', 
$expiryCond, $fname );
+                       }
+               ) );
+       }
+
+       /**
+        * Returns GlobalUserGroupMembership objects for all the groups a user 
currently
+        * belongs to.
+        *
+        * @param int $userId ID of the user to search for
+        * @param IDatabase|null $db Optional database connection
+        * @return array Associative array of (group name => 
UserGroupMembership object)
+        */
+       public static function getMembershipsForUser( $userId, IDatabase $db = 
null ) {
+               if ( !$db ) {
+                       $db = wfGetDB( DB_MASTER );
+               }
+
+               $res = $db->select( 'global_user_groups',
+                       self::selectFields(),
+                       [ 'gug_user' => $userId ],
+                       __METHOD__ );
+
+               $gugms = [];
+               foreach ( $res as $row ) {
+                       $ugm = self::newFromRow( $row );
+                       if ( !$ugm->isExpired() ) {
+                               $gugms[$ugm->group] = $ugm;
+                       }
+               }
+
+               return $gugms;
+       }
+
+       /**
+        * Returns a UserGroupMembership object that pertains to the given user 
and group,
+        * or false if the user does not belong to that group (or the 
assignment has
+        * expired).
+        *
+        * @param int $userId ID of the user to search for
+        * @param string $group User group name
+        * @param IDatabase|null $db Optional database connection
+        * @return UserGroupMembership|false
+        */
+       public static function getMembership( $userId, $group, IDatabase $db = 
null ) {
+               if ( !$db ) {
+                       $db = wfGetDB( DB_REPLICA );
+               }
+
+               $row = $db->selectRow( 'global_user_groups',
+                       self::selectFields(),
+                       [ 'gug_user' => $userId, 'gug_group' => $group ],
+                       __METHOD__ );
+               if ( !$row ) {
+                       return false;
+               }
+
+               $ugm = self::newFromRow( $row );
+               if ( !$ugm->isExpired() ) {
+                       return $ugm;
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Gets a link for a user group, possibly including the expiry date if 
relevant.
+        *
+        * @param string|GlobalUserGroupMembership $ugm Either a group name as 
a string, or
+        *   a GlobalUserGroupMembership object
+        * @param IContextSource $context
+        * @param string $format Either 'wiki' or 'html'
+        * @param string|null $userName If you want to use the group member 
message
+        *   ("administrator"), pass the name of the user who belongs to the 
group; it
+        *   is used for GENDER of the group member message. If you instead 
want the
+        *   group name message ("Administrators"), omit this parameter.
+        * @return string
+        * @throws MWException
+        */
+       public static function getLink( $ugm, IContextSource $context, $format, 
$userName = null ) {
+
+               if ( $format !== 'wiki' && $format !== 'html' ) {
+                       throw new MWException( 
'GlobalUserGroupMembership::getLink() $format parameter should be ' .
+                                              "'wiki' or 'html'" );
+               }
+
+               if ( $ugm instanceof GlobalUserGroupMembership ) {
+                       $expiry = $ugm->getExpiry();
+                       $group = $ugm->getGroup();
+               } else {
+                       $expiry = null;
+                       $group = $ugm;
+               }
+
+               if ( $userName !== null ) {
+                       $groupName = self::getGroupMemberName( $group, 
$userName );
+               } else {
+                       $groupName = self::getGroupName( $group );
+               }
+
+               // link to the group description page, if it exists
+               $linkTitle = self::getGroupPage( $group );
+               if ( $linkTitle ) {
+                       if ( $format === 'wiki' ) {
+                               $linkPage = $linkTitle->getFullText();
+                               $groupLink = "[[$linkPage|$groupName]]";
+                       } else {
+                               $groupLink = Linker::link( $linkTitle, 
htmlspecialchars( $groupName ) );
+                       }
+               } else {
+                       $groupLink = htmlspecialchars( $groupName );
+               }
+
+               if ( $expiry ) {
+                       // format the expiry to a nice string
+                       $uiLanguage = $context->getLanguage();
+                       $uiUser = $context->getUser();
+                       $expiryDT = $uiLanguage->userTimeAndDate( $expiry, 
$uiUser );
+                       $expiryD = $uiLanguage->userDate( $expiry, $uiUser );
+                       $expiryT = $uiLanguage->userTime( $expiry, $uiUser );
+                       if ( $format === 'html' ) {
+                               $groupLink = Message::rawParam( $groupLink );
+                       }
+                       return $context->msg( 
'group-membership-link-with-expiry' )
+                               ->params( $groupLink, $expiryDT, $expiryD, 
$expiryT )->text();
+               } else {
+                       return $groupLink;
+               }
+       }
+}
diff --git a/GlobalUserrights.alias.php b/GlobalUserrights.alias.php
index 158e0eb..8ee67c7 100644
--- a/GlobalUserrights.alias.php
+++ b/GlobalUserrights.alias.php
@@ -7,244 +7,244 @@
  */
 // @codingStandardsIgnoreFile
 
-$specialPageAliases = array();
+$specialPageAliases = [];
 
 /** English (English) */
-$specialPageAliases['en'] = array(
-       'GlobalUserrights' => array( 'GlobalUserRights', 
'GlobalGroupMembership' ),
-);
+$specialPageAliases['en'] = [
+       'GlobalUserrights' => [ 'GlobalUserRights', 'GlobalGroupMembership' ],
+];
 
 /** Arabic (العربية) */
-$specialPageAliases['ar'] = array(
-       'GlobalUserrights' => array( 'صلاحيات_المستخدم_العالمية', 
'عضوية_المجموعة_العالمية' ),
-);
+$specialPageAliases['ar'] = [
+       'GlobalUserrights' => [ 'صلاحيات_المستخدم_العالمية', 
'عضوية_المجموعة_العالمية' ],
+];
 
 /** Egyptian Spoken Arabic (مصرى) */
-$specialPageAliases['arz'] = array(
-       'GlobalUserrights' => array( 'حقوق_اليوزر_العالميه' ),
-);
+$specialPageAliases['arz'] = [
+       'GlobalUserrights' => [ 'حقوق_اليوزر_العالميه' ],
+];
 
 /** Banjar (Bahasa Banjar) */
-$specialPageAliases['bjn'] = array(
-       'GlobalUserrights' => array( 'Kaanggutaan_galambang_global' ),
-);
+$specialPageAliases['bjn'] = [
+       'GlobalUserrights' => [ 'Kaanggutaan_galambang_global' ],
+];
 
 /** Bosnian (bosanski) */
-$specialPageAliases['bs'] = array(
-       'GlobalUserrights' => array( 'PravaGlobalnihKorisnika', 
'ClanstvoGlobalnihGrupa' ),
-);
+$specialPageAliases['bs'] = [
+       'GlobalUserrights' => [ 'PravaGlobalnihKorisnika', 
'ClanstvoGlobalnihGrupa' ],
+];
 
 /** German (Deutsch) */
-$specialPageAliases['de'] = array(
-       'GlobalUserrights' => array( 'Globale_Benutzerrechte' ),
-);
+$specialPageAliases['de'] = [
+       'GlobalUserrights' => [ 'Globale_Benutzerrechte' ],
+];
 
 /** Zazaki (Zazaki) */
-$specialPageAliases['diq'] = array(
-       'GlobalUserrights' => array( 'GlobalHeqaKarberan' ),
-);
+$specialPageAliases['diq'] = [
+       'GlobalUserrights' => [ 'GlobalHeqaKarberan' ],
+];
 
 /** Lower Sorbian (dolnoserbski) */
-$specialPageAliases['dsb'] = array(
-       'GlobalUserrights' => array( 'Globalne_wužywarske_pšawa' ),
-);
+$specialPageAliases['dsb'] = [
+       'GlobalUserrights' => [ 'Globalne_wužywarske_pšawa' ],
+];
 
 /** Spanish (español) */
-$specialPageAliases['es'] = array(
-       'GlobalUserrights' => array( 'Permisos_de_usuarios_globales', 
'Permisos_usuarios_globales', 'Derechos_usuarios_globales' ),
-);
+$specialPageAliases['es'] = [
+       'GlobalUserrights' => [ 'Permisos_de_usuarios_globales', 
'Permisos_usuarios_globales', 'Derechos_usuarios_globales' ],
+];
 
 /** Persian (فارسی) */
-$specialPageAliases['fa'] = array(
-       'GlobalUserrights' => array( 'دسترسی_سراسری' ),
-);
+$specialPageAliases['fa'] = [
+       'GlobalUserrights' => [ 'دسترسی_سراسری' ],
+];
 
 /** French (français) */
-$specialPageAliases['fr'] = array(
-       'GlobalUserrights' => array( 'Droits_des_utilisateurs_globaux', 
'DroitsDesUtilisateursGlobaux' ),
-);
+$specialPageAliases['fr'] = [
+       'GlobalUserrights' => [ 'Droits_des_utilisateurs_globaux', 
'DroitsDesUtilisateursGlobaux' ],
+];
 
 /** Franco-Provençal (arpetan) */
-$specialPageAliases['frp'] = array(
-       'GlobalUserrights' => array( 'Drêts_ux_usanciérs_globâls', 
'DrêtsUxUsanciérsGlobâls' ),
-);
+$specialPageAliases['frp'] = [
+       'GlobalUserrights' => [ 'Drêts_ux_usanciérs_globâls', 
'DrêtsUxUsanciérsGlobâls' ],
+];
 
 /** Galician (galego) */
-$specialPageAliases['gl'] = array(
-       'GlobalUserrights' => array( 'Dereitos_de_usuario_globais' ),
-);
+$specialPageAliases['gl'] = [
+       'GlobalUserrights' => [ 'Dereitos_de_usuario_globais' ],
+];
 
 /** Gujarati (ગુજરાતી) */
-$specialPageAliases['gu'] = array(
-       'GlobalUserrights' => array( 'વૈશ્વિકસભ્ય_હક્કો', 'વૈશ્વિકસમૂહસભ્યપદ' ),
-);
+$specialPageAliases['gu'] = [
+       'GlobalUserrights' => [ 'વૈશ્વિકસભ્ય_હક્કો', 'વૈશ્વિકસમૂહસભ્યપદ' ],
+];
 
 /** Hebrew (עברית) */
-$specialPageAliases['he'] = array(
-       'GlobalUserrights' => array( 'הרשאות_משתמש_גלובליות' ),
-);
+$specialPageAliases['he'] = [
+       'GlobalUserrights' => [ 'הרשאות_משתמש_גלובליות' ],
+];
 
 /** Croatian (hrvatski) */
-$specialPageAliases['hr'] = array(
-       'GlobalUserrights' => array( 'Globalna_prava', 
'Globalno_članstvo_grupe' ),
-);
+$specialPageAliases['hr'] = [
+       'GlobalUserrights' => [ 'Globalna_prava', 'Globalno_članstvo_grupe' ],
+];
 
 /** Upper Sorbian (hornjoserbsce) */
-$specialPageAliases['hsb'] = array(
-       'GlobalUserrights' => array( 'Globalne_wužwiarske_prawa' ),
-);
+$specialPageAliases['hsb'] = [
+       'GlobalUserrights' => [ 'Globalne_wužwiarske_prawa' ],
+];
 
 /** Haitian (Kreyòl ayisyen) */
-$specialPageAliases['ht'] = array(
-       'GlobalUserrights' => array( 'DwaItilizatèGlobal2', 
'FèPatiGwoupGlobal2' ),
-);
+$specialPageAliases['ht'] = [
+       'GlobalUserrights' => [ 'DwaItilizatèGlobal2', 'FèPatiGwoupGlobal2' ],
+];
 
 /** Interlingua (interlingua) */
-$specialPageAliases['ia'] = array(
-       'GlobalUserrights' => array( 'Derectos_global_de_usatores' ),
-);
+$specialPageAliases['ia'] = [
+       'GlobalUserrights' => [ 'Derectos_global_de_usatores' ],
+];
 
 /** Indonesian (Bahasa Indonesia) */
-$specialPageAliases['id'] = array(
-       'GlobalUserrights' => array( 'Keanggotaan_grup_global', 
'KeanggotaanGrupGlobal' ),
-);
+$specialPageAliases['id'] = [
+       'GlobalUserrights' => [ 'Keanggotaan_grup_global', 
'KeanggotaanGrupGlobal' ],
+];
 
 /** Italian (italiano) */
-$specialPageAliases['it'] = array(
-       'GlobalUserrights' => array( 'DirittiUtenteGlobale' ),
-);
+$specialPageAliases['it'] = [
+       'GlobalUserrights' => [ 'DirittiUtenteGlobale' ],
+];
 
 /** Japanese (日本語) */
-$specialPageAliases['ja'] = array(
-       'GlobalUserrights' => array( 'グローバル利用者権限' ),
-);
+$specialPageAliases['ja'] = [
+       'GlobalUserrights' => [ 'グローバル利用者権限' ],
+];
 
 /** Georgian (ქართული) */
-$specialPageAliases['ka'] = array(
-       'GlobalUserrights' => array( 'გლობალური_მომხმარებლის_უფლებები' ),
-);
+$specialPageAliases['ka'] = [
+       'GlobalUserrights' => [ 'გლობალური_მომხმარებლის_უფლებები' ],
+];
 
 /** Korean (한국어) */
-$specialPageAliases['ko'] = array(
-       'GlobalUserrights' => array( '전체사용자권한' ),
-);
+$specialPageAliases['ko'] = [
+       'GlobalUserrights' => [ '전체사용자권한' ],
+];
 
 /** Colognian (Ripoarisch) */
-$specialPageAliases['ksh'] = array(
-       'GlobalUserrights' => array( 'Jemeinsam_Metmaacher_Rääschte', 
'Jemeinsam_Medmaacher_Rääschte', 'JemeinsamMetmaacherRääschte', 
'JemeinsamMedmaacherRääschte' ),
-);
+$specialPageAliases['ksh'] = [
+       'GlobalUserrights' => [ 'Jemeinsam_Metmaacher_Rääschte', 
'Jemeinsam_Medmaacher_Rääschte', 'JemeinsamMetmaacherRääschte', 
'JemeinsamMedmaacherRääschte' ],
+];
 
 /** Ladino (Ladino) */
-$specialPageAliases['lad'] = array(
-       'GlobalUserrights' => array( 'Permessos_de_usadores_globbales' ),
-);
+$specialPageAliases['lad'] = [
+       'GlobalUserrights' => [ 'Permessos_de_usadores_globbales' ],
+];
 
 /** Luxembourgish (Lëtzebuergesch) */
-$specialPageAliases['lb'] = array(
-       'GlobalUserrights' => array( 'Global_Benotzerrechter' ),
-);
+$specialPageAliases['lb'] = [
+       'GlobalUserrights' => [ 'Global_Benotzerrechter' ],
+];
 
 /** Macedonian (македонски) */
-$specialPageAliases['mk'] = array(
-       'GlobalUserrights' => array( 'ГлобалниКорисничкиПрава' ),
-);
+$specialPageAliases['mk'] = [
+       'GlobalUserrights' => [ 'ГлобалниКорисничкиПрава' ],
+];
 
 /** Malayalam (മലയാളം) */
-$specialPageAliases['ml'] = array(
-       'GlobalUserrights' => array( 'ആഗോളാംഗത്വാവകാശങ്ങൾ', 'ആഗോളസംഘാംഗത്വം' ),
-);
+$specialPageAliases['ml'] = [
+       'GlobalUserrights' => [ 'ആഗോളാംഗത്വാവകാശങ്ങൾ', 'ആഗോളസംഘാംഗത്വം' ],
+];
 
 /** Marathi (मराठी) */
-$specialPageAliases['mr'] = array(
-       'GlobalUserrights' => array( 'वैश्विकसदस्यअधिकार' ),
-);
+$specialPageAliases['mr'] = [
+       'GlobalUserrights' => [ 'वैश्विकसदस्यअधिकार' ],
+];
 
 /** Norwegian Bokmål (norsk bokmål) */
-$specialPageAliases['nb'] = array(
-       'GlobalUserrights' => array( 'Globale_brukerrettigheter2' ),
-);
+$specialPageAliases['nb'] = [
+       'GlobalUserrights' => [ 'Globale_brukerrettigheter2' ],
+];
 
 /** Low Saxon (Netherlands) (Nedersaksies) */
-$specialPageAliases['nds-nl'] = array(
-       'GlobalUserrights' => array( 'Globle_gebrukersrechten' ),
-);
+$specialPageAliases['nds-nl'] = [
+       'GlobalUserrights' => [ 'Globle_gebrukersrechten' ],
+];
 
 /** Dutch (Nederlands) */
-$specialPageAliases['nl'] = array(
-       'GlobalUserrights' => array( 'GlobaleGebruikersrechten' ),
-);
+$specialPageAliases['nl'] = [
+       'GlobalUserrights' => [ 'GlobaleGebruikersrechten' ],
+];
 
 /** Norwegian Nynorsk (norsk nynorsk) */
-$specialPageAliases['nn'] = array(
-       'GlobalUserrights' => array( 'Brukarrettar_globalt' ),
-);
+$specialPageAliases['nn'] = [
+       'GlobalUserrights' => [ 'Brukarrettar_globalt' ],
+];
 
 /** Occitan (occitan) */
-$specialPageAliases['oc'] = array(
-       'GlobalUserrights' => array( 'Dreches_dels_utilizaires_globals', 
'DrechesDelsUtilizairesGlobals' ),
-);
+$specialPageAliases['oc'] = [
+       'GlobalUserrights' => [ 'Dreches_dels_utilizaires_globals', 
'DrechesDelsUtilizairesGlobals' ],
+];
 
 /** Polish (polski) */
-$specialPageAliases['pl'] = array(
-       'GlobalUserrights' => array( 'Globalne_uprawnienia_użytkownika' ),
-);
+$specialPageAliases['pl'] = [
+       'GlobalUserrights' => [ 'Globalne_uprawnienia_użytkownika' ],
+];
 
 /** Portuguese (português) */
-$specialPageAliases['pt'] = array(
-       'GlobalUserrights' => array( 'Privilégios_globais_de_utilizador' ),
-);
+$specialPageAliases['pt'] = [
+       'GlobalUserrights' => [ 'Privilégios_globais_de_utilizador' ],
+];
 
 /** Brazilian Portuguese (português do Brasil) */
-$specialPageAliases['pt-br'] = array(
-       'GlobalUserrights' => array( 'Privilégios_globais_de_usuário' ),
-);
+$specialPageAliases['pt-br'] = [
+       'GlobalUserrights' => [ 'Privilégios_globais_de_usuário' ],
+];
 
 /** Romanian (română) */
-$specialPageAliases['ro'] = array(
-       'GlobalUserrights' => array( 'Drepturiglobaleutilizator' ),
-);
+$specialPageAliases['ro'] = [
+       'GlobalUserrights' => [ 'Drepturiglobaleutilizator' ],
+];
 
 /** Sanskrit (संस्कृतम्) */
-$specialPageAliases['sa'] = array(
-       'GlobalUserrights' => array( 'वैश्विकयोजकाधिकार' ),
-);
+$specialPageAliases['sa'] = [
+       'GlobalUserrights' => [ 'वैश्विकयोजकाधिकार' ],
+];
 
 /** Slovak (slovenčina) */
-$specialPageAliases['sk'] = array(
-       'GlobalUserrights' => array( 'GlobálnePoužívateľskéPráva' ),
-);
+$specialPageAliases['sk'] = [
+       'GlobalUserrights' => [ 'GlobálnePoužívateľskéPráva' ],
+];
 
 /** Tagalog (Tagalog) */
-$specialPageAliases['tl'] = array(
-       'GlobalUserrights' => array( 'Mga_karapatan_ng_pangglobong_tagagamit' ),
-);
+$specialPageAliases['tl'] = [
+       'GlobalUserrights' => [ 'Mga_karapatan_ng_pangglobong_tagagamit' ],
+];
 
 /** Turkish (Türkçe) */
-$specialPageAliases['tr'] = array(
-       'GlobalUserrights' => array( 'KüreselKullanıcıHakları' ),
-);
+$specialPageAliases['tr'] = [
+       'GlobalUserrights' => [ 'KüreselKullanıcıHakları' ],
+];
 
 /** Tatar (Cyrillic script) (татарча) */
-$specialPageAliases['tt-cyrl'] = array(
-       'GlobalUserrights' => array( 'Кулланучыларның_глобаль_хокуклары' ),
-);
+$specialPageAliases['tt-cyrl'] = [
+       'GlobalUserrights' => [ 'Кулланучыларның_глобаль_хокуклары' ],
+];
 
 /** Ukrainian (українська) */
-$specialPageAliases['uk'] = array(
-       'GlobalUserrights' => array( 'Глобальні_права_користувача' ),
-);
+$specialPageAliases['uk'] = [
+       'GlobalUserrights' => [ 'Глобальні_права_користувача' ],
+];
 
 /** Cantonese (粵語) */
-$specialPageAliases['yue'] = array(
-       'GlobalUserrights' => array( '全域用戶權' ),
-);
+$specialPageAliases['yue'] = [
+       'GlobalUserrights' => [ '全域用戶權' ],
+];
 
 /** Simplified Chinese (中文(简体)‎) */
-$specialPageAliases['zh-hans'] = array(
-       'GlobalUserrights' => array( '全域用户权利', '全域成员组' ),
-);
+$specialPageAliases['zh-hans'] = [
+       'GlobalUserrights' => [ '全域用户权利', '全域成员组' ],
+];
 
 /** Traditional Chinese (中文(繁體)‎) */
-$specialPageAliases['zh-hant'] = array(
-       'GlobalUserrights' => array( '全域用戶組權限' ),
-);
\ No newline at end of file
+$specialPageAliases['zh-hant'] = [
+       'GlobalUserrights' => [ '全域用戶組權限' ],
+];
\ No newline at end of file
diff --git a/GlobalUserrightsHooks.php b/GlobalUserrightsHooks.php
index 3547334..db512bd 100644
--- a/GlobalUserrightsHooks.php
+++ b/GlobalUserrightsHooks.php
@@ -5,48 +5,48 @@
        /**
         * Function to get a given user's global groups
         *
-        * @param $user instance of User class
+        * @param User|int $user instance of User class or uid
         * @return array of global groups
         */
        public static function getGroups( $user ) {
+               return array_keys( self::getGroupMemberships( $user ) );
+       }
+
+       /**
+        * Function to get a given user's global groups memberships
+        *
+        * @param int|User $user instance of User class or uid
+        * @return array
+        */
+       public static function getGroupMemberships( $user ) {
                if ( $user instanceof User ) {
-                       $uid = $user->getId();
+                       $uidLookup = CentralIdLookup::factory();
+
+                       $uid = $uidLookup->centralIdFromLocalUser( $user );
                } else {
                        // if $user isn't an instance of user, assume it's the 
uid
                        $uid = $user;
                }
 
-               $groups = array();
                if ( $uid === 0 ) {
                        // Optimization -- we know that anons (user ID #0) 
cannot be members
                        // of any (global) user groups, so we don't need to run 
the DB query
                        // to figure that out and we can just return the empty 
array here.
-                       return $groups;
+                       return [];
+               } else {
+                       return 
GlobalUserGroupMembership::getMembershipsForUser( $uid );
                }
-
-               $dbr = wfGetDB( DB_MASTER );
-               $res = $dbr->select(
-                       'global_user_groups',
-                       array( 'gug_group' ),
-                       array( 'gug_user' => $uid ),
-                       __METHOD__
-               );
-
-               foreach ( $res as $row ) {
-                       $groups[] = $row->gug_group;
-               }
-
-               return $groups;
        }
 
        /**
         * Hook function for UserEffectiveGroups
         * Adds any global groups the user has to $groups
         *
-        * @param $user instance of User
+        * @param User $user instance of User
         * @param &$groups array of groups the user is in
+        * @return bool
         */
-       public static function onUserEffectiveGroups( $user, &$groups ) {
+       public static function onUserEffectiveGroups( User $user, &$groups ) {
                $groups = array_merge( $groups, 
GlobalUserrightsHooks::getGroups( $user ) );
                $groups = array_unique( $groups );
 
@@ -58,17 +58,18 @@
         * Updates UsersPager::getQueryInfo() to account for the 
global_user_groups table
         * This ensures that global rights show up on Special:ListUsers
         *
-        * @param $that instance of UsersPager
-        * @param &$query the query array to be returned
+        * @param UsersPager $that instance of UsersPager
+        * @param array &$query the query array to be returned
+        * @return bool
         */
        public static function onSpecialListusersQueryInfo( $that, &$query ) {
                $dbr = wfGetDB( DB_SLAVE );
 
                $query['tables'][] = 'global_user_groups';
-               $query['join_conds']['global_user_groups'] = array(
+               $query['join_conds']['global_user_groups'] = [
                        'LEFT JOIN',
                        'user_id = gug_user'
-               );
+               ];
 
                $query['fields'][3] = 'COUNT(ug_group) + COUNT(gug_group) AS 
numgroups';
                // kind of yucky statement, I blame MySQL 5.0.13 
http://bugs.mysql.com/bug.php?id=15610
@@ -79,6 +80,34 @@
                        unset( $query['conds']['ug_group'] );
                        $reqgrp = $dbr->addQuotes( $that->requestedGroup );
                        $query['conds'][] = 'ug_group = ' . $reqgrp . 'OR 
gug_group = ' . $reqgrp;
+               }
+
+               return true;
+       }
+
+       /**
+        * Hook function for UsersPagerDoBatchLookups
+        *
+        * @param \Wikimedia\Rdbms\IDatabase $dbr
+        * @param array $userIds
+        * @param array $cache
+        * @param array $groups
+        * @return bool
+        */
+       public static function onUsersPagerDoBatchLookups( 
\Wikimedia\Rdbms\IDatabase $dbr, array $userIds, array &$cache, array &$groups 
) {
+               $globalGroupsRes = $dbr->select(
+                       'global_user_groups',
+                       GlobalUserGroupMembership::selectFields(),
+                       [ 'gug_user' => $userIds ],
+                       __METHOD__
+               );
+
+               foreach ( $globalGroupsRes as $row ) {
+                       $gugm = GlobalUserGroupMembership::newFromRow( $row );
+                       if ( !$gugm->isExpired() ) {
+                               $cache[$row->gug_user][$row->gug_group] = $gugm;
+                               $groups[$row->gug_group] = true;
+                       }
                }
 
                return true;
@@ -98,7 +127,7 @@
                        $hit = $dbr->selectField(
                                'global_user_groups',
                                'COUNT(*)',
-                               array( 'gug_group' => $group ),
+                               [ 'gug_group' => $group ],
                                __METHOD__
                        );
                }
@@ -108,9 +137,22 @@
        /**
         * Create SQL automatically when running update.php so sql does not 
have to be
         * applied manually
+        *
+        * @param DatabaseUpdater $updater
+        * @return bool
         */
-       public static function onLoadExtensionSchemaUpdates( $updater ) {
-               $updater->addExtensionTable( 'global_user_groups', __DIR__ . 
'/global_user_groups.sql' );
+       public static function onLoadExtensionSchemaUpdates( DatabaseUpdater 
$updater ) {
+               $dir = __DIR__;
+               $updater->addExtensionTable( 'global_user_groups', $dir . 
'/global_user_groups.sql' );
+
+               $dir .= '/db_patches';
+
+               // Update the table with the new definitions
+               // This ensures backwards compatibility
+               $updater->addExtensionField( 'global_user_groups', 
'gug_expiry', $dir . '/patch-gug_expiry-field.sql' );
+               $updater->modifyExtensionField( 'global_user_groups', 
'gug_group', $dir . '/patch-gug_group-field.sql' );
+               $updater->addExtensionIndex( 'global_user_groups', 
'gug_expiry', $dir . '/patch-gug_expiry-index.sql' );
+
                return true;
        }
 }
diff --git a/GlobalUserrights_body.php b/GlobalUserrights_body.php
index c728b2a..62269e3 100644
--- a/GlobalUserrights_body.php
+++ b/GlobalUserrights_body.php
@@ -20,17 +20,29 @@
        /**
         * Save global user groups changes in the DB
         *
-        * @param $username String: username
-        * @param $reason String: reason
+        * @param User|UserRightsProxy $user
+        * @param array $add Array of groups to add
+        * @param array $remove Array of groups to remove
+        * @param string $reason Reason for group change
+        * @param array $tags Array of change tags to add to the log entry
+        * @param array $groupExpiries Associative array of (group name => 
expiry),
+        *   containing only those groups that are to have new expiry values set
+        * @return array
+        * @internal param string $username username
         */
-       function doSaveUserGroups( $user, $add, $remove, $reason = '' ) {
-               $oldGroups = GlobalUserrightsHooks::getGroups( $user );
+       function doSaveUserGroups( $user, $add, $remove, $reason = '', $tags = 
[], $groupExpiries = [] ) {
+               $uidLookup = CentralIdLookup::factory();
+
+               $uid = $uidLookup->centralIdFromLocalUser( $user );
+
+               $oldUGMs =  GlobalUserrightsHooks::getGroupMemberships( $uid );
+               $oldGroups = GlobalUserrightsHooks::getGroups( $uid );
                $newGroups = $oldGroups;
 
                // remove then add groups
                if ( $remove ) {
                        $newGroups = array_diff( $newGroups, $remove );
-                       $uid = $user->getId();
+
                        foreach ( $remove as $group ) {
                                // whole reason we're redefining this function 
is to make it use
                                // $this->removeGroup instead of 
$user->removeGroup, etc.
@@ -39,110 +51,156 @@
                }
                if ( $add ) {
                        $newGroups = array_merge( $newGroups, $add );
-                       $uid = $user->getId();
+
                        foreach ( $add as $group ) {
-                               $this->addGroup( $uid, $group );
+                               $expiry = isset( $groupExpiries[$group] ) ? 
$groupExpiries[$group] : null;
+                               $this->addGroup( $uid, $group, $expiry );
                        }
                }
+
                // get rid of duplicate groups there might be
                $newGroups = array_unique( $newGroups );
+               $newUGMs =  GlobalUserrightsHooks::getGroupMemberships( $uid );
 
                // Ensure that caches are cleared
                $user->invalidateCache();
 
+               wfDebug( 'oldGlobalGroups: ' . print_r( $oldGroups, true ) . 
"\n" );
+               wfDebug( 'newGlobalGroups: ' . print_r( $newGroups, true ) . 
"\n" );
+               wfDebug( 'oldGlobalUGMs: ' . print_r( $oldUGMs, true ) . "\n" );
+               wfDebug( 'newGlobalUGMs: ' . print_r( $newUGMs, true ) . "\n" );
+
                // if anything changed, log it
-               if ( $newGroups != $oldGroups ) {
-                       $this->addLogEntry( $user, $oldGroups, $newGroups, 
$reason );
+               if ( $newGroups != $oldGroups || $newUGMs != $oldUGMs ) {
+                       $this->addLogEntry( $user, $oldGroups, $newGroups, 
$reason, $tags, $oldUGMs, $newUGMs );
                }
-               return array( $add, $remove );
+               return [ $add, $remove ];
        }
 
-       function addGroup( $uid, $group ) {
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->insert(
-                       'global_user_groups',
-                       array(
-                               'gug_user' => $uid,
-                               'gug_group' => $group
-                       ),
-                       __METHOD__,
-                       'IGNORE'
-               );
+       /**
+        * Add a user to a group
+        *
+        * @param int $uid central Id
+        * @param string $group name of the group to add
+        * @param string $expiry expiration of the group membership
+        * @return bool
+        */
+       function addGroup( $uid, $group, $expiry = null ) {
+               if ( $expiry ) {
+                       $expiry = wfTimestamp( TS_MW, $expiry );
+               }
+
+               $gugm = new GlobalUserGroupMembership( $uid, $group, $expiry );
+               if ( !$gugm->insert( true ) ) {
+                       return false;
+               }
+
+               return true;
        }
 
+       /**
+        * Removes a user from a group
+        *
+        * @param int $uid central Id
+        * @param string $group name of the group
+        * @return bool
+        */
        function removeGroup( $uid, $group ) {
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->delete(
-                       'global_user_groups',
-                       array(
-                               'gug_user' => $uid,
-                               'gug_group' => $group
-                       ),
-                       __METHOD__
-               );
+               $gugm = new GlobalUserGroupMembership( $uid, $group );
+
+               if ( !$gugm || !$gugm->delete() ) {
+                       return false;
+               }
+
+               return true;
        }
 
        /**
         * Add a gblrights log entry
+        *
+        * @param User|UserRightsProxy $user
+        * @param array $oldGroups list of groups before the change
+        * @param array $newGroups list of groups after the change
+        * @param string $reason reason for the group change
+        * @param array $tags Change tags for the log entry
+        * @param array $oldUGMs Associative array of (group name => 
GlobalUserGroupMembership)
+        * @param array $newUGMs Associative array of (group name => 
GlobalUserGroupMembership)
         */
-       function addLogEntry( $user, $oldGroups, $newGroups, $reason ) {
-               $log = new LogPage( 'gblrights' );
+       protected function addLogEntry( $user, $oldGroups, $newGroups, $reason, 
$tags, $oldUGMs, $newUGMs ) {
 
-               $log->addEntry( 'rights',
-                       $user->getUserPage(),
-                       $reason,
-                       array(
-                               $this->makeGroupNameList( $oldGroups ),
-                               $this->makeGroupNameList( $newGroups )
-                       )
-               );
+               // make sure $oldUGMs and $newUGMs are in the same order, and 
serialise
+               // each UGM object to a simplified array
+               $oldUGMs = array_map( function( $group ) use ( $oldUGMs ) {
+                       return isset( $oldUGMs[$group] ) ?
+                               self::serialiseUgmForLog( $oldUGMs[$group] ) :
+                               null;
+               }, $oldGroups );
+               $newUGMs = array_map( function( $group ) use ( $newUGMs ) {
+                       return isset( $newUGMs[$group] ) ?
+                               self::serialiseUgmForLog( $newUGMs[$group] ) :
+                               null;
+               }, $newGroups );
+
+               $logEntry = new ManualLogEntry( 'gblrights', 'rights' );
+               $logEntry->setPerformer( $this->getUser() );
+               $logEntry->setTarget( $user->getUserPage() );
+               $logEntry->setComment( $reason );
+               $logEntry->setParameters( [
+                       '4::oldgroups' => $oldGroups,
+                       '5::newgroups' => $newGroups,
+                       'oldmetadata' => $oldUGMs,
+                       'newmetadata' => $newUGMs,
+               ] );
+               $logid = $logEntry->insert();
+               if ( count( $tags ) ) {
+                       $logEntry->setTags( $tags );
+               }
+               $logEntry->publish( $logid );
        }
 
        /**
-        * Make a list of group names to be stored as parameter for log entries.
-        *
-        * This is an ugly hack backported from MediaWiki 1.26.
-        * @todo FIXME Per the associated comment in MW 1.26 and older, we 
shouldn't
-        * be using this but rather LogFormatter.
-        *
-        * @param array $ids
-        * @return string
+        * @param User|UserRightsProxy $user
+        * @param array $groups
+        * @param array $groupMemberships
         */
-       function makeGroupNameListForLog( $ids ) {
-               if ( empty( $ids ) ) {
-                       return '';
-               } else {
-                       return $this->makeGroupNameList( $ids );
-               }
-       }
-
-       protected function showEditUserGroupsForm( $user, $groups ) {
+       protected function showEditUserGroupsForm( $user, $groups, 
$groupMemberships ) {
                // override the $groups that is passed, which will be
                // the user's local groups
-               $groups = GlobalUserrightsHooks::getGroups( $user );
-               parent::showEditUserGroupsForm( $user, $groups );
+               $groupMemberships = GlobalUserrightsHooks::getGroupMemberships( 
$user );
+               parent::showEditUserGroupsForm( $user, $groups, 
$groupMemberships );
        }
 
+       /**
+        * @return array
+        */
        function changeableGroups() {
-               global $wgUser;
-               if ( $wgUser->isAllowed( 'userrights-global' ) ) {
+               $groups = [
+                       'add' => [],
+                       'remove' => [],
+                       'add-self' => [],
+                       'remove-self' => []
+               ];
+
+               if ( $this->getUser()->isAllowed( 'userrights-global' ) ) {
                        // all groups can be added globally
                        $all = array_merge( User::getAllGroups() );
-                       return array(
-                               'add' => $all,
-                               'remove' => $all,
-                               'add-self' => array(),
-                               'remove-self' => array()
-                       );
-               } else {
-                       return array();
+                       $groups['add'] = $all;
+                       $groups['remove'] = $all;
                }
+
+               return $groups;
        }
 
+       /**
+        * Show a rights log fragment for the specified user
+        *
+        * @param User $user
+        * @param OutputPage $output
+        */
        protected function showLogFragment( $user, $output ) {
                $log = new LogPage( 'gblrights' );
-               $output->addHTML( Xml::element( 'h2', null, $log->getName() . 
"\n" ) );
-               LogEventsList::showLogExtract( $output, 'gblrights', 
$user->getUserPage()->getPrefixedText() );
+               $output->addHTML( Xml::element( 'h2', null, 
$log->getName()->text() ) );
+               LogEventsList::showLogExtract( $output, 'gblrights', 
$user->getUserPage() );
        }
 
        protected function getGroupName() {
diff --git a/db_patches/patch-gug_expiry-field.sql 
b/db_patches/patch-gug_expiry-field.sql
new file mode 100644
index 0000000..872583f
--- /dev/null
+++ b/db_patches/patch-gug_expiry-field.sql
@@ -0,0 +1,3 @@
+-- Patch to add the "gug_expiry" field to the global user groups table
+
+ALTER TABLE /*_*/global_user_groups ADD COLUMN gug_expiry varbinary(14) NULL 
default NULL;
\ No newline at end of file
diff --git a/db_patches/patch-gug_expiry-index.sql 
b/db_patches/patch-gug_expiry-index.sql
new file mode 100644
index 0000000..a6ba4a9
--- /dev/null
+++ b/db_patches/patch-gug_expiry-index.sql
@@ -0,0 +1,2 @@
+-- Add an index for the expiry column
+CREATE INDEX /*i*/gug_expiry ON /*_*/global_user_groups (gug_expiry);
diff --git a/db_patches/patch-gug_group-field.sql 
b/db_patches/patch-gug_group-field.sql
new file mode 100644
index 0000000..ebca8d1
--- /dev/null
+++ b/db_patches/patch-gug_group-field.sql
@@ -0,0 +1,2 @@
+-- Changes the length of groups from 14 to 255 similar to user_group
+ALTER TABLE /*_*/global_user_groups MODIFY gug_group varbinary(255) NOT NULL 
NULL default '';
diff --git a/extension.json b/extension.json
index ffe0d86..13bd9ac 100644
--- a/extension.json
+++ b/extension.json
@@ -1,8 +1,9 @@
 {
        "name": "GlobalUserrights",
-       "version": "1.3.1-SW",
+       "version": "1.5.0",
        "author": [
-               "Nathaniel Herman"
+               "Nathaniel Herman",
+               "Mainframe98"
        ],
        "license-name": "GPL-2.0+",
        "url": "https://www.mediawiki.org/wiki/Extension:GlobalUserrights";,
@@ -21,7 +22,9 @@
        },
        "AutoloadClasses": {
                "GlobalUserrights": "GlobalUserrights_body.php",
-               "GlobalUserrightsHooks": "GlobalUserrightsHooks.php"
+               "GlobalUserrightsHooks": "GlobalUserrightsHooks.php",
+               "GlobalUserGroupMembership": "GlobalUserGroupMembership.php",
+               "GlobalRightsLogFormatter": "GlobalRightsLogFormatter.php"
        },
        "Hooks": {
                "LoadExtensionSchemaUpdates": [
@@ -35,6 +38,9 @@
                ],
                "SiteStatsNumberInGroup": [
                        "GlobalUserrightsHooks::updateStatsForGUR"
+               ],
+               "UsersPagerDoBatchLookups": [
+                       "GlobalUserrightsHooks::onUsersPagerDoBatchLookups"
                ]
        },
        "LogTypes": [
@@ -46,8 +52,8 @@
        "LogHeaders": {
                "gblrights": "gur-rightslog-header"
        },
-       "LogActions": {
-               "gblrights/rights": "gur-rightslog-entry"
+       "LogActionsHandlers": {
+               "gblrights/rights": "GlobalRightsLogFormatter"
        },
        "AvailableRights": [
                "userrights-global"
@@ -57,5 +63,8 @@
                        "userrights-global": true
                }
        },
+       "requires": {
+               "MediaWiki": ">= 1.29.0"
+       },
        "manifest_version": 1
 }
diff --git a/global_user_groups.sql b/global_user_groups.sql
index 7b3525f..0f6e35b 100644
--- a/global_user_groups.sql
+++ b/global_user_groups.sql
@@ -3,10 +3,11 @@
 
 CREATE TABLE /*_*/global_user_groups (
   -- Key to user_id
-  gug_user int unsigned NOT NULL default '0',
+  gug_user int unsigned NOT NULL default 0,
   -- Group name
   gug_group varbinary(16) NOT NULL default '',
 
-  PRIMARY KEY  (gug_user,gug_group),
-  KEY (gug_group)
+  PRIMARY KEY (gug_user, gug_group)
 ) /*$wgDBTableOptions*/;
+
+CREATE INDEX /*i*/gug_group ON /*_*/global_user_groups (gug_group);
diff --git a/i18n/en.json b/i18n/en.json
index f382be2..2241132 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1,7 +1,8 @@
 {
        "@metadata": {
                "authors": [
-                       "Nathaniel Herman"
+                       "Nathaniel Herman",
+                       "Mainframe98"
                ]
        },
        "globaluserrights": "Global User Rights Management",
@@ -9,5 +10,6 @@
        "gur-rightslog-name": "Global rights log",
        "gur-rightslog-header": "This is a log of changes to global rights.",
        "gur-rightslog-entry": "changed global group membership for $1 from $2 
to $3",
+       "logentry-gblrights-rights": "$1 {{GENDER:$2|changed}} global group 
membership for {{GENDER:$6|$3}} from $4 to $5",
        "right-userrights-global": "Manage global user rights"
 }
\ No newline at end of file
diff --git a/i18n/qqq.json b/i18n/qqq.json
index ff44120..4da461d 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -5,7 +5,8 @@
                        "Fryed-peach",
                        "Purodha",
                        "Umherirrender",
-                       "Shirayuki"
+                       "Shirayuki",
+                       "Mainframe98"
                ]
        },
        "globaluserrights": "Used as title on [[Special:GlobalUserRights]]",
@@ -13,5 +14,6 @@
        "gur-rightslog-name": "See also {{msg-mw|Gblrights-logpage}}",
        "gur-rightslog-header": "See also {{msg-mw|Gblrights-pagetext}}",
        "gur-rightslog-entry": "Identical to 
{{msg-mw|Gblrights-rights-entry}}.\n\nSimilar to 
{{msg-mw|Rightslogentry}}.\n\nParameters:\n* $1 - the username\n* $2 - list of 
user groups or empty string\n* $3 - list of user groups or empty string",
+       "logentry-gblrights-rights": "Similar to 
{{msg-mw|logentry-rights-rights}}.\n\nParameters:\n* $1 - (see below)\n* $2 - 
the username of the user changing the rights\n* $3 - the target username\n* $4 
- previous list of user groups or empty string\n* $5 - new list of user groups 
or empty string\n* $6 - see $3",
        "right-userrights-global": "{{doc-right|userrights-global}}\n\nSee also 
{{msg-mw|Right-userrights-shared}}"
 }

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

Gerrit-MessageType: merged
Gerrit-Change-Id: I2eca420b3590ce7680cbc96c3432ff777e091d2a
Gerrit-PatchSet: 4
Gerrit-Project: mediawiki/extensions/GlobalUserrights
Gerrit-Branch: master
Gerrit-Owner: Mainframe98 <[email protected]>
Gerrit-Reviewer: Cook879 <[email protected]>
Gerrit-Reviewer: Jack Phoenix <[email protected]>
Gerrit-Reviewer: Legoktm <[email protected]>
Gerrit-Reviewer: Mainframe98 <[email protected]>
Gerrit-Reviewer: Nbdd0121 <[email protected]>
Gerrit-Reviewer: Paladox <[email protected]>
Gerrit-Reviewer: Reedy <[email protected]>
Gerrit-Reviewer: Siebrand <[email protected]>
Gerrit-Reviewer: TTO <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to