user: Move UserRightsProxy::invalidateCache to UserFactory

To remove the use of UserRightsProxy from Special:UserRights the touch
update happen in UserRightsProxy::invalidateCache needs a new location,
before a cross-wiki aware UserIdentityLookup can be used.

Move it to the UserFactory service which is the storage layer for the
user table to handle user_touched database field for cross-wiki user
right changes.

For compatibility call the User::invalidateCache for local identities

Bug: T255309
Bug: T307301
Depends-On: I60a665de6aa8550d9bc0f5c78d54b8894ea5913e
Change-Id: I0c3d36a05abaa3548e554acf8d34e8e959c26776
This commit is contained in:
Umherirrender 2023-02-20 22:51:41 +01:00
parent 9a7e435e79
commit 9116c38745
6 changed files with 134 additions and 26 deletions

View file

@ -2101,7 +2101,10 @@ return [
'UserFactory' => static function ( MediaWikiServices $services ): UserFactory {
return new UserFactory(
$services->getDBLoadBalancer(),
new ServiceOptions(
UserFactory::CONSTRUCTOR_OPTIONS, $services->getMainConfig()
),
$services->getDBLoadBalancerFactory(),
$services->getUserNameUtils()
);
},

View file

@ -460,7 +460,7 @@ class SpecialUserRights extends SpecialPage {
$newUGMs = $userGroupManager->getUserGroupMemberships( $user );
// Ensure that caches are cleared
$user->invalidateCache();
$this->userFactory->invalidateCache( $user );
// update groups in external authentication database
$this->getHookRunner()->onUserGroupsChanged( $user, $add, $remove,

View file

@ -25,9 +25,13 @@ namespace MediaWiki\User;
use DBAccessObjectUtils;
use IDBAccessObject;
use InvalidArgumentException;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\MainConfigNames;
use MediaWiki\Permissions\Authority;
use stdClass;
use User;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\ILBFactory;
use Wikimedia\Rdbms\ILoadBalancer;
/**
@ -42,6 +46,18 @@ class UserFactory implements IDBAccessObject, UserRigorOptions {
* READ_* constants are inherited from IDBAccessObject
*/
/** @internal */
public const CONSTRUCTOR_OPTIONS = [
MainConfigNames::SharedDB,
MainConfigNames::SharedTables,
];
/** @var ServiceOptions */
private $options;
/** @var ILBFactory */
private $loadBalancerFactory;
/** @var ILoadBalancer */
private $loadBalancer;
@ -52,14 +68,19 @@ class UserFactory implements IDBAccessObject, UserRigorOptions {
private $lastUserFromIdentity = null;
/**
* @param ILoadBalancer $loadBalancer
* @param ServiceOptions $options
* @param ILBFactory $loadBalancerFactory
* @param UserNameUtils $userNameUtils
*/
public function __construct(
ILoadBalancer $loadBalancer,
ServiceOptions $options,
ILBFactory $loadBalancerFactory,
UserNameUtils $userNameUtils
) {
$this->loadBalancer = $loadBalancer;
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
$this->options = $options;
$this->loadBalancerFactory = $loadBalancerFactory;
$this->loadBalancer = $loadBalancerFactory->getMainLB();
$this->userNameUtils = $userNameUtils;
}
@ -339,4 +360,61 @@ class UserFactory implements IDBAccessObject, UserRigorOptions {
$user->setName( $name );
return $user;
}
/**
* Purge user related caches, "touch" the user table to invalidate further caches
* @since 1.41
* @param UserIdentity $userIdentity
*/
public function invalidateCache( UserIdentity $userIdentity ) {
if ( !$userIdentity->isRegistered() ) {
return;
}
$wikiId = $userIdentity->getWikiId();
if ( $wikiId === UserIdentity::LOCAL ) {
$legacyUser = $this->newFromUserIdentity( $userIdentity );
// Update user_touched within User class to manage state of User::mTouched for CAS check
$legacyUser->invalidateCache();
} else {
// cross-wiki invalidation
$userId = $userIdentity->getId( $wikiId );
$dbw = $this->getUserTableConnection( ILoadBalancer::DB_PRIMARY, $wikiId );
$dbw->newUpdateQueryBuilder()
->update( 'user' )
->set( [ 'user_touched' => $dbw->timestamp() ] )
->where( [ 'user_id' => $userId ] )
->caller( __METHOD__ )->execute();
$dbw->onTransactionPreCommitOrIdle(
static function () use ( $wikiId, $userId ) {
User::purge( $wikiId, $userId );
},
__METHOD__
);
}
}
/**
* @param int $mode
* @param string|false $wikiId
* @return IDatabase
*/
private function getUserTableConnection( $mode, $wikiId ) {
if ( is_string( $wikiId ) && $this->loadBalancerFactory->getLocalDomainID() === $wikiId ) {
$wikiId = UserIdentity::LOCAL;
}
if ( $this->options->get( MainConfigNames::SharedDB ) &&
in_array( 'user', $this->options->get( MainConfigNames::SharedTables ) )
) {
// The main LB is aliased for the shared database in Setup.php
$lb = $this->loadBalancer;
} else {
$lb = $this->loadBalancerFactory->getMainLB( $wikiId );
}
return $lb->getConnection( $mode, [], $wikiId );
}
}

View file

@ -39,8 +39,6 @@ class UserRightsProxy implements UserIdentity {
/** @var IDatabase */
private $db;
/** @var IDatabase */
private $userdb;
/** @var string */
private $dbDomain;
/** @var string */
@ -56,14 +54,12 @@ class UserRightsProxy implements UserIdentity {
* @see newFromId()
* @see newFromName()
* @param IDatabase $db Db connection
* @param IDatabase $userdb Db connection (user table)
* @param string $dbDomain Database name
* @param string $name User name
* @param int $id User ID
*/
private function __construct( $db, $userdb, $dbDomain, $name, $id ) {
private function __construct( $db, $dbDomain, $name, $id ) {
$this->db = $db;
$this->userdb = $userdb;
$this->dbDomain = $dbDomain;
$this->name = $name;
$this->id = intval( $id );
@ -157,7 +153,7 @@ class UserRightsProxy implements UserIdentity {
if ( $row !== false ) {
return new UserRightsProxy(
$db, $userdb, $dbDomain, $row->user_name, intval( $row->user_id ) );
$db, $dbDomain, $row->user_name, intval( $row->user_id ) );
}
}
return null;
@ -294,21 +290,9 @@ class UserRightsProxy implements UserIdentity {
* Replaces User::touchUser()
*/
public function invalidateCache() {
$this->userdb->update(
'user',
[ 'user_touched' => $this->userdb->timestamp() ],
[ 'user_id' => $this->id ],
__METHOD__
);
$domainId = $this->userdb->getDomainID();
$userId = $this->id;
$this->userdb->onTransactionPreCommitOrIdle(
static function () use ( $domainId, $userId ) {
User::purge( $domainId, $userId );
},
__METHOD__
);
MediaWikiServices::getInstance()
->getUserFactory()
->invalidateCache( $this );
}
/**

View file

@ -5,6 +5,10 @@ use MediaWiki\Permissions\UltimateAuthority;
use MediaWiki\User\UserFactory;
use MediaWiki\User\UserIdentityValue;
use Wikimedia\IPUtils;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\ILoadBalancer;
use Wikimedia\Rdbms\LBFactory;
use Wikimedia\Rdbms\UpdateQueryBuilder;
/**
* @covers \MediaWiki\User\UserFactory
@ -222,4 +226,38 @@ class UserFactoryTest extends MediaWikiIntegrationTestCase {
$this->assertFalse( $user->isNamed() );
}
public function testInvalidateCacheLocal() {
$userMock = $this->createMock( User::class );
$userMock->method( 'isRegistered' )->willReturn( true );
$userMock->method( 'getWikiId' )->willReturn( User::LOCAL );
$userMock->expects( $this->once() )->method( 'invalidateCache' );
$this->getUserFactory()->invalidateCache( $userMock );
}
public function testInvalidateCacheCrossWiki() {
$dbMock = $this->createMock( IDatabase::class );
$dbMock->method( 'timestamp' )->willReturn( 'timestamp' );
$dbMock->expects( $this->once() )
->method( 'newUpdateQueryBuilder' )
->willReturn( new UpdateQueryBuilder( $dbMock ) );
$dbMock->expects( $this->once() )
->method( 'update' )
->with(
'user',
[ 'user_touched' => 'timestamp' ],
[ 'user_id' => 123 ]
);
$lbMock = $this->createMock( ILoadBalancer::class );
$lbMock->method( 'getConnection' )->willReturn( $dbMock );
$lbFactoryMock = $this->createMock( LBFactory::class );
$lbFactoryMock->method( 'getMainLB' )->willReturn( $lbMock );
$this->setService( 'DBLoadBalancerFactory', $lbFactoryMock );
$user = new UserIdentityValue( 123, 'UserIdentityCacheUpdaterTest', 'meta' );
$this->getUserFactory()->invalidateCache( $user );
}
}

View file

@ -10,6 +10,7 @@ use UserRightsProxy;
use Wikimedia\Rdbms\DBConnRef;
use Wikimedia\Rdbms\ILoadBalancer;
use Wikimedia\Rdbms\LBFactory;
use Wikimedia\Rdbms\UpdateQueryBuilder;
/**
* @coversDefaultClass UserRightsProxy
@ -204,6 +205,9 @@ class UserRightsProxyTest extends MediaWikiIntegrationTestCase {
]
]
);
$dbMock->expects( $this->once() )
->method( 'newUpdateQueryBuilder' )
->willReturn( new UpdateQueryBuilder( $dbMock ) );
$dbMock->expects( $this->once() )
->method( 'update' )
->with(
@ -216,6 +220,7 @@ class UserRightsProxyTest extends MediaWikiIntegrationTestCase {
$lbMock = $this->createMock( ILoadBalancer::class );
$lbMock->method( 'getMaintenanceConnectionRef' )->willReturn( $dbMock );
$lbMock->method( 'getConnection' )->willReturn( $dbMock );
$lbFactoryMock = $this->createMock( LBFactory::class );
$lbFactoryMock->method( 'getMainLB' )->willReturn( $lbMock );