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