diff --git a/includes/dao/WikiAwareEntity.php b/includes/dao/WikiAwareEntity.php index b8bf33e1da8c5..6a9d3f1647a32 100644 --- a/includes/dao/WikiAwareEntity.php +++ b/includes/dao/WikiAwareEntity.php @@ -35,7 +35,7 @@ interface WikiAwareEntity { /** - * @var bool Wiki ID value to use with instances that are + * @var false Wiki ID value to use with instances that are * defined relative to the local wiki. */ public const LOCAL = false; diff --git a/includes/specials/pagers/UsersPager.php b/includes/specials/pagers/UsersPager.php index 072455d89a355..daf9248154c70 100644 --- a/includes/specials/pagers/UsersPager.php +++ b/includes/specials/pagers/UsersPager.php @@ -329,17 +329,11 @@ protected function doBatchLookups() { } // Lookup groups for all the users - $queryBuilder = $this->userGroupManager->newQueryBuilder( $this->getDatabase() ); - $groupRes = $queryBuilder->where( [ 'ug_user' => $userIds ] ) - ->caller( __METHOD__ ) - ->fetchResultSet(); - $cache = []; + $cache = $this->userGroupManager->getUserGroupMembershipsFromUserIds( $userIds ); $groups = []; - foreach ( $groupRes as $row ) { - $ugm = $this->userGroupManager->newGroupMembershipFromRow( $row ); - if ( !$ugm->isExpired() ) { - $cache[$row->ug_user][$row->ug_group] = $ugm; - $groups[$row->ug_group] = true; + foreach ( $cache as $userGroups ) { + foreach ( $userGroups as $group => $ugm ) { + $groups[$group] = true; } } diff --git a/includes/user/UserGroupManager.php b/includes/user/UserGroupManager.php index 7a871ef8ba91e..3f591af94f2e7 100644 --- a/includes/user/UserGroupManager.php +++ b/includes/user/UserGroupManager.php @@ -23,10 +23,8 @@ use InvalidArgumentException; use LogicException; use MediaWiki\Config\ServiceOptions; -use MediaWiki\Deferred\DeferredUpdates; use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; -use MediaWiki\JobQueue\JobQueueGroup; use MediaWiki\Logging\ManualLogEntry; use MediaWiki\MainConfigNames; use MediaWiki\Parser\Sanitizer; @@ -35,15 +33,10 @@ use MediaWiki\User\TempUser\TempUserConfig; use MediaWiki\WikiMap\WikiMap; use Psr\Log\LoggerInterface; -use UserGroupExpiryJob; use Wikimedia\Assert\Assert; use Wikimedia\IPUtils; -use Wikimedia\Rdbms\IConnectionProvider; use Wikimedia\Rdbms\IDBAccessObject; -use Wikimedia\Rdbms\ILBFactory; -use Wikimedia\Rdbms\IReadableDatabase; use Wikimedia\Rdbms\ReadOnlyMode; -use Wikimedia\Rdbms\SelectQueryBuilder; /** * Manage user group memberships. @@ -83,13 +76,11 @@ class UserGroupManager { public const VALID_OPS = [ '&', '|', '^', '!' ]; private ServiceOptions $options; - private IConnectionProvider $dbProvider; private HookContainer $hookContainer; private HookRunner $hookRunner; private ReadOnlyMode $readOnlyMode; private UserEditTracker $userEditTracker; private GroupPermissionsLookup $groupPermissionsLookup; - private JobQueueGroup $jobQueueGroup; private LoggerInterface $logger; private TempUserConfig $tempUserConfig; @@ -148,15 +139,15 @@ class UserGroupManager { * an ongoing condition check. */ private $recursionMap = []; + private UserGroupStore $store; /** * @param ServiceOptions $options * @param ReadOnlyMode $readOnlyMode - * @param ILBFactory $lbFactory + * @param UserGroupStore $userGroupStore * @param HookContainer $hookContainer * @param UserEditTracker $userEditTracker * @param GroupPermissionsLookup $groupPermissionsLookup - * @param JobQueueGroup $jobQueueGroup * @param LoggerInterface $logger * @param TempUserConfig $tempUserConfig * @param callable[] $clearCacheCallbacks @@ -165,11 +156,10 @@ class UserGroupManager { public function __construct( ServiceOptions $options, ReadOnlyMode $readOnlyMode, - ILBFactory $lbFactory, + UserGroupStore $userGroupStore, HookContainer $hookContainer, UserEditTracker $userEditTracker, GroupPermissionsLookup $groupPermissionsLookup, - JobQueueGroup $jobQueueGroup, LoggerInterface $logger, TempUserConfig $tempUserConfig, array $clearCacheCallbacks = [], @@ -177,17 +167,16 @@ public function __construct( ) { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->options = $options; - $this->dbProvider = $lbFactory; $this->hookContainer = $hookContainer; $this->hookRunner = new HookRunner( $hookContainer ); $this->userEditTracker = $userEditTracker; $this->groupPermissionsLookup = $groupPermissionsLookup; - $this->jobQueueGroup = $jobQueueGroup; $this->logger = $logger; $this->tempUserConfig = $tempUserConfig; $this->readOnlyMode = $readOnlyMode; $this->clearCacheCallbacks = $clearCacheCallbacks; $this->wikiId = $wikiId; + $this->store = $userGroupStore; } /** @@ -227,14 +216,7 @@ public function listAllImplicitGroups(): array { * @return UserGroupMembership */ public function newGroupMembershipFromRow( \stdClass $row ): UserGroupMembership { - return new UserGroupMembership( - (int)$row->ug_user, - $row->ug_group, - $row->ug_expiry === null ? null : wfTimestamp( - TS_MW, - $row->ug_expiry - ) - ); + return $this->store->newGroupMembershipFromRow( $row ); } /** @@ -379,16 +361,7 @@ public function getUserFormerGroups( return []; } - $res = $this->getDBConnectionRefForQueryFlags( $queryFlags )->newSelectQueryBuilder() - ->select( 'ufg_group' ) - ->from( 'user_former_groups' ) - ->where( [ 'ufg_user' => $user->getId( $this->wikiId ) ] ) - ->caller( __METHOD__ ) - ->fetchResultSet(); - $formerGroups = []; - foreach ( $res as $row ) { - $formerGroups[] = $row->ufg_group; - } + $formerGroups = $this->store->getFormerGroups( $user, $queryFlags ); $this->setCache( $userKey, self::CACHE_FORMER, $formerGroups, $queryFlags ); return $this->userGroupCache[$userKey][self::CACHE_FORMER]; @@ -794,19 +767,7 @@ public function getUserGroupMemberships( return []; } - $queryBuilder = $this->newQueryBuilder( $this->getDBConnectionRefForQueryFlags( $queryFlags ) ); - $res = $queryBuilder - ->where( [ 'ug_user' => $user->getId( $this->wikiId ) ] ) - ->caller( __METHOD__ ) - ->fetchResultSet(); - - $ugms = []; - foreach ( $res as $row ) { - $ugm = $this->newGroupMembershipFromRow( $row ); - if ( !$ugm->isExpired() ) { - $ugms[$ugm->getGroup()] = $ugm; - } - } + $ugms = $this->store->getGroupMemberships( $user, $queryFlags ); ksort( $ugms ); $this->setCache( $userKey, self::CACHE_MEMBERSHIP, $ugms, $queryFlags ); @@ -814,6 +775,21 @@ public function getUserGroupMemberships( return $ugms; } + /** + * Loads and returns UserGroupMembership objects for all the groups users currently + * belong to. + * + * @param array $userIds the user ids to search for + * @param int $queryFlags + * @return UserGroupMembership[][] Associative array of (user id => (group name => UserGroupMembership object)) + */ + public function getUserGroupMembershipsFromUserIds( + array $userIds, + int $queryFlags = IDBAccessObject::READ_NORMAL + ): array { + return $this->store->getGroupMembershipsFromUserIds( $userIds, $queryFlags ); + } + /** * Add the user to the given group. This takes immediate effect. * If the user is already in the group, the expiry time will be updated to the new @@ -868,61 +844,9 @@ public function addUserToGroup( } $oldUgms = $this->getUserGroupMemberships( $user, IDBAccessObject::READ_LATEST ); - $dbw = $this->dbProvider->getPrimaryDatabase( $this->wikiId ); - - $dbw->startAtomic( __METHOD__ ); - $dbw->newInsertQueryBuilder() - ->insertInto( 'user_groups' ) - ->ignore() - ->row( [ - 'ug_user' => $user->getId( $this->wikiId ), - 'ug_group' => $group, - 'ug_expiry' => $expiry ? $dbw->timestamp( $expiry ) : null, - ] ) - ->caller( __METHOD__ )->execute(); - - $affected = $dbw->affectedRows(); - if ( !$affected ) { - // Conflicting row already exists; it should be overridden if it is either expired - // or if $allowUpdate is true and the current row is different than the loaded row. - $conds = [ - 'ug_user' => $user->getId( $this->wikiId ), - 'ug_group' => $group - ]; - if ( $allowUpdate ) { - // Update the current row if its expiry does not match that of the loaded row - $conds[] = $expiry - ? $dbw->expr( 'ug_expiry', '=', null ) - ->or( 'ug_expiry', '!=', $dbw->timestamp( $expiry ) ) - : $dbw->expr( 'ug_expiry', '!=', null ); - } else { - // Update the current row if it is expired - $conds[] = $dbw->expr( 'ug_expiry', '<', $dbw->timestamp() ); - } - $dbw->newUpdateQueryBuilder() - ->update( 'user_groups' ) - ->set( [ 'ug_expiry' => $expiry ? $dbw->timestamp( $expiry ) : null ] ) - ->where( $conds ) - ->caller( __METHOD__ )->execute(); - $affected = $dbw->affectedRows(); - } - $dbw->endAtomic( __METHOD__ ); - - // Purge old, expired memberships from the DB - DeferredUpdates::addCallableUpdate( function ( $fname ) { - $dbr = $this->dbProvider->getReplicaDatabase( $this->wikiId ); - $hasExpiredRow = (bool)$dbr->newSelectQueryBuilder() - ->select( '1' ) - ->from( 'user_groups' ) - ->where( [ $dbr->expr( 'ug_expiry', '<', $dbr->timestamp() ) ] ) - ->caller( $fname ) - ->fetchField(); - if ( $hasExpiredRow ) { - $this->jobQueueGroup->push( new UserGroupExpiryJob( [] ) ); - } - } ); + $changed = $this->store->addGroup( $user, $group, $expiry, $allowUpdate ); - if ( $affected > 0 ) { + if ( $changed ) { $oldUgms[$group] = new UserGroupMembership( $user->getId( $this->wikiId ), $group, $expiry ); if ( !$oldUgms[$group]->isExpired() ) { $this->setCache( @@ -998,21 +922,9 @@ public function removeUserFromGroup( UserIdentity $user, string $group ): bool { $oldUgms = $this->getUserGroupMemberships( $user, IDBAccessObject::READ_LATEST ); $oldFormerGroups = $this->getUserFormerGroups( $user, IDBAccessObject::READ_LATEST ); - $dbw = $this->dbProvider->getPrimaryDatabase( $this->wikiId ); - $dbw->newDeleteQueryBuilder() - ->deleteFrom( 'user_groups' ) - ->where( [ 'ug_user' => $user->getId( $this->wikiId ), 'ug_group' => $group ] ) - ->caller( __METHOD__ )->execute(); - - if ( !$dbw->affectedRows() ) { + if ( !$this->store->removeGroup( $user, $group ) ) { return false; } - // Remember that the user was in this group - $dbw->newInsertQueryBuilder() - ->insertInto( 'user_former_groups' ) - ->ignore() - ->row( [ 'ufg_user' => $user->getId( $this->wikiId ), 'ufg_group' => $group ] ) - ->caller( __METHOD__ )->execute(); unset( $oldUgms[$group] ); $userKey = $this->getCacheKey( $user ); @@ -1026,23 +938,6 @@ public function removeUserFromGroup( UserIdentity $user, string $group ): bool { return true; } - /** - * Return the query builder to build upon and query - * - * @param IReadableDatabase $db - * @return SelectQueryBuilder - * @internal - */ - public function newQueryBuilder( IReadableDatabase $db ): SelectQueryBuilder { - return $db->newSelectQueryBuilder() - ->select( [ - 'ug_user', - 'ug_group', - 'ug_expiry', - ] ) - ->from( 'user_groups' ); - } - /** * Purge expired memberships from the user_groups table * @internal @@ -1055,54 +950,8 @@ public function purgeExpired() { return false; } - $ticket = $this->dbProvider->getEmptyTransactionTicket( __METHOD__ ); - $dbw = $this->dbProvider->getPrimaryDatabase( $this->wikiId ); - - $lockKey = "{$dbw->getDomainID()}:UserGroupManager:purge"; // per-wiki - $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 0 ); - if ( !$scopedLock ) { - return false; // already running - } - - $now = time(); - $purgedRows = 0; - do { - $dbw->startAtomic( __METHOD__ ); - $res = $this->newQueryBuilder( $dbw ) - ->where( [ $dbw->expr( 'ug_expiry', '<', $dbw->timestamp( $now ) ) ] ) - ->forUpdate() - ->limit( 100 ) - ->caller( __METHOD__ ) - ->fetchResultSet(); - - if ( $res->numRows() > 0 ) { - $insertData = []; // array of users/groups to insert to user_former_groups - $deleteCond = []; // array for deleting the rows that are to be moved around - foreach ( $res as $row ) { - $insertData[] = [ 'ufg_user' => $row->ug_user, 'ufg_group' => $row->ug_group ]; - $deleteCond[] = $dbw - ->expr( 'ug_user', '=', $row->ug_user ) - ->and( 'ug_group', '=', $row->ug_group ); - } - // Delete the rows we're about to move - $dbw->newDeleteQueryBuilder() - ->deleteFrom( 'user_groups' ) - ->where( $dbw->orExpr( $deleteCond ) ) - ->caller( __METHOD__ )->execute(); - // Push the groups to user_former_groups - $dbw->newInsertQueryBuilder() - ->insertInto( 'user_former_groups' ) - ->ignore() - ->rows( $insertData ) - ->caller( __METHOD__ )->execute(); - // Count how many rows were purged - $purgedRows += $res->numRows(); - } + $purgedRows = $this->store->purgeExpired(); - $dbw->endAtomic( __METHOD__ ); - - $this->dbProvider->commitAndWaitForReplication( __METHOD__, $ticket ); - } while ( $res->numRows() > 0 ); return $purgedRows; } @@ -1240,17 +1089,6 @@ private function clearUserCacheForKind( UserIdentity $user, string $cacheKind ) unset( $this->queryFlagsUsedForCaching[$userKey][$cacheKind] ); } - /** - * @param int $recency a bit field composed of IDBAccessObject::READ_XXX flags - * @return IReadableDatabase - */ - private function getDBConnectionRefForQueryFlags( int $recency ): IReadableDatabase { - if ( ( IDBAccessObject::READ_LATEST & $recency ) == IDBAccessObject::READ_LATEST ) { - return $this->dbProvider->getPrimaryDatabase( $this->wikiId ); - } - return $this->dbProvider->getReplicaDatabase( $this->wikiId ); - } - /** * Gets a unique key for various caches. * @param UserIdentity $user diff --git a/includes/user/UserGroupManagerFactory.php b/includes/user/UserGroupManagerFactory.php index f5d92d518b339..3aa36ea3acc33 100644 --- a/includes/user/UserGroupManagerFactory.php +++ b/includes/user/UserGroupManagerFactory.php @@ -105,11 +105,14 @@ public function getUserGroupManager( $wikiId = UserIdentity::LOCAL ): UserGroupM $this->instances[$key] = new UserGroupManager( $this->options, $this->readOnlyMode, - $this->dbLoadBalancerFactory, + new UserGroupStore( + $this->dbLoadBalancerFactory, + $this->jobQueueGroupFactory->makeJobQueueGroup( $wikiId ), + $wikiId + ), $this->hookContainer, $this->userEditTracker, $this->groupPermissionLookup, - $this->jobQueueGroupFactory->makeJobQueueGroup( $wikiId ), $this->logger, $this->tempUserConfig, $this->clearCacheCallbacks, diff --git a/includes/user/UserGroupStore.php b/includes/user/UserGroupStore.php new file mode 100644 index 0000000000000..7af3d57978590 --- /dev/null +++ b/includes/user/UserGroupStore.php @@ -0,0 +1,349 @@ +ug_user, + $row->ug_group, + $row->ug_expiry === null ? null : wfTimestamp( + TS_MW, + $row->ug_expiry + ) + ); + } + + /** + * Loads and returns UserGroupMembership objects for all the groups a user currently + * belongs to. + * + * @param UserIdentity $user the user to search for + * @param int $queryFlags + * @return UserGroupMembership[] Associative array of (group name => UserGroupMembership object) + */ + public function getGroupMemberships( + UserIdentity $user, + int $queryFlags = IDBAccessObject::READ_NORMAL + ): array { + $res = $this->newQueryBuilder( $this->getConnectionForQueryFlags( $queryFlags ) ) + ->where( [ 'ug_user' => $user->getId( $this->wikiId ) ] ) + ->caller( __METHOD__ ) + ->fetchResultSet(); + + $ugms = []; + foreach ( $res as $row ) { + $ugm = $this->newGroupMembershipFromRow( $row ); + if ( !$ugm->isExpired() ) { + $ugms[$ugm->getGroup()] = $ugm; + } + } + + return $ugms; + } + + /** + * Loads and returns UserGroupMembership objects for all the groups users currently + * belong to. + * + * @param array $userIds the user ids to search for + * @param int $queryFlags + * @return UserGroupMembership[][] Associative array of (user id => (group name => UserGroupMembership object)) + */ + public function getGroupMembershipsFromUserIds( + array $userIds, + int $queryFlags = IDBAccessObject::READ_NORMAL + ): array { + // Lookup groups for all the users + $res = $this->newQueryBuilder( $this->getConnectionForQueryFlags( $queryFlags ) ) + ->where( [ 'ug_user' => $userIds ] ) + ->caller( __METHOD__ ) + ->fetchResultSet(); + + $ugmsByUserId = []; + foreach ( $res as $row ) { + $ugm = $this->newGroupMembershipFromRow( $row ); + if ( !$ugm->isExpired() ) { + $ugmsByUserId[$row->ug_user][$row->ug_group] = $ugm; + } + } + + return $ugmsByUserId; + } + + /** + * Add the user to the given group. This takes immediate effect. + * If the user is already in the group, the expiry time will be updated to the new + * expiry time. (If $expiry is omitted or null, the membership will be altered to + * never expire.) + * + * @param UserIdentity $user + * @param string $group Name of the group to add + * @param string|null $expiry Optional expiry timestamp in any format acceptable to + * wfTimestamp(), or null if the group assignment should not expire + * @param bool $allowUpdate Whether to perform "upsert" instead of INSERT + * @return bool + */ + public function addGroup( + UserIdentity $user, string $group, string|null $expiry, bool $allowUpdate + ): bool { + $dbw = $this->dbProvider->getPrimaryDatabase( $this->wikiId ); + + $dbw->startAtomic( __METHOD__ ); + $dbw->newInsertQueryBuilder()->insertInto( 'user_groups' )->ignore()->row( [ + 'ug_user' => $user->getId( $this->wikiId ), + 'ug_group' => $group, + 'ug_expiry' => $expiry ? $dbw->timestamp( $expiry ) : null, + ] )->caller( __METHOD__ )->execute(); + + $affected = $dbw->affectedRows(); + if ( !$affected ) { + // Conflicting row already exists; it should be overridden if it is either expired + // or if $allowUpdate is true and the current row is different than the loaded row. + $conds = [ + 'ug_user' => $user->getId( $this->wikiId ), + 'ug_group' => $group, + ]; + if ( $allowUpdate ) { + // Update the current row if its expiry does not match that of the loaded row + $conds[] = $expiry + ? $dbw->expr( 'ug_expiry', '=', null ) + ->or( 'ug_expiry', '!=', $dbw->timestamp( $expiry ) ) + : $dbw->expr( 'ug_expiry', '!=', null ); + } else { + // Update the current row if it is expired + $conds[] = $dbw->expr( 'ug_expiry', '<', $dbw->timestamp() ); + } + $dbw->newUpdateQueryBuilder() + ->update( 'user_groups' ) + ->set( [ + 'ug_expiry' => $expiry ? $dbw->timestamp( $expiry ) : null, + ] ) + ->where( $conds ) + ->caller( __METHOD__ ) + ->execute(); + $affected = $dbw->affectedRows(); + } + $dbw->endAtomic( __METHOD__ ); + + // Purge old, expired memberships from the DB + DeferredUpdates::addCallableUpdate( function ( $fname ) { + $dbr = $this->dbProvider->getReplicaDatabase( $this->wikiId ); + $hasExpiredRow = (bool)$dbr->newSelectQueryBuilder() + ->select( '1' ) + ->from( 'user_groups' ) + ->where( [ $dbr->expr( 'ug_expiry', '<', $dbr->timestamp() ) ] ) + ->caller( $fname ) + ->fetchField(); + if ( $hasExpiredRow ) { + $this->jobQueueGroup->push( new UserGroupExpiryJob( [] ) ); + } + } ); + + return $affected > 0; + } + + /** + * Remove the user from the given group. This takes immediate effect. + * + * @param UserIdentity $user + * @param string $group Name of the group to remove + * @return bool + */ + public function removeGroup( UserIdentity $user, string $group ): bool { + $dbw = $this->dbProvider->getPrimaryDatabase( $this->wikiId ); + $dbw->newDeleteQueryBuilder() + ->deleteFrom( 'user_groups' ) + ->where( [ + 'ug_user' => $user->getId( $this->wikiId ), + 'ug_group' => $group, + ] ) + ->caller( __METHOD__ ) + ->execute(); + + if ( !$dbw->affectedRows() ) { + return false; + } + + // Remember that the user was in this group + $dbw->newInsertQueryBuilder() + ->insertInto( 'user_former_groups' ) + ->ignore() + ->row( [ + 'ufg_user' => $user->getId( $this->wikiId ), + 'ufg_group' => $group, + ] ) + ->caller( __METHOD__ ) + ->execute(); + + return true; + } + + /** + * Returns the groups the user has belonged to. + * + * The user may still belong to the returned groups. Compare with + * getUserGroups(). + * + * The function will not return groups the user had belonged to before MW 1.17 + * + * @param UserIdentity $user + * @param int $queryFlags + * @return string[] Names of the groups the user has belonged to. + */ + public function getFormerGroups( + UserIdentity $user, + int $queryFlags = IDBAccessObject::READ_NORMAL + ): array { + return $this->getConnectionForQueryFlags( $queryFlags ) + ->newSelectQueryBuilder() + ->select( 'ufg_group' ) + ->from( 'user_former_groups' ) + ->where( [ 'ufg_user' => $user->getId( $this->wikiId ) ] ) + ->caller( __METHOD__ ) + ->fetchFieldValues(); + } + + /** + * Purge expired memberships from the user_groups table + * @internal + * @note this could be slow and is intended for use in a background job + * @return int|false false if lock cannot be acquired, + * the number of rows purged (might be 0) otherwise + */ + public function purgeExpired(): int|false { + $ticket = $this->dbProvider->getEmptyTransactionTicket( __METHOD__ ); + $dbw = $this->dbProvider->getPrimaryDatabase( $this->wikiId ); + + $lockKey = "{$dbw->getDomainID()}:UserGroupManager:purge"; // per-wiki + $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 0 ); + if ( !$scopedLock ) { + return false; // already running + } + + $now = time(); + $purgedRows = 0; + do { + $dbw->startAtomic( __METHOD__ ); + $res = $this->newQueryBuilder( $dbw ) + ->where( [ + $dbw->expr( 'ug_expiry', '<', $dbw->timestamp( $now ) ), + ] ) + ->forUpdate() + ->limit( 100 ) + ->caller( __METHOD__ ) + ->fetchResultSet(); + + if ( $res->numRows() > 0 ) { + $insertData = []; // array of users/groups to insert to user_former_groups + $deleteCond = []; // array for deleting the rows that are to be moved around + foreach ( $res as $row ) { + $insertData[] = [ 'ufg_user' => $row->ug_user, 'ufg_group' => $row->ug_group ]; + $deleteCond[] = $dbw->expr( 'ug_user', '=', $row->ug_user ) + ->and( 'ug_group', '=', $row->ug_group ); + } + // Delete the rows we're about to move + $dbw->newDeleteQueryBuilder() + ->deleteFrom( 'user_groups' ) + ->where( $dbw->orExpr( $deleteCond ) ) + ->caller( __METHOD__ ) + ->execute(); + // Push the groups to user_former_groups + $dbw->newInsertQueryBuilder() + ->insertInto( 'user_former_groups' ) + ->ignore() + ->rows( $insertData ) + ->caller( __METHOD__ ) + ->execute(); + // Count how many rows were purged + $purgedRows += $res->numRows(); + } + + $dbw->endAtomic( __METHOD__ ); + + $this->dbProvider->commitAndWaitForReplication( __METHOD__, $ticket ); + } while ( $res->numRows() > 0 ); + + return $purgedRows; + } + + /** + * Return the query builder to build upon and query + * + * @param IReadableDatabase $db + * @return SelectQueryBuilder + * @internal + */ + private function newQueryBuilder( IReadableDatabase $db ): SelectQueryBuilder { + return $db->newSelectQueryBuilder() + ->select( [ + 'ug_user', + 'ug_group', + 'ug_expiry', + ] ) + ->from( 'user_groups' ); + } + + /** + * @param int $recency a bit field composed of IDBAccessObject::READ_XXX flags + * @return IReadableDatabase + */ + private function getConnectionForQueryFlags( int $recency ): IReadableDatabase { + if ( ( IDBAccessObject::READ_LATEST & $recency ) == IDBAccessObject::READ_LATEST ) { + return $this->dbProvider->getPrimaryDatabase( $this->wikiId ); + } + + return $this->dbProvider->getReplicaDatabase( $this->wikiId ); + } +} diff --git a/tests/phpunit/includes/user/UserGroupManagerTest.php b/tests/phpunit/includes/user/UserGroupManagerTest.php index 041c6db0fb92d..483b3572a3a18 100644 --- a/tests/phpunit/includes/user/UserGroupManagerTest.php +++ b/tests/phpunit/includes/user/UserGroupManagerTest.php @@ -36,6 +36,7 @@ use MediaWiki\User\User; use MediaWiki\User\UserEditTracker; use MediaWiki\User\UserGroupManager; +use MediaWiki\User\UserGroupStore; use MediaWiki\User\UserIdentity; use MediaWiki\User\UserIdentityValue; use MediaWiki\Utils\MWTimestamp; @@ -95,11 +96,13 @@ private function getManager( $services->getMainConfig() ), $services->getReadOnlyMode(), - $services->getDBLoadBalancerFactory(), + new UserGroupStore( + $services->getDBLoadBalancerFactory(), + $services->getJobQueueGroup() + ), $services->getHookContainer(), $userEditTrackerOverride ?? $services->getUserEditTracker(), $services->getGroupPermissionsLookup(), - $services->getJobQueueGroup(), new TestLogger(), new RealTempUserConfig( [ 'enabled' => true,