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
