2020-01-17 06:21:28 +00:00
|
|
|
<?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;
|
|
|
|
|
|
|
|
|
|
use DBAccessObjectUtils;
|
|
|
|
|
use HTMLCheckMatrix;
|
|
|
|
|
use HTMLFormField;
|
|
|
|
|
use HTMLMultiSelectField;
|
|
|
|
|
use IContextSource;
|
2020-05-28 18:40:49 +00:00
|
|
|
use InvalidArgumentException;
|
2020-01-17 06:21:28 +00:00
|
|
|
use LanguageCode;
|
|
|
|
|
use MediaWiki\Config\ServiceOptions;
|
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;
|
2020-01-17 06:21:28 +00:00
|
|
|
use MediaWiki\Languages\LanguageConverterFactory;
|
|
|
|
|
use MediaWiki\MediaWikiServices;
|
|
|
|
|
use Psr\Log\LoggerInterface;
|
2021-10-25 19:56:47 +00:00
|
|
|
use Wikimedia\Rdbms\IDatabase;
|
2020-01-17 06:21:28 +00:00
|
|
|
use Wikimedia\Rdbms\ILoadBalancer;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A service class to control user options
|
|
|
|
|
* @since 1.35
|
|
|
|
|
*/
|
2020-05-28 18:40:49 +00:00
|
|
|
class UserOptionsManager extends UserOptionsLookup {
|
2020-01-17 06:21:28 +00:00
|
|
|
|
2019-10-25 08:07:22 +00:00
|
|
|
/**
|
|
|
|
|
* @internal For use by ServiceWiring
|
|
|
|
|
*/
|
2020-01-17 06:21:28 +00:00
|
|
|
public const CONSTRUCTOR_OPTIONS = [
|
2021-04-15 20:54:58 +00:00
|
|
|
'HiddenPrefs',
|
|
|
|
|
'LocalTZoffset',
|
2020-01-17 06:21:28 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/** @var ServiceOptions */
|
|
|
|
|
private $serviceOptions;
|
|
|
|
|
|
2020-05-11 19:19:13 +00:00
|
|
|
/** @var DefaultOptionsLookup */
|
|
|
|
|
private $defaultOptionsLookup;
|
2020-01-17 06:21:28 +00:00
|
|
|
|
|
|
|
|
/** @var LanguageConverterFactory */
|
|
|
|
|
private $languageConverterFactory;
|
|
|
|
|
|
|
|
|
|
/** @var ILoadBalancer */
|
|
|
|
|
private $loadBalancer;
|
|
|
|
|
|
2021-10-25 19:56:47 +00:00
|
|
|
/** @var UserFactory */
|
|
|
|
|
private $userFactory;
|
|
|
|
|
|
2020-01-17 06:21:28 +00:00
|
|
|
/** @var LoggerInterface */
|
|
|
|
|
private $logger;
|
|
|
|
|
|
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
|
|
|
/** @var array options modified withing this request */
|
|
|
|
|
private $modifiedOptions = [];
|
2020-01-17 06:21:28 +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
|
|
|
/**
|
|
|
|
|
* @var array Cached original user options with all the adjustments
|
|
|
|
|
* like time correction and hook changes applied.
|
|
|
|
|
* Ready to be returned.
|
|
|
|
|
*/
|
2020-01-17 06:21:28 +00:00
|
|
|
private $originalOptionsCache = [];
|
|
|
|
|
|
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
|
|
|
/**
|
|
|
|
|
* @var array Cached original user options as fetched from database,
|
|
|
|
|
* no adjustments applied.
|
|
|
|
|
*/
|
|
|
|
|
private $optionsFromDb = [];
|
|
|
|
|
|
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 HookRunner */
|
|
|
|
|
private $hookRunner;
|
|
|
|
|
|
2020-05-28 18:40:49 +00:00
|
|
|
/** @var array Query flags used to retrieve options from database */
|
|
|
|
|
private $queryFlagsUsedForCaching = [];
|
|
|
|
|
|
2020-01-17 06:21:28 +00:00
|
|
|
/**
|
|
|
|
|
* @param ServiceOptions $options
|
2020-05-11 19:19:13 +00:00
|
|
|
* @param DefaultOptionsLookup $defaultOptionsLookup
|
2020-01-17 06:21:28 +00:00
|
|
|
* @param LanguageConverterFactory $languageConverterFactory
|
|
|
|
|
* @param ILoadBalancer $loadBalancer
|
|
|
|
|
* @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
|
2021-10-25 19:56:47 +00:00
|
|
|
* @param UserFactory $userFactory
|
2020-01-17 06:21:28 +00:00
|
|
|
*/
|
|
|
|
|
public function __construct(
|
|
|
|
|
ServiceOptions $options,
|
2020-05-11 19:19:13 +00:00
|
|
|
DefaultOptionsLookup $defaultOptionsLookup,
|
2020-01-17 06:21:28 +00:00
|
|
|
LanguageConverterFactory $languageConverterFactory,
|
|
|
|
|
ILoadBalancer $loadBalancer,
|
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,
|
2021-10-25 19:56:47 +00:00
|
|
|
HookContainer $hookContainer,
|
|
|
|
|
UserFactory $userFactory
|
2020-01-17 06:21:28 +00:00
|
|
|
) {
|
|
|
|
|
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
|
|
|
|
|
$this->serviceOptions = $options;
|
2020-05-11 19:19:13 +00:00
|
|
|
$this->defaultOptionsLookup = $defaultOptionsLookup;
|
2020-01-17 06:21:28 +00:00
|
|
|
$this->languageConverterFactory = $languageConverterFactory;
|
|
|
|
|
$this->loadBalancer = $loadBalancer;
|
|
|
|
|
$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 );
|
2021-10-25 19:56:47 +00:00
|
|
|
$this->userFactory = $userFactory;
|
2020-01-17 06:21:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @inheritDoc
|
|
|
|
|
*/
|
|
|
|
|
public function getDefaultOptions(): array {
|
2020-05-11 19:19:13 +00:00
|
|
|
return $this->defaultOptionsLookup->getDefaultOptions();
|
2020-01-17 06:21:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @inheritDoc
|
|
|
|
|
*/
|
|
|
|
|
public function getOption(
|
|
|
|
|
UserIdentity $user,
|
|
|
|
|
string $oname,
|
|
|
|
|
$defaultOverride = null,
|
2020-05-28 18:40:49 +00:00
|
|
|
bool $ignoreHidden = false,
|
|
|
|
|
int $queryFlags = self::READ_NORMAL
|
2020-01-17 06:21:28 +00:00
|
|
|
) {
|
|
|
|
|
# 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( 'HiddenPrefs' ) ) ) {
|
2020-05-11 19:19:13 +00:00
|
|
|
return $this->defaultOptionsLookup->getDefaultOption( $oname );
|
2020-01-17 06:21:28 +00:00
|
|
|
}
|
|
|
|
|
|
2020-05-28 18:40:49 +00:00
|
|
|
$options = $this->loadUserOptions( $user, $queryFlags );
|
2020-01-17 06:21:28 +00:00
|
|
|
if ( array_key_exists( $oname, $options ) ) {
|
|
|
|
|
return $options[$oname];
|
|
|
|
|
}
|
|
|
|
|
return $defaultOverride;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @inheritDoc
|
|
|
|
|
*/
|
2020-05-28 18:40:49 +00:00
|
|
|
public function getOptions(
|
|
|
|
|
UserIdentity $user,
|
|
|
|
|
int $flags = 0,
|
|
|
|
|
int $queryFlags = self::READ_NORMAL
|
|
|
|
|
): array {
|
|
|
|
|
$options = $this->loadUserOptions( $user, $queryFlags );
|
2020-01-17 06:21:28 +00:00
|
|
|
|
|
|
|
|
# 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( 'HiddenPrefs' ) as $pref ) {
|
2020-05-11 19:19:13 +00:00
|
|
|
$default = $this->defaultOptionsLookup->getDefaultOption( $pref );
|
2020-01-17 06:21:28 +00:00
|
|
|
if ( $default !== null ) {
|
|
|
|
|
$options[$pref] = $default;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $flags & self::EXCLUDE_DEFAULTS ) {
|
2021-09-29 00:10:56 +00:00
|
|
|
$defaultOptions = $this->defaultOptionsLookup->getDefaultOptions();
|
|
|
|
|
foreach ( $options as $option => $value ) {
|
2021-11-01 19:41:40 +00:00
|
|
|
if ( array_key_exists( $option, $defaultOptions )
|
2021-09-29 00:10:56 +00:00
|
|
|
&& $this->isValueEqual( $value, $defaultOptions[$option] )
|
|
|
|
|
) {
|
|
|
|
|
unset( $options[$option] );
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-01-17 06:21:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $options;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set the given option for a user.
|
|
|
|
|
*
|
|
|
|
|
* You need to call saveOptions() to actually write to the database.
|
|
|
|
|
*
|
|
|
|
|
* @param UserIdentity $user
|
|
|
|
|
* @param string $oname The option to set
|
|
|
|
|
* @param mixed $val New value to set
|
|
|
|
|
*/
|
|
|
|
|
public function setOption( UserIdentity $user, string $oname, $val ) {
|
|
|
|
|
// Explicitly NULL values should refer to defaults
|
|
|
|
|
if ( $val === null ) {
|
2020-05-11 19:19:13 +00:00
|
|
|
$val = $this->defaultOptionsLookup->getDefaultOption( $oname );
|
2020-01-17 06:21:28 +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
|
|
|
$this->modifiedOptions[$this->getCacheKey( $user )][$oname] = $val;
|
2020-01-17 06:21:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*
|
2020-01-17 06:21:28 +00:00
|
|
|
* @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' ]
|
|
|
|
|
) {
|
2021-07-13 01:51:46 +00:00
|
|
|
$oldOptions = $this->loadUserOptions( $user, self::READ_LATEST );
|
2020-05-11 19:19:13 +00:00
|
|
|
$defaultOptions = $this->defaultOptionsLookup->getDefaultOptions();
|
2020-01-17 06:21:28 +00:00
|
|
|
|
|
|
|
|
if ( !is_array( $resetKinds ) ) {
|
|
|
|
|
$resetKinds = [ $resetKinds ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( in_array( 'all', $resetKinds ) ) {
|
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
|
|
|
$newOptions = $defaultOptions + array_fill_keys( array_keys( $oldOptions ), null );
|
2020-01-17 06:21:28 +00:00
|
|
|
} else {
|
|
|
|
|
$optionKinds = $this->getOptionKinds( $user, $context );
|
|
|
|
|
$resetKinds = array_intersect( $resetKinds, $this->listOptionKinds() );
|
|
|
|
|
$newOptions = [];
|
|
|
|
|
|
|
|
|
|
// Use default values for the options that should be deleted, and
|
|
|
|
|
// copy old values for the ones that shouldn't.
|
|
|
|
|
foreach ( $oldOptions as $key => $value ) {
|
|
|
|
|
if ( in_array( $optionKinds[$key], $resetKinds ) ) {
|
|
|
|
|
if ( array_key_exists( $key, $defaultOptions ) ) {
|
|
|
|
|
$newOptions[$key] = $defaultOptions[$key];
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$newOptions[$key] = $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
|
|
|
$this->modifiedOptions[$this->getCacheKey( $user )] = $newOptions;
|
2020-01-17 06:21:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return a list of the types of user options currently returned by
|
|
|
|
|
* UserOptionsManager::getOptionKinds().
|
|
|
|
|
*
|
|
|
|
|
* Currently, the option kinds are:
|
|
|
|
|
* - 'registered' - preferences which are registered in core MediaWiki or
|
|
|
|
|
* by extensions using the UserGetDefaultOptions hook.
|
|
|
|
|
* - 'registered-multiselect' - as above, using the 'multiselect' type.
|
|
|
|
|
* - 'registered-checkmatrix' - as above, using the 'checkmatrix' type.
|
|
|
|
|
* - 'userjs' - preferences with names starting with 'userjs-', intended to
|
|
|
|
|
* be used by user scripts.
|
2021-04-12 14:45:26 +00:00
|
|
|
* - 'special' - "preferences" that are not accessible via
|
|
|
|
|
* UserOptionsLookup::getOptions or UserOptionsManager::setOptions.
|
2020-01-17 06:21:28 +00:00
|
|
|
* - 'unused' - preferences about which MediaWiki doesn't know anything.
|
|
|
|
|
* These are usually legacy options, removed in newer versions.
|
|
|
|
|
*
|
|
|
|
|
* The API (and possibly others) use this function to determine the possible
|
|
|
|
|
* option types for validation purposes, so make sure to update this when a
|
|
|
|
|
* new option kind is added.
|
|
|
|
|
*
|
|
|
|
|
* @see getOptionKinds
|
2020-10-28 10:01:33 +00:00
|
|
|
* @return string[] Option kinds
|
2020-01-17 06:21:28 +00:00
|
|
|
*/
|
|
|
|
|
public function listOptionKinds(): array {
|
|
|
|
|
return [
|
|
|
|
|
'registered',
|
|
|
|
|
'registered-multiselect',
|
|
|
|
|
'registered-checkmatrix',
|
|
|
|
|
'userjs',
|
|
|
|
|
'special',
|
|
|
|
|
'unused'
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return an associative array mapping preferences keys to the kind of a preference they're
|
|
|
|
|
* used for. Different kinds are handled differently when setting or reading preferences.
|
|
|
|
|
*
|
|
|
|
|
* See UserOptionsManager::listOptionKinds for the list of valid option types that can be provided.
|
|
|
|
|
*
|
|
|
|
|
* @see UserOptionsManager::listOptionKinds
|
2020-04-13 01:21:01 +00:00
|
|
|
* @param UserIdentity $userIdentity
|
2020-01-17 06:21:28 +00:00
|
|
|
* @param IContextSource $context
|
|
|
|
|
* @param array|null $options Assoc. array with options keys to check as keys.
|
|
|
|
|
* Defaults user options.
|
2020-10-28 10:01:33 +00:00
|
|
|
* @return string[] The key => kind mapping data
|
2020-01-17 06:21:28 +00:00
|
|
|
*/
|
|
|
|
|
public function getOptionKinds(
|
2020-04-13 01:21:01 +00:00
|
|
|
UserIdentity $userIdentity,
|
2020-01-17 06:21:28 +00:00
|
|
|
IContextSource $context,
|
|
|
|
|
$options = null
|
|
|
|
|
): array {
|
|
|
|
|
if ( $options === null ) {
|
2020-04-13 01:21:01 +00:00
|
|
|
$options = $this->loadUserOptions( $userIdentity );
|
2020-01-17 06:21:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: injecting the preferences factory creates a cyclic dependency between
|
|
|
|
|
// PreferencesFactory and UserOptionsManager. See T250822
|
|
|
|
|
$preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory();
|
2021-10-25 19:56:47 +00:00
|
|
|
$user = $this->userFactory->newFromUserIdentity( $userIdentity );
|
2020-04-13 01:21:01 +00:00
|
|
|
$prefs = $preferencesFactory->getFormDescriptor( $user, $context );
|
2020-01-17 06:21:28 +00:00
|
|
|
$mapping = [];
|
|
|
|
|
|
|
|
|
|
// Pull out the "special" options, so they don't get converted as
|
|
|
|
|
// multiselect or checkmatrix.
|
|
|
|
|
$specialOptions = array_fill_keys( $preferencesFactory->getSaveBlacklist(), true );
|
|
|
|
|
foreach ( $specialOptions as $name => $value ) {
|
|
|
|
|
unset( $prefs[$name] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Multiselect and checkmatrix options are stored in the database with
|
|
|
|
|
// one key per option, each having a boolean value. Extract those keys.
|
|
|
|
|
$multiselectOptions = [];
|
|
|
|
|
foreach ( $prefs as $name => $info ) {
|
|
|
|
|
if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
|
|
|
|
|
( isset( $info['class'] ) && $info['class'] == HTMLMultiSelectField::class )
|
|
|
|
|
) {
|
2021-06-03 00:01:58 +00:00
|
|
|
$opts = HTMLFormField::flattenOptions( $info['options'] ?? $info['options-messages'] );
|
2020-01-17 06:21:28 +00:00
|
|
|
$prefix = $info['prefix'] ?? $name;
|
|
|
|
|
|
|
|
|
|
foreach ( $opts as $value ) {
|
|
|
|
|
$multiselectOptions["$prefix$value"] = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unset( $prefs[$name] );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$checkmatrixOptions = [];
|
|
|
|
|
foreach ( $prefs as $name => $info ) {
|
|
|
|
|
if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
|
|
|
|
|
( isset( $info['class'] ) && $info['class'] == HTMLCheckMatrix::class )
|
|
|
|
|
) {
|
|
|
|
|
$columns = HTMLFormField::flattenOptions( $info['columns'] );
|
|
|
|
|
$rows = HTMLFormField::flattenOptions( $info['rows'] );
|
|
|
|
|
$prefix = $info['prefix'] ?? $name;
|
|
|
|
|
|
|
|
|
|
foreach ( $columns as $column ) {
|
|
|
|
|
foreach ( $rows as $row ) {
|
|
|
|
|
$checkmatrixOptions["$prefix$column-$row"] = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unset( $prefs[$name] );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// $value is ignored
|
|
|
|
|
foreach ( $options as $key => $value ) {
|
|
|
|
|
if ( isset( $prefs[$key] ) ) {
|
|
|
|
|
$mapping[$key] = 'registered';
|
|
|
|
|
} elseif ( isset( $multiselectOptions[$key] ) ) {
|
|
|
|
|
$mapping[$key] = 'registered-multiselect';
|
|
|
|
|
} elseif ( isset( $checkmatrixOptions[$key] ) ) {
|
|
|
|
|
$mapping[$key] = 'registered-checkmatrix';
|
|
|
|
|
} elseif ( isset( $specialOptions[$key] ) ) {
|
|
|
|
|
$mapping[$key] = 'special';
|
|
|
|
|
} elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
|
|
|
|
|
$mapping[$key] = 'userjs';
|
|
|
|
|
} else {
|
|
|
|
|
$mapping[$key] = 'unused';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $mapping;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Saves the non-default options for this user, as previously set e.g. via
|
|
|
|
|
* setOption(), in the database's "user_properties" (preferences) table.
|
2021-10-25 19:56:47 +00:00
|
|
|
*
|
|
|
|
|
* @since 1.38, this method was internal before that.
|
2020-01-17 06:21:28 +00:00
|
|
|
* @param UserIdentity $user
|
|
|
|
|
*/
|
|
|
|
|
public function saveOptions( UserIdentity $user ) {
|
2021-10-25 19:56:47 +00:00
|
|
|
$dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
|
|
|
|
|
$changed = $this->saveOptionsInternal( $user, $dbw );
|
|
|
|
|
$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
|
|
|
|
|
* @param IDatabase $dbw
|
|
|
|
|
* @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, IDatabase $dbw ): bool {
|
2020-05-28 18:40:49 +00:00
|
|
|
if ( !$user->isRegistered() ) {
|
|
|
|
|
throw new InvalidArgumentException( __METHOD__ . ' was called on anon user' );
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-26 18:58:30 +00:00
|
|
|
$userKey = $this->getCacheKey( $user );
|
2021-07-13 21:05:10 +00:00
|
|
|
$modifiedOptions = $this->modifiedOptions[$userKey] ?? [];
|
2021-09-23 00:37:21 +00:00
|
|
|
$originalOptions = $this->loadOriginalOptions( $user );
|
2021-07-26 17:25:12 +00:00
|
|
|
if ( !$this->hookRunner->onSaveUserOptions( $user, $modifiedOptions, $originalOptions ) ) {
|
2021-10-25 19:56:47 +00:00
|
|
|
return false;
|
2021-07-13 21:05:10 +00:00
|
|
|
}
|
2020-01-17 06:21:28 +00:00
|
|
|
|
2021-09-23 00:37:21 +00:00
|
|
|
$rowsToInsert = [];
|
|
|
|
|
$keysToDelete = [];
|
|
|
|
|
foreach ( $modifiedOptions as $key => $value ) {
|
2021-06-22 02:42:18 +00:00
|
|
|
// Don't store unchanged or default values
|
|
|
|
|
$defaultValue = $this->defaultOptionsLookup->getDefaultOption( $key );
|
|
|
|
|
$oldValue = $this->optionsFromDb[$userKey][$key] ?? null;
|
2021-09-23 00:37:21 +00:00
|
|
|
if ( $value === null || $this->isValueEqual( $value, $defaultValue ) ) {
|
|
|
|
|
$keysToDelete[] = $key;
|
|
|
|
|
} elseif ( !$this->isValueEqual( $value, $oldValue ) ) {
|
|
|
|
|
// Update by deleting and reinserting
|
|
|
|
|
$rowsToInsert[] = [
|
|
|
|
|
'up_user' => $user->getId(),
|
2020-01-17 06:21:28 +00:00
|
|
|
'up_property' => $key,
|
|
|
|
|
'up_value' => $value,
|
|
|
|
|
];
|
2021-09-23 00:37:21 +00:00
|
|
|
if ( $oldValue !== null ) {
|
|
|
|
|
$keysToDelete[] = $key;
|
2021-06-22 02:42:18 +00:00
|
|
|
}
|
2020-01-17 06:21:28 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-15 01:05:22 +00:00
|
|
|
if ( !count( $keysToDelete ) && !count( $rowsToInsert ) ) {
|
2021-06-22 02:42:18 +00:00
|
|
|
// Nothing to do
|
2021-10-25 19:56:47 +00:00
|
|
|
return false;
|
2020-01-17 06:21:28 +00:00
|
|
|
}
|
|
|
|
|
|
2021-06-22 02:42:18 +00:00
|
|
|
// Do the DELETE
|
2021-07-15 01:05:22 +00:00
|
|
|
if ( $keysToDelete ) {
|
|
|
|
|
$dbw->delete(
|
|
|
|
|
'user_properties',
|
|
|
|
|
[
|
2021-09-23 00:37:21 +00:00
|
|
|
'up_user' => $user->getId(),
|
2021-07-15 01:05:22 +00:00
|
|
|
'up_property' => $keysToDelete
|
|
|
|
|
],
|
|
|
|
|
__METHOD__
|
|
|
|
|
);
|
|
|
|
|
}
|
2021-06-22 02:42:18 +00:00
|
|
|
if ( $rowsToInsert ) {
|
|
|
|
|
// Insert the new preference rows
|
|
|
|
|
$dbw->insert( 'user_properties', $rowsToInsert, __METHOD__, [ '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
|
|
|
|
|
|
|
|
// It's pretty cheap to recalculate new original later
|
|
|
|
|
// to apply whatever adjustments we apply when fetching from DB
|
|
|
|
|
// and re-merge with the defaults.
|
|
|
|
|
unset( $this->originalOptionsCache[$userKey] );
|
|
|
|
|
// And nothing is modified anymore
|
|
|
|
|
unset( $this->modifiedOptions[$userKey] );
|
2021-10-25 19:56:47 +00:00
|
|
|
return true;
|
2020-01-17 06:21:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2020-05-28 18:40:49 +00:00
|
|
|
* 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.
|
|
|
|
|
*
|
2020-01-17 06:21:28 +00:00
|
|
|
* @param UserIdentity $user
|
|
|
|
|
* @param 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
|
|
|
* @param array|null $data associative array of non-default options.
|
2020-01-17 06:21:28 +00:00
|
|
|
* @return array
|
|
|
|
|
* @internal To be called by User loading code to provide the $data
|
|
|
|
|
*/
|
|
|
|
|
public function loadUserOptions(
|
|
|
|
|
UserIdentity $user,
|
|
|
|
|
int $queryFlags = self::READ_NORMAL,
|
|
|
|
|
array $data = null
|
|
|
|
|
): array {
|
|
|
|
|
$userKey = $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
|
|
|
$originalOptions = $this->loadOriginalOptions( $user, $queryFlags, $data );
|
|
|
|
|
return array_merge( $originalOptions, $this->modifiedOptions[$userKey] ?? [] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clears cached user options.
|
|
|
|
|
* @internal To be used by User::clearInstanceCache
|
|
|
|
|
* @param UserIdentity $user
|
|
|
|
|
*/
|
|
|
|
|
public function clearUserOptionsCache( UserIdentity $user ) {
|
|
|
|
|
$cacheKey = $this->getCacheKey( $user );
|
|
|
|
|
unset( $this->modifiedOptions[$cacheKey] );
|
|
|
|
|
unset( $this->optionsFromDb[$cacheKey] );
|
|
|
|
|
unset( $this->originalOptionsCache[$cacheKey] );
|
|
|
|
|
unset( $this->queryFlagsUsedForCaching[$cacheKey] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fetches the options directly from the database with no caches.
|
|
|
|
|
*
|
|
|
|
|
* @param UserIdentity $user
|
|
|
|
|
* @param int $queryFlags
|
|
|
|
|
* @param array|null $prefetchedOptions
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
private function loadOptionsFromDb(
|
|
|
|
|
UserIdentity $user,
|
|
|
|
|
int $queryFlags,
|
|
|
|
|
array $prefetchedOptions = null
|
|
|
|
|
): array {
|
|
|
|
|
if ( $prefetchedOptions === null ) {
|
|
|
|
|
$this->logger->debug( 'Loading options from database', [ 'user_id' => $user->getId() ] );
|
|
|
|
|
[ $dbr, $options ] = $this->getDBAndOptionsForQueryFlags( $queryFlags );
|
|
|
|
|
$res = $dbr->select(
|
|
|
|
|
'user_properties',
|
|
|
|
|
[ 'up_property', 'up_value' ],
|
|
|
|
|
[ 'up_user' => $user->getId() ],
|
|
|
|
|
__METHOD__,
|
|
|
|
|
$options
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
$res = [];
|
|
|
|
|
foreach ( $prefetchedOptions as $name => $value ) {
|
|
|
|
|
$res[] = [
|
|
|
|
|
'up_property' => $name,
|
|
|
|
|
'up_value' => $value,
|
|
|
|
|
];
|
|
|
|
|
}
|
2020-01-17 06:21:28 +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
|
|
|
return $this->setOptionsFromDb( $user, $queryFlags, $res );
|
|
|
|
|
}
|
2020-01-17 06:21:28 +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
|
|
|
/**
|
|
|
|
|
* Builds associative options array from rows fetched from DB.
|
|
|
|
|
*
|
|
|
|
|
* @param UserIdentity $user
|
|
|
|
|
* @param int $queryFlags
|
|
|
|
|
* @param iterable<object|array> $rows
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
private function setOptionsFromDb(
|
|
|
|
|
UserIdentity $user,
|
|
|
|
|
int $queryFlags,
|
|
|
|
|
iterable $rows
|
|
|
|
|
): array {
|
|
|
|
|
$userKey = $this->getCacheKey( $user );
|
|
|
|
|
$options = [];
|
|
|
|
|
foreach ( $rows as $row ) {
|
|
|
|
|
$row = (object)$row;
|
|
|
|
|
// Convert '0' to 0. PHP's boolean conversion considers them both
|
|
|
|
|
// false, but e.g. JavaScript considers the former as true.
|
|
|
|
|
// @todo: T54542 Somehow determine the desired type (string/int/bool)
|
|
|
|
|
// and convert all values here.
|
|
|
|
|
if ( $row->up_value === '0' ) {
|
|
|
|
|
$row->up_value = 0;
|
|
|
|
|
}
|
|
|
|
|
$options[$row->up_property] = $row->up_value;
|
|
|
|
|
}
|
|
|
|
|
$this->optionsFromDb[$userKey] = $options;
|
|
|
|
|
$this->queryFlagsUsedForCaching[$userKey] = $queryFlags;
|
|
|
|
|
return $options;
|
|
|
|
|
}
|
2020-01-17 06:21:28 +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
|
|
|
|
|
* @param array|null $data associative array of non-default options
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
private function loadOriginalOptions(
|
|
|
|
|
UserIdentity $user,
|
|
|
|
|
int $queryFlags = self::READ_NORMAL,
|
|
|
|
|
array $data = null
|
|
|
|
|
): array {
|
|
|
|
|
$userKey = $this->getCacheKey( $user );
|
|
|
|
|
$defaultOptions = $this->defaultOptionsLookup->getDefaultOptions();
|
2020-01-17 06:21:28 +00:00
|
|
|
if ( !$user->isRegistered() ) {
|
|
|
|
|
// 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;
|
|
|
|
|
$this->originalOptionsCache[$userKey] = $defaultOptions;
|
|
|
|
|
return $defaultOptions;
|
2020-01-17 06:21:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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.
|
2020-05-28 18:40:49 +00:00
|
|
|
if ( $this->canUseCachedValues( $user, $queryFlags )
|
|
|
|
|
&& isset( $this->originalOptionsCache[$userKey] )
|
|
|
|
|
) {
|
2020-05-26 18:58:30 +00:00
|
|
|
return $this->originalOptionsCache[$userKey];
|
2020-01-17 06:21:28 +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
|
|
|
$options = $this->loadOptionsFromDb( $user, $queryFlags, $data ) + $defaultOptions;
|
2020-01-17 06:21:28 +00:00
|
|
|
// Replace deprecated language codes
|
|
|
|
|
$options['language'] = LanguageCode::replaceDeprecatedCodes( $options['language'] );
|
2021-04-15 20:54:58 +00:00
|
|
|
// 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( 'LocalTZoffset' )
|
|
|
|
|
) )->toString();
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-26 18:58:30 +00:00
|
|
|
// Need to store what we have so far before the hook to prevent
|
|
|
|
|
// infinite recursion if the hook attempts to reload options
|
|
|
|
|
$this->originalOptionsCache[$userKey] = $options;
|
2020-05-28 18:40:49 +00:00
|
|
|
$this->queryFlagsUsedForCaching[$userKey] = $queryFlags;
|
2021-07-13 21:05:10 +00:00
|
|
|
$this->hookRunner->onLoadUserOptions( $user, $options );
|
2020-05-26 18:58:30 +00:00
|
|
|
$this->originalOptionsCache[$userKey] = $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;
|
2020-01-17 06:21:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2020-12-15 14:16:22 +00:00
|
|
|
* Gets a key for various caches.
|
2020-01-17 06:21:28 +00:00
|
|
|
* @param UserIdentity $user
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
private function getCacheKey( UserIdentity $user ): string {
|
2020-12-15 14:16:22 +00:00
|
|
|
return $user->isRegistered() ? "u:{$user->getId()}" : 'anon';
|
2020-01-17 06:21:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @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 [ IDatabase $db, array $options ]
|
2020-01-17 06:21:28 +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
|
|
|
private function getDBAndOptionsForQueryFlags( $queryFlags ): array {
|
|
|
|
|
list( $mode, $options ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
|
|
|
|
|
return [ $this->loadBalancer->getConnectionRef( $mode, [] ), $options ];
|
2020-01-17 06:21:28 +00:00
|
|
|
}
|
2020-05-28 18:40:49 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determines if it's ok to use cached options values for a given user and query flags
|
|
|
|
|
* @param UserIdentity $user
|
|
|
|
|
* @param int $queryFlags
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
2021-07-22 03:11:47 +00:00
|
|
|
private function canUseCachedValues( UserIdentity $user, int $queryFlags ): bool {
|
2020-05-28 18:40:49 +00:00
|
|
|
if ( !$user->isRegistered() ) {
|
|
|
|
|
// Anon users don't have options stored in the database,
|
|
|
|
|
// so $queryFlags are ignored.
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
$userKey = $this->getCacheKey( $user );
|
|
|
|
|
$queryFlagsUsed = $this->queryFlagsUsedForCaching[$userKey] ?? self::READ_NONE;
|
|
|
|
|
return $queryFlagsUsed >= $queryFlags;
|
|
|
|
|
}
|
2021-06-22 02:42:18 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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().
|
|
|
|
|
*
|
|
|
|
|
* @param mixed $a
|
|
|
|
|
* @param mixed $b
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
private function isValueEqual( $a, $b ) {
|
|
|
|
|
if ( is_bool( $a ) ) {
|
|
|
|
|
$a = (int)$a;
|
|
|
|
|
}
|
|
|
|
|
if ( is_bool( $b ) ) {
|
|
|
|
|
$b = (int)$b;
|
|
|
|
|
}
|
|
|
|
|
return (string)$a === (string)$b;
|
|
|
|
|
}
|
2020-01-17 06:21:28 +00:00
|
|
|
}
|