wiki.techinc.nl/includes/user/Options/UserOptionsManager.php

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

663 lines
22 KiB
PHP
Raw Normal View History

<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
namespace MediaWiki\User\Options;
use IDBAccessObject;
use InvalidArgumentException;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Context\IContextSource;
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
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\HookContainer\HookRunner;
use MediaWiki\Language\LanguageCode;
use MediaWiki\Language\LanguageConverter;
use MediaWiki\Languages\LanguageConverterFactory;
use MediaWiki\MainConfigNames;
use MediaWiki\MediaWikiServices;
use MediaWiki\Registration\ExtensionRegistry;
use MediaWiki\User\UserFactory;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserNameUtils;
use MediaWiki\User\UserTimeCorrection;
use Psr\Log\LoggerInterface;
use Wikimedia\ObjectFactory\ObjectFactory;
use Wikimedia\Rdbms\IConnectionProvider;
/**
* A service class to control user options
* @since 1.35
*/
class UserOptionsManager extends UserOptionsLookup {
/**
* @internal For use by ServiceWiring
*/
public const CONSTRUCTOR_OPTIONS = [
MainConfigNames::HiddenPrefs,
MainConfigNames::LocalTZoffset,
];
/**
* @since 1.39.5, 1.40
*/
public const MAX_BYTES_OPTION_VALUE = 65530;
/**
* If the option was set globally, ignore the update.
* @since 1.43
*/
public const GLOBAL_IGNORE = 'ignore';
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
/**
* If the option was set globally, add a local override.
* @since 1.43
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
*/
public const GLOBAL_OVERRIDE = 'override';
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
/**
* If the option was set globally, update the global value.
* @since 1.43
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
*/
public const GLOBAL_UPDATE = 'update';
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
private const LOCAL_STORE_KEY = 'local';
private ServiceOptions $serviceOptions;
private DefaultOptionsLookup $defaultOptionsLookup;
private LanguageConverterFactory $languageConverterFactory;
private IConnectionProvider $dbProvider;
private UserFactory $userFactory;
private LoggerInterface $logger;
private HookRunner $hookRunner;
private UserNameUtils $userNameUtils;
private ObjectFactory $objectFactory;
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
/** @var UserOptionsCacheEntry[] */
private $cache = [];
/** @var UserOptionsStore[]|null */
private $stores;
/**
* @param ServiceOptions $options
* @param DefaultOptionsLookup $defaultOptionsLookup
* @param LanguageConverterFactory $languageConverterFactory
* @param IConnectionProvider $dbProvider
* @param LoggerInterface $logger
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
* @param HookContainer $hookContainer
* @param UserFactory $userFactory
* @param UserNameUtils $userNameUtils
* @param ObjectFactory $objectFactory
*/
public function __construct(
ServiceOptions $options,
DefaultOptionsLookup $defaultOptionsLookup,
LanguageConverterFactory $languageConverterFactory,
IConnectionProvider $dbProvider,
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
LoggerInterface $logger,
HookContainer $hookContainer,
UserFactory $userFactory,
UserNameUtils $userNameUtils,
ObjectFactory $objectFactory
) {
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
$this->serviceOptions = $options;
$this->defaultOptionsLookup = $defaultOptionsLookup;
$this->languageConverterFactory = $languageConverterFactory;
$this->dbProvider = $dbProvider;
$this->logger = $logger;
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
$this->hookRunner = new HookRunner( $hookContainer );
$this->userFactory = $userFactory;
$this->userNameUtils = $userNameUtils;
$this->objectFactory = $objectFactory;
}
/**
* @inheritDoc
*/
public function getDefaultOptions( ?UserIdentity $userIdentity = null ): array {
return $this->defaultOptionsLookup->getDefaultOptions( $userIdentity );
}
/**
* @inheritDoc
*/
public function getDefaultOption( string $opt, ?UserIdentity $userIdentity = null ) {
return $this->defaultOptionsLookup->getDefaultOption( $opt, $userIdentity );
}
/**
* @inheritDoc
*/
public function getOption(
UserIdentity $user,
string $oname,
$defaultOverride = null,
bool $ignoreHidden = false,
int $queryFlags = IDBAccessObject::READ_NORMAL
) {
# We want 'disabled' preferences to always behave as the default value for
# users, even if they have set the option explicitly in their settings (ie they
# set it, and then it was disabled removing their ability to change it). But
# we don't want to erase the preferences in the database in case the preference
# is re-enabled again. So don't touch $mOptions, just override the returned value
if ( !$ignoreHidden && in_array( $oname, $this->serviceOptions->get( MainConfigNames::HiddenPrefs ) ) ) {
return $this->defaultOptionsLookup->getDefaultOption( $oname, $user );
}
$options = $this->loadUserOptions( $user, $queryFlags );
if ( array_key_exists( $oname, $options ) ) {
return $options[$oname];
}
return $defaultOverride;
}
/**
* @inheritDoc
*/
public function getOptions(
UserIdentity $user,
int $flags = 0,
int $queryFlags = IDBAccessObject::READ_NORMAL
): array {
$options = $this->loadUserOptions( $user, $queryFlags );
# We want 'disabled' preferences to always behave as the default value for
# users, even if they have set the option explicitly in their settings (ie they
# set it, and then it was disabled removing their ability to change it). But
# we don't want to erase the preferences in the database in case the preference
# is re-enabled again. So don't touch $mOptions, just override the returned value
foreach ( $this->serviceOptions->get( MainConfigNames::HiddenPrefs ) as $pref ) {
$default = $this->defaultOptionsLookup->getDefaultOption( $pref, $user );
if ( $default !== null ) {
$options[$pref] = $default;
}
}
if ( $flags & self::EXCLUDE_DEFAULTS ) {
// NOTE: This intentionally ignores conditional defaults, so that `mw.user.options`
// work correctly for options with conditional defaults.
$defaultOptions = $this->defaultOptionsLookup->getDefaultOptions( null );
foreach ( $options as $option => $value ) {
if ( array_key_exists( $option, $defaultOptions )
&& self::isValueEqual( $value, $defaultOptions[$option] )
) {
unset( $options[$option] );
}
}
}
return $options;
}
public function isOptionGlobal( UserIdentity $user, string $key ) {
$this->getOptions( $user );
$source = $this->cache[ $this->getCacheKey( $user ) ]->sources[$key] ?? self::LOCAL_STORE_KEY;
return $source !== self::LOCAL_STORE_KEY;
}
/**
* Set the given option for a user.
*
* You need to call saveOptions() to actually write to the database.
*
* $val should be null or a string. Other types are accepted for B/C with legacy
* code but can result in surprising behavior and are discouraged. Values are always
* stored as strings in the database, so if you pass a non-string value, it will be
* eventually converted; but before the call to saveOptions(), getOption() will return
* the passed value from instance cache without any type conversion.
*
* A null value means resetting the option to its default value (removing the user_properties
* row). Passing in the same value as the default value fo the user has the same result.
* This behavior supports some level of type juggling - e.g. if the default value is 1,
* and you pass in '1', the option will be reset to its default value.
*
* When an option is reset to its default value, that means whenever the default value
* is changed in the site configuration, the user preference for this user will also change.
* There is no way to set a user preference to be the same as the default but avoid it
* changing when the default changes. You can instead use $wgConditionalUserOptions to
* split the default based on user registration date.
*
* If a global user option exists with the given name, the behaviour depends on the value
* of $global.
*
* @param UserIdentity $user
* @param string $oname The option to set
* @param mixed $val New value to set.
* @param string $global Since 1.43. What to do if the option was set
* globally using the GlobalPreferences extension. One of the
* self::GLOBAL_* constants:
* - GLOBAL_IGNORE: Do nothing. The option remains with its previous value.
* - GLOBAL_OVERRIDE: Add a local override.
* - GLOBAL_UPDATE: Update the option globally.
* The UI should typically ask for the user's consent before setting a global
* option.
*/
public function setOption( UserIdentity $user, string $oname, $val,
$global = self::GLOBAL_IGNORE
) {
// Explicitly NULL values should refer to defaults
$val ??= $this->defaultOptionsLookup->getDefaultOption( $oname, $user );
$userKey = $this->getCacheKey( $user );
$info = $this->cache[$userKey] ??= new UserOptionsCacheEntry;
$info->modifiedValues[$oname] = $val;
$info->globalUpdateActions[$oname] = $global;
}
/**
* Reset certain (or all) options to the site defaults
*
* The optional parameter determines which kinds of preferences will be reset.
* Supported values are everything that can be reported by getOptionKinds()
* and 'all', which forces a reset of *all* preferences and overrides everything else.
*
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
* @note You need to call saveOptions() to actually write to the database.
*
2024-05-27 06:15:03 +00:00
* @deprecated since 1.43 use resetOptionsByName() with PreferencesFactory::getOptionNamesForReset()
*
* @param UserIdentity $user
* @param IContextSource $context Context source used when $resetKinds does not contain 'all'.
* @param array|string $resetKinds Which kinds of preferences to reset.
* Defaults to [ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ]
*/
public function resetOptions(
UserIdentity $user,
IContextSource $context,
$resetKinds = [ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ]
) {
2024-05-27 06:15:03 +00:00
wfDeprecated( __METHOD__, '1.43' );
$preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory();
$optionsToReset = $preferencesFactory->getOptionNamesForReset(
$this->userFactory->newFromUserIdentity( $user ), $context, $resetKinds );
$this->resetOptionsByName( $user, $optionsToReset );
}
2024-05-27 06:15:03 +00:00
/**
* Reset a list of options to the site defaults
*
* @note You need to call saveOptions() to actually write to the database.
*
* @param UserIdentity $user
* @param string[] $optionNames
*/
public function resetOptionsByName(
UserIdentity $user,
array $optionNames
) {
foreach ( $optionNames as $name ) {
$this->setOption( $user, $name, null );
}
}
/**
2024-05-27 06:15:03 +00:00
* Reset all options that were set to a non-default value by the given user
*
2024-05-27 06:15:03 +00:00
* @note You need to call saveOptions() to actually write to the database.
*
2024-05-27 06:15:03 +00:00
* @param UserIdentity $user
*/
public function resetAllOptions( UserIdentity $user ) {
foreach ( $this->loadUserOptions( $user ) as $name => $value ) {
$this->setOption( $user, $name, null );
}
}
/**
* @deprecated since 1.43 use PreferencesFactory::listResetKinds()
*
* @return string[] Option kinds
*/
public function listOptionKinds(): array {
2024-05-27 06:15:03 +00:00
wfDeprecated( __METHOD__, '1.43' );
$preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory();
return $preferencesFactory->listResetKinds();
}
/**
2024-05-27 06:15:03 +00:00
* @deprecated since 1.43 use PreferencesFactory::getResetKinds
*
* @param UserIdentity $userIdentity
* @param IContextSource $context
2024-05-27 06:15:03 +00:00
* @param array|null $options
* @return string[]
*/
public function getOptionKinds(
UserIdentity $userIdentity,
IContextSource $context,
$options = null
): array {
2024-05-27 06:15:03 +00:00
wfDeprecated( __METHOD__, '1.43' );
$user = $this->userFactory->newFromUserIdentity( $userIdentity );
2024-05-27 06:15:03 +00:00
$preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory();
return $preferencesFactory->getResetKinds( $user, $context, $options );
}
/**
* Saves the non-default options for this user, as previously set e.g. via
* setOption(), in the database's "user_properties" (preferences) table.
*
* @since 1.38, this method was internal before that.
* @param UserIdentity $user
*/
public function saveOptions( UserIdentity $user ) {
$dbw = $this->dbProvider->getPrimaryDatabase();
$changed = $this->saveOptionsInternal( $user );
$legacyUser = $this->userFactory->newFromUserIdentity( $user );
// Before UserOptionsManager, User::saveSettings was used for user options
// saving. Some extensions might depend on UserSaveSettings hook being run
// when options are saved, so run this hook for legacy reasons.
// Once UserSaveSettings hook is deprecated and replaced with a different hook
// with more modern interface, extensions should use 'SaveUserOptions' hook.
$this->hookRunner->onUserSaveSettings( $legacyUser );
if ( $changed ) {
$dbw->onTransactionCommitOrIdle( static function () use ( $legacyUser ) {
$legacyUser->checkAndSetTouched();
}, __METHOD__ );
}
}
/**
* Saves the non-default options for this user, as previously set e.g. via
* setOption(), in the database's "user_properties" (preferences) table.
*
* @param UserIdentity $user
* @return bool true if options were changed and new options successfully saved.
* @internal only public for use in User::saveSettings
*/
public function saveOptionsInternal( UserIdentity $user ): bool {
if ( !$user->isRegistered() || $this->userNameUtils->isTemp( $user->getName() ) ) {
throw new InvalidArgumentException( __METHOD__ . ' was called on anon or temporary user' );
}
$userKey = $this->getCacheKey( $user );
$cache = $this->cache[$userKey] ?? new UserOptionsCacheEntry;
$modifiedOptions = $cache->modifiedValues;
// FIXME: should probably use READ_LATEST here
$originalOptions = $this->loadOriginalOptions( $user );
if ( !$this->hookRunner->onSaveUserOptions( $user, $modifiedOptions, $originalOptions ) ) {
return false;
}
$updatesByStore = [];
foreach ( $modifiedOptions as $key => $value ) {
// Don't store unchanged or default values
$defaultValue = $this->defaultOptionsLookup->getDefaultOption( $key, $user );
if ( $value === null || self::isValueEqual( $value, $defaultValue ) ) {
$valOrNull = null;
} else {
$valOrNull = (string)$value;
}
$source = $cache->sources[$key] ?? self::LOCAL_STORE_KEY;
if ( $source === self::LOCAL_STORE_KEY ) {
$updatesByStore[self::LOCAL_STORE_KEY][$key] = $valOrNull;
} else {
$updateAction = $cache->globalUpdateActions[$key] ?? self::GLOBAL_IGNORE;
if ( $updateAction === self::GLOBAL_UPDATE ) {
$updatesByStore[$source][$key] = $valOrNull;
} elseif ( $updateAction === self::GLOBAL_OVERRIDE ) {
$updatesByStore[self::LOCAL_STORE_KEY][$key] = $valOrNull;
$updatesByStore[self::LOCAL_STORE_KEY][$key . self::LOCAL_EXCEPTION_SUFFIX] = '1';
}
}
}
$changed = false;
$stores = $this->getStores();
foreach ( $updatesByStore as $source => $updates ) {
$changed = $stores[$source]->store( $user, $updates ) || $changed;
}
if ( !$changed ) {
return false;
}
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
// Clear the cache and the update queue
unset( $this->cache[$userKey] );
return true;
}
/**
* Loads user options either from cache or from the database.
*
* @note Query flags are ignored for anons, since they do not have any
* options stored in the database. If the UserIdentity was itself
* obtained from a replica and doesn't have ID set due to replication lag,
* it will be treated as anon regardless of the query flags passed here.
*
* @internal
*
* @param UserIdentity $user
* @param int $queryFlags
* @return array
*/
public function loadUserOptions(
UserIdentity $user,
int $queryFlags = IDBAccessObject::READ_NORMAL
): array {
$userKey = $this->getCacheKey( $user );
$originalOptions = $this->loadOriginalOptions( $user, $queryFlags );
$cache = $this->cache[$userKey] ?? null;
if ( $cache ) {
return array_merge( $originalOptions, $cache->modifiedValues );
} else {
return $originalOptions;
}
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
}
/**
* Clears cached user options.
* @internal To be used by User::clearInstanceCache
* @param UserIdentity $user
*/
public function clearUserOptionsCache( UserIdentity $user ) {
unset( $this->cache[ $this->getCacheKey( $user ) ] );
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
}
/**
* Fetches the options directly from the database with no caches.
*
* @param UserIdentity $user
* @param int $queryFlags a bit field composed of READ_XXX flags
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
* @return array
*/
private function loadOptionsFromStore(
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
UserIdentity $user,
int $queryFlags
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
): array {
$this->logger->debug( 'Loading options from database', [ 'user_id' => $user->getId() ] );
$mergedOptions = [];
$cache = $this->cache[ $this->getCacheKey( $user ) ] ??= new UserOptionsCacheEntry;
foreach ( $this->getStores() as $storeName => $store ) {
$options = $store->fetch( $user, $queryFlags );
foreach ( $options as $name => $value ) {
// Handle a local exception which is the default
if ( str_ends_with( $name, self::LOCAL_EXCEPTION_SUFFIX ) && $value ) {
$baseName = substr( $name, 0, -strlen( self::LOCAL_EXCEPTION_SUFFIX ) );
if ( !isset( $options[$baseName] ) ) {
// T368595: The source should always be set to local for local exceptions
$cache->sources[$baseName] = self::LOCAL_STORE_KEY;
unset( $mergedOptions[$baseName] );
}
}
// Handle a non-default option or non-default local exception
if ( !isset( $mergedOptions[$name] )
|| !empty( $options[$name . self::LOCAL_EXCEPTION_SUFFIX] )
) {
$cache->sources[$name] = $storeName;
$mergedOptions[$name] = $this->normalizeValueType( $value );
}
}
}
return $mergedOptions;
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
}
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
/**
* Convert '0' to 0. PHP's boolean conversion considers them both
* false, but e.g. JavaScript considers the former as true.
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
*
* @todo T54542 Somehow determine the desired type (string/int/bool)
* and convert all values here.
*
* @param string $value
* @return mixed
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
*/
private function normalizeValueType( $value ) {
if ( $value === '0' ) {
$value = 0;
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
}
return $value;
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
}
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
/**
* Loads the original user options from the database and applies various transforms,
* like timecorrection. Runs hooks.
*
* @param UserIdentity $user
* @param int $queryFlags
* @return array
*/
private function loadOriginalOptions(
UserIdentity $user,
int $queryFlags = IDBAccessObject::READ_NORMAL
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
): array {
$userKey = $this->getCacheKey( $user );
$defaultOptions = $this->defaultOptionsLookup->getDefaultOptions( $user );
$cache = $this->cache[$userKey] ??= new UserOptionsCacheEntry;
if ( !$user->isRegistered() || $this->userNameUtils->isTemp( $user->getName() ) ) {
// For unlogged-in users, load language/variant options from request.
// There's no need to do it for logged-in users: they can set preferences,
// and handling of page content is done by $pageLang->getPreferredVariant() and such,
// so don't override user's choice (especially when the user chooses site default).
$variant = $this->languageConverterFactory->getLanguageConverter()->getDefaultVariant();
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
$defaultOptions['variant'] = $variant;
$defaultOptions['language'] = $variant;
$cache->originalValues = $defaultOptions;
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
return $defaultOptions;
}
// In case options were already loaded from the database before and no options
// changes were saved to the database, we can use the cached original options.
if ( $cache->canUseCachedValues( $queryFlags )
&& $cache->originalValues !== null
) {
return $cache->originalValues;
}
$options = $this->loadOptionsFromStore( $user, $queryFlags ) + $defaultOptions;
// Replace deprecated language codes
$options['language'] = LanguageCode::replaceDeprecatedCodes( $options['language'] );
$options['variant'] = LanguageCode::replaceDeprecatedCodes( $options['variant'] );
foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
$variant = "variant-$langCode";
if ( isset( $options[$variant] ) ) {
$options[$variant] = LanguageCode::replaceDeprecatedCodes( $options[$variant] );
}
}
// Fix up timezone offset (Due to DST it can change from what was stored in the DB)
// ZoneInfo|offset|TimeZoneName
if ( isset( $options['timecorrection'] ) ) {
$options['timecorrection'] = ( new UserTimeCorrection(
$options['timecorrection'],
null,
$this->serviceOptions->get( MainConfigNames::LocalTZoffset )
) )->toString();
}
// Need to store what we have so far before the hook to prevent
// infinite recursion if the hook attempts to reload options
$cache->originalValues = $options;
$cache->recency = $queryFlags;
$this->hookRunner->onLoadUserOptions( $user, $options );
$cache->originalValues = $options;
Improvements to user preferences fetching/saving == Status quo == When saving user preferences, we want to lock the rows to avoid accidentally overriding a concurrent options update. So usually what extensions do is: $value = $mngr->getOption( 'option', ..., READ_LOCKING ); if ( $value !== 'new_value' ) { $mngr->setOption( 'option', 'new_value' ); $mngr->saveOptions() } Previously for extra caution we've ignored all caches in options manager if >= READ_LOCKING flags were passed. This resulted in re-reading all the options multiple times. At worst, 3 times: 1. If READ_NORMAL read was made for update - that's once, 2. On setOption, one more read, forcefully from primary 3. On saveOptions, one more read from primary, non-locking, to figure out which option keys need to be deleted. Also, READ_LOCKING was not used where it clearly had to be used, for example right before the update. This was trying to fix any kind of error on part of the manager clients, unsuccessfully so. == New approach == 1. Cache modified user options separately from originals and merge them on demand. This means when refetching originals with LOCKING we don't wipe out all modifications made to the cache with setOption. Extra bonus - we no longer need to load all options to set an option. 2. Split the originals cache into 2 layers - one for stuff that comes from DB directly, and one with applied normalizations and whatever hooks modify. This let's us avoid refetching DB options after we save them, but still let's the hooks execute on newly set options after they're saved. 3. Cache options with all query flags. This is a bit controversial, but ideally LOCKING flags will be applied on options fetch right before they are saved. We have to re-read options with LOCKING in saveOptions to avoid races, but if the caller did 'getOption( ..., LOCKING), setOption(), save()' we will not need to re-select with LOCKING again. Bug: T280220 Change-Id: Ibed2789f5260b725fd806b4470631aa30d814ce6
2021-06-11 18:48:19 +00:00
return $options;
}
/**
* Get a cache key for a user
* @param UserIdentity $user
* @return string
*/
private function getCacheKey( UserIdentity $user ): string {
if ( !$user->isRegistered() || $this->userNameUtils->isTemp( $user->getName() ) ) {
return 'anon';
} else {
return "u:{$user->getId()}";
}
}
/**
* Determines whether two values are sufficiently similar that the database
* does not need to be updated to reflect the change. This is basically the
* same as comparing the result of Database::addQuotes().
*
* @since 1.43
*
* @param mixed $a
* @param mixed $b
* @return bool
*/
public static function isValueEqual( $a, $b ) {
// null is only equal to another null (T355086)
if ( $a === null || $b === null ) {
return $a === $b;
}
if ( is_bool( $a ) ) {
$a = (int)$a;
}
if ( is_bool( $b ) ) {
$b = (int)$b;
}
return (string)$a === (string)$b;
}
/**
* Get the storage backends in descending order of priority
*
* @return UserOptionsStore[]
*/
private function getStores() {
if ( !$this->stores ) {
$stores = [
self::LOCAL_STORE_KEY => new LocalUserOptionsStore( $this->dbProvider )
];
$specs = ExtensionRegistry::getInstance()
->getAttribute( 'UserOptionsStoreProviders' );
foreach ( $specs as $name => $spec ) {
$store = $this->objectFactory->createObject( $spec );
if ( !$store instanceof UserOptionsStore ) {
throw new \RuntimeException( "Invalid type for extension store \"$name\"" );
}
$stores[$name] = $store;
}
// Query global providers first, preserve keys
$this->stores = array_reverse( $stores, true );
}
return $this->stores;
}
}
/** @deprecated class alias since 1.42 */
class_alias( UserOptionsManager::class, 'MediaWiki\\User\\UserOptionsManager' );