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