664 lines
22 KiB
PHP
664 lines
22 KiB
PHP
<?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 ExtensionRegistry;
|
|
use IDBAccessObject;
|
|
use InvalidArgumentException;
|
|
use LanguageCode;
|
|
use LanguageConverter;
|
|
use MediaWiki\Config\ServiceOptions;
|
|
use MediaWiki\Context\IContextSource;
|
|
use MediaWiki\HookContainer\HookContainer;
|
|
use MediaWiki\HookContainer\HookRunner;
|
|
use MediaWiki\Languages\LanguageConverterFactory;
|
|
use MediaWiki\MainConfigNames;
|
|
use MediaWiki\MediaWikiServices;
|
|
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';
|
|
|
|
/**
|
|
* If the option was set globally, add a local override.
|
|
* @since 1.43
|
|
*/
|
|
public const GLOBAL_OVERRIDE = 'override';
|
|
|
|
/**
|
|
* If the option was set globally, update the global value.
|
|
* @since 1.43
|
|
*/
|
|
public const GLOBAL_UPDATE = 'update';
|
|
|
|
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;
|
|
|
|
/** @var UserOptionsCacheEntry[] */
|
|
private $cache = [];
|
|
|
|
/** @var UserOptionsStore[]|null */
|
|
private $stores;
|
|
|
|
/**
|
|
* @param ServiceOptions $options
|
|
* @param DefaultOptionsLookup $defaultOptionsLookup
|
|
* @param LanguageConverterFactory $languageConverterFactory
|
|
* @param IConnectionProvider $dbProvider
|
|
* @param LoggerInterface $logger
|
|
* @param HookContainer $hookContainer
|
|
* @param UserFactory $userFactory
|
|
* @param UserNameUtils $userNameUtils
|
|
* @param ObjectFactory $objectFactory
|
|
*/
|
|
public function __construct(
|
|
ServiceOptions $options,
|
|
DefaultOptionsLookup $defaultOptionsLookup,
|
|
LanguageConverterFactory $languageConverterFactory,
|
|
IConnectionProvider $dbProvider,
|
|
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;
|
|
$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
|
|
if ( $val === null ) {
|
|
$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.
|
|
*
|
|
* @note You need to call saveOptions() to actually write to the database.
|
|
*
|
|
* @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' ]
|
|
) {
|
|
wfDeprecated( __METHOD__, '1.43' );
|
|
$preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory();
|
|
$optionsToReset = $preferencesFactory->getOptionNamesForReset(
|
|
$this->userFactory->newFromUserIdentity( $user ), $context, $resetKinds );
|
|
$this->resetOptionsByName( $user, $optionsToReset );
|
|
}
|
|
|
|
/**
|
|
* 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 );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset all options that were set to a non-default value by the given user
|
|
*
|
|
* @note You need to call saveOptions() to actually write to the database.
|
|
*
|
|
* @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 {
|
|
wfDeprecated( __METHOD__, '1.43' );
|
|
$preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory();
|
|
return $preferencesFactory->listResetKinds();
|
|
}
|
|
|
|
/**
|
|
* @deprecated since 1.43 use PreferencesFactory::getResetKinds
|
|
*
|
|
* @param UserIdentity $userIdentity
|
|
* @param IContextSource $context
|
|
* @param array|null $options
|
|
* @return string[]
|
|
*/
|
|
public function getOptionKinds(
|
|
UserIdentity $userIdentity,
|
|
IContextSource $context,
|
|
$options = null
|
|
): array {
|
|
wfDeprecated( __METHOD__, '1.43' );
|
|
$user = $this->userFactory->newFromUserIdentity( $userIdentity );
|
|
$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;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 ) ] );
|
|
}
|
|
|
|
/**
|
|
* Fetches the options directly from the database with no caches.
|
|
*
|
|
* @param UserIdentity $user
|
|
* @param int $queryFlags a bit field composed of READ_XXX flags
|
|
* @return array
|
|
*/
|
|
private function loadOptionsFromStore(
|
|
UserIdentity $user,
|
|
int $queryFlags
|
|
): 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;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @param string $value
|
|
* @return mixed
|
|
*/
|
|
private function normalizeValueType( $value ) {
|
|
if ( $value === '0' ) {
|
|
$value = 0;
|
|
}
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
): 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();
|
|
$defaultOptions['variant'] = $variant;
|
|
$defaultOptions['language'] = $variant;
|
|
$cache->originalValues = $defaultOptions;
|
|
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;
|
|
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' );
|