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