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