MediaWiki  master
User.php
Go to the documentation of this file.
1 <?php
35 use Wikimedia\IPSet;
40 
51 class User implements IDBAccessObject, UserIdentity {
52 
56  const TOKEN_LENGTH = 32;
57 
61  const INVALID_TOKEN = '*** INVALID ***';
62 
67  const VERSION = 14;
68 
74 
78  const CHECK_USER_RIGHTS = true;
79 
83  const IGNORE_USER_RIGHTS = false;
84 
92  protected static $mCacheVars = [
93  // user table
94  'mId',
95  'mName',
96  'mRealName',
97  'mEmail',
98  'mTouched',
99  'mToken',
100  'mEmailAuthenticated',
101  'mEmailToken',
102  'mEmailTokenExpires',
103  'mRegistration',
104  'mEditCount',
105  // user_groups table
106  'mGroupMemberships',
107  // user_properties table
108  'mOptionOverrides',
109  // actor table
110  'mActorId',
111  ];
112 
114  // @{
116  public $mId;
118  public $mName;
120  protected $mActorId;
122  public $mRealName;
123 
125  public $mEmail;
127  public $mTouched;
129  protected $mQuickTouched;
131  protected $mToken;
135  protected $mEmailToken;
139  protected $mRegistration;
141  protected $mEditCount;
145  protected $mOptionOverrides;
146  // @}
147 
148  // @{
153 
157  protected $mLoadedItems = [];
158  // @}
159 
170  public $mFrom;
171 
176  protected $mNewtalk;
178  protected $mDatePreference;
180  public $mBlockedby;
182  protected $mHash;
184  protected $mBlockreason;
186  protected $mEffectiveGroups;
188  protected $mImplicitGroups;
190  protected $mFormerGroups;
192  protected $mGlobalBlock;
194  protected $mLocked;
196  public $mHideName;
198  public $mOptions;
199 
201  private $mRequest;
202 
204  public $mBlock;
205 
207  protected $mAllowUsertalk;
208 
210  private $mBlockedFromCreateAccount = false;
211 
213  protected $queryFlagsUsed = self::READ_NORMAL;
214 
216  public static $idCacheByName = [];
217 
229  public function __construct() {
230  $this->clearInstanceCache( 'defaults' );
231  }
232 
236  public function __toString() {
237  return (string)$this->getName();
238  }
239 
240  public function &__get( $name ) {
241  // A shortcut for $mRights deprecation phase
242  if ( $name === 'mRights' ) {
243  $copy = $this->getRights();
244  return $copy;
245  } elseif ( !property_exists( $this, $name ) ) {
246  // T227688 - do not break $u->foo['bar'] = 1
247  wfLogWarning( 'tried to get non-existent property' );
248  $this->$name = null;
249  return $this->$name;
250  } else {
251  wfLogWarning( 'tried to get non-visible property' );
252  $null = null;
253  return $null;
254  }
255  }
256 
257  public function __set( $name, $value ) {
258  // A shortcut for $mRights deprecation phase, only known legitimate use was for
259  // testing purposes, other uses seem bad in principle
260  if ( $name === 'mRights' ) {
261  MediaWikiServices::getInstance()->getPermissionManager()->overrideUserRightsForTesting(
262  $this,
263  is_null( $value ) ? [] : $value
264  );
265  } elseif ( !property_exists( $this, $name ) ) {
266  $this->$name = $value;
267  } else {
268  wfLogWarning( 'tried to set non-visible property' );
269  }
270  }
271 
286  public function isSafeToLoad() {
287  global $wgFullyInitialised;
288 
289  // The user is safe to load if:
290  // * MW_NO_SESSION is undefined AND $wgFullyInitialised is true (safe to use session data)
291  // * mLoadedItems === true (already loaded)
292  // * mFrom !== 'session' (sessions not involved at all)
293 
294  return ( !defined( 'MW_NO_SESSION' ) && $wgFullyInitialised ) ||
295  $this->mLoadedItems === true || $this->mFrom !== 'session';
296  }
297 
303  public function load( $flags = self::READ_NORMAL ) {
304  global $wgFullyInitialised;
305 
306  if ( $this->mLoadedItems === true ) {
307  return;
308  }
309 
310  // Set it now to avoid infinite recursion in accessors
311  $oldLoadedItems = $this->mLoadedItems;
312  $this->mLoadedItems = true;
313  $this->queryFlagsUsed = $flags;
314 
315  // If this is called too early, things are likely to break.
316  if ( !$wgFullyInitialised && $this->mFrom === 'session' ) {
318  ->warning( 'User::loadFromSession called before the end of Setup.php', [
319  'exception' => new Exception( 'User::loadFromSession called before the end of Setup.php' ),
320  ] );
321  $this->loadDefaults();
322  $this->mLoadedItems = $oldLoadedItems;
323  return;
324  }
325 
326  switch ( $this->mFrom ) {
327  case 'defaults':
328  $this->loadDefaults();
329  break;
330  case 'id':
331  // Make sure this thread sees its own changes, if the ID isn't 0
332  if ( $this->mId != 0 ) {
333  $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
334  if ( $lb->hasOrMadeRecentMasterChanges() ) {
335  $flags |= self::READ_LATEST;
336  $this->queryFlagsUsed = $flags;
337  }
338  }
339 
340  $this->loadFromId( $flags );
341  break;
342  case 'actor':
343  case 'name':
344  // Make sure this thread sees its own changes
345  $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
346  if ( $lb->hasOrMadeRecentMasterChanges() ) {
347  $flags |= self::READ_LATEST;
348  $this->queryFlagsUsed = $flags;
349  }
350 
351  list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
352  $row = wfGetDB( $index )->selectRow(
353  'actor',
354  [ 'actor_id', 'actor_user', 'actor_name' ],
355  $this->mFrom === 'name' ? [ 'actor_name' => $this->mName ] : [ 'actor_id' => $this->mActorId ],
356  __METHOD__,
357  $options
358  );
359 
360  if ( !$row ) {
361  // Ugh.
362  $this->loadDefaults( $this->mFrom === 'name' ? $this->mName : false );
363  } elseif ( $row->actor_user ) {
364  $this->mId = $row->actor_user;
365  $this->loadFromId( $flags );
366  } else {
367  $this->loadDefaults( $row->actor_name, $row->actor_id );
368  }
369  break;
370  case 'session':
371  if ( !$this->loadFromSession() ) {
372  // Loading from session failed. Load defaults.
373  $this->loadDefaults();
374  }
375  Hooks::run( 'UserLoadAfterLoadFromSession', [ $this ] );
376  break;
377  default:
378  throw new UnexpectedValueException(
379  "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
380  }
381  }
382 
388  public function loadFromId( $flags = self::READ_NORMAL ) {
389  if ( $this->mId == 0 ) {
390  // Anonymous users are not in the database (don't need cache)
391  $this->loadDefaults();
392  return false;
393  }
394 
395  // Try cache (unless this needs data from the master DB).
396  // NOTE: if this thread called saveSettings(), the cache was cleared.
397  $latest = DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST );
398  if ( $latest ) {
399  if ( !$this->loadFromDatabase( $flags ) ) {
400  // Can't load from ID
401  return false;
402  }
403  } else {
404  $this->loadFromCache();
405  }
406 
407  $this->mLoadedItems = true;
408  $this->queryFlagsUsed = $flags;
409 
410  return true;
411  }
412 
418  public static function purge( $dbDomain, $userId ) {
419  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
420  $key = $cache->makeGlobalKey( 'user', 'id', $dbDomain, $userId );
421  $cache->delete( $key );
422  }
423 
429  protected function getCacheKey( WANObjectCache $cache ) {
430  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
431 
432  return $cache->makeGlobalKey( 'user', 'id', $lbFactory->getLocalDomainID(), $this->mId );
433  }
434 
441  $id = $this->getId();
442 
443  return $id ? [ $this->getCacheKey( $cache ) ] : [];
444  }
445 
452  protected function loadFromCache() {
453  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
454  $data = $cache->getWithSetCallback(
455  $this->getCacheKey( $cache ),
456  $cache::TTL_HOUR,
457  function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache ) {
458  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
459  wfDebug( "User: cache miss for user {$this->mId}\n" );
460 
461  $this->loadFromDatabase( self::READ_NORMAL );
462  $this->loadGroups();
463  $this->loadOptions();
464 
465  $data = [];
466  foreach ( self::$mCacheVars as $name ) {
467  $data[$name] = $this->$name;
468  }
469 
470  $ttl = $cache->adaptiveTTL( wfTimestamp( TS_UNIX, $this->mTouched ), $ttl );
471 
472  // if a user group membership is about to expire, the cache needs to
473  // expire at that time (T163691)
474  foreach ( $this->mGroupMemberships as $ugm ) {
475  if ( $ugm->getExpiry() ) {
476  $secondsUntilExpiry = wfTimestamp( TS_UNIX, $ugm->getExpiry() ) - time();
477  if ( $secondsUntilExpiry > 0 && $secondsUntilExpiry < $ttl ) {
478  $ttl = $secondsUntilExpiry;
479  }
480  }
481  }
482 
483  return $data;
484  },
485  [ 'pcTTL' => $cache::TTL_PROC_LONG, 'version' => self::VERSION ]
486  );
487 
488  // Restore from cache
489  foreach ( self::$mCacheVars as $name ) {
490  $this->$name = $data[$name];
491  }
492 
493  return true;
494  }
495 
497  // @{
498 
515  public static function newFromName( $name, $validate = 'valid' ) {
516  if ( $validate === true ) {
517  $validate = 'valid';
518  }
519  $name = self::getCanonicalName( $name, $validate );
520  if ( $name === false ) {
521  return false;
522  }
523 
524  // Create unloaded user object
525  $u = new User;
526  $u->mName = $name;
527  $u->mFrom = 'name';
528  $u->setItemLoaded( 'name' );
529 
530  return $u;
531  }
532 
539  public static function newFromId( $id ) {
540  $u = new User;
541  $u->mId = $id;
542  $u->mFrom = 'id';
543  $u->setItemLoaded( 'id' );
544  return $u;
545  }
546 
554  public static function newFromActorId( $id ) {
555  $u = new User;
556  $u->mActorId = $id;
557  $u->mFrom = 'actor';
558  $u->setItemLoaded( 'actor' );
559  return $u;
560  }
561 
571  public static function newFromIdentity( UserIdentity $identity ) {
572  if ( $identity instanceof User ) {
573  return $identity;
574  }
575 
576  return self::newFromAnyId(
577  $identity->getId() === 0 ? null : $identity->getId(),
578  $identity->getName() === '' ? null : $identity->getName(),
579  $identity->getActorId() === 0 ? null : $identity->getActorId()
580  );
581  }
582 
596  public static function newFromAnyId( $userId, $userName, $actorId, $dbDomain = false ) {
597  // Stop-gap solution for the problem described in T222212.
598  // Force the User ID and Actor ID to zero for users loaded from the database
599  // of another wiki, to prevent subtle data corruption and confusing failure modes.
600  if ( $dbDomain !== false ) {
601  $userId = 0;
602  $actorId = 0;
603  }
604 
605  $user = new User;
606  $user->mFrom = 'defaults';
607 
608  if ( $actorId !== null ) {
609  $user->mActorId = (int)$actorId;
610  if ( $user->mActorId !== 0 ) {
611  $user->mFrom = 'actor';
612  }
613  $user->setItemLoaded( 'actor' );
614  }
615 
616  if ( $userName !== null && $userName !== '' ) {
617  $user->mName = $userName;
618  $user->mFrom = 'name';
619  $user->setItemLoaded( 'name' );
620  }
621 
622  if ( $userId !== null ) {
623  $user->mId = (int)$userId;
624  if ( $user->mId !== 0 ) {
625  $user->mFrom = 'id';
626  }
627  $user->setItemLoaded( 'id' );
628  }
629 
630  if ( $user->mFrom === 'defaults' ) {
631  throw new InvalidArgumentException(
632  'Cannot create a user with no name, no ID, and no actor ID'
633  );
634  }
635 
636  return $user;
637  }
638 
650  public static function newFromConfirmationCode( $code, $flags = 0 ) {
651  $db = ( $flags & self::READ_LATEST ) == self::READ_LATEST
652  ? wfGetDB( DB_MASTER )
653  : wfGetDB( DB_REPLICA );
654 
655  $id = $db->selectField(
656  'user',
657  'user_id',
658  [
659  'user_email_token' => md5( $code ),
660  'user_email_token_expires > ' . $db->addQuotes( $db->timestamp() ),
661  ]
662  );
663 
664  return $id ? self::newFromId( $id ) : null;
665  }
666 
674  public static function newFromSession( WebRequest $request = null ) {
675  $user = new User;
676  $user->mFrom = 'session';
677  $user->mRequest = $request;
678  return $user;
679  }
680 
696  public static function newFromRow( $row, $data = null ) {
697  $user = new User;
698  $user->loadFromRow( $row, $data );
699  return $user;
700  }
701 
737  public static function newSystemUser( $name, $options = [] ) {
738  $options += [
739  'validate' => 'valid',
740  'create' => true,
741  'steal' => false,
742  ];
743 
744  $name = self::getCanonicalName( $name, $options['validate'] );
745  if ( $name === false ) {
746  return null;
747  }
748 
749  $dbr = wfGetDB( DB_REPLICA );
750  $userQuery = self::getQueryInfo();
751  $row = $dbr->selectRow(
752  $userQuery['tables'],
753  $userQuery['fields'],
754  [ 'user_name' => $name ],
755  __METHOD__,
756  [],
757  $userQuery['joins']
758  );
759  if ( !$row ) {
760  // Try the master database...
761  $dbw = wfGetDB( DB_MASTER );
762  $row = $dbw->selectRow(
763  $userQuery['tables'],
764  $userQuery['fields'],
765  [ 'user_name' => $name ],
766  __METHOD__,
767  [],
768  $userQuery['joins']
769  );
770  }
771 
772  if ( !$row ) {
773  // No user. Create it?
774  return $options['create']
775  ? self::createNew( $name, [ 'token' => self::INVALID_TOKEN ] )
776  : null;
777  }
778 
779  $user = self::newFromRow( $row );
780 
781  // A user is considered to exist as a non-system user if it can
782  // authenticate, or has an email set, or has a non-invalid token.
783  if ( $user->mEmail || $user->mToken !== self::INVALID_TOKEN ||
784  AuthManager::singleton()->userCanAuthenticate( $name )
785  ) {
786  // User exists. Steal it?
787  if ( !$options['steal'] ) {
788  return null;
789  }
790 
791  AuthManager::singleton()->revokeAccessForUser( $name );
792 
793  $user->invalidateEmail();
794  $user->mToken = self::INVALID_TOKEN;
795  $user->saveSettings();
796  SessionManager::singleton()->preventSessionsForUser( $user->getName() );
797  }
798 
799  return $user;
800  }
801 
802  // @}
803 
809  public static function whoIs( $id ) {
810  return UserCache::singleton()->getProp( $id, 'name' );
811  }
812 
819  public static function whoIsReal( $id ) {
820  return UserCache::singleton()->getProp( $id, 'real_name' );
821  }
822 
829  public static function idFromName( $name, $flags = self::READ_NORMAL ) {
830  // Don't explode on self::$idCacheByName[$name] if $name is not a string but e.g. a User object
831  $name = (string)$name;
832  $nt = Title::makeTitleSafe( NS_USER, $name );
833  if ( is_null( $nt ) ) {
834  // Illegal name
835  return null;
836  }
837 
838  if ( !( $flags & self::READ_LATEST ) && array_key_exists( $name, self::$idCacheByName ) ) {
839  return is_null( self::$idCacheByName[$name] ) ? null : (int)self::$idCacheByName[$name];
840  }
841 
842  list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
843  $db = wfGetDB( $index );
844 
845  $s = $db->selectRow(
846  'user',
847  [ 'user_id' ],
848  [ 'user_name' => $nt->getText() ],
849  __METHOD__,
850  $options
851  );
852 
853  if ( $s === false ) {
854  $result = null;
855  } else {
856  $result = (int)$s->user_id;
857  }
858 
859  if ( count( self::$idCacheByName ) >= 1000 ) {
860  self::$idCacheByName = [];
861  }
862 
863  self::$idCacheByName[$name] = $result;
864 
865  return $result;
866  }
867 
871  public static function resetIdByNameCache() {
872  self::$idCacheByName = [];
873  }
874 
891  public static function isIP( $name ) {
892  return preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/', $name )
893  || IP::isIPv6( $name );
894  }
895 
902  public function isIPRange() {
903  return IP::isValidRange( $this->mName );
904  }
905 
917  public static function isValidUserName( $name ) {
918  global $wgMaxNameChars;
919 
920  if ( $name == ''
921  || self::isIP( $name )
922  || strpos( $name, '/' ) !== false
923  || strlen( $name ) > $wgMaxNameChars
924  || $name != MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $name )
925  ) {
926  return false;
927  }
928 
929  // Ensure that the name can't be misresolved as a different title,
930  // such as with extra namespace keys at the start.
931  $parsed = Title::newFromText( $name );
932  if ( is_null( $parsed )
933  || $parsed->getNamespace()
934  || strcmp( $name, $parsed->getPrefixedText() ) ) {
935  return false;
936  }
937 
938  // Check an additional blacklist of troublemaker characters.
939  // Should these be merged into the title char list?
940  $unicodeBlacklist = '/[' .
941  '\x{0080}-\x{009f}' . # iso-8859-1 control chars
942  '\x{00a0}' . # non-breaking space
943  '\x{2000}-\x{200f}' . # various whitespace
944  '\x{2028}-\x{202f}' . # breaks and control chars
945  '\x{3000}' . # ideographic space
946  '\x{e000}-\x{f8ff}' . # private use
947  ']/u';
948  if ( preg_match( $unicodeBlacklist, $name ) ) {
949  return false;
950  }
951 
952  return true;
953  }
954 
966  public static function isUsableName( $name ) {
967  global $wgReservedUsernames;
968  // Must be a valid username, obviously ;)
969  if ( !self::isValidUserName( $name ) ) {
970  return false;
971  }
972 
973  static $reservedUsernames = false;
974  if ( !$reservedUsernames ) {
975  $reservedUsernames = $wgReservedUsernames;
976  Hooks::run( 'UserGetReservedNames', [ &$reservedUsernames ] );
977  }
978 
979  // Certain names may be reserved for batch processes.
980  foreach ( $reservedUsernames as $reserved ) {
981  if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
982  $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->plain();
983  }
984  if ( $reserved == $name ) {
985  return false;
986  }
987  }
988  return true;
989  }
990 
1001  public static function findUsersByGroup( $groups, $limit = 5000, $after = null ) {
1002  if ( $groups === [] ) {
1003  return UserArrayFromResult::newFromIDs( [] );
1004  }
1005 
1006  $groups = array_unique( (array)$groups );
1007  $limit = min( 5000, $limit );
1008 
1009  $conds = [ 'ug_group' => $groups ];
1010  if ( $after !== null ) {
1011  $conds[] = 'ug_user > ' . (int)$after;
1012  }
1013 
1014  $dbr = wfGetDB( DB_REPLICA );
1015  $ids = $dbr->selectFieldValues(
1016  'user_groups',
1017  'ug_user',
1018  $conds,
1019  __METHOD__,
1020  [
1021  'DISTINCT' => true,
1022  'ORDER BY' => 'ug_user',
1023  'LIMIT' => $limit,
1024  ]
1025  ) ?: [];
1026  return UserArray::newFromIDs( $ids );
1027  }
1028 
1041  public static function isCreatableName( $name ) {
1043 
1044  // Ensure that the username isn't longer than 235 bytes, so that
1045  // (at least for the builtin skins) user javascript and css files
1046  // will work. (T25080)
1047  if ( strlen( $name ) > 235 ) {
1048  wfDebugLog( 'username', __METHOD__ .
1049  ": '$name' invalid due to length" );
1050  return false;
1051  }
1052 
1053  // Preg yells if you try to give it an empty string
1054  if ( $wgInvalidUsernameCharacters !== '' &&
1055  preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name )
1056  ) {
1057  wfDebugLog( 'username', __METHOD__ .
1058  ": '$name' invalid due to wgInvalidUsernameCharacters" );
1059  return false;
1060  }
1061 
1062  return self::isUsableName( $name );
1063  }
1064 
1071  public function isValidPassword( $password ) {
1072  // simple boolean wrapper for checkPasswordValidity
1073  return $this->checkPasswordValidity( $password )->isGood();
1074  }
1075 
1097  public function checkPasswordValidity( $password ) {
1098  global $wgPasswordPolicy;
1099 
1100  $upp = new UserPasswordPolicy(
1101  $wgPasswordPolicy['policies'],
1102  $wgPasswordPolicy['checks']
1103  );
1104 
1105  $status = Status::newGood( [] );
1106  $result = false; // init $result to false for the internal checks
1107 
1108  if ( !Hooks::run( 'isValidPassword', [ $password, &$result, $this ] ) ) {
1109  $status->error( $result );
1110  return $status;
1111  }
1112 
1113  if ( $result === false ) {
1114  $status->merge( $upp->checkUserPassword( $this, $password ), true );
1115  return $status;
1116  }
1117 
1118  if ( $result === true ) {
1119  return $status;
1120  }
1121 
1122  $status->error( $result );
1123  return $status; // the isValidPassword hook set a string $result and returned true
1124  }
1125 
1139  public static function getCanonicalName( $name, $validate = 'valid' ) {
1140  // Force usernames to capital
1141  $name = MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $name );
1142 
1143  # Reject names containing '#'; these will be cleaned up
1144  # with title normalisation, but then it's too late to
1145  # check elsewhere
1146  if ( strpos( $name, '#' ) !== false ) {
1147  return false;
1148  }
1149 
1150  // Clean up name according to title rules,
1151  // but only when validation is requested (T14654)
1152  $t = ( $validate !== false ) ?
1153  Title::newFromText( $name, NS_USER ) : Title::makeTitle( NS_USER, $name );
1154  // Check for invalid titles
1155  if ( is_null( $t ) || $t->getNamespace() !== NS_USER || $t->isExternal() ) {
1156  return false;
1157  }
1158 
1159  $name = $t->getText();
1160 
1161  switch ( $validate ) {
1162  case false:
1163  break;
1164  case 'valid':
1165  if ( !self::isValidUserName( $name ) ) {
1166  $name = false;
1167  }
1168  break;
1169  case 'usable':
1170  if ( !self::isUsableName( $name ) ) {
1171  $name = false;
1172  }
1173  break;
1174  case 'creatable':
1175  if ( !self::isCreatableName( $name ) ) {
1176  $name = false;
1177  }
1178  break;
1179  default:
1180  throw new InvalidArgumentException(
1181  'Invalid parameter value for $validate in ' . __METHOD__ );
1182  }
1183  return $name;
1184  }
1185 
1195  public function loadDefaults( $name = false, $actorId = null ) {
1196  $this->mId = 0;
1197  $this->mName = $name;
1198  $this->mActorId = $actorId;
1199  $this->mRealName = '';
1200  $this->mEmail = '';
1201  $this->mOptionOverrides = null;
1202  $this->mOptionsLoaded = false;
1203 
1204  $loggedOut = $this->mRequest && !defined( 'MW_NO_SESSION' )
1205  ? $this->mRequest->getSession()->getLoggedOutTimestamp() : 0;
1206  if ( $loggedOut !== 0 ) {
1207  $this->mTouched = wfTimestamp( TS_MW, $loggedOut );
1208  } else {
1209  $this->mTouched = '1'; # Allow any pages to be cached
1210  }
1211 
1212  $this->mToken = null; // Don't run cryptographic functions till we need a token
1213  $this->mEmailAuthenticated = null;
1214  $this->mEmailToken = '';
1215  $this->mEmailTokenExpires = null;
1216  $this->mRegistration = wfTimestamp( TS_MW );
1217  $this->mGroupMemberships = [];
1218 
1219  Hooks::run( 'UserLoadDefaults', [ $this, $name ] );
1220  }
1221 
1234  public function isItemLoaded( $item, $all = 'all' ) {
1235  return ( $this->mLoadedItems === true && $all === 'all' ) ||
1236  ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true );
1237  }
1238 
1244  protected function setItemLoaded( $item ) {
1245  if ( is_array( $this->mLoadedItems ) ) {
1246  $this->mLoadedItems[$item] = true;
1247  }
1248  }
1249 
1255  private function loadFromSession() {
1256  // MediaWiki\Session\Session already did the necessary authentication of the user
1257  // returned here, so just use it if applicable.
1258  $session = $this->getRequest()->getSession();
1259  $user = $session->getUser();
1260  if ( $user->isLoggedIn() ) {
1261  $this->loadFromUserObject( $user );
1262 
1263  // If this user is autoblocked, set a cookie to track the block. This has to be done on
1264  // every session load, because an autoblocked editor might not edit again from the same
1265  // IP address after being blocked.
1266  MediaWikiServices::getInstance()->getBlockManager()->trackBlockWithCookie( $this );
1267 
1268  // Other code expects these to be set in the session, so set them.
1269  $session->set( 'wsUserID', $this->getId() );
1270  $session->set( 'wsUserName', $this->getName() );
1271  $session->set( 'wsToken', $this->getToken() );
1272 
1273  return true;
1274  }
1275 
1276  return false;
1277  }
1278 
1284  public function trackBlockWithCookie() {
1285  MediaWikiServices::getInstance()->getBlockManager()->trackBlockWithCookie( $this );
1286  }
1287 
1295  public function loadFromDatabase( $flags = self::READ_LATEST ) {
1296  // Paranoia
1297  $this->mId = intval( $this->mId );
1298 
1299  if ( !$this->mId ) {
1300  // Anonymous users are not in the database
1301  $this->loadDefaults();
1302  return false;
1303  }
1304 
1305  list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
1306  $db = wfGetDB( $index );
1307 
1308  $userQuery = self::getQueryInfo();
1309  $s = $db->selectRow(
1310  $userQuery['tables'],
1311  $userQuery['fields'],
1312  [ 'user_id' => $this->mId ],
1313  __METHOD__,
1314  $options,
1315  $userQuery['joins']
1316  );
1317 
1318  $this->queryFlagsUsed = $flags;
1319  Hooks::run( 'UserLoadFromDatabase', [ $this, &$s ] );
1320 
1321  if ( $s !== false ) {
1322  // Initialise user table data
1323  $this->loadFromRow( $s );
1324  $this->mGroupMemberships = null; // deferred
1325  $this->getEditCount(); // revalidation for nulls
1326  return true;
1327  }
1328 
1329  // Invalid user_id
1330  $this->mId = 0;
1331  $this->loadDefaults();
1332 
1333  return false;
1334  }
1335 
1348  protected function loadFromRow( $row, $data = null ) {
1349  if ( !is_object( $row ) ) {
1350  throw new InvalidArgumentException( '$row must be an object' );
1351  }
1352 
1353  $all = true;
1354 
1355  $this->mGroupMemberships = null; // deferred
1356 
1357  if ( isset( $row->actor_id ) ) {
1358  $this->mActorId = (int)$row->actor_id;
1359  if ( $this->mActorId !== 0 ) {
1360  $this->mFrom = 'actor';
1361  }
1362  $this->setItemLoaded( 'actor' );
1363  } else {
1364  $all = false;
1365  }
1366 
1367  if ( isset( $row->user_name ) && $row->user_name !== '' ) {
1368  $this->mName = $row->user_name;
1369  $this->mFrom = 'name';
1370  $this->setItemLoaded( 'name' );
1371  } else {
1372  $all = false;
1373  }
1374 
1375  if ( isset( $row->user_real_name ) ) {
1376  $this->mRealName = $row->user_real_name;
1377  $this->setItemLoaded( 'realname' );
1378  } else {
1379  $all = false;
1380  }
1381 
1382  if ( isset( $row->user_id ) ) {
1383  $this->mId = intval( $row->user_id );
1384  if ( $this->mId !== 0 ) {
1385  $this->mFrom = 'id';
1386  }
1387  $this->setItemLoaded( 'id' );
1388  } else {
1389  $all = false;
1390  }
1391 
1392  if ( isset( $row->user_id ) && isset( $row->user_name ) && $row->user_name !== '' ) {
1393  self::$idCacheByName[$row->user_name] = $row->user_id;
1394  }
1395 
1396  if ( isset( $row->user_editcount ) ) {
1397  $this->mEditCount = $row->user_editcount;
1398  } else {
1399  $all = false;
1400  }
1401 
1402  if ( isset( $row->user_touched ) ) {
1403  $this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
1404  } else {
1405  $all = false;
1406  }
1407 
1408  if ( isset( $row->user_token ) ) {
1409  // The definition for the column is binary(32), so trim the NULs
1410  // that appends. The previous definition was char(32), so trim
1411  // spaces too.
1412  $this->mToken = rtrim( $row->user_token, " \0" );
1413  if ( $this->mToken === '' ) {
1414  $this->mToken = null;
1415  }
1416  } else {
1417  $all = false;
1418  }
1419 
1420  if ( isset( $row->user_email ) ) {
1421  $this->mEmail = $row->user_email;
1422  $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
1423  $this->mEmailToken = $row->user_email_token;
1424  $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
1425  $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
1426  } else {
1427  $all = false;
1428  }
1429 
1430  if ( $all ) {
1431  $this->mLoadedItems = true;
1432  }
1433 
1434  if ( is_array( $data ) ) {
1435  if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) {
1436  if ( $data['user_groups'] === [] ) {
1437  $this->mGroupMemberships = [];
1438  } else {
1439  $firstGroup = reset( $data['user_groups'] );
1440  if ( is_array( $firstGroup ) || is_object( $firstGroup ) ) {
1441  $this->mGroupMemberships = [];
1442  foreach ( $data['user_groups'] as $row ) {
1443  $ugm = UserGroupMembership::newFromRow( (object)$row );
1444  $this->mGroupMemberships[$ugm->getGroup()] = $ugm;
1445  }
1446  }
1447  }
1448  }
1449  if ( isset( $data['user_properties'] ) && is_array( $data['user_properties'] ) ) {
1450  $this->loadOptions( $data['user_properties'] );
1451  }
1452  }
1453  }
1454 
1460  protected function loadFromUserObject( $user ) {
1461  $user->load();
1462  foreach ( self::$mCacheVars as $var ) {
1463  $this->$var = $user->$var;
1464  }
1465  }
1466 
1470  private function loadGroups() {
1471  if ( is_null( $this->mGroupMemberships ) ) {
1472  $db = ( $this->queryFlagsUsed & self::READ_LATEST )
1473  ? wfGetDB( DB_MASTER )
1474  : wfGetDB( DB_REPLICA );
1475  $this->mGroupMemberships = UserGroupMembership::getMembershipsForUser(
1476  $this->mId, $db );
1477  }
1478  }
1479 
1494  public function addAutopromoteOnceGroups( $event ) {
1496 
1497  if ( wfReadOnly() || !$this->getId() ) {
1498  return [];
1499  }
1500 
1501  $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
1502  if ( $toPromote === [] ) {
1503  return [];
1504  }
1505 
1506  if ( !$this->checkAndSetTouched() ) {
1507  return []; // raced out (bug T48834)
1508  }
1509 
1510  $oldGroups = $this->getGroups(); // previous groups
1511  $oldUGMs = $this->getGroupMemberships();
1512  foreach ( $toPromote as $group ) {
1513  $this->addGroup( $group );
1514  }
1515  $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
1516  $newUGMs = $this->getGroupMemberships();
1517 
1518  // update groups in external authentication database
1519  Hooks::run( 'UserGroupsChanged', [ $this, $toPromote, [], false, false, $oldUGMs, $newUGMs ] );
1520 
1521  $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
1522  $logEntry->setPerformer( $this );
1523  $logEntry->setTarget( $this->getUserPage() );
1524  $logEntry->setParameters( [
1525  '4::oldgroups' => $oldGroups,
1526  '5::newgroups' => $newGroups,
1527  ] );
1528  $logid = $logEntry->insert();
1529  if ( $wgAutopromoteOnceLogInRC ) {
1530  $logEntry->publish( $logid );
1531  }
1532 
1533  return $toPromote;
1534  }
1535 
1545  protected function makeUpdateConditions( IDatabase $db, array $conditions ) {
1546  if ( $this->mTouched ) {
1547  // CAS check: only update if the row wasn't changed sicne it was loaded.
1548  $conditions['user_touched'] = $db->timestamp( $this->mTouched );
1549  }
1550 
1551  return $conditions;
1552  }
1553 
1563  protected function checkAndSetTouched() {
1564  $this->load();
1565 
1566  if ( !$this->mId ) {
1567  return false; // anon
1568  }
1569 
1570  // Get a new user_touched that is higher than the old one
1571  $newTouched = $this->newTouchedTimestamp();
1572 
1573  $dbw = wfGetDB( DB_MASTER );
1574  $dbw->update( 'user',
1575  [ 'user_touched' => $dbw->timestamp( $newTouched ) ],
1576  $this->makeUpdateConditions( $dbw, [
1577  'user_id' => $this->mId,
1578  ] ),
1579  __METHOD__
1580  );
1581  $success = ( $dbw->affectedRows() > 0 );
1582 
1583  if ( $success ) {
1584  $this->mTouched = $newTouched;
1585  $this->clearSharedCache( 'changed' );
1586  } else {
1587  // Clears on failure too since that is desired if the cache is stale
1588  $this->clearSharedCache( 'refresh' );
1589  }
1590 
1591  return $success;
1592  }
1593 
1601  public function clearInstanceCache( $reloadFrom = false ) {
1602  global $wgFullyInitialised;
1603 
1604  $this->mNewtalk = -1;
1605  $this->mDatePreference = null;
1606  $this->mBlockedby = -1; # Unset
1607  $this->mHash = false;
1608  $this->mEffectiveGroups = null;
1609  $this->mImplicitGroups = null;
1610  $this->mGroupMemberships = null;
1611  $this->mOptions = null;
1612  $this->mOptionsLoaded = false;
1613  $this->mEditCount = null;
1614 
1615  // Replacement of former `$this->mRights = null` line
1616  if ( $wgFullyInitialised && $this->mFrom ) {
1617  MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache(
1618  $this
1619  );
1620  }
1621 
1622  if ( $reloadFrom ) {
1623  $this->mLoadedItems = [];
1624  $this->mFrom = $reloadFrom;
1625  }
1626  }
1627 
1629  private static $defOpt = null;
1631  private static $defOptLang = null;
1632 
1639  public static function resetGetDefaultOptionsForTestsOnly() {
1640  Assert::invariant( defined( 'MW_PHPUNIT_TEST' ), 'Unit tests only' );
1641  self::$defOpt = null;
1642  self::$defOptLang = null;
1643  }
1644 
1651  public static function getDefaultOptions() {
1653 
1654  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1655  if ( self::$defOpt !== null && self::$defOptLang === $contLang->getCode() ) {
1656  // The content language does not change (and should not change) mid-request, but the
1657  // unit tests change it anyway, and expect this method to return values relevant to the
1658  // current content language.
1659  return self::$defOpt;
1660  }
1661 
1662  self::$defOpt = $wgDefaultUserOptions;
1663  // Default language setting
1664  self::$defOptLang = $contLang->getCode();
1665  self::$defOpt['language'] = self::$defOptLang;
1666  foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
1667  if ( $langCode === $contLang->getCode() ) {
1668  self::$defOpt['variant'] = $langCode;
1669  } else {
1670  self::$defOpt["variant-$langCode"] = $langCode;
1671  }
1672  }
1673 
1674  // NOTE: don't use SearchEngineConfig::getSearchableNamespaces here,
1675  // since extensions may change the set of searchable namespaces depending
1676  // on user groups/permissions.
1677  foreach ( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
1678  self::$defOpt['searchNs' . $nsnum] = (bool)$val;
1679  }
1680  self::$defOpt['skin'] = Skin::normalizeKey( $wgDefaultSkin );
1681 
1682  Hooks::run( 'UserGetDefaultOptions', [ &self::$defOpt ] );
1683 
1684  return self::$defOpt;
1685  }
1686 
1693  public static function getDefaultOption( $opt ) {
1694  $defOpts = self::getDefaultOptions();
1695  return $defOpts[$opt] ?? null;
1696  }
1697 
1707  private function getBlockedStatus( $fromReplica = true ) {
1708  if ( $this->mBlockedby != -1 ) {
1709  return;
1710  }
1711 
1712  wfDebug( __METHOD__ . ": checking...\n" );
1713 
1714  // Initialize data...
1715  // Otherwise something ends up stomping on $this->mBlockedby when
1716  // things get lazy-loaded later, causing false positive block hits
1717  // due to -1 !== 0. Probably session-related... Nothing should be
1718  // overwriting mBlockedby, surely?
1719  $this->load();
1720 
1721  // TODO: Block checking shouldn't really be done from the User object. Block
1722  // checking can involve checking for IP blocks, cookie blocks, and/or XFF blocks,
1723  // which need more knowledge of the request context than the User should have.
1724  // Since we do currently check blocks from the User, we have to do the following
1725  // here:
1726  // - Check if this is the user associated with the main request
1727  // - If so, pass the relevant request information to the block manager
1728  $request = null;
1729 
1730  // The session user is set up towards the end of Setup.php. Until then,
1731  // assume it's a logged-out user.
1732  $sessionUser = RequestContext::getMain()->getUser();
1733  $globalUserName = $sessionUser->isSafeToLoad()
1734  ? $sessionUser->getName()
1735  : IP::sanitizeIP( $sessionUser->getRequest()->getIP() );
1736 
1737  if ( $this->getName() === $globalUserName ) {
1738  // This is the global user, so we need to pass the request
1739  $request = $this->getRequest();
1740  }
1741 
1742  // @phan-suppress-next-line PhanAccessMethodInternal It's the only allowed use
1743  $block = MediaWikiServices::getInstance()->getBlockManager()->getUserBlock(
1744  $this,
1745  $request,
1746  $fromReplica
1747  );
1748 
1749  if ( $block ) {
1750  $this->mBlock = $block;
1751  $this->mBlockedby = $block->getByName();
1752  $this->mBlockreason = $block->getReason();
1753  $this->mHideName = $block->getHideName();
1754  $this->mAllowUsertalk = $block->isUsertalkEditAllowed();
1755  } else {
1756  $this->mBlock = null;
1757  $this->mBlockedby = '';
1758  $this->mBlockreason = '';
1759  $this->mHideName = 0;
1760  $this->mAllowUsertalk = false;
1761  }
1762 
1763  // Avoid PHP 7.1 warning of passing $this by reference
1764  $thisUser = $this;
1765  // Extensions
1766  Hooks::run( 'GetBlockedStatus', [ &$thisUser ], '1.34' );
1767  }
1768 
1777  public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
1778  return MediaWikiServices::getInstance()->getBlockManager()
1779  ->isDnsBlacklisted( $ip, $checkWhitelist );
1780  }
1781 
1790  public function inDnsBlacklist( $ip, $bases ) {
1791  wfDeprecated( __METHOD__, '1.34' );
1792 
1793  $found = false;
1794  // @todo FIXME: IPv6 ??? (https://bugs.php.net/bug.php?id=33170)
1795  if ( IP::isIPv4( $ip ) ) {
1796  // Reverse IP, T23255
1797  $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
1798 
1799  foreach ( (array)$bases as $base ) {
1800  // Make hostname
1801  // If we have an access key, use that too (ProjectHoneypot, etc.)
1802  $basename = $base;
1803  if ( is_array( $base ) ) {
1804  if ( count( $base ) >= 2 ) {
1805  // Access key is 1, base URL is 0
1806  $host = "{$base[1]}.$ipReversed.{$base[0]}";
1807  } else {
1808  $host = "$ipReversed.{$base[0]}";
1809  }
1810  $basename = $base[0];
1811  } else {
1812  $host = "$ipReversed.$base";
1813  }
1814 
1815  // Send query
1816  $ipList = gethostbynamel( $host );
1817 
1818  if ( $ipList ) {
1819  wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $basename!" );
1820  $found = true;
1821  break;
1822  }
1823 
1824  wfDebugLog( 'dnsblacklist', "Requested $host, not found in $basename." );
1825  }
1826  }
1827 
1828  return $found;
1829  }
1830 
1838  public static function isLocallyBlockedProxy( $ip ) {
1839  wfDeprecated( __METHOD__, '1.34' );
1840 
1841  global $wgProxyList;
1842 
1843  if ( !$wgProxyList ) {
1844  return false;
1845  }
1846 
1847  if ( !is_array( $wgProxyList ) ) {
1848  // Load values from the specified file
1849  $wgProxyList = array_map( 'trim', file( $wgProxyList ) );
1850  }
1851 
1852  $resultProxyList = [];
1853  $deprecatedIPEntries = [];
1854 
1855  // backward compatibility: move all ip addresses in keys to values
1856  foreach ( $wgProxyList as $key => $value ) {
1857  $keyIsIP = IP::isIPAddress( $key );
1858  $valueIsIP = IP::isIPAddress( $value );
1859  if ( $keyIsIP && !$valueIsIP ) {
1860  $deprecatedIPEntries[] = $key;
1861  $resultProxyList[] = $key;
1862  } elseif ( $keyIsIP && $valueIsIP ) {
1863  $deprecatedIPEntries[] = $key;
1864  $resultProxyList[] = $key;
1865  $resultProxyList[] = $value;
1866  } else {
1867  $resultProxyList[] = $value;
1868  }
1869  }
1870 
1871  if ( $deprecatedIPEntries ) {
1872  wfDeprecated(
1873  'IP addresses in the keys of $wgProxyList (found the following IP addresses in keys: ' .
1874  implode( ', ', $deprecatedIPEntries ) . ', please move them to values)', '1.30' );
1875  }
1876 
1877  $proxyListIPSet = new IPSet( $resultProxyList );
1878  return $proxyListIPSet->match( $ip );
1879  }
1880 
1886  public function isPingLimitable() {
1887  global $wgRateLimitsExcludedIPs;
1888  if ( IP::isInRanges( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) {
1889  // No other good way currently to disable rate limits
1890  // for specific IPs. :P
1891  // But this is a crappy hack and should die.
1892  return false;
1893  }
1894  return !$this->isAllowed( 'noratelimit' );
1895  }
1896 
1911  public function pingLimiter( $action = 'edit', $incrBy = 1 ) {
1912  // Avoid PHP 7.1 warning of passing $this by reference
1913  $user = $this;
1914  // Call the 'PingLimiter' hook
1915  $result = false;
1916  if ( !Hooks::run( 'PingLimiter', [ &$user, $action, &$result, $incrBy ] ) ) {
1917  return $result;
1918  }
1919 
1920  global $wgRateLimits;
1921  if ( !isset( $wgRateLimits[$action] ) ) {
1922  return false;
1923  }
1924 
1925  $limits = array_merge(
1926  [ '&can-bypass' => true ],
1927  $wgRateLimits[$action]
1928  );
1929 
1930  // Some groups shouldn't trigger the ping limiter, ever
1931  if ( $limits['&can-bypass'] && !$this->isPingLimitable() ) {
1932  return false;
1933  }
1934 
1935  $keys = [];
1936  $id = $this->getId();
1937  $userLimit = false;
1938  $isNewbie = $this->isNewbie();
1940 
1941  if ( $id == 0 ) {
1942  // limits for anons
1943  if ( isset( $limits['anon'] ) ) {
1944  $keys[$cache->makeKey( 'limiter', $action, 'anon' )] = $limits['anon'];
1945  }
1946  } elseif ( isset( $limits['user'] ) ) {
1947  // limits for logged-in users
1948  $userLimit = $limits['user'];
1949  }
1950 
1951  // limits for anons and for newbie logged-in users
1952  if ( $isNewbie ) {
1953  // ip-based limits
1954  if ( isset( $limits['ip'] ) ) {
1955  $ip = $this->getRequest()->getIP();
1956  $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
1957  }
1958  // subnet-based limits
1959  if ( isset( $limits['subnet'] ) ) {
1960  $ip = $this->getRequest()->getIP();
1961  $subnet = IP::getSubnet( $ip );
1962  if ( $subnet !== false ) {
1963  $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
1964  }
1965  }
1966  }
1967 
1968  // Check for group-specific permissions
1969  // If more than one group applies, use the group with the highest limit ratio (max/period)
1970  foreach ( $this->getGroups() as $group ) {
1971  if ( isset( $limits[$group] ) ) {
1972  if ( $userLimit === false
1973  || $limits[$group][0] / $limits[$group][1] > $userLimit[0] / $userLimit[1]
1974  ) {
1975  $userLimit = $limits[$group];
1976  }
1977  }
1978  }
1979 
1980  // limits for newbie logged-in users (override all the normal user limits)
1981  if ( $id !== 0 && $isNewbie && isset( $limits['newbie'] ) ) {
1982  $userLimit = $limits['newbie'];
1983  }
1984 
1985  // Set the user limit key
1986  if ( $userLimit !== false ) {
1987  // phan is confused because &can-bypass's value is a bool, so it assumes
1988  // that $userLimit is also a bool here.
1989  // @phan-suppress-next-line PhanTypeInvalidExpressionArrayDestructuring
1990  list( $max, $period ) = $userLimit;
1991  wfDebug( __METHOD__ . ": effective user limit: $max in {$period}s\n" );
1992  $keys[$cache->makeKey( 'limiter', $action, 'user', $id )] = $userLimit;
1993  }
1994 
1995  // ip-based limits for all ping-limitable users
1996  if ( isset( $limits['ip-all'] ) ) {
1997  $ip = $this->getRequest()->getIP();
1998  // ignore if user limit is more permissive
1999  if ( $isNewbie || $userLimit === false
2000  || $limits['ip-all'][0] / $limits['ip-all'][1] > $userLimit[0] / $userLimit[1] ) {
2001  $keys["mediawiki:limiter:$action:ip-all:$ip"] = $limits['ip-all'];
2002  }
2003  }
2004 
2005  // subnet-based limits for all ping-limitable users
2006  if ( isset( $limits['subnet-all'] ) ) {
2007  $ip = $this->getRequest()->getIP();
2008  $subnet = IP::getSubnet( $ip );
2009  if ( $subnet !== false ) {
2010  // ignore if user limit is more permissive
2011  if ( $isNewbie || $userLimit === false
2012  || $limits['ip-all'][0] / $limits['ip-all'][1]
2013  > $userLimit[0] / $userLimit[1] ) {
2014  $keys["mediawiki:limiter:$action:subnet-all:$subnet"] = $limits['subnet-all'];
2015  }
2016  }
2017  }
2018 
2019  $triggered = false;
2020  foreach ( $keys as $key => $limit ) {
2021  // phan is confused because &can-bypass's value is a bool, so it assumes
2022  // that $userLimit is also a bool here.
2023  // @phan-suppress-next-line PhanTypeInvalidExpressionArrayDestructuring
2024  list( $max, $period ) = $limit;
2025  $summary = "(limit $max in {$period}s)";
2026  $count = $cache->get( $key );
2027  // Already pinged?
2028  if ( $count && $count >= $max ) {
2029  wfDebugLog( 'ratelimit', "User '{$this->getName()}' " .
2030  "(IP {$this->getRequest()->getIP()}) tripped $key at $count $summary" );
2031  $triggered = true;
2032  } else {
2033  wfDebug( __METHOD__ . ": adding record for $key $summary\n" );
2034  if ( $incrBy > 0 ) {
2035  $cache->add( $key, 0, intval( $period ) ); // first ping
2036  }
2037  }
2038  if ( $incrBy > 0 ) {
2039  $cache->incrWithInit( $key, (int)$period, $incrBy, $incrBy );
2040  }
2041  }
2042 
2043  return $triggered;
2044  }
2045 
2057  public function isBlocked( $fromReplica = true ) {
2058  return $this->getBlock( $fromReplica ) instanceof AbstractBlock &&
2059  $this->getBlock()->appliesToRight( 'edit' );
2060  }
2061 
2068  public function getBlock( $fromReplica = true ) {
2069  $this->getBlockedStatus( $fromReplica );
2070  return $this->mBlock instanceof AbstractBlock ? $this->mBlock : null;
2071  }
2072 
2084  public function isBlockedFrom( $title, $fromReplica = false ) {
2085  return MediaWikiServices::getInstance()->getPermissionManager()
2086  ->isBlockedFrom( $this, $title, $fromReplica );
2087  }
2088 
2093  public function blockedBy() {
2094  $this->getBlockedStatus();
2095  return $this->mBlockedby;
2096  }
2097 
2102  public function blockedFor() {
2103  $this->getBlockedStatus();
2104  return $this->mBlockreason;
2105  }
2106 
2111  public function getBlockId() {
2112  $this->getBlockedStatus();
2113  return ( $this->mBlock ? $this->mBlock->getId() : false );
2114  }
2115 
2124  public function isBlockedGlobally( $ip = '' ) {
2125  return $this->getGlobalBlock( $ip ) instanceof AbstractBlock;
2126  }
2127 
2138  public function getGlobalBlock( $ip = '' ) {
2139  if ( $this->mGlobalBlock !== null ) {
2140  return $this->mGlobalBlock ?: null;
2141  }
2142  // User is already an IP?
2143  if ( IP::isIPAddress( $this->getName() ) ) {
2144  $ip = $this->getName();
2145  } elseif ( !$ip ) {
2146  $ip = $this->getRequest()->getIP();
2147  }
2148  // Avoid PHP 7.1 warning of passing $this by reference
2149  $user = $this;
2150  $blocked = false;
2151  $block = null;
2152  Hooks::run( 'UserIsBlockedGlobally', [ &$user, $ip, &$blocked, &$block ] );
2153 
2154  if ( $blocked && $block === null ) {
2155  // back-compat: UserIsBlockedGlobally didn't have $block param first
2156  $block = new SystemBlock( [
2157  'address' => $ip,
2158  'systemBlock' => 'global-block'
2159  ] );
2160  }
2161 
2162  $this->mGlobalBlock = $blocked ? $block : false;
2163  return $this->mGlobalBlock ?: null;
2164  }
2165 
2171  public function isLocked() {
2172  if ( $this->mLocked !== null ) {
2173  return $this->mLocked;
2174  }
2175  // Reset for hook
2176  $this->mLocked = false;
2177  Hooks::run( 'UserIsLocked', [ $this, &$this->mLocked ] );
2178  return $this->mLocked;
2179  }
2180 
2186  public function isHidden() {
2187  if ( $this->mHideName !== null ) {
2188  return (bool)$this->mHideName;
2189  }
2190  $this->getBlockedStatus();
2191  if ( !$this->mHideName ) {
2192  // Reset for hook
2193  $this->mHideName = false;
2194  Hooks::run( 'UserIsHidden', [ $this, &$this->mHideName ], '1.34' );
2195  }
2196  return (bool)$this->mHideName;
2197  }
2198 
2203  public function getId() {
2204  if ( $this->mId === null && $this->mName !== null &&
2205  ( self::isIP( $this->mName ) || ExternalUserNames::isExternal( $this->mName ) )
2206  ) {
2207  // Special case, we know the user is anonymous
2208  return 0;
2209  }
2210 
2211  if ( !$this->isItemLoaded( 'id' ) ) {
2212  // Don't load if this was initialized from an ID
2213  $this->load();
2214  }
2215 
2216  return (int)$this->mId;
2217  }
2218 
2223  public function setId( $v ) {
2224  $this->mId = $v;
2225  $this->clearInstanceCache( 'id' );
2226  }
2227 
2232  public function getName() {
2233  if ( $this->isItemLoaded( 'name', 'only' ) ) {
2234  // Special case optimisation
2235  return $this->mName;
2236  }
2237 
2238  $this->load();
2239  if ( $this->mName === false ) {
2240  // Clean up IPs
2241  $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() );
2242  }
2243 
2244  return $this->mName;
2245  }
2246 
2260  public function setName( $str ) {
2261  $this->load();
2262  $this->mName = $str;
2263  }
2264 
2271  public function getActorId( IDatabase $dbw = null ) {
2272  if ( !$this->isItemLoaded( 'actor' ) ) {
2273  $this->load();
2274  }
2275 
2276  if ( !$this->mActorId && $dbw ) {
2277  $q = [
2278  'actor_user' => $this->getId() ?: null,
2279  'actor_name' => (string)$this->getName(),
2280  ];
2281  if ( $q['actor_user'] === null && self::isUsableName( $q['actor_name'] ) ) {
2282  throw new CannotCreateActorException(
2283  'Cannot create an actor for a usable name that is not an existing user'
2284  );
2285  }
2286  if ( $q['actor_name'] === '' ) {
2287  throw new CannotCreateActorException( 'Cannot create an actor for a user with no name' );
2288  }
2289  $dbw->insert( 'actor', $q, __METHOD__, [ 'IGNORE' ] );
2290  if ( $dbw->affectedRows() ) {
2291  $this->mActorId = (int)$dbw->insertId();
2292  } else {
2293  // Outdated cache?
2294  // Use LOCK IN SHARE MODE to bypass any MySQL REPEATABLE-READ snapshot.
2295  $this->mActorId = (int)$dbw->selectField(
2296  'actor',
2297  'actor_id',
2298  $q,
2299  __METHOD__,
2300  [ 'LOCK IN SHARE MODE' ]
2301  );
2302  if ( !$this->mActorId ) {
2303  throw new CannotCreateActorException(
2304  "Cannot create actor ID for user_id={$this->getId()} user_name={$this->getName()}"
2305  );
2306  }
2307  }
2308  $this->invalidateCache();
2309  $this->setItemLoaded( 'actor' );
2310  }
2311 
2312  return (int)$this->mActorId;
2313  }
2314 
2319  public function getTitleKey() {
2320  return str_replace( ' ', '_', $this->getName() );
2321  }
2322 
2327  public function getNewtalk() {
2328  $this->load();
2329 
2330  // Load the newtalk status if it is unloaded (mNewtalk=-1)
2331  if ( $this->mNewtalk === -1 ) {
2332  $this->mNewtalk = false; # reset talk page status
2333 
2334  // Check memcached separately for anons, who have no
2335  // entire User object stored in there.
2336  if ( !$this->mId ) {
2337  global $wgDisableAnonTalk;
2338  if ( $wgDisableAnonTalk ) {
2339  // Anon newtalk disabled by configuration.
2340  $this->mNewtalk = false;
2341  } else {
2342  $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
2343  }
2344  } else {
2345  $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
2346  }
2347  }
2348 
2349  return (bool)$this->mNewtalk;
2350  }
2351 
2365  public function getNewMessageLinks() {
2366  // Avoid PHP 7.1 warning of passing $this by reference
2367  $user = $this;
2368  $talks = [];
2369  if ( !Hooks::run( 'UserRetrieveNewTalks', [ &$user, &$talks ] ) ) {
2370  return $talks;
2371  }
2372 
2373  if ( !$this->getNewtalk() ) {
2374  return [];
2375  }
2376  $utp = $this->getTalkPage();
2377  $dbr = wfGetDB( DB_REPLICA );
2378  // Get the "last viewed rev" timestamp from the oldest message notification
2379  $timestamp = $dbr->selectField( 'user_newtalk',
2380  'MIN(user_last_timestamp)',
2381  $this->isAnon() ? [ 'user_ip' => $this->getName() ] : [ 'user_id' => $this->getId() ],
2382  __METHOD__ );
2383  $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null;
2384  return [
2385  [
2387  'link' => $utp->getLocalURL(),
2388  'rev' => $rev
2389  ]
2390  ];
2391  }
2392 
2398  public function getNewMessageRevisionId() {
2399  $newMessageRevisionId = null;
2400  $newMessageLinks = $this->getNewMessageLinks();
2401 
2402  // Note: getNewMessageLinks() never returns more than a single link
2403  // and it is always for the same wiki, but we double-check here in
2404  // case that changes some time in the future.
2405  if ( $newMessageLinks && count( $newMessageLinks ) === 1
2406  && WikiMap::isCurrentWikiId( $newMessageLinks[0]['wiki'] )
2407  && $newMessageLinks[0]['rev']
2408  ) {
2410  $newMessageRevision = $newMessageLinks[0]['rev'];
2411  $newMessageRevisionId = $newMessageRevision->getId();
2412  }
2413 
2414  return $newMessageRevisionId;
2415  }
2416 
2425  protected function checkNewtalk( $field, $id ) {
2426  $dbr = wfGetDB( DB_REPLICA );
2427 
2428  $ok = $dbr->selectField( 'user_newtalk', $field, [ $field => $id ], __METHOD__ );
2429 
2430  return $ok !== false;
2431  }
2432 
2440  protected function updateNewtalk( $field, $id, $curRev = null ) {
2441  // Get timestamp of the talk page revision prior to the current one
2442  $prevRev = $curRev ? $curRev->getPrevious() : false;
2443  $ts = $prevRev ? $prevRev->getTimestamp() : null;
2444  // Mark the user as having new messages since this revision
2445  $dbw = wfGetDB( DB_MASTER );
2446  $dbw->insert( 'user_newtalk',
2447  [ $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ],
2448  __METHOD__,
2449  [ 'IGNORE' ] );
2450  if ( $dbw->affectedRows() ) {
2451  wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
2452  return true;
2453  }
2454 
2455  wfDebug( __METHOD__ . " already set ($field, $id)\n" );
2456  return false;
2457  }
2458 
2465  protected function deleteNewtalk( $field, $id ) {
2466  $dbw = wfGetDB( DB_MASTER );
2467  $dbw->delete( 'user_newtalk',
2468  [ $field => $id ],
2469  __METHOD__ );
2470  if ( $dbw->affectedRows() ) {
2471  wfDebug( __METHOD__ . ": killed on ($field, $id)\n" );
2472  return true;
2473  }
2474 
2475  wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
2476  return false;
2477  }
2478 
2485  public function setNewtalk( $val, $curRev = null ) {
2486  if ( wfReadOnly() ) {
2487  return;
2488  }
2489 
2490  $this->load();
2491  $this->mNewtalk = $val;
2492 
2493  if ( $this->isAnon() ) {
2494  $field = 'user_ip';
2495  $id = $this->getName();
2496  } else {
2497  $field = 'user_id';
2498  $id = $this->getId();
2499  }
2500 
2501  if ( $val ) {
2502  $changed = $this->updateNewtalk( $field, $id, $curRev );
2503  } else {
2504  $changed = $this->deleteNewtalk( $field, $id );
2505  }
2506 
2507  if ( $changed ) {
2508  $this->invalidateCache();
2509  }
2510  }
2511 
2518  private function newTouchedTimestamp() {
2519  $time = time();
2520  if ( $this->mTouched ) {
2521  $time = max( $time, wfTimestamp( TS_UNIX, $this->mTouched ) + 1 );
2522  }
2523 
2524  return wfTimestamp( TS_MW, $time );
2525  }
2526 
2537  public function clearSharedCache( $mode = 'refresh' ) {
2538  if ( !$this->getId() ) {
2539  return;
2540  }
2541 
2542  $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
2543  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
2544  $key = $this->getCacheKey( $cache );
2545 
2546  if ( $mode === 'refresh' ) {
2547  $cache->delete( $key, 1 ); // low tombstone/"hold-off" TTL
2548  } else {
2549  $lb->getConnectionRef( DB_MASTER )->onTransactionPreCommitOrIdle(
2550  function () use ( $cache, $key ) {
2551  $cache->delete( $key );
2552  },
2553  __METHOD__
2554  );
2555  }
2556  }
2557 
2563  public function invalidateCache() {
2564  $this->touch();
2565  $this->clearSharedCache( 'changed' );
2566  }
2567 
2580  public function touch() {
2581  $id = $this->getId();
2582  if ( $id ) {
2583  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
2584  $key = $cache->makeKey( 'user-quicktouched', 'id', $id );
2585  $cache->touchCheckKey( $key );
2586  $this->mQuickTouched = null;
2587  }
2588  }
2589 
2595  public function validateCache( $timestamp ) {
2596  return ( $timestamp >= $this->getTouched() );
2597  }
2598 
2607  public function getTouched() {
2608  $this->load();
2609 
2610  if ( $this->mId ) {
2611  if ( $this->mQuickTouched === null ) {
2612  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
2613  $key = $cache->makeKey( 'user-quicktouched', 'id', $this->mId );
2614 
2615  $this->mQuickTouched = wfTimestamp( TS_MW, $cache->getCheckKeyTime( $key ) );
2616  }
2617 
2618  return max( $this->mTouched, $this->mQuickTouched );
2619  }
2620 
2621  return $this->mTouched;
2622  }
2623 
2629  public function getDBTouched() {
2630  $this->load();
2631 
2632  return $this->mTouched;
2633  }
2634 
2651  public function setPassword( $str ) {
2652  wfDeprecated( __METHOD__, '1.27' );
2653  return $this->setPasswordInternal( $str );
2654  }
2655 
2664  public function setInternalPassword( $str ) {
2665  wfDeprecated( __METHOD__, '1.27' );
2666  $this->setPasswordInternal( $str );
2667  }
2668 
2677  private function setPasswordInternal( $str ) {
2678  $manager = AuthManager::singleton();
2679 
2680  // If the user doesn't exist yet, fail
2681  if ( !$manager->userExists( $this->getName() ) ) {
2682  throw new LogicException( 'Cannot set a password for a user that is not in the database.' );
2683  }
2684 
2685  $status = $this->changeAuthenticationData( [
2686  'username' => $this->getName(),
2687  'password' => $str,
2688  'retype' => $str,
2689  ] );
2690  if ( !$status->isGood() ) {
2692  ->info( __METHOD__ . ': Password change rejected: '
2693  . $status->getWikiText( null, null, 'en' ) );
2694  return false;
2695  }
2696 
2697  $this->setOption( 'watchlisttoken', false );
2698  SessionManager::singleton()->invalidateSessionsForUser( $this );
2699 
2700  return true;
2701  }
2702 
2715  public function changeAuthenticationData( array $data ) {
2716  $manager = AuthManager::singleton();
2717  $reqs = $manager->getAuthenticationRequests( AuthManager::ACTION_CHANGE, $this );
2718  $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
2719 
2720  $status = Status::newGood( 'ignored' );
2721  foreach ( $reqs as $req ) {
2722  $status->merge( $manager->allowsAuthenticationDataChange( $req ), true );
2723  }
2724  if ( $status->getValue() === 'ignored' ) {
2725  $status->warning( 'authenticationdatachange-ignored' );
2726  }
2727 
2728  if ( $status->isGood() ) {
2729  foreach ( $reqs as $req ) {
2730  $manager->changeAuthenticationData( $req );
2731  }
2732  }
2733  return $status;
2734  }
2735 
2742  public function getToken( $forceCreation = true ) {
2744 
2745  $this->load();
2746  if ( !$this->mToken && $forceCreation ) {
2747  $this->setToken();
2748  }
2749 
2750  if ( !$this->mToken ) {
2751  // The user doesn't have a token, return null to indicate that.
2752  return null;
2753  }
2754 
2755  if ( $this->mToken === self::INVALID_TOKEN ) {
2756  // We return a random value here so existing token checks are very
2757  // likely to fail.
2758  return MWCryptRand::generateHex( self::TOKEN_LENGTH );
2759  }
2760 
2761  if ( $wgAuthenticationTokenVersion === null ) {
2762  // $wgAuthenticationTokenVersion not in use, so return the raw secret
2763  return $this->mToken;
2764  }
2765 
2766  // $wgAuthenticationTokenVersion in use, so hmac it.
2767  $ret = MWCryptHash::hmac( $wgAuthenticationTokenVersion, $this->mToken, false );
2768 
2769  // The raw hash can be overly long. Shorten it up.
2770  $len = max( 32, self::TOKEN_LENGTH );
2771  if ( strlen( $ret ) < $len ) {
2772  // Should never happen, even md5 is 128 bits
2773  throw new \UnexpectedValueException( 'Hmac returned less than 128 bits' );
2774  }
2775 
2776  return substr( $ret, -$len );
2777  }
2778 
2785  public function setToken( $token = false ) {
2786  $this->load();
2787  if ( $this->mToken === self::INVALID_TOKEN ) {
2789  ->debug( __METHOD__ . ": Ignoring attempt to set token for system user \"$this\"" );
2790  } elseif ( !$token ) {
2791  $this->mToken = MWCryptRand::generateHex( self::TOKEN_LENGTH );
2792  } else {
2793  $this->mToken = $token;
2794  }
2795  }
2796 
2801  public function getEmail() {
2802  $this->load();
2803  Hooks::run( 'UserGetEmail', [ $this, &$this->mEmail ] );
2804  return $this->mEmail;
2805  }
2806 
2812  $this->load();
2813  Hooks::run( 'UserGetEmailAuthenticationTimestamp', [ $this, &$this->mEmailAuthenticated ] );
2815  }
2816 
2821  public function setEmail( $str ) {
2822  $this->load();
2823  if ( $str == $this->mEmail ) {
2824  return;
2825  }
2826  $this->invalidateEmail();
2827  $this->mEmail = $str;
2828  Hooks::run( 'UserSetEmail', [ $this, &$this->mEmail ] );
2829  }
2830 
2838  public function setEmailWithConfirmation( $str ) {
2840 
2841  if ( !$wgEnableEmail ) {
2842  return Status::newFatal( 'emaildisabled' );
2843  }
2844 
2845  $oldaddr = $this->getEmail();
2846  if ( $str === $oldaddr ) {
2847  return Status::newGood( true );
2848  }
2849 
2850  $type = $oldaddr != '' ? 'changed' : 'set';
2851  $notificationResult = null;
2852 
2853  if ( $wgEmailAuthentication && $type === 'changed' ) {
2854  // Send the user an email notifying the user of the change in registered
2855  // email address on their previous email address
2856  $change = $str != '' ? 'changed' : 'removed';
2857  $notificationResult = $this->sendMail(
2858  wfMessage( 'notificationemail_subject_' . $change )->text(),
2859  wfMessage( 'notificationemail_body_' . $change,
2860  $this->getRequest()->getIP(),
2861  $this->getName(),
2862  $str )->text()
2863  );
2864  }
2865 
2866  $this->setEmail( $str );
2867 
2868  if ( $str !== '' && $wgEmailAuthentication ) {
2869  // Send a confirmation request to the new address if needed
2870  $result = $this->sendConfirmationMail( $type );
2871 
2872  if ( $notificationResult !== null ) {
2873  $result->merge( $notificationResult );
2874  }
2875 
2876  if ( $result->isGood() ) {
2877  // Say to the caller that a confirmation and notification mail has been sent
2878  $result->value = 'eauth';
2879  }
2880  } else {
2881  $result = Status::newGood( true );
2882  }
2883 
2884  return $result;
2885  }
2886 
2891  public function getRealName() {
2892  if ( !$this->isItemLoaded( 'realname' ) ) {
2893  $this->load();
2894  }
2895 
2896  return $this->mRealName;
2897  }
2898 
2903  public function setRealName( $str ) {
2904  $this->load();
2905  $this->mRealName = $str;
2906  }
2907 
2918  public function getOption( $oname, $defaultOverride = null, $ignoreHidden = false ) {
2919  global $wgHiddenPrefs;
2920  $this->loadOptions();
2921 
2922  # We want 'disabled' preferences to always behave as the default value for
2923  # users, even if they have set the option explicitly in their settings (ie they
2924  # set it, and then it was disabled removing their ability to change it). But
2925  # we don't want to erase the preferences in the database in case the preference
2926  # is re-enabled again. So don't touch $mOptions, just override the returned value
2927  if ( !$ignoreHidden && in_array( $oname, $wgHiddenPrefs ) ) {
2928  return self::getDefaultOption( $oname );
2929  }
2930 
2931  if ( array_key_exists( $oname, $this->mOptions ) ) {
2932  return $this->mOptions[$oname];
2933  }
2934 
2935  return $defaultOverride;
2936  }
2937 
2946  public function getOptions( $flags = 0 ) {
2947  global $wgHiddenPrefs;
2948  $this->loadOptions();
2949  $options = $this->mOptions;
2950 
2951  # We want 'disabled' preferences to always behave as the default value for
2952  # users, even if they have set the option explicitly in their settings (ie they
2953  # set it, and then it was disabled removing their ability to change it). But
2954  # we don't want to erase the preferences in the database in case the preference
2955  # is re-enabled again. So don't touch $mOptions, just override the returned value
2956  foreach ( $wgHiddenPrefs as $pref ) {
2957  $default = self::getDefaultOption( $pref );
2958  if ( $default !== null ) {
2959  $options[$pref] = $default;
2960  }
2961  }
2962 
2963  if ( $flags & self::GETOPTIONS_EXCLUDE_DEFAULTS ) {
2964  $options = array_diff_assoc( $options, self::getDefaultOptions() );
2965  }
2966 
2967  return $options;
2968  }
2969 
2977  public function getBoolOption( $oname ) {
2978  return (bool)$this->getOption( $oname );
2979  }
2980 
2989  public function getIntOption( $oname, $defaultOverride = 0 ) {
2990  $val = $this->getOption( $oname );
2991  if ( $val == '' ) {
2992  $val = $defaultOverride;
2993  }
2994  return intval( $val );
2995  }
2996 
3005  public function setOption( $oname, $val ) {
3006  $this->loadOptions();
3007 
3008  // Explicitly NULL values should refer to defaults
3009  if ( is_null( $val ) ) {
3010  $val = self::getDefaultOption( $oname );
3011  }
3012 
3013  $this->mOptions[$oname] = $val;
3014  }
3015 
3026  public function getTokenFromOption( $oname ) {
3027  global $wgHiddenPrefs;
3028 
3029  $id = $this->getId();
3030  if ( !$id || in_array( $oname, $wgHiddenPrefs ) ) {
3031  return false;
3032  }
3033 
3034  $token = $this->getOption( $oname );
3035  if ( !$token ) {
3036  // Default to a value based on the user token to avoid space
3037  // wasted on storing tokens for all users. When this option
3038  // is set manually by the user, only then is it stored.
3039  $token = hash_hmac( 'sha1', "$oname:$id", $this->getToken() );
3040  }
3041 
3042  return $token;
3043  }
3044 
3054  public function resetTokenFromOption( $oname ) {
3055  global $wgHiddenPrefs;
3056  if ( in_array( $oname, $wgHiddenPrefs ) ) {
3057  return false;
3058  }
3059 
3060  $token = MWCryptRand::generateHex( 40 );
3061  $this->setOption( $oname, $token );
3062  return $token;
3063  }
3064 
3088  public static function listOptionKinds() {
3089  return [
3090  'registered',
3091  'registered-multiselect',
3092  'registered-checkmatrix',
3093  'userjs',
3094  'special',
3095  'unused'
3096  ];
3097  }
3098 
3111  public function getOptionKinds( IContextSource $context, $options = null ) {
3112  $this->loadOptions();
3113  if ( $options === null ) {
3114  $options = $this->mOptions;
3115  }
3116 
3117  $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory();
3118  $prefs = $preferencesFactory->getFormDescriptor( $this, $context );
3119  $mapping = [];
3120 
3121  // Pull out the "special" options, so they don't get converted as
3122  // multiselect or checkmatrix.
3123  $specialOptions = array_fill_keys( $preferencesFactory->getSaveBlacklist(), true );
3124  foreach ( $specialOptions as $name => $value ) {
3125  unset( $prefs[$name] );
3126  }
3127 
3128  // Multiselect and checkmatrix options are stored in the database with
3129  // one key per option, each having a boolean value. Extract those keys.
3130  $multiselectOptions = [];
3131  foreach ( $prefs as $name => $info ) {
3132  if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
3133  ( isset( $info['class'] ) && $info['class'] == HTMLMultiSelectField::class ) ) {
3134  $opts = HTMLFormField::flattenOptions( $info['options'] );
3135  $prefix = $info['prefix'] ?? $name;
3136 
3137  foreach ( $opts as $value ) {
3138  $multiselectOptions["$prefix$value"] = true;
3139  }
3140 
3141  unset( $prefs[$name] );
3142  }
3143  }
3144  $checkmatrixOptions = [];
3145  foreach ( $prefs as $name => $info ) {
3146  if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
3147  ( isset( $info['class'] ) && $info['class'] == HTMLCheckMatrix::class ) ) {
3148  $columns = HTMLFormField::flattenOptions( $info['columns'] );
3149  $rows = HTMLFormField::flattenOptions( $info['rows'] );
3150  $prefix = $info['prefix'] ?? $name;
3151 
3152  foreach ( $columns as $column ) {
3153  foreach ( $rows as $row ) {
3154  $checkmatrixOptions["$prefix$column-$row"] = true;
3155  }
3156  }
3157 
3158  unset( $prefs[$name] );
3159  }
3160  }
3161 
3162  // $value is ignored
3163  foreach ( $options as $key => $value ) {
3164  if ( isset( $prefs[$key] ) ) {
3165  $mapping[$key] = 'registered';
3166  } elseif ( isset( $multiselectOptions[$key] ) ) {
3167  $mapping[$key] = 'registered-multiselect';
3168  } elseif ( isset( $checkmatrixOptions[$key] ) ) {
3169  $mapping[$key] = 'registered-checkmatrix';
3170  } elseif ( isset( $specialOptions[$key] ) ) {
3171  $mapping[$key] = 'special';
3172  } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
3173  $mapping[$key] = 'userjs';
3174  } else {
3175  $mapping[$key] = 'unused';
3176  }
3177  }
3178 
3179  return $mapping;
3180  }
3181 
3196  public function resetOptions(
3197  $resetKinds = [ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ],
3198  IContextSource $context = null
3199  ) {
3200  $this->load();
3201  $defaultOptions = self::getDefaultOptions();
3202 
3203  if ( !is_array( $resetKinds ) ) {
3204  $resetKinds = [ $resetKinds ];
3205  }
3206 
3207  if ( in_array( 'all', $resetKinds ) ) {
3208  $newOptions = $defaultOptions;
3209  } else {
3210  if ( $context === null ) {
3212  }
3213 
3214  $optionKinds = $this->getOptionKinds( $context );
3215  $resetKinds = array_intersect( $resetKinds, self::listOptionKinds() );
3216  $newOptions = [];
3217 
3218  // Use default values for the options that should be deleted, and
3219  // copy old values for the ones that shouldn't.
3220  foreach ( $this->mOptions as $key => $value ) {
3221  if ( in_array( $optionKinds[$key], $resetKinds ) ) {
3222  if ( array_key_exists( $key, $defaultOptions ) ) {
3223  $newOptions[$key] = $defaultOptions[$key];
3224  }
3225  } else {
3226  $newOptions[$key] = $value;
3227  }
3228  }
3229  }
3230 
3231  Hooks::run( 'UserResetAllOptions', [ $this, &$newOptions, $this->mOptions, $resetKinds ] );
3232 
3233  $this->mOptions = $newOptions;
3234  $this->mOptionsLoaded = true;
3235  }
3236 
3241  public function getDatePreference() {
3242  // Important migration for old data rows
3243  if ( is_null( $this->mDatePreference ) ) {
3244  global $wgLang;
3245  $value = $this->getOption( 'date' );
3246  $map = $wgLang->getDatePreferenceMigrationMap();
3247  if ( isset( $map[$value] ) ) {
3248  $value = $map[$value];
3249  }
3250  $this->mDatePreference = $value;
3251  }
3252  return $this->mDatePreference;
3253  }
3254 
3261  public function requiresHTTPS() {
3262  global $wgSecureLogin;
3263  if ( !$wgSecureLogin ) {
3264  return false;
3265  }
3266 
3267  $https = $this->getBoolOption( 'prefershttps' );
3268  Hooks::run( 'UserRequiresHTTPS', [ $this, &$https ] );
3269  if ( $https ) {
3270  $https = wfCanIPUseHTTPS( $this->getRequest()->getIP() );
3271  }
3272 
3273  return $https;
3274  }
3275 
3281  public function getStubThreshold() {
3282  global $wgMaxArticleSize; # Maximum article size, in Kb
3283  $threshold = $this->getIntOption( 'stubthreshold' );
3284  if ( $threshold > $wgMaxArticleSize * 1024 ) {
3285  // If they have set an impossible value, disable the preference
3286  // so we can use the parser cache again.
3287  $threshold = 0;
3288  }
3289  return $threshold;
3290  }
3291 
3300  public function getRights() {
3301  return MediaWikiServices::getInstance()->getPermissionManager()->getUserPermissions( $this );
3302  }
3303 
3310  public function getGroups() {
3311  $this->load();
3312  $this->loadGroups();
3313  return array_keys( $this->mGroupMemberships );
3314  }
3315 
3323  public function getGroupMemberships() {
3324  $this->load();
3325  $this->loadGroups();
3326  return $this->mGroupMemberships;
3327  }
3328 
3336  public function getEffectiveGroups( $recache = false ) {
3337  if ( $recache || is_null( $this->mEffectiveGroups ) ) {
3338  $this->mEffectiveGroups = array_unique( array_merge(
3339  $this->getGroups(), // explicit groups
3340  $this->getAutomaticGroups( $recache ) // implicit groups
3341  ) );
3342  // Avoid PHP 7.1 warning of passing $this by reference
3343  $user = $this;
3344  // Hook for additional groups
3345  Hooks::run( 'UserEffectiveGroups', [ &$user, &$this->mEffectiveGroups ] );
3346  // Force reindexation of groups when a hook has unset one of them
3347  $this->mEffectiveGroups = array_values( array_unique( $this->mEffectiveGroups ) );
3348  }
3349  return $this->mEffectiveGroups;
3350  }
3351 
3359  public function getAutomaticGroups( $recache = false ) {
3360  if ( $recache || is_null( $this->mImplicitGroups ) ) {
3361  $this->mImplicitGroups = [ '*' ];
3362  if ( $this->getId() ) {
3363  $this->mImplicitGroups[] = 'user';
3364 
3365  $this->mImplicitGroups = array_unique( array_merge(
3366  $this->mImplicitGroups,
3368  ) );
3369  }
3370  if ( $recache ) {
3371  // Assure data consistency with rights/groups,
3372  // as getEffectiveGroups() depends on this function
3373  $this->mEffectiveGroups = null;
3374  }
3375  }
3376  return $this->mImplicitGroups;
3377  }
3378 
3388  public function getFormerGroups() {
3389  $this->load();
3390 
3391  if ( is_null( $this->mFormerGroups ) ) {
3392  $db = ( $this->queryFlagsUsed & self::READ_LATEST )
3393  ? wfGetDB( DB_MASTER )
3394  : wfGetDB( DB_REPLICA );
3395  $res = $db->select( 'user_former_groups',
3396  [ 'ufg_group' ],
3397  [ 'ufg_user' => $this->mId ],
3398  __METHOD__ );
3399  $this->mFormerGroups = [];
3400  foreach ( $res as $row ) {
3401  $this->mFormerGroups[] = $row->ufg_group;
3402  }
3403  }
3404 
3405  return $this->mFormerGroups;
3406  }
3407 
3412  public function getEditCount() {
3413  if ( !$this->getId() ) {
3414  return null;
3415  }
3416 
3417  if ( $this->mEditCount === null ) {
3418  /* Populate the count, if it has not been populated yet */
3419  $dbr = wfGetDB( DB_REPLICA );
3420  // check if the user_editcount field has been initialized
3421  $count = $dbr->selectField(
3422  'user', 'user_editcount',
3423  [ 'user_id' => $this->mId ],
3424  __METHOD__
3425  );
3426 
3427  if ( $count === null ) {
3428  // it has not been initialized. do so.
3429  $count = $this->initEditCountInternal( $dbr );
3430  }
3431  $this->mEditCount = $count;
3432  }
3433  return (int)$this->mEditCount;
3434  }
3435 
3447  public function addGroup( $group, $expiry = null ) {
3448  $this->load();
3449  $this->loadGroups();
3450 
3451  if ( $expiry ) {
3452  $expiry = wfTimestamp( TS_MW, $expiry );
3453  }
3454 
3455  if ( !Hooks::run( 'UserAddGroup', [ $this, &$group, &$expiry ] ) ) {
3456  return false;
3457  }
3458 
3459  // create the new UserGroupMembership and put it in the DB
3460  $ugm = new UserGroupMembership( $this->mId, $group, $expiry );
3461  if ( !$ugm->insert( true ) ) {
3462  return false;
3463  }
3464 
3465  $this->mGroupMemberships[$group] = $ugm;
3466 
3467  // Refresh the groups caches, and clear the rights cache so it will be
3468  // refreshed on the next call to $this->getRights().
3469  $this->getEffectiveGroups( true );
3470  MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache( $this );
3471  $this->invalidateCache();
3472 
3473  return true;
3474  }
3475 
3482  public function removeGroup( $group ) {
3483  $this->load();
3484 
3485  if ( !Hooks::run( 'UserRemoveGroup', [ $this, &$group ] ) ) {
3486  return false;
3487  }
3488 
3489  $ugm = UserGroupMembership::getMembership( $this->mId, $group );
3490  // delete the membership entry
3491  if ( !$ugm || !$ugm->delete() ) {
3492  return false;
3493  }
3494 
3495  $this->loadGroups();
3496  unset( $this->mGroupMemberships[$group] );
3497 
3498  // Refresh the groups caches, and clear the rights cache so it will be
3499  // refreshed on the next call to $this->getRights().
3500  $this->getEffectiveGroups( true );
3501  MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache( $this );
3502  $this->invalidateCache();
3503 
3504  return true;
3505  }
3506 
3516  public function isRegistered() {
3517  return $this->getId() != 0;
3518  }
3519 
3524  public function isLoggedIn() {
3525  return $this->isRegistered();
3526  }
3527 
3532  public function isAnon() {
3533  return !$this->isRegistered();
3534  }
3535 
3540  public function isBot() {
3541  if ( in_array( 'bot', $this->getGroups() ) && $this->isAllowed( 'bot' ) ) {
3542  return true;
3543  }
3544 
3545  $isBot = false;
3546  Hooks::run( "UserIsBot", [ $this, &$isBot ] );
3547 
3548  return $isBot;
3549  }
3550 
3561  public function isAllowedAny() {
3562  return MediaWikiServices::getInstance()
3563  ->getPermissionManager()
3564  ->userHasAnyRight( $this, ...func_get_args() );
3565  }
3566 
3574  public function isAllowedAll() {
3575  return MediaWikiServices::getInstance()
3576  ->getPermissionManager()
3577  ->userHasAllRights( $this, ...func_get_args() );
3578  }
3579 
3590  public function isAllowed( $action = '' ) {
3591  return MediaWikiServices::getInstance()->getPermissionManager()
3592  ->userHasRight( $this, $action );
3593  }
3594 
3599  public function useRCPatrol() {
3600  global $wgUseRCPatrol;
3601  return $wgUseRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' );
3602  }
3603 
3608  public function useNPPatrol() {
3610  return (
3611  ( $wgUseRCPatrol || $wgUseNPPatrol )
3612  && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
3613  );
3614  }
3615 
3620  public function useFilePatrol() {
3622  return (
3623  ( $wgUseRCPatrol || $wgUseFilePatrol )
3624  && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
3625  );
3626  }
3627 
3633  public function getRequest() {
3634  if ( $this->mRequest ) {
3635  return $this->mRequest;
3636  }
3637 
3638  global $wgRequest;
3639  return $wgRequest;
3640  }
3641 
3650  public function isWatched( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
3651  if ( $title->isWatchable() && ( !$checkRights || $this->isAllowed( 'viewmywatchlist' ) ) ) {
3652  return MediaWikiServices::getInstance()->getWatchedItemStore()->isWatched( $this, $title );
3653  }
3654  return false;
3655  }
3656 
3664  public function addWatch( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
3665  if ( !$checkRights || $this->isAllowed( 'editmywatchlist' ) ) {
3666  MediaWikiServices::getInstance()->getWatchedItemStore()->addWatchBatchForUser(
3667  $this,
3668  [ $title->getSubjectPage(), $title->getTalkPage() ]
3669  );
3670  }
3671  $this->invalidateCache();
3672  }
3673 
3681  public function removeWatch( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
3682  if ( !$checkRights || $this->isAllowed( 'editmywatchlist' ) ) {
3683  $store = MediaWikiServices::getInstance()->getWatchedItemStore();
3684  $store->removeWatch( $this, $title->getSubjectPage() );
3685  $store->removeWatch( $this, $title->getTalkPage() );
3686  }
3687  $this->invalidateCache();
3688  }
3689 
3698  public function clearNotification( &$title, $oldid = 0 ) {
3700 
3701  // Do nothing if the database is locked to writes
3702  if ( wfReadOnly() ) {
3703  return;
3704  }
3705 
3706  // Do nothing if not allowed to edit the watchlist
3707  if ( !$this->isAllowed( 'editmywatchlist' ) ) {
3708  return;
3709  }
3710 
3711  // If we're working on user's talk page, we should update the talk page message indicator
3712  if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
3713  // Avoid PHP 7.1 warning of passing $this by reference
3714  $user = $this;
3715  if ( !Hooks::run( 'UserClearNewTalkNotification', [ &$user, $oldid ] ) ) {
3716  return;
3717  }
3718 
3719  // Try to update the DB post-send and only if needed...
3720  DeferredUpdates::addCallableUpdate( function () use ( $title, $oldid ) {
3721  if ( !$this->getNewtalk() ) {
3722  return; // no notifications to clear
3723  }
3724 
3725  // Delete the last notifications (they stack up)
3726  $this->setNewtalk( false );
3727 
3728  // If there is a new, unseen, revision, use its timestamp
3729  if ( $oldid ) {
3730  $rl = MediaWikiServices::getInstance()->getRevisionLookup();
3731  $oldRev = $rl->getRevisionById( $oldid, Title::READ_LATEST );
3732  if ( $oldRev ) {
3733  $newRev = $rl->getNextRevision( $oldRev );
3734  if ( $newRev ) {
3735  // TODO: actually no need to wrap in a revision,
3736  // setNewtalk really only needs a RevRecord
3737  $this->setNewtalk( true, new Revision( $newRev ) );
3738  }
3739  }
3740  }
3741  } );
3742  }
3743 
3744  if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
3745  return;
3746  }
3747 
3748  if ( $this->isAnon() ) {
3749  // Nothing else to do...
3750  return;
3751  }
3752 
3753  // Only update the timestamp if the page is being watched.
3754  // The query to find out if it is watched is cached both in memcached and per-invocation,
3755  // and when it does have to be executed, it can be on a replica DB
3756  // If this is the user's newtalk page, we always update the timestamp
3757  $force = '';
3758  if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
3759  $force = 'force';
3760  }
3761 
3762  MediaWikiServices::getInstance()->getWatchedItemStore()
3763  ->resetNotificationTimestamp( $this, $title, $force, $oldid );
3764  }
3765 
3772  public function clearAllNotifications() {
3774  // Do nothing if not allowed to edit the watchlist
3775  if ( wfReadOnly() || !$this->isAllowed( 'editmywatchlist' ) ) {
3776  return;
3777  }
3778 
3779  if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
3780  $this->setNewtalk( false );
3781  return;
3782  }
3783 
3784  $id = $this->getId();
3785  if ( !$id ) {
3786  return;
3787  }
3788 
3789  $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
3790  $watchedItemStore->resetAllNotificationTimestampsForUser( $this );
3791 
3792  // We also need to clear here the "you have new message" notification for the own
3793  // user_talk page; it's cleared one page view later in WikiPage::doViewUpdates().
3794  }
3795 
3801  public function getExperienceLevel() {
3802  global $wgLearnerEdits,
3806 
3807  if ( $this->isAnon() ) {
3808  return false;
3809  }
3810 
3811  $editCount = $this->getEditCount();
3812  $registration = $this->getRegistration();
3813  $now = time();
3814  $learnerRegistration = wfTimestamp( TS_MW, $now - $wgLearnerMemberSince * 86400 );
3815  $experiencedRegistration = wfTimestamp( TS_MW, $now - $wgExperiencedUserMemberSince * 86400 );
3816 
3817  if ( $editCount < $wgLearnerEdits ||
3818  $registration > $learnerRegistration ) {
3819  return 'newcomer';
3820  }
3821 
3822  if ( $editCount > $wgExperiencedUserEdits &&
3823  $registration <= $experiencedRegistration
3824  ) {
3825  return 'experienced';
3826  }
3827 
3828  return 'learner';
3829  }
3830 
3839  public function setCookies( $request = null, $secure = null, $rememberMe = false ) {
3840  $this->load();
3841  if ( $this->mId == 0 ) {
3842  return;
3843  }
3844 
3845  $session = $this->getRequest()->getSession();
3846  if ( $request && $session->getRequest() !== $request ) {
3847  $session = $session->sessionWithRequest( $request );
3848  }
3849  $delay = $session->delaySave();
3850 
3851  if ( !$session->getUser()->equals( $this ) ) {
3852  if ( !$session->canSetUser() ) {
3854  ->warning( __METHOD__ .
3855  ": Cannot save user \"$this\" to a user \"{$session->getUser()}\"'s immutable session"
3856  );
3857  return;
3858  }
3859  $session->setUser( $this );
3860  }
3861 
3862  $session->setRememberUser( $rememberMe );
3863  if ( $secure !== null ) {
3864  $session->setForceHTTPS( $secure );
3865  }
3866 
3867  $session->persist();
3868 
3869  ScopedCallback::consume( $delay );
3870  }
3871 
3875  public function logout() {
3876  // Avoid PHP 7.1 warning of passing $this by reference
3877  $user = $this;
3878  if ( Hooks::run( 'UserLogout', [ &$user ] ) ) {
3879  $this->doLogout();
3880  }
3881  }
3882 
3887  public function doLogout() {
3888  $session = $this->getRequest()->getSession();
3889  if ( !$session->canSetUser() ) {
3891  ->warning( __METHOD__ . ": Cannot log out of an immutable session" );
3892  $error = 'immutable';
3893  } elseif ( !$session->getUser()->equals( $this ) ) {
3895  ->warning( __METHOD__ .
3896  ": Cannot log user \"$this\" out of a user \"{$session->getUser()}\"'s session"
3897  );
3898  // But we still may as well make this user object anon
3899  $this->clearInstanceCache( 'defaults' );
3900  $error = 'wronguser';
3901  } else {
3902  $this->clearInstanceCache( 'defaults' );
3903  $delay = $session->delaySave();
3904  $session->unpersist(); // Clear cookies (T127436)
3905  $session->setLoggedOutTimestamp( time() );
3906  $session->setUser( new User );
3907  $session->set( 'wsUserID', 0 ); // Other code expects this
3908  $session->resetAllTokens();
3909  ScopedCallback::consume( $delay );
3910  $error = false;
3911  }
3912  \MediaWiki\Logger\LoggerFactory::getInstance( 'authevents' )->info( 'Logout', [
3913  'event' => 'logout',
3914  'successful' => $error === false,
3915  'status' => $error ?: 'success',
3916  ] );
3917  }
3918 
3923  public function saveSettings() {
3924  if ( wfReadOnly() ) {
3925  // @TODO: caller should deal with this instead!
3926  // This should really just be an exception.
3928  null,
3929  "Could not update user with ID '{$this->mId}'; DB is read-only."
3930  ) );
3931  return;
3932  }
3933 
3934  $this->load();
3935  if ( $this->mId == 0 ) {
3936  return; // anon
3937  }
3938 
3939  // Get a new user_touched that is higher than the old one.
3940  // This will be used for a CAS check as a last-resort safety
3941  // check against race conditions and replica DB lag.
3942  $newTouched = $this->newTouchedTimestamp();
3943 
3944  $dbw = wfGetDB( DB_MASTER );
3945  $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) use ( $newTouched ) {
3946  $dbw->update( 'user',
3947  [ /* SET */
3948  'user_name' => $this->mName,
3949  'user_real_name' => $this->mRealName,
3950  'user_email' => $this->mEmail,
3951  'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
3952  'user_touched' => $dbw->timestamp( $newTouched ),
3953  'user_token' => strval( $this->mToken ),
3954  'user_email_token' => $this->mEmailToken,
3955  'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
3956  ], $this->makeUpdateConditions( $dbw, [ /* WHERE */
3957  'user_id' => $this->mId,
3958  ] ), $fname
3959  );
3960 
3961  if ( !$dbw->affectedRows() ) {
3962  // Maybe the problem was a missed cache update; clear it to be safe
3963  $this->clearSharedCache( 'refresh' );
3964  // User was changed in the meantime or loaded with stale data
3965  $from = ( $this->queryFlagsUsed & self::READ_LATEST ) ? 'master' : 'replica';
3966  LoggerFactory::getInstance( 'preferences' )->warning(
3967  "CAS update failed on user_touched for user ID '{user_id}' ({db_flag} read)",
3968  [ 'user_id' => $this->mId, 'db_flag' => $from ]
3969  );
3970  throw new MWException( "CAS update failed on user_touched. " .
3971  "The version of the user to be saved is older than the current version."
3972  );
3973  }
3974 
3975  $dbw->update(
3976  'actor',
3977  [ 'actor_name' => $this->mName ],
3978  [ 'actor_user' => $this->mId ],
3979  $fname
3980  );
3981  } );
3982 
3983  $this->mTouched = $newTouched;
3984  $this->saveOptions();
3985 
3986  Hooks::run( 'UserSaveSettings', [ $this ] );
3987  $this->clearSharedCache( 'changed' );
3988  $this->getUserPage()->purgeSquid();
3989  }
3990 
3997  public function idForName( $flags = 0 ) {
3998  $s = trim( $this->getName() );
3999  if ( $s === '' ) {
4000  return 0;
4001  }
4002 
4003  $db = ( ( $flags & self::READ_LATEST ) == self::READ_LATEST )
4004  ? wfGetDB( DB_MASTER )
4005  : wfGetDB( DB_REPLICA );
4006 
4007  $options = ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING )
4008  ? [ 'LOCK IN SHARE MODE' ]
4009  : [];
4010 
4011  $id = $db->selectField( 'user',
4012  'user_id', [ 'user_name' => $s ], __METHOD__, $options );
4013 
4014  return (int)$id;
4015  }
4016 
4032  public static function createNew( $name, $params = [] ) {
4033  foreach ( [ 'password', 'newpassword', 'newpass_time', 'password_expires' ] as $field ) {
4034  if ( isset( $params[$field] ) ) {
4035  wfDeprecated( __METHOD__ . " with param '$field'", '1.27' );
4036  unset( $params[$field] );
4037  }
4038  }
4039 
4040  $user = new User;
4041  $user->load();
4042  $user->setToken(); // init token
4043  if ( isset( $params['options'] ) ) {
4044  $user->mOptions = $params['options'] + (array)$user->mOptions;
4045  unset( $params['options'] );
4046  }
4047  $dbw = wfGetDB( DB_MASTER );
4048 
4049  $noPass = PasswordFactory::newInvalidPassword()->toString();
4050 
4051  $fields = [
4052  'user_name' => $name,
4053  'user_password' => $noPass,
4054  'user_newpassword' => $noPass,
4055  'user_email' => $user->mEmail,
4056  'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
4057  'user_real_name' => $user->mRealName,
4058  'user_token' => strval( $user->mToken ),
4059  'user_registration' => $dbw->timestamp( $user->mRegistration ),
4060  'user_editcount' => 0,
4061  'user_touched' => $dbw->timestamp( $user->newTouchedTimestamp() ),
4062  ];
4063  foreach ( $params as $name => $value ) {
4064  $fields["user_$name"] = $value;
4065  }
4066 
4067  return $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) use ( $fields ) {
4068  $dbw->insert( 'user', $fields, $fname, [ 'IGNORE' ] );
4069  if ( $dbw->affectedRows() ) {
4070  $newUser = self::newFromId( $dbw->insertId() );
4071  $newUser->mName = $fields['user_name'];
4072  $newUser->updateActorId( $dbw );
4073  // Load the user from master to avoid replica lag
4074  $newUser->load( self::READ_LATEST );
4075  } else {
4076  $newUser = null;
4077  }
4078  return $newUser;
4079  } );
4080  }
4081 
4108  public function addToDatabase() {
4109  $this->load();
4110  if ( !$this->mToken ) {
4111  $this->setToken(); // init token
4112  }
4113 
4114  if ( !is_string( $this->mName ) ) {
4115  throw new RuntimeException( "User name field is not set." );
4116  }
4117 
4118  $this->mTouched = $this->newTouchedTimestamp();
4119 
4120  $dbw = wfGetDB( DB_MASTER );
4121  $status = $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) {
4122  $noPass = PasswordFactory::newInvalidPassword()->toString();
4123  $dbw->insert( 'user',
4124  [
4125  'user_name' => $this->mName,
4126  'user_password' => $noPass,
4127  'user_newpassword' => $noPass,
4128  'user_email' => $this->mEmail,
4129  'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
4130  'user_real_name' => $this->mRealName,
4131  'user_token' => strval( $this->mToken ),
4132  'user_registration' => $dbw->timestamp( $this->mRegistration ),
4133  'user_editcount' => 0,
4134  'user_touched' => $dbw->timestamp( $this->mTouched ),
4135  ], $fname,
4136  [ 'IGNORE' ]
4137  );
4138  if ( !$dbw->affectedRows() ) {
4139  // Use locking reads to bypass any REPEATABLE-READ snapshot.
4140  $this->mId = $dbw->selectField(
4141  'user',
4142  'user_id',
4143  [ 'user_name' => $this->mName ],
4144  $fname,
4145  [ 'LOCK IN SHARE MODE' ]
4146  );
4147  $loaded = false;
4148  if ( $this->mId && $this->loadFromDatabase( self::READ_LOCKING ) ) {
4149  $loaded = true;
4150  }
4151  if ( !$loaded ) {
4152  throw new MWException( $fname . ": hit a key conflict attempting " .
4153  "to insert user '{$this->mName}' row, but it was not present in select!" );
4154  }
4155  return Status::newFatal( 'userexists' );
4156  }
4157  $this->mId = $dbw->insertId();
4158  self::$idCacheByName[$this->mName] = $this->mId;
4159  $this->updateActorId( $dbw );
4160 
4161  return Status::newGood();
4162  } );
4163  if ( !$status->isGood() ) {
4164  return $status;
4165  }
4166 
4167  // Clear instance cache other than user table data and actor, which is already accurate
4168  $this->clearInstanceCache();
4169 
4170  $this->saveOptions();
4171  return Status::newGood();
4172  }
4173 
4178  private function updateActorId( IDatabase $dbw ) {
4179  $dbw->insert(
4180  'actor',
4181  [ 'actor_user' => $this->mId, 'actor_name' => $this->mName ],
4182  __METHOD__
4183  );
4184  $this->mActorId = (int)$dbw->insertId();
4185  }
4186 
4192  public function spreadAnyEditBlock() {
4193  if ( $this->isLoggedIn() && $this->getBlock() ) {
4194  return $this->spreadBlock();
4195  }
4196 
4197  return false;
4198  }
4199 
4205  protected function spreadBlock() {
4206  wfDebug( __METHOD__ . "()\n" );
4207  $this->load();
4208  if ( $this->mId == 0 ) {
4209  return false;
4210  }
4211 
4212  $userblock = DatabaseBlock::newFromTarget( $this->getName() );
4213  if ( !$userblock ) {
4214  return false;
4215  }
4216 
4217  return (bool)$userblock->doAutoblock( $this->getRequest()->getIP() );
4218  }
4219 
4224  public function isBlockedFromCreateAccount() {
4225  $this->getBlockedStatus();
4226  if ( $this->mBlock && $this->mBlock->appliesToRight( 'createaccount' ) ) {
4227  return $this->mBlock;
4228  }
4229 
4230  # T15611: if the IP address the user is trying to create an account from is
4231  # blocked with createaccount disabled, prevent new account creation there even
4232  # when the user is logged in
4233  if ( $this->mBlockedFromCreateAccount === false && !$this->isAllowed( 'ipblock-exempt' ) ) {
4234  $this->mBlockedFromCreateAccount = DatabaseBlock::newFromTarget(
4235  null, $this->getRequest()->getIP()
4236  );
4237  }
4238  return $this->mBlockedFromCreateAccount instanceof AbstractBlock
4239  && $this->mBlockedFromCreateAccount->appliesToRight( 'createaccount' )
4240  ? $this->mBlockedFromCreateAccount
4241  : false;
4242  }
4243 
4248  public function isBlockedFromEmailuser() {
4249  $this->getBlockedStatus();
4250  return $this->mBlock && $this->mBlock->appliesToRight( 'sendemail' );
4251  }
4252 
4259  public function isBlockedFromUpload() {
4260  $this->getBlockedStatus();
4261  return $this->mBlock && $this->mBlock->appliesToRight( 'upload' );
4262  }
4263 
4268  public function isAllowedToCreateAccount() {
4269  return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
4270  }
4271 
4277  public function getUserPage() {
4278  return Title::makeTitle( NS_USER, $this->getName() );
4279  }
4280 
4286  public function getTalkPage() {
4287  $title = $this->getUserPage();
4288  return $title->getTalkPage();
4289  }
4290 
4296  public function isNewbie() {
4297  return !$this->isAllowed( 'autoconfirmed' );
4298  }
4299 
4306  public function checkPassword( $password ) {
4307  wfDeprecated( __METHOD__, '1.27' );
4308 
4309  $manager = AuthManager::singleton();
4310  $reqs = AuthenticationRequest::loadRequestsFromSubmission(
4311  $manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN ),
4312  [
4313  'username' => $this->getName(),
4314  'password' => $password,
4315  ]
4316  );
4317  $res = $manager->beginAuthentication( $reqs, 'null:' );
4318  switch ( $res->status ) {
4319  case AuthenticationResponse::PASS:
4320  return true;
4321  case AuthenticationResponse::FAIL:
4322  // Hope it's not a PreAuthenticationProvider that failed...
4323  LoggerFactory::getInstance( 'authentication' )
4324  ->info( __METHOD__ . ': Authentication failed: ' . $res->message->plain() );
4325  return false;
4326  default:
4327  throw new BadMethodCallException(
4328  'AuthManager returned a response unsupported by ' . __METHOD__
4329  );
4330  }
4331  }
4332 
4341  public function checkTemporaryPassword( $plaintext ) {
4342  wfDeprecated( __METHOD__, '1.27' );
4343  // Can't check the temporary password individually.
4344  return $this->checkPassword( $plaintext );
4345  }
4346 
4358  public function getEditTokenObject( $salt = '', $request = null ) {
4359  if ( $this->isAnon() ) {
4360  return new LoggedOutEditToken();
4361  }
4362 
4363  if ( !$request ) {
4364  $request = $this->getRequest();
4365  }
4366  return $request->getSession()->getToken( $salt );
4367  }
4368 
4382  public function getEditToken( $salt = '', $request = null ) {
4383  return $this->getEditTokenObject( $salt, $request )->toString();
4384  }
4385 
4398  public function matchEditToken( $val, $salt = '', $request = null, $maxage = null ) {
4399  return $this->getEditTokenObject( $salt, $request )->match( $val, $maxage );
4400  }
4401 
4412  public function matchEditTokenNoSuffix( $val, $salt = '', $request = null, $maxage = null ) {
4413  $val = substr( $val, 0, strspn( $val, '0123456789abcdef' ) ) . Token::SUFFIX;
4414  return $this->matchEditToken( $val, $salt, $request, $maxage );
4415  }
4416 
4424  public function sendConfirmationMail( $type = 'created' ) {
4425  global $wgLang;
4426  $expiration = null; // gets passed-by-ref and defined in next line.
4427  $token = $this->confirmationToken( $expiration );
4428  $url = $this->confirmationTokenUrl( $token );
4429  $invalidateURL = $this->invalidationTokenUrl( $token );
4430  $this->saveSettings();
4431 
4432  if ( $type == 'created' || $type === false ) {
4433  $message = 'confirmemail_body';
4434  $type = 'created';
4435  } elseif ( $type === true ) {
4436  $message = 'confirmemail_body_changed';
4437  $type = 'changed';
4438  } else {
4439  // Messages: confirmemail_body_changed, confirmemail_body_set
4440  $message = 'confirmemail_body_' . $type;
4441  }
4442 
4443  $mail = [
4444  'subject' => wfMessage( 'confirmemail_subject' )->text(),
4445  'body' => wfMessage( $message,
4446  $this->getRequest()->getIP(),
4447  $this->getName(),
4448  $url,
4449  $wgLang->userTimeAndDate( $expiration, $this ),
4450  $invalidateURL,
4451  $wgLang->userDate( $expiration, $this ),
4452  $wgLang->userTime( $expiration, $this ) )->text(),
4453  'from' => null,
4454  'replyTo' => null,
4455  ];
4456  $info = [
4457  'type' => $type,
4458  'ip' => $this->getRequest()->getIP(),
4459  'confirmURL' => $url,
4460  'invalidateURL' => $invalidateURL,
4461  'expiration' => $expiration
4462  ];
4463 
4464  Hooks::run( 'UserSendConfirmationMail', [ $this, &$mail, $info ] );
4465  return $this->sendMail( $mail['subject'], $mail['body'], $mail['from'], $mail['replyTo'] );
4466  }
4467 
4479  public function sendMail( $subject, $body, $from = null, $replyto = null ) {
4480  global $wgPasswordSender;
4481 
4482  if ( $from instanceof User ) {
4483  $sender = MailAddress::newFromUser( $from );
4484  } else {
4485  $sender = new MailAddress( $wgPasswordSender,
4486  wfMessage( 'emailsender' )->inContentLanguage()->text() );
4487  }
4488  $to = MailAddress::newFromUser( $this );
4489 
4490  return UserMailer::send( $to, $sender, $subject, $body, [
4491  'replyTo' => $replyto,
4492  ] );
4493  }
4494 
4505  protected function confirmationToken( &$expiration ) {
4507  $now = time();
4508  $expires = $now + $wgUserEmailConfirmationTokenExpiry;
4509  $expiration = wfTimestamp( TS_MW, $expires );
4510  $this->load();
4511  $token = MWCryptRand::generateHex( 32 );
4512  $hash = md5( $token );
4513  $this->mEmailToken = $hash;
4514  $this->mEmailTokenExpires = $expiration;
4515  return $token;
4516  }
4517 
4523  protected function confirmationTokenUrl( $token ) {
4524  return $this->getTokenUrl( 'ConfirmEmail', $token );
4525  }
4526 
4532  protected function invalidationTokenUrl( $token ) {
4533  return $this->getTokenUrl( 'InvalidateEmail', $token );
4534  }
4535 
4550  protected function getTokenUrl( $page, $token ) {
4551  // Hack to bypass localization of 'Special:'
4552  $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" );
4553  return $title->getCanonicalURL();
4554  }
4555 
4563  public function confirmEmail() {
4564  // Check if it's already confirmed, so we don't touch the database
4565  // and fire the ConfirmEmailComplete hook on redundant confirmations.
4566  if ( !$this->isEmailConfirmed() ) {
4568  Hooks::run( 'ConfirmEmailComplete', [ $this ] );
4569  }
4570  return true;
4571  }
4572 
4580  public function invalidateEmail() {
4581  $this->load();
4582  $this->mEmailToken = null;
4583  $this->mEmailTokenExpires = null;
4584  $this->setEmailAuthenticationTimestamp( null );
4585  $this->mEmail = '';
4586  Hooks::run( 'InvalidateEmailComplete', [ $this ] );
4587  return true;
4588  }
4589 
4594  public function setEmailAuthenticationTimestamp( $timestamp ) {
4595  $this->load();
4596  $this->mEmailAuthenticated = $timestamp;
4597  Hooks::run( 'UserSetEmailAuthenticationTimestamp', [ $this, &$this->mEmailAuthenticated ] );
4598  }
4599 
4605  public function canSendEmail() {
4607  if ( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
4608  return false;
4609  }
4610  $canSend = $this->isEmailConfirmed();
4611  // Avoid PHP 7.1 warning of passing $this by reference
4612  $user = $this;
4613  Hooks::run( 'UserCanSendEmail', [ &$user, &$canSend ] );
4614  return $canSend;
4615  }
4616 
4622  public function canReceiveEmail() {
4623  return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
4624  }
4625 
4636  public function isEmailConfirmed() {
4637  global $wgEmailAuthentication;
4638  $this->load();
4639  // Avoid PHP 7.1 warning of passing $this by reference
4640  $user = $this;
4641  $confirmed = true;
4642  if ( Hooks::run( 'EmailConfirmed', [ &$user, &$confirmed ] ) ) {
4643  if ( $this->isAnon() ) {
4644  return false;
4645  }
4646  if ( !Sanitizer::validateEmail( $this->mEmail ) ) {
4647  return false;
4648  }
4649  if ( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) {
4650  return false;
4651  }
4652  return true;
4653  }
4654 
4655  return $confirmed;
4656  }
4657 
4662  public function isEmailConfirmationPending() {
4663  global $wgEmailAuthentication;
4664  return $wgEmailAuthentication &&
4665  !$this->isEmailConfirmed() &&
4666  $this->mEmailToken &&
4667  $this->mEmailTokenExpires > wfTimestamp();
4668  }
4669 
4677  public function getRegistration() {
4678  if ( $this->isAnon() ) {
4679  return false;
4680  }
4681  $this->load();
4682  return $this->mRegistration;
4683  }
4684 
4691  public function getFirstEditTimestamp() {
4692  return $this->getEditTimestamp( true );
4693  }
4694 
4702  public function getLatestEditTimestamp() {
4703  return $this->getEditTimestamp( false );
4704  }
4705 
4713  private function getEditTimestamp( $first ) {
4714  if ( $this->getId() == 0 ) {
4715  return false; // anons
4716  }
4717  $dbr = wfGetDB( DB_REPLICA );
4718  $actorWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $this );
4719  $tsField = isset( $actorWhere['tables']['temp_rev_user'] )
4720  ? 'revactor_timestamp' : 'rev_timestamp';
4721  $sortOrder = $first ? 'ASC' : 'DESC';
4722  $time = $dbr->selectField(
4723  [ 'revision' ] + $actorWhere['tables'],
4724  $tsField,
4725  [ $actorWhere['conds'] ],
4726  __METHOD__,
4727  [ 'ORDER BY' => "$tsField $sortOrder" ],
4728  $actorWhere['joins']
4729  );
4730  if ( !$time ) {
4731  return false; // no edits
4732  }
4733  return wfTimestamp( TS_MW, $time );
4734  }
4735 
4745  public static function getGroupPermissions( $groups ) {
4746  return MediaWikiServices::getInstance()->getPermissionManager()->getGroupPermissions( $groups );
4747  }
4748 
4758  public static function getGroupsWithPermission( $role ) {
4759  return MediaWikiServices::getInstance()->getPermissionManager()->getGroupsWithPermission( $role );
4760  }
4761 
4777  public static function groupHasPermission( $group, $role ) {
4778  return MediaWikiServices::getInstance()->getPermissionManager()
4779  ->groupHasPermission( $group, $role );
4780  }
4781 
4800  public static function isEveryoneAllowed( $right ) {
4801  return MediaWikiServices::getInstance()->getPermissionManager()->isEveryoneAllowed( $right );
4802  }
4803 
4810  public static function getAllGroups() {
4812  return array_values( array_diff(
4813  array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
4814  self::getImplicitGroups()
4815  ) );
4816  }
4817 
4825  public static function getAllRights() {
4826  return MediaWikiServices::getInstance()->getPermissionManager()->getAllPermissions();
4827  }
4828 
4835  public static function getImplicitGroups() {
4836  global $wgImplicitGroups;
4837  return $wgImplicitGroups;
4838  }
4839 
4849  public static function changeableByGroup( $group ) {
4851 
4852  $groups = [
4853  'add' => [],
4854  'remove' => [],
4855  'add-self' => [],
4856  'remove-self' => []
4857  ];
4858 
4859  if ( empty( $wgAddGroups[$group] ) ) {
4860  // Don't add anything to $groups
4861  } elseif ( $wgAddGroups[$group] === true ) {
4862  // You get everything
4863  $groups['add'] = self::getAllGroups();
4864  } elseif ( is_array( $wgAddGroups[$group] ) ) {
4865  $groups['add'] = $wgAddGroups[$group];
4866  }
4867 
4868  // Same thing for remove
4869  if ( empty( $wgRemoveGroups[$group] ) ) {
4870  // Do nothing
4871  } elseif ( $wgRemoveGroups[$group] === true ) {
4872  $groups['remove'] = self::getAllGroups();
4873  } elseif ( is_array( $wgRemoveGroups[$group] ) ) {
4874  $groups['remove'] = $wgRemoveGroups[$group];
4875  }
4876 
4877  // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
4878  if ( empty( $wgGroupsAddToSelf['user'] ) || $wgGroupsAddToSelf['user'] !== true ) {
4879  foreach ( $wgGroupsAddToSelf as $key => $value ) {
4880  if ( is_int( $key ) ) {
4881  $wgGroupsAddToSelf['user'][] = $value;
4882  }
4883  }
4884  }
4885 
4886  if ( empty( $wgGroupsRemoveFromSelf['user'] ) || $wgGroupsRemoveFromSelf['user'] !== true ) {
4887  foreach ( $wgGroupsRemoveFromSelf as $key => $value ) {
4888  if ( is_int( $key ) ) {
4889  $wgGroupsRemoveFromSelf['user'][] = $value;
4890  }
4891  }
4892  }
4893 
4894  // Now figure out what groups the user can add to him/herself
4895  if ( empty( $wgGroupsAddToSelf[$group] ) ) {
4896  // Do nothing
4897  } elseif ( $wgGroupsAddToSelf[$group] === true ) {
4898  // No idea WHY this would be used, but it's there
4899  $groups['add-self'] = self::getAllGroups();
4900  } elseif ( is_array( $wgGroupsAddToSelf[$group] ) ) {
4901  $groups['add-self'] = $wgGroupsAddToSelf[$group];
4902  }
4903 
4904  if ( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
4905  // Do nothing
4906  } elseif ( $wgGroupsRemoveFromSelf[$group] === true ) {
4907  $groups['remove-self'] = self::getAllGroups();
4908  } elseif ( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
4909  $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
4910  }
4911 
4912  return $groups;
4913  }
4914 
4922  public function changeableGroups() {
4923  if ( $this->isAllowed( 'userrights' ) ) {
4924  // This group gives the right to modify everything (reverse-
4925  // compatibility with old "userrights lets you change
4926  // everything")
4927  // Using array_merge to make the groups reindexed
4928  $all = array_merge( self::getAllGroups() );
4929  return [
4930  'add' => $all,
4931  'remove' => $all,
4932  'add-self' => [],
4933  'remove-self' => []
4934  ];
4935  }
4936 
4937  // Okay, it's not so simple, we will have to go through the arrays
4938  $groups = [
4939  'add' => [],
4940  'remove' => [],
4941  'add-self' => [],
4942  'remove-self' => []
4943  ];
4944  $addergroups = $this->getEffectiveGroups();
4945 
4946  foreach ( $addergroups as $addergroup ) {
4947  $groups = array_merge_recursive(
4948  $groups, $this->changeableByGroup( $addergroup )
4949  );
4950  $groups['add'] = array_unique( $groups['add'] );
4951  $groups['remove'] = array_unique( $groups['remove'] );
4952  $groups['add-self'] = array_unique( $groups['add-self'] );
4953  $groups['remove-self'] = array_unique( $groups['remove-self'] );
4954  }
4955  return $groups;
4956  }
4957 
4961  public function incEditCount() {
4962  if ( $this->isAnon() ) {
4963  return; // sanity
4964  }
4965 
4967  new UserEditCountUpdate( $this, 1 ),
4969  );
4970  }
4971 
4977  public function setEditCountInternal( $count ) {
4978  $this->mEditCount = $count;
4979  }
4980 
4988  public function initEditCountInternal( IDatabase $dbr ) {
4989  // Pull from a replica DB to be less cruel to servers
4990  // Accuracy isn't the point anyway here
4991  $actorWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $this );
4992  $count = (int)$dbr->selectField(
4993  [ 'revision' ] + $actorWhere['tables'],
4994  'COUNT(*)',
4995  [ $actorWhere['conds'] ],
4996  __METHOD__,
4997  [],
4998  $actorWhere['joins']
4999  );
5000 
5001  $dbw = wfGetDB( DB_MASTER );
5002  $dbw->update(
5003  'user',
5004  [ 'user_editcount' => $count ],
5005  [
5006  'user_id' => $this->getId(),
5007  'user_editcount IS NULL OR user_editcount < ' . (int)$count
5008  ],
5009  __METHOD__
5010  );
5011 
5012  return $count;
5013  }
5014 
5022  public static function getRightDescription( $right ) {
5023  $key = "right-$right";
5024  $msg = wfMessage( $key );
5025  return $msg->isDisabled() ? $right : $msg->text();
5026  }
5027 
5035  public static function getGrantName( $grant ) {
5036  $key = "grant-$grant";
5037  $msg = wfMessage( $key );
5038  return $msg->isDisabled() ? $grant : $msg->text();
5039  }
5040 
5061  public function addNewUserLogEntry( $action = false, $reason = '' ) {
5062  return true; // disabled
5063  }
5064 
5073  public function addNewUserLogEntryAutoCreate() {
5074  wfDeprecated( __METHOD__, '1.27' );
5075  $this->addNewUserLogEntry( 'autocreate' );
5076 
5077  return true;
5078  }
5079 
5085  protected function loadOptions( $data = null ) {
5086  $this->load();
5087 
5088  if ( $this->mOptionsLoaded ) {
5089  return;
5090  }
5091 
5092  $this->mOptions = self::getDefaultOptions();
5093 
5094  if ( !$this->getId() ) {
5095  // For unlogged-in users, load language/variant options from request.
5096  // There's no need to do it for logged-in users: they can set preferences,
5097  // and handling of page content is done by $pageLang->getPreferredVariant() and such,
5098  // so don't override user's choice (especially when the user chooses site default).
5099  $variant = MediaWikiServices::getInstance()->getContentLanguage()->getDefaultVariant();
5100  $this->mOptions['variant'] = $variant;
5101  $this->mOptions['language'] = $variant;
5102  $this->mOptionsLoaded = true;
5103  return;
5104  }
5105 
5106  // Maybe load from the object
5107  if ( !is_null( $this->mOptionOverrides ) ) {
5108  wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" );
5109  foreach ( $this->mOptionOverrides as $key => $value ) {
5110  $this->mOptions[$key] = $value;
5111  }
5112  } else {
5113  if ( !is_array( $data ) ) {
5114  wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" );
5115  // Load from database
5116  $dbr = ( $this->queryFlagsUsed & self::READ_LATEST )
5117  ? wfGetDB( DB_MASTER )
5118  : wfGetDB( DB_REPLICA );
5119 
5120  $res = $dbr->select(
5121  'user_properties',
5122  [ 'up_property', 'up_value' ],
5123  [ 'up_user' => $this->getId() ],
5124  __METHOD__
5125  );
5126 
5127  $this->mOptionOverrides = [];
5128  $data = [];
5129  foreach ( $res as $row ) {
5130  // Convert '0' to 0. PHP's boolean conversion considers them both
5131  // false, but e.g. JavaScript considers the former as true.
5132  // @todo: T54542 Somehow determine the desired type (string/int/bool)
5133  // and convert all values here.
5134  if ( $row->up_value === '0' ) {
5135  $row->up_value = 0;
5136  }
5137  $data[$row->up_property] = $row->up_value;
5138  }
5139  }
5140 
5141  foreach ( $data as $property => $value ) {
5142  $this->mOptionOverrides[$property] = $value;
5143  $this->mOptions[$property] = $value;
5144  }
5145  }
5146 
5147  // Replace deprecated language codes
5148  $this->mOptions['language'] = LanguageCode::replaceDeprecatedCodes(
5149  $this->mOptions['language']
5150  );
5151 
5152  $this->mOptionsLoaded = true;
5153 
5154  Hooks::run( 'UserLoadOptions', [ $this, &$this->mOptions ] );
5155  }
5156 
5162  protected function saveOptions() {
5163  $this->loadOptions();
5164 
5165  // Not using getOptions(), to keep hidden preferences in database
5166  $saveOptions = $this->mOptions;
5167 
5168  // Allow hooks to abort, for instance to save to a global profile.
5169  // Reset options to default state before saving.
5170  if ( !Hooks::run( 'UserSaveOptions', [ $this, &$saveOptions ] ) ) {
5171  return;
5172  }
5173 
5174  $userId = $this->getId();
5175 
5176  $insert_rows = []; // all the new preference rows
5177  foreach ( $saveOptions as $key => $value ) {
5178  // Don't bother storing default values
5179  $defaultOption = self::getDefaultOption( $key );
5180  if ( ( $defaultOption === null && $value !== false && $value !== null )
5181  || $value != $defaultOption
5182  ) {
5183  $insert_rows[] = [
5184  'up_user' => $userId,
5185  'up_property' => $key,
5186  'up_value' => $value,
5187  ];
5188  }
5189  }
5190 
5191  $dbw = wfGetDB( DB_MASTER );
5192 
5193  $res = $dbw->select( 'user_properties',
5194  [ 'up_property', 'up_value' ], [ 'up_user' => $userId ], __METHOD__ );
5195 
5196  // Find prior rows that need to be removed or updated. These rows will
5197  // all be deleted (the latter so that INSERT IGNORE applies the new values).
5198  $keysDelete = [];
5199  foreach ( $res as $row ) {
5200  if ( !isset( $saveOptions[$row->up_property] )
5201  || strcmp( $saveOptions[$row->up_property], $row->up_value ) != 0
5202  ) {
5203  $keysDelete[] = $row->up_property;
5204  }
5205  }
5206 
5207  if ( count( $keysDelete ) ) {
5208  // Do the DELETE by PRIMARY KEY for prior rows.
5209  // In the past a very large portion of calls to this function are for setting
5210  // 'rememberpassword' for new accounts (a preference that has since been removed).
5211  // Doing a blanket per-user DELETE for new accounts with no rows in the table
5212  // caused gap locks on [max user ID,+infinity) which caused high contention since
5213  // updates would pile up on each other as they are for higher (newer) user IDs.
5214  // It might not be necessary these days, but it shouldn't hurt either.
5215  $dbw->delete( 'user_properties',
5216  [ 'up_user' => $userId, 'up_property' => $keysDelete ], __METHOD__ );
5217  }
5218  // Insert the new preference rows
5219  $dbw->insert( 'user_properties', $insert_rows, __METHOD__, [ 'IGNORE' ] );
5220  }
5221 
5228  public static function selectFields() {
5229  wfDeprecated( __METHOD__, '1.31' );
5230  return [
5231  'user_id',
5232  'user_name',
5233  'user_real_name',
5234  'user_email',
5235  'user_touched',
5236  'user_token',
5237  'user_email_authenticated',
5238  'user_email_token',
5239  'user_email_token_expires',
5240  'user_registration',
5241  'user_editcount',
5242  ];
5243  }
5244 
5254  public static function getQueryInfo() {
5255  $ret = [
5256  'tables' => [ 'user', 'user_actor' => 'actor' ],
5257  'fields' => [
5258  'user_id',
5259  'user_name',
5260  'user_real_name',
5261  'user_email',
5262  'user_touched',
5263  'user_token',
5264  'user_email_authenticated',
5265  'user_email_token',
5266  'user_email_token_expires',
5267  'user_registration',
5268  'user_editcount',
5269  'user_actor.actor_id',
5270  ],
5271  'joins' => [
5272  'user_actor' => [ 'JOIN', 'user_actor.actor_user = user_id' ],
5273  ],
5274  ];
5275 
5276  return $ret;
5277  }
5278 
5286  static function newFatalPermissionDeniedStatus( $permission ) {
5287  global $wgLang;
5288 
5289  $groups = [];
5290  foreach ( MediaWikiServices::getInstance()
5291  ->getPermissionManager()
5292  ->getGroupsWithPermission( $permission ) as $group ) {
5293  $groups[] = UserGroupMembership::getLink( $group, RequestContext::getMain(), 'wiki' );
5294  }
5295 
5296  if ( $groups ) {
5297  return Status::newFatal( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) );
5298  }
5299 
5300  return Status::newFatal( 'badaccess-group0' );
5301  }
5302 
5312  public function getInstanceForUpdate() {
5313  if ( !$this->getId() ) {
5314  return null; // anon
5315  }
5316 
5317  $user = self::newFromId( $this->getId() );
5318  if ( !$user->loadFromId( self::READ_EXCLUSIVE ) ) {
5319  return null;
5320  }
5321 
5322  return $user;
5323  }
5324 
5332  public function equals( UserIdentity $user ) {
5333  // XXX it's not clear whether central ID providers are supposed to obey this
5334  return $this->getName() === $user->getName();
5335  }
5336 
5342  public function isAllowUsertalk() {
5343  return $this->mAllowUsertalk;
5344  }
5345 
5346 }
static array null $defOpt
Definition: User.php:1629
initEditCountInternal(IDatabase $dbr)
Initialize user_editcount from data out of the revision table.
Definition: User.php:4988
getEmail()
Get the user&#39;s e-mail address.
Definition: User.php:2801
$wgUserEmailConfirmationTokenExpiry
The time, in seconds, when an email confirmation email expires.
isHidden()
Check if user account is hidden.
Definition: User.php:2186
string null $wgAuthenticationTokenVersion
Versioning for authentication tokens.
static getMembershipsForUser( $userId, IDatabase $db=null)
Returns UserGroupMembership objects for all the groups a user currently belongs to.
static getMembership( $userId, $group, IDatabase $db=null)
Returns a UserGroupMembership object that pertains to the given user and group, or false if the user ...
setInternalPassword( $str)
Set the password and reset the random token unconditionally.
Definition: User.php:2664
string $mBlockedby
Definition: User.php:180
static isValidRange( $ipRange)
Validate an IP range (valid address with a valid CIDR prefix).
Definition: IP.php:125
const VERSION
Version number to tag cached versions of serialized User objects.
Definition: User.php:67
static isIPAddress( $ip)
Determine if a string is as valid IP address or network (CIDR prefix).
Definition: IP.php:77
setRealName( $str)
Set the user&#39;s real name.
Definition: User.php:2903
$wgMaxArticleSize
Maximum article size in kilobytes.
setItemLoaded( $item)
Set that an item has been loaded.
Definition: User.php:1244
getNewMessageLinks()
Return the data needed to construct links for new talk page message alerts.
Definition: User.php:2365
isAllowedAll()
Definition: User.php:3574
clearInstanceCache( $reloadFrom=false)
Clear various cached data stored in this object.
Definition: User.php:1601
string $mDatePreference
Definition: User.php:178
isNewbie()
Determine whether the user is a newbie.
Definition: User.php:4296
static whoIs( $id)
Get the username corresponding to a given user ID.
Definition: User.php:809
static normalizeKey( $key)
Normalize a skin preference value to a form that can be loaded.
Definition: Skin.php:104
addWatch( $title, $checkRights=self::CHECK_USER_RIGHTS)
Watch an article.
Definition: User.php:3664
$context
Definition: load.php:45
$wgMaxNameChars
Maximum number of bytes in username.
UserGroupMembership [] $mGroupMemberships
Associative array of (group name => UserGroupMembership object)
Definition: User.php:143
affectedRows()
Get the number of rows affected by the last write query.
$wgDefaultUserOptions
Settings added to this array will override the default globals for the user preferences used by anony...
loadFromSession()
Load user data from the session.
Definition: User.php:1255
const NS_MAIN
Definition: Defines.php:60
static changeableByGroup( $group)
Returns an array of the groups that a particular group can add/remove.
Definition: User.php:4849
isAllowedToCreateAccount()
Get whether the user is allowed to create an account.
Definition: User.php:4268
timestampOrNull( $ts=null)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
$success
logout()
Log this user out.
Definition: User.php:3875
clearNotification(&$title, $oldid=0)
Clear the user&#39;s notification timestamp for the given title.
Definition: User.php:3698
static getGroupPermissions( $groups)
Get the permissions associated with a given list of groups.
Definition: User.php:4745
static getImplicitGroups()
Get a list of implicit groups TODO: Should we deprecate this? It&#39;s trivial, but we don&#39;t want to enco...
Definition: User.php:4835
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a single field from a single result row.
saveSettings()
Save this user&#39;s settings into the database.
Definition: User.php:3923
setId( $v)
Set the user and reload all fields according to a given ID.
Definition: User.php:2223
static send( $to, $from, $subject, $body, $options=[])
This function will perform a direct (authenticated) login to a SMTP Server to use for mail relaying i...
Definition: UserMailer.php:115
getFirstEditTimestamp()
Get the timestamp of the first edit.
Definition: User.php:4691
static isEveryoneAllowed( $right)
Check if all users may be assumed to have the given permission.
Definition: User.php:4800
static isIP( $name)
Does the string match an anonymous IP address?
Definition: User.php:891
validateCache( $timestamp)
Validate the cache for this account.
Definition: User.php:2595
const GETOPTIONS_EXCLUDE_DEFAULTS
Exclude user options that are set to their default value.
Definition: User.php:73
isAllowedAny()
Check if user is allowed to access a feature / make an action.
Definition: User.php:3561
isBlockedFromEmailuser()
Get whether the user is blocked from using Special:Emailuser.
Definition: User.php:4248
getEffectiveGroups( $recache=false)
Get the list of implicit group memberships this user has.
Definition: User.php:3336
const TOKEN_LENGTH
Number of characters required for the user_token field.
Definition: User.php:56
touch()
Update the "touched" timestamp for the user.
Definition: User.php:2580
Handles increment the edit count for a given set of users.
AbstractBlock $mGlobalBlock
Definition: User.php:192
insert( $table, $a, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
isBlockedGlobally( $ip='')
Check if user is blocked on all wikis.
Definition: User.php:2124
setName( $str)
Set the user name.
Definition: User.php:2260
getOption( $oname, $defaultOverride=null, $ignoreHidden=false)
Get the user&#39;s current setting for a given option.
Definition: User.php:2918
blockedBy()
If user is blocked, return the name of the user who placed the block.
Definition: User.php:2093
array bool $mLoadedItems
Array with already loaded items or true if all items have been loaded.
Definition: User.php:157
string $mQuickTouched
TS_MW timestamp from cache.
Definition: User.php:129
$wgReservedUsernames
Array of usernames which may not be registered or logged in from Maintenance scripts can still use th...
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
& __get( $name)
Definition: User.php:240
static loadFromTimestamp( $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: Revision.php:296
if(ini_get( 'mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition: Setup.php:57
static newFromAnyId( $userId, $userName, $actorId, $dbDomain=false)
Static factory method for creation from an ID, name, and/or actor ID.
Definition: User.php:596
getBlock( $fromReplica=true)
Get the block affecting the user, or null if the user is not blocked.
Definition: User.php:2068
static getWikiIdFromDbDomain( $domain)
Get the wiki ID of a database domain.
Definition: WikiMap.php:268
getExperienceLevel()
Compute experienced level based on edit count and registration date.
Definition: User.php:3801
__toString()
Definition: User.php:236
Exception thrown when an actor can&#39;t be created.
getRealName()
Get the user&#39;s real name.
Definition: User.php:2891
$wgSecureLogin
This is to let user authenticate using https when they come from http.
checkNewtalk( $field, $id)
Internal uncached check for new messages.
Definition: User.php:2425
sendMail( $subject, $body, $from=null, $replyto=null)
Send an e-mail to this user&#39;s account.
Definition: User.php:4479
Check if a user&#39;s password complies with any password policies that apply to that user...
checkAndSetTouched()
Bump user_touched if it didn&#39;t change since this object was loaded.
Definition: User.php:1563
changeableGroups()
Returns an array of groups that this user can add and remove.
Definition: User.php:4922
clearAllNotifications()
Resets all of the given user&#39;s page-change notification timestamps.
Definition: User.php:3772
matchEditTokenNoSuffix( $val, $salt='', $request=null, $maxage=null)
Check given value against the token value stored in the session, ignoring the suffix.
Definition: User.php:4412
static string null $defOptLang
Definition: User.php:1631
isLoggedIn()
Get whether the user is logged in.
Definition: User.php:3524
static getLocalClusterInstance()
Get the main cluster-local cache object.
static getInstance( $channel)
Get a named logger instance from the currently configured logger factory.
$wgGroupsRemoveFromSelf
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
setToken( $token=false)
Set the random token (used for persistent authentication) Called from loadDefaults() among other plac...
Definition: User.php:2785
insertId()
Get the inserted value of an auto-increment row.
static idFromName( $name, $flags=self::READ_NORMAL)
Get database id given a user name.
Definition: User.php:829
static newFromConfirmationCode( $code, $flags=0)
Factory method to fetch whichever user has a given email confirmation code.
Definition: User.php:650
isBlockedFromUpload()
Get whether the user is blocked from using Special:Upload.
Definition: User.php:4259
checkTemporaryPassword( $plaintext)
Check if the given clear-text password matches the temporary password sent by e-mail for password res...
Definition: User.php:4341
$wgGroupsAddToSelf
A map of group names that the user is in, to group names that those users are allowed to add or revok...
$wgHiddenPrefs
An array of preferences to not show for the user.
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new user object...
Definition: User.php:5254
wfTimestampOrNull( $outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
static resetIdByNameCache()
Reset the cache used in idFromName().
Definition: User.php:871
array $mFormerGroups
Definition: User.php:190
getDBTouched()
Get the user_touched timestamp field (time of last DB updates)
Definition: User.php:2629
$wgRevokePermissions
Permission keys revoked from users in each group.
static newFromActorId( $id)
Static factory method for creation from a given actor ID.
Definition: User.php:554
setOption( $oname, $val)
Set the given option for a user.
Definition: User.php:3005
getGlobalBlock( $ip='')
Check if user is blocked on all wikis.
Definition: User.php:2138
getEditTimestamp( $first)
Get the timestamp of the first or latest edit.
Definition: User.php:4713
isDnsBlacklisted( $ip, $checkWhitelist=false)
Whether the given IP is in a DNS blacklist.
Definition: User.php:1777
isBlocked( $fromReplica=true)
Check if user is blocked.
Definition: User.php:2057
static getLink( $ugm, IContextSource $context, $format, $userName=null)
Gets a link for a user group, possibly including the expiry date if relevant.
$wgUseNPPatrol
Use new page patrolling to check new pages on Special:Newpages.
Value object representing a logged-out user&#39;s edit token.
const DB_MASTER
Definition: defines.php:26
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
static getSubnet( $ip)
Returns the subnet of a given IP.
Definition: IP.php:729
setCookies( $request=null, $secure=null, $rememberMe=false)
Persist this user&#39;s session (e.g.
Definition: User.php:3839
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2232
static selectFields()
Return the list of user fields that should be selected to create a new user object.
Definition: User.php:5228
updateActorId(IDatabase $dbw)
Update the actor ID after an insert.
Definition: User.php:4178
string $mName
Cache variables.
Definition: User.php:118
string $mRegistration
Cache variables.
Definition: User.php:139
$wgEnableEmail
Set to true to enable the e-mail basic features: Password reminders, etc.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
int $mEditCount
Cache variables.
Definition: User.php:141
changeAuthenticationData(array $data)
Changes credentials of the user.
Definition: User.php:2715
int null $mActorId
Cache variables.
Definition: User.php:120
removeWatch( $title, $checkRights=self::CHECK_USER_RIGHTS)
Stop watching an article.
Definition: User.php:3681
static purge( $dbDomain, $userId)
Definition: User.php:418
checkPassword( $password)
Check to see if the given clear-text password is one of the accepted passwords.
Definition: User.php:4306
getTitleKey()
Get the user&#39;s name escaped by underscores.
Definition: User.php:2319
static getAllGroups()
Return the set of defined explicit groups.
Definition: User.php:4810
static listOptionKinds()
Return a list of the types of user options currently returned by User::getOptionKinds().
Definition: User.php:3088
$wgGroupPermissions
Permission keys given to users in each group.
static findUsersByGroup( $groups, $limit=5000, $after=null)
Return the users who are members of the given group(s).
Definition: User.php:1001
static validateEmail( $addr)
Does a string look like an e-mail address?
Definition: Sanitizer.php:2165
Interface for objects representing user identity.
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
string $mEmailTokenExpires
Cache variables.
Definition: User.php:137
static getAllRights()
Get a list of all available permissions.
Definition: User.php:4825
isEmailConfirmed()
Is this user&#39;s e-mail address valid-looking and confirmed within limits of the current site configura...
Definition: User.php:4636
int bool $mNewtalk
Lazy-initialized variables, invalidated with clearInstanceCache.
Definition: User.php:176
getEditToken( $salt='', $request=null)
Initialize (if necessary) and return a session token value which can be used in edit forms to show th...
Definition: User.php:4382
static getCanonicalName( $name, $validate='valid')
Given unvalidated user input, return a canonical username, or false if the username is invalid...
Definition: User.php:1139
static newFatal( $message)
Factory function for fatal errors.
Definition: StatusValue.php:68
isBlockedFromCreateAccount()
Get whether the user is explicitly blocked from account creation.
Definition: User.php:4224
loadFromId( $flags=self::READ_NORMAL)
Load user table data, given mId has already been set.
Definition: User.php:388
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
static whoIsReal( $id)
Get the real name of a user given their user ID.
Definition: User.php:819
updateNewtalk( $field, $id, $curRev=null)
Add or update the new messages flag.
Definition: User.php:2440
string $mEmailToken
Cache variables.
Definition: User.php:135
getTokenFromOption( $oname)
Get a token stored in the preferences (like the watchlist one), resetting it if it&#39;s empty (and savin...
Definition: User.php:3026
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
addAutopromoteOnceGroups( $event)
Add the user to the group if he/she meets given criteria.
Definition: User.php:1494
$wgPasswordPolicy
Password policy for the wiki.
getRegistration()
Get the timestamp of account creation.
Definition: User.php:4677
getGroupMemberships()
Get the list of explicit group memberships this user has, stored as UserGroupMembership objects...
Definition: User.php:3323
$wgRateLimitsExcludedIPs
Array of IPs / CIDR ranges which should be excluded from rate limits.
wfReadOnly()
Check whether the wiki is in read-only mode.
$wgLang
Definition: Setup.php:864
static getRightDescription( $right)
Get the description of a given right.
Definition: User.php:5022
static newMigration()
Static constructor.
static getMain()
Get the RequestContext object associated with the main request.
static isCreatableName( $name)
Usernames which fail to pass this function will be blocked from new account registrations, but may be used internally either by batch processes or by user accounts which have already been created.
Definition: User.php:1041
confirmationTokenUrl( $token)
Return a URL the user can use to confirm their email address.
Definition: User.php:4523
getMutableCacheKeys(WANObjectCache $cache)
Definition: User.php:440
static newFromIDs( $ids)
Definition: UserArray.php:42
addNewUserLogEntryAutoCreate()
Add an autocreate newuser log entry for this user Used by things like CentralAuth and perhaps other a...
Definition: User.php:5073
$wgExperiencedUserEdits
Name of the external diff engine to use.
isValidPassword( $password)
Is the input a valid password for this user?
Definition: User.php:1071
static getGroupsWithPermission( $role)
Get all the groups who have a given permission.
Definition: User.php:4758
static sanitizeIP( $ip)
Convert an IP into a verbose, uppercase, normalized form.
Definition: IP.php:139
getBoolOption( $oname)
Get the user&#39;s current setting for a given option, as a boolean value.
Definition: User.php:2977
makeUpdateConditions(IDatabase $db, array $conditions)
Builds update conditions.
Definition: User.php:1545
string $mBlockreason
Definition: User.php:184
isAnon()
Get whether the user is anonymous.
Definition: User.php:3532
isAllowUsertalk()
Checks if usertalk is allowed.
Definition: User.php:5342
getBlockedStatus( $fromReplica=true)
Get blocking information.
Definition: User.php:1707
getTokenUrl( $page, $token)
Internal function to format the e-mail validation/invalidation URLs.
Definition: User.php:4550
$wgUseFilePatrol
Use file patrolling to check new files on Special:Newfiles.
clearSharedCache( $mode='refresh')
Clear user data from memcached.
Definition: User.php:2537
static int [] $idCacheByName
Definition: User.php:216
static isLocallyBlockedProxy( $ip)
Check if an IP address is in the local proxy list.
Definition: User.php:1838
Stores a single person&#39;s name and email address.
Definition: MailAddress.php:32
static getGrantName( $grant)
Get the name of a given grant.
Definition: User.php:5035
resetTokenFromOption( $oname)
Reset a token stored in the preferences (like the watchlist one).
Definition: User.php:3054
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
int $queryFlagsUsed
User::READ_* constant bitfield used to load data.
Definition: User.php:213
$wgImplicitGroups
Implicit groups, aren&#39;t shown on Special:Listusers or somewhere else.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
getEditTokenObject( $salt='', $request=null)
Initialize (if necessary) and return a session token value which can be used in edit forms to show th...
Definition: User.php:4358
doLogout()
Clear the user&#39;s session, and reset the instance cache.
Definition: User.php:3887
load( $flags=self::READ_NORMAL)
Load the user table data for this object from the source given by mFrom.
Definition: User.php:303
equals(UserIdentity $user)
Checks if two user objects point to the same user.
Definition: User.php:5332
loadFromCache()
Load user data from shared cache, given mId has already been set.
Definition: User.php:452
isEmailConfirmationPending()
Check whether there is an outstanding request for e-mail confirmation.
Definition: User.php:4662
incEditCount()
Schedule a deferred update to update the user&#39;s edit count.
Definition: User.php:4961
System blocks are temporary blocks that are created on enforcement (e.g.
Definition: SystemBlock.php:35
newTouchedTimestamp()
Generate a current or new-future timestamp to be stored in the user_touched field when we update thin...
Definition: User.php:2518
inDnsBlacklist( $ip, $bases)
Whether the given IP is in a given DNS blacklist.
Definition: User.php:1790
$wgShowUpdatedMarker
Show "Updated (since my last visit)" marker in RC view, watchlist and history view for watched pages ...
getOptionKinds(IContextSource $context, $options=null)
Return an associative array mapping preferences keys to the kind of a preference they&#39;re used for...
Definition: User.php:3111
loadFromDatabase( $flags=self::READ_LATEST)
Load user and user_group data from the database.
Definition: User.php:1295
$wgProxyList
Big list of banned IP addresses.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
string $mTouched
TS_MW timestamp from the DB.
Definition: User.php:127
canSendEmail()
Is this user allowed to send e-mails within limits of current site configuration? ...
Definition: User.php:4605
getLatestEditTimestamp()
Get the timestamp of the latest edit.
Definition: User.php:4702
array $mEffectiveGroups
Definition: User.php:186
$cache
Definition: mcc.php:33
setPasswordInternal( $str)
Actually set the password and such.
Definition: User.php:2677
const IGNORE_USER_RIGHTS
Definition: User.php:83
static logException( $e, $catcher=self::CAUGHT_BY_OTHER)
Log an exception to the exception log (if enabled).
string $mEmailAuthenticated
Cache variables.
Definition: User.php:133
isAllowed( $action='')
Internal mechanics of testing a permission.
Definition: User.php:3590
array $wgLearnerEdits
The following variables define 3 user experience levels:
string $mFrom
Initialization data source if mLoadedItems!==true.
Definition: User.php:170
isWatched( $title, $checkRights=self::CHECK_USER_RIGHTS)
Check the watched status of an article.
Definition: User.php:3650
WebRequest $mRequest
Definition: User.php:201
$wgRemoveGroups
static generateHex( $chars)
Generate a run of cryptographically random data and return it in hexadecimal string format...
Definition: MWCryptRand.php:36
isItemLoaded( $item, $all='all')
Return whether an item has been loaded.
Definition: User.php:1234
static getAutopromoteGroups(User $user)
Get the groups for the given user based on $wgAutopromote.
Definition: Autopromote.php:37
static isUsableName( $name)
Usernames which fail to pass this function will be blocked from user login and new account registrati...
Definition: User.php:966
trackBlockWithCookie()
Set the &#39;BlockID&#39; cookie depending on block type and user authentication status.
Definition: User.php:1284
requiresHTTPS()
Determine based on the wiki configuration and the user&#39;s options, whether this user must be over HTTP...
Definition: User.php:3261
$wgRateLimits
Simple rate limiter options to brake edit floods.
isBlockedFrom( $title, $fromReplica=false)
Check if user is blocked from editing a particular article.
Definition: User.php:2084
isRegistered()
Alias of isLoggedIn() with a name that describes its actual functionality.
Definition: User.php:3516
array $mImplicitGroups
Definition: User.php:188
loadFromUserObject( $user)
Load the data for this user object from another user object.
Definition: User.php:1460
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
spreadBlock()
If this (non-anonymous) user is blocked, block the IP address they&#39;ve successfully logged in from...
Definition: User.php:4205
static newFromUser(User $user)
Create a new MailAddress object for the given user.
Definition: MailAddress.php:66
static hasFlags( $bitfield, $flags)
$wgAutopromoteOnceLogInRC
Put user rights log entries for autopromotion in recent changes?
$wgExperiencedUserMemberSince
Name of the external diff engine to use.
isBot()
Definition: User.php:3540
$wgEnableUserEmail
Set to true to enable user-to-user e-mail.
AbstractBlock $mBlock
Definition: User.php:204
setEmailAuthenticationTimestamp( $timestamp)
Set the e-mail authentication timestamp.
Definition: User.php:4594
__set( $name, $value)
Definition: User.php:257
$wgNamespacesToBeSearchedDefault
List of namespaces which are searched by default.
setEditCountInternal( $count)
This method should not be called outside User/UserEditCountUpdate.
Definition: User.php:4977
$wgDisableAnonTalk
Disable links to talk pages of anonymous users (IPs) in listings on special pages like page history...
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:571
string $mToken
Cache variables.
Definition: User.php:131
static groupHasPermission( $group, $role)
Check, if the given group has the given permission.
Definition: User.php:4777
getToken( $forceCreation=true)
Get the user&#39;s current token.
Definition: User.php:2742
foreach( $wgExtensionFunctions as $func) if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) if(! $wgCommandLineMode) $wgFullyInitialised
Definition: Setup.php:915
static resetGetDefaultOptionsForTestsOnly()
Reset the process cache of default user options.
Definition: User.php:1639
static newInvalidPassword()
Create an InvalidPassword.
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition: User.php:3599
$wgPasswordSender
Sender email address for e-mail notifications.
appliesToRight( $right)
Determine whether the block prevents a given right.
$wgLearnerMemberSince
Name of the external diff engine to use.
array $mOptions
Definition: User.php:198
AbstractBlock bool $mBlockedFromCreateAccount
Definition: User.php:210
static getDefaultOption( $opt)
Get a given default option value.
Definition: User.php:1693
$wgEmailAuthentication
Require email authentication before sending mail to an email address.
invalidateEmail()
Invalidate the user&#39;s e-mail confirmation, and unauthenticate the e-mail address if it was already co...
Definition: User.php:4580
loadGroups()
Load the groups from the database if they aren&#39;t already loaded.
Definition: User.php:1470
saveOptions()
Saves the non-default options for this user, as previously set e.g.
Definition: User.php:5162
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:613
static string [] $mCacheVars
Array of Strings List of member variables which are saved to the shared cache (memcached).
Definition: User.php:92
isIPRange()
Is the user an IP range?
Definition: User.php:902
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:586
static getCurrentWikiDbDomain()
Definition: WikiMap.php:292
getStubThreshold()
Get the user preferred stub threshold.
Definition: User.php:3281
getRequest()
Get the WebRequest object to use with this object.
Definition: User.php:3633
string $mEmail
Cache variables.
Definition: User.php:125
getNewtalk()
Check if the user has new messages.
Definition: User.php:2327
bool $mLocked
Definition: User.php:194
static isIPv6( $ip)
Given a string, determine if it as valid IP in IPv6 only.
Definition: IP.php:88
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:539
loadFromRow( $row, $data=null)
Initialize this object from a row from the user table.
Definition: User.php:1348
$wgUseEnotif
Definition: Setup.php:438
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred list to be run later by execute()
getGroups()
Get the list of explicit group memberships this user has.
Definition: User.php:3310
__construct()
Lightweight constructor for an anonymous user.
Definition: User.php:229
static replaceDeprecatedCodes( $code)
Replace deprecated language codes that were used in previous versions of MediaWiki to up-to-date...
getIntOption( $oname, $defaultOverride=0)
Get the user&#39;s current setting for a given option, as an integer value.
Definition: User.php:2989
idForName( $flags=0)
If only this user&#39;s username is known, and it exists, return the user ID.
Definition: User.php:3997
getOptions( $flags=0)
Get all user&#39;s options.
Definition: User.php:2946
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
spreadAnyEditBlock()
If this user is logged-in and blocked, block any IP address they&#39;ve successfully logged in from...
Definition: User.php:4192
static isCurrentWikiId( $wikiId)
Definition: WikiMap.php:312
static isValidUserName( $name)
Is the input a valid username?
Definition: User.php:917
useFilePatrol()
Check whether to enable new files patrol features for this user.
Definition: User.php:3620
getId()
Get the user&#39;s ID.
Definition: User.php:2203
matchEditToken( $val, $salt='', $request=null, $maxage=null)
Check given value against the token value stored in the session.
Definition: User.php:4398
getEmailAuthenticationTimestamp()
Get the timestamp of the user&#39;s e-mail authentication.
Definition: User.php:2811
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition: User.php:1911
loadOptions( $data=null)
Load the user options either from cache, the database or an array.
Definition: User.php:5085
addToDatabase()
Add this existing user object to the database.
Definition: User.php:4108
getActorId(IDatabase $dbw=null)
Get the user&#39;s actor ID.
Definition: User.php:2271
$wgInvalidUsernameCharacters
Characters to prevent during new account creations.
static newFromRow( $row, $data=null)
Create a new user object from a user row.
Definition: User.php:696
bool $mAllowUsertalk
Definition: User.php:207
invalidateCache()
Immediately touch the user data cache for this account.
Definition: User.php:2563
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not...
$wgDefaultSkin
Default skin, for new users and anonymous visitors.
confirmEmail()
Mark the e-mail address confirmed.
Definition: User.php:4563
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition: User.php:5286
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
bool $mHideName
Definition: User.php:196
static newFromSession(WebRequest $request=null)
Create a new user object using data from session.
Definition: User.php:674
getEditCount()
Get the user&#39;s edit count.
Definition: User.php:3412
getUserPage()
Get this user&#39;s personal page title.
Definition: User.php:4277
setNewtalk( $val, $curRev=null)
Update the &#39;You have new messages!&#39; status.
Definition: User.php:2485
static isIPv4( $ip)
Given a string, determine if it as valid IP in IPv4 only.
Definition: IP.php:99
getInstanceForUpdate()
Get a new instance of this user that was loaded from the master via a locking read.
Definition: User.php:5312
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition: User.php:3608
$wgAddGroups
$wgAddGroups and $wgRemoveGroups can be used to give finer control over who can assign which groups a...
getDatePreference()
Get the user&#39;s preferred date format.
Definition: User.php:3241
getBlockId()
If user is blocked, return the ID for the block.
Definition: User.php:2111
static isInRanges( $ip, $ranges)
Determines if an IP address is a list of CIDR a.b.c.d/n ranges.
Definition: IP.php:655
sendConfirmationMail( $type='created')
Generate a new e-mail confirmation token and send a confirmation/invalidation mail to the user&#39;s give...
Definition: User.php:4424
getNewMessageRevisionId()
Get the revision ID for the last talk page revision viewed by the talk page owner.
Definition: User.php:2398
isLocked()
Check if user account is locked.
Definition: User.php:2171
makeGlobalKey( $class,... $components)
if(! $wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:735
string $mHash
Definition: User.php:182
deleteNewtalk( $field, $id)
Clear the new messages flag for the given user.
Definition: User.php:2465
bool $mOptionsLoaded
Whether the cache variables have been loaded.
Definition: User.php:152
canReceiveEmail()
Is this user allowed to receive e-mails within limits of current site configuration?
Definition: User.php:4622
const DB_REPLICA
Definition: defines.php:25
wfCanIPUseHTTPS( $ip)
Determine whether the client at a given source IP is likely to be able to access the wiki via HTTPS...
invalidationTokenUrl( $token)
Return a URL the user can use to invalidate their email address.
Definition: User.php:4532
checkPasswordValidity( $password)
Check if this is a valid password for this user.
Definition: User.php:1097
setEmailWithConfirmation( $str)
Set the user&#39;s e-mail address and a confirmation mail if needed.
Definition: User.php:2838
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
static createNew( $name, $params=[])
Add a user to the database, return the user object.
Definition: User.php:4032
getAutomaticGroups( $recache=false)
Get the list of implicit group memberships this user has.
Definition: User.php:3359
array $mOptionOverrides
Cache variables.
Definition: User.php:145
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:515
static getDefaultOptions()
Combine the language default options with any site-specific options and add the default language vari...
Definition: User.php:1651
const INVALID_TOKEN
An invalid string value for the user_token field.
Definition: User.php:61
setPassword( $str)
Set the password and reset the random token.
Definition: User.php:2651
string $mRealName
Cache variables.
Definition: User.php:122
addGroup( $group, $expiry=null)
Add the user to the given group.
Definition: User.php:3447
isSafeToLoad()
Test if it&#39;s safe to load this User object.
Definition: User.php:286
static getAutopromoteOnceGroups(User $user, $event)
Get the groups for the given user based on the given criteria.
Definition: Autopromote.php:65
getCacheKey(WANObjectCache $cache)
Definition: User.php:429
setEmail( $str)
Set the user&#39;s e-mail address.
Definition: User.php:2821
const NS_USER_TALK
Definition: Defines.php:63
static array $languagesWithVariants
languages supporting variants
addNewUserLogEntry( $action=false, $reason='')
Add a newuser log entry for this user.
Definition: User.php:5061
getTalkPage()
Get this user&#39;s talk page title.
Definition: User.php:4286
loadDefaults( $name=false, $actorId=null)
Set cached properties to default.
Definition: User.php:1195
static hmac( $data, $key, $raw=true)
Generate an acceptably unstable one-way-hmac of some text making use of the best hash algorithm that ...
isPingLimitable()
Is this user subject to rate limiting?
Definition: User.php:1886
static singleton()
Definition: UserCache.php:34
removeGroup( $group)
Remove the user from the given group.
Definition: User.php:3482
return true
Definition: router.php:82
const CHECK_USER_RIGHTS
Definition: User.php:78
static newFromRow( $row)
Creates a new UserGroupMembership object from a database row.
getRights()
Get the permissions this user has.
Definition: User.php:3300
getFormerGroups()
Returns the groups the user has belonged to.
Definition: User.php:3388
blockedFor()
If user is blocked, return the specified reason for the block.
Definition: User.php:2102
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
Base class for the more common types of database errors.
resetOptions( $resetKinds=[ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused'], IContextSource $context=null)
Reset certain (or all) options to the site defaults.
Definition: User.php:3196
static newSystemUser( $name, $options=[])
Static factory method for creation of a "system" user from username.
Definition: User.php:737
static isExternal( $username)
Tells whether the username is external or not.
int $mId
Cache variables.
Definition: User.php:116
static flattenOptions( $options)
flatten an array of options to a single array, for instance, a set of "<options>" inside "<optgroups>...
confirmationToken(&$expiration)
Generate, store, and return a new e-mail confirmation code.
Definition: User.php:4505
getTouched()
Get the user touched timestamp.
Definition: User.php:2607
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:316