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