Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
39.32% |
462 / 1175 |
|
42.65% |
58 / 136 |
CRAP | |
0.00% |
0 / 1 |
User | |
39.35% |
462 / 1174 |
|
42.65% |
58 / 136 |
31575.75 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getWikiId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__toString | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
__get | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
__set | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
__sleep | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
isSafeToLoad | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
20 | |||
load | |
0.00% |
0 / 59 |
|
0.00% |
0 / 1 |
342 | |||
loadFromId | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
purge | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getCacheKey | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
loadFromCache | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
72 | |||
newFromName | |
85.71% |
12 / 14 |
|
0.00% |
0 / 1 |
4.05 | |||
newFromId | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
newFromActorId | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
newFromIdentity | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
newFromAnyId | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
newFromConfirmationCode | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
newFromSession | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
newFromRow | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
newSystemUser | |
96.23% |
51 / 53 |
|
0.00% |
0 / 1 |
10 | |||
whoIs | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
whoIsReal | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
findUsersByGroup | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
4 | |||
isValidPassword | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
checkPasswordValidity | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
4 | |||
loadDefaults | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
4.00 | |||
isItemLoaded | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
4 | |||
setItemLoaded | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
loadFromSession | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
loadFromDatabase | |
90.00% |
18 / 20 |
|
0.00% |
0 / 1 |
3.01 | |||
loadFromRow | |
85.71% |
48 / 56 |
|
0.00% |
0 / 1 |
21.17 | |||
loadFromUserObject | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
makeUpdateConditions | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
checkAndSetTouched | |
88.89% |
16 / 18 |
|
0.00% |
0 / 1 |
3.01 | |||
clearInstanceCache | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
132 | |||
isPingLimitable | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
pingLimiter | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
toRateLimitSubject | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getBlock | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
5.01 | |||
isBlockedGlobally | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getGlobalBlock | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
90 | |||
isLocked | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
isHidden | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getId | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
6.07 | |||
setId | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getName | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
setName | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getActorId | |
25.00% |
4 / 16 |
|
0.00% |
0 / 1 |
21.19 | |||
setActorId | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getTitleKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
newTouchedTimestamp | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
clearSharedCache | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
invalidateCache | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
touch | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
validateCache | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTouched | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
3.02 | |||
getDBTouched | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
changeAuthenticationData | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
getToken | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
56 | |||
setToken | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getEmail | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getEmailAuthenticationTimestamp | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
setEmail | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
setEmailWithConfirmation | |
25.00% |
7 / 28 |
|
0.00% |
0 / 1 |
62.05 | |||
getRealName | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setRealName | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getTokenFromOption | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
resetTokenFromOption | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
getDatePreference | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
requiresHTTPS | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
4.02 | |||
getEditCount | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
isRegistered | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isAnon | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isBot | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
isSystemUser | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
isAllowedAny | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isAllowedAll | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isAllowed | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
useRCPatrol | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
useNPPatrol | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
useFilePatrol | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
getRequest | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getExperienceLevel | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
7 | |||
setCookies | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
56 | |||
logout | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
doLogout | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
20 | |||
saveSettings | |
0.00% |
0 / 52 |
|
0.00% |
0 / 1 |
42 | |||
idForName | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
6 | |||
createNew | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
insertNewUser | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
30 | |||
addToDatabase | |
73.08% |
38 / 52 |
|
0.00% |
0 / 1 |
10.58 | |||
spreadAnyEditBlock | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
spreadBlock | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
3.10 | |||
isBlockedFromEmailuser | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
isBlockedFromUpload | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
isAllowedToCreateAccount | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getUserPage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTalkPage | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
isNewbie | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getEditTokenObject | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getEditToken | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
matchEditToken | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
sendConfirmationMail | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
20 | |||
sendMail | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
12 | |||
confirmationToken | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
isWellFormedConfirmationToken | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
confirmationTokenUrl | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
invalidationTokenUrl | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTokenUrl | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
confirmEmail | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
invalidateEmail | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
setEmailAuthenticationTimestamp | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
canSendEmail | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
canReceiveEmail | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
isEmailConfirmed | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
isEmailConfirmationPending | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
getRegistration | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getRightDescription | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getRightDescriptionHtml | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getQueryInfo | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
2 | |||
newQueryBuilder | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
2 | |||
newFatalPermissionDeniedStatus | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getInstanceForUpdate | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
equals | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
getUser | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
probablyCan | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
definitelyCan | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isDefinitelyAllowed | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
authorizeAction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
authorizeRead | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
authorizeWrite | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getThisAsAuthority | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
isGlobalSessionUser | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
isTemp | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
isNamed | |
100.00% |
1 / 1 |
|
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 | |
23 | namespace MediaWiki\User; |
24 | |
25 | use AllowDynamicProperties; |
26 | use ArrayIterator; |
27 | use InvalidArgumentException; |
28 | use MailAddress; |
29 | use MediaWiki\Auth\AuthenticationRequest; |
30 | use MediaWiki\Auth\AuthManager; |
31 | use MediaWiki\Block\AbstractBlock; |
32 | use MediaWiki\Block\Block; |
33 | use MediaWiki\Block\SystemBlock; |
34 | use MediaWiki\Context\RequestContext; |
35 | use MediaWiki\DAO\WikiAwareEntityTrait; |
36 | use MediaWiki\HookContainer\ProtectedHookAccessorTrait; |
37 | use MediaWiki\Logger\LoggerFactory; |
38 | use MediaWiki\Mail\UserEmailContact; |
39 | use MediaWiki\MainConfigNames; |
40 | use MediaWiki\MainConfigSchema; |
41 | use MediaWiki\MediaWikiServices; |
42 | use MediaWiki\Page\PageIdentity; |
43 | use MediaWiki\Parser\Sanitizer; |
44 | use MediaWiki\Password\PasswordFactory; |
45 | use MediaWiki\Password\UserPasswordPolicy; |
46 | use MediaWiki\Permissions\Authority; |
47 | use MediaWiki\Permissions\PermissionStatus; |
48 | use MediaWiki\Permissions\RateLimitSubject; |
49 | use MediaWiki\Permissions\UserAuthority; |
50 | use MediaWiki\Request\WebRequest; |
51 | use MediaWiki\Session\SessionManager; |
52 | use MediaWiki\Session\Token; |
53 | use MediaWiki\Status\Status; |
54 | use MediaWiki\Title\Title; |
55 | use MWCryptHash; |
56 | use MWCryptRand; |
57 | use MWExceptionHandler; |
58 | use RuntimeException; |
59 | use stdClass; |
60 | use Stringable; |
61 | use UnexpectedValueException; |
62 | use Wikimedia\Assert\Assert; |
63 | use Wikimedia\Assert\PreconditionException; |
64 | use Wikimedia\DebugInfo\DebugInfoTrait; |
65 | use Wikimedia\IPUtils; |
66 | use Wikimedia\ObjectCache\WANObjectCache; |
67 | use Wikimedia\Rdbms\Database; |
68 | use Wikimedia\Rdbms\DBAccessObjectUtils; |
69 | use Wikimedia\Rdbms\DBExpectedError; |
70 | use Wikimedia\Rdbms\IDatabase; |
71 | use Wikimedia\Rdbms\IDBAccessObject; |
72 | use Wikimedia\Rdbms\IReadableDatabase; |
73 | use Wikimedia\Rdbms\SelectQueryBuilder; |
74 | use Wikimedia\ScopedCallback; |
75 | use 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] |
93 | class 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 */ |
3397 | class_alias( User::class, 'User' ); |