2020-01-17 06:21:28 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
use MediaWiki\Config\ServiceOptions;
|
|
|
|
|
use MediaWiki\MediaWikiServices;
|
|
|
|
|
use MediaWiki\User\UserOptionsLookup;
|
|
|
|
|
use MediaWiki\User\UserOptionsManager;
|
|
|
|
|
use Psr\Log\NullLogger;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @group Database
|
|
|
|
|
* @covers MediaWiki\User\UserOptionsManager
|
|
|
|
|
*/
|
|
|
|
|
class UserOptionsManagerTest extends UserOptionsLookupTest {
|
|
|
|
|
|
|
|
|
|
private function getManager(
|
|
|
|
|
string $langCode = 'qqq',
|
|
|
|
|
array $defaultOptionsOverrides = []
|
|
|
|
|
) {
|
|
|
|
|
$services = MediaWikiServices::getInstance();
|
|
|
|
|
return new UserOptionsManager(
|
|
|
|
|
new ServiceOptions(
|
|
|
|
|
UserOptionsManager::CONSTRUCTOR_OPTIONS,
|
|
|
|
|
new HashConfig( [ 'HiddenPrefs' => [ 'hidden_user_option' ] ] )
|
|
|
|
|
),
|
|
|
|
|
$this->getDefaultManager( $langCode, $defaultOptionsOverrides ),
|
|
|
|
|
$services->getLanguageConverterFactory(),
|
|
|
|
|
$services->getDBLoadBalancer(),
|
Hooks::run() call site migration
Migrate all callers of Hooks::run() to use the new
HookContainer/HookRunner system.
General principles:
* Use DI if it is already used. We're not changing the way state is
managed in this patch.
* HookContainer is always injected, not HookRunner. HookContainer
is a service, it's a more generic interface, it is the only
thing that provides isRegistered() which is needed in some cases,
and a HookRunner can be efficiently constructed from it
(confirmed by benchmark). Because HookContainer is needed
for object construction, it is also needed by all factories.
* "Ask your friendly local base class". Big hierarchies like
SpecialPage and ApiBase have getHookContainer() and getHookRunner()
methods in the base class, and classes that extend that base class
are not expected to know or care where the base class gets its
HookContainer from.
* ProtectedHookAccessorTrait provides protected getHookContainer() and
getHookRunner() methods, getting them from the global service
container. The point of this is to ease migration to DI by ensuring
that call sites ask their local friendly base class rather than
getting a HookRunner from the service container directly.
* Private $this->hookRunner. In some smaller classes where accessor
methods did not seem warranted, there is a private HookRunner property
which is accessed directly. Very rarely (two cases), there is a
protected property, for consistency with code that conventionally
assumes protected=private, but in cases where the class might actually
be overridden, a protected accessor is preferred over a protected
property.
* The last resort: Hooks::runner(). Mostly for static, file-scope and
global code. In a few cases it was used for objects with broken
construction schemes, out of horror or laziness.
Constructors with new required arguments:
* AuthManager
* BadFileLookup
* BlockManager
* ClassicInterwikiLookup
* ContentHandlerFactory
* ContentSecurityPolicy
* DefaultOptionsManager
* DerivedPageDataUpdater
* FullSearchResultWidget
* HtmlCacheUpdater
* LanguageFactory
* LanguageNameUtils
* LinkRenderer
* LinkRendererFactory
* LocalisationCache
* MagicWordFactory
* MessageCache
* NamespaceInfo
* PageEditStash
* PageHandlerFactory
* PageUpdater
* ParserFactory
* PermissionManager
* RevisionStore
* RevisionStoreFactory
* SearchEngineConfig
* SearchEngineFactory
* SearchFormWidget
* SearchNearMatcher
* SessionBackend
* SpecialPageFactory
* UserNameUtils
* UserOptionsManager
* WatchedItemQueryService
* WatchedItemStore
Constructors with new optional arguments:
* DefaultPreferencesFactory
* Language
* LinkHolderArray
* MovePage
* Parser
* ParserCache
* PasswordReset
* Router
setHookContainer() now required after construction:
* AuthenticationProvider
* ResourceLoaderModule
* SearchEngine
Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
2020-03-19 02:42:09 +00:00
|
|
|
new NullLogger(),
|
|
|
|
|
$services->getHookContainer()
|
2020-01-17 06:21:28 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function getLookup(
|
|
|
|
|
string $langCode = 'qqq',
|
|
|
|
|
array $defaultOptionsOverrides = []
|
|
|
|
|
) : UserOptionsLookup {
|
|
|
|
|
return $this->getManager( $langCode, $defaultOptionsOverrides );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers MediaWiki\User\UserOptionsManager::getOption
|
|
|
|
|
*/
|
|
|
|
|
public function testGetOptionsExcludeDefaults() {
|
|
|
|
|
$manager = $this->getManager();
|
|
|
|
|
$manager->setOption( $this->getAnon( __METHOD__ ), 'new_option', 'new_value' );
|
|
|
|
|
$this->assertSame( [
|
|
|
|
|
'language' => 'en',
|
|
|
|
|
'variant' => 'en',
|
|
|
|
|
'new_option' => 'new_value'
|
|
|
|
|
], $manager->getOptions( $this->getAnon( __METHOD__ ), UserOptionsManager::EXCLUDE_DEFAULTS ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers MediaWiki\User\UserOptionsManager::getOption
|
|
|
|
|
*/
|
|
|
|
|
public function testGetOptionHiddenPref() {
|
|
|
|
|
$user = $this->getAnon( __METHOD__ );
|
|
|
|
|
$manager = $this->getManager();
|
|
|
|
|
$manager->setOption( $user, 'hidden_user_option', 'hidden_value' );
|
|
|
|
|
$this->assertNull( $manager->getOption( $user, 'hidden_user_option' ) );
|
|
|
|
|
$this->assertSame( 'hidden_value',
|
|
|
|
|
$manager->getOption( $user, 'hidden_user_option', null, true ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers MediaWiki\User\UserOptionsManager::setOption
|
|
|
|
|
*/
|
|
|
|
|
public function testSetOptionNullIsDefault() {
|
|
|
|
|
$user = $this->getAnon( __METHOD__ );
|
|
|
|
|
$manager = $this->getManager();
|
|
|
|
|
$manager->setOption( $user, 'default_string_option', 'override_value' );
|
|
|
|
|
$this->assertSame( 'override_value', $manager->getOption( $user, 'default_string_option' ) );
|
|
|
|
|
$manager->setOption( $user, 'default_string_option', null );
|
|
|
|
|
$this->assertSame( 'string_value', $manager->getOption( $user, 'default_string_option' ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers MediaWiki\User\UserOptionsManager::getOption
|
|
|
|
|
* @covers MediaWiki\User\UserOptionsManager::setOption
|
|
|
|
|
* @covers MediaWiki\User\UserOptionsManager::saveOptions
|
|
|
|
|
*/
|
|
|
|
|
public function testGetSetSave() {
|
|
|
|
|
$user = $this->getTestUser()->getUser();
|
|
|
|
|
$manager = $this->getManager();
|
|
|
|
|
$this->assertSame( [], $manager->getOptions( $user, UserOptionsManager::EXCLUDE_DEFAULTS ) );
|
|
|
|
|
$manager->setOption( $user, 'string_option', 'user_value' );
|
|
|
|
|
$manager->setOption( $user, 'int_option', 42 );
|
|
|
|
|
$manager->setOption( $user, 'bool_option', true );
|
|
|
|
|
$this->assertSame( 'user_value', $manager->getOption( $user, 'string_option' ) );
|
|
|
|
|
$this->assertSame( 42, $manager->getIntOption( $user, 'int_option' ) );
|
|
|
|
|
$this->assertSame( true, $manager->getBoolOption( $user, 'bool_option' ) );
|
|
|
|
|
$manager->saveOptions( $user );
|
|
|
|
|
$manager = $this->getManager();
|
|
|
|
|
$this->assertSame( 'user_value', $manager->getOption( $user, 'string_option' ) );
|
|
|
|
|
$this->assertSame( 42, $manager->getIntOption( $user, 'int_option' ) );
|
|
|
|
|
$this->assertSame( true, $manager->getBoolOption( $user, 'bool_option' ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers MediaWiki\User\UserOptionsManager::loadUserOptions
|
|
|
|
|
*/
|
|
|
|
|
public function testLoadUserOptionsHook() {
|
|
|
|
|
$user = $this->getTestUser()->getUser();
|
|
|
|
|
$this->setTemporaryHook(
|
|
|
|
|
'UserLoadOptions',
|
2021-02-07 13:10:36 +00:00
|
|
|
static function ( User $hookUser, &$options ) use ( $user ) {
|
2020-01-17 06:21:28 +00:00
|
|
|
if ( $hookUser->equals( $user ) ) {
|
|
|
|
|
$options['from_hook'] = 'value_from_hook';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
$this->assertSame( 'value_from_hook', $this->getManager()->getOption( $user, 'from_hook' ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers MediaWiki\User\UserOptionsManager::saveOptions
|
|
|
|
|
*/
|
|
|
|
|
public function testSaveUserOptionsHookAbort() {
|
|
|
|
|
$user = $this->getTestUser()->getUser();
|
|
|
|
|
$this->setTemporaryHook(
|
|
|
|
|
'UserSaveOptions',
|
2021-02-07 13:10:36 +00:00
|
|
|
static function () {
|
2020-01-17 06:21:28 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
$manager = $this->getManager();
|
|
|
|
|
$manager->setOption( $user, 'will_be_aborted_by_hook', 'value' );
|
|
|
|
|
$manager->saveOptions( $user );
|
|
|
|
|
$this->assertNull( $this->getManager()->getOption( $user, 'will_be_aborted_by_hook' ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers MediaWiki\User\UserOptionsManager::saveOptions
|
|
|
|
|
*/
|
|
|
|
|
public function testSaveUserOptionsHookModify() {
|
|
|
|
|
$user = $this->getTestUser()->getUser();
|
|
|
|
|
$this->setTemporaryHook(
|
2020-05-26 18:58:30 +00:00
|
|
|
'UserSaveOptions',
|
2021-02-07 13:10:36 +00:00
|
|
|
static function ( User $hookUser, &$options ) use ( $user ) {
|
2020-01-17 06:21:28 +00:00
|
|
|
if ( $hookUser->equals( $user ) ) {
|
|
|
|
|
$options['from_hook'] = 'value_from_hook';
|
|
|
|
|
}
|
2020-05-26 18:58:30 +00:00
|
|
|
return true;
|
2020-01-17 06:21:28 +00:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
$manager = $this->getManager();
|
|
|
|
|
$manager->saveOptions( $user );
|
|
|
|
|
$this->assertSame( 'value_from_hook', $manager->getOption( $user, 'from_hook' ) );
|
|
|
|
|
$this->assertSame( 'value_from_hook', $this->getManager()->getOption( $user, 'from_hook' ) );
|
|
|
|
|
}
|
2020-05-26 18:58:30 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers MediaWiki\User\UserOptionsManager::saveOptions
|
|
|
|
|
*/
|
|
|
|
|
public function testSaveUserOptionsHookOriginal() {
|
|
|
|
|
$user = $this->getTestUser()->getUser();
|
|
|
|
|
$manager = $this->getManager();
|
|
|
|
|
$originalLanguage = $manager->getOption( $user, 'language' );
|
|
|
|
|
$manager->setOption( $user, 'language', 'ru' );
|
|
|
|
|
$this->setTemporaryHook(
|
|
|
|
|
'UserSaveOptions',
|
|
|
|
|
function ( User $hookUser, &$options, $originalOptions ) use ( $user, $originalLanguage ) {
|
|
|
|
|
if ( $hookUser->equals( $user ) ) {
|
|
|
|
|
$this->assertSame( $originalLanguage, $originalOptions['language'] );
|
|
|
|
|
$this->assertSame( 'ru', $options['language'] );
|
|
|
|
|
$options['language'] = 'tr';
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
$manager->saveOptions( $user );
|
|
|
|
|
$this->assertSame( 'tr', $manager->getOption( $user, 'language' ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers \MediaWiki\User\UserOptionsManager::saveOptions
|
|
|
|
|
* @covers \MediaWiki\User\UserOptionsManager::loadUserOptions
|
|
|
|
|
*/
|
|
|
|
|
public function testLoadOptionsHookReflectsInOriginalOptions() {
|
|
|
|
|
$user = $this->getTestUser()->getUser();
|
|
|
|
|
$manager = $this->getManager();
|
|
|
|
|
$this->setTemporaryHook(
|
|
|
|
|
'UserLoadOptions',
|
2021-02-07 13:10:36 +00:00
|
|
|
static function ( User $hookUser, &$options ) use ( $user ) {
|
2020-05-26 18:58:30 +00:00
|
|
|
if ( $hookUser->equals( $user ) ) {
|
|
|
|
|
$options['from_load_hook'] = 'from_load_hook';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
$this->setTemporaryHook(
|
|
|
|
|
'UserSaveOptions',
|
|
|
|
|
function ( User $hookUser, &$options, $originalOptions ) use ( $user ) {
|
|
|
|
|
if ( $hookUser->equals( $user ) ) {
|
|
|
|
|
$this->assertSame( 'from_load_hook', $options['from_load_hook'] );
|
|
|
|
|
$this->assertSame( 'from_load_hook', $originalOptions['from_load_hook'] );
|
|
|
|
|
$options['from_save_hook'] = 'from_save_hook';
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
$manager->saveOptions( $user );
|
|
|
|
|
$this->assertSame( 'from_load_hook', $manager->getOption( $user, 'from_load_hook' ) );
|
|
|
|
|
$this->assertSame( 'from_save_hook', $manager->getOption( $user, 'from_save_hook' ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers \MediaWiki\User\UserOptionsManager::loadUserOptions
|
|
|
|
|
*/
|
|
|
|
|
public function testInfiniteRecursionOnUserLoadOptionsHook() {
|
|
|
|
|
$user = $this->getTestUser()->getUser();
|
|
|
|
|
$manager = $this->getManager();
|
|
|
|
|
$recursionCounter = 0;
|
|
|
|
|
$this->setTemporaryHook(
|
|
|
|
|
'UserLoadOptions',
|
|
|
|
|
function ( User $hookUser ) use ( $user, $manager, &$recursionCounter ) {
|
|
|
|
|
if ( $hookUser->equals( $user ) ) {
|
|
|
|
|
$recursionCounter += 1;
|
|
|
|
|
$this->assertSame( 1, $recursionCounter );
|
|
|
|
|
$manager->loadUserOptions( $hookUser );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
2020-05-28 18:40:49 +00:00
|
|
|
$manager->loadUserOptions( $user, UserOptionsManager::READ_LATEST );
|
2020-05-26 18:58:30 +00:00
|
|
|
$this->assertSame( 1, $recursionCounter );
|
|
|
|
|
}
|
2020-05-28 18:40:49 +00:00
|
|
|
|
|
|
|
|
public function testSaveOptionsForAnonUser() {
|
|
|
|
|
$this->expectException( InvalidArgumentException::class );
|
|
|
|
|
$this->getManager()->saveOptions( $this->getAnon( __METHOD__ ) );
|
|
|
|
|
}
|
UserOptionsManager: fix options reset.
This is going to fix the bug, but it's not going far enough, READ_LATEST
needs to be bumped to READ_EXCLUSIVE.
The problem is that the options mananger cache serves dual purpose:
on option lookup it's a cache, while it also holds the modifications
made to options before saving. We need to require the options fetched
with READ_EXCLUSIVE before we are going to save them, and not discard
the options cache if they were already read with READ_EXCLUSIVE. Relying
on the callers to use correct query flags seems very prone to errors.
Before this was handled with User::getInstanceForUpdate. I wonder if we
should establish a similar pattern and remove the query flags from the
individual methods paramaters, but establish that UserOptionsLookup
service, responsible for reading-only, will use replica DB, while
UserOptionsManager will use master and do locking reads. The usage
pattern would then be - if you only need to read the options, use
lookup. If you have an intention of modifying the options - grab
and instance of the manager, and go into master by default. Thoughts?
Bug: T255842
Change-Id: I399ab0da8880320fd9d5f725ead8a62026cd7b7d
2020-06-24 22:33:19 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers \MediaWiki\User\UserOptionsManager::resetOptions
|
|
|
|
|
*/
|
|
|
|
|
public function testUserOptionsSaveAfterReset() {
|
|
|
|
|
$user = $this->getTestUser()->getUser();
|
|
|
|
|
$manager = $this->getManager();
|
|
|
|
|
$manager->setOption( $user, 'test_option', 'test_value' );
|
|
|
|
|
$manager->saveOptions( $user );
|
|
|
|
|
$manager->clearUserOptionsCache( $user );
|
|
|
|
|
$this->assertSame( 'test_value', $manager->getOption( $user, 'test_option' ) );
|
|
|
|
|
$manager->resetOptions( $user, RequestContext::getMain(), 'all' );
|
|
|
|
|
$this->assertNull( $manager->getOption( $user, 'test_option' ) );
|
|
|
|
|
$manager->saveOptions( $user );
|
|
|
|
|
$manager->clearUserOptionsCache( $user );
|
|
|
|
|
$this->assertNull( $manager->getOption( $user, 'test_option' ) );
|
|
|
|
|
}
|
2020-01-17 06:21:28 +00:00
|
|
|
}
|