Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.57% covered (warning)
68.57%
794 / 1158
59.12% covered (warning)
59.12%
81 / 137
CRAP
0.00% covered (danger)
0.00%
0 / 1
User
68.63% covered (warning)
68.63%
794 / 1157
59.12% covered (warning)
59.12%
81 / 137
4574.06
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWikiId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __get
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 __set
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 __sleep
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 isSafeToLoad
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
4
 load
74.63% covered (warning)
74.63%
50 / 67
0.00% covered (danger)
0.00%
0 / 1
29.91
 loadFromId
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
4.32
 purge
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getCacheKey
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 loadFromCache
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
72
 newFromName
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 newFromId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 newFromActorId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 newFromIdentity
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 newFromAnyId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 newFromConfirmationCode
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 newFromSession
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 newFromRow
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 newSystemUser
100.00% covered (success)
100.00%
52 / 52
100.00% covered (success)
100.00%
1 / 1
9
 findUsersByGroup
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 isValidPassword
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkPasswordValidity
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
5
 loadDefaults
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
4.00
 isItemLoaded
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
4
 setItemLoaded
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 loadFromSession
44.44% covered (danger)
44.44%
4 / 9
0.00% covered (danger)
0.00%
0 / 1
2.69
 loadFromDatabase
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
3.01
 loadFromRow
84.75% covered (warning)
84.75%
50 / 59
0.00% covered (danger)
0.00%
0 / 1
21.42
 loadFromUserObject
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 makeUpdateConditions
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 checkAndSetTouched
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
3.01
 clearInstanceCache
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
11
 isPingLimitable
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 pingLimiter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toRateLimitSubject
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getBlock
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
5.01
 isLocked
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 isHidden
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getId
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 setId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getName
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 setName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getActorId
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
6
 setActorId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getTitleKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newTouchedTimestamp
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 clearSharedCache
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
3.00
 invalidateCache
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 touch
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 debouncedDBTouch
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 validateCache
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTouched
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 getDBTouched
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 changeAuthenticationData
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 getToken
68.75% covered (warning)
68.75%
11 / 16
0.00% covered (danger)
0.00%
0 / 1
8.50
 setToken
57.14% covered (warning)
57.14%
4 / 7
0.00% covered (danger)
0.00%
0 / 1
3.71
 getEmail
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getEmailAuthenticationTimestamp
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 setEmail
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 setEmailWithConfirmation
25.00% covered (danger)
25.00%
7 / 28
0.00% covered (danger)
0.00%
0 / 1
72.75
 getRealName
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setRealName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getTokenFromOption
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 resetTokenFromOption
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getDatePreference
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 requiresHTTPS
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
4.02
 getEditCount
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 isRegistered
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isAnon
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isBot
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 isSystemUser
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 isAllowedAny
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isAllowedAll
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isAllowed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 useRCPatrol
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 useNPPatrol
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 useFilePatrol
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getRequest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExperienceLevel
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
7
 setCookies
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
56
 logout
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 doLogout
57.14% covered (warning)
57.14%
16 / 28
0.00% covered (danger)
0.00%
0 / 1
5.26
 saveSettings
70.59% covered (warning)
70.59%
36 / 51
0.00% covered (danger)
0.00%
0 / 1
6.92
 idForName
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 createNew
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 insertNewUser
92.86% covered (success)
92.86%
39 / 42
0.00% covered (danger)
0.00%
0 / 1
5.01
 addToDatabase
