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