Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
38.26% covered (danger)
38.26%
448 / 1171
41.61% covered (danger)
41.61%
57 / 137
CRAP
0.00% covered (danger)
0.00%
0 / 1
User
38.29% covered (danger)
38.29%
448 / 1170
41.61% covered (danger)
41.61%
57 / 137
33598.30
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getWikiId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __toString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __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%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 isSafeToLoad
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
20
 load
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 1
342
 loadFromId
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 purge
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getCacheKey
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 loadFromCache
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
72
 newFromName
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
4.05
 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
96.23% covered (success)
96.23%
51 / 53
0.00% covered (danger)
0.00%
0 / 1
10
 whoIs
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 whoIsReal
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 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%
18 / 18
100.00% covered (success)
100.00%
1 / 1
4
 loadDefaults
94.12% covered (success)
94.12%
16 / 17
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
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 loadFromDatabase
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
3.01
 loadFromRow
85.71% covered (warning)
85.71%
48 / 56
0.00% covered (danger)
0.00%
0 / 1
21.17
 loadFromUserObject
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 makeUpdateConditions
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 checkAndSetTouched
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
3.01
 clearInstanceCache
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
132
 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
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getBlock
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
5.01
 isBlockedGlobally
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getGlobalBlock
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
90
 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
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
6.07
 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
 canExist
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 getActorId
25.00% covered (danger)
25.00%
4 / 16
0.00% covered (danger)
0.00%
0 / 1
21.19
 setActorId
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getTitleKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newTouchedTimestamp
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 clearSharedCache
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 invalidateCache
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 touch
0.00% covered (danger)
0.00%
0 / 6
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
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 setToken
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getEmail
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getEmailAuthenticationTimestamp
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 setEmail
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 setEmailWithConfirmation
25.00% covered (danger)
25.00%
7 / 28
0.00% covered (danger)
0.00%
0 / 1
62.05
 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
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 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
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
20
 saveSettings
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
42
 idForName
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 createNew
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 insertNewUser
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
30
 addToDatabase
73.08% covered (warning)
73.08%
38 / 52
0.00% covered (danger)
0.00%
0 / 1
10.58
 spreadAnyEditBlock
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 spreadBlock
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 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
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
20
 sendMail
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
 confirmationToken