73.58% covered (warning)
73.58%
39 / 53
0.00% covered (danger)
0.00%
0 / 1
10.49
 scheduleSpreadBlock
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 spreadAnyEditBlock
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 spreadBlock
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
5.27
 isBlockedFromEmailuser
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 isBlockedFromUpload
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 isAllowedToCreateAccount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUserPage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTalkPage
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isNewbie
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEditTokenObject
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getEditToken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 matchEditToken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sendConfirmationMail
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 sendMail
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
 getConfirmationToken
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 confirmationToken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isWellFormedConfirmationToken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getConfirmationTokenUrl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getInvalidationTokenUrl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 invalidationTokenUrl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTokenUrl
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 confirmEmail
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 invalidateEmail
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 setEmailAuthenticationTimestamp
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 canSendEmail
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 canReceiveEmail
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 isEmailConfirmed
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 isEmailConfirmationPending
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 getRegistration
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getRightDescription
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getRightDescriptionHtml
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
2
 newQueryBuilder
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 newFatalPermissionDeniedStatus
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getInstanceForUpdate
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getInstanceFromPrimary
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 equals
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 probablyCan
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 definitelyCan
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDefinitelyAllowed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 authorizeAction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 authorizeRead
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 authorizeWrite
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getThisAsAuthority
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 isGlobalSessionUser
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 isTemp
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 isNamed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\User;
8
9use AllowDynamicProperties;
10use ArrayIterator;
11use BadMethodCallException;
12use InvalidArgumentException;
13use MediaWiki\Auth\AuthenticationRequest;
14use MediaWiki\Auth\AuthManager;
15use MediaWiki\Block\AbstractBlock;
16use MediaWiki\Block\Block;
17use MediaWiki\Block\DatabaseBlock;
18use MediaWiki\Context\RequestContext;
19use MediaWiki\DAO\WikiAwareEntityTrait;
20use MediaWiki\Deferred\DeferredUpdates;
21use MediaWiki\Exception\MWExceptionHandler;
22use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
23use MediaWiki\Logger\LoggerFactory;
24use MediaWiki\Mail\ConfirmEmail\ConfirmEmailData;
25use MediaWiki\Mail\MailAddress;
26use MediaWiki\Mail\UserEmailContact;
27use MediaWiki\MainConfigNames;
28use MediaWiki\MainConfigSchema;
29use MediaWiki\MediaWikiServices;
30use MediaWiki\Page\PageIdentity;
31use MediaWiki\Parser\Sanitizer;
32use MediaWiki\Password\PasswordFactory;
33use MediaWiki\Password\UserPasswordPolicy;
34use MediaWiki\Permissions\Authority;
35use MediaWiki\Permissions\PermissionStatus;
36use MediaWiki\Permissions\RateLimitSubject;
37use MediaWiki\Permissions\UserAuthority;
38use MediaWiki\Profiler\Profiler;
39use MediaWiki\Request\WebRequest;
40use MediaWiki\Session\SessionManager;
41use MediaWiki\Session\Token;
42use MediaWiki\Status\Status;
43use MediaWiki\Title\Title;
44use MediaWiki\Utils\MWCryptRand;
45use MWCryptHash;
46use RuntimeException;
47use stdClass;
48use Stringable;
49use UnexpectedValueException;
50use Wikimedia\Assert\Assert;
51use Wikimedia\Assert\PreconditionException;
52use Wikimedia\DebugInfo\DebugInfoTrait;
53use Wikimedia\IPUtils;
54use Wikimedia\ObjectCache\WANObjectCache;
55use Wikimedia\Rdbms\Database;
56use Wikimedia\Rdbms\DBAccessObjectUtils;
57use Wikimedia\Rdbms\DBExpectedError;
58use Wikimedia\Rdbms\IDatabase;
59use Wikimedia\Rdbms\IDBAccessObject;
60use Wikimedia\Rdbms\IReadableDatabase;
61use Wikimedia\Rdbms\SelectQueryBuilder;
62use Wikimedia\ScopedCallback;
63use Wikimedia\Timestamp\ConvertibleTimestamp;
64use Wikimedia\Timestamp\TimestampFormat as TS;
65
66/**
67 * @defgroup User User management
68 */
69
70/**
71 * User class for the %MediaWiki software.
72 *
73 * User objects manage reading and writing of user-specific storage, including:
74 * - `user` table (user_id, user_name, email, password, last login, etc.)
75 * - `user_properties` table (user options)
76 * - `user_groups` table (user rights and permissions)
77 * - `user_newtalk` table (last-seen for your own user talk page)
78 * - `watchlist` table (watched page titles by user, and their last-seen marker)
79 * - `block` table, formerly known as `ipblocks` (user blocks)
80 *
81 * Callers use getter methods (getXXX) to read these fields. These getter functions
82 * manage all higher-level responsibilities such as expanding default user options,
83 * interpreting user groups into specific rights. Most user data needed when
84 * rendering page views are cached (or stored in the session) to minimize repeat
85 * database queries.
86 *
87 * New code is encouraged to use the following narrower classes instead.
88 * If no replacement exist, and the User class method is not deprecated, feel
89 * free to use it in new code (instead of duplicating business logic).
90 *
91 * - UserIdentityValue, to represent a user name/id.
92 *
93 * - Authority via RequestContext::getAuthority, to represent the current user
94 *   with a easy shortcuts to interpret user permissions (can user X do Y on page Z)
95 *   without needing te call low-level PermissionManager and RateLimiter services.
96 *   This replaces methods like User::isAllowed, User::definitelyCan,
97 *   and User::pingLimiter.
98 *
99 * - UserOptionsManager service, to read-write user options from user_properties.
100 *   These were previously stored in a user.user_options field and moved to
101 *   a new user_properties table in MediaWiki 1.19 (T33204).
102 *   The service was extracted in MediaWiki 1.35 (T248527) to support alternative
103 *   providers via $wgUserOptionsStoreProviders for GlobalPreferences.
104 *   This replaces methods like User::getOption, User::setOption,
105 *   User::getDefaultOptions, and User::resetOptions.
106 *
107 * - TalkPageNotificationManager service, to read-write data from user_newtalk.
108 *   This replaces User::getNewtalk.
109 *
110 * - PermissionManager service, to interpret rights and permissions of any user.
111 *
112 * - WatchlistManager service, to read-write from the watchlist table.
113 *   This replaces methods like User::isWatched, User::addWatch,
114 *   and User::clearNotification.
115 *
116 * - BlockManager service, replacing User::getBlock.
117 *
118 * - UserRegistrationLookup service, for read-only access to user_registration.
119 *   The service was extracted in MediaWiki 1.41 (T352871) to support batching
120 *   and alternative providers via $wgUserRegistrationProviders for CentralAuth.
121 *   This replaces User::getRegistration.
122 *   Writes to user_registration remain in the User class for account creation!
123 *
124 * @note User implements Authority to ease transition. Always prefer
125 * using existing Authority or obtaining a proper Authority implementation.
126 *
127 * @ingroup User
128 */
129#[AllowDynamicProperties]
130class User implements Stringable, Authority, UserIdentity, UserEmailContact {
131    use DebugInfoTrait;
132    use ProtectedHookAccessorTrait;
133    use WikiAwareEntityTrait;
134
135    /**
136     * @see IDBAccessObject::READ_EXCLUSIVE
137     */
138    public const READ_EXCLUSIVE = IDBAccessObject::READ_EXCLUSIVE;
139
140    /**
141     * @see IDBAccessObject::READ_LOCKING
142     */
143    public const READ_LOCKING = IDBAccessObject::READ_LOCKING;
144
145    /**
146     * Number of characters required for the user_token field.
147     */
148    public const TOKEN_LENGTH = 32;
149
150    /**
151     * An invalid string value for the user_token field.
152     */
153    public const INVALID_TOKEN = '*** INVALID ***';
154
155    /**
156     * Version number to tag cached versions of serialized User objects. Should be increased when
157     * {@link $mCacheVars} or one of its members changes.
158     */
159    private const VERSION = 18;
160
161    /**
162     * Username used for various maintenance scripts.
163     * @since 1.37
164     */
165    public const MAINTENANCE_SCRIPT_USER = 'Maintenance script';
166
167    /**
168     * List of member variables which are saved to the
169     * shared cache (memcached). Any operation which changes the
170     * corresponding database fields must call a cache-clearing function.
171     * @showinitializer
172     * @var string[]
173     */
174    protected static $mCacheVars = [
175        // user table
176        'mId',
177        'mName',
178        'mRealName',
179        'mEmail',
180        'mTouched',
181        'mToken',
182        'mEmailAuthenticated',
183        'mEmailToken',
184        'mEmailTokenExpires',
185        // actor table
186        'mActorId',
187    ];
188
189    /** Cache variables */
190    // Some of these are public, including for use by the UserFactory, but they generally
191    // should not be set manually
192    // @{
193    /** @var int */
194    public $mId;
195    /** @var string */
196    public $mName;
197    /**
198     * Switched from protected to public for use in UserFactory
199     *
200     * @var int|null
201     */
202    public $mActorId;
203    /** @var string */
204    public $mRealName;
205
206    /** @var string */
207    public $mEmail;
208    /** @var string TS::MW timestamp from the DB */
209    public $mTouched;
210    /** @var string|null TS::MW timestamp from cache */
211    protected $mQuickTouched;
212    /** @var string|null */
213    protected $mToken;
214    /** @var string|null */
215    public $mEmailAuthenticated;
216    /** @var string|null */
217    protected $mEmailToken;
218    /** @var string|null */
219    protected $mEmailTokenExpires;
220    // @}
221
222    // @{
223    /**
224     * @var array|bool Array with already loaded items or true if all items have been loaded.
225     */
226    protected $mLoadedItems = [];
227    // @}
228
229    /**
230     * @var string Initialization data source if mLoadedItems!==true. May be one of:
231     *  - 'defaults'   anonymous user initialised from class defaults
232     *  - 'name'       initialise from mName
233     *  - 'id'         initialise from mId
234     *  - 'actor'      initialise from mActorId
235     *  - 'session'    log in from session if possible
236     *
237     * Use the User::newFrom*() family of functions to set this.
238     */
239    public $mFrom;
240
241    /**
242     * Lazy-initialized variables, invalidated with clearInstanceCache
243     */
244    /** @var string|null */
245    protected $mDatePreference;
246    /** @var AbstractBlock|false|null Null when uninitialized, false when there is no block */
247    protected $mGlobalBlock;
248    /** @var bool|null */
249    protected $mLocked;
250
251    /** @var WebRequest|null */
252    private $mRequest;
253
254    /** @var int IDBAccessObject::READ_* constant bitfield used to load data */
255    protected $queryFlagsUsed = IDBAccessObject::READ_NORMAL;
256
257    /**
258     * @var UserAuthority|null lazy-initialized Authority of this user
259     * @noVarDump
260     */
261    private $mThisAsAuthority;
262
263    /** @var bool|null */
264    private $isTemp;
265
266    /**
267     * @internal since 1.36, use the UserFactory service instead
268     *
269     * @see \MediaWiki\User\UserFactory
270     *
271     * @see newFromName()
272     * @see newFromId()
273     * @see newFromActorId()
274     * @see newFromConfirmationCode()
275     * @see newFromSession()
276     * @see newFromRow()
277     */
278    public function __construct() {
279        // By default, this is a lightweight constructor representing
280        // an anonymous user from the current web request and IP.
281        $this->clearInstanceCache( 'defaults' );
282    }
283
284    /**
285     * Returns self::LOCAL to indicate the user is associated with the local wiki.
286     *
287     * @since 1.36
288     */
289    public function getWikiId(): string|false {
290        return self::LOCAL;
291    }
292
293    /**
294     * @return string
295     */
296    public function __toString() {
297        return $this->getName();
298    }
299
300    public function &__get( $name ) {
301        // A shortcut for $mRights deprecation phase
302        if ( $name === 'mRights' ) {
303            // hard deprecated since 1.40
304            wfDeprecated( 'User::$mRights', '1.34' );
305            $copy = MediaWikiServices::getInstance()
306                ->getPermissionManager()
307                ->getUserPermissions( $this );
308            return $copy;
309        } elseif ( !property_exists( $this, $name ) ) {
310            // T227688 - do not break $u->foo['bar'] = 1
311            wfLogWarning( 'tried to get non-existent property' );
312            $this->$name = null;
313            return $this->$name;
314        } else {
315            wfLogWarning( 'tried to get non-visible property' );
316            $null = null;
317            return $null;
318        }
319    }
320
321    public function __set( $name, $value ) {
322        // A shortcut for $mRights deprecation phase, only known legitimate use was for
323        // testing purposes, other uses seem bad in principle
324        if ( $name === 'mRights' ) {
325            // hard deprecated since 1.40
326            wfDeprecated( 'User::$mRights', '1.34' );
327            MediaWikiServices::getInstance()->getPermissionManager()->overrideUserRightsForTesting(
328                $this,
329                $value ?? []
330            );
331        } elseif ( !property_exists( $this, $name ) ) {
332            $this->$name = $value;
333        } else {
334            wfLogWarning( 'tried to set non-visible property' );
335        }
336    }
337
338    public function __sleep(): array {
339        return array_diff(
340            array_keys( get_object_vars( $this ) ),
341            [
342                'mThisAsAuthority', // memoization, will be recreated on demand.
343                'mRequest', // contains Session, reloaded when needed, T400549
344            ]
345        );
346    }
347
348    /**
349     * Test if it's safe to load this User object.
350     *
351     * You should typically check this before using $wgUser or
352     * RequestContext::getUser in a method that might be called before the
353     * system has been fully initialized. If the object is unsafe, you should
354     * use an anonymous user:
355     * \code
356     * $user = $wgUser->isSafeToLoad() ? $wgUser : new User;
357     * \endcode
358     *
359     * @since 1.27
360     * @return bool
361     */
362    public function isSafeToLoad() {
363        global $wgFullyInitialised;
364
365        // The user is safe to load if:
366        // * MW_NO_SESSION is undefined AND $wgFullyInitialised is true (safe to use session data)
367        // * mLoadedItems === true (already loaded)
368        // * mFrom !== 'session' (sessions not involved at all)
369
370        return ( !defined( 'MW_NO_SESSION' ) && $wgFullyInitialised ) ||
371            $this->mLoadedItems === true || $this->mFrom !== 'session';
372    }
373
374    /**
375     * Load the user table data for this object from the source given by mFrom.
376     *
377     * @param int $flags IDBAccessObject::READ_* constant bitfield
378     */
379    public function load( $flags = IDBAccessObject::READ_NORMAL ) {
380        global $wgFullyInitialised;
381
382        if ( $this->mLoadedItems === true ) {
383            return;
384        }
385
386        // Set it now to avoid infinite recursion in accessors
387        $oldLoadedItems = $this->mLoadedItems;
388        $this->mLoadedItems = true;
389        $this->queryFlagsUsed = $flags;
390
391        // If this is called too early, things are likely to break.
392        if ( !$wgFullyInitialised && $this->mFrom === 'session' ) {
393            LoggerFactory::getInstance( 'session' )
394                ->warning( 'User::loadFromSession called before the end of Setup.php', [
395                    'userId' => $this->mId,
396                    'userName' => $this->mName,
397                    'exception' => new RuntimeException(
398                        'User::loadFromSession called before the end of Setup.php'
399                    ),
400                ] );
401            $this->loadDefaults();
402            $this->mLoadedItems = $oldLoadedItems;
403            return;
404        } elseif ( $this->mFrom === 'session'
405            && defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn'
406        ) {
407            // Even though we are throwing an exception, make sure the User object is left in a
408            // clean state as sometimes these exceptions are caught and the object accessed again.
409            $this->loadDefaults();
410            $this->mLoadedItems = $oldLoadedItems;
411            $ep = defined( 'MW_ENTRY_POINT' ) ? MW_ENTRY_POINT : 'this';
412            throw new BadMethodCallException( "Sessions are disabled for $ep entry point" );
413        }
414
415        switch ( $this->mFrom ) {
416            case 'defaults':
417                $this->loadDefaults();
418                break;
419            case 'id':
420                // Make sure this thread sees its own changes, if the ID isn't 0
421                if ( $this->mId != 0 ) {
422                    $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
423                    if ( $lb->hasOrMadeRecentPrimaryChanges() ) {
424                        $flags |= IDBAccessObject::READ_LATEST;
425                        $this->queryFlagsUsed = $flags;
426                    }
427                }
428
429                $this->loadFromId( $flags );
430                break;
431            case 'actor':
432            case 'name':
433                // Make sure this thread sees its own changes
434                $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
435                if ( $lb->hasOrMadeRecentPrimaryChanges() ) {
436                    $flags |= IDBAccessObject::READ_LATEST;
437                    $this->queryFlagsUsed = $flags;
438                }
439
440                $dbr = DBAccessObjectUtils::getDBFromRecency(
441                    MediaWikiServices::getInstance()->getDBLoadBalancerFactory(),
442                    $flags
443                );
444                $queryBuilder = $dbr->newSelectQueryBuilder()
445                    ->select( [ 'actor_id', 'actor_user', 'actor_name' ] )
446                    ->from( 'actor' )
447                    ->recency( $flags );
448                if ( $this->mFrom === 'name' ) {
449                    // make sure to use normalized form of IP for anonymous users
450                    $queryBuilder->where( [ 'actor_name' => IPUtils::sanitizeIP( $this->mName ) ] );
451                } else {
452                    $queryBuilder->where( [ 'actor_id' => $this->mActorId ] );
453                }
454                $row = $queryBuilder->caller( __METHOD__ )->fetchRow();
455
456                if ( !$row ) {
457                    // Ugh.
458                    $this->loadDefaults( $this->mFrom === 'name' ? $this->mName : false );
459                } elseif ( $row->actor_user ) {
460                    $this->mId = $row->actor_user;
461                    $this->loadFromId( $flags );
462                } else {
463                    $this->loadDefaults( $row->actor_name, $row->actor_id );
464                }
465                break;
466            case 'session':
467                if ( !$this->loadFromSession() ) {
468                    // Loading from session failed. Load defaults.
469                    $this->loadDefaults();
470                }
471                $this->getHookRunner()->onUserLoadAfterLoadFromSession( $this );
472                break;
473            default:
474                throw new UnexpectedValueException(
475                    "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
476        }
477    }
478
479    /**
480     * Load user table data, given mId has already been set.
481     * @param int $flags IDBAccessObject::READ_* constant bitfield
482     * @return bool False if the ID does not exist, true otherwise
483     */
484    public function loadFromId( $flags = IDBAccessObject::READ_NORMAL ) {
485        if ( $this->mId == 0 ) {
486            // Anonymous users are not in the database (don't need cache)
487            $this->loadDefaults();
488            return false;
489        }
490
491        // Try cache (unless this needs data from the primary DB).
492        // NOTE: if this thread called saveSettings(), the cache was cleared.
493        $latest = DBAccessObjectUtils::hasFlags( $flags, IDBAccessObject::READ_LATEST );
494        if ( $latest ) {
495            if ( !$this->loadFromDatabase( $flags ) ) {
496                // Can't load from ID
497                return false;
498            }
499        } else {
500            $this->loadFromCache();
501        }
502
503        $this->mLoadedItems = true;
504        $this->queryFlagsUsed = $flags;
505
506        return true;
507    }
508
509    /**
510     * @since 1.27
511     * @param string $dbDomain
512     * @param int $userId
513     */
514    public static function purge( $dbDomain, $userId ) {
515        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
516        $key = $cache->makeGlobalKey( 'user', 'id', $dbDomain, $userId );
517        $cache->delete( $key );
518    }
519
520    /**
521     * @since 1.27
522     * @param WANObjectCache $cache
523     * @return string
524     */
525    protected function getCacheKey( WANObjectCache $cache ) {
526        $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
527
528        return $cache->makeGlobalKey( 'user', 'id',
529            $lbFactory->getLocalDomainID(), $this->mId );
530    }
531
532    /**
533     * Load user data from shared cache, given mId has already been set.
534     *
535     * @return bool True
536     * @since 1.25
537     */
538    protected function loadFromCache() {
539        global $wgFullyInitialised;
540
541        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
542        $data = $cache->getWithSetCallback(
543            $this->getCacheKey( $cache ),
544            $cache::TTL_HOUR,
545            function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache, $wgFullyInitialised ) {
546                $setOpts += Database::getCacheSetOptions(
547                    MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase()
548                );
549                wfDebug( "User: cache miss for user {$this->mId}" );
550
551                $this->loadFromDatabase( IDBAccessObject::READ_NORMAL );
552
553                $data = [];
554                foreach ( self::$mCacheVars as $name ) {
555                    $data[$name] = $this->$name;
556                }
557
558                $ttl = $cache->adaptiveTTL(
559                    (int)wfTimestamp( TS::UNIX, $this->mTouched ),
560                    $ttl
561                );
562
563                if ( $wgFullyInitialised ) {
564                    $groupMemberships = MediaWikiServices::getInstance()
565                        ->getUserGroupManager()
566                        ->getUserGroupMemberships( $this, $this->queryFlagsUsed );
567
568                    // if a user group membership is about to expire, the cache needs to
569                    // expire at that time (T163691)
570                    foreach ( $groupMemberships as $ugm ) {
571                        if ( $ugm->getExpiry() ) {
572                            $secondsUntilExpiry =
573                                (int)wfTimestamp( TS::UNIX, $ugm->getExpiry() ) - time();
574
575                            if ( $secondsUntilExpiry > 0 && $secondsUntilExpiry < $ttl ) {
576                                $ttl = $secondsUntilExpiry;
577                            }
578                        }
579                    }
580                }
581
582                return $data;
583            },
584            [ 'pcTTL' => $cache::TTL_PROC_LONG, 'version' => self::VERSION ]
585        );
586
587        // Restore from cache
588        foreach ( self::$mCacheVars as $name ) {
589            $this->$name = $data[$name];
590        }
591
592        return true;
593    }
594
595    /***************************************************************************/
596    // region   newFrom*() static factory methods
597    /** @name   newFrom*() static factory methods
598     * @{
599     */
600
601    /**
602     * @see UserFactory::newFromName
603     *
604     * @deprecated since 1.36, use a UserFactory instead
605     *
606     * This is slightly less efficient than newFromId(), so use newFromId() if
607     * you have both an ID and a name handy.
608     *
609     * @param string $name Username, validated by Title::newFromText()
610     * @param string|bool $validate Validate username.Type of validation to use:
611     *   - false        No validation
612     *   - 'valid'      Valid for batch processes
613     *   - 'usable'     Valid for batch processes and login
614     *   - 'creatable'  Valid for batch processes, login and account creation,
615     *  except that true is accepted as an alias for 'valid', for BC.
616     *
617     * @return User|false User object, or false if the username is invalid
618     *  (e.g. if it contains illegal characters or is an IP address). If the
619     *  username is not present in the database, the result will be a user object
620     *  with a name, zero user ID and default settings.
621     */
622    public static function newFromName( $name, $validate = 'valid' ) {
623        // Backwards compatibility with strings / false
624        $validation = match ( $validate ) {
625            'valid' => UserRigorOptions::RIGOR_VALID,
626            'usable' => UserRigorOptions::RIGOR_USABLE,
627            'creatable' => UserRigorOptions::RIGOR_CREATABLE,
628            true => UserRigorOptions::RIGOR_VALID,
629            false => UserRigorOptions::RIGOR_NONE,
630            // Not a recognized value, probably a test for unsupported validation
631            // levels, regardless, just pass it along
632            default => $validate,
633        };
634        return MediaWikiServices::getInstance()->getUserFactory()
635            ->newFromName( (string)$name, $validation ) ?? false;
636    }
637
638    /**
639     * Static factory method for creation from a given user ID.
640     *
641     * @see UserFactory::newFromId
642     *
643     * @deprecated since 1.36, use a UserFactory instead
644     *
645     * @param int $id Valid user ID
646     * @return User
647     */
648    public static function newFromId( $id ) {
649        return MediaWikiServices::getInstance()
650            ->getUserFactory()
651            ->newFromId( (int)$id );
652    }
653
654    /**
655     * Static factory method for creation from a given actor ID.
656     *
657     * @see UserFactory::newFromActorId
658     *
659     * @deprecated since 1.36, use a UserFactory instead
660     *
661     * @since 1.31
662     * @param int $id Valid actor ID
663     * @return User
664     */
665    public static function newFromActorId( $id ) {
666        return MediaWikiServices::getInstance()
667            ->getUserFactory()
668            ->newFromActorId( (int)$id );
669    }
670
671    /**
672     * Returns a User object corresponding to the given UserIdentity.
673     *
674     * @see UserFactory::newFromUserIdentity
675     *
676     * @deprecated since 1.36, use a UserFactory instead
677     *
678     * @since 1.32
679     *
680     * @param UserIdentity $identity
681     *
682     * @return User
683     */
684    public static function newFromIdentity( UserIdentity $identity ) {
685        // Don't use the service if we already have a User object,
686        // so that User::newFromIdentity calls don't break things in unit tests.
687        if ( $identity instanceof User ) {
688            return $identity;
689        }
690
691        return MediaWikiServices::getInstance()
692            ->getUserFactory()
693            ->newFromUserIdentity( $identity );
694    }
695
696    /**
697     * Static factory method for creation from an ID, name, and/or actor ID
698     *
699     * This does not check that the ID, name, and actor ID all correspond to
700     * the same user.
701     *
702     * @see UserFactory::newFromAnyId
703     *
704     * @deprecated since 1.36, use a UserFactory instead
705     *
706     * @since 1.31
707     * @param int|null $userId User ID, if known
708     * @param string|null $userName User name, if known
709     * @param int|null $actorId Actor ID, if known
710     * @param string|false $dbDomain remote wiki to which the User/Actor ID
711     *   applies, or false if none
712     * @return User
713     */
714    public static function newFromAnyId( $userId, $userName, $actorId, $dbDomain = false ) {
715        return MediaWikiServices::getInstance()
716            ->getUserFactory()
717            ->newFromAnyId( $userId, $userName, $actorId, $dbDomain );
718    }
719
720    /**
721     * Factory method to fetch whichever user has a given email confirmation code.
722     * This code is generated when an account is created or its e-mail address
723     * has changed.
724     *
725     * If the code is invalid or has expired, returns NULL.
726     *
727     * @see UserFactory::newFromConfirmationCode
728     *
729     * @deprecated since 1.36, use a UserFactory instead
730     *
731     * @param string $code Confirmation code
732     * @param int $flags IDBAccessObject::READ_* bitfield
733     * @return User|null
734     */
735    public static function newFromConfirmationCode( $code, $flags = IDBAccessObject::READ_NORMAL ) {
736        return MediaWikiServices::getInstance()
737            ->getUserFactory()
738            ->newFromConfirmationCode( (string)$code, $flags );
739    }
740
741    /**
742     * Create a new user object using data from session. If the login
743     * credentials are invalid, the result is an anonymous user.
744     *
745     * @param WebRequest|null $request Object to use; the global request will be used if omitted.
746     * @return User
747     */
748    public static function newFromSession( ?WebRequest $request = null ) {
749        $user = new User;
750        $user->mFrom = 'session';
751        $user->mRequest = $request;
752        return $user;
753    }
754
755    /**
756     * Create a new user object from a user row.
757     * The row should have the following fields from the user table in it:
758     * - either user_name or user_id to load further data if needed (or both)
759     * - user_real_name
760     * - all other fields (email, etc.)
761     * It is useless to provide the remaining fields if either user_id,
762     * user_name and user_real_name are not provided because the whole row
763     * will be loaded once more from the database when accessing them.
764     *
765     * @param stdClass $row A row from the user table
766     * @param array|null $data Further data to load into the object
767     *  (see User::loadFromRow for valid keys)
768     * @return User
769     */
770    public static function newFromRow( $row, $data = null ) {
771        $user = new User;
772        $user->loadFromRow( $row, $data );
773        return $user;
774    }
775
776    /**
777     * Static factory method for creation of a "system" user from username.
778     *
779     * A "system" user is an account that's used to attribute logged actions
780     * taken by MediaWiki itself, as opposed to a bot or human user. Examples
781     * might include the 'Maintenance script' or 'Conversion script' accounts
782     * used by various scripts in the maintenance/ directory or accounts such
783     * as 'MediaWiki message delivery' used by the MassMessage extension.
784     *
785     * This can optionally create the user if it doesn't exist, and "steal" the
786     * account if it does exist.
787     *
788     * "Stealing" an existing user is intended to make it impossible for normal
789     * authentication processes to use the account, effectively disabling the
790     * account for normal use:
791     * - Email is invalidated, to prevent account recovery by emailing a
792     *   temporary password and to disassociate the account from the existing
793     *   human.
794     * - The token is set to a magic invalid value, to kill existing sessions
795     *   and to prevent $this->setToken() calls from resetting the token to a
796     *   valid value.
797     * - SessionManager is instructed to prevent new sessions for the user, to
798     *   do things like deauthorizing OAuth consumers.
799     * - AuthManager is instructed to revoke access, to invalidate or remove
800     *   passwords and other credentials.
801     *
802     * System users should usually be listed in $wgReservedUsernames.
803     *
804     * @param string $name Username
805     * @param array $options Options are:
806     *  - validate: Type of validation to use:
807     *    - false        No validation
808     *    - 'valid'      Valid for batch processes
809     *    - 'usable'     Valid for batch processes and login
810     *    - 'creatable'  Valid for batch processes, login and account creation,
811     *    default 'valid'. Deprecated since 1.36.
812     *  - create: Whether to create the user if it doesn't already exist, default true
813     *  - steal: Whether to "disable" the account for normal use if it already
814     *    exists, default false
815     * @return User|null
816     * @since 1.27
817     * @see self::isSystemUser()
818     * @see MainConfigSchema::ReservedUsernames
819     */
820    public static function newSystemUser( $name, $options = [] ) {
821        $options += [
822            'validate' => UserRigorOptions::RIGOR_VALID,
823            'create' => true,
824            'steal' => false,
825        ];
826
827        // Username validation
828        $validate = $options['validate'];
829        // Backwards compatibility with strings / false
830        $validation = match ( $validate ) {
831            'valid' => UserRigorOptions::RIGOR_VALID,
832            'usable' => UserRigorOptions::RIGOR_USABLE,
833            'creatable' => UserRigorOptions::RIGOR_CREATABLE,
834            false => UserRigorOptions::RIGOR_NONE,
835            // Not a recognized value, probably a test for unsupported validation
836            // levels, regardless, just pass it along
837            default => $validate,
838        };
839
840        if ( $validation !== UserRigorOptions::RIGOR_VALID ) {
841            wfDeprecatedMsg(
842                __METHOD__ . ' options["validation"] parameter must be omitted or set to "valid".',
843                '1.36'
844            );
845        }
846        $services = MediaWikiServices::getInstance();
847        $userNameUtils = $services->getUserNameUtils();
848
849        $name = $userNameUtils->getCanonical( (string)$name, $validation );
850        if ( $name === false ) {
851            return null;
852        }
853
854        $dbProvider = $services->getDBLoadBalancerFactory();
855        $dbr = $dbProvider->getReplicaDatabase();
856
857        $userQuery = self::newQueryBuilder( $dbr )
858            ->where( [ 'user_name' => $name ] )
859            ->caller( __METHOD__ );
860        $row = $userQuery->fetchRow();
861        if ( !$row ) {
862            // Try the primary database
863            $userQuery->connection( $dbProvider->getPrimaryDatabase() );
864            // Lock the row to prevent insertNewUser() returning null due to race conditions
865            $userQuery->forUpdate();
866            $row = $userQuery->fetchRow();
867        }
868
869        if ( !$row ) {
870            // No user. Create it?
871            if ( !$options['create'] ) {
872                // No.
873                return null;
874            }
875
876            // If it's a reserved user that had an anonymous actor created for it at
877            // some point, we need special handling.
878            return self::insertNewUser( static function ( UserIdentity $actor, IDatabase $dbw ) {
879                return MediaWikiServices::getInstance()->getActorStore()
880                    ->acquireSystemActorId( $actor, $dbw );
881            }, $name, [ 'token' => self::INVALID_TOKEN ] );
882        }
883
884        $user = self::newFromRow( $row );
885
886        if ( !$user->isSystemUser() ) {
887            // User exists. Steal it?
888            if ( !$options['steal'] ) {
889                return null;
890            }
891
892            $services->getAuthManager()->revokeAccessForUser( $name );
893
894            $user->invalidateEmail();
895            $user->mToken = self::INVALID_TOKEN;
896            $user->saveSettings();
897            $manager = $services->getSessionManager();
898            if ( $manager instanceof SessionManager ) {
899                $manager->preventSessionsForUser( $user->getName() );
900            }
901        }
902
903        return $user;
904    }
905
906    /** @} */
907    // endregion -- end of newFrom*() static factory methods
908
909    /**
910     * Return the users who are members of the given group(s). In case of multiple groups,
911     * users who are members of at least one of them are returned.
912     *
913     * @param string|array $groups A single group name or an array of group names
914     * @param int $limit Max number of users to return. The actual limit will never exceed 5000
915     *   records; larger values are ignored.
916     * @param int|null $after ID the user to start after
917     * @return UserArray|ArrayIterator
918     */
919    public static function findUsersByGroup( $groups, $limit = 5000, $after = null ) {
920        if ( $groups === [] ) {
921            return UserArrayFromResult::newFromIDs( [] );
922        }
923        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
924        $queryBuilder = $dbr->newSelectQueryBuilder()
925            ->select( 'ug_user' )
926            ->distinct()
927            ->from( 'user_groups' )
928            ->where( [ 'ug_group' => array_unique( (array)$groups ) ] )
929            ->orderBy( 'ug_user' )
930            ->limit( min( 5000, $limit ) );
931
932        if ( $after !== null ) {
933            $queryBuilder->andWhere( $dbr->expr( 'ug_user', '>', (int)$after ) );
934        }
935
936        $ids = $queryBuilder->caller( __METHOD__ )->fetchFieldValues() ?: [];
937        return UserArray::newFromIDs( $ids );
938    }
939
940    /**
941     * Is the input a valid password for this user?
942     *
943     * @param string $password Desired password
944     * @return bool
945     */
946    public function isValidPassword( $password ) {
947        // simple boolean wrapper for checkPasswordValidity
948        return $this->checkPasswordValidity( $password )->isGood();
949    }
950
951    /**
952     * Check if this is a valid password for this user
953     *
954     * Returns a Status object with a set of messages describing
955     * problems with the password. If the return status is fatal,
956     * the action should be refused and the password should not be
957     * checked at all (this is mainly meant for DoS mitigation).
958     * If the return value is OK but not good, the password can be checked,
959     * but the user should not be able to set their password to this.
960     * The value of the returned Status object will be an array which
961     * can have the following fields:
962     * - forceChange (bool): if set to true, the user should not be
963     *   allowed to log with this password unless they change it during
964     *   the login process (see ResetPasswordSecondaryAuthenticationProvider).
965     * - suggestChangeOnLogin (bool): if set to true, the user should be prompted for
966     *   a password change on login.
967     *
968     * @param string $password Desired password
969     * @return Status
970     * @since 1.23
971     */
972    public function checkPasswordValidity( $password ) {
973        $services = MediaWikiServices::getInstance();
974        $userNameUtils = $services->getUserNameUtils();
975        if ( $userNameUtils->isTemp( $this->getName() ) ) {
976            return Status::newFatal( 'error-temporary-accounts-cannot-have-passwords' );
977        }
978
979        $passwordPolicy = $services->getMainConfig()->get( MainConfigNames::PasswordPolicy );
980
981        $upp = new UserPasswordPolicy(
982            $passwordPolicy['policies'],
983            $passwordPolicy['checks']
984        );
985
986        $status = Status::newGood( [] );
987        $result = false; // init $result to false for the internal checks
988
989        if ( !$this->getHookRunner()->onIsValidPassword( $password, $result, $this ) ) {
990            $status->error( $result );
991            return $status;
992        }
993
994        if ( $result === false ) {
995            $status->merge( $upp->checkUserPassword( $this, $password ), true );
996            return $status;
997        }
998
999        if ( $result === true ) {
1000            return $status;
1001        }
1002
1003        $status->error( $result );
1004        return $status; // the isValidPassword hook set a string $result and returned true
1005    }
1006
1007    /**
1008     * Set cached properties to default.
1009     *
1010     * @note This no longer clears uncached lazy-initialised properties;
1011     *       the constructor does that instead.
1012     *
1013     * @param string|false $name
1014     * @param int|null $actorId
1015     */
1016    public function loadDefaults( $name = false, $actorId = null ) {
1017        $this->mId = 0;
1018        $this->mName = $name;
1019        $this->mActorId = $actorId;
1020        $this->mRealName = '';
1021        $this->mEmail = '';
1022        $this->isTemp = null;
1023
1024        $loggedOut = $this->mRequest && !defined( 'MW_NO_SESSION' )
1025            ? $this->mRequest->getSession()->getLoggedOutTimestamp() : 0;
1026        if ( $loggedOut !== 0 ) {
1027            $this->mTouched = wfTimestamp( TS::MW, $loggedOut );
1028        } else {
1029            $this->mTouched = '1'; # Allow any pages to be cached
1030        }
1031
1032        $this->mToken = null; // Don't run cryptographic functions till we need a token
1033        $this->mEmailAuthenticated = null;
1034        $this->mEmailToken = '';
1035        $this->mEmailTokenExpires = null;
1036
1037        $this->getHookRunner()->onUserLoadDefaults( $this, $name );
1038    }
1039
1040    /**
1041     * Return whether an item has been loaded.
1042     *
1043     * @param string $item Item to check. Current possibilities:
1044     *   - id
1045     *   - name
1046     *   - realname
1047     * @param string $all 'all' to check if the whole object has been loaded
1048     *   or any other string to check if only the item is available (e.g.
1049     *   for optimisation)
1050     * @return bool
1051     */
1052    public function isItemLoaded( $item, $all = 'all' ) {
1053        return ( $this->mLoadedItems === true && $all === 'all' ) ||
1054            ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true );
1055    }
1056
1057    /**
1058     * Set that an item has been loaded
1059     *
1060     * @internal Only public for use in UserFactory
1061     *
1062     * @param string $item
1063     */
1064    public function setItemLoaded( $item ) {
1065        if ( is_array( $this->mLoadedItems ) ) {
1066            $this->mLoadedItems[$item] = true;
1067        }
1068    }
1069
1070    /**
1071     * Load user data from the session.
1072     *
1073     * @return bool True if the user is logged in, false otherwise.
1074     */
1075    private function loadFromSession() {
1076        // MediaWiki\Session\Session already did the necessary authentication of the user
1077        // returned here, so just use it if applicable.
1078        $session = $this->getRequest()->getSession();
1079        $user = $session->getUser();
1080        if ( $user->isRegistered() ) {
1081            $this->loadFromUserObject( $user );
1082
1083            // Other code expects these to be set in the session, so set them.
1084            $session->set( 'wsUserID', $this->getId() );
1085            $session->set( 'wsUserName', $this->getName() );
1086            $session->set( 'wsToken', $this->getToken() );
1087
1088            return true;
1089        }
1090
1091        return false;
1092    }
1093
1094    /**
1095     * Load user data from the database.
1096     * $this->mId must be set, this is how the user is identified.
1097     *
1098     * @param int $flags IDBAccessObject::READ_* constant bitfield
1099     * @return bool True if the user exists, false if the user is anonymous
1100     */
1101    public function loadFromDatabase( $flags = IDBAccessObject::READ_LATEST ) {
1102        // Paranoia
1103        $this->mId = intval( $this->mId );
1104
1105        if ( !$this->mId ) {
1106            // Anonymous users are not in the database
1107            $this->loadDefaults();
1108            return false;
1109        }
1110
1111        $db = DBAccessObjectUtils::getDBFromRecency(
1112            MediaWikiServices::getInstance()->getDBLoadBalancerFactory(),
1113            $flags
1114        );
1115        $row = self::newQueryBuilder( $db )
1116            ->where( [ 'user_id' => $this->mId ] )
1117            ->recency( $flags )
1118            ->caller( __METHOD__ )
1119            ->fetchRow();
1120
1121        $this->queryFlagsUsed = $flags;
1122
1123        if ( $row !== false ) {
1124            // Initialise user table data
1125            $this->loadFromRow( $row );
1126            return true;
1127        }
1128
1129        // Invalid user_id
1130        $this->mId = 0;
1131        $this->loadDefaults( 'Unknown user' );
1132
1133        return false;
1134    }
1135
1136    /**
1137     * Initialize this object from a row from the user table.
1138     *
1139     * @param stdClass $row Row from the user table to load.
1140     * @param array|null $data Further user data to load into the object
1141     *
1142     *  user_groups   Array of arrays or stdClass result rows out of the user_groups
1143     *                table. Previously you were supposed to pass an array of strings
1144     *                here, but we also need expiry info nowadays, so an array of
1145     *                strings is ignored.
1146     */
1147    protected function loadFromRow( $row, $data = null ) {
1148        if ( !( $row instanceof stdClass ) ) {
1149            throw new InvalidArgumentException( '$row must be an object' );
1150        }
1151
1152        $all = true;
1153
1154        if ( isset( $row->actor_id ) ) {
1155            $this->mActorId = (int)$row->actor_id;
1156            if ( $this->mActorId !== 0 ) {
1157                $this->mFrom = 'actor';
1158            }
1159            $this->setItemLoaded( 'actor' );
1160        } else {
1161            $all = false;
1162        }
1163
1164        if ( isset( $row->user_name ) && $row->user_name !== '' ) {
1165            $this->mName = $row->user_name;
1166            $this->mFrom = 'name';
1167            $this->setItemLoaded( 'name' );
1168        } else {
1169            $all = false;
1170        }
1171
1172        if ( isset( $row->user_real_name ) ) {
1173            $this->mRealName = $row->user_real_name;
1174            $this->setItemLoaded( 'realname' );
1175        } else {
1176            $all = false;
1177        }
1178
1179        if ( isset( $row->user_id ) ) {
1180            $this->mId = intval( $row->user_id );
1181            if ( $this->mId !== 0 ) {
1182                $this->mFrom = 'id';
1183            }
1184            $this->setItemLoaded( 'id' );
1185        } else {
1186            $all = false;
1187        }
1188
1189        if ( isset( $row->user_editcount ) ) {
1190            // Don't try to set edit count for anonymous users
1191            // We check the id here and not in UserEditTracker because calling
1192            // User::getId() can trigger some other loading. This will result in
1193            // discarding the user_editcount field for rows if the id wasn't set.
1194            if ( $this->mId !== null && $this->mId !== 0 ) {
1195                MediaWikiServices::getInstance()
1196                    ->getUserEditTracker()
1197                    ->setCachedUserEditCount( $this, (int)$row->user_editcount );
1198            }
1199        } else {
1200            $all = false;
1201        }
1202
1203        if ( isset( $row->user_touched ) ) {
1204            $this->mTouched = wfTimestamp( TS::MW, $row->user_touched );
1205        } else {
1206            $all = false;
1207        }
1208
1209        if ( isset( $row->user_token ) ) {
1210            // The definition for the column is binary(32), so trim the NULs
1211            // that appends. The previous definition was char(32), so trim
1212            // spaces too.
1213            $this->mToken = rtrim( $row->user_token, " \0" );
1214            if ( $this->mToken === '' ) {
1215                $this->mToken = null;
1216            }
1217        } else {
1218            $all = false;
1219        }
1220
1221        if ( isset( $row->user_email ) ) {
1222            $this->mEmail = $row->user_email;
1223            $this->mEmailAuthenticated = wfTimestampOrNull( TS::MW, $row->user_email_authenticated );
1224            $this->mEmailToken = $row->user_email_token;
1225            $this->mEmailTokenExpires = wfTimestampOrNull( TS::MW, $row->user_email_token_expires );
1226            $registration = wfTimestampOrNull( TS::MW, $row->user_registration );
1227            MediaWikiServices::getInstance()
1228                ->getUserRegistrationLookup()
1229                ->setCachedRegistration( $this, $registration );
1230        } else {
1231            $all = false;
1232        }
1233
1234        if ( $all ) {
1235            $this->mLoadedItems = true;
1236        }
1237
1238        if ( is_array( $data ) ) {
1239
1240            if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) {
1241                MediaWikiServices::getInstance()
1242                    ->getUserGroupManager()
1243                    ->loadGroupMembershipsFromArray(
1244                        $this,
1245                        $data['user_groups'],
1246                        $this->queryFlagsUsed
1247                    );
1248            }
1249        }
1250    }
1251
1252    /**
1253     * Load the data for this user object from another user object.
1254     *
1255     * @param User $user
1256     */
1257    protected function loadFromUserObject( $user ) {
1258        $user->load();
1259        foreach ( self::$mCacheVars as $var ) {
1260            $this->$var = $user->$var;
1261        }
1262    }
1263
1264    /**
1265     * Build additional update conditions to protect against race conditions using a compare-and-set
1266     * (CAS) mechanism based on comparing $this->mTouched with the user_touched field.
1267     *
1268     * @param IReadableDatabase $db
1269     * @return array WHERE conditions for use with Database::update
1270     */
1271    protected function makeUpdateConditions( IReadableDatabase $db ) {
1272        if ( $this->mTouched ) {
1273            // CAS check: only update if the row wasn't changed since it was loaded.
1274            return [ 'user_touched' => $db->timestamp( $this->mTouched ) ];
1275        }
1276        return [];
1277    }
1278
1279    /**
1280     * Bump user_touched if it didn't change since this object was loaded
1281     *
1282     * On success, the mTouched field is updated.
1283     * The user serialization cache is always cleared.
1284     *
1285     * @internal
1286     * @return bool Whether user_touched was actually updated
1287     * @since 1.26
1288     */
1289    public function checkAndSetTouched() {
1290        $this->load();
1291
1292        if ( !$this->mId ) {
1293            return false; // anon
1294        }
1295
1296        // Get a new user_touched that is higher than the old one
1297        $newTouched = $this->newTouchedTimestamp();
1298
1299        $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
1300        $dbw->newUpdateQueryBuilder()
1301            ->update( 'user' )
1302            ->set( [ 'user_touched' => $dbw->timestamp( $newTouched ) ] )
1303            ->where( [ 'user_id' => $this->mId ] )
1304            ->andWhere( $this->makeUpdateConditions( $dbw ) )
1305            ->caller( __METHOD__ )->execute();
1306        $success = ( $dbw->affectedRows() > 0 );
1307
1308        if ( $success ) {
1309            $this->mTouched = $newTouched;
1310            $this->clearSharedCache( 'changed' );
1311        } else {
1312            // Clears on failure too since that is desired if the cache is stale
1313            $this->clearSharedCache( 'refresh' );
1314        }
1315
1316        return $success;
1317    }
1318
1319    /**
1320     * Clear various cached data stored in this object. The cache of the user table
1321     * data (i.e. self::$mCacheVars) is not cleared unless $reloadFrom is given.
1322     *
1323     * @param bool|string $reloadFrom Reload user and user_groups table data from a
1324     *   given source. May be "name", "id", "actor", "defaults", "session", or false for no reload.
1325     */
1326    public function clearInstanceCache( $reloadFrom = false ) {
1327        global $wgFullyInitialised;
1328
1329        $this->mDatePreference = null;
1330        $this->mThisAsAuthority = null;
1331
1332        if ( $wgFullyInitialised && $this->mFrom ) {
1333            $services = MediaWikiServices::getInstance();
1334
1335            if ( $services->peekService( 'PermissionManager' ) ) {
1336                $services->getPermissionManager()->invalidateUsersRightsCache( $this );
1337            }
1338
1339            if ( $services->peekService( 'UserOptionsManager' ) ) {
1340                $services->getUserOptionsManager()->clearUserOptionsCache( $this );
1341            }
1342
1343            if ( $services->peekService( 'TalkPageNotificationManager' ) ) {
1344                $services->getTalkPageNotificationManager()->clearInstanceCache( $this );
1345            }
1346
1347            if ( $services->peekService( 'UserGroupManager' ) ) {
1348                $services->getUserGroupManager()->clearCache( $this );
1349            }
1350
1351            if ( $services->peekService( 'UserEditTracker' ) ) {
1352                $services->getUserEditTracker()->clearUserEditCache( $this );
1353            }
1354
1355            if ( $services->peekService( 'BlockManager' ) ) {
1356                $services->getBlockManager()->clearUserCache( $this );
1357            }
1358        }
1359
1360        if ( $reloadFrom ) {
1361            if ( in_array( $reloadFrom, [ 'name', 'id', 'actor' ] ) ) {
1362                $this->mLoadedItems = [ $reloadFrom => true ];
1363            } else {
1364                $this->mLoadedItems = [];
1365            }
1366            $this->mFrom = $reloadFrom;
1367        }
1368    }
1369
1370    /**
1371     * Is this user subject to rate limiting?
1372     *
1373     * @return bool True if rate limited
1374     */
1375    public function isPingLimitable() {
1376        $limiter = MediaWikiServices::getInstance()->getRateLimiter();
1377        $subject = $this->toRateLimitSubject();
1378        return !$limiter->isExempt( $subject );
1379    }
1380
1381    /**
1382     * Primitive rate limits: enforce maximum actions per time period
1383     * to put a brake on flooding.
1384     *
1385     * The method generates both a generic profiling point and a per action one
1386     * (suffix being "-$action").
1387     *
1388     * @note When using a shared cache like memcached, IP-address
1389     * last-hit counters will be shared across wikis.
1390     *
1391     * @param string $action Action to enforce; 'edit' if unspecified
1392     * @param int $incrBy Positive amount to increment counter by [defaults to 1]
1393     *
1394     * @return bool True if a rate limiter was tripped
1395     */
1396    public function pingLimiter( $action = 'edit', $incrBy = 1 ) {
1397        return $this->getThisAsAuthority()->limit( $action, $incrBy, null );
1398    }
1399
1400    /**
1401     * @internal for use by UserAuthority only!
1402     * @return RateLimitSubject
1403     */
1404    public function toRateLimitSubject(): RateLimitSubject {
1405        $flags = [
1406            'exempt' => $this->isAllowed( 'noratelimit' ),
1407            'newbie' => $this->isNewbie(),
1408        ];
1409
1410        return new RateLimitSubject( $this, $this->getRequest()->getIP(), $flags );
1411    }
1412
1413    /**
1414     * Get the block affecting the user, or null if the user is not blocked
1415     *
1416     * @param int|bool $freshness One of the IDBAccessObject::READ_XXX constants.
1417     *                 For backwards compatibility, a boolean is also accepted,
1418     *                 with true meaning READ_NORMAL and false meaning
1419     *                 READ_LATEST.
1420     * @param bool $disableIpBlockExemptChecking This is used internally to prevent
1421     *   a infinite recursion with autopromote. See T270145.
1422     *
1423     * @return ?AbstractBlock
1424     */
1425    public function getBlock(
1426        $freshness = IDBAccessObject::READ_NORMAL,
1427        $disableIpBlockExemptChecking = false
1428    ): ?Block {
1429        if ( is_bool( $freshness ) ) {
1430            $fromReplica = $freshness;
1431        } else {
1432            $fromReplica = ( $freshness !== IDBAccessObject::READ_LATEST );
1433        }
1434
1435        if ( $disableIpBlockExemptChecking ) {
1436            $isExempt = false;
1437        } else {
1438            $isExempt = $this->isAllowed( 'ipblock-exempt' );
1439        }
1440
1441        // TODO: Block checking shouldn't really be done from the User object. Block
1442        // checking can involve checking for IP blocks, cookie blocks, and/or XFF blocks,
1443        // which need more knowledge of the request context than the User should have.
1444        // Since we do currently check blocks from the User, we have to do the following
1445        // here:
1446        // - Check if this is the user associated with the main request
1447        // - If so, pass the relevant request information to the block manager
1448        $request = null;
1449        if ( !$isExempt && $this->isGlobalSessionUser() ) {
1450            // This is the global user, so we need to pass the request
1451            $request = $this->getRequest();
1452        }
1453
1454        return MediaWikiServices::getInstance()->getBlockManager()->getBlock(
1455            $this,
1456            $request,
1457            $fromReplica,
1458        );
1459    }
1460
1461    /**
1462     * Check if user account is locked
1463     *
1464     * @return bool True if locked, false otherwise
1465     */
1466    public function isLocked() {
1467        if ( $this->mLocked !== null ) {
1468            return $this->mLocked;
1469        }
1470        // Reset for hook
1471        $this->mLocked = false;
1472        $this->getHookRunner()->onUserIsLocked( $this, $this->mLocked );
1473        return $this->mLocked;
1474    }
1475
1476    /**
1477     * Check if user account is hidden
1478     *
1479     * @return bool True if hidden, false otherwise
1480     */
1481    public function isHidden() {
1482        $block = $this->getBlock();
1483        return $block ? $block->getHideName() : false;
1484    }
1485
1486    /**
1487     * Get the user's ID.
1488     * @param string|false $wikiId The wiki ID expected by the caller.
1489     * @return int The user's ID; 0 if the user is anonymous or nonexistent
1490     */
1491    public function getId( $wikiId = self::LOCAL ): int {
1492        $this->assertWiki( $wikiId );
1493        if ( $this->mId === null && $this->mName !== null ) {
1494            $userNameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
1495            if ( $userNameUtils->isIP( $this->mName ) || ExternalUserNames::isExternal( $this->mName ) ) {
1496                // Special case, we know the user is anonymous
1497                // Note that "external" users are "local" (they have an actor ID that is relative to
1498                // the local wiki).
1499                return 0;
1500            }
1501        }
1502
1503        if ( !$this->isItemLoaded( 'id' ) ) {
1504            // Don't load if this was initialized from an ID
1505            $this->load();
1506        }
1507
1508        return (int)$this->mId;
1509    }
1510
1511    /**
1512     * Set the user and reload all fields according to a given ID
1513     * @param int $v User ID to reload
1514     */
1515    public function setId( $v ) {
1516        $this->mId = $v;
1517        $this->clearInstanceCache( 'id' );
1518    }
1519
1520    /**
1521     * Get the user name, or the IP of an anonymous user
1522     * @return string User's name or IP address
1523     */
1524    public function getName(): string {
1525        if ( $this->isItemLoaded( 'name', 'only' ) ) {
1526            // Special case optimisation
1527            return $this->mName;
1528        }
1529
1530        $this->load();
1531        if ( $this->mName === false ) {
1532            // Clean up IPs
1533            $this->mName = IPUtils::sanitizeIP( $this->getRequest()->getIP() );
1534        }
1535
1536        return $this->mName;
1537    }
1538
1539    /**
1540     * Set the user name.
1541     *
1542     * This does not reload fields from the database according to the given
1543     * name. Rather, it is used to create a temporary "nonexistent user" for
1544     * later addition to the database. It can also be used to set the IP
1545     * address for an anonymous user to something other than the current
1546     * remote IP.
1547     *
1548     * @note User::newFromName() has roughly the same function, when the named user
1549     * does not exist.
1550     * @param string $str New user name to set
1551     */
1552    public function setName( $str ) {
1553        $this->load();
1554        $this->mName = $str;
1555    }
1556
1557    /**
1558     * Get the user's actor ID.
1559     * @since 1.31
1560     * @note This method was removed from the UserIdentity interface in 1.36,
1561     *       but remains supported in the User class for now.
1562     *       New code should use ActorNormalization::findActorId() or
1563     *       ActorNormalization::acquireActorId() instead.
1564     * @param IDatabase|string|false $dbwOrWikiId Deprecated since 1.36.
1565     *        If a database connection is passed, a new actor ID is assigned if needed.
1566     *        ActorNormalization::acquireActorId() should be used for that purpose instead.
1567     * @return int The actor's ID, or 0 if no actor ID exists and $dbw was null
1568     * @throws PreconditionException if $dbwOrWikiId is a string and does not match the local wiki
1569     */
1570    public function getActorId( $dbwOrWikiId = self::LOCAL ): int {
1571        if ( $dbwOrWikiId ) {
1572            wfDeprecatedMsg( 'Passing a parameter to getActorId() is deprecated', '1.36' );
1573        }
1574
1575        if ( is_string( $dbwOrWikiId ) ) {
1576            $this->assertWiki( $dbwOrWikiId );
1577        }
1578
1579        if ( !$this->isItemLoaded( 'actor' ) ) {
1580            $this->load();
1581        }
1582
1583        if ( !$this->mActorId && $dbwOrWikiId instanceof IDatabase ) {
1584            MediaWikiServices::getInstance()
1585                ->getActorStoreFactory()
1586                ->getActorNormalization( $dbwOrWikiId->getDomainID() )
1587                ->acquireActorId( $this, $dbwOrWikiId );
1588            // acquireActorId will call setActorId on $this
1589            Assert::postcondition(
1590                $this->mActorId !== null,
1591                "Failed to acquire actor ID for user id {$this->mId} name {$this->mName}"
1592            );
1593        }
1594
1595        return (int)$this->mActorId;
1596    }
1597
1598    /**
1599     * Sets the actor id.
1600     * For use by ActorStore only.
1601     * Should be removed once callers of getActorId() have been migrated to using ActorNormalization.
1602     *
1603     * @internal
1604     * @deprecated since 1.36
1605     * @param int $actorId
1606     */
1607    public function setActorId( int $actorId ) {
1608        $this->mActorId = $actorId;
1609        $this->setItemLoaded( 'actor' );
1610    }
1611
1612    /**
1613     * Get the user's name escaped by underscores.
1614     * @return string Username escaped by underscores.
1615     */
1616    public function getTitleKey(): string {
1617        return str_replace( ' ', '_', $this->getName() );
1618    }
1619
1620    /**
1621     * Generate a current or new-future timestamp to be stored in the
1622     * user_touched field when we update things.
1623     *
1624     * @return string Timestamp in TS::MW format
1625     */
1626    private function newTouchedTimestamp() {
1627        $time = (int)ConvertibleTimestamp::now( TS::UNIX );
1628        if ( $this->mTouched ) {
1629            $time = max( $time, (int)ConvertibleTimestamp::convert( TS::UNIX, $this->mTouched ) + 1 );
1630        }
1631
1632        return ConvertibleTimestamp::convert( TS::MW, $time );
1633    }
1634
1635    /**
1636     * Clear user data from memcached
1637     *
1638     * Use after applying updates to the database; caller's
1639     * responsibility to update user_touched if appropriate.
1640     *
1641     * Called implicitly from invalidateCache() and saveSettings().
1642     *
1643     * @param string $mode Use 'refresh' to clear now or 'changed' to clear before DB commit
1644     */
1645    public function clearSharedCache( $mode = 'refresh' ) {
1646        if ( !$this->getId() ) {
1647            return;
1648        }
1649
1650        $dbProvider = MediaWikiServices::getInstance()->getConnectionProvider();
1651        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1652        $key = $this->getCacheKey( $cache );
1653
1654        if ( $mode === 'refresh' ) {
1655            $cache->delete( $key, 1 ); // low tombstone/"hold-off" TTL
1656        } else {
1657            $dbProvider->getPrimaryDatabase()->onTransactionPreCommitOrIdle(
1658                static function () use ( $cache, $key ) {
1659                    $cache->delete( $key );
1660                },
1661                __METHOD__
1662            );
1663        }
1664    }
1665
1666    /**
1667     * Immediately touch the user data cache for this account
1668     *
1669     * Calls touch() and removes account data from memcached
1670     */
1671    public function invalidateCache() {
1672        $this->touch();
1673        $this->clearSharedCache( 'changed' );
1674    }
1675
1676    /**
1677     * Update the "touched" timestamp for the user
1678     *
1679     * This is useful on various login/logout events when making sure that
1680     * a browser or proxy that has multiple tenants does not suffer cache
1681     * pollution where the new user sees the old users content. The value
1682     * of getTouched() is checked when determining 304 vs 200 responses.
1683     * Unlike invalidateCache(), this preserves the User object cache and
1684     * avoids database writes.
1685     *
1686     * @since 1.25
1687     */
1688    public function touch() {
1689        $id = $this->getId();
1690        if ( $id ) {
1691            $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1692            $key = $cache->makeKey( 'user-quicktouched', 'id', $id );
1693            $cache->touchCheckKey( $key );
1694            $this->mQuickTouched = null;
1695        }
1696    }
1697
1698    /**
1699     * Update the db touched timestamp for the user if it hasn't been updated recently
1700     *
1701     * @since 1.45
1702     */
1703    public function debouncedDBTouch() {
1704        $oldTouched = (int)ConvertibleTimestamp::convert( TS::UNIX, $this->getDBTouched() );
1705        $newTouched = (int)ConvertibleTimestamp::now( TS::UNIX );
1706
1707        if ( ( $newTouched - $oldTouched ) < ( 300 + mt_rand( 1, 20 ) ) ) {
1708            // Touched would be updated too soon, skip this round
1709            // Adding jitter to avoid stampede.
1710            return;
1711        }
1712
1713        // Too old, definitely update.
1714        $this->checkAndSetTouched();
1715    }
1716
1717    /**
1718     * Validate the cache for this account.
1719     * @param string $timestamp A timestamp in TS::MW format
1720     * @return bool
1721     */
1722    public function validateCache( $timestamp ) {
1723        return ( $timestamp >= $this->getTouched() );
1724    }
1725
1726    /**
1727     * Get the user touched timestamp
1728     *
1729     * Use this value only to validate caches via inequalities
1730     * such as in the case of HTTP If-Modified-Since response logic
1731     *
1732     * @return string TS::MW Timestamp
1733     */
1734    public function getTouched() {
1735        $this->load();
1736
1737        if ( $this->mId ) {
1738            if ( $this->mQuickTouched === null ) {
1739                $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1740                $key = $cache->makeKey( 'user-quicktouched', 'id', $this->mId );
1741
1742                $this->mQuickTouched = wfTimestamp( TS::MW, $cache->getCheckKeyTime( $key ) );
1743            }
1744
1745            return max( $this->mTouched, $this->mQuickTouched );
1746        }
1747
1748        return $this->mTouched;
1749    }
1750
1751    /**
1752     * Get the user_touched timestamp field (time of last DB updates)
1753     * @return string TS::MW Timestamp
1754     * @since 1.26
1755     */
1756    public function getDBTouched() {
1757        $this->load();
1758
1759        return $this->mTouched;
1760    }
1761
1762    /**
1763     * Changes credentials of the user.
1764     *
1765     * This is a convenience wrapper around AuthManager::changeAuthenticationData.
1766     * Note that this can return a status that isOK() but not isGood() on certain types of failures,
1767     * e.g. when no provider handled the change.
1768     *
1769     * @param array $data A set of authentication data in fieldname => value format. This is the
1770     *   same data you would pass the changeauthenticationdata API - 'username', 'password' etc.
1771     * @return Status
1772     * @since 1.27
1773     */
1774    public function changeAuthenticationData( array $data ) {
1775        $manager = MediaWikiServices::getInstance()->getAuthManager();
1776        $reqs = $manager->getAuthenticationRequests( AuthManager::ACTION_CHANGE, $this );
1777        $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
1778
1779        $status = Status::newGood( 'ignored' );
1780        foreach ( $reqs as $req ) {
1781            $status->merge( $manager->allowsAuthenticationDataChange( $req ), true );
1782        }
1783        if ( $status->getValue() === 'ignored' ) {
1784            $status->warning( 'authenticationdatachange-ignored' );
1785        }
1786
1787        if ( $status->isGood() ) {
1788            foreach ( $reqs as $req ) {
1789                $manager->changeAuthenticationData( $req );
1790            }
1791        }
1792        return $status;
1793    }
1794
1795    /**
1796     * Get the user's current token.
1797     * @param bool $forceCreation Force the generation of a new token if the
1798     *   user doesn't have one (default=true for backwards compatibility).
1799     * @return string|null Token
1800     */
1801    public function getToken( $forceCreation = true ) {
1802        $authenticationTokenVersion = MediaWikiServices::getInstance()
1803            ->getMainConfig()->get( MainConfigNames::AuthenticationTokenVersion );
1804
1805        $this->load();
1806        if ( !$this->mToken && $forceCreation ) {
1807            $this->setToken();
1808        }
1809
1810        if ( !$this->mToken ) {
1811            // The user doesn't have a token, return null to indicate that.
1812            return null;
1813        }
1814
1815        if ( $this->mToken === self::INVALID_TOKEN ) {
1816            // We return a random value here so existing token checks are very
1817            // likely to fail.
1818            return MWCryptRand::generateHex( self::TOKEN_LENGTH );
1819        }
1820
1821        if ( $authenticationTokenVersion === null ) {
1822            // $wgAuthenticationTokenVersion not in use, so return the raw secret
1823            return $this->mToken;
1824        }
1825
1826        // $wgAuthenticationTokenVersion in use, so hmac it.
1827        $ret = MWCryptHash::hmac( $authenticationTokenVersion, $this->mToken, false );
1828
1829        // The raw hash can be overly long. Shorten it up.
1830        $len = max( 32, self::TOKEN_LENGTH );
1831        if ( strlen( $ret ) < $len ) {
1832            // Should never happen, even md5 is 128 bits
1833            throw new \UnexpectedValueException( 'Hmac returned less than 128 bits' );
1834        }
1835
1836        return substr( $ret, -$len );
1837    }
1838
1839    /**
1840     * Set the random token (used for persistent authentication)
1841     * Called from loadDefaults() among other places.
1842     *
1843     * @param string|false $token If specified, set the token to this value
1844     */
1845    public function setToken( $token = false ) {
1846        $this->load();
1847        if ( $this->mToken === self::INVALID_TOKEN ) {
1848            LoggerFactory::getInstance( 'session' )
1849                ->debug( __METHOD__ . ": Ignoring attempt to set token for system user \"$this\"" );
1850        } elseif ( !$token ) {
1851            $this->mToken = MWCryptRand::generateHex( self::TOKEN_LENGTH );
1852        } else {
1853            $this->mToken = $token;
1854        }
1855    }
1856
1857    /**
1858     * Get the user's e-mail address
1859     * @return string User's email address
1860     */
1861    public function getEmail(): string {
1862        $this->load();
1863        $email = $this->mEmail;
1864        $this->getHookRunner()->onUserGetEmail( $this, $email );
1865        // In case a hook handler returns e.g. null
1866        $this->mEmail = is_string( $email ) ? $email : '';
1867        return $this->mEmail;
1868    }
1869
1870    /**
1871     * Get the timestamp of the user's e-mail authentication
1872     * @return string TS::MW timestamp
1873     */
1874    public function getEmailAuthenticationTimestamp() {
1875        $this->load();
1876        $this->getHookRunner()->onUserGetEmailAuthenticationTimestamp(
1877            $this, $this->mEmailAuthenticated );
1878        return $this->mEmailAuthenticated;
1879    }
1880
1881    /**
1882     * Set the user's e-mail address
1883     * @param string $str New e-mail address
1884     */
1885    public function setEmail( string $str ) {
1886        $this->load();
1887        if ( $str == $this->getEmail() ) {
1888            return;
1889        }
1890        $this->invalidateEmail();
1891        $this->mEmail = $str;
1892        $this->getHookRunner()->onUserSetEmail( $this, $this->mEmail );
1893    }
1894
1895    /**
1896     * Set the user's e-mail address and send a confirmation mail if needed.
1897     *
1898     * @since 1.20
1899     * @param string $str New e-mail address
1900     * @return Status
1901     */
1902    public function setEmailWithConfirmation( string $str ) {
1903        $config = MediaWikiServices::getInstance()->getMainConfig();
1904        $enableEmail = $config->get( MainConfigNames::EnableEmail );
1905
1906        if ( !$enableEmail ) {
1907            return Status::newFatal( 'emaildisabled' );
1908        }
1909
1910        $oldaddr = $this->getEmail();
1911        if ( $str === $oldaddr ) {
1912            return Status::newGood( true );
1913        }
1914
1915        $type = $oldaddr != '' ? 'changed' : 'set';
1916        $notificationResult = null;
1917
1918        $emailAuthentication = $config->get( MainConfigNames::EmailAuthentication );
1919
1920        if ( $emailAuthentication && $type === 'changed' && $this->isEmailConfirmed() ) {
1921            // Send the user an email notifying the user of the change in registered
1922            // email address on their previous verified email address
1923            $change = $str != '' ? 'changed' : 'removed';
1924            $notificationResult = $this->sendMail(
1925                wfMessage( 'notificationemail_subject_' . $change )->text(),
1926                wfMessage( 'notificationemail_body_' . $change,
1927                    $this->getRequest()->getIP(),
1928                    $this->getName(),
1929                    $str )->text()
1930            );
1931        }
1932
1933        $this->setEmail( $str );
1934
1935        if ( $str !== '' && $emailAuthentication ) {
1936            // Send a confirmation request to the new address if needed
1937            $result = $this->sendConfirmationMail( $type );
1938
1939            if ( $notificationResult !== null ) {
1940                $result->merge( $notificationResult );
1941            }
1942
1943            if ( $result->isGood() ) {
1944                // Say to the caller that a confirmation and notification mail has been sent
1945                $result->value = 'eauth';
1946            }
1947        } else {
1948            $result = Status::newGood( true );
1949        }
1950
1951        return $result;
1952    }
1953
1954    /**
1955     * Get the user's real name
1956     * @return string User's real name
1957     */
1958    public function getRealName(): string {
1959        if ( !$this->isItemLoaded( 'realname' ) ) {
1960            $this->load();
1961        }
1962
1963        return $this->mRealName;
1964    }
1965
1966    /**
1967     * Set the user's real name
1968     * @param string $str New real name
1969     */
1970    public function setRealName( string $str ) {
1971        $this->load();
1972        $this->mRealName = $str;
1973    }
1974
1975    /**
1976     * Get a token stored in the preferences (like the watchlist one),
1977     * resetting it if it's empty (and saving changes).
1978     *
1979     * @param string $oname The option name to retrieve the token from
1980     * @return string|false User's current value for the option, or false if this option is disabled.
1981     * @see resetTokenFromOption()
1982     * @see getOption()
1983     * @deprecated since 1.26 Applications should use the OAuth extension
1984     */
1985    public function getTokenFromOption( $oname ) {
1986        $hiddenPrefs =
1987            MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::HiddenPrefs );
1988
1989        $id = $this->getId();
1990        if ( !$id || in_array( $oname, $hiddenPrefs ) ) {
1991            return false;
1992        }
1993
1994        $userOptionsLookup = MediaWikiServices::getInstance()
1995            ->getUserOptionsLookup();
1996        $token = $userOptionsLookup->getOption( $this, (string)$oname );
1997        if ( !$token ) {
1998            // Default to a value based on the user token to avoid space
1999            // wasted on storing tokens for all users. When this option
2000            // is set manually by the user, only then is it stored.
2001            $token = hash_hmac( 'sha1', "$oname:$id", $this->getToken() );
2002        }
2003
2004        return $token;
2005    }
2006
2007    /**
2008     * Reset a token stored in the preferences (like the watchlist one).
2009     * *Does not* save user's preferences (similarly to UserOptionsManager::setOption()).
2010     *
2011     * @param string $oname The option name to reset the token in
2012     * @return string|false New token value, or false if this option is disabled.
2013     * @see getTokenFromOption()
2014     * @see \MediaWiki\User\Options\UserOptionsManager::setOption
2015     */
2016    public function resetTokenFromOption( $oname ) {
2017        $hiddenPrefs =
2018            MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::HiddenPrefs );
2019        if ( in_array( $oname, $hiddenPrefs ) ) {
2020            return false;
2021        }
2022
2023        $token = MWCryptRand::generateHex( 40 );
2024        MediaWikiServices::getInstance()
2025            ->getUserOptionsManager()
2026            ->setOption( $this, $oname, $token );
2027        return $token;
2028    }
2029
2030    /**
2031     * Get the user's preferred date format.
2032     * @return string User's preferred date format
2033     */
2034    public function getDatePreference() {
2035        // Important migration for old data rows
2036        if ( $this->mDatePreference === null ) {
2037            global $wgLang;
2038            $userOptionsLookup = MediaWikiServices::getInstance()
2039                ->getUserOptionsLookup();
2040            $value = $userOptionsLookup->getOption( $this, 'date' ) ?? 'default';
2041            $map = $wgLang->getDatePreferenceMigrationMap();
2042            if ( isset( $map[$value] ) ) {
2043                $value = $map[$value];
2044            }
2045            $this->mDatePreference = $value;
2046        }
2047        return $this->mDatePreference;
2048    }
2049
2050    /**
2051     * Determine based on the wiki configuration and the user's options,
2052     * whether this user must be over HTTPS no matter what.
2053     *
2054     * @return bool
2055     */
2056    public function requiresHTTPS() {
2057        if ( !$this->isRegistered() ) {
2058            return false;
2059        }
2060
2061        $services = MediaWikiServices::getInstance();
2062        $config = $services->getMainConfig();
2063        if ( $config->get( MainConfigNames::ForceHTTPS ) ) {
2064            return true;
2065        }
2066        if ( !$config->get( MainConfigNames::SecureLogin ) ) {
2067            return false;
2068        }
2069        return $services->getUserOptionsLookup()
2070            ->getBoolOption( $this, 'prefershttps' );
2071    }
2072
2073    /**
2074     * Get the user's edit count.
2075     * @return int|null Null for anonymous users
2076     */
2077    public function getEditCount() {
2078        return MediaWikiServices::getInstance()
2079            ->getUserEditTracker()
2080            ->getUserEditCount( $this );
2081    }
2082
2083    /**
2084     * Get whether the user is registered.
2085     *
2086     * @return bool True if user is registered on this wiki, i.e., has a user ID. False if user is
2087     *   anonymous or has no local account (which can happen when importing). This is equivalent to
2088     *   getId() != 0 and is provided for code readability.
2089     * @since 1.34
2090     */
2091    public function isRegistered(): bool {
2092        return $this->getId() != 0;
2093    }
2094
2095    /**
2096     * Get whether the user is anonymous
2097     * @return bool
2098     */
2099    public function isAnon() {
2100        return !$this->isRegistered();
2101    }
2102
2103    /**
2104     * @return bool Whether this user is flagged as being a bot role account
2105     * @since 1.28
2106     */
2107    public function isBot() {
2108        $userGroupManager = MediaWikiServices::getInstance()->getUserGroupManager();
2109        if ( in_array( 'bot', $userGroupManager->getUserGroups( $this ) )
2110            && $this->isAllowed( 'bot' )
2111        ) {
2112            return true;
2113        }
2114
2115        $isBot = false;
2116        $this->getHookRunner()->onUserIsBot( $this, $isBot );
2117
2118        return $isBot;
2119    }
2120
2121    /**
2122     * Get whether the user is a system user
2123     *
2124     * A user is considered to exist as a non-system user if it can
2125     * authenticate, or has an email set, or has a non-invalid token.
2126     *
2127     * @return bool Whether this user is a system user
2128     * @since 1.35
2129     */
2130    public function isSystemUser() {
2131        $this->load();
2132        if ( $this->getEmail() || $this->mToken !== self::INVALID_TOKEN ||
2133            MediaWikiServices::getInstance()->getAuthManager()->userCanAuthenticate( $this->mName )
2134        ) {
2135            return false;
2136        }
2137        return true;
2138    }
2139
2140    /** @inheritDoc */
2141    public function isAllowedAny( ...$permissions ): bool {
2142        return $this->getThisAsAuthority()->isAllowedAny( ...$permissions );
2143    }
2144
2145    /** @inheritDoc */
2146    public function isAllowedAll( ...$permissions ): bool {
2147        return $this->getThisAsAuthority()->isAllowedAll( ...$permissions );
2148    }
2149
2150    public function isAllowed( string $permission, ?PermissionStatus $status = null ): bool {
2151        return $this->getThisAsAuthority()->isAllowed( $permission, $status );
2152    }
2153
2154    /**
2155     * Check whether to enable recent changes patrol features for this user
2156     * @return bool True or false
2157     */
2158    public function useRCPatrol() {
2159        $useRCPatrol =
2160            MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::UseRCPatrol );
2161        return $useRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' );
2162    }
2163
2164    /**
2165     * Check whether to enable new pages patrol features for this user
2166     * @return bool True or false
2167     */
2168    public function useNPPatrol() {
2169        $useRCPatrol =
2170            MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::UseRCPatrol );
2171        $useNPPatrol =
2172            MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::UseNPPatrol );
2173        return (
2174            ( $useRCPatrol || $useNPPatrol )
2175                && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
2176        );
2177    }
2178
2179    /**
2180     * Check whether to enable new files patrol features for this user
2181     * @return bool True or false
2182     */
2183    public function useFilePatrol() {
2184        $useRCPatrol =
2185            MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::UseRCPatrol );
2186        $useFilePatrol = MediaWikiServices::getInstance()->getMainConfig()
2187            ->get( MainConfigNames::UseFilePatrol );
2188        return (
2189            ( $useRCPatrol || $useFilePatrol )
2190                && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
2191        );
2192    }
2193
2194    /**
2195     * Get the WebRequest object to use with this object
2196     */
2197    public function getRequest(): WebRequest {
2198        return $this->mRequest ?? RequestContext::getMain()->getRequest();
2199    }
2200
2201    /**
2202     * Compute experienced level based on edit count and registration date.
2203     *
2204     * @return string|false 'newcomer', 'learner', or 'experienced', false for anonymous users
2205     */
2206    public function getExperienceLevel() {
2207        $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
2208        $learnerEdits = $mainConfig->get( MainConfigNames::LearnerEdits );
2209        $experiencedUserEdits = $mainConfig->get( MainConfigNames::ExperiencedUserEdits );
2210        $learnerMemberSince = $mainConfig->get( MainConfigNames::LearnerMemberSince );
2211        $experiencedUserMemberSince =
2212            $mainConfig->get( MainConfigNames::ExperiencedUserMemberSince );
2213        if ( $this->isAnon() ) {
2214            return false;
2215        }
2216
2217        $editCount = $this->getEditCount();
2218        $registration = $this->getRegistration();
2219        $now = time();
2220        $learnerRegistration = wfTimestamp( TS::MW, $now - $learnerMemberSince * 86400 );
2221        $experiencedRegistration = wfTimestamp( TS::MW, $now - $experiencedUserMemberSince * 86400 );
2222        if ( $registration === null ) {
2223            // for some very old accounts, this information is missing in the database
2224            // treat them as old enough to be 'experienced'
2225            $registration = $experiencedRegistration;
2226        }
2227
2228        if ( $editCount < $learnerEdits ||
2229        $registration > $learnerRegistration ) {
2230            return 'newcomer';
2231        }
2232
2233        if ( $editCount > $experiencedUserEdits &&
2234            $registration <= $experiencedRegistration
2235        ) {
2236            return 'experienced';
2237        }
2238
2239        return 'learner';
2240    }
2241
2242    /**
2243     * Persist this user's session (e.g. set cookies)
2244     *
2245     * @param WebRequest|null $request WebRequest object to use; the global request
2246     *        will be used if null is passed.
2247     * @param bool|null $secure Whether to force secure/insecure cookies or use default
2248     * @param bool $rememberMe Whether to add a Token cookie for elongated sessions
2249     */
2250    public function setCookies( $request = null, $secure = null, $rememberMe = false ) {
2251        $this->load();
2252        if ( $this->mId == 0 ) {
2253            return;
2254        }
2255
2256        $session = $this->getRequest()->getSession();
2257        if ( $request && $session->getRequest() !== $request ) {
2258            $session = $session->sessionWithRequest( $request );
2259        }
2260        $delay = $session->delaySave();
2261
2262        if ( !$session->getUser()->equals( $this ) ) {
2263            if ( !$session->canSetUser() ) {
2264                LoggerFactory::getInstance( 'session' )
2265                    ->warning( __METHOD__ .
2266                        ": Cannot save user \"$this\" to a user " .
2267                        "\"{$session->getUser()}\"'s immutable session"
2268                    );
2269                return;
2270            }
2271            $session->setUser( $this );
2272        }
2273
2274        $session->setRememberUser( $rememberMe );
2275        if ( $secure !== null ) {
2276            $session->setForceHTTPS( $secure );
2277        }
2278
2279        $session->persist();
2280
2281        ScopedCallback::consume( $delay );
2282    }
2283
2284    /**
2285     * Log this user out.
2286     */
2287    public function logout() {
2288        if ( $this->getHookRunner()->onUserLogout( $this ) ) {
2289            $this->doLogout();
2290        }
2291    }
2292
2293    /**
2294     * Clear the user's session, and reset the instance cache.
2295     * @see logout()
2296     */
2297    public function doLogout() {
2298        $session = $this->getRequest()->getSession();
2299        $accountType = MediaWikiServices::getInstance()->getUserIdentityUtils()->getShortUserTypeInternal( $this );
2300        if ( !$session->canSetUser() ) {
2301            LoggerFactory::getInstance( 'session' )
2302                ->warning( __METHOD__ . ": Cannot log out of an immutable session" );
2303            $error = 'immutable';
2304        } elseif ( !$session->getUser()->equals( $this ) ) {
2305            LoggerFactory::getInstance( 'session' )
2306                ->warning( __METHOD__ .
2307                    ": Cannot log user \"$this\" out of a user \"{$session->getUser()}\"'s session"
2308                );
2309            // But we still may as well make this user object anon
2310            $this->clearInstanceCache( 'defaults' );
2311            $error = 'wronguser';
2312        } else {
2313            $this->clearInstanceCache( 'defaults' );
2314            $delay = $session->delaySave();
2315            $session->unpersist(); // Clear cookies (T127436)
2316            $session->setLoggedOutTimestamp( time() );
2317            $session->setUser( new User );
2318            $session->set( 'wsUserID', 0 ); // Other code expects this
2319            $session->resetAllTokens();
2320            ScopedCallback::consume( $delay );
2321            $error = false;
2322        }
2323        LoggerFactory::getInstance( 'authevents' )->info( 'Logout', [
2324            'event' => 'logout',
2325            'successful' => $error === false,
2326            'status' => $error ?: 'success',
2327            'accountType' => $accountType,
2328        ] );
2329    }
2330
2331    /**
2332     * Save this user's settings into the database.
2333     * @todo Only rarely do all these fields need to be set!
2334     */
2335    public function saveSettings() {
2336        if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) {
2337            // @TODO: caller should deal with this instead!
2338            // This should really just be an exception.
2339            MWExceptionHandler::logException( new DBExpectedError(
2340                null,
2341                "Could not update user with ID '{$this->mId}'; DB is read-only."
2342            ) );
2343            return;
2344        }
2345
2346        $this->load();
2347        if ( $this->mId == 0 ) {
2348            return; // anon
2349        }
2350
2351        // Get a new user_touched that is higher than the old one.
2352        // This will be used for a CAS check as a last-resort safety
2353        // check against race conditions and replica DB lag.
2354        $newTouched = $this->newTouchedTimestamp();
2355
2356        $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
2357        $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) use ( $newTouched ) {
2358            $dbw->newUpdateQueryBuilder()
2359                ->update( 'user' )
2360                ->set( [
2361                    'user_name' => $this->mName,
2362                    'user_real_name' => $this->mRealName,
2363                    'user_email' => $this->mEmail,
2364                    'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2365                    'user_touched' => $dbw->timestamp( $newTouched ),
2366                    'user_token' => strval( $this->mToken ),
2367                    'user_email_token' => $this->mEmailToken,
2368                    'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
2369                ] )
2370                ->where( [ 'user_id' => $this->mId ] )
2371                ->andWhere( $this->makeUpdateConditions( $dbw ) )
2372                ->caller( $fname )->execute();
2373
2374            if ( !$dbw->affectedRows() ) {
2375                // Maybe the problem was a missed cache update; clear it to be safe
2376                $this->clearSharedCache( 'refresh' );
2377                // User was changed in the meantime or loaded with stale data
2378                $from = ( $this->queryFlagsUsed & IDBAccessObject::READ_LATEST ) ? 'primary' : 'replica';
2379                LoggerFactory::getInstance( 'preferences' )->warning(
2380                    "CAS update failed on user_touched for user ID '{user_id}' ({db_flag} read)",
2381                    [ 'user_id' => $this->mId, 'db_flag' => $from ]
2382                );
2383                throw new RuntimeException( "CAS update failed on user_touched. " .
2384                    "The version of the user to be saved is older than the current version."
2385                );
2386            }
2387
2388            $dbw->newUpdateQueryBuilder()
2389                ->update( 'actor' )
2390                ->set( [ 'actor_name' => $this->mName ] )
2391                ->where( [ 'actor_user' => $this->mId ] )
2392                ->caller( $fname )->execute();
2393            MediaWikiServices::getInstance()->getActorStore()->deleteUserIdentityFromCache( $this );
2394        } );
2395
2396        $this->mTouched = $newTouched;
2397        if ( $this->isNamed() ) {
2398            MediaWikiServices::getInstance()->getUserOptionsManager()->saveOptionsInternal( $this );
2399        }
2400
2401        $this->getHookRunner()->onUserSaveSettings( $this );
2402        $this->clearSharedCache( 'changed' );
2403        $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2404        $hcu->purgeTitleUrls( $this->getUserPage(), $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2405    }
2406
2407    /**
2408     * If only this user's username is known, and it exists, return the user ID.
2409     *
2410     * @param int $flags Bitfield of IDBAccessObject::READ_* constants; useful for existence checks
2411     * @return int
2412     */
2413    public function idForName( $flags = IDBAccessObject::READ_NORMAL ) {
2414        $s = trim( $this->getName() );
2415        if ( $s === '' ) {
2416            return 0;
2417        }
2418
2419        $db = DBAccessObjectUtils::getDBFromRecency(
2420            MediaWikiServices::getInstance()->getDBLoadBalancerFactory(),
2421            $flags
2422        );
2423        $id = $db->newSelectQueryBuilder()
2424            ->select( 'user_id' )
2425            ->from( 'user' )
2426            ->where( [ 'user_name' => $s ] )
2427            ->recency( $flags )
2428            ->caller( __METHOD__ )->fetchField();
2429
2430        return (int)$id;
2431    }
2432
2433    /**
2434     * Add a user to the database, return the user object
2435     *
2436     * @param string $name Username to add
2437     * @param array $params Array of Strings Non-default parameters to save to
2438     *   the database as user_* fields:
2439     *   - email: The user's email address.
2440     *   - email_authenticated: The email authentication timestamp.
2441     *   - real_name: The user's real name.
2442     *   - token: Random authentication token. Do not set.
2443     *   - registration: Registration timestamp. Do not set.
2444     * @return User|null User object, or null if the username already exists.
2445     */
2446    public static function createNew( $name, $params = [] ) {
2447        return self::insertNewUser( static function ( UserIdentity $actor, IDatabase $dbw ) {
2448            return MediaWikiServices::getInstance()->getActorStore()->createNewActor( $actor, $dbw );
2449        }, $name, $params );
2450    }
2451
2452    /**
2453     * See ::createNew
2454     * @param callable $insertActor ( UserIdentity $actor, IDatabase $dbw ): int actor ID,
2455     * @param string $name
2456     * @param array $params
2457     * @return User|null User object, or null if the username already exists.
2458     */
2459    private static function insertNewUser( callable $insertActor, $name, $params = [] ) {
2460        foreach ( [ 'password', 'newpassword', 'newpass_time', 'password_expires' ] as $field ) {
2461            if ( isset( $params[$field] ) ) {
2462                wfDeprecated( __METHOD__ . " with param '$field'", '1.27' );
2463                unset( $params[$field] );
2464            }
2465        }
2466
2467        $user = new User;
2468        $user->load();
2469        $user->setToken(); // init token
2470        $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
2471
2472        $noPass = PasswordFactory::newInvalidPassword()->toString();
2473
2474        $fields = [
2475            'user_name' => $name,
2476            'user_password' => $noPass,
2477            'user_newpassword' => $noPass,
2478            'user_email' => $user->mEmail,
2479            'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
2480            'user_real_name' => $user->mRealName,
2481            'user_token' => strval( $user->mToken ),
2482            'user_registration' => $dbw->timestamp(),
2483            'user_editcount' => 0,
2484            'user_touched' => $dbw->timestamp( $user->newTouchedTimestamp() ),
2485        ];
2486        foreach ( $params as $name => $value ) {
2487            $fields["user_$name"] = $value;
2488        }
2489
2490        return $dbw->doAtomicSection( __METHOD__, static function ( IDatabase $dbw, $fname )
2491            use ( $fields, $insertActor )
2492        {
2493            $dbw->newInsertQueryBuilder()
2494                ->insertInto( 'user' )
2495                ->ignore()
2496                ->row( $fields )
2497                ->caller( $fname )->execute();
2498            if ( $dbw->affectedRows() ) {
2499                $newUser = self::newFromId( $dbw->insertId() );
2500                $newUser->mName = $fields['user_name'];
2501                // Don't pass $this, since calling ::getId, ::getName might force ::load
2502                // and this user might not be ready for the yet.
2503                $newUser->mActorId = $insertActor(
2504                    new UserIdentityValue( $newUser->mId, $newUser->mName ),
2505                    $dbw
2506                );
2507                // Load the user from primary DB to avoid replica lag
2508                $newUser->load( IDBAccessObject::READ_LATEST );
2509            } else {
2510                $newUser = null;
2511            }
2512            return $newUser;
2513        } );
2514    }
2515
2516    /**
2517     * Add this existing user object to the database. If the user already
2518     * exists, a fatal status object is returned, and the user object is
2519     * initialised with the data from the database.
2520     *
2521     * Previously, this function generated a DB error due to a key conflict
2522     * if the user already existed. Many extension callers use this function
2523     * in code along the lines of:
2524     *
2525     *   $user = User::newFromName( $name );
2526     *   if ( !$user->isRegistered() ) {
2527     *       $user->addToDatabase();
2528     *   }
2529     *   // do something with $user...
2530     *
2531     * However, this was vulnerable to a race condition (T18020). By
2532     * initialising the user object if the user exists, we aim to support this
2533     * calling sequence as far as possible.
2534     *
2535     * Note that if the user exists, this function will acquire a write lock,
2536     * so it is still advisable to make the call conditional on isRegistered(),
2537     * and to commit the transaction after calling.
2538     *
2539     * @return Status
2540     */
2541    public function addToDatabase() {
2542        $this->load();
2543        if ( !$this->mToken ) {
2544            $this->setToken(); // init token
2545        }
2546
2547        if ( !is_string( $this->mName ) ) {
2548            throw new RuntimeException( "User name field is not set." );
2549        }
2550
2551        $this->mTouched = $this->newTouchedTimestamp();
2552
2553        $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
2554        $status = $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) {
2555            $noPass = PasswordFactory::newInvalidPassword()->toString();
2556            $dbw->newInsertQueryBuilder()
2557                ->insertInto( 'user' )
2558                ->ignore()
2559                ->row( [
2560                    'user_name' => $this->mName,
2561                    'user_password' => $noPass,
2562                    'user_newpassword' => $noPass,
2563                    'user_email' => $this->mEmail,
2564                    'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2565                    'user_real_name' => $this->mRealName,
2566                    'user_token' => strval( $this->mToken ),
2567                    'user_registration' => $dbw->timestamp(),
2568                    'user_editcount' => 0,
2569                    'user_touched' => $dbw->timestamp( $this->mTouched ),
2570                    'user_is_temp' => $this->isTemp(),
2571                ] )
2572                ->caller( $fname )->execute();
2573            if ( !$dbw->affectedRows() ) {
2574                // Use locking reads to bypass any REPEATABLE-READ snapshot.
2575                $this->mId = $dbw->newSelectQueryBuilder()
2576                    ->select( 'user_id' )
2577                    ->lockInShareMode()
2578                    ->from( 'user' )
2579                    ->where( [ 'user_name' => $this->mName ] )
2580                    ->caller( $fname )->fetchField();
2581                $loaded = false;
2582                if ( $this->mId && $this->loadFromDatabase( IDBAccessObject::READ_LOCKING ) ) {
2583                    $loaded = true;
2584                }
2585                if ( !$loaded ) {
2586                    throw new RuntimeException( $fname . ": hit a key conflict attempting " .
2587                        "to insert user '{$this->mName}' row, but it was not present in select!" );
2588                }
2589                return Status::newFatal( 'userexists' );
2590            }
2591            $this->mId = $dbw->insertId();
2592            $this->queryFlagsUsed = IDBAccessObject::READ_LATEST;
2593
2594            // Don't pass $this, since calling ::getId, ::getName might force ::load
2595            // and this user might not be ready for that yet.
2596            $this->mActorId = MediaWikiServices::getInstance()
2597                ->getActorNormalization()
2598                ->acquireActorId( new UserIdentityValue( $this->mId, $this->mName ), $dbw );
2599            return Status::newGood();
2600        } );
2601        if ( !$status->isGood() ) {
2602            return $status;
2603        }
2604
2605        // Clear instance cache other than user table data and actor, which is already accurate
2606        $this->clearInstanceCache();
2607
2608        if ( $this->isNamed() ) {
2609            MediaWikiServices::getInstance()->getUserOptionsManager()->saveOptions( $this );
2610        }
2611        return Status::newGood();
2612    }
2613
2614    /**
2615     * Schedule a deferred update which will block the IP address of the current
2616     * user, if they are blocked with the autoblocking option.
2617     *
2618     * @since 1.45
2619     */
2620    public function scheduleSpreadBlock() {
2621        DeferredUpdates::addCallableUpdate( function () {
2622            // Permit master queries in a GET request
2623            $scope = Profiler::instance()->getTransactionProfiler()->silenceForScope();
2624            $this->spreadAnyEditBlock();
2625            ScopedCallback::consume( $scope );
2626        } );
2627    }
2628
2629    /**
2630     * If this user is logged-in and blocked, block any IP address they've successfully logged in from.
2631     * Calls the "SpreadAnyEditBlock" hook, so this may block the IP address using a non-core blocking mechanism.
2632     *
2633     * @return bool A block was spread
2634     */
2635    public function spreadAnyEditBlock() {
2636        if ( !$this->isRegistered() ) {
2637            return false;
2638        }
2639
2640        $blockWasSpread = false;
2641        $this->getHookRunner()->onSpreadAnyEditBlock( $this, $blockWasSpread );
2642
2643        $block = $this->getBlock();
2644        if ( $block ) {
2645            $blockWasSpread = $blockWasSpread || $this->spreadBlock( $block );
2646        }
2647
2648        return $blockWasSpread;
2649    }
2650
2651    /**
2652     * If this (non-anonymous) user is blocked,
2653     * block the IP address they've successfully logged in from.
2654     * @param Block $block The active block on the user
2655     * @return bool A block was spread
2656     */
2657    protected function spreadBlock( Block $block ): bool {
2658        wfDebug( __METHOD__ . "()" );
2659        $this->load();
2660        if ( $this->mId == 0 ) {
2661            return false;
2662        }
2663
2664        $blockStore = MediaWikiServices::getInstance()->getDatabaseBlockStore();
2665        foreach ( $block->toArray() as $singleBlock ) {
2666            if ( $singleBlock instanceof DatabaseBlock && $singleBlock->isAutoblocking() ) {
2667                return (bool)$blockStore->doAutoblock( $singleBlock, $this->getRequest()->getIP() );
2668            }
2669        }
2670        return false;
2671    }
2672
2673    /**
2674     * Get whether the user is blocked from using Special:Emailuser.
2675     * @return bool
2676     * @deprecated since 1.41, emits deprecation warnings since 1.43. EmailUser::canSend
2677     *   checks blocks amongst other things. If you only need this check, use
2678     *   ::getBlock()->appliesToRight( 'sendemail' ).
2679     */
2680    public function isBlockedFromEmailuser() {
2681        wfDeprecated( __METHOD__, '1.41' );
2682        $block = $this->getBlock();
2683        return $block && $block->appliesToRight( 'sendemail' );
2684    }
2685
2686    /**
2687     * Get whether the user is blocked from using Special:Upload
2688     *
2689     * @since 1.33
2690     * @return bool
2691     */
2692    public function isBlockedFromUpload() {
2693        $block = $this->getBlock();
2694        return $block && $block->appliesToRight( 'upload' );
2695    }
2696
2697    /**
2698     * Get whether the user is allowed to create an account.
2699     * @return bool
2700     */
2701    public function isAllowedToCreateAccount() {
2702        return $this->getThisAsAuthority()->isDefinitelyAllowed( 'createaccount' );
2703    }
2704
2705    /**
2706     * Get this user's personal page title.
2707     *
2708     * @return Title User's personal page title
2709     */
2710    public function getUserPage() {
2711        return Title::makeTitle( NS_USER, $this->getName() );
2712    }
2713
2714    /**
2715     * Get this user's talk page title.
2716     *
2717     * @return Title
2718     */
2719    public function getTalkPage() {
2720        $title = $this->getUserPage();
2721        return $title->getTalkPage();
2722    }
2723
2724    /**
2725     * Determine whether the user is a newbie. Newbies are one of:
2726     * - IP address editors
2727     * - temporary accounts
2728     * - most recently created full accounts.
2729     * @return bool
2730     */
2731    public function isNewbie() {
2732        // IP users and temp account users are excluded from the autoconfirmed group.
2733        return !$this->isAllowed( 'autoconfirmed' );
2734    }
2735
2736    /**
2737     * Initialize (if necessary) and return a session token value
2738     * which can be used in edit forms to show that the user's
2739     * login credentials aren't being hijacked with a foreign form
2740     * submission.
2741     *
2742     * @since 1.27
2743     * @deprecated since 1.37. Use CsrfTokenSet::getToken instead
2744     * @param string|string[] $salt Optional function-specific data for hashing
2745     * @param WebRequest|null $request WebRequest object to use, or null to use the global request
2746     * @return Token The new edit token
2747     */
2748    public function getEditTokenObject( $salt = '', $request = null ) {
2749        if ( $this->isAnon() ) {
2750            return new LoggedOutEditToken();
2751        }
2752
2753        if ( !$request ) {
2754            $request = $this->getRequest();
2755        }
2756        return $request->getSession()->getToken( $salt );
2757    }
2758
2759    /**
2760     * Initialize (if necessary) and return a session token value
2761     * which can be used in edit forms to show that the user's
2762     * login credentials aren't being hijacked with a foreign form
2763     * submission.
2764     *
2765     * The $salt for 'edit' and 'csrf' tokens is the default (empty string).
2766     *
2767     * @since 1.19
2768     * @deprecated since 1.37. Use CsrfTokenSet::getToken instead
2769     * @param string|string[] $salt Optional function-specific data for hashing
2770     * @param WebRequest|null $request WebRequest object to use, or null to use the global request
2771     * @return string The new edit token
2772     */
2773    public function getEditToken( $salt = '', $request = null ) {
2774        return $this->getEditTokenObject( $salt, $request )->toString();
2775    }
2776
2777    /**
2778     * Check given value against the token value stored in the session.
2779     * A match should confirm that the form was submitted from the
2780     * user's own login session, not a form submission from a third-party
2781     * site.
2782     *
2783     * @deprecated since 1.37. Use CsrfTokenSet::matchToken instead
2784     * @param string|null $val Input value to compare
2785     * @param string|array $salt Optional function-specific data for hashing
2786     * @param WebRequest|null $request Object to use, or null to use the global request
2787     * @param int|null $maxage Fail tokens older than this, in seconds
2788     * @return bool Whether the token matches
2789     */
2790    public function matchEditToken( $val, $salt = '', $request = null, $maxage = null ) {
2791        return $this->getEditTokenObject( $salt, $request )->match( $val, $maxage );
2792    }
2793
2794    /**
2795     * Generate a new e-mail confirmation token and send a confirmation/invalidation
2796     * mail to the user's given address.
2797     * Any preexisting e-mail confirmation token will be invalidated.
2798     *
2799     * @param string $type Message to send, either "created", "changed" or "set"
2800     * @return Status
2801     */
2802    public function sendConfirmationMail( $type = 'created' ) {
2803        $emailer = MediaWikiServices::getInstance()->getConfirmEmailSender();
2804        $expiration = null; // gets passed-by-ref and defined in next line
2805        $token = $this->getConfirmationToken( $expiration );
2806        $confirmationUrl = $this->getConfirmationTokenUrl( $token );
2807        $invalidateUrl = $this->getInvalidationTokenUrl( $token );
2808        $this->saveSettings();
2809
2810        return Status::wrap( $emailer->sendConfirmationMail(
2811            RequestContext::getMain(),
2812            $type,
2813            new ConfirmEmailData(
2814                $this->getUser(),
2815                $confirmationUrl,
2816                $invalidateUrl,
2817                $expiration
2818            )
2819        ) );
2820    }
2821
2822    /**
2823     * Send an e-mail to this user's account. Does not check for
2824     * confirmed status or validity.
2825     *
2826     * @param string $subject Message subject
2827     * @param string|string[] $body Message body or array containing keys text and html
2828     * @param User|null $from Optional sending user; if unspecified, default
2829     *   $wgPasswordSender will be used.
2830     * @param MailAddress|null $replyto Reply-To address
2831     * @return Status
2832     */
2833    public function sendMail( $subject, $body, $from = null, $replyto = null ) {
2834        $passwordSender = MediaWikiServices::getInstance()->getMainConfig()
2835            ->get( MainConfigNames::PasswordSender );
2836
2837        if ( $from instanceof User ) {
2838            $sender = MailAddress::newFromUser( $from );
2839        } else {
2840            $sender = new MailAddress( $passwordSender,
2841                wfMessage( 'emailsender' )->inContentLanguage()->text() );
2842        }
2843        $to = MailAddress::newFromUser( $this );
2844
2845        if ( is_array( $body ) ) {
2846            $bodyText = $body['text'] ?? '';
2847            $bodyHtml = $body['html'] ?? null;
2848        } else {
2849            $bodyText = $body;
2850            $bodyHtml = null;
2851        }
2852
2853        return Status::wrap( MediaWikiServices::getInstance()->getEmailer()
2854            ->send(
2855                [ $to ],
2856                $sender,
2857                $subject,
2858                $bodyText,
2859                $bodyHtml,
2860                [ 'replyTo' => $replyto ]
2861            ) );
2862    }
2863
2864    /**
2865     * Generate, store, and return a new e-mail confirmation code.
2866     * A hash (unsalted, since it's used as a key) is stored.
2867     * Any preexisting e-mail confirmation token will be invalidated.
2868     *
2869     * @note Call saveSettings() after calling this function to commit
2870     * this change to the database.
2871     *
2872     * @since 1.45
2873     *
2874     * @param null|string &$expiration Timestamp at which the generated token expires @phan-output-reference
2875     * @param int|null $tokenLifeTimeSeconds Optional lifetime of the token in seconds.
2876     * Defaults to the value of $wgUserEmailConfirmationTokenExpiry if not set.
2877     * @return string New token
2878     */
2879    public function getConfirmationToken(
2880        ?string &$expiration,
2881        ?int $tokenLifeTimeSeconds = null
2882    ): string {
2883        $tokenLifeTimeSeconds ??= MediaWikiServices::getInstance()
2884            ->getMainConfig()->get( MainConfigNames::UserEmailConfirmationTokenExpiry );
2885        $now = ConvertibleTimestamp::time();
2886
2887        $expires = $now + $tokenLifeTimeSeconds;
2888        $expiration = wfTimestamp( TS::MW, $expires );
2889        $this->load();
2890        $token = MWCryptRand::generateHex( 32 );
2891        $hash = md5( $token );
2892        $this->mEmailToken = $hash;
2893        $this->mEmailTokenExpires = $expiration;
2894        return $token;
2895    }
2896
2897    /**
2898     * Deprecated alias for getConfirmationToken() for CentralAuth.
2899     * @deprecated Use getConfirmationToken() instead.
2900     * @param string|null &$expiration @phan-output-reference
2901     * @return string
2902     */
2903    protected function confirmationToken( &$expiration ) {
2904        return $this->getConfirmationToken( $expiration );
2905    }
2906
2907    /**
2908     * Check if the given email confirmation token is well-formed (to detect mangling by
2909     * email clients). This does not check whether the token is valid.
2910     * @param string $token
2911     * @return bool
2912     */
2913    public static function isWellFormedConfirmationToken( string $token ): bool {
2914        return preg_match( '/^[a-f0-9]{32}$/', $token );
2915    }
2916
2917    /**
2918     * Return a URL the user can use to confirm their email address.
2919     *
2920     * @since 1.45
2921     * @param string $token Accepts the email confirmation token
2922     * @return string New token URL
2923     */
2924    public function getConfirmationTokenUrl( string $token ): string {
2925        return $this->getTokenUrl( 'ConfirmEmail', $token );
2926    }
2927
2928    /**
2929     * Return a URL the user can use to invalidate their email address.
2930     *
2931     * @since 1.45
2932     * @param string $token Accepts the email confirmation token
2933     * @return string New token URL
2934     */
2935    public function getInvalidationTokenUrl( string $token ): string {
2936        return $this->getTokenUrl( 'InvalidateEmail', $token );
2937    }
2938
2939    /**
2940     * Deprecated alias for getInvalidationTokenUrl() for CentralAuth.
2941     *
2942     * @deprecated Use getInvalidationTokenUrl() instead.
2943     * @param string $token Accepts the email confirmation token
2944     * @return string New token URL
2945     */
2946    protected function invalidationTokenUrl( $token ) {
2947        return $this->getTokenUrl( 'InvalidateEmail', $token );
2948    }
2949
2950    /**
2951     * Function to create a special page URL with a token path parameter.
2952     * This uses a quickie hack to use the
2953     * hardcoded English names of the Special: pages, for ASCII safety.
2954     *
2955     * @note Since these URLs get dropped directly into emails, using the
2956     * short English names avoids really long URL-encoded links, which
2957     * also sometimes can get corrupted in some browsers/mailers
2958     * (T8957 with Gmail and Internet Explorer).
2959     *
2960     * @since 1.45
2961     * @param string $page Special page
2962     * @param string $token
2963     * @return string Formatted URL
2964     */
2965    public function getTokenUrl( string $page, string $token ): string {
2966        // Hack to bypass localization of 'Special:'
2967        $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" );
2968        return $title->getCanonicalURL();
2969    }
2970
2971    /**
2972     * Mark the e-mail address confirmed.
2973     *
2974     * @note Call saveSettings() after calling this function to commit the change.
2975     *
2976     * @return bool
2977     */
2978    public function confirmEmail() {
2979        // Check if it's already confirmed, so we don't touch the database
2980        // and fire the ConfirmEmailComplete hook on redundant confirmations.
2981        if ( !$this->isEmailConfirmed() ) {
2982            $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
2983            $this->getHookRunner()->onConfirmEmailComplete( $this );
2984        }
2985        return true;
2986    }
2987
2988    /**
2989     * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
2990     * address if it was already confirmed.
2991     *
2992     * @note Call saveSettings() after calling this function to commit the change.
2993     * @return bool Returns true
2994     */
2995    public function invalidateEmail() {
2996        $this->load();
2997        $this->mEmailToken = null;
2998        $this->mEmailTokenExpires = null;
2999        $this->setEmailAuthenticationTimestamp( null );
3000        $this->mEmail = '';
3001        $this->getHookRunner()->onInvalidateEmailComplete( $this );
3002        return true;
3003    }
3004
3005    /**
3006     * Set the e-mail authentication timestamp.
3007     * @param string|null $timestamp TS::MW timestamp
3008     */
3009    public function setEmailAuthenticationTimestamp( $timestamp ) {
3010        $this->load();
3011        $this->mEmailAuthenticated = $timestamp;
3012        $this->getHookRunner()->onUserSetEmailAuthenticationTimestamp(
3013            $this, $this->mEmailAuthenticated );
3014    }
3015
3016    /**
3017     * Is this user allowed to send e-mails within limits of current
3018     * site configuration?
3019     * @deprecated since 1.41, emits deprecation warnings since 1.43.
3020     *   Use EmailUser::canSend() instead.
3021     * @return bool
3022     */
3023    public function canSendEmail() {
3024        wfDeprecated( __METHOD__, '1.41' );
3025        $permError = MediaWikiServices::getInstance()->getEmailUserFactory()
3026            ->newEmailUser( $this->getThisAsAuthority() )
3027            ->canSend();
3028        return $permError->isGood();
3029    }
3030
3031    /**
3032     * Is this user allowed to receive e-mails within limits of current
3033     * site configuration?
3034     * @return bool
3035     */
3036    public function canReceiveEmail() {
3037        $userOptionsLookup = MediaWikiServices::getInstance()
3038            ->getUserOptionsLookup();
3039        return $this->isEmailConfirmed() && !$userOptionsLookup->getOption( $this, 'disablemail' );
3040    }
3041
3042    /**
3043     * Is this user's e-mail address valid-looking and confirmed within
3044     * limits of the current site configuration?
3045     *
3046     * @note If $wgEmailAuthentication is on, this may require the user to have
3047     * confirmed their address by returning a code or using a password
3048     * sent to the address from the wiki.
3049     *
3050     * @return bool
3051     */
3052    public function isEmailConfirmed(): bool {
3053        $emailAuthentication = MediaWikiServices::getInstance()->getMainConfig()
3054            ->get( MainConfigNames::EmailAuthentication );
3055        $this->load();
3056        $confirmed = true;
3057        if ( $this->getHookRunner()->onEmailConfirmed( $this, $confirmed ) ) {
3058            return !$this->isAnon() &&
3059                Sanitizer::validateEmail( $this->getEmail() ) &&
3060                ( !$emailAuthentication || $this->getEmailAuthenticationTimestamp() );
3061        }
3062
3063        return $confirmed;
3064    }
3065
3066    /**
3067     * Check whether there is an outstanding request for e-mail confirmation.
3068     * @return bool
3069     */
3070    public function isEmailConfirmationPending() {
3071        $emailAuthentication = MediaWikiServices::getInstance()->getMainConfig()
3072            ->get( MainConfigNames::EmailAuthentication );
3073        return $emailAuthentication &&
3074            !$this->isEmailConfirmed() &&
3075            $this->mEmailToken &&
3076            $this->mEmailTokenExpires > wfTimestamp();
3077    }
3078
3079    /**
3080     * Get the timestamp of account creation.
3081     *
3082     * @deprecated since 1.45 use UserRegistrationLookup instead.
3083     * @return string|false|null Timestamp of account creation, false for
3084     *  non-existent/anonymous user accounts, or null if existing account
3085     *  but information is not in database.
3086     */
3087    public function getRegistration() {
3088        return MediaWikiServices::getInstance()
3089            ->getUserRegistrationLookup()
3090            ->getRegistration( $this );
3091    }
3092
3093    /**
3094     * Get the description of a given right as wikitext
3095     *
3096     * @since 1.29
3097     * @param string $right Right to query
3098     * @return string Localized description of the right
3099     */
3100    public static function getRightDescription( $right ) {
3101        $key = "right-$right";
3102        $msg = wfMessage( $key );
3103        return $msg->isDisabled() ? $right : $msg->text();
3104    }
3105
3106    /**
3107     * Get the description of a given right as rendered HTML
3108     *
3109     * @since 1.42
3110     * @param string $right Right to query
3111     * @return string HTML description of the right
3112     */
3113    public static function getRightDescriptionHtml( $right ) {
3114        return wfMessage( "right-$right" )->parse();
3115    }
3116
3117    /**
3118     * Return the tables, fields, and join conditions to be selected to create
3119     * a new user object.
3120     * @since 1.31
3121     * @return array[] With three keys:
3122     *   - tables: (string[]) to include in the `$table` to `IDatabase->select()`
3123     *     or `SelectQueryBuilder::tables`
3124     *   - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
3125     *     or `SelectQueryBuilder::fields`
3126     *   - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
3127     *     or `SelectQueryBuilder::joinConds`
3128     * @phan-return array{tables:string[],fields:string[],joins:array}
3129     */
3130    public static function getQueryInfo() {
3131        $ret = [
3132            'tables' => [ 'user', 'user_actor' => 'actor' ],
3133            'fields' => [
3134                'user_id',
3135                'user_name',
3136                'user_real_name',
3137                'user_email',
3138                'user_touched',
3139                'user_token',
3140                'user_email_authenticated',
3141                'user_email_token',
3142                'user_email_token_expires',
3143                'user_registration',
3144                'user_editcount',
3145                'user_actor.actor_id',
3146            ],
3147            'joins' => [
3148                'user_actor' => [ 'JOIN', 'user_actor.actor_user = user_id' ],
3149            ],
3150        ];
3151
3152        return $ret;
3153    }
3154
3155    /**
3156     * Get a SelectQueryBuilder with the tables, fields and join conditions
3157     * needed to create a new User object.
3158     *
3159     * The return value is a plain SelectQueryBuilder, not a UserSelectQueryBuilder.
3160     * That way, there is no need for an ActorStore.
3161     *
3162     * @return SelectQueryBuilder
3163     */
3164    public static function newQueryBuilder( IReadableDatabase $db ) {
3165        return $db->newSelectQueryBuilder()
3166            ->select( [
3167                'user_id',
3168                'user_name',
3169                'user_real_name',
3170                'user_email',
3171                'user_touched',
3172                'user_token',
3173                'user_email_authenticated',
3174                'user_email_token',
3175                'user_email_token_expires',
3176                'user_registration',
3177                'user_editcount',
3178                'user_actor.actor_id',
3179            ] )
3180            ->from( 'user' )