MediaWiki  master
UserOptionsManager.php
Go to the documentation of this file.
1 <?php
21 namespace MediaWiki\User;
22 
24 use HTMLCheckMatrix;
25 use HTMLFormField;
27 use IContextSource;
28 use IDBAccessObject;
29 use LanguageCode;
35 use Psr\Log\LoggerInterface;
36 use User;
39 
45 
46  public const CONSTRUCTOR_OPTIONS = [
47  'HiddenPrefs'
48  ];
49 
51  private $serviceOptions;
52 
55 
58 
60  private $loadBalancer;
61 
63  private $logger;
64 
66  private $optionsCache = [];
67 
69  private $originalOptionsCache = [];
70 
72  private $hookRunner;
73 
82  public function __construct(
83  ServiceOptions $options,
87  LoggerInterface $logger,
88  HookContainer $hookContainer
89  ) {
90  $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
91  $this->serviceOptions = $options;
92  $this->defaultOptionsLookup = $defaultOptionsLookup;
93  $this->languageConverterFactory = $languageConverterFactory;
94  $this->loadBalancer = $loadBalancer;
95  $this->logger = $logger;
96  $this->hookRunner = new HookRunner( $hookContainer );
97  }
98 
102  public function getDefaultOptions(): array {
103  return $this->defaultOptionsLookup->getDefaultOptions();
104  }
105 
109  public function getDefaultOption( string $opt ) {
110  return $this->defaultOptionsLookup->getDefaultOption( $opt );
111  }
112 
116  public function getOption(
117  UserIdentity $user,
118  string $oname,
119  $defaultOverride = null,
120  bool $ignoreHidden = false
121  ) {
122  # We want 'disabled' preferences to always behave as the default value for
123  # users, even if they have set the option explicitly in their settings (ie they
124  # set it, and then it was disabled removing their ability to change it). But
125  # we don't want to erase the preferences in the database in case the preference
126  # is re-enabled again. So don't touch $mOptions, just override the returned value
127  if ( !$ignoreHidden && in_array( $oname, $this->serviceOptions->get( 'HiddenPrefs' ) ) ) {
128  return $this->defaultOptionsLookup->getDefaultOption( $oname );
129  }
130 
131  $options = $this->loadUserOptions( $user );
132  if ( array_key_exists( $oname, $options ) ) {
133  return $options[$oname];
134  }
135  return $defaultOverride;
136  }
137 
141  public function getOptions( UserIdentity $user, int $flags = 0 ): array {
142  $options = $this->loadUserOptions( $user );
143 
144  # We want 'disabled' preferences to always behave as the default value for
145  # users, even if they have set the option explicitly in their settings (ie they
146  # set it, and then it was disabled removing their ability to change it). But
147  # we don't want to erase the preferences in the database in case the preference
148  # is re-enabled again. So don't touch $mOptions, just override the returned value
149  foreach ( $this->serviceOptions->get( 'HiddenPrefs' ) as $pref ) {
150  $default = $this->defaultOptionsLookup->getDefaultOption( $pref );
151  if ( $default !== null ) {
152  $options[$pref] = $default;
153  }
154  }
155 
156  if ( $flags & self::EXCLUDE_DEFAULTS ) {
157  $options = array_diff_assoc( $options, $this->defaultOptionsLookup->getDefaultOptions() );
158  }
159 
160  return $options;
161  }
162 
172  public function setOption( UserIdentity $user, string $oname, $val ) {
173  $this->loadUserOptions( $user );
174 
175  // Explicitly NULL values should refer to defaults
176  if ( $val === null ) {
177  $val = $this->defaultOptionsLookup->getDefaultOption( $oname );
178  }
179 
180  $userKey = $this->getCacheKey( $user );
181  $this->optionsCache[$userKey][$oname] = $val;
182  }
183 
196  public function resetOptions(
197  UserIdentity $user,
199  $resetKinds = [ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ]
200  ) {
201  $oldOptions = $this->loadUserOptions( $user );
202  $defaultOptions = $this->defaultOptionsLookup->getDefaultOptions();
203 
204  if ( !is_array( $resetKinds ) ) {
205  $resetKinds = [ $resetKinds ];
206  }
207 
208  if ( in_array( 'all', $resetKinds ) ) {
209  $newOptions = $defaultOptions;
210  } else {
211  $optionKinds = $this->getOptionKinds( $user, $context );
212  $resetKinds = array_intersect( $resetKinds, $this->listOptionKinds() );
213  $newOptions = [];
214 
215  // Use default values for the options that should be deleted, and
216  // copy old values for the ones that shouldn't.
217  foreach ( $oldOptions as $key => $value ) {
218  if ( in_array( $optionKinds[$key], $resetKinds ) ) {
219  if ( array_key_exists( $key, $defaultOptions ) ) {
220  $newOptions[$key] = $defaultOptions[$key];
221  }
222  } else {
223  $newOptions[$key] = $value;
224  }
225  }
226  }
227 
228  // TODO: Deprecate passing full user to the hook
229  $this->hookRunner->onUserResetAllOptions(
230  User::newFromIdentity( $user ), $newOptions, $oldOptions, $resetKinds
231  );
232 
233  $this->optionsCache[$this->getCacheKey( $user )] = $newOptions;
234  }
235 
259  public function listOptionKinds(): array {
260  return [
261  'registered',
262  'registered-multiselect',
263  'registered-checkmatrix',
264  'userjs',
265  'special',
266  'unused'
267  ];
268  }
269 
283  public function getOptionKinds(
284  UserIdentity $userIdentity,
286  $options = null
287  ): array {
288  if ( $options === null ) {
289  $options = $this->loadUserOptions( $userIdentity );
290  }
291 
292  // TODO: injecting the preferences factory creates a cyclic dependency between
293  // PreferencesFactory and UserOptionsManager. See T250822
294  $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory();
295  $user = User::newFromIdentity( $userIdentity );
296  $preferencesFactory->setUser( $user );
297  // Note that the $user parameter of getFormDescriptor() is deprecated.
298  $prefs = $preferencesFactory->getFormDescriptor( $user, $context );
299  $mapping = [];
300 
301  // Pull out the "special" options, so they don't get converted as
302  // multiselect or checkmatrix.
303  $specialOptions = array_fill_keys( $preferencesFactory->getSaveBlacklist(), true );
304  foreach ( $specialOptions as $name => $value ) {
305  unset( $prefs[$name] );
306  }
307 
308  // Multiselect and checkmatrix options are stored in the database with
309  // one key per option, each having a boolean value. Extract those keys.
310  $multiselectOptions = [];
311  foreach ( $prefs as $name => $info ) {
312  if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
313  ( isset( $info['class'] ) && $info['class'] == HTMLMultiSelectField::class )
314  ) {
315  $opts = HTMLFormField::flattenOptions( $info['options'] );
316  $prefix = $info['prefix'] ?? $name;
317 
318  foreach ( $opts as $value ) {
319  $multiselectOptions["$prefix$value"] = true;
320  }
321 
322  unset( $prefs[$name] );
323  }
324  }
325  $checkmatrixOptions = [];
326  foreach ( $prefs as $name => $info ) {
327  if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
328  ( isset( $info['class'] ) && $info['class'] == HTMLCheckMatrix::class )
329  ) {
330  $columns = HTMLFormField::flattenOptions( $info['columns'] );
331  $rows = HTMLFormField::flattenOptions( $info['rows'] );
332  $prefix = $info['prefix'] ?? $name;
333 
334  foreach ( $columns as $column ) {
335  foreach ( $rows as $row ) {
336  $checkmatrixOptions["$prefix$column-$row"] = true;
337  }
338  }
339 
340  unset( $prefs[$name] );
341  }
342  }
343 
344  // $value is ignored
345  foreach ( $options as $key => $value ) {
346  if ( isset( $prefs[$key] ) ) {
347  $mapping[$key] = 'registered';
348  } elseif ( isset( $multiselectOptions[$key] ) ) {
349  $mapping[$key] = 'registered-multiselect';
350  } elseif ( isset( $checkmatrixOptions[$key] ) ) {
351  $mapping[$key] = 'registered-checkmatrix';
352  } elseif ( isset( $specialOptions[$key] ) ) {
353  $mapping[$key] = 'special';
354  } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
355  $mapping[$key] = 'userjs';
356  } else {
357  $mapping[$key] = 'unused';
358  }
359  }
360 
361  return $mapping;
362  }
363 
371  public function saveOptions( UserIdentity $user ) {
372  $userKey = $this->getCacheKey( $user );
373  // Not using getOptions(), to keep hidden preferences in database
374  $saveOptions = $this->loadUserOptions( $user, self::READ_LATEST );
375  $originalOptions = $this->originalOptionsCache[$userKey] ?? [];
376 
377  // Allow hooks to abort, for instance to save to a global profile.
378  // Reset options to default state before saving.
379  // TODO: Deprecate passing User to the hook.
380  if ( !$this->hookRunner->onUserSaveOptions(
381  User::newFromIdentity( $user ), $saveOptions, $originalOptions )
382  ) {
383  return;
384  }
385  // In case options were modified by the hook
386  $this->optionsCache[$userKey] = $saveOptions;
387 
388  $userId = $user->getId();
389  $insert_rows = []; // all the new preference rows
390  foreach ( $saveOptions as $key => $value ) {
391  // Don't bother storing default values
392  $defaultOption = $this->defaultOptionsLookup->getDefaultOption( $key );
393  if ( ( $defaultOption === null && $value !== false && $value !== null )
394  || $value != $defaultOption
395  ) {
396  $insert_rows[] = [
397  'up_user' => $userId,
398  'up_property' => $key,
399  'up_value' => $value,
400  ];
401  }
402  }
403 
404  $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
405 
406  $res = $dbw->select(
407  'user_properties',
408  [ 'up_property', 'up_value' ],
409  [ 'up_user' => $userId ],
410  __METHOD__
411  );
412 
413  // Find prior rows that need to be removed or updated. These rows will
414  // all be deleted (the latter so that INSERT IGNORE applies the new values).
415  $keysDelete = [];
416  foreach ( $res as $row ) {
417  if ( !isset( $saveOptions[$row->up_property] ) ||
418  $saveOptions[$row->up_property] !== $row->up_value
419  ) {
420  $keysDelete[] = $row->up_property;
421  }
422  }
423 
424  if ( !count( $keysDelete ) && !count( $insert_rows ) ) {
425  return;
426  }
427  $this->originalOptionsCache[$userKey] = null;
428 
429  if ( count( $keysDelete ) ) {
430  // Do the DELETE by PRIMARY KEY for prior rows.
431  // In the past a very large portion of calls to this function are for setting
432  // 'rememberpassword' for new accounts (a preference that has since been removed).
433  // Doing a blanket per-user DELETE for new accounts with no rows in the table
434  // caused gap locks on [max user ID,+infinity) which caused high contention since
435  // updates would pile up on each other as they are for higher (newer) user IDs.
436  // It might not be necessary these days, but it shouldn't hurt either.
437  $dbw->delete(
438  'user_properties',
439  [
440  'up_user' => $userId,
441  'up_property' => $keysDelete
442  ],
443  __METHOD__
444  );
445  }
446  // Insert the new preference rows
447  $dbw->insert(
448  'user_properties',
449  $insert_rows,
450  __METHOD__,
451  [ 'IGNORE' ]
452  );
453  }
454 
462  public function loadUserOptions(
463  UserIdentity $user,
464  int $queryFlags = self::READ_NORMAL,
465  array $data = null
466  ): array {
467  $userKey = $this->getCacheKey( $user );
468  if ( isset( $this->optionsCache[$userKey] ) ) {
469  return $this->optionsCache[$userKey];
470  }
471 
472  $options = $this->defaultOptionsLookup->getDefaultOptions();
473 
474  if ( !$user->isRegistered() ) {
475  // For unlogged-in users, load language/variant options from request.
476  // There's no need to do it for logged-in users: they can set preferences,
477  // and handling of page content is done by $pageLang->getPreferredVariant() and such,
478  // so don't override user's choice (especially when the user chooses site default).
479  $variant = $this->languageConverterFactory->getLanguageConverter()->getDefaultVariant();
480  $options['variant'] = $variant;
481  $options['language'] = $variant;
482  $this->optionsCache[$userKey] = $options;
483  return $options;
484  }
485 
486  // In case options were already loaded from the database before and no options
487  // changes were saved to the database, we can use the cached original options.
488  if ( isset( $this->originalOptionsCache[$userKey] ) ) {
489  $this->logger->debug( 'Loading options from override cache', [
490  'user_id' => $user->getId()
491  ] );
492  return $this->originalOptionsCache[$userKey];
493  } else {
494  if ( !is_array( $data ) ) {
495  $this->logger->debug( 'Loading options from database', [
496  'user_id' => $user->getId()
497  ] );
498 
499  $dbr = $this->getDBForQueryFlags( $queryFlags );
500  $res = $dbr->select(
501  'user_properties',
502  [ 'up_property', 'up_value' ],
503  [ 'up_user' => $user->getId() ],
504  __METHOD__
505  );
506 
507  $data = [];
508  foreach ( $res as $row ) {
509  // Convert '0' to 0. PHP's boolean conversion considers them both
510  // false, but e.g. JavaScript considers the former as true.
511  // @todo: T54542 Somehow determine the desired type (string/int/bool)
512  // and convert all values here.
513  if ( $row->up_value === '0' ) {
514  $row->up_value = 0;
515  }
516  $data[$row->up_property] = $row->up_value;
517  }
518  }
519 
520  foreach ( $data as $property => $value ) {
521  $options[$property] = $value;
522  }
523  }
524 
525  // Replace deprecated language codes
526  $options['language'] = LanguageCode::replaceDeprecatedCodes( $options['language'] );
527  // Need to store what we have so far before the hook to prevent
528  // infinite recursion if the hook attempts to reload options
529  $this->originalOptionsCache[$userKey] = $options;
530  // TODO: Deprecate passing full User object into the hook.
531  $this->hookRunner->onUserLoadOptions(
532  User::newFromIdentity( $user ), $options
533  );
534 
535  $this->originalOptionsCache[$userKey] = $options;
536  $this->optionsCache[$userKey] = $options;
537  return $this->optionsCache[$userKey];
538  }
539 
545  public function clearUserOptionsCache( UserIdentity $user ) {
546  $cacheKey = $this->getCacheKey( $user );
547  $this->optionsCache[$cacheKey] = null;
548  $this->originalOptionsCache[$cacheKey] = null;
549  }
550 
556  private function getCacheKey( UserIdentity $user ): string {
557  return $user->isRegistered() ? "u:{$user->getId()}" : "anon:{$user->getName()}";
558  }
559 
564  private function getDBForQueryFlags( $queryFlags ): IDatabase {
565  list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
566  return $this->loadBalancer->getConnectionRef( $mode, [] );
567  }
568 }
MediaWiki\User\UserOptionsManager\setOption
setOption(UserIdentity $user, string $oname, $val)
Set the given option for a user.
Definition: UserOptionsManager.php:172
LanguageCode\replaceDeprecatedCodes
static replaceDeprecatedCodes( $code)
Replace deprecated language codes that were used in previous versions of MediaWiki to up-to-date,...
Definition: LanguageCode.php:161
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:149
MediaWiki\User\UserOptionsManager\saveOptions
saveOptions(UserIdentity $user)
Saves the non-default options for this user, as previously set e.g.
Definition: UserOptionsManager.php:371
MediaWiki\User\UserOptionsManager\getDefaultOptions
getDefaultOptions()
Combine the language default options with any site-specific options and add the default language vari...
Definition: UserOptionsManager.php:102
MediaWiki\User\UserOptionsManager\$hookRunner
HookRunner $hookRunner
Definition: UserOptionsManager.php:72
User\newFromIdentity
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:590
DBAccessObjectUtils\getDBOptions
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
Definition: DBAccessObjectUtils.php:52
$res
$res
Definition: testCompression.php:57
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:55
MediaWiki\Languages\LanguageConverterFactory
An interface for creating language converters.
Definition: LanguageConverterFactory.php:44
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:32
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
MediaWiki\User\UserOptionsManager\listOptionKinds
listOptionKinds()
Return a list of the types of user options currently returned by UserOptionsManager::getOptionKinds()...
Definition: UserOptionsManager.php:259
$dbr
$dbr
Definition: testCompression.php:54
MediaWiki\MediaWikiServices\getInstance
static getInstance()
Returns the global default instance of the top level service locator.
Definition: MediaWikiServices.php:180
MediaWiki\User\DefaultOptionsLookup
A service class to control default user options.
Definition: DefaultOptionsLookup.php:35
MediaWiki\Config\ServiceOptions
A class for passing options to services.
Definition: ServiceOptions.php:25
MediaWiki\User\UserOptionsManager\$loadBalancer
ILoadBalancer $loadBalancer
Definition: UserOptionsManager.php:60
MediaWiki\User\UserIdentity\isRegistered
isRegistered()
MediaWiki\User\UserOptionsManager\getDBForQueryFlags
getDBForQueryFlags( $queryFlags)
Definition: UserOptionsManager.php:564
MediaWiki\User\UserOptionsManager\getCacheKey
getCacheKey(UserIdentity $user)
Gets a unique key for various caches.
Definition: UserOptionsManager.php:556
MediaWiki\User\UserOptionsManager\getOptions
getOptions(UserIdentity $user, int $flags=0)
Get all user's options.The user to get the option for Bitwise combination of: UserOptionsManager::EXC...
Definition: UserOptionsManager.php:141
MediaWiki\User\UserOptionsManager\$originalOptionsCache
array $originalOptionsCache
Cached original user options fetched from database.
Definition: UserOptionsManager.php:69
HTMLFormField
The parent class to generate form fields.
Definition: HTMLFormField.php:7
MediaWiki\User\UserOptionsManager\clearUserOptionsCache
clearUserOptionsCache(UserIdentity $user)
Clears cached user options.
Definition: UserOptionsManager.php:545
MediaWiki\User\UserOptionsManager\$defaultOptionsLookup
DefaultOptionsLookup $defaultOptionsLookup
Definition: UserOptionsManager.php:54
MediaWiki\User\UserOptionsManager\getOptionKinds
getOptionKinds(UserIdentity $userIdentity, IContextSource $context, $options=null)
Return an associative array mapping preferences keys to the kind of a preference they're used for.
Definition: UserOptionsManager.php:283
DB_MASTER
const DB_MASTER
Definition: defines.php:26
MediaWiki\User\UserOptionsManager\getDefaultOption
getDefaultOption(string $opt)
Get a given default option value.Name of option to retrieve string|null Default option value
Definition: UserOptionsManager.php:109
DBAccessObjectUtils
Helper class for DAO classes.
Definition: DBAccessObjectUtils.php:29
MediaWiki\User\UserOptionsManager\loadUserOptions
loadUserOptions(UserIdentity $user, int $queryFlags=self::READ_NORMAL, array $data=null)
Definition: UserOptionsManager.php:462
MediaWiki\User\UserOptionsManager\resetOptions
resetOptions(UserIdentity $user, IContextSource $context, $resetKinds=[ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused'])
Reset certain (or all) options to the site defaults.
Definition: UserOptionsManager.php:196
MediaWiki\User\UserOptionsManager\$logger
LoggerInterface $logger
Definition: UserOptionsManager.php:63
MediaWiki\User
Definition: DefaultOptionsLookup.php:21
MediaWiki\User\UserOptionsLookup
Provides access to user options.
Definition: UserOptionsLookup.php:27
MediaWiki\User\UserOptionsManager\$serviceOptions
ServiceOptions $serviceOptions
Definition: UserOptionsManager.php:51
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:53
MediaWiki\User\UserOptionsManager
A service class to control user options.
Definition: UserOptionsManager.php:44
MediaWiki\User\UserOptionsManager\getOption
getOption(UserIdentity $user, string $oname, $defaultOverride=null, bool $ignoreHidden=false)
Get the user's current setting for a given option.The user to get the option for The option to check ...
Definition: UserOptionsManager.php:116
MediaWiki\User\UserIdentity\getId
getId()
HTMLCheckMatrix
A checkbox matrix Operates similarly to HTMLMultiSelectField, but instead of using an array of option...
Definition: HTMLCheckMatrix.php:25
MediaWiki\User\UserOptionsManager\$languageConverterFactory
LanguageConverterFactory $languageConverterFactory
Definition: UserOptionsManager.php:57
MediaWiki\User\UserOptionsManager\$optionsCache
array $optionsCache
Cached options by user.
Definition: UserOptionsManager.php:66
HTMLMultiSelectField
Multi-select field.
Definition: HTMLMultiSelectField.php:6
MediaWiki\User\UserOptionsManager\CONSTRUCTOR_OPTIONS
const CONSTRUCTOR_OPTIONS
Definition: UserOptionsManager.php:46
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:44
LanguageCode
Methods for dealing with language codes.
Definition: LanguageCode.php:27
HTMLFormField\flattenOptions
static flattenOptions( $options)
flatten an array of options to a single array, for instance, a set of "<options>" inside "<optgroups>...
Definition: HTMLFormField.php:1094
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:563
MediaWiki\$context
IContextSource $context
Definition: MediaWiki.php:40
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:55
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
MediaWiki\Config\ServiceOptions\assertRequiredOptions
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
Definition: ServiceOptions.php:62
MediaWiki\User\UserOptionsManager\__construct
__construct(ServiceOptions $options, DefaultOptionsLookup $defaultOptionsLookup, LanguageConverterFactory $languageConverterFactory, ILoadBalancer $loadBalancer, LoggerInterface $logger, HookContainer $hookContainer)
Definition: UserOptionsManager.php:82