0.00% covered (danger)
0.00%
0 / 11
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
 confirmationTokenUrl
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
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 confirmEmail
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 invalidateEmail
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 setEmailAuthenticationTimestamp
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 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
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getRightDescription
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getRightDescriptionHtml
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
2
 newQueryBuilder
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 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 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 equals
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 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
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 isGlobalSessionUser
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 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 * Implements the User class for the %MediaWiki software.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22
23namespace MediaWiki\User;
24
25use AllowDynamicProperties;
26use ArrayIterator;
27use DBAccessObjectUtils;
28use IDBAccessObject;
29use InvalidArgumentException;
30use MailAddress;
31use MediaWiki\Auth\AuthenticationRequest;
32use MediaWiki\Auth\AuthManager;
33use MediaWiki\Block\AbstractBlock;
34use MediaWiki\Block\Block;
35use MediaWiki\Block\SystemBlock;
36use MediaWiki\Context\RequestContext;
37use MediaWiki\DAO\WikiAwareEntityTrait;
38use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
39use MediaWiki\Logger\LoggerFactory;
40use MediaWiki\Mail\UserEmailContact;
41use MediaWiki\MainConfigNames;
42use MediaWiki\MainConfigSchema;
43use MediaWiki\MediaWikiServices;
44use MediaWiki\Page\PageIdentity;
45use MediaWiki\Parser\Sanitizer;
46use MediaWiki\Password\PasswordFactory;
47use MediaWiki\Password\UserPasswordPolicy;
48use MediaWiki\Permissions\Authority;
49use MediaWiki\Permissions\PermissionStatus;
50use MediaWiki\Permissions\RateLimitSubject;
51use MediaWiki\Permissions\UserAuthority;
52use MediaWiki\Request\WebRequest;
53use MediaWiki\Session\SessionManager;
54use MediaWiki\Session\Token;
55use MediaWiki\Status\Status;
56use MediaWiki\Title\Title;
57use MWCryptHash;
58use MWCryptRand;
59use MWExceptionHandler;
60use RuntimeException;
61use stdClass;
62use Stringable;
63use UnexpectedValueException;
64use WANObjectCache;
65use Wikimedia\Assert\Assert;
66use Wikimedia\Assert\PreconditionException;
67use Wikimedia\DebugInfo\DebugInfoTrait;
68use Wikimedia\IPUtils;
69use Wikimedia\Rdbms\Database;
70use Wikimedia\Rdbms\DBExpectedError;
71use Wikimedia\Rdbms\IDatabase;
72use Wikimedia\Rdbms\IReadableDatabase;
73use Wikimedia\Rdbms\SelectQueryBuilder;
74use Wikimedia\ScopedCallback;
75use Wikimedia\Timestamp\ConvertibleTimestamp;
76
77/**
78 * The User object encapsulates all of the user-specific settings (user_id,
79 * name, rights, email address, options, last login time). Client
80 * classes use the getXXX() functions to access these fields. These functions
81 * do all the work of determining whether the user is logged in,
82 * whether the requested option can be satisfied from cookies or
83 * whether a database query is needed. Most of the settings needed
84 * for rendering normal pages are set in the cookie to minimize use
85 * of the database.
86 *
87 * @note User implements Authority to ease transition. Always prefer
88 * using existing Authority or obtaining a proper Authority implementation.
89 *
90 * @note {@}newable in 1.35 only, the constructor is {@}internal since 1.36
91 */
92#[AllowDynamicProperties]
93class User implements Stringable, Authority, UserIdentity, UserEmailContact {
94    use DebugInfoTrait;
95    use ProtectedHookAccessorTrait;
96    use WikiAwareEntityTrait;
97
98    /**
99     * @var int
100     * @see IDBAccessObject::READ_EXCLUSIVE
101     */
102    public const READ_EXCLUSIVE = IDBAccessObject::READ_EXCLUSIVE;
103
104    /**
105     * @var int
106     * @see IDBAccessObject::READ_LOCKING
107     */
108    public const READ_LOCKING = IDBAccessObject::READ_LOCKING;
109
110    /**
111     * Number of characters required for the user_token field.
112     */
113    public const TOKEN_LENGTH = 32;
114
115    /**
116     * An invalid string value for the user_token field.
117     */
118    public const INVALID_TOKEN = '*** INVALID ***';
119
120    /**
121     * Version number to tag cached versions of serialized User objects. Should be increased when
122     * {@link $mCacheVars} or one of its members changes.
123     */
124    private const VERSION = 17;
125
126    /**
127     * Username used for various maintenance scripts.
128     * @since 1.37
129     */
130    public const MAINTENANCE_SCRIPT_USER = 'Maintenance script';
131
132    /**
133     * List of member variables which are saved to the
134     * shared cache (memcached). Any operation which changes the
135     * corresponding database fields must call a cache-clearing function.
136     * @showinitializer
137     * @var string[]
138     */
139    protected static $mCacheVars = [
140        // user table
141        'mId',
142        'mName',
143        'mRealName',
144        'mEmail',
145        'mTouched',
146        'mToken',
147        'mEmailAuthenticated',
148        'mEmailToken',
149        'mEmailTokenExpires',
150        'mRegistration',
151        // actor table
152        'mActorId',
153    ];
154
155    /** Cache variables */
156    // Some of these are public, including for use by the UserFactory, but they generally
157    // should not be set manually
158    // @{
159    /** @var int */
160    public $mId;
161    /** @var string */
162    public $mName;
163    /**
164     * Switched from protected to public for use in UserFactory
165     *
166     * @var int|null
167     */
168    public $mActorId;
169    /** @var string */
170    public $mRealName;
171
172    /** @var string */
173    public $mEmail;
174    /** @var string TS_MW timestamp from the DB */
175    public $mTouched;
176    /** @var string|null TS_MW timestamp from cache */
177    protected $mQuickTouched;
178    /** @var string|null */
179    protected $mToken;
180    /** @var string|null */
181    public $mEmailAuthenticated;
182    /** @var string|null */
183    protected $mEmailToken;
184    /** @var string|null */
185    protected $mEmailTokenExpires;
186    /** @var string|null */
187    protected $mRegistration;
188    // @}
189
190    // @{
191    /**
192     * @var array|bool Array with already loaded items or true if all items have been loaded.
193     */
194    protected $mLoadedItems = [];
195    // @}
196
197    /**
198     * @var string Initialization data source if mLoadedItems!==true. May be one of:
199     *  - 'defaults'   anonymous user initialised from class defaults
200     *  - 'name'       initialise from mName
201     *  - 'id'         initialise from mId
202     *  - 'actor'      initialise from mActorId
203     *  - 'session'    log in from session if possible
204     *
205     * Use the User::newFrom*() family of functions to set this.
206     */
207    public $mFrom;
208
209    /**
210     * Lazy-initialized variables, invalidated with clearInstanceCache
211     */
212    /** @var string|null */
213    protected $mDatePreference;
214    /** @var string|false */
215    protected $mHash;
216    /** @var AbstractBlock */
217    protected $mGlobalBlock;
218    /** @var bool */
219    protected $mLocked;
220
221    /** @var WebRequest|null */
222    private $mRequest;
223
224    /** @var int IDBAccessObject::READ_* constant bitfield used to load data */
225    protected $queryFlagsUsed = IDBAccessObject::READ_NORMAL;
226
227    /**
228     * @var UserAuthority|null lazy-initialized Authority of this user
229     * @noVarDump
230     */
231    private $mThisAsAuthority;
232
233    /** @var bool|null */
234    private $isTemp;
235
236    /**
237     * Lightweight constructor for an anonymous user.
238     *
239     * @stable to call since 1.35
240     * @internal since 1.36, use the UserFactory service instead
241     *
242     * @see MediaWiki\User\UserFactory
243     *
244     * @see newFromName()
245     * @see newFromId()
246     * @see newFromActorId()
247     * @see newFromConfirmationCode()
248     * @see newFromSession()
249     * @see newFromRow()
250     */
251    public function __construct() {
252        $this->clearInstanceCache( 'defaults' );
253    }
254
255    /**
256     * Returns self::LOCAL to indicate the user is associated with the local wiki.
257     *
258     * @since 1.36
259     * @return string|false
260     */
261    public function getWikiId() {
262        return self::LOCAL;
263    }
264
265    /**
266     * @return string
267     */
268    public function __toString() {
269        return $this->getName();
270    }
271
272    public function &__get( $name ) {
273        // A shortcut for $mRights deprecation phase
274        if ( $name === 'mRights' ) {
275            // hard deprecated since 1.40
276            wfDeprecated( 'User::$mRights', '1.34' );
277            $copy = MediaWikiServices::getInstance()
278                ->getPermissionManager()
279                ->getUserPermissions( $this );
280            return $copy;
281        } elseif ( !property_exists( $this, $name ) ) {
282            // T227688 - do not break $u->foo['bar'] = 1
283            wfLogWarning( 'tried to get non-existent property' );
284            $this->$name = null;
285            return $this->$name;
286        } else {
287            wfLogWarning( 'tried to get non-visible property' );
288            $null = null;
289            return $null;
290        }
291    }
292
293    public function __set( $name, $value ) {
294        // A shortcut for $mRights deprecation phase, only known legitimate use was for
295        // testing purposes, other uses seem bad in principle
296        if ( $name === 'mRights' ) {
297            // hard deprecated since 1.40
298            wfDeprecated( 'User::$mRights', '1.34' );
299            MediaWikiServices::getInstance()->getPermissionManager()->overrideUserRightsForTesting(
300                $this,
301                $value ?? []
302            );
303        } elseif ( !property_exists( $this, $name ) ) {
304            $this->$name = $value;
305        } else {
306            wfLogWarning( 'tried to set non-visible property' );
307        }
308    }
309
310    public function __sleep(): array {
311        return array_diff(
312            array_keys( get_object_vars( $this ) ),
313            [
314                'mThisAsAuthority' // memoization, will be recreated on demand.
315            ]
316        );
317    }
318
319    /**
320     * Test if it's safe to load this User object.
321     *
322     * You should typically check this before using $wgUser or
323     * RequestContext::getUser in a method that might be called before the
324     * system has been fully initialized. If the object is unsafe, you should
325     * use an anonymous user:
326     * \code
327     * $user = $wgUser->isSafeToLoad() ? $wgUser : new User;
328     * \endcode
329     *
330     * @since 1.27
331     * @return bool
332     */
333    public function isSafeToLoad() {
334        global $wgFullyInitialised;
335
336        // The user is safe to load if:
337        // * MW_NO_SESSION is undefined AND $wgFullyInitialised is true (safe to use session data)
338        // * mLoadedItems === true (already loaded)
339        // * mFrom !== 'session' (sessions not involved at all)
340
341        return ( !defined( 'MW_NO_SESSION' ) && $wgFullyInitialised ) ||
342            $this->mLoadedItems === true || $this->mFrom !== 'session';
343    }
344
345    /**
346     * Load the user table data for this object from the source given by mFrom.
347     *
348     * @param int $flags IDBAccessObject::READ_* constant bitfield
349     */
350    public function load( $flags = IDBAccessObject::READ_NORMAL ) {
351        global $wgFullyInitialised;
352
353        if ( $this->mLoadedItems === true ) {
354            return;
355        }
356
357        // Set it now to avoid infinite recursion in accessors
358        $oldLoadedItems = $this->mLoadedItems;
359        $this->mLoadedItems = true;
360        $this->queryFlagsUsed = $flags;
361
362        // If this is called too early, things are likely to break.
363        if ( !$wgFullyInitialised && $this->mFrom === 'session' ) {
364            LoggerFactory::getInstance( 'session' )
365                ->warning( 'User::loadFromSession called before the end of Setup.php', [
366                    'exception' => new RuntimeException(
367                        'User::loadFromSession called before the end of Setup.php'
368                    ),
369                ] );
370            $this->loadDefaults();
371            $this->mLoadedItems = $oldLoadedItems;
372            return;
373        }
374
375        switch ( $this->mFrom ) {
376            case 'defaults':
377                $this->loadDefaults();
378                break;
379            case 'id':
380                // Make sure this thread sees its own changes, if the ID isn't 0
381                if ( $this->mId != 0 ) {
382                    $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
383                    if ( $lb->hasOrMadeRecentPrimaryChanges() ) {
384                        $flags |= IDBAccessObject::READ_LATEST;
385                        $this->queryFlagsUsed = $flags;
386                    }
387                }
388
389                $this->loadFromId( $flags );
390                break;
391            case 'actor':
392            case 'name':
393                // Make sure this thread sees its own changes
394                $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
395                if ( $lb->hasOrMadeRecentPrimaryChanges() ) {
396                    $flags |= IDBAccessObject::READ_LATEST;
397                    $this->queryFlagsUsed = $flags;
398                }
399
400                $dbr = DBAccessObjectUtils::getDBFromRecency(
401                    MediaWikiServices::getInstance()->getDBLoadBalancerFactory(),
402                    $flags
403                );
404                $queryBuilder = $dbr->newSelectQueryBuilder()
405                    ->select( [ 'actor_id', 'actor_user', 'actor_name' ] )
406                    ->from( 'actor' )
407                    ->recency( $flags );
408                if ( $this->mFrom === 'name' ) {
409                    // make sure to use normalized form of IP for anonymous users
410                    $queryBuilder->where( [ 'actor_name' => IPUtils::sanitizeIP( $this->mName ) ] );
411                } else {
412                    $queryBuilder->where( [ 'actor_id' => $this->mActorId ] );
413                }
414                $row = $queryBuilder->caller( __METHOD__ )->fetchRow();
415
416                if ( !$row ) {
417                    // Ugh.
418                    $this->loadDefaults( $this->mFrom === 'name' ? $this->mName : false );
419                } elseif ( $row->actor_user ) {
420                    $this->mId = $row->actor_user;
421                    $this->loadFromId( $flags );
422                } else {
423                    $this->loadDefaults( $row->actor_name, $row->actor_id );
424                }
425                break;
426            case 'session':
427                if ( !$this->loadFromSession() ) {
428                    // Loading from session failed. Load defaults.
429                    $this->loadDefaults();
430                }
431                $this->getHookRunner()->onUserLoadAfterLoadFromSession( $this );
432                break;
433            default:
434                throw new UnexpectedValueException(
435                    "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
436        }
437    }
438
439    /**
440     * Load user table data, given mId has already been set.
441     * @param int $flags IDBAccessObject::READ_* constant bitfield
442     * @return bool False if the ID does not exist, true otherwise
443     */
444    public function loadFromId( $flags = IDBAccessObject::READ_NORMAL ) {
445        if ( $this->mId == 0 ) {
446            // Anonymous users are not in the database (don't need cache)
447            $this->loadDefaults();
448            return false;
449        }
450
451        // Try cache (unless this needs data from the primary DB).
452        // NOTE: if this thread called saveSettings(), the cache was cleared.
453        $latest = DBAccessObjectUtils::hasFlags( $flags, IDBAccessObject::READ_LATEST );
454        if ( $latest ) {
455            if ( !$this->loadFromDatabase( $flags ) ) {
456                // Can't load from ID
457                return false;
458            }
459        } else {
460            $this->loadFromCache();
461        }
462
463        $this->mLoadedItems = true;
464        $this->queryFlagsUsed = $flags;
465
466        return true;
467    }
468
469    /**
470     * @since 1.27
471     * @param string $dbDomain
472     * @param int $userId
473     */
474    public static function purge( $dbDomain, $userId ) {
475        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
476        $key = $cache->makeGlobalKey( 'user', 'id', $dbDomain, $userId );
477        $cache->delete( $key );
478    }
479
480    /**
481     * @since 1.27
482     * @param WANObjectCache $cache
483     * @return string
484     */
485    protected function getCacheKey( WANObjectCache $cache ) {
486        $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
487
488        return $cache->makeGlobalKey( 'user', 'id',
489            $lbFactory->getLocalDomainID(), $this->mId );
490    }
491
492    /**
493     * Load user data from shared cache, given mId has already been set.
494     *
495     * @return bool True
496     * @since 1.25
497     */
498    protected function loadFromCache() {
499        global $wgFullyInitialised;
500
501        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
502        $data = $cache->getWithSetCallback(
503            $this->getCacheKey( $cache ),
504            $cache::TTL_HOUR,
505            function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache, $wgFullyInitialised ) {
506                $setOpts += Database::getCacheSetOptions(
507                    MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase()
508                );
509                wfDebug( "User: cache miss for user {$this->mId}" );
510
511                $this->loadFromDatabase( IDBAccessObject::READ_NORMAL );
512
513                $data = [];
514                foreach ( self::$mCacheVars as $name ) {
515                    $data[$name] = $this->$name;
516                }
517
518                $ttl = $cache->adaptiveTTL(
519                    (int)wfTimestamp( TS_UNIX, $this->mTouched ),
520                    $ttl
521                );
522
523                if ( $wgFullyInitialised ) {
524                    $groupMemberships = MediaWikiServices::getInstance()
525                        ->getUserGroupManager()
526                        ->getUserGroupMemberships( $this, $this->queryFlagsUsed );
527
528                    // if a user group membership is about to expire, the cache needs to
529                    // expire at that time (T163691)
530                    foreach ( $groupMemberships as $ugm ) {
531                        if ( $ugm->getExpiry() ) {
532                            $secondsUntilExpiry =
533                                (int)wfTimestamp( TS_UNIX, $ugm->getExpiry() ) - time();
534
535                            if ( $secondsUntilExpiry > 0 && $secondsUntilExpiry < $ttl ) {
536                                $ttl = $secondsUntilExpiry;
537                            }
538                        }
539                    }
540                }
541
542                return $data;
543            },
544            [ 'pcTTL' => $cache::TTL_PROC_LONG, 'version' => self::VERSION ]
545        );
546
547        // Restore from cache
548        foreach ( self::$mCacheVars as $name ) {
549            $this->$name = $data[$name];
550        }
551
552        return true;
553    }
554
555    /***************************************************************************/
556    // region   newFrom*() static factory methods
557    /** @name   newFrom*() static factory methods
558     * @{
559     */
560
561    /**
562     * @see UserFactory::newFromName
563     *
564     * @deprecated since 1.36, use a UserFactory instead
565     *
566     * This is slightly less efficient than newFromId(), so use newFromId() if
567     * you have both an ID and a name handy.
568     *
569     * @param string $name Username, validated by Title::newFromText()
570     * @param string|bool $validate Validate username.Type of validation to use:
571     *   - false        No validation
572     *   - 'valid'      Valid for batch processes
573     *   - 'usable'     Valid for batch processes and login
574     *   - 'creatable'  Valid for batch processes, login and account creation,
575     *  except that true is accepted as an alias for 'valid', for BC.
576     *
577     * @return User|false User object, or false if the username is invalid
578     *  (e.g. if it contains illegal characters or is an IP address). If the
579     *  username is not present in the database, the result will be a user object
580     *  with a name, zero user ID and default settings.
581     */
582    public static function newFromName( $name, $validate = 'valid' ) {
583        // Backwards compatibility with strings / false
584        $validationLevels = [
585            'valid' => UserRigorOptions::RIGOR_VALID,
586            'usable' => UserRigorOptions::RIGOR_USABLE,
587            'creatable' => UserRigorOptions::RIGOR_CREATABLE
588        ];
589        if ( $validate === true ) {
590            $validate = 'valid';
591        }
592        if ( $validate === false ) {
593            $validation = UserRigorOptions::RIGOR_NONE;
594        } elseif ( array_key_exists( $validate, $validationLevels ) ) {
595            $validation = $validationLevels[ $validate ];
596        } else {
597            // Not a recognized value, probably a test for unsupported validation
598            // levels, regardless, just pass it along
599            $validation = $validate;
600        }
601
602        return MediaWikiServices::getInstance()->getUserFactory()
603            ->newFromName( (string)$name, $validation ) ?? false;
604    }
605
606    /**
607     * Static factory method for creation from a given user ID.
608     *
609     * @see UserFactory::newFromId
610     *
611     * @deprecated since 1.36, use a UserFactory instead
612     *
613     * @param int $id Valid user ID
614     * @return User
615     */
616    public static function newFromId( $id ) {
617        return MediaWikiServices::getInstance()
618            ->getUserFactory()
619            ->newFromId( (int)$id );
620    }
621
622    /**
623     * Static factory method for creation from a given actor ID.
624     *
625     * @see UserFactory::newFromActorId
626     *
627     * @deprecated since 1.36, use a UserFactory instead
628     *
629     * @since 1.31
630     * @param int $id Valid actor ID
631     * @return User
632     */
633    public static function newFromActorId( $id ) {
634        return MediaWikiServices::getInstance()
635            ->getUserFactory()
636            ->newFromActorId( (int)$id );
637    }
638
639    /**
640     * Returns a User object corresponding to the given UserIdentity.
641     *
642     * @see UserFactory::newFromUserIdentity
643     *
644     * @deprecated since 1.36, use a UserFactory instead
645     *
646     * @since 1.32
647     *
648     * @param UserIdentity $identity
649     *
650     * @return User
651     */
652    public static function newFromIdentity( UserIdentity $identity ) {
653        // Don't use the service if we already have a User object,
654        // so that User::newFromIdentity calls don't break things in unit tests.
655        if ( $identity instanceof User ) {
656            return $identity;
657        }
658
659        return MediaWikiServices::getInstance()
660            ->getUserFactory()
661            ->newFromUserIdentity( $identity );
662    }
663
664    /**
665     * Static factory method for creation from an ID, name, and/or actor ID
666     *
667     * This does not check that the ID, name, and actor ID all correspond to
668     * the same user.
669     *
670     * @see UserFactory::newFromAnyId
671     *
672     * @deprecated since 1.36, use a UserFactory instead
673     *
674     * @since 1.31
675     * @param int|null $userId User ID, if known
676     * @param string|null $userName User name, if known
677     * @param int|null $actorId Actor ID, if known
678     * @param string|false $dbDomain remote wiki to which the User/Actor ID
679     *   applies, or false if none
680     * @return User
681     */
682    public static function newFromAnyId( $userId, $userName, $actorId, $dbDomain = false ) {
683        return MediaWikiServices::getInstance()
684            ->getUserFactory()
685            ->newFromAnyId( $userId, $userName, $actorId, $dbDomain );
686    }
687
688    /**
689     * Factory method to fetch whichever user has a given email confirmation code.
690     * This code is generated when an account is created or its e-mail address
691     * has changed.
692     *
693     * If the code is invalid or has expired, returns NULL.
694     *
695     * @see UserFactory::newFromConfirmationCode
696     *
697     * @deprecated since 1.36, use a UserFactory instead
698     *
699     * @param string $code Confirmation code
700     * @param int $flags IDBAccessObject::READ_* bitfield
701     * @return User|null
702     */
703    public static function newFromConfirmationCode( $code, $flags = IDBAccessObject::READ_NORMAL ) {
704        return MediaWikiServices::getInstance()
705            ->getUserFactory()
706            ->newFromConfirmationCode( (string)$code, $flags );
707    }
708
709    /**
710     * Create a new user object using data from session. If the login
711     * credentials are invalid, the result is an anonymous user.
712     *
713     * @param WebRequest|null $request Object to use; the global request will be used if omitted.
714     * @return User
715     */
716    public static function newFromSession( WebRequest $request = null ) {
717        $user = new User;
718        $user->mFrom = 'session';
719        $user->mRequest = $request;
720        return $user;
721    }
722
723    /**
724     * Create a new user object from a user row.
725     * The row should have the following fields from the user table in it:
726     * - either user_name or user_id to load further data if needed (or both)
727     * - user_real_name
728     * - all other fields (email, etc.)
729     * It is useless to provide the remaining fields if either user_id,
730     * user_name and user_real_name are not provided because the whole row
731     * will be loaded once more from the database when accessing them.
732     *
733     * @param stdClass $row A row from the user table
734     * @param array|null $data Further data to load into the object
735     *  (see User::loadFromRow for valid keys)
736     * @return User
737     */
738    public static function newFromRow( $row, $data = null ) {
739        $user = new User;
740        $user->loadFromRow( $row, $data );
741        return $user;
742    }
743
744    /**
745     * Static factory method for creation of a "system" user from username.
746     *
747     * A "system" user is an account that's used to attribute logged actions
748     * taken by MediaWiki itself, as opposed to a bot or human user. Examples
749     * might include the 'Maintenance script' or 'Conversion script' accounts
750     * used by various scripts in the maintenance/ directory or accounts such
751     * as 'MediaWiki message delivery' used by the MassMessage extension.
752     *
753     * This can optionally create the user if it doesn't exist, and "steal" the
754     * account if it does exist.
755     *
756     * "Stealing" an existing user is intended to make it impossible for normal
757     * authentication processes to use the account, effectively disabling the
758     * account for normal use:
759     * - Email is invalidated, to prevent account recovery by emailing a
760     *   temporary password and to disassociate the account from the existing
761     *   human.
762     * - The token is set to a magic invalid value, to kill existing sessions
763     *   and to prevent $this->setToken() calls from resetting the token to a
764     *   valid value.
765     * - SessionManager is instructed to prevent new sessions for the user, to
766     *   do things like deauthorizing OAuth consumers.
767     * - AuthManager is instructed to revoke access, to invalidate or remove
768     *   passwords and other credentials.
769     *
770     * System users should usually be listed in $wgReservedUsernames.
771     *
772     * @param string $name Username
773     * @param array $options Options are:
774     *  - validate: Type of validation to use:
775     *    - false        No validation
776     *    - 'valid'      Valid for batch processes
777     *    - 'usable'     Valid for batch processes and login
778     *    - 'creatable'  Valid for batch processes, login and account creation,
779     *    default 'valid'. Deprecated since 1.36.
780     *  - create: Whether to create the user if it doesn't already exist, default true
781     *  - steal: Whether to "disable" the account for normal use if it already
782     *    exists, default false
783     * @return User|null
784     * @since 1.27
785     * @see self::isSystemUser()
786     * @see MainConfigSchema::ReservedUsernames
787     */
788    public static function newSystemUser( $name, $options = [] ) {
789        $options += [
790            'validate' => UserRigorOptions::RIGOR_VALID,
791            'create' => true,
792            'steal' => false,
793        ];
794
795        // Username validation
796        // Backwards compatibility with strings / false
797        $validationLevels = [
798            'valid' => UserRigorOptions::RIGOR_VALID,
799            'usable' => UserRigorOptions::RIGOR_USABLE,
800            'creatable' => UserRigorOptions::RIGOR_CREATABLE
801        ];
802        $validate = $options['validate'];
803
804        // @phan-suppress-next-line PhanSuspiciousValueComparison
805        if ( $validate === false ) {
806            $validation = UserRigorOptions::RIGOR_NONE;
807        } elseif ( array_key_exists( $validate, $validationLevels ) ) {
808            $validation = $validationLevels[ $validate ];
809        } else {
810            // Not a recognized value, probably a test for unsupported validation
811            // levels, regardless, just pass it along
812            $validation = $validate;
813        }
814
815        if ( $validation !== UserRigorOptions::RIGOR_VALID ) {
816            wfDeprecatedMsg(
817                __METHOD__ . ' options["validation"] parameter must be omitted or set to "valid".',
818                '1.36'
819            );
820        }
821        $services = MediaWikiServices::getInstance();
822        $userNameUtils = $services->getUserNameUtils();
823
824        $name = $userNameUtils->getCanonical( (string)$name, $validation );
825        if ( $name === false ) {
826            return null;
827        }
828
829        $dbProvider = $services->getDBLoadBalancerFactory();
830        $dbr = $dbProvider->getReplicaDatabase();
831
832        $userQuery = self::newQueryBuilder( $dbr )
833            ->where( [ 'user_name' => $name ] )
834            ->caller( __METHOD__ );
835        $row = $userQuery->fetchRow();
836        if ( !$row ) {
837            // Try the primary database
838            $userQuery->connection( $dbProvider->getPrimaryDatabase() );
839            // Lock the row to prevent insertNewUser() returning null due to race conditions
840            $userQuery->forUpdate();
841            $row = $userQuery->fetchRow();
842        }
843
844        if ( !$row ) {
845            // No user. Create it?
846            // @phan-suppress-next-line PhanImpossibleCondition
847            if ( !$options['create'] ) {
848                // No.
849                return null;
850            }
851
852            // If it's a reserved user that had an anonymous actor created for it at
853            // some point, we need special handling.
854            return self::insertNewUser( static function ( UserIdentity $actor, IDatabase $dbw ) {
855                return MediaWikiServices::getInstance()->getActorStore()
856                    ->acquireSystemActorId( $actor, $dbw );
857            }, $name, [ 'token' => self::INVALID_TOKEN ] );
858        }
859
860        $user = self::newFromRow( $row );
861
862        if ( !$user->isSystemUser() ) {
863            // User exists. Steal it?
864            // @phan-suppress-next-line PhanRedundantCondition
865            if ( !$options['steal'] ) {
866                return null;
867            }
868
869            $services->getAuthManager()->revokeAccessForUser( $name );
870
871            $user->invalidateEmail();
872            $user->mToken = self::INVALID_TOKEN;
873            $user->saveSettings();
874            SessionManager::singleton()->preventSessionsForUser( $user->getName() );
875        }
876
877        return $user;
878    }
879
880    /** @} */
881    // endregion -- end of newFrom*() static factory methods
882
883    /**
884     * Get the username corresponding to a given user ID
885     * @deprecated since 1.43, Use UserIdentityLookup to get name from id
886     * @param int $id User ID
887     * @return string|false The corresponding username
888     */
889    public static function whoIs( $id ) {
890        return MediaWikiServices::getInstance()->getUserCache()
891            ->getProp( $id, 'name' );
892    }
893
894    /**
895     * Get the real name of a user given their user ID
896     *
897     * @deprecated since 1.43, Use UserFactory to get user instance and use User::getRealName
898     * @param int $id User ID
899     * @return string|false The corresponding user's real name
900     */
901    public static function whoIsReal( $id ) {
902        return MediaWikiServices::getInstance()->getUserCache()
903            ->getProp( $id, 'real_name' );
904    }
905
906    /**
907     * Return the users who are members of the given group(s). In case of multiple groups,
908     * users who are members of at least one of them are returned.
909     *
910     * @param string|array $groups A single group name or an array of group names
911     * @param int $limit Max number of users to return. The actual limit will never exceed 5000
912     *   records; larger values are ignored.
913     * @param int|null $after ID the user to start after
914     * @return UserArrayFromResult|ArrayIterator
915     */
916    public static function findUsersByGroup( $groups, $limit = 5000, $after = null ) {
917        if ( $groups === [] ) {
918            return UserArrayFromResult::newFromIDs( [] );
919        }
920        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
921        $queryBuilder = $dbr->newSelectQueryBuilder()
922            ->select( 'ug_user' )
923            ->distinct()
924            ->from( 'user_groups' )
925            ->where( [ 'ug_group' => array_unique( (array)$groups ) ] )
926            ->orderBy( 'ug_user' )
927            ->limit( min( 5000, $limit ) );
928
929        if ( $after !== null ) {
930            $queryBuilder->andWhere( 'ug_user > ' . (int)$after );
931        }
932
933        $ids = $queryBuilder->caller( __METHOD__ )->fetchFieldValues() ?: [];
934        return UserArray::newFromIDs( $ids );
935    }
936
937    /**
938     * Is the input a valid password for this user?
939     *
940     * @param string $password Desired password
941     * @return bool
942     */
943    public function isValidPassword( $password ) {
944        // simple boolean wrapper for checkPasswordValidity
945        return $this->checkPasswordValidity( $password )->isGood();
946    }
947
948    /**
949     * Check if this is a valid password for this user
950     *
951     * Returns a Status object with a set of messages describing
952     * problems with the password. If the return status is fatal,
953     * the action should be refused and the password should not be
954     * checked at all (this is mainly meant for DoS mitigation).
955     * If the return value is OK but not good, the password can be checked,
956     * but the user should not be able to set their password to this.
957     * The value of the returned Status object will be an array which
958     * can have the following fields:
959     * - forceChange (bool): if set to true, the user should not be
960     *   allowed to log with this password unless they change it during
961     *   the login process (see ResetPasswordSecondaryAuthenticationProvider).
962     * - suggestChangeOnLogin (bool): if set to true, the user should be prompted for
963     *   a password change on login.
964     *
965     * @param string $password Desired password
966     * @return Status
967     * @since 1.23
968     */
969    public function checkPasswordValidity( $password ) {
970        $passwordPolicy = MediaWikiServices::getInstance()->getMainConfig()
971            ->get( MainConfigNames::PasswordPolicy );
972
973        $upp = new UserPasswordPolicy(
974            $passwordPolicy['policies'],
975            $passwordPolicy['checks']
976        );
977
978        $status = Status::newGood( [] );
979        $result = false; // init $result to false for the internal checks
980
981        if ( !$this->getHookRunner()->onIsValidPassword( $password, $result, $this ) ) {
982            $status->error( $result );
983            return $status;
984        }
985
986        if ( $result === false ) {
987            $status->merge( $upp->checkUserPassword( $this, $password ), true );
988            return $status;
989        }
990
991        if ( $result === true ) {
992            return $status;
993        }
994
995        $status->error( $result );
996        return $status; // the isValidPassword hook set a string $result and returned true
997    }
998
999    /**
1000     * Set cached properties to default.
1001     *
1002     * @note This no longer clears uncached lazy-initialised properties;
1003     *       the constructor does that instead.
1004     *
1005     * @param string|false $name
1006     * @param int|null $actorId
1007     */
1008    public function loadDefaults( $name = false, $actorId = null ) {
1009        $this->mId = 0;
1010        $this->mName = $name;
1011        $this->mActorId = $actorId;
1012        $this->mRealName = '';
1013        $this->mEmail = '';
1014        $this->isTemp = null;
1015
1016        $loggedOut = $this->mRequest && !defined( 'MW_NO_SESSION' )
1017            ? $this->mRequest->getSession()->getLoggedOutTimestamp() : 0;
1018        if ( $loggedOut !== 0 ) {
1019            $this->mTouched = wfTimestamp( TS_MW, $loggedOut );
1020        } else {
1021            $this->mTouched = '1'; # Allow any pages to be cached
1022        }
1023
1024        $this->mToken = null; // Don't run cryptographic functions till we need a token
1025        $this->mEmailAuthenticated = null;
1026        $this->mEmailToken = '';
1027        $this->mEmailTokenExpires = null;
1028        $this->mRegistration = wfTimestamp( TS_MW );
1029
1030        $this->getHookRunner()->onUserLoadDefaults( $this, $name );
1031    }
1032
1033    /**
1034     * Return whether an item has been loaded.
1035     *
1036     * @param string $item Item to check. Current possibilities:
1037     *   - id
1038     *   - name
1039     *   - realname
1040     * @param string $all 'all' to check if the whole object has been loaded
1041     *   or any other string to check if only the item is available (e.g.
1042     *   for optimisation)
1043     * @return bool
1044     */
1045    public function isItemLoaded( $item, $all = 'all' ) {
1046        return ( $this->mLoadedItems === true && $all === 'all' ) ||
1047            ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true );
1048    }
1049
1050    /**
1051     * Set that an item has been loaded
1052     *
1053     * @internal Only public for use in UserFactory
1054     *
1055     * @param string $item
1056     */
1057    public function setItemLoaded( $item ) {
1058        if ( is_array( $this->mLoadedItems ) ) {
1059            $this->mLoadedItems[$item] = true;
1060        }
1061    }
1062
1063    /**
1064     * Load user data from the session.
1065     *
1066     * @return bool True if the user is logged in, false otherwise.
1067     */
1068    private function loadFromSession() {
1069        // MediaWiki\Session\Session already did the necessary authentication of the user
1070        // returned here, so just use it if applicable.
1071        $session = $this->getRequest()->getSession();
1072        $user = $session->getUser();
1073        if ( $user->isRegistered() ) {
1074            $this->loadFromUserObject( $user );
1075
1076            // Other code expects these to be set in the session, so set them.
1077            $session->set( 'wsUserID', $this->getId() );
1078            $session->set( 'wsUserName', $this->getName() );
1079            $session->set( 'wsToken', $this->getToken() );
1080
1081            return true;
1082        }
1083
1084        return false;
1085    }
1086
1087    /**
1088     * Load user data from the database.
1089     * $this->mId must be set, this is how the user is identified.
1090     *
1091     * @param int $flags IDBAccessObject::READ_* constant bitfield
1092     * @return bool True if the user exists, false if the user is anonymous
1093     */
1094    public function loadFromDatabase( $flags = IDBAccessObject::READ_LATEST ) {
1095        // Paranoia
1096        $this->mId = intval( $this->mId );
1097
1098        if ( !$this->mId ) {
1099            // Anonymous users are not in the database
1100            $this->loadDefaults();
1101            return false;
1102        }
1103
1104        $db = DBAccessObjectUtils::getDBFromRecency(
1105            MediaWikiServices::getInstance()->getDBLoadBalancerFactory(),
1106            $flags
1107        );
1108        $row = self::newQueryBuilder( $db )
1109            ->where( [ 'user_id' => $this->mId ] )
1110            ->recency( $flags )
1111            ->caller( __METHOD__ )
1112            ->fetchRow();
1113
1114        $this->queryFlagsUsed = $flags;
1115
1116        if ( $row !== false ) {
1117            // Initialise user table data
1118            $this->loadFromRow( $row );
1119            return true;
1120        }
1121
1122        // Invalid user_id
1123        $this->mId = 0;
1124        $this->loadDefaults( 'Unknown user' );
1125
1126        return false;
1127    }
1128
1129    /**
1130     * Initialize this object from a row from the user table.
1131     *
1132     * @param stdClass $row Row from the user table to load.
1133     * @param array|null $data Further user data to load into the object
1134     *
1135     *  user_groups   Array of arrays or stdClass result rows out of the user_groups
1136     *                table. Previously you were supposed to pass an array of strings
1137     *                here, but we also need expiry info nowadays, so an array of
1138     *                strings is ignored.
1139     */
1140    protected function loadFromRow( $row, $data = null ) {
1141        if ( !is_object( $row ) ) {
1142            throw new InvalidArgumentException( '$row must be an object' );
1143        }
1144
1145        $all = true;
1146
1147        if ( isset( $row->actor_id ) ) {
1148            $this->mActorId = (int)$row->actor_id;
1149            if ( $this->mActorId !== 0 ) {
1150                $this->mFrom = 'actor';
1151            }
1152            $this->setItemLoaded( 'actor' );
1153        } else {
1154            $all = false;
1155        }
1156
1157        if ( isset( $row->user_name ) && $row->user_name !== '' ) {
1158            $this->mName = $row->user_name;
1159            $this->mFrom = 'name';
1160            $this->setItemLoaded( 'name' );
1161        } else {
1162            $all = false;
1163        }
1164
1165        if ( isset( $row->user_real_name ) ) {
1166            $this->mRealName = $row->user_real_name;
1167            $this->setItemLoaded( 'realname' );
1168        } else {
1169            $all = false;
1170        }
1171
1172        if ( isset( $row->user_id ) ) {
1173            $this->mId = intval( $row->user_id );
1174            if ( $this->mId !== 0 ) {
1175                $this->mFrom = 'id';
1176            }
1177            $this->setItemLoaded( 'id' );
1178        } else {
1179            $all = false;
1180        }
1181
1182        if ( isset( $row->user_editcount ) ) {
1183            // Don't try to set edit count for anonymous users
1184            // We check the id here and not in UserEditTracker because calling
1185            // User::getId() can trigger some other loading. This will result in
1186            // discarding the user_editcount field for rows if the id wasn't set.
1187            if ( $this->mId !== null && $this->mId !== 0 ) {
1188                MediaWikiServices::getInstance()
1189                    ->getUserEditTracker()
1190                    ->setCachedUserEditCount( $this, (int)$row->user_editcount );
1191            }
1192        } else {
1193            $all = false;
1194        }
1195
1196        if ( isset( $row->user_touched ) ) {
1197            $this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
1198        } else {
1199            $all = false;
1200        }
1201
1202        if ( isset( $row->user_token ) ) {
1203            // The definition for the column is binary(32), so trim the NULs
1204            // that appends. The previous definition was char(32), so trim
1205            // spaces too.
1206            $this->mToken = rtrim( $row->user_token, " \0" );
1207            if ( $this->mToken === '' ) {
1208                $this->mToken = null;
1209            }
1210        } else {
1211            $all = false;
1212        }
1213
1214        if ( isset( $row->user_email ) ) {
1215            $this->mEmail = $row->user_email;
1216            $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
1217            $this->mEmailToken = $row->user_email_token;
1218            $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
1219            $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
1220        } else {
1221            $all = false;
1222        }
1223
1224        if ( $all ) {
1225            $this->mLoadedItems = true;
1226        }
1227
1228        if ( is_array( $data ) ) {
1229
1230            if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) {
1231                MediaWikiServices::getInstance()
1232                    ->getUserGroupManager()
1233                    ->loadGroupMembershipsFromArray(
1234                        $this,
1235                        $data['user_groups'],
1236                        $this->queryFlagsUsed
1237                    );
1238            }
1239        }
1240    }
1241
1242    /**
1243     * Load the data for this user object from another user object.
1244     *
1245     * @param User $user
1246     */
1247    protected function loadFromUserObject( $user ) {
1248        $user->load();
1249        foreach ( self::$mCacheVars as $var ) {
1250            $this->$var = $user->$var;
1251        }
1252    }
1253
1254    /**
1255     * Builds update conditions. Additional conditions may be added to $conditions to
1256     * protected against race conditions using a compare-and-set (CAS) mechanism
1257     * based on comparing $this->mTouched with the user_touched field.
1258     *
1259     * @param IReadableDatabase $db
1260     * @param array $conditions WHERE conditions for use with Database::update
1261     * @return array WHERE conditions for use with Database::update
1262     */
1263    protected function makeUpdateConditions( IReadableDatabase $db, array $conditions ) {
1264        if ( $this->mTouched ) {
1265            // CAS check: only update if the row wasn't changed since it was loaded.
1266            $conditions['user_touched'] = $db->timestamp( $this->mTouched );
1267        }
1268
1269        return $conditions;
1270    }
1271
1272    /**
1273     * Bump user_touched if it didn't change since this object was loaded
1274     *
1275     * On success, the mTouched field is updated.
1276     * The user serialization cache is always cleared.
1277     *
1278     * @internal
1279     * @return bool Whether user_touched was actually updated
1280     * @since 1.26
1281     */
1282    public function checkAndSetTouched() {
1283        $this->load();
1284
1285        if ( !$this->mId ) {
1286            return false; // anon
1287        }
1288
1289        // Get a new user_touched that is higher than the old one
1290        $newTouched = $this->newTouchedTimestamp();
1291
1292        $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
1293        $dbw->newUpdateQueryBuilder()
1294            ->update( 'user' )
1295            ->set( [ 'user_touched' => $dbw->timestamp( $newTouched ) ] )
1296            ->where( $this->makeUpdateConditions( $dbw, [
1297                'user_id' => $this->mId,
1298            ] ) )
1299            ->caller( __METHOD__ )->execute();
1300        $success = ( $dbw->affectedRows() > 0 );
1301
1302        if ( $success ) {
1303            $this->mTouched = $newTouched;
1304            $this->clearSharedCache( 'changed' );
1305        } else {
1306            // Clears on failure too since that is desired if the cache is stale
1307            $this->clearSharedCache( 'refresh' );
1308        }
1309
1310        return $success;
1311    }
1312
1313    /**
1314     * Clear various cached data stored in this object. The cache of the user table
1315     * data (i.e. self::$mCacheVars) is not cleared unless $reloadFrom is given.
1316     *
1317     * @param bool|string $reloadFrom Reload user and user_groups table data from a
1318     *   given source. May be "name", "id", "actor", "defaults", "session", or false for no reload.
1319     */
1320    public function clearInstanceCache( $reloadFrom = false ) {
1321        global $wgFullyInitialised;
1322
1323        $this->mDatePreference = null;
1324        $this->mHash = false;
1325        $this->mThisAsAuthority = null;
1326
1327        if ( $wgFullyInitialised && $this->mFrom ) {
1328            $services = MediaWikiServices::getInstance();
1329
1330            if ( $services->peekService( 'PermissionManager' ) ) {
1331                $services->getPermissionManager()->invalidateUsersRightsCache( $this );
1332            }
1333
1334            if ( $services->peekService( 'UserOptionsManager' ) ) {
1335                $services->getUserOptionsManager()->clearUserOptionsCache( $this );
1336            }
1337
1338            if ( $services->peekService( 'TalkPageNotificationManager' ) ) {
1339                $services->getTalkPageNotificationManager()->clearInstanceCache( $this );
1340            }
1341
1342            if ( $services->peekService( 'UserGroupManager' ) ) {
1343                $services->getUserGroupManager()->clearCache( $this );
1344            }
1345
1346            if ( $services->peekService( 'UserEditTracker' ) ) {
1347                $services->getUserEditTracker()->clearUserEditCache( $this );
1348            }
1349
1350            if ( $services->peekService( 'BlockManager' ) ) {
1351                $services->getBlockManager()->clearUserCache( $this );
1352            }
1353        }
1354
1355        if ( $reloadFrom ) {
1356            if ( in_array( $reloadFrom, [ 'name', 'id', 'actor' ] ) ) {
1357                $this->mLoadedItems = [ $reloadFrom => true ];
1358            } else {
1359                $this->mLoadedItems = [];
1360            }
1361            $this->mFrom = $reloadFrom;
1362        }
1363    }
1364
1365    /**
1366     * Is this user subject to rate limiting?
1367     *
1368     * @return bool True if rate limited
1369     */
1370    public function isPingLimitable() {
1371        $limiter = MediaWikiServices::getInstance()->getRateLimiter();
1372        $subject = $this->toRateLimitSubject();
1373        return !$limiter->isExempt( $subject );
1374    }
1375
1376    /**
1377     * Primitive rate limits: enforce maximum actions per time period
1378     * to put a brake on flooding.
1379     *
1380     * The method generates both a generic profiling point and a per action one
1381     * (suffix being "-$action").
1382     *
1383     * @note When using a shared cache like memcached, IP-address
1384     * last-hit counters will be shared across wikis.
1385     *
1386     * @param string $action Action to enforce; 'edit' if unspecified
1387     * @param int $incrBy Positive amount to increment counter by [defaults to 1]
1388     *
1389     * @return bool True if a rate limiter was tripped
1390     */
1391    public function pingLimiter( $action = 'edit', $incrBy = 1 ) {
1392        return $this->getThisAsAuthority()->limit( $action, $incrBy, null );
1393    }
1394
1395    /**
1396     * @internal for use by UserAuthority only!
1397     * @return RateLimitSubject
1398     */
1399    public function toRateLimitSubject(): RateLimitSubject {
1400        $flags = [
1401            'exempt' => $this->isAllowed( 'noratelimit' ),
1402            'newbie' => $this->isNewbie(),
1403        ];
1404
1405        return new RateLimitSubject( $this, $this->getRequest()->getIP(), $flags );
1406    }
1407
1408    /**
1409     * Get the block affecting the user, or null if the user is not blocked
1410     *
1411     * @param int|bool $freshness One of the IDBAccessObject::READ_XXX constants.
1412     *                 For backwards compatibility, a boolean is also accepted,
1413     *                 with true meaning READ_NORMAL and false meaning
1414     *                 READ_LATEST.
1415     * @param bool $disableIpBlockExemptChecking This is used internally to prevent
1416     *   a infinite recursion with autopromote. See T270145.
1417     *
1418     * @return ?AbstractBlock
1419     */
1420    public function getBlock(
1421        $freshness = IDBAccessObject::READ_NORMAL,
1422        $disableIpBlockExemptChecking = false
1423    ): ?Block {
1424        if ( is_bool( $freshness ) ) {
1425            $fromReplica = $freshness;
1426        } else {
1427            $fromReplica = ( $freshness !== IDBAccessObject::READ_LATEST );
1428        }
1429
1430        if ( $disableIpBlockExemptChecking ) {
1431            $isExempt = false;
1432        } else {
1433            $isExempt = $this->isAllowed( 'ipblock-exempt' );
1434        }
1435
1436        // TODO: Block checking shouldn't really be done from the User object. Block
1437        // checking can involve checking for IP blocks, cookie blocks, and/or XFF blocks,
1438        // which need more knowledge of the request context than the User should have.
1439        // Since we do currently check blocks from the User, we have to do the following
1440        // here:
1441        // - Check if this is the user associated with the main request
1442        // - If so, pass the relevant request information to the block manager
1443        $request = null;
1444        if ( !$isExempt && $this->isGlobalSessionUser() ) {
1445            // This is the global user, so we need to pass the request
1446            $request = $this->getRequest();
1447        }
1448
1449        return MediaWikiServices::getInstance()->getBlockManager()->getBlock(
1450            $this,
1451            $request,
1452            $fromReplica,
1453        );
1454    }
1455
1456    /**
1457     * Check if user is blocked on all wikis.
1458     * Do not use for actual edit permission checks!
1459     * This is intended for quick UI checks.
1460     *
1461     * @param string $ip IP address, uses current client if none given
1462     * @return bool True if blocked, false otherwise
1463     * @deprecated since 1.40, emits deprecation warnings since 1.43. Use getBlock instead.
1464     */
1465    public function isBlockedGlobally( $ip = '' ) {
1466        wfDeprecated( __METHOD__, '1.40' );
1467        return $this->getGlobalBlock( $ip ) instanceof AbstractBlock;
1468    }
1469
1470    /**
1471     * Check if user is blocked on all wikis.
1472     * Do not use for actual edit permission checks!
1473     * This is intended for quick UI checks.
1474     *
1475     * @param string $ip IP address, uses current client if none given
1476     * @return AbstractBlock|null Block object if blocked, null otherwise
1477     * @deprecated since 1.40. Use getBlock instead
1478     */
1479    public function getGlobalBlock( $ip = '' ) {
1480        if ( $this->mGlobalBlock !== null ) {
1481            return $this->mGlobalBlock ?: null;
1482        }
1483        // User is already an IP?
1484        if ( IPUtils::isIPAddress( $this->getName() ) ) {
1485            $ip = $this->getName();
1486        } elseif ( !$ip ) {
1487            $ip = $this->getRequest()->getIP();
1488        }
1489        $blocked = false;
1490        $block = null;
1491        $this->getHookRunner()->onUserIsBlockedGlobally( $this, $ip, $blocked, $block );
1492
1493        if ( $blocked && $block === null ) {
1494            // back-compat: UserIsBlockedGlobally didn't have $block param first
1495            $block = new SystemBlock( [
1496                'address' => $ip,
1497                'systemBlock' => 'global-block'
1498            ] );
1499        }
1500
1501        $this->mGlobalBlock = $blocked ? $block : false;
1502        return $this->mGlobalBlock ?: null;
1503    }
1504
1505    /**
1506     * Check if user account is locked
1507     *
1508     * @return bool True if locked, false otherwise
1509     */
1510    public function isLocked() {
1511        if ( $this->mLocked !== null ) {
1512            return $this->mLocked;
1513        }
1514        // Reset for hook
1515        $this->mLocked = false;
1516        $this->getHookRunner()->onUserIsLocked( $this, $this->mLocked );
1517        return $this->mLocked;
1518    }
1519
1520    /**
1521     * Check if user account is hidden
1522     *
1523     * @return bool True if hidden, false otherwise
1524     */
1525    public function isHidden() {
1526        $block = $this->getBlock();
1527        return $block ? $block->getHideName() : false;
1528    }
1529
1530    /**
1531     * Get the user's ID.
1532     * @param string|false $wikiId The wiki ID expected by the caller.
1533     * @return int The user's ID; 0 if the user is anonymous or nonexistent
1534     */
1535    public function getId( $wikiId = self::LOCAL ): int {
1536        $this->assertWiki( $wikiId );
1537        if ( $this->mId === null && $this->mName !== null ) {
1538            $userNameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
1539            if ( $userNameUtils->isIP( $this->mName ) || ExternalUserNames::isExternal( $this->mName ) ) {
1540                // Special case, we know the user is anonymous
1541                // Note that "external" users are "local" (they have an actor ID that is relative to
1542                // the local wiki).
1543                return 0;
1544            }
1545        }
1546
1547        if ( !$this->isItemLoaded( 'id' ) ) {
1548            // Don't load if this was initialized from an ID
1549            $this->load();
1550        }
1551
1552        return (int)$this->mId;
1553    }
1554
1555    /**
1556     * Set the user and reload all fields according to a given ID
1557     * @param int $v User ID to reload
1558     */
1559    public function setId( $v ) {
1560        $this->mId = $v;
1561        $this->clearInstanceCache( 'id' );
1562    }
1563
1564    /**
1565     * Get the user name, or the IP of an anonymous user
1566     * @return string User's name or IP address
1567     */
1568    public function getName(): string {
1569        if ( $this->isItemLoaded( 'name', 'only' ) ) {
1570            // Special case optimisation
1571            return $this->mName;
1572        }
1573
1574        $this->load();
1575        if ( $this->mName === false ) {
1576            // Clean up IPs
1577            $this->mName = IPUtils::sanitizeIP( $this->getRequest()->getIP() );
1578        }
1579
1580        return $this->mName;
1581    }
1582
1583    /**
1584     * Set the user name.
1585     *
1586     * This does not reload fields from the database according to the given
1587     * name. Rather, it is used to create a temporary "nonexistent user" for
1588     * later addition to the database. It can also be used to set the IP
1589     * address for an anonymous user to something other than the current
1590     * remote IP.
1591     *
1592     * @note User::newFromName() has roughly the same function, when the named user
1593     * does not exist.
1594     * @param string $str New user name to set
1595     */
1596    public function setName( $str ) {
1597        $this->load();
1598        $this->mName = $str;
1599    }
1600
1601    /**
1602     * Check if user can exist as an entry in the user table, i.e. it is a user that is not an
1603     * IP address or an external user. Users that can exist are either created from a name, or
1604     * the user is registered (temp or permanent user).
1605     *
1606     * See T316303 for the use case for this.  This method is likely only useful for wiki farms
1607     * where a central user exists and processing is being done with the user name, but the
1608     * corresponding local user on a wiki may not yet have been created.
1609     *
1610     * @return bool
1611     * @since 1.43
1612     */
1613    public function canExist() {
1614        $userNameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
1615        return ( ( $this->mFrom === 'name' && $userNameUtils->isUsable( $this->mName ) ) || $this->isRegistered() );
1616    }
1617
1618    /**
1619     * Get the user's actor ID.
1620     * @since 1.31
1621     * @note This method was removed from the UserIdentity interface in 1.36,
1622     *       but remains supported in the User class for now.
1623     *       New code should use ActorNormalization::findActorId() or
1624     *       ActorNormalization::acquireActorId() instead.
1625     * @param IDatabase|string|false $dbwOrWikiId Deprecated since 1.36.
1626     *        If a database connection is passed, a new actor ID is assigned if needed.
1627     *        ActorNormalization::acquireActorId() should be used for that purpose instead.
1628     * @return int The actor's ID, or 0 if no actor ID exists and $dbw was null
1629     * @throws PreconditionException if $dbwOrWikiId is a string and does not match the local wiki
1630     */
1631    public function getActorId( $dbwOrWikiId = self::LOCAL ): int {
1632        if ( $dbwOrWikiId ) {
1633            wfDeprecatedMsg( 'Passing a parameter to getActorId() is deprecated', '1.36' );
1634        }
1635
1636        if ( is_string( $dbwOrWikiId ) ) {
1637            $this->assertWiki( $dbwOrWikiId );
1638        }
1639
1640        if ( !$this->isItemLoaded( 'actor' ) ) {
1641            $this->load();
1642        }
1643
1644        if ( !$this->mActorId && $dbwOrWikiId instanceof IDatabase ) {
1645            MediaWikiServices::getInstance()
1646                ->getActorStoreFactory()
1647                ->getActorNormalization( $dbwOrWikiId->getDomainID() )
1648                ->acquireActorId( $this, $dbwOrWikiId );
1649            // acquireActorId will call setActorId on $this
1650            Assert::postcondition(
1651                $this->mActorId !== null,
1652                "Failed to acquire actor ID for user id {$this->mId} name {$this->mName}"
1653            );
1654        }
1655
1656        return (int)$this->mActorId;
1657    }
1658
1659    /**
1660     * Sets the actor id.
1661     * For use by ActorStore only.
1662     * Should be removed once callers of getActorId() have been migrated to using ActorNormalization.
1663     *
1664     * @internal
1665     * @deprecated since 1.36
1666     * @param int $actorId
1667     */
1668    public function setActorId( int $actorId ) {
1669        $this->mActorId = $actorId;
1670        $this->setItemLoaded( 'actor' );
1671    }
1672
1673    /**
1674     * Get the user's name escaped by underscores.
1675     * @return string Username escaped by underscores.
1676     */
1677    public function getTitleKey(): string {
1678        return str_replace( ' ', '_', $this->getName() );
1679    }
1680
1681    /**
1682     * Generate a current or new-future timestamp to be stored in the
1683     * user_touched field when we update things.
1684     *
1685     * @return string Timestamp in TS_MW format
1686     */
1687    private function newTouchedTimestamp() {
1688        $time = (int)ConvertibleTimestamp::now( TS_UNIX );
1689        if ( $this->mTouched ) {
1690            $time = max( $time, (int)ConvertibleTimestamp::convert( TS_UNIX, $this->mTouched ) + 1 );
1691        }
1692
1693        return ConvertibleTimestamp::convert( TS_MW, $time );
1694    }
1695
1696    /**
1697     * Clear user data from memcached
1698     *
1699     * Use after applying updates to the database; caller's
1700     * responsibility to update user_touched if appropriate.
1701     *
1702     * Called implicitly from invalidateCache() and saveSettings().
1703     *
1704     * @param string $mode Use 'refresh' to clear now or 'changed' to clear before DB commit
1705     */
1706    public function clearSharedCache( $mode = 'refresh' ) {
1707        if ( !$this->getId() ) {
1708            return;
1709        }
1710
1711        $dbProvider = MediaWikiServices::getInstance()->getConnectionProvider();
1712        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1713        $key = $this->getCacheKey( $cache );
1714
1715        if ( $mode === 'refresh' ) {
1716            $cache->delete( $key, 1 ); // low tombstone/"hold-off" TTL
1717        } else {
1718            $dbProvider->getPrimaryDatabase()->onTransactionPreCommitOrIdle(
1719                static function () use ( $cache, $key ) {
1720                    $cache->delete( $key );
1721                },
1722                __METHOD__
1723            );
1724        }
1725    }
1726
1727    /**
1728     * Immediately touch the user data cache for this account
1729     *
1730     * Calls touch() and removes account data from memcached
1731     */
1732    public function invalidateCache() {
1733        $this->touch();
1734        $this->clearSharedCache( 'changed' );
1735    }
1736
1737    /**
1738     * Update the "touched" timestamp for the user
1739     *
1740     * This is useful on various login/logout events when making sure that
1741     * a browser or proxy that has multiple tenants does not suffer cache
1742     * pollution where the new user sees the old users content. The value
1743     * of getTouched() is checked when determining 304 vs 200 responses.
1744     * Unlike invalidateCache(), this preserves the User object cache and
1745     * avoids database writes.
1746     *
1747     * @since 1.25
1748     */
1749    public function touch() {
1750        $id = $this->getId();
1751        if ( $id ) {
1752            $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1753            $key = $cache->makeKey( 'user-quicktouched', 'id', $id );
1754            $cache->touchCheckKey( $key );
1755            $this->mQuickTouched = null;
1756        }
1757    }
1758
1759    /**
1760     * Validate the cache for this account.
1761     * @param string $timestamp A timestamp in TS_MW format
1762     * @return bool
1763     */
1764    public function validateCache( $timestamp ) {
1765        return ( $timestamp >= $this->getTouched() );
1766    }
1767
1768    /**
1769     * Get the user touched timestamp
1770     *
1771     * Use this value only to validate caches via inequalities
1772     * such as in the case of HTTP If-Modified-Since response logic
1773     *
1774     * @return string TS_MW Timestamp
1775     */
1776    public function getTouched() {
1777        $this->load();
1778
1779        if ( $this->mId ) {
1780            if ( $this->mQuickTouched === null ) {
1781                $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1782                $key = $cache->makeKey( 'user-quicktouched', 'id', $this->mId );
1783
1784                $this->mQuickTouched = wfTimestamp( TS_MW, $cache->getCheckKeyTime( $key ) );
1785            }
1786
1787            return max( $this->mTouched, $this->mQuickTouched );
1788        }
1789
1790        return $this->mTouched;
1791    }
1792
1793    /**
1794     * Get the user_touched timestamp field (time of last DB updates)
1795     * @return string TS_MW Timestamp
1796     * @since 1.26
1797     */
1798    public function getDBTouched() {
1799        $this->load();
1800
1801        return $this->mTouched;
1802    }
1803
1804    /**
1805     * Changes credentials of the user.
1806     *
1807     * This is a convenience wrapper around AuthManager::changeAuthenticationData.
1808     * Note that this can return a status that isOK() but not isGood() on certain types of failures,
1809     * e.g. when no provider handled the change.
1810     *
1811     * @param array $data A set of authentication data in fieldname => value format. This is the
1812     *   same data you would pass the changeauthenticationdata API - 'username', 'password' etc.
1813     * @return Status
1814     * @since 1.27
1815     */
1816    public function changeAuthenticationData( array $data ) {
1817        $manager = MediaWikiServices::getInstance()->getAuthManager();
1818        $reqs = $manager->getAuthenticationRequests( AuthManager::ACTION_CHANGE, $this );
1819        $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
1820
1821        $status = Status::newGood( 'ignored' );
1822        foreach ( $reqs as $req ) {
1823            $status->merge( $manager->allowsAuthenticationDataChange( $req ), true );
1824        }
1825        if ( $status->getValue() === 'ignored' ) {
1826            $status->warning( 'authenticationdatachange-ignored' );
1827        }
1828
1829        if ( $status->isGood() ) {
1830            foreach ( $reqs as $req ) {
1831                $manager->changeAuthenticationData( $req );
1832            }
1833        }
1834        return $status;
1835    }
1836
1837    /**
1838     * Get the user's current token.
1839     * @param bool $forceCreation Force the generation of a new token if the
1840     *   user doesn't have one (default=true for backwards compatibility).
1841     * @return string|null Token
1842     */
1843    public function getToken( $forceCreation = true ) {
1844        $authenticationTokenVersion = MediaWikiServices::getInstance()
1845            ->getMainConfig()->get( MainConfigNames::AuthenticationTokenVersion );
1846
1847        $this->load();
1848        if ( !$this->mToken && $forceCreation ) {
1849            $this->setToken();
1850        }
1851
1852        if ( !$this->mToken ) {
1853            // The user doesn't have a token, return null to indicate that.
1854            return null;
1855        }
1856
1857        if ( $this->mToken === self::INVALID_TOKEN ) {
1858            // We return a random value here so existing token checks are very
1859            // likely to fail.
1860            return MWCryptRand::generateHex( self::TOKEN_LENGTH );
1861        }
1862
1863        if ( $authenticationTokenVersion === null ) {
1864            // $wgAuthenticationTokenVersion not in use, so return the raw secret
1865            return $this->mToken;
1866        }
1867
1868        // $wgAuthenticationTokenVersion in use, so hmac it.
1869        $ret = MWCryptHash::hmac( $authenticationTokenVersion, $this->mToken, false );
1870
1871        // The raw hash can be overly long. Shorten it up.
1872        $len = max( 32, self::TOKEN_LENGTH );
1873        if ( strlen( $ret ) < $len ) {
1874            // Should never happen, even md5 is 128 bits
1875            throw new \UnexpectedValueException( 'Hmac returned less than 128 bits' );
1876        }
1877
1878        return substr( $ret, -$len );
1879    }
1880
1881    /**
1882     * Set the random token (used for persistent authentication)
1883     * Called from loadDefaults() among other places.
1884     *
1885     * @param string|false $token If specified, set the token to this value
1886     */
1887    public function setToken( $token = false ) {
1888        $this->load();
1889        if ( $this->mToken === self::INVALID_TOKEN ) {
1890            LoggerFactory::getInstance( 'session' )
1891                ->debug( __METHOD__ . ": Ignoring attempt to set token for system user \"$this\"" );
1892        } elseif ( !$token ) {
1893            $this->mToken = MWCryptRand::generateHex( self::TOKEN_LENGTH );
1894        } else {
1895            $this->mToken = $token;
1896        }
1897    }
1898
1899    /**
1900     * Get the user's e-mail address
1901     * @return string User's email address
1902     */
1903    public function getEmail(): string {
1904        $this->load();
1905        $email = $this->mEmail;
1906        $this->getHookRunner()->onUserGetEmail( $this, $email );
1907        // In case a hook handler returns e.g. null
1908        $this->mEmail = is_string( $email ) ? $email : '';
1909        return $this->mEmail;
1910    }
1911
1912    /**
1913     * Get the timestamp of the user's e-mail authentication
1914     * @return string TS_MW timestamp
1915     */
1916    public function getEmailAuthenticationTimestamp() {
1917        $this->load();
1918        $this->getHookRunner()->onUserGetEmailAuthenticationTimestamp(
1919            $this, $this->mEmailAuthenticated );
1920        return $this->mEmailAuthenticated;
1921    }
1922
1923    /**
1924     * Set the user's e-mail address
1925     * @param string $str New e-mail address
1926     */
1927    public function setEmail( string $str ) {
1928        $this->load();
1929        if ( $str == $this->getEmail() ) {
1930            return;
1931        }
1932        $this->invalidateEmail();
1933        $this->mEmail = $str;
1934        $this->getHookRunner()->onUserSetEmail( $this, $this->mEmail );
1935    }
1936
1937    /**
1938     * Set the user's e-mail address and send a confirmation mail if needed.
1939     *
1940     * @since 1.20
1941     * @param string $str New e-mail address
1942     * @return Status
1943     */
1944    public function setEmailWithConfirmation( string $str ) {
1945        $config = MediaWikiServices::getInstance()->getMainConfig();
1946        $enableEmail = $config->get( MainConfigNames::EnableEmail );
1947
1948        if ( !$enableEmail ) {
1949            return Status::newFatal( 'emaildisabled' );
1950        }
1951
1952        $oldaddr = $this->getEmail();
1953        if ( $str === $oldaddr ) {
1954            return Status::newGood( true );
1955        }
1956
1957        $type = $oldaddr != '' ? 'changed' : 'set';
1958        $notificationResult = null;
1959
1960        $emailAuthentication = $config->get( MainConfigNames::EmailAuthentication );
1961
1962        if ( $emailAuthentication && $type === 'changed' ) {
1963            // Send the user an email notifying the user of the change in registered
1964            // email address on their previous email address
1965            $change = $str != '' ? 'changed' : 'removed';
1966            $notificationResult = $this->sendMail(
1967                wfMessage( 'notificationemail_subject_' . $change )->text(),
1968                wfMessage( 'notificationemail_body_' . $change,
1969                    $this->getRequest()->getIP(),
1970                    $this->getName(),
1971                    $str )->text()
1972            );
1973        }
1974
1975        $this->setEmail( $str );
1976
1977        if ( $str !== '' && $emailAuthentication ) {
1978            // Send a confirmation request to the new address if needed
1979            $result = $this->sendConfirmationMail( $type );
1980
1981            if ( $notificationResult !== null ) {
1982                $result->merge( $notificationResult );
1983            }
1984
1985            if ( $result->isGood() ) {
1986                // Say to the caller that a confirmation and notification mail has been sent
1987                $result->value = 'eauth';
1988            }
1989        } else {
1990            $result = Status::newGood( true );
1991        }
1992
1993        return $result;
1994    }
1995
1996    /**
1997     * Get the user's real name
1998     * @return string User's real name
1999     */
2000    public function getRealName(): string {
2001        if ( !$this->isItemLoaded( 'realname' ) ) {
2002            $this->load();
2003        }
2004
2005        return $this->mRealName;
2006    }
2007
2008    /**
2009     * Set the user's real name
2010     * @param string $str New real name
2011     */
2012    public function setRealName( string $str ) {
2013        $this->load();
2014        $this->mRealName = $str;
2015    }
2016
2017    /**
2018     * Get a token stored in the preferences (like the watchlist one),
2019     * resetting it if it's empty (and saving changes).
2020     *
2021     * @param string $oname The option name to retrieve the token from
2022     * @return string|false User's current value for the option, or false if this option is disabled.
2023     * @see resetTokenFromOption()
2024     * @see getOption()
2025     * @deprecated since 1.26 Applications should use the OAuth extension
2026     */
2027    public function getTokenFromOption( $oname ) {
2028        $hiddenPrefs =
2029            MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::HiddenPrefs );
2030
2031        $id = $this->getId();
2032        if ( !$id || in_array( $oname, $hiddenPrefs ) ) {
2033            return false;
2034        }
2035
2036        $userOptionsLookup = MediaWikiServices::getInstance()
2037            ->getUserOptionsLookup();
2038        $token = $userOptionsLookup->getOption( $this, (string)$oname );
2039        if ( !$token ) {
2040            // Default to a value based on the user token to avoid space
2041            // wasted on storing tokens for all users. When this option
2042            // is set manually by the user, only then is it stored.
2043            $token = hash_hmac( 'sha1', "$oname:$id", $this->getToken() );
2044        }
2045
2046        return $token;
2047    }
2048
2049    /**
2050