Collapse some else statements and nested if statements
[lhc/web/wiklou.git] / includes / user / User.php
1 <?php
2 /**
3 * Implements the User class for the %MediaWiki software.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22
23 use MediaWiki\MediaWikiServices;
24 use MediaWiki\Session\SessionManager;
25 use MediaWiki\Session\Token;
26 use MediaWiki\Auth\AuthManager;
27 use MediaWiki\Auth\AuthenticationResponse;
28 use MediaWiki\Auth\AuthenticationRequest;
29 use MediaWiki\User\UserIdentity;
30 use MediaWiki\Logger\LoggerFactory;
31 use Wikimedia\IPSet;
32 use Wikimedia\ScopedCallback;
33 use Wikimedia\Rdbms\Database;
34 use Wikimedia\Rdbms\DBExpectedError;
35 use Wikimedia\Rdbms\IDatabase;
36
37 /**
38 * The User object encapsulates all of the user-specific settings (user_id,
39 * name, rights, email address, options, last login time). Client
40 * classes use the getXXX() functions to access these fields. These functions
41 * do all the work of determining whether the user is logged in,
42 * whether the requested option can be satisfied from cookies or
43 * whether a database query is needed. Most of the settings needed
44 * for rendering normal pages are set in the cookie to minimize use
45 * of the database.
46 */
47 class User implements IDBAccessObject, UserIdentity {
48 /**
49 * @const int Number of characters in user_token field.
50 */
51 const TOKEN_LENGTH = 32;
52
53 /**
54 * @const string An invalid value for user_token
55 */
56 const INVALID_TOKEN = '*** INVALID ***';
57
58 /**
59 * @const int Serialized record version.
60 */
61 const VERSION = 13;
62
63 /**
64 * Exclude user options that are set to their default value.
65 * @since 1.25
66 */
67 const GETOPTIONS_EXCLUDE_DEFAULTS = 1;
68
69 /**
70 * @since 1.27
71 */
72 const CHECK_USER_RIGHTS = true;
73
74 /**
75 * @since 1.27
76 */
77 const IGNORE_USER_RIGHTS = false;
78
79 /**
80 * Array of Strings List of member variables which are saved to the
81 * shared cache (memcached). Any operation which changes the
82 * corresponding database fields must call a cache-clearing function.
83 * @showinitializer
84 */
85 protected static $mCacheVars = [
86 // user table
87 'mId',
88 'mName',
89 'mRealName',
90 'mEmail',
91 'mTouched',
92 'mToken',
93 'mEmailAuthenticated',
94 'mEmailToken',
95 'mEmailTokenExpires',
96 'mRegistration',
97 'mEditCount',
98 // user_groups table
99 'mGroupMemberships',
100 // user_properties table
101 'mOptionOverrides',
102 // actor table
103 'mActorId',
104 ];
105
106 /**
107 * Array of Strings Core rights.
108 * Each of these should have a corresponding message of the form
109 * "right-$right".
110 * @showinitializer
111 */
112 protected static $mCoreRights = [
113 'apihighlimits',
114 'applychangetags',
115 'autoconfirmed',
116 'autocreateaccount',
117 'autopatrol',
118 'bigdelete',
119 'block',
120 'blockemail',
121 'bot',
122 'browsearchive',
123 'changetags',
124 'createaccount',
125 'createpage',
126 'createtalk',
127 'delete',
128 'deletechangetags',
129 'deletedhistory',
130 'deletedtext',
131 'deletelogentry',
132 'deleterevision',
133 'edit',
134 'editcontentmodel',
135 'editinterface',
136 'editprotected',
137 'editmyoptions',
138 'editmyprivateinfo',
139 'editmyusercss',
140 'editmyuserjson',
141 'editmyuserjs',
142 'editmywatchlist',
143 'editsemiprotected',
144 'editsitecss',
145 'editsitejson',
146 'editsitejs',
147 'editusercss',
148 'edituserjson',
149 'edituserjs',
150 'hideuser',
151 'import',
152 'importupload',
153 'ipblock-exempt',
154 'managechangetags',
155 'markbotedits',
156 'mergehistory',
157 'minoredit',
158 'move',
159 'movefile',
160 'move-categorypages',
161 'move-rootuserpages',
162 'move-subpages',
163 'nominornewtalk',
164 'noratelimit',
165 'override-export-depth',
166 'pagelang',
167 'patrol',
168 'patrolmarks',
169 'protect',
170 'purge',
171 'read',
172 'reupload',
173 'reupload-own',
174 'reupload-shared',
175 'rollback',
176 'sendemail',
177 'siteadmin',
178 'suppressionlog',
179 'suppressredirect',
180 'suppressrevision',
181 'unblockself',
182 'undelete',
183 'unwatchedpages',
184 'upload',
185 'upload_by_url',
186 'userrights',
187 'userrights-interwiki',
188 'viewmyprivateinfo',
189 'viewmywatchlist',
190 'viewsuppressed',
191 'writeapi',
192 ];
193
194 /**
195 * String Cached results of getAllRights()
196 */
197 protected static $mAllRights = false;
198
199 /** Cache variables */
200 // @{
201 /** @var int */
202 public $mId;
203 /** @var string */
204 public $mName;
205 /** @var int|null */
206 protected $mActorId;
207 /** @var string */
208 public $mRealName;
209
210 /** @var string */
211 public $mEmail;
212 /** @var string TS_MW timestamp from the DB */
213 public $mTouched;
214 /** @var string TS_MW timestamp from cache */
215 protected $mQuickTouched;
216 /** @var string */
217 protected $mToken;
218 /** @var string */
219 public $mEmailAuthenticated;
220 /** @var string */
221 protected $mEmailToken;
222 /** @var string */
223 protected $mEmailTokenExpires;
224 /** @var string */
225 protected $mRegistration;
226 /** @var int */
227 protected $mEditCount;
228 /** @var UserGroupMembership[] Associative array of (group name => UserGroupMembership object) */
229 protected $mGroupMemberships;
230 /** @var array */
231 protected $mOptionOverrides;
232 // @}
233
234 /**
235 * Bool Whether the cache variables have been loaded.
236 */
237 // @{
238 public $mOptionsLoaded;
239
240 /**
241 * Array with already loaded items or true if all items have been loaded.
242 */
243 protected $mLoadedItems = [];
244 // @}
245
246 /**
247 * String Initialization data source if mLoadedItems!==true. May be one of:
248 * - 'defaults' anonymous user initialised from class defaults
249 * - 'name' initialise from mName
250 * - 'id' initialise from mId
251 * - 'actor' initialise from mActorId
252 * - 'session' log in from session if possible
253 *
254 * Use the User::newFrom*() family of functions to set this.
255 */
256 public $mFrom;
257
258 /**
259 * Lazy-initialized variables, invalidated with clearInstanceCache
260 */
261 protected $mNewtalk;
262 /** @var string */
263 protected $mDatePreference;
264 /** @var string */
265 public $mBlockedby;
266 /** @var string */
267 protected $mHash;
268 /** @var array */
269 public $mRights;
270 /** @var string */
271 protected $mBlockreason;
272 /** @var array */
273 protected $mEffectiveGroups;
274 /** @var array */
275 protected $mImplicitGroups;
276 /** @var array */
277 protected $mFormerGroups;
278 /** @var Block */
279 protected $mGlobalBlock;
280 /** @var bool */
281 protected $mLocked;
282 /** @var bool */
283 public $mHideName;
284 /** @var array */
285 public $mOptions;
286
287 /** @var WebRequest */
288 private $mRequest;
289
290 /** @var Block */
291 public $mBlock;
292
293 /** @var bool */
294 protected $mAllowUsertalk;
295
296 /** @var Block */
297 private $mBlockedFromCreateAccount = false;
298
299 /** @var int User::READ_* constant bitfield used to load data */
300 protected $queryFlagsUsed = self::READ_NORMAL;
301
302 public static $idCacheByName = [];
303
304 /**
305 * Lightweight constructor for an anonymous user.
306 * Use the User::newFrom* factory functions for other kinds of users.
307 *
308 * @see newFromName()
309 * @see newFromId()
310 * @see newFromActorId()
311 * @see newFromConfirmationCode()
312 * @see newFromSession()
313 * @see newFromRow()
314 */
315 public function __construct() {
316 $this->clearInstanceCache( 'defaults' );
317 }
318
319 /**
320 * @return string
321 */
322 public function __toString() {
323 return (string)$this->getName();
324 }
325
326 /**
327 * Test if it's safe to load this User object.
328 *
329 * You should typically check this before using $wgUser or
330 * RequestContext::getUser in a method that might be called before the
331 * system has been fully initialized. If the object is unsafe, you should
332 * use an anonymous user:
333 * \code
334 * $user = $wgUser->isSafeToLoad() ? $wgUser : new User;
335 * \endcode
336 *
337 * @since 1.27
338 * @return bool
339 */
340 public function isSafeToLoad() {
341 global $wgFullyInitialised;
342
343 // The user is safe to load if:
344 // * MW_NO_SESSION is undefined AND $wgFullyInitialised is true (safe to use session data)
345 // * mLoadedItems === true (already loaded)
346 // * mFrom !== 'session' (sessions not involved at all)
347
348 return ( !defined( 'MW_NO_SESSION' ) && $wgFullyInitialised ) ||
349 $this->mLoadedItems === true || $this->mFrom !== 'session';
350 }
351
352 /**
353 * Load the user table data for this object from the source given by mFrom.
354 *
355 * @param int $flags User::READ_* constant bitfield
356 */
357 public function load( $flags = self::READ_NORMAL ) {
358 global $wgFullyInitialised;
359
360 if ( $this->mLoadedItems === true ) {
361 return;
362 }
363
364 // Set it now to avoid infinite recursion in accessors
365 $oldLoadedItems = $this->mLoadedItems;
366 $this->mLoadedItems = true;
367 $this->queryFlagsUsed = $flags;
368
369 // If this is called too early, things are likely to break.
370 if ( !$wgFullyInitialised && $this->mFrom === 'session' ) {
371 \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
372 ->warning( 'User::loadFromSession called before the end of Setup.php', [
373 'exception' => new Exception( 'User::loadFromSession called before the end of Setup.php' ),
374 ] );
375 $this->loadDefaults();
376 $this->mLoadedItems = $oldLoadedItems;
377 return;
378 }
379
380 switch ( $this->mFrom ) {
381 case 'defaults':
382 $this->loadDefaults();
383 break;
384 case 'name':
385 // Make sure this thread sees its own changes
386 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
387 if ( $lb->hasOrMadeRecentMasterChanges() ) {
388 $flags |= self::READ_LATEST;
389 $this->queryFlagsUsed = $flags;
390 }
391
392 $this->mId = self::idFromName( $this->mName, $flags );
393 if ( !$this->mId ) {
394 // Nonexistent user placeholder object
395 $this->loadDefaults( $this->mName );
396 } else {
397 $this->loadFromId( $flags );
398 }
399 break;
400 case 'id':
401 // Make sure this thread sees its own changes, if the ID isn't 0
402 if ( $this->mId != 0 ) {
403 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
404 if ( $lb->hasOrMadeRecentMasterChanges() ) {
405 $flags |= self::READ_LATEST;
406 $this->queryFlagsUsed = $flags;
407 }
408 }
409
410 $this->loadFromId( $flags );
411 break;
412 case 'actor':
413 // Make sure this thread sees its own changes
414 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
415 if ( $lb->hasOrMadeRecentMasterChanges() ) {
416 $flags |= self::READ_LATEST;
417 $this->queryFlagsUsed = $flags;
418 }
419
420 list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
421 $row = wfGetDB( $index )->selectRow(
422 'actor',
423 [ 'actor_user', 'actor_name' ],
424 [ 'actor_id' => $this->mActorId ],
425 __METHOD__,
426 $options
427 );
428
429 if ( !$row ) {
430 // Ugh.
431 $this->loadDefaults();
432 } elseif ( $row->actor_user ) {
433 $this->mId = $row->actor_user;
434 $this->loadFromId( $flags );
435 } else {
436 $this->loadDefaults( $row->actor_name );
437 }
438 break;
439 case 'session':
440 if ( !$this->loadFromSession() ) {
441 // Loading from session failed. Load defaults.
442 $this->loadDefaults();
443 }
444 Hooks::run( 'UserLoadAfterLoadFromSession', [ $this ] );
445 break;
446 default:
447 throw new UnexpectedValueException(
448 "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
449 }
450 }
451
452 /**
453 * Load user table data, given mId has already been set.
454 * @param int $flags User::READ_* constant bitfield
455 * @return bool False if the ID does not exist, true otherwise
456 */
457 public function loadFromId( $flags = self::READ_NORMAL ) {
458 if ( $this->mId == 0 ) {
459 // Anonymous users are not in the database (don't need cache)
460 $this->loadDefaults();
461 return false;
462 }
463
464 // Try cache (unless this needs data from the master DB).
465 // NOTE: if this thread called saveSettings(), the cache was cleared.
466 $latest = DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST );
467 if ( $latest ) {
468 if ( !$this->loadFromDatabase( $flags ) ) {
469 // Can't load from ID
470 return false;
471 }
472 } else {
473 $this->loadFromCache();
474 }
475
476 $this->mLoadedItems = true;
477 $this->queryFlagsUsed = $flags;
478
479 return true;
480 }
481
482 /**
483 * @since 1.27
484 * @param string $wikiId
485 * @param int $userId
486 */
487 public static function purge( $wikiId, $userId ) {
488 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
489 $key = $cache->makeGlobalKey( 'user', 'id', $wikiId, $userId );
490 $cache->delete( $key );
491 }
492
493 /**
494 * @since 1.27
495 * @param WANObjectCache $cache
496 * @return string
497 */
498 protected function getCacheKey( WANObjectCache $cache ) {
499 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
500 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
501
502 return $cache->makeGlobalKey( 'user', 'id', $lbFactory->getLocalDomainID(), $this->mId );
503 }
504
505 /**
506 * @param WANObjectCache $cache
507 * @return string[]
508 * @since 1.28
509 */
510 public function getMutableCacheKeys( WANObjectCache $cache ) {
511 $id = $this->getId();
512
513 return $id ? [ $this->getCacheKey( $cache ) ] : [];
514 }
515
516 /**
517 * Load user data from shared cache, given mId has already been set.
518 *
519 * @return bool True
520 * @since 1.25
521 */
522 protected function loadFromCache() {
523 $cache = ObjectCache::getMainWANInstance();
524 $data = $cache->getWithSetCallback(
525 $this->getCacheKey( $cache ),
526 $cache::TTL_HOUR,
527 function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache ) {
528 $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
529 wfDebug( "User: cache miss for user {$this->mId}\n" );
530
531 $this->loadFromDatabase( self::READ_NORMAL );
532 $this->loadGroups();
533 $this->loadOptions();
534
535 $data = [];
536 foreach ( self::$mCacheVars as $name ) {
537 $data[$name] = $this->$name;
538 }
539
540 $ttl = $cache->adaptiveTTL( wfTimestamp( TS_UNIX, $this->mTouched ), $ttl );
541
542 // if a user group membership is about to expire, the cache needs to
543 // expire at that time (T163691)
544 foreach ( $this->mGroupMemberships as $ugm ) {
545 if ( $ugm->getExpiry() ) {
546 $secondsUntilExpiry = wfTimestamp( TS_UNIX, $ugm->getExpiry() ) - time();
547 if ( $secondsUntilExpiry > 0 && $secondsUntilExpiry < $ttl ) {
548 $ttl = $secondsUntilExpiry;
549 }
550 }
551 }
552
553 return $data;
554 },
555 [ 'pcTTL' => $cache::TTL_PROC_LONG, 'version' => self::VERSION ]
556 );
557
558 // Restore from cache
559 foreach ( self::$mCacheVars as $name ) {
560 $this->$name = $data[$name];
561 }
562
563 return true;
564 }
565
566 /** @name newFrom*() static factory methods */
567 // @{
568
569 /**
570 * Static factory method for creation from username.
571 *
572 * This is slightly less efficient than newFromId(), so use newFromId() if
573 * you have both an ID and a name handy.
574 *
575 * @param string $name Username, validated by Title::newFromText()
576 * @param string|bool $validate Validate username. Takes the same parameters as
577 * User::getCanonicalName(), except that true is accepted as an alias
578 * for 'valid', for BC.
579 *
580 * @return User|bool User object, or false if the username is invalid
581 * (e.g. if it contains illegal characters or is an IP address). If the
582 * username is not present in the database, the result will be a user object
583 * with a name, zero user ID and default settings.
584 */
585 public static function newFromName( $name, $validate = 'valid' ) {
586 if ( $validate === true ) {
587 $validate = 'valid';
588 }
589 $name = self::getCanonicalName( $name, $validate );
590 if ( $name === false ) {
591 return false;
592 }
593
594 // Create unloaded user object
595 $u = new User;
596 $u->mName = $name;
597 $u->mFrom = 'name';
598 $u->setItemLoaded( 'name' );
599
600 return $u;
601 }
602
603 /**
604 * Static factory method for creation from a given user ID.
605 *
606 * @param int $id Valid user ID
607 * @return User The corresponding User object
608 */
609 public static function newFromId( $id ) {
610 $u = new User;
611 $u->mId = $id;
612 $u->mFrom = 'id';
613 $u->setItemLoaded( 'id' );
614 return $u;
615 }
616
617 /**
618 * Static factory method for creation from a given actor ID.
619 *
620 * @since 1.31
621 * @param int $id Valid actor ID
622 * @return User The corresponding User object
623 */
624 public static function newFromActorId( $id ) {
625 global $wgActorTableSchemaMigrationStage;
626
627 // Technically we shouldn't allow this without SCHEMA_COMPAT_READ_NEW,
628 // but it does little harm and might be needed for write callers loading a User.
629 if ( !( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_NEW ) ) {
630 throw new BadMethodCallException(
631 'Cannot use ' . __METHOD__
632 . ' when $wgActorTableSchemaMigrationStage lacks SCHEMA_COMPAT_NEW'
633 );
634 }
635
636 $u = new User;
637 $u->mActorId = $id;
638 $u->mFrom = 'actor';
639 $u->setItemLoaded( 'actor' );
640 return $u;
641 }
642
643 /**
644 * Returns a User object corresponding to the given UserIdentity.
645 *
646 * @since 1.32
647 *
648 * @param UserIdentity $identity
649 *
650 * @return User
651 */
652 public static function newFromIdentity( UserIdentity $identity ) {
653 if ( $identity instanceof User ) {
654 return $identity;
655 }
656
657 return self::newFromAnyId(
658 $identity->getId() === 0 ? null : $identity->getId(),
659 $identity->getName() === '' ? null : $identity->getName(),
660 $identity->getActorId() === 0 ? null : $identity->getActorId()
661 );
662 }
663
664 /**
665 * Static factory method for creation from an ID, name, and/or actor ID
666 *
667 * This does not check that the ID, name, and actor ID all correspond to
668 * the same user.
669 *
670 * @since 1.31
671 * @param int|null $userId User ID, if known
672 * @param string|null $userName User name, if known
673 * @param int|null $actorId Actor ID, if known
674 * @return User
675 */
676 public static function newFromAnyId( $userId, $userName, $actorId ) {
677 global $wgActorTableSchemaMigrationStage;
678
679 $user = new User;
680 $user->mFrom = 'defaults';
681
682 // Technically we shouldn't allow this without SCHEMA_COMPAT_READ_NEW,
683 // but it does little harm and might be needed for write callers loading a User.
684 if ( ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_NEW ) && $actorId !== null ) {
685 $user->mActorId = (int)$actorId;
686 if ( $user->mActorId !== 0 ) {
687 $user->mFrom = 'actor';
688 }
689 $user->setItemLoaded( 'actor' );
690 }
691
692 if ( $userName !== null && $userName !== '' ) {
693 $user->mName = $userName;
694 $user->mFrom = 'name';
695 $user->setItemLoaded( 'name' );
696 }
697
698 if ( $userId !== null ) {
699 $user->mId = (int)$userId;
700 if ( $user->mId !== 0 ) {
701 $user->mFrom = 'id';
702 }
703 $user->setItemLoaded( 'id' );
704 }
705
706 if ( $user->mFrom === 'defaults' ) {
707 throw new InvalidArgumentException(
708 'Cannot create a user with no name, no ID, and no actor ID'
709 );
710 }
711
712 return $user;
713 }
714
715 /**
716 * Factory method to fetch whichever user has a given email confirmation code.
717 * This code is generated when an account is created or its e-mail address
718 * has changed.
719 *
720 * If the code is invalid or has expired, returns NULL.
721 *
722 * @param string $code Confirmation code
723 * @param int $flags User::READ_* bitfield
724 * @return User|null
725 */
726 public static function newFromConfirmationCode( $code, $flags = 0 ) {
727 $db = ( $flags & self::READ_LATEST ) == self::READ_LATEST
728 ? wfGetDB( DB_MASTER )
729 : wfGetDB( DB_REPLICA );
730
731 $id = $db->selectField(
732 'user',
733 'user_id',
734 [
735 'user_email_token' => md5( $code ),
736 'user_email_token_expires > ' . $db->addQuotes( $db->timestamp() ),
737 ]
738 );
739
740 return $id ? self::newFromId( $id ) : null;
741 }
742
743 /**
744 * Create a new user object using data from session. If the login
745 * credentials are invalid, the result is an anonymous user.
746 *
747 * @param WebRequest|null $request Object to use; $wgRequest will be used if omitted.
748 * @return User
749 */
750 public static function newFromSession( WebRequest $request = null ) {
751 $user = new User;
752 $user->mFrom = 'session';
753 $user->mRequest = $request;
754 return $user;
755 }
756
757 /**
758 * Create a new user object from a user row.
759 * The row should have the following fields from the user table in it:
760 * - either user_name or user_id to load further data if needed (or both)
761 * - user_real_name
762 * - all other fields (email, etc.)
763 * It is useless to provide the remaining fields if either user_id,
764 * user_name and user_real_name are not provided because the whole row
765 * will be loaded once more from the database when accessing them.
766 *
767 * @param stdClass $row A row from the user table
768 * @param array|null $data Further data to load into the object
769 * (see User::loadFromRow for valid keys)
770 * @return User
771 */
772 public static function newFromRow( $row, $data = null ) {
773 $user = new User;
774 $user->loadFromRow( $row, $data );
775 return $user;
776 }
777
778 /**
779 * Static factory method for creation of a "system" user from username.
780 *
781 * A "system" user is an account that's used to attribute logged actions
782 * taken by MediaWiki itself, as opposed to a bot or human user. Examples
783 * might include the 'Maintenance script' or 'Conversion script' accounts
784 * used by various scripts in the maintenance/ directory or accounts such
785 * as 'MediaWiki message delivery' used by the MassMessage extension.
786 *
787 * This can optionally create the user if it doesn't exist, and "steal" the
788 * account if it does exist.
789 *
790 * "Stealing" an existing user is intended to make it impossible for normal
791 * authentication processes to use the account, effectively disabling the
792 * account for normal use:
793 * - Email is invalidated, to prevent account recovery by emailing a
794 * temporary password and to disassociate the account from the existing
795 * human.
796 * - The token is set to a magic invalid value, to kill existing sessions
797 * and to prevent $this->setToken() calls from resetting the token to a
798 * valid value.
799 * - SessionManager is instructed to prevent new sessions for the user, to
800 * do things like deauthorizing OAuth consumers.
801 * - AuthManager is instructed to revoke access, to invalidate or remove
802 * passwords and other credentials.
803 *
804 * @param string $name Username
805 * @param array $options Options are:
806 * - validate: As for User::getCanonicalName(), default 'valid'
807 * - create: Whether to create the user if it doesn't already exist, default true
808 * - steal: Whether to "disable" the account for normal use if it already
809 * exists, default false
810 * @return User|null
811 * @since 1.27
812 */
813 public static function newSystemUser( $name, $options = [] ) {
814 $options += [
815 'validate' => 'valid',
816 'create' => true,
817 'steal' => false,
818 ];
819
820 $name = self::getCanonicalName( $name, $options['validate'] );
821 if ( $name === false ) {
822 return null;
823 }
824
825 $dbr = wfGetDB( DB_REPLICA );
826 $userQuery = self::getQueryInfo();
827 $row = $dbr->selectRow(
828 $userQuery['tables'],
829 $userQuery['fields'],
830 [ 'user_name' => $name ],
831 __METHOD__,
832 [],
833 $userQuery['joins']
834 );
835 if ( !$row ) {
836 // Try the master database...
837 $dbw = wfGetDB( DB_MASTER );
838 $row = $dbw->selectRow(
839 $userQuery['tables'],
840 $userQuery['fields'],
841 [ 'user_name' => $name ],
842 __METHOD__,
843 [],
844 $userQuery['joins']
845 );
846 }
847
848 if ( !$row ) {
849 // No user. Create it?
850 return $options['create']
851 ? self::createNew( $name, [ 'token' => self::INVALID_TOKEN ] )
852 : null;
853 }
854
855 $user = self::newFromRow( $row );
856
857 // A user is considered to exist as a non-system user if it can
858 // authenticate, or has an email set, or has a non-invalid token.
859 if ( $user->mEmail || $user->mToken !== self::INVALID_TOKEN ||
860 AuthManager::singleton()->userCanAuthenticate( $name )
861 ) {
862 // User exists. Steal it?
863 if ( !$options['steal'] ) {
864 return null;
865 }
866
867 AuthManager::singleton()->revokeAccessForUser( $name );
868
869 $user->invalidateEmail();
870 $user->mToken = self::INVALID_TOKEN;
871 $user->saveSettings();
872 SessionManager::singleton()->preventSessionsForUser( $user->getName() );
873 }
874
875 return $user;
876 }
877
878 // @}
879
880 /**
881 * Get the username corresponding to a given user ID
882 * @param int $id User ID
883 * @return string|bool The corresponding username
884 */
885 public static function whoIs( $id ) {
886 return UserCache::singleton()->getProp( $id, 'name' );
887 }
888
889 /**
890 * Get the real name of a user given their user ID
891 *
892 * @param int $id User ID
893 * @return string|bool The corresponding user's real name
894 */
895 public static function whoIsReal( $id ) {
896 return UserCache::singleton()->getProp( $id, 'real_name' );
897 }
898
899 /**
900 * Get database id given a user name
901 * @param string $name Username
902 * @param int $flags User::READ_* constant bitfield
903 * @return int|null The corresponding user's ID, or null if user is nonexistent
904 */
905 public static function idFromName( $name, $flags = self::READ_NORMAL ) {
906 // Don't explode on self::$idCacheByName[$name] if $name is not a string but e.g. a User object
907 $name = (string)$name;
908 $nt = Title::makeTitleSafe( NS_USER, $name );
909 if ( is_null( $nt ) ) {
910 // Illegal name
911 return null;
912 }
913
914 if ( !( $flags & self::READ_LATEST ) && array_key_exists( $name, self::$idCacheByName ) ) {
915 return self::$idCacheByName[$name];
916 }
917
918 list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
919 $db = wfGetDB( $index );
920
921 $s = $db->selectRow(
922 'user',
923 [ 'user_id' ],
924 [ 'user_name' => $nt->getText() ],
925 __METHOD__,
926 $options
927 );
928
929 if ( $s === false ) {
930 $result = null;
931 } else {
932 $result = (int)$s->user_id;
933 }
934
935 self::$idCacheByName[$name] = $result;
936
937 if ( count( self::$idCacheByName ) > 1000 ) {
938 self::$idCacheByName = [];
939 }
940
941 return $result;
942 }
943
944 /**
945 * Reset the cache used in idFromName(). For use in tests.
946 */
947 public static function resetIdByNameCache() {
948 self::$idCacheByName = [];
949 }
950
951 /**
952 * Does the string match an anonymous IP address?
953 *
954 * This function exists for username validation, in order to reject
955 * usernames which are similar in form to IP addresses. Strings such
956 * as 300.300.300.300 will return true because it looks like an IP
957 * address, despite not being strictly valid.
958 *
959 * We match "\d{1,3}\.\d{1,3}\.\d{1,3}\.xxx" as an anonymous IP
960 * address because the usemod software would "cloak" anonymous IP
961 * addresses like this, if we allowed accounts like this to be created
962 * new users could get the old edits of these anonymous users.
963 *
964 * @param string $name Name to match
965 * @return bool
966 */
967 public static function isIP( $name ) {
968 return preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/', $name )
969 || IP::isIPv6( $name );
970 }
971
972 /**
973 * Is the user an IP range?
974 *
975 * @since 1.30
976 * @return bool
977 */
978 public function isIPRange() {
979 return IP::isValidRange( $this->mName );
980 }
981
982 /**
983 * Is the input a valid username?
984 *
985 * Checks if the input is a valid username, we don't want an empty string,
986 * an IP address, anything that contains slashes (would mess up subpages),
987 * is longer than the maximum allowed username size or doesn't begin with
988 * a capital letter.
989 *
990 * @param string $name Name to match
991 * @return bool
992 */
993 public static function isValidUserName( $name ) {
994 global $wgMaxNameChars;
995
996 if ( $name == ''
997 || self::isIP( $name )
998 || strpos( $name, '/' ) !== false
999 || strlen( $name ) > $wgMaxNameChars
1000 || $name != MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $name )
1001 ) {
1002 return false;
1003 }
1004
1005 // Ensure that the name can't be misresolved as a different title,
1006 // such as with extra namespace keys at the start.
1007 $parsed = Title::newFromText( $name );
1008 if ( is_null( $parsed )
1009 || $parsed->getNamespace()
1010 || strcmp( $name, $parsed->getPrefixedText() ) ) {
1011 return false;
1012 }
1013
1014 // Check an additional blacklist of troublemaker characters.
1015 // Should these be merged into the title char list?
1016 $unicodeBlacklist = '/[' .
1017 '\x{0080}-\x{009f}' . # iso-8859-1 control chars
1018 '\x{00a0}' . # non-breaking space
1019 '\x{2000}-\x{200f}' . # various whitespace
1020 '\x{2028}-\x{202f}' . # breaks and control chars
1021 '\x{3000}' . # ideographic space
1022 '\x{e000}-\x{f8ff}' . # private use
1023 ']/u';
1024 if ( preg_match( $unicodeBlacklist, $name ) ) {
1025 return false;
1026 }
1027
1028 return true;
1029 }
1030
1031 /**
1032 * Usernames which fail to pass this function will be blocked
1033 * from user login and new account registrations, but may be used
1034 * internally by batch processes.
1035 *
1036 * If an account already exists in this form, login will be blocked
1037 * by a failure to pass this function.
1038 *
1039 * @param string $name Name to match
1040 * @return bool
1041 */
1042 public static function isUsableName( $name ) {
1043 global $wgReservedUsernames;
1044 // Must be a valid username, obviously ;)
1045 if ( !self::isValidUserName( $name ) ) {
1046 return false;
1047 }
1048
1049 static $reservedUsernames = false;
1050 if ( !$reservedUsernames ) {
1051 $reservedUsernames = $wgReservedUsernames;
1052 Hooks::run( 'UserGetReservedNames', [ &$reservedUsernames ] );
1053 }
1054
1055 // Certain names may be reserved for batch processes.
1056 foreach ( $reservedUsernames as $reserved ) {
1057 if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
1058 $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->plain();
1059 }
1060 if ( $reserved == $name ) {
1061 return false;
1062 }
1063 }
1064 return true;
1065 }
1066
1067 /**
1068 * Return the users who are members of the given group(s). In case of multiple groups,
1069 * users who are members of at least one of them are returned.
1070 *
1071 * @param string|array $groups A single group name or an array of group names
1072 * @param int $limit Max number of users to return. The actual limit will never exceed 5000
1073 * records; larger values are ignored.
1074 * @param int|null $after ID the user to start after
1075 * @return UserArrayFromResult
1076 */
1077 public static function findUsersByGroup( $groups, $limit = 5000, $after = null ) {
1078 if ( $groups === [] ) {
1079 return UserArrayFromResult::newFromIDs( [] );
1080 }
1081
1082 $groups = array_unique( (array)$groups );
1083 $limit = min( 5000, $limit );
1084
1085 $conds = [ 'ug_group' => $groups ];
1086 if ( $after !== null ) {
1087 $conds[] = 'ug_user > ' . (int)$after;
1088 }
1089
1090 $dbr = wfGetDB( DB_REPLICA );
1091 $ids = $dbr->selectFieldValues(
1092 'user_groups',
1093 'ug_user',
1094 $conds,
1095 __METHOD__,
1096 [
1097 'DISTINCT' => true,
1098 'ORDER BY' => 'ug_user',
1099 'LIMIT' => $limit,
1100 ]
1101 ) ?: [];
1102 return UserArray::newFromIDs( $ids );
1103 }
1104
1105 /**
1106 * Usernames which fail to pass this function will be blocked
1107 * from new account registrations, but may be used internally
1108 * either by batch processes or by user accounts which have
1109 * already been created.
1110 *
1111 * Additional blacklisting may be added here rather than in
1112 * isValidUserName() to avoid disrupting existing accounts.
1113 *
1114 * @param string $name String to match
1115 * @return bool
1116 */
1117 public static function isCreatableName( $name ) {
1118 global $wgInvalidUsernameCharacters;
1119
1120 // Ensure that the username isn't longer than 235 bytes, so that
1121 // (at least for the builtin skins) user javascript and css files
1122 // will work. (T25080)
1123 if ( strlen( $name ) > 235 ) {
1124 wfDebugLog( 'username', __METHOD__ .
1125 ": '$name' invalid due to length" );
1126 return false;
1127 }
1128
1129 // Preg yells if you try to give it an empty string
1130 if ( $wgInvalidUsernameCharacters !== '' &&
1131 preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name )
1132 ) {
1133 wfDebugLog( 'username', __METHOD__ .
1134 ": '$name' invalid due to wgInvalidUsernameCharacters" );
1135 return false;
1136 }
1137
1138 return self::isUsableName( $name );
1139 }
1140
1141 /**
1142 * Is the input a valid password for this user?
1143 *
1144 * @param string $password Desired password
1145 * @return bool
1146 */
1147 public function isValidPassword( $password ) {
1148 // simple boolean wrapper for checkPasswordValidity
1149 return $this->checkPasswordValidity( $password )->isGood();
1150 }
1151
1152 /**
1153 * Given unvalidated password input, return error message on failure.
1154 *
1155 * @param string $password Desired password
1156 * @return bool|string|array True on success, string or array of error message on failure
1157 * @deprecated since 1.33, use checkPasswordValidity
1158 */
1159 public function getPasswordValidity( $password ) {
1160 wfDeprecated( __METHOD__, '1.33' );
1161
1162 $result = $this->checkPasswordValidity( $password );
1163 if ( $result->isGood() ) {
1164 return true;
1165 }
1166
1167 $messages = [];
1168 foreach ( $result->getErrorsByType( 'error' ) as $error ) {
1169 $messages[] = $error['message'];
1170 }
1171 foreach ( $result->getErrorsByType( 'warning' ) as $warning ) {
1172 $messages[] = $warning['message'];
1173 }
1174 if ( count( $messages ) === 1 ) {
1175 return $messages[0];
1176 }
1177
1178 return $messages;
1179 }
1180
1181 /**
1182 * Check if this is a valid password for this user
1183 *
1184 * Returns a Status object with a set of messages describing
1185 * problems with the password. If the return status is fatal,
1186 * the action should be refused and the password should not be
1187 * checked at all (this is mainly meant for DoS mitigation).
1188 * If the return value is OK but not good, the password can be checked,
1189 * but the user should not be able to set their password to this.
1190 * The value of the returned Status object will be an array which
1191 * can have the following fields:
1192 * - forceChange (bool): if set to true, the user should not be
1193 * allowed to log with this password unless they change it during
1194 * the login process (see ResetPasswordSecondaryAuthenticationProvider).
1195 *
1196 * @param string $password Desired password
1197 * @return Status
1198 * @since 1.23
1199 */
1200 public function checkPasswordValidity( $password ) {
1201 global $wgPasswordPolicy;
1202
1203 $upp = new UserPasswordPolicy(
1204 $wgPasswordPolicy['policies'],
1205 $wgPasswordPolicy['checks']
1206 );
1207
1208 $status = Status::newGood( [] );
1209 $result = false; // init $result to false for the internal checks
1210
1211 if ( !Hooks::run( 'isValidPassword', [ $password, &$result, $this ] ) ) {
1212 $status->error( $result );
1213 return $status;
1214 }
1215
1216 if ( $result === false ) {
1217 $status->merge( $upp->checkUserPassword( $this, $password ), true );
1218 return $status;
1219 }
1220
1221 if ( $result === true ) {
1222 return $status;
1223 }
1224
1225 $status->error( $result );
1226 return $status; // the isValidPassword hook set a string $result and returned true
1227 }
1228
1229 /**
1230 * Given unvalidated user input, return a canonical username, or false if
1231 * the username is invalid.
1232 * @param string $name User input
1233 * @param string|bool $validate Type of validation to use:
1234 * - false No validation
1235 * - 'valid' Valid for batch processes
1236 * - 'usable' Valid for batch processes and login
1237 * - 'creatable' Valid for batch processes, login and account creation
1238 *
1239 * @throws InvalidArgumentException
1240 * @return bool|string
1241 */
1242 public static function getCanonicalName( $name, $validate = 'valid' ) {
1243 // Force usernames to capital
1244 $name = MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $name );
1245
1246 # Reject names containing '#'; these will be cleaned up
1247 # with title normalisation, but then it's too late to
1248 # check elsewhere
1249 if ( strpos( $name, '#' ) !== false ) {
1250 return false;
1251 }
1252
1253 // Clean up name according to title rules,
1254 // but only when validation is requested (T14654)
1255 $t = ( $validate !== false ) ?
1256 Title::newFromText( $name, NS_USER ) : Title::makeTitle( NS_USER, $name );
1257 // Check for invalid titles
1258 if ( is_null( $t ) || $t->getNamespace() !== NS_USER || $t->isExternal() ) {
1259 return false;
1260 }
1261
1262 // Reject various classes of invalid names
1263 $name = AuthManager::callLegacyAuthPlugin(
1264 'getCanonicalName', [ $t->getText() ], $t->getText()
1265 );
1266
1267 switch ( $validate ) {
1268 case false:
1269 break;
1270 case 'valid':
1271 if ( !self::isValidUserName( $name ) ) {
1272 $name = false;
1273 }
1274 break;
1275 case 'usable':
1276 if ( !self::isUsableName( $name ) ) {
1277 $name = false;
1278 }
1279 break;
1280 case 'creatable':
1281 if ( !self::isCreatableName( $name ) ) {
1282 $name = false;
1283 }
1284 break;
1285 default:
1286 throw new InvalidArgumentException(
1287 'Invalid parameter value for $validate in ' . __METHOD__ );
1288 }
1289 return $name;
1290 }
1291
1292 /**
1293 * Return a random password.
1294 *
1295 * @deprecated since 1.27, use PasswordFactory::generateRandomPasswordString()
1296 * @return string New random password
1297 */
1298 public static function randomPassword() {
1299 global $wgMinimalPasswordLength;
1300 return PasswordFactory::generateRandomPasswordString( $wgMinimalPasswordLength );
1301 }
1302
1303 /**
1304 * Set cached properties to default.
1305 *
1306 * @note This no longer clears uncached lazy-initialised properties;
1307 * the constructor does that instead.
1308 *
1309 * @param string|bool $name
1310 */
1311 public function loadDefaults( $name = false ) {
1312 $this->mId = 0;
1313 $this->mName = $name;
1314 $this->mActorId = null;
1315 $this->mRealName = '';
1316 $this->mEmail = '';
1317 $this->mOptionOverrides = null;
1318 $this->mOptionsLoaded = false;
1319
1320 $loggedOut = $this->mRequest && !defined( 'MW_NO_SESSION' )
1321 ? $this->mRequest->getSession()->getLoggedOutTimestamp() : 0;
1322 if ( $loggedOut !== 0 ) {
1323 $this->mTouched = wfTimestamp( TS_MW, $loggedOut );
1324 } else {
1325 $this->mTouched = '1'; # Allow any pages to be cached
1326 }
1327
1328 $this->mToken = null; // Don't run cryptographic functions till we need a token
1329 $this->mEmailAuthenticated = null;
1330 $this->mEmailToken = '';
1331 $this->mEmailTokenExpires = null;
1332 $this->mRegistration = wfTimestamp( TS_MW );
1333 $this->mGroupMemberships = [];
1334
1335 Hooks::run( 'UserLoadDefaults', [ $this, $name ] );
1336 }
1337
1338 /**
1339 * Return whether an item has been loaded.
1340 *
1341 * @param string $item Item to check. Current possibilities:
1342 * - id
1343 * - name
1344 * - realname
1345 * @param string $all 'all' to check if the whole object has been loaded
1346 * or any other string to check if only the item is available (e.g.
1347 * for optimisation)
1348 * @return bool
1349 */
1350 public function isItemLoaded( $item, $all = 'all' ) {
1351 return ( $this->mLoadedItems === true && $all === 'all' ) ||
1352 ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true );
1353 }
1354
1355 /**
1356 * Set that an item has been loaded
1357 *
1358 * @param string $item
1359 */
1360 protected function setItemLoaded( $item ) {
1361 if ( is_array( $this->mLoadedItems ) ) {
1362 $this->mLoadedItems[$item] = true;
1363 }
1364 }
1365
1366 /**
1367 * Load user data from the session.
1368 *
1369 * @return bool True if the user is logged in, false otherwise.
1370 */
1371 private function loadFromSession() {
1372 // Deprecated hook
1373 $result = null;
1374 Hooks::run( 'UserLoadFromSession', [ $this, &$result ], '1.27' );
1375 if ( $result !== null ) {
1376 return $result;
1377 }
1378
1379 // MediaWiki\Session\Session already did the necessary authentication of the user
1380 // returned here, so just use it if applicable.
1381 $session = $this->getRequest()->getSession();
1382 $user = $session->getUser();
1383 if ( $user->isLoggedIn() ) {
1384 $this->loadFromUserObject( $user );
1385 if ( $user->isBlocked() ) {
1386 // If this user is autoblocked, set a cookie to track the Block. This has to be done on
1387 // every session load, because an autoblocked editor might not edit again from the same
1388 // IP address after being blocked.
1389 $this->trackBlockWithCookie();
1390 }
1391
1392 // Other code expects these to be set in the session, so set them.
1393 $session->set( 'wsUserID', $this->getId() );
1394 $session->set( 'wsUserName', $this->getName() );
1395 $session->set( 'wsToken', $this->getToken() );
1396
1397 return true;
1398 }
1399
1400 return false;
1401 }
1402
1403 /**
1404 * Set the 'BlockID' cookie depending on block type and user authentication status.
1405 */
1406 public function trackBlockWithCookie() {
1407 $block = $this->getBlock();
1408 if ( $block && $this->getRequest()->getCookie( 'BlockID' ) === null ) {
1409 $config = RequestContext::getMain()->getConfig();
1410 $shouldSetCookie = false;
1411
1412 if ( $this->isAnon() && $config->get( 'CookieSetOnIpBlock' ) ) {
1413 // If user is logged-out, set a cookie to track the Block
1414 $shouldSetCookie = in_array( $block->getType(), [
1415 Block::TYPE_IP, Block::TYPE_RANGE
1416 ] );
1417 if ( $shouldSetCookie ) {
1418 $block->setCookie( $this->getRequest()->response() );
1419
1420 // temporary measure the use of cookies on ip blocks
1421 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
1422 $stats->increment( 'block.ipblock.setCookie.success' );
1423 }
1424 } elseif ( $this->isLoggedIn() && $config->get( 'CookieSetOnAutoblock' ) ) {
1425 $shouldSetCookie = $block->getType() === Block::TYPE_USER && $block->isAutoblocking();
1426 if ( $shouldSetCookie ) {
1427 $block->setCookie( $this->getRequest()->response() );
1428 }
1429 }
1430 }
1431 }
1432
1433 /**
1434 * Load user and user_group data from the database.
1435 * $this->mId must be set, this is how the user is identified.
1436 *
1437 * @param int $flags User::READ_* constant bitfield
1438 * @return bool True if the user exists, false if the user is anonymous
1439 */
1440 public function loadFromDatabase( $flags = self::READ_LATEST ) {
1441 // Paranoia
1442 $this->mId = intval( $this->mId );
1443
1444 if ( !$this->mId ) {
1445 // Anonymous users are not in the database
1446 $this->loadDefaults();
1447 return false;
1448 }
1449
1450 list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
1451 $db = wfGetDB( $index );
1452
1453 $userQuery = self::getQueryInfo();
1454 $s = $db->selectRow(
1455 $userQuery['tables'],
1456 $userQuery['fields'],
1457 [ 'user_id' => $this->mId ],
1458 __METHOD__,
1459 $options,
1460 $userQuery['joins']
1461 );
1462
1463 $this->queryFlagsUsed = $flags;
1464 Hooks::run( 'UserLoadFromDatabase', [ $this, &$s ] );
1465
1466 if ( $s !== false ) {
1467 // Initialise user table data
1468 $this->loadFromRow( $s );
1469 $this->mGroupMemberships = null; // deferred
1470 $this->getEditCount(); // revalidation for nulls
1471 return true;
1472 }
1473
1474 // Invalid user_id
1475 $this->mId = 0;
1476 $this->loadDefaults();
1477
1478 return false;
1479 }
1480
1481 /**
1482 * Initialize this object from a row from the user table.
1483 *
1484 * @param stdClass $row Row from the user table to load.
1485 * @param array|null $data Further user data to load into the object
1486 *
1487 * user_groups Array of arrays or stdClass result rows out of the user_groups
1488 * table. Previously you were supposed to pass an array of strings
1489 * here, but we also need expiry info nowadays, so an array of
1490 * strings is ignored.
1491 * user_properties Array with properties out of the user_properties table
1492 */
1493 protected function loadFromRow( $row, $data = null ) {
1494 global $wgActorTableSchemaMigrationStage;
1495
1496 if ( !is_object( $row ) ) {
1497 throw new InvalidArgumentException( '$row must be an object' );
1498 }
1499
1500 $all = true;
1501
1502 $this->mGroupMemberships = null; // deferred
1503
1504 // Technically we shouldn't allow this without SCHEMA_COMPAT_READ_NEW,
1505 // but it does little harm and might be needed for write callers loading a User.
1506 if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_NEW ) {
1507 if ( isset( $row->actor_id ) ) {
1508 $this->mActorId = (int)$row->actor_id;
1509 if ( $this->mActorId !== 0 ) {
1510 $this->mFrom = 'actor';
1511 }
1512 $this->setItemLoaded( 'actor' );
1513 } else {
1514 $all = false;
1515 }
1516 }
1517
1518 if ( isset( $row->user_name ) && $row->user_name !== '' ) {
1519 $this->mName = $row->user_name;
1520 $this->mFrom = 'name';
1521 $this->setItemLoaded( 'name' );
1522 } else {
1523 $all = false;
1524 }
1525
1526 if ( isset( $row->user_real_name ) ) {
1527 $this->mRealName = $row->user_real_name;
1528 $this->setItemLoaded( 'realname' );
1529 } else {
1530 $all = false;
1531 }
1532
1533 if ( isset( $row->user_id ) ) {
1534 $this->mId = intval( $row->user_id );
1535 if ( $this->mId !== 0 ) {
1536 $this->mFrom = 'id';
1537 }
1538 $this->setItemLoaded( 'id' );
1539 } else {
1540 $all = false;
1541 }
1542
1543 if ( isset( $row->user_id ) && isset( $row->user_name ) && $row->user_name !== '' ) {
1544 self::$idCacheByName[$row->user_name] = $row->user_id;
1545 }
1546
1547 if ( isset( $row->user_editcount ) ) {
1548 $this->mEditCount = $row->user_editcount;
1549 } else {
1550 $all = false;
1551 }
1552
1553 if ( isset( $row->user_touched ) ) {
1554 $this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
1555 } else {
1556 $all = false;
1557 }
1558
1559 if ( isset( $row->user_token ) ) {
1560 // The definition for the column is binary(32), so trim the NULs
1561 // that appends. The previous definition was char(32), so trim
1562 // spaces too.
1563 $this->mToken = rtrim( $row->user_token, " \0" );
1564 if ( $this->mToken === '' ) {
1565 $this->mToken = null;
1566 }
1567 } else {
1568 $all = false;
1569 }
1570
1571 if ( isset( $row->user_email ) ) {
1572 $this->mEmail = $row->user_email;
1573 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
1574 $this->mEmailToken = $row->user_email_token;
1575 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
1576 $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
1577 } else {
1578 $all = false;
1579 }
1580
1581 if ( $all ) {
1582 $this->mLoadedItems = true;
1583 }
1584
1585 if ( is_array( $data ) ) {
1586 if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) {
1587 if ( $data['user_groups'] === [] ) {
1588 $this->mGroupMemberships = [];
1589 } else {
1590 $firstGroup = reset( $data['user_groups'] );
1591 if ( is_array( $firstGroup ) || is_object( $firstGroup ) ) {
1592 $this->mGroupMemberships = [];
1593 foreach ( $data['user_groups'] as $row ) {
1594 $ugm = UserGroupMembership::newFromRow( (object)$row );
1595 $this->mGroupMemberships[$ugm->getGroup()] = $ugm;
1596 }
1597 }
1598 }
1599 }
1600 if ( isset( $data['user_properties'] ) && is_array( $data['user_properties'] ) ) {
1601 $this->loadOptions( $data['user_properties'] );
1602 }
1603 }
1604 }
1605
1606 /**
1607 * Load the data for this user object from another user object.
1608 *
1609 * @param User $user
1610 */
1611 protected function loadFromUserObject( $user ) {
1612 $user->load();
1613 foreach ( self::$mCacheVars as $var ) {
1614 $this->$var = $user->$var;
1615 }
1616 }
1617
1618 /**
1619 * Load the groups from the database if they aren't already loaded.
1620 */
1621 private function loadGroups() {
1622 if ( is_null( $this->mGroupMemberships ) ) {
1623 $db = ( $this->queryFlagsUsed & self::READ_LATEST )
1624 ? wfGetDB( DB_MASTER )
1625 : wfGetDB( DB_REPLICA );
1626 $this->mGroupMemberships = UserGroupMembership::getMembershipsForUser(
1627 $this->mId, $db );
1628 }
1629 }
1630
1631 /**
1632 * Add the user to the group if he/she meets given criteria.
1633 *
1634 * Contrary to autopromotion by \ref $wgAutopromote, the group will be
1635 * possible to remove manually via Special:UserRights. In such case it
1636 * will not be re-added automatically. The user will also not lose the
1637 * group if they no longer meet the criteria.
1638 *
1639 * @param string $event Key in $wgAutopromoteOnce (each one has groups/criteria)
1640 *
1641 * @return array Array of groups the user has been promoted to.
1642 *
1643 * @see $wgAutopromoteOnce
1644 */
1645 public function addAutopromoteOnceGroups( $event ) {
1646 global $wgAutopromoteOnceLogInRC;
1647
1648 if ( wfReadOnly() || !$this->getId() ) {
1649 return [];
1650 }
1651
1652 $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
1653 if ( $toPromote === [] ) {
1654 return [];
1655 }
1656
1657 if ( !$this->checkAndSetTouched() ) {
1658 return []; // raced out (bug T48834)
1659 }
1660
1661 $oldGroups = $this->getGroups(); // previous groups
1662 $oldUGMs = $this->getGroupMemberships();
1663 foreach ( $toPromote as $group ) {
1664 $this->addGroup( $group );
1665 }
1666 $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
1667 $newUGMs = $this->getGroupMemberships();
1668
1669 // update groups in external authentication database
1670 Hooks::run( 'UserGroupsChanged', [ $this, $toPromote, [], false, false, $oldUGMs, $newUGMs ] );
1671 AuthManager::callLegacyAuthPlugin( 'updateExternalDBGroups', [ $this, $toPromote ] );
1672
1673 $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
1674 $logEntry->setPerformer( $this );
1675 $logEntry->setTarget( $this->getUserPage() );
1676 $logEntry->setParameters( [
1677 '4::oldgroups' => $oldGroups,
1678 '5::newgroups' => $newGroups,
1679 ] );
1680 $logid = $logEntry->insert();
1681 if ( $wgAutopromoteOnceLogInRC ) {
1682 $logEntry->publish( $logid );
1683 }
1684
1685 return $toPromote;
1686 }
1687
1688 /**
1689 * Builds update conditions. Additional conditions may be added to $conditions to
1690 * protected against race conditions using a compare-and-set (CAS) mechanism
1691 * based on comparing $this->mTouched with the user_touched field.
1692 *
1693 * @param Database $db
1694 * @param array $conditions WHERE conditions for use with Database::update
1695 * @return array WHERE conditions for use with Database::update
1696 */
1697 protected function makeUpdateConditions( Database $db, array $conditions ) {
1698 if ( $this->mTouched ) {
1699 // CAS check: only update if the row wasn't changed sicne it was loaded.
1700 $conditions['user_touched'] = $db->timestamp( $this->mTouched );
1701 }
1702
1703 return $conditions;
1704 }
1705
1706 /**
1707 * Bump user_touched if it didn't change since this object was loaded
1708 *
1709 * On success, the mTouched field is updated.
1710 * The user serialization cache is always cleared.
1711 *
1712 * @return bool Whether user_touched was actually updated
1713 * @since 1.26
1714 */
1715 protected function checkAndSetTouched() {
1716 $this->load();
1717
1718 if ( !$this->mId ) {
1719 return false; // anon
1720 }
1721
1722 // Get a new user_touched that is higher than the old one
1723 $newTouched = $this->newTouchedTimestamp();
1724
1725 $dbw = wfGetDB( DB_MASTER );
1726 $dbw->update( 'user',
1727 [ 'user_touched' => $dbw->timestamp( $newTouched ) ],
1728 $this->makeUpdateConditions( $dbw, [
1729 'user_id' => $this->mId,
1730 ] ),
1731 __METHOD__
1732 );
1733 $success = ( $dbw->affectedRows() > 0 );
1734
1735 if ( $success ) {
1736 $this->mTouched = $newTouched;
1737 $this->clearSharedCache();
1738 } else {
1739 // Clears on failure too since that is desired if the cache is stale
1740 $this->clearSharedCache( 'refresh' );
1741 }
1742
1743 return $success;
1744 }
1745
1746 /**
1747 * Clear various cached data stored in this object. The cache of the user table
1748 * data (i.e. self::$mCacheVars) is not cleared unless $reloadFrom is given.
1749 *
1750 * @param bool|string $reloadFrom Reload user and user_groups table data from a
1751 * given source. May be "name", "id", "actor", "defaults", "session", or false for no reload.
1752 */
1753 public function clearInstanceCache( $reloadFrom = false ) {
1754 $this->mNewtalk = -1;
1755 $this->mDatePreference = null;
1756 $this->mBlockedby = -1; # Unset
1757 $this->mHash = false;
1758 $this->mRights = null;
1759 $this->mEffectiveGroups = null;
1760 $this->mImplicitGroups = null;
1761 $this->mGroupMemberships = null;
1762 $this->mOptions = null;
1763 $this->mOptionsLoaded = false;
1764 $this->mEditCount = null;
1765
1766 if ( $reloadFrom ) {
1767 $this->mLoadedItems = [];
1768 $this->mFrom = $reloadFrom;
1769 }
1770 }
1771
1772 /**
1773 * Combine the language default options with any site-specific options
1774 * and add the default language variants.
1775 *
1776 * @return array Array of String options
1777 */
1778 public static function getDefaultOptions() {
1779 global $wgNamespacesToBeSearchedDefault, $wgDefaultUserOptions, $wgDefaultSkin;
1780
1781 static $defOpt = null;
1782 static $defOptLang = null;
1783
1784 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1785 if ( $defOpt !== null && $defOptLang === $contLang->getCode() ) {
1786 // The content language does not change (and should not change) mid-request, but the
1787 // unit tests change it anyway, and expect this method to return values relevant to the
1788 // current content language.
1789 return $defOpt;
1790 }
1791
1792 $defOpt = $wgDefaultUserOptions;
1793 // Default language setting
1794 $defOptLang = $contLang->getCode();
1795 $defOpt['language'] = $defOptLang;
1796 foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
1797 if ( $langCode === $contLang->getCode() ) {
1798 $defOpt['variant'] = $langCode;
1799 } else {
1800 $defOpt["variant-$langCode"] = $langCode;
1801 }
1802 }
1803
1804 // NOTE: don't use SearchEngineConfig::getSearchableNamespaces here,
1805 // since extensions may change the set of searchable namespaces depending
1806 // on user groups/permissions.
1807 foreach ( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
1808 $defOpt['searchNs' . $nsnum] = (bool)$val;
1809 }
1810 $defOpt['skin'] = Skin::normalizeKey( $wgDefaultSkin );
1811
1812 Hooks::run( 'UserGetDefaultOptions', [ &$defOpt ] );
1813
1814 return $defOpt;
1815 }
1816
1817 /**
1818 * Get a given default option value.
1819 *
1820 * @param string $opt Name of option to retrieve
1821 * @return string Default option value
1822 */
1823 public static function getDefaultOption( $opt ) {
1824 $defOpts = self::getDefaultOptions();
1825 return $defOpts[$opt] ?? null;
1826 }
1827
1828 /**
1829 * Get blocking information
1830 * @param bool $bFromReplica Whether to check the replica DB first.
1831 * To improve performance, non-critical checks are done against replica DBs.
1832 * Check when actually saving should be done against master.
1833 */
1834 private function getBlockedStatus( $bFromReplica = true ) {
1835 global $wgProxyWhitelist, $wgUser, $wgApplyIpBlocksToXff, $wgSoftBlockRanges;
1836
1837 if ( $this->mBlockedby != -1 ) {
1838 return;
1839 }
1840
1841 wfDebug( __METHOD__ . ": checking...\n" );
1842
1843 // Initialize data...
1844 // Otherwise something ends up stomping on $this->mBlockedby when
1845 // things get lazy-loaded later, causing false positive block hits
1846 // due to -1 !== 0. Probably session-related... Nothing should be
1847 // overwriting mBlockedby, surely?
1848 $this->load();
1849
1850 # We only need to worry about passing the IP address to the Block generator if the
1851 # user is not immune to autoblocks/hardblocks, and they are the current user so we
1852 # know which IP address they're actually coming from
1853 $ip = null;
1854 if ( !$this->isAllowed( 'ipblock-exempt' ) ) {
1855 // $wgUser->getName() only works after the end of Setup.php. Until
1856 // then, assume it's a logged-out user.
1857 $globalUserName = $wgUser->isSafeToLoad()
1858 ? $wgUser->getName()
1859 : IP::sanitizeIP( $wgUser->getRequest()->getIP() );
1860 if ( $this->getName() === $globalUserName ) {
1861 $ip = $this->getRequest()->getIP();
1862 }
1863 }
1864
1865 // User/IP blocking
1866 $block = Block::newFromTarget( $this, $ip, !$bFromReplica );
1867
1868 // Cookie blocking
1869 if ( !$block instanceof Block ) {
1870 $block = $this->getBlockFromCookieValue( $this->getRequest()->getCookie( 'BlockID' ) );
1871 }
1872
1873 // Proxy blocking
1874 if ( !$block instanceof Block && $ip !== null && !in_array( $ip, $wgProxyWhitelist ) ) {
1875 // Local list
1876 if ( self::isLocallyBlockedProxy( $ip ) ) {
1877 $block = new Block( [
1878 'byText' => wfMessage( 'proxyblocker' )->text(),
1879 'reason' => wfMessage( 'proxyblockreason' )->plain(),
1880 'address' => $ip,
1881 'systemBlock' => 'proxy',
1882 ] );
1883 } elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) {
1884 $block = new Block( [
1885 'byText' => wfMessage( 'sorbs' )->text(),
1886 'reason' => wfMessage( 'sorbsreason' )->plain(),
1887 'address' => $ip,
1888 'systemBlock' => 'dnsbl',
1889 ] );
1890 }
1891 }
1892
1893 // (T25343) Apply IP blocks to the contents of XFF headers, if enabled
1894 if ( !$block instanceof Block
1895 && $wgApplyIpBlocksToXff
1896 && $ip !== null
1897 && !in_array( $ip, $wgProxyWhitelist )
1898 ) {
1899 $xff = $this->getRequest()->getHeader( 'X-Forwarded-For' );
1900 $xff = array_map( 'trim', explode( ',', $xff ) );
1901 $xff = array_diff( $xff, [ $ip ] );
1902 $xffblocks = Block::getBlocksForIPList( $xff, $this->isAnon(), !$bFromReplica );
1903 $block = Block::chooseBlock( $xffblocks, $xff );
1904 if ( $block instanceof Block ) {
1905 # Mangle the reason to alert the user that the block
1906 # originated from matching the X-Forwarded-For header.
1907 $block->mReason = wfMessage( 'xffblockreason', $block->mReason )->plain();
1908 }
1909 }
1910
1911 if ( !$block instanceof Block
1912 && $ip !== null
1913 && $this->isAnon()
1914 && IP::isInRanges( $ip, $wgSoftBlockRanges )
1915 ) {
1916 $block = new Block( [
1917 'address' => $ip,
1918 'byText' => 'MediaWiki default',
1919 'reason' => wfMessage( 'softblockrangesreason', $ip )->plain(),
1920 'anonOnly' => true,
1921 'systemBlock' => 'wgSoftBlockRanges',
1922 ] );
1923 }
1924
1925 if ( $block instanceof Block ) {
1926 wfDebug( __METHOD__ . ": Found block.\n" );
1927 $this->mBlock = $block;
1928 $this->mBlockedby = $block->getByName();
1929 $this->mBlockreason = $block->mReason;
1930 $this->mHideName = $block->mHideName;
1931 $this->mAllowUsertalk = !$block->prevents( 'editownusertalk' );
1932 } else {
1933 $this->mBlock = null;
1934 $this->mBlockedby = '';
1935 $this->mBlockreason = '';
1936 $this->mHideName = 0;
1937 $this->mAllowUsertalk = false;
1938 }
1939
1940 // Avoid PHP 7.1 warning of passing $this by reference
1941 $user = $this;
1942 // Extensions
1943 Hooks::run( 'GetBlockedStatus', [ &$user ] );
1944 }
1945
1946 /**
1947 * Try to load a Block from an ID given in a cookie value.
1948 * @param string|null $blockCookieVal The cookie value to check.
1949 * @return Block|bool The Block object, or false if none could be loaded.
1950 */
1951 protected function getBlockFromCookieValue( $blockCookieVal ) {
1952 // Make sure there's something to check. The cookie value must start with a number.
1953 if ( strlen( $blockCookieVal ) < 1 || !is_numeric( substr( $blockCookieVal, 0, 1 ) ) ) {
1954 return false;
1955 }
1956 // Load the Block from the ID in the cookie.
1957 $blockCookieId = Block::getIdFromCookieValue( $blockCookieVal );
1958 if ( $blockCookieId !== null ) {
1959 // An ID was found in the cookie.
1960 $tmpBlock = Block::newFromID( $blockCookieId );
1961 if ( $tmpBlock instanceof Block ) {
1962 $config = RequestContext::getMain()->getConfig();
1963
1964 switch ( $tmpBlock->getType() ) {
1965 case Block::TYPE_USER:
1966 $blockIsValid = !$tmpBlock->isExpired() && $tmpBlock->isAutoblocking();
1967 $useBlockCookie = ( $config->get( 'CookieSetOnAutoblock' ) === true );
1968 break;
1969 case Block::TYPE_IP:
1970 case Block::TYPE_RANGE:
1971 // If block is type IP or IP range, load only if user is not logged in (T152462)
1972 $blockIsValid = !$tmpBlock->isExpired() && !$this->isLoggedIn();
1973 $useBlockCookie = ( $config->get( 'CookieSetOnIpBlock' ) === true );
1974 break;
1975 default:
1976 $blockIsValid = false;
1977 $useBlockCookie = false;
1978 }
1979
1980 if ( $blockIsValid && $useBlockCookie ) {
1981 // Use the block.
1982 return $tmpBlock;
1983 }
1984
1985 // If the block is not valid, remove the cookie.
1986 Block::clearCookie( $this->getRequest()->response() );
1987 } else {
1988 // If the block doesn't exist, remove the cookie.
1989 Block::clearCookie( $this->getRequest()->response() );
1990 }
1991 }
1992 return false;
1993 }
1994
1995 /**
1996 * Whether the given IP is in a DNS blacklist.
1997 *
1998 * @param string $ip IP to check
1999 * @param bool $checkWhitelist Whether to check the whitelist first
2000 * @return bool True if blacklisted.
2001 */
2002 public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
2003 global $wgEnableDnsBlacklist, $wgDnsBlacklistUrls, $wgProxyWhitelist;
2004
2005 if ( !$wgEnableDnsBlacklist ||
2006 ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) )
2007 ) {
2008 return false;
2009 }
2010
2011 return $this->inDnsBlacklist( $ip, $wgDnsBlacklistUrls );
2012 }
2013
2014 /**
2015 * Whether the given IP is in a given DNS blacklist.
2016 *
2017 * @param string $ip IP to check
2018 * @param string|array $bases Array of Strings: URL of the DNS blacklist
2019 * @return bool True if blacklisted.
2020 */
2021 public function inDnsBlacklist( $ip, $bases ) {
2022 $found = false;
2023 // @todo FIXME: IPv6 ??? (https://bugs.php.net/bug.php?id=33170)
2024 if ( IP::isIPv4( $ip ) ) {
2025 // Reverse IP, T23255
2026 $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
2027
2028 foreach ( (array)$bases as $base ) {
2029 // Make hostname
2030 // If we have an access key, use that too (ProjectHoneypot, etc.)
2031 $basename = $base;
2032 if ( is_array( $base ) ) {
2033 if ( count( $base ) >= 2 ) {
2034 // Access key is 1, base URL is 0
2035 $host = "{$base[1]}.$ipReversed.{$base[0]}";
2036 } else {
2037 $host = "$ipReversed.{$base[0]}";
2038 }
2039 $basename = $base[0];
2040 } else {
2041 $host = "$ipReversed.$base";
2042 }
2043
2044 // Send query
2045 $ipList = gethostbynamel( $host );
2046
2047 if ( $ipList ) {
2048 wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $basename!" );
2049 $found = true;
2050 break;
2051 }
2052
2053 wfDebugLog( 'dnsblacklist', "Requested $host, not found in $basename." );
2054 }
2055 }
2056
2057 return $found;
2058 }
2059
2060 /**
2061 * Check if an IP address is in the local proxy list
2062 *
2063 * @param string $ip
2064 *
2065 * @return bool
2066 */
2067 public static function isLocallyBlockedProxy( $ip ) {
2068 global $wgProxyList;
2069
2070 if ( !$wgProxyList ) {
2071 return false;
2072 }
2073
2074 if ( !is_array( $wgProxyList ) ) {
2075 // Load values from the specified file
2076 $wgProxyList = array_map( 'trim', file( $wgProxyList ) );
2077 }
2078
2079 $resultProxyList = [];
2080 $deprecatedIPEntries = [];
2081
2082 // backward compatibility: move all ip addresses in keys to values
2083 foreach ( $wgProxyList as $key => $value ) {
2084 $keyIsIP = IP::isIPAddress( $key );
2085 $valueIsIP = IP::isIPAddress( $value );
2086 if ( $keyIsIP && !$valueIsIP ) {
2087 $deprecatedIPEntries[] = $key;
2088 $resultProxyList[] = $key;
2089 } elseif ( $keyIsIP && $valueIsIP ) {
2090 $deprecatedIPEntries[] = $key;
2091 $resultProxyList[] = $key;
2092 $resultProxyList[] = $value;
2093 } else {
2094 $resultProxyList[] = $value;
2095 }
2096 }
2097
2098 if ( $deprecatedIPEntries ) {
2099 wfDeprecated(
2100 'IP addresses in the keys of $wgProxyList (found the following IP addresses in keys: ' .
2101 implode( ', ', $deprecatedIPEntries ) . ', please move them to values)', '1.30' );
2102 }
2103
2104 $proxyListIPSet = new IPSet( $resultProxyList );
2105 return $proxyListIPSet->match( $ip );
2106 }
2107
2108 /**
2109 * Is this user subject to rate limiting?
2110 *
2111 * @return bool True if rate limited
2112 */
2113 public function isPingLimitable() {
2114 global $wgRateLimitsExcludedIPs;
2115 if ( IP::isInRanges( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) {
2116 // No other good way currently to disable rate limits
2117 // for specific IPs. :P
2118 // But this is a crappy hack and should die.
2119 return false;
2120 }
2121 return !$this->isAllowed( 'noratelimit' );
2122 }
2123
2124 /**
2125 * Primitive rate limits: enforce maximum actions per time period
2126 * to put a brake on flooding.
2127 *
2128 * The method generates both a generic profiling point and a per action one
2129 * (suffix being "-$action".
2130 *
2131 * @note When using a shared cache like memcached, IP-address
2132 * last-hit counters will be shared across wikis.
2133 *
2134 * @param string $action Action to enforce; 'edit' if unspecified
2135 * @param int $incrBy Positive amount to increment counter by [defaults to 1]
2136 * @return bool True if a rate limiter was tripped
2137 */
2138 public function pingLimiter( $action = 'edit', $incrBy = 1 ) {
2139 // Avoid PHP 7.1 warning of passing $this by reference
2140 $user = $this;
2141 // Call the 'PingLimiter' hook
2142 $result = false;
2143 if ( !Hooks::run( 'PingLimiter', [ &$user, $action, &$result, $incrBy ] ) ) {
2144 return $result;
2145 }
2146
2147 global $wgRateLimits;
2148 if ( !isset( $wgRateLimits[$action] ) ) {
2149 return false;
2150 }
2151
2152 $limits = array_merge(
2153 [ '&can-bypass' => true ],
2154 $wgRateLimits[$action]
2155 );
2156
2157 // Some groups shouldn't trigger the ping limiter, ever
2158 if ( $limits['&can-bypass'] && !$this->isPingLimitable() ) {
2159 return false;
2160 }
2161
2162 $keys = [];
2163 $id = $this->getId();
2164 $userLimit = false;
2165 $isNewbie = $this->isNewbie();
2166 $cache = ObjectCache::getLocalClusterInstance();
2167
2168 if ( $id == 0 ) {
2169 // limits for anons
2170 if ( isset( $limits['anon'] ) ) {
2171 $keys[$cache->makeKey( 'limiter', $action, 'anon' )] = $limits['anon'];
2172 }
2173 } elseif ( isset( $limits['user'] ) ) {
2174 // limits for logged-in users
2175 $userLimit = $limits['user'];
2176 }
2177
2178 // limits for anons and for newbie logged-in users
2179 if ( $isNewbie ) {
2180 // ip-based limits
2181 if ( isset( $limits['ip'] ) ) {
2182 $ip = $this->getRequest()->getIP();
2183 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
2184 }
2185 // subnet-based limits
2186 if ( isset( $limits['subnet'] ) ) {
2187 $ip = $this->getRequest()->getIP();
2188 $subnet = IP::getSubnet( $ip );
2189 if ( $subnet !== false ) {
2190 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
2191 }
2192 }
2193 }
2194
2195 // Check for group-specific permissions
2196 // If more than one group applies, use the group with the highest limit ratio (max/period)
2197 foreach ( $this->getGroups() as $group ) {
2198 if ( isset( $limits[$group] ) ) {
2199 if ( $userLimit === false
2200 || $limits[$group][0] / $limits[$group][1] > $userLimit[0] / $userLimit[1]
2201 ) {
2202 $userLimit = $limits[$group];
2203 }
2204 }
2205 }
2206
2207 // limits for newbie logged-in users (override all the normal user limits)
2208 if ( $id !== 0 && $isNewbie && isset( $limits['newbie'] ) ) {
2209 $userLimit = $limits['newbie'];
2210 }
2211
2212 // Set the user limit key
2213 if ( $userLimit !== false ) {
2214 list( $max, $period ) = $userLimit;
2215 wfDebug( __METHOD__ . ": effective user limit: $max in {$period}s\n" );
2216 $keys[$cache->makeKey( 'limiter', $action, 'user', $id )] = $userLimit;
2217 }
2218
2219 // ip-based limits for all ping-limitable users
2220 if ( isset( $limits['ip-all'] ) ) {
2221 $ip = $this->getRequest()->getIP();
2222 // ignore if user limit is more permissive
2223 if ( $isNewbie || $userLimit === false
2224 || $limits['ip-all'][0] / $limits['ip-all'][1] > $userLimit[0] / $userLimit[1] ) {
2225 $keys["mediawiki:limiter:$action:ip-all:$ip"] = $limits['ip-all'];
2226 }
2227 }
2228
2229 // subnet-based limits for all ping-limitable users
2230 if ( isset( $limits['subnet-all'] ) ) {
2231 $ip = $this->getRequest()->getIP();
2232 $subnet = IP::getSubnet( $ip );
2233 if ( $subnet !== false ) {
2234 // ignore if user limit is more permissive
2235 if ( $isNewbie || $userLimit === false
2236 || $limits['ip-all'][0] / $limits['ip-all'][1]
2237 > $userLimit[0] / $userLimit[1] ) {
2238 $keys["mediawiki:limiter:$action:subnet-all:$subnet"] = $limits['subnet-all'];
2239 }
2240 }
2241 }
2242
2243 $triggered = false;
2244 foreach ( $keys as $key => $limit ) {
2245 list( $max, $period ) = $limit;
2246 $summary = "(limit $max in {$period}s)";
2247 $count = $cache->get( $key );
2248 // Already pinged?
2249 if ( $count ) {
2250 if ( $count >= $max ) {
2251 wfDebugLog( 'ratelimit', "User '{$this->getName()}' " .
2252 "(IP {$this->getRequest()->getIP()}) tripped $key at $count $summary" );
2253 $triggered = true;
2254 } else {
2255 wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" );
2256 }
2257 } else {
2258 wfDebug( __METHOD__ . ": adding record for $key $summary\n" );
2259 if ( $incrBy > 0 ) {
2260 $cache->add( $key, 0, intval( $period ) ); // first ping
2261 }
2262 }
2263 if ( $incrBy > 0 ) {
2264 $cache->incr( $key, $incrBy );
2265 }
2266 }
2267
2268 return $triggered;
2269 }
2270
2271 /**
2272 * Check if user is blocked
2273 *
2274 * @param bool $bFromReplica Whether to check the replica DB instead of
2275 * the master. Hacked from false due to horrible probs on site.
2276 * @return bool True if blocked, false otherwise
2277 */
2278 public function isBlocked( $bFromReplica = true ) {
2279 return $this->getBlock( $bFromReplica ) instanceof Block && $this->getBlock()->prevents( 'edit' );
2280 }
2281
2282 /**
2283 * Get the block affecting the user, or null if the user is not blocked
2284 *
2285 * @param bool $bFromReplica Whether to check the replica DB instead of the master
2286 * @return Block|null
2287 */
2288 public function getBlock( $bFromReplica = true ) {
2289 $this->getBlockedStatus( $bFromReplica );
2290 return $this->mBlock instanceof Block ? $this->mBlock : null;
2291 }
2292
2293 /**
2294 * Check if user is blocked from editing a particular article
2295 *
2296 * @param Title $title Title to check
2297 * @param bool $fromReplica Whether to check the replica DB instead of the master
2298 * @return bool
2299 */
2300 public function isBlockedFrom( $title, $fromReplica = false ) {
2301 $blocked = $this->isHidden();
2302
2303 if ( !$blocked ) {
2304 $block = $this->getBlock( $fromReplica );
2305 if ( $block ) {
2306 // Special handling for a user's own talk page. The block is not aware
2307 // of the user, so this must be done here.
2308 if ( $title->equals( $this->getTalkPage() ) ) {
2309 if ( $block->isSitewide() ) {
2310 // If the block is sitewide, whatever is set is what is honored.
2311 // This must be checked here, because Block::appliesToPage will
2312 // return true for a sitewide block.
2313 $blocked = $block->prevents( 'editownusertalk' );
2314 } else {
2315 // The page restrictions always take precedence over the namespace
2316 // restrictions. If the user is explicity blocked from their own
2317 // talk page, nothing can change that.
2318 $blocked = $block->appliesToPage( $title->getArticleID() );
2319
2320 // If the block applies to the user talk namespace, then whatever is
2321 // set is what is honored.
2322 if ( !$blocked && $block->appliesToNamespace( NS_USER_TALK ) ) {
2323 $blocked = $block->prevents( 'editownusertalk' );
2324 }
2325
2326 // If another type of restriction is added, it should be checked
2327 // here.
2328 }
2329 } else {
2330 $blocked = $block->appliesToTitle( $title );
2331 }
2332 }
2333 }
2334
2335 // only for the purpose of the hook. We really don't need this here.
2336 $allowUsertalk = $this->mAllowUsertalk;
2337
2338 Hooks::run( 'UserIsBlockedFrom', [ $this, $title, &$blocked, &$allowUsertalk ] );
2339
2340 return $blocked;
2341 }
2342
2343 /**
2344 * If user is blocked, return the name of the user who placed the block
2345 * @return string Name of blocker
2346 */
2347 public function blockedBy() {
2348 $this->getBlockedStatus();
2349 return $this->mBlockedby;
2350 }
2351
2352 /**
2353 * If user is blocked, return the specified reason for the block
2354 * @return string Blocking reason
2355 */
2356 public function blockedFor() {
2357 $this->getBlockedStatus();
2358 return $this->mBlockreason;
2359 }
2360
2361 /**
2362 * If user is blocked, return the ID for the block
2363 * @return int Block ID
2364 */
2365 public function getBlockId() {
2366 $this->getBlockedStatus();
2367 return ( $this->mBlock ? $this->mBlock->getId() : false );
2368 }
2369
2370 /**
2371 * Check if user is blocked on all wikis.
2372 * Do not use for actual edit permission checks!
2373 * This is intended for quick UI checks.
2374 *
2375 * @param string $ip IP address, uses current client if none given
2376 * @return bool True if blocked, false otherwise
2377 */
2378 public function isBlockedGlobally( $ip = '' ) {
2379 return $this->getGlobalBlock( $ip ) instanceof Block;
2380 }
2381
2382 /**
2383 * Check if user is blocked on all wikis.
2384 * Do not use for actual edit permission checks!
2385 * This is intended for quick UI checks.
2386 *
2387 * @param string $ip IP address, uses current client if none given
2388 * @return Block|null Block object if blocked, null otherwise
2389 * @throws FatalError
2390 * @throws MWException
2391 */
2392 public function getGlobalBlock( $ip = '' ) {
2393 if ( $this->mGlobalBlock !== null ) {
2394 return $this->mGlobalBlock ?: null;
2395 }
2396 // User is already an IP?
2397 if ( IP::isIPAddress( $this->getName() ) ) {
2398 $ip = $this->getName();
2399 } elseif ( !$ip ) {
2400 $ip = $this->getRequest()->getIP();
2401 }
2402 // Avoid PHP 7.1 warning of passing $this by reference
2403 $user = $this;
2404 $blocked = false;
2405 $block = null;
2406 Hooks::run( 'UserIsBlockedGlobally', [ &$user, $ip, &$blocked, &$block ] );
2407
2408 if ( $blocked && $block === null ) {
2409 // back-compat: UserIsBlockedGlobally didn't have $block param first
2410 $block = new Block( [
2411 'address' => $ip,
2412 'systemBlock' => 'global-block'
2413 ] );
2414 }
2415
2416 $this->mGlobalBlock = $blocked ? $block : false;
2417 return $this->mGlobalBlock ?: null;
2418 }
2419
2420 /**
2421 * Check if user account is locked
2422 *
2423 * @return bool True if locked, false otherwise
2424 */
2425 public function isLocked() {
2426 if ( $this->mLocked !== null ) {
2427 return $this->mLocked;
2428 }
2429 // Avoid PHP 7.1 warning of passing $this by reference
2430 $user = $this;
2431 $authUser = AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$user ], null );
2432 $this->mLocked = $authUser && $authUser->isLocked();
2433 Hooks::run( 'UserIsLocked', [ $this, &$this->mLocked ] );
2434 return $this->mLocked;
2435 }
2436
2437 /**
2438 * Check if user account is hidden
2439 *
2440 * @return bool True if hidden, false otherwise
2441 */
2442 public function isHidden() {
2443 if ( $this->mHideName !== null ) {
2444 return (bool)$this->mHideName;
2445 }
2446 $this->getBlockedStatus();
2447 if ( !$this->mHideName ) {
2448 // Avoid PHP 7.1 warning of passing $this by reference
2449 $user = $this;
2450 $authUser = AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$user ], null );
2451 $this->mHideName = $authUser && $authUser->isHidden();
2452 Hooks::run( 'UserIsHidden', [ $this, &$this->mHideName ] );
2453 }
2454 return (bool)$this->mHideName;
2455 }
2456
2457 /**
2458 * Get the user's ID.
2459 * @return int The user's ID; 0 if the user is anonymous or nonexistent
2460 */
2461 public function getId() {
2462 if ( $this->mId === null && $this->mName !== null && self::isIP( $this->mName ) ) {
2463 // Special case, we know the user is anonymous
2464 return 0;
2465 }
2466
2467 if ( !$this->isItemLoaded( 'id' ) ) {
2468 // Don't load if this was initialized from an ID
2469 $this->load();
2470 }
2471
2472 return (int)$this->mId;
2473 }
2474
2475 /**
2476 * Set the user and reload all fields according to a given ID
2477 * @param int $v User ID to reload
2478 */
2479 public function setId( $v ) {
2480 $this->mId = $v;
2481 $this->clearInstanceCache( 'id' );
2482 }
2483
2484 /**
2485 * Get the user name, or the IP of an anonymous user
2486 * @return string User's name or IP address
2487 */
2488 public function getName() {
2489 if ( $this->isItemLoaded( 'name', 'only' ) ) {
2490 // Special case optimisation
2491 return $this->mName;
2492 }
2493
2494 $this->load();
2495 if ( $this->mName === false ) {
2496 // Clean up IPs
2497 $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() );
2498 }
2499
2500 return $this->mName;
2501 }
2502
2503 /**
2504 * Set the user name.
2505 *
2506 * This does not reload fields from the database according to the given
2507 * name. Rather, it is used to create a temporary "nonexistent user" for
2508 * later addition to the database. It can also be used to set the IP
2509 * address for an anonymous user to something other than the current
2510 * remote IP.
2511 *
2512 * @note User::newFromName() has roughly the same function, when the named user
2513 * does not exist.
2514 * @param string $str New user name to set
2515 */
2516 public function setName( $str ) {
2517 $this->load();
2518 $this->mName = $str;
2519 }
2520
2521 /**
2522 * Get the user's actor ID.
2523 * @since 1.31
2524 * @param IDatabase|null $dbw Assign a new actor ID, using this DB handle, if none exists
2525 * @return int The actor's ID, or 0 if no actor ID exists and $dbw was null
2526 */
2527 public function getActorId( IDatabase $dbw = null ) {
2528 global $wgActorTableSchemaMigrationStage;
2529
2530 // Technically we should always return 0 without SCHEMA_COMPAT_READ_NEW,
2531 // but it does little harm and might be needed for write callers loading a User.
2532 if ( !( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) ) {
2533 return 0;
2534 }
2535
2536 if ( !$this->isItemLoaded( 'actor' ) ) {
2537 $this->load();
2538 }
2539
2540 // Currently $this->mActorId might be null if $this was loaded from a
2541 // cache entry that was written when $wgActorTableSchemaMigrationStage
2542 // was SCHEMA_COMPAT_OLD. Once that is no longer a possibility (i.e. when
2543 // User::VERSION is incremented after $wgActorTableSchemaMigrationStage
2544 // has been removed), that condition may be removed.
2545 if ( $this->mActorId === null || !$this->mActorId && $dbw ) {
2546 $q = [
2547 'actor_user' => $this->getId() ?: null,
2548 'actor_name' => (string)$this->getName(),
2549 ];
2550 if ( $dbw ) {
2551 if ( $q['actor_user'] === null && self::isUsableName( $q['actor_name'] ) ) {
2552 throw new CannotCreateActorException(
2553 'Cannot create an actor for a usable name that is not an existing user'
2554 );
2555 }
2556 if ( $q['actor_name'] === '' ) {
2557 throw new CannotCreateActorException( 'Cannot create an actor for a user with no name' );
2558 }
2559 $dbw->insert( 'actor', $q, __METHOD__, [ 'IGNORE' ] );
2560 if ( $dbw->affectedRows() ) {
2561 $this->mActorId = (int)$dbw->insertId();
2562 } else {
2563 // Outdated cache?
2564 // Use LOCK IN SHARE MODE to bypass any MySQL REPEATABLE-READ snapshot.
2565 $this->mActorId = (int)$dbw->selectField(
2566 'actor',
2567 'actor_id',
2568 $q,
2569 __METHOD__,
2570 [ 'LOCK IN SHARE MODE' ]
2571 );
2572 if ( !$this->mActorId ) {
2573 throw new CannotCreateActorException(
2574 "Cannot create actor ID for user_id={$this->getId()} user_name={$this->getName()}"
2575 );
2576 }
2577 }
2578 $this->invalidateCache();
2579 } else {
2580 list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $this->queryFlagsUsed );
2581 $db = wfGetDB( $index );
2582 $this->mActorId = (int)$db->selectField( 'actor', 'actor_id', $q, __METHOD__, $options );
2583 }
2584 $this->setItemLoaded( 'actor' );
2585 }
2586
2587 return (int)$this->mActorId;
2588 }
2589
2590 /**
2591 * Get the user's name escaped by underscores.
2592 * @return string Username escaped by underscores.
2593 */
2594 public function getTitleKey() {
2595 return str_replace( ' ', '_', $this->getName() );
2596 }
2597
2598 /**
2599 * Check if the user has new messages.
2600 * @return bool True if the user has new messages
2601 */
2602 public function getNewtalk() {
2603 $this->load();
2604
2605 // Load the newtalk status if it is unloaded (mNewtalk=-1)
2606 if ( $this->mNewtalk === -1 ) {
2607 $this->mNewtalk = false; # reset talk page status
2608
2609 // Check memcached separately for anons, who have no
2610 // entire User object stored in there.
2611 if ( !$this->mId ) {
2612 global $wgDisableAnonTalk;
2613 if ( $wgDisableAnonTalk ) {
2614 // Anon newtalk disabled by configuration.
2615 $this->mNewtalk = false;
2616 } else {
2617 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
2618 }
2619 } else {
2620 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
2621 }
2622 }
2623
2624 return (bool)$this->mNewtalk;
2625 }
2626
2627 /**
2628 * Return the data needed to construct links for new talk page message
2629 * alerts. If there are new messages, this will return an associative array
2630 * with the following data:
2631 * wiki: The database name of the wiki
2632 * link: Root-relative link to the user's talk page
2633 * rev: The last talk page revision that the user has seen or null. This
2634 * is useful for building diff links.
2635 * If there are no new messages, it returns an empty array.
2636 * @note This function was designed to accomodate multiple talk pages, but
2637 * currently only returns a single link and revision.
2638 * @return array
2639 */
2640 public function getNewMessageLinks() {
2641 // Avoid PHP 7.1 warning of passing $this by reference
2642 $user = $this;
2643 $talks = [];
2644 if ( !Hooks::run( 'UserRetrieveNewTalks', [ &$user, &$talks ] ) ) {
2645 return $talks;
2646 }
2647
2648 if ( !$this->getNewtalk() ) {
2649 return [];
2650 }
2651 $utp = $this->getTalkPage();
2652 $dbr = wfGetDB( DB_REPLICA );
2653 // Get the "last viewed rev" timestamp from the oldest message notification
2654 $timestamp = $dbr->selectField( 'user_newtalk',
2655 'MIN(user_last_timestamp)',
2656 $this->isAnon() ? [ 'user_ip' => $this->getName() ] : [ 'user_id' => $this->getId() ],
2657 __METHOD__ );
2658 $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null;
2659 return [
2660 [
2661 'wiki' => WikiMap::getWikiIdFromDomain( WikiMap::getCurrentWikiDomain() ),
2662 'link' => $utp->getLocalURL(),
2663 'rev' => $rev
2664 ]
2665 ];
2666 }
2667
2668 /**
2669 * Get the revision ID for the last talk page revision viewed by the talk
2670 * page owner.
2671 * @return int|null Revision ID or null
2672 */
2673 public function getNewMessageRevisionId() {
2674 $newMessageRevisionId = null;
2675 $newMessageLinks = $this->getNewMessageLinks();
2676
2677 // Note: getNewMessageLinks() never returns more than a single link
2678 // and it is always for the same wiki, but we double-check here in
2679 // case that changes some time in the future.
2680 if ( $newMessageLinks && count( $newMessageLinks ) === 1
2681 && WikiMap::isCurrentWikiId( $newMessageLinks[0]['wiki'] )
2682 && $newMessageLinks[0]['rev']
2683 ) {
2684 /** @var Revision $newMessageRevision */
2685 $newMessageRevision = $newMessageLinks[0]['rev'];
2686 $newMessageRevisionId = $newMessageRevision->getId();
2687 }
2688
2689 return $newMessageRevisionId;
2690 }
2691
2692 /**
2693 * Internal uncached check for new messages
2694 *
2695 * @see getNewtalk()
2696 * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
2697 * @param string|int $id User's IP address for anonymous users, User ID otherwise
2698 * @return bool True if the user has new messages
2699 */
2700 protected function checkNewtalk( $field, $id ) {
2701 $dbr = wfGetDB( DB_REPLICA );
2702
2703 $ok = $dbr->selectField( 'user_newtalk', $field, [ $field => $id ], __METHOD__ );
2704
2705 return $ok !== false;
2706 }
2707
2708 /**
2709 * Add or update the new messages flag
2710 * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
2711 * @param string|int $id User's IP address for anonymous users, User ID otherwise
2712 * @param Revision|null $curRev New, as yet unseen revision of the user talk page. Ignored if null.
2713 * @return bool True if successful, false otherwise
2714 */
2715 protected function updateNewtalk( $field, $id, $curRev = null ) {
2716 // Get timestamp of the talk page revision prior to the current one
2717 $prevRev = $curRev ? $curRev->getPrevious() : false;
2718 $ts = $prevRev ? $prevRev->getTimestamp() : null;
2719 // Mark the user as having new messages since this revision
2720 $dbw = wfGetDB( DB_MASTER );
2721 $dbw->insert( 'user_newtalk',
2722 [ $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ],
2723 __METHOD__,
2724 'IGNORE' );
2725 if ( $dbw->affectedRows() ) {
2726 wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
2727 return true;
2728 }
2729
2730 wfDebug( __METHOD__ . " already set ($field, $id)\n" );
2731 return false;
2732 }
2733
2734 /**
2735 * Clear the new messages flag for the given user
2736 * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
2737 * @param string|int $id User's IP address for anonymous users, User ID otherwise
2738 * @return bool True if successful, false otherwise
2739 */
2740 protected function deleteNewtalk( $field, $id ) {
2741 $dbw = wfGetDB( DB_MASTER );
2742 $dbw->delete( 'user_newtalk',
2743 [ $field => $id ],
2744 __METHOD__ );
2745 if ( $dbw->affectedRows() ) {
2746 wfDebug( __METHOD__ . ": killed on ($field, $id)\n" );
2747 return true;
2748 }
2749
2750 wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
2751 return false;
2752 }
2753
2754 /**
2755 * Update the 'You have new messages!' status.
2756 * @param bool $val Whether the user has new messages
2757 * @param Revision|null $curRev New, as yet unseen revision of the user talk
2758 * page. Ignored if null or !$val.
2759 */
2760 public function setNewtalk( $val, $curRev = null ) {
2761 if ( wfReadOnly() ) {
2762 return;
2763 }
2764
2765 $this->load();
2766 $this->mNewtalk = $val;
2767
2768 if ( $this->isAnon() ) {
2769 $field = 'user_ip';
2770 $id = $this->getName();
2771 } else {
2772 $field = 'user_id';
2773 $id = $this->getId();
2774 }
2775
2776 if ( $val ) {
2777 $changed = $this->updateNewtalk( $field, $id, $curRev );
2778 } else {
2779 $changed = $this->deleteNewtalk( $field, $id );
2780 }
2781
2782 if ( $changed ) {
2783 $this->invalidateCache();
2784 }
2785 }
2786
2787 /**
2788 * Generate a current or new-future timestamp to be stored in the
2789 * user_touched field when we update things.
2790 * @return string Timestamp in TS_MW format
2791 */
2792 private function newTouchedTimestamp() {
2793 global $wgClockSkewFudge;
2794
2795 $time = wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
2796 if ( $this->mTouched && $time <= $this->mTouched ) {
2797 $time = wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $this->mTouched ) + 1 );
2798 }
2799
2800 return $time;
2801 }
2802
2803 /**
2804 * Clear user data from memcached
2805 *
2806 * Use after applying updates to the database; caller's
2807 * responsibility to update user_touched if appropriate.
2808 *
2809 * Called implicitly from invalidateCache() and saveSettings().
2810 *
2811 * @param string $mode Use 'refresh' to clear now; otherwise before DB commit
2812 */
2813 public function clearSharedCache( $mode = 'changed' ) {
2814 if ( !$this->getId() ) {
2815 return;
2816 }
2817
2818 $cache = ObjectCache::getMainWANInstance();
2819 $key = $this->getCacheKey( $cache );
2820 if ( $mode === 'refresh' ) {
2821 $cache->delete( $key, 1 );
2822 } else {
2823 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
2824 if ( $lb->hasOrMadeRecentMasterChanges() ) {
2825 $lb->getConnection( DB_MASTER )->onTransactionPreCommitOrIdle(
2826 function () use ( $cache, $key ) {
2827 $cache->delete( $key );
2828 },
2829 __METHOD__
2830 );
2831 } else {
2832 $cache->delete( $key );
2833 }
2834 }
2835 }
2836
2837 /**
2838 * Immediately touch the user data cache for this account
2839 *
2840 * Calls touch() and removes account data from memcached
2841 */
2842 public function invalidateCache() {
2843 $this->touch();
2844 $this->clearSharedCache();
2845 }
2846
2847 /**
2848 * Update the "touched" timestamp for the user
2849 *
2850 * This is useful on various login/logout events when making sure that
2851 * a browser or proxy that has multiple tenants does not suffer cache
2852 * pollution where the new user sees the old users content. The value
2853 * of getTouched() is checked when determining 304 vs 200 responses.
2854 * Unlike invalidateCache(), this preserves the User object cache and
2855 * avoids database writes.
2856 *
2857 * @since 1.25
2858 */
2859 public function touch() {
2860 $id = $this->getId();
2861 if ( $id ) {
2862 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
2863 $key = $cache->makeKey( 'user-quicktouched', 'id', $id );
2864 $cache->touchCheckKey( $key );
2865 $this->mQuickTouched = null;
2866 }
2867 }
2868
2869 /**
2870 * Validate the cache for this account.
2871 * @param string $timestamp A timestamp in TS_MW format
2872 * @return bool
2873 */
2874 public function validateCache( $timestamp ) {
2875 return ( $timestamp >= $this->getTouched() );
2876 }
2877
2878 /**
2879 * Get the user touched timestamp
2880 *
2881 * Use this value only to validate caches via inequalities
2882 * such as in the case of HTTP If-Modified-Since response logic
2883 *
2884 * @return string TS_MW Timestamp
2885 */
2886 public function getTouched() {
2887 $this->load();
2888
2889 if ( $this->mId ) {
2890 if ( $this->mQuickTouched === null ) {
2891 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
2892 $key = $cache->makeKey( 'user-quicktouched', 'id', $this->mId );
2893
2894 $this->mQuickTouched = wfTimestamp( TS_MW, $cache->getCheckKeyTime( $key ) );
2895 }
2896
2897 return max( $this->mTouched, $this->mQuickTouched );
2898 }
2899
2900 return $this->mTouched;
2901 }
2902
2903 /**
2904 * Get the user_touched timestamp field (time of last DB updates)
2905 * @return string TS_MW Timestamp
2906 * @since 1.26
2907 */
2908 public function getDBTouched() {
2909 $this->load();
2910
2911 return $this->mTouched;
2912 }
2913
2914 /**
2915 * Set the password and reset the random token.
2916 * Calls through to authentication plugin if necessary;
2917 * will have no effect if the auth plugin refuses to
2918 * pass the change through or if the legal password
2919 * checks fail.
2920 *
2921 * As a special case, setting the password to null
2922 * wipes it, so the account cannot be logged in until
2923 * a new password is set, for instance via e-mail.
2924 *
2925 * @deprecated since 1.27, use AuthManager instead
2926 * @param string $str New password to set
2927 * @throws PasswordError On failure
2928 * @return bool
2929 */
2930 public function setPassword( $str ) {
2931 wfDeprecated( __METHOD__, '1.27' );
2932 return $this->setPasswordInternal( $str );
2933 }
2934
2935 /**
2936 * Set the password and reset the random token unconditionally.
2937 *
2938 * @deprecated since 1.27, use AuthManager instead
2939 * @param string|null $str New password to set or null to set an invalid
2940 * password hash meaning that the user will not be able to log in
2941 * through the web interface.
2942 */
2943 public function setInternalPassword( $str ) {
2944 wfDeprecated( __METHOD__, '1.27' );
2945 $this->setPasswordInternal( $str );
2946 }
2947
2948 /**
2949 * Actually set the password and such
2950 * @since 1.27 cannot set a password for a user not in the database
2951 * @param string|null $str New password to set or null to set an invalid
2952 * password hash meaning that the user will not be able to log in
2953 * through the web interface.
2954 * @return bool Success
2955 */
2956 private function setPasswordInternal( $str ) {
2957 $manager = AuthManager::singleton();
2958
2959 // If the user doesn't exist yet, fail
2960 if ( !$manager->userExists( $this->getName() ) ) {
2961 throw new LogicException( 'Cannot set a password for a user that is not in the database.' );
2962 }
2963
2964 $status = $this->changeAuthenticationData( [
2965 'username' => $this->getName(),
2966 'password' => $str,
2967 'retype' => $str,
2968 ] );
2969 if ( !$status->isGood() ) {
2970 \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
2971 ->info( __METHOD__ . ': Password change rejected: '
2972 . $status->getWikiText( null, null, 'en' ) );
2973 return false;
2974 }
2975
2976 $this->setOption( 'watchlisttoken', false );
2977 SessionManager::singleton()->invalidateSessionsForUser( $this );
2978
2979 return true;
2980 }
2981
2982 /**
2983 * Changes credentials of the user.
2984 *
2985 * This is a convenience wrapper around AuthManager::changeAuthenticationData.
2986 * Note that this can return a status that isOK() but not isGood() on certain types of failures,
2987 * e.g. when no provider handled the change.
2988 *
2989 * @param array $data A set of authentication data in fieldname => value format. This is the
2990 * same data you would pass the changeauthenticationdata API - 'username', 'password' etc.
2991 * @return Status
2992 * @since 1.27
2993 */
2994 public function changeAuthenticationData( array $data ) {
2995 $manager = AuthManager::singleton();
2996 $reqs = $manager->getAuthenticationRequests( AuthManager::ACTION_CHANGE, $this );
2997 $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
2998
2999 $status = Status::newGood( 'ignored' );
3000 foreach ( $reqs as $req ) {
3001 $status->merge( $manager->allowsAuthenticationDataChange( $req ), true );
3002 }
3003 if ( $status->getValue() === 'ignored' ) {
3004 $status->warning( 'authenticationdatachange-ignored' );
3005 }
3006
3007 if ( $status->isGood() ) {
3008 foreach ( $reqs as $req ) {
3009 $manager->changeAuthenticationData( $req );
3010 }
3011 }
3012 return $status;
3013 }
3014
3015 /**
3016 * Get the user's current token.
3017 * @param bool $forceCreation Force the generation of a new token if the
3018 * user doesn't have one (default=true for backwards compatibility).
3019 * @return string|null Token
3020 */
3021 public function getToken( $forceCreation = true ) {
3022 global $wgAuthenticationTokenVersion;
3023
3024 $this->load();
3025 if ( !$this->mToken && $forceCreation ) {
3026 $this->setToken();
3027 }
3028
3029 if ( !$this->mToken ) {
3030 // The user doesn't have a token, return null to indicate that.
3031 return null;
3032 }
3033
3034 if ( $this->mToken === self::INVALID_TOKEN ) {
3035 // We return a random value here so existing token checks are very
3036 // likely to fail.
3037 return MWCryptRand::generateHex( self::TOKEN_LENGTH );
3038 }
3039
3040 if ( $wgAuthenticationTokenVersion === null ) {
3041 // $wgAuthenticationTokenVersion not in use, so return the raw secret
3042 return $this->mToken;
3043 }
3044
3045 // $wgAuthenticationTokenVersion in use, so hmac it.
3046 $ret = MWCryptHash::hmac( $wgAuthenticationTokenVersion, $this->mToken, false );
3047
3048 // The raw hash can be overly long. Shorten it up.
3049 $len = max( 32, self::TOKEN_LENGTH );
3050 if ( strlen( $ret ) < $len ) {
3051 // Should never happen, even md5 is 128 bits
3052 throw new \UnexpectedValueException( 'Hmac returned less than 128 bits' );
3053 }
3054
3055 return substr( $ret, -$len );
3056 }
3057
3058 /**
3059 * Set the random token (used for persistent authentication)
3060 * Called from loadDefaults() among other places.
3061 *
3062 * @param string|bool $token If specified, set the token to this value
3063 */
3064 public function setToken( $token = false ) {
3065 $this->load();
3066 if ( $this->mToken === self::INVALID_TOKEN ) {
3067 \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
3068 ->debug( __METHOD__ . ": Ignoring attempt to set token for system user \"$this\"" );
3069 } elseif ( !$token ) {
3070 $this->mToken = MWCryptRand::generateHex( self::TOKEN_LENGTH );
3071 } else {
3072 $this->mToken = $token;
3073 }
3074 }
3075
3076 /**
3077 * Set the password for a password reminder or new account email
3078 *
3079 * @deprecated Removed in 1.27. Use PasswordReset instead.
3080 * @param string $str New password to set or null to set an invalid
3081 * password hash meaning that the user will not be able to use it
3082 * @param bool $throttle If true, reset the throttle timestamp to the present
3083 */
3084 public function setNewpassword( $str, $throttle = true ) {
3085 throw new BadMethodCallException( __METHOD__ . ' has been removed in 1.27' );
3086 }
3087
3088 /**
3089 * Get the user's e-mail address
3090 * @return string User's email address
3091 */
3092 public function getEmail() {
3093 $this->load();
3094 Hooks::run( 'UserGetEmail', [ $this, &$this->mEmail ] );
3095 return $this->mEmail;
3096 }
3097
3098 /**
3099 * Get the timestamp of the user's e-mail authentication
3100 * @return string TS_MW timestamp
3101 */
3102 public function getEmailAuthenticationTimestamp() {
3103 $this->load();
3104 Hooks::run( 'UserGetEmailAuthenticationTimestamp', [ $this, &$this->mEmailAuthenticated ] );
3105 return $this->mEmailAuthenticated;
3106 }
3107
3108 /**
3109 * Set the user's e-mail address
3110 * @param string $str New e-mail address
3111 */
3112 public function setEmail( $str ) {
3113 $this->load();
3114 if ( $str == $this->mEmail ) {
3115 return;
3116 }
3117 $this->invalidateEmail();
3118 $this->mEmail = $str;
3119 Hooks::run( 'UserSetEmail', [ $this, &$this->mEmail ] );
3120 }
3121
3122 /**
3123 * Set the user's e-mail address and a confirmation mail if needed.
3124 *
3125 * @since 1.20
3126 * @param string $str New e-mail address
3127 * @return Status
3128 */
3129 public function setEmailWithConfirmation( $str ) {
3130 global $wgEnableEmail, $wgEmailAuthentication;
3131
3132 if ( !$wgEnableEmail ) {
3133 return Status::newFatal( 'emaildisabled' );
3134 }
3135
3136 $oldaddr = $this->getEmail();
3137 if ( $str === $oldaddr ) {
3138 return Status::newGood( true );
3139 }
3140
3141 $type = $oldaddr != '' ? 'changed' : 'set';
3142 $notificationResult = null;
3143
3144 if ( $wgEmailAuthentication && $type === 'changed' ) {
3145 // Send the user an email notifying the user of the change in registered
3146 // email address on their previous email address
3147 $change = $str != '' ? 'changed' : 'removed';
3148 $notificationResult = $this->sendMail(
3149 wfMessage( 'notificationemail_subject_' . $change )->text(),
3150 wfMessage( 'notificationemail_body_' . $change,
3151 $this->getRequest()->getIP(),
3152 $this->getName(),
3153 $str )->text()
3154 );
3155 }
3156
3157 $this->setEmail( $str );
3158
3159 if ( $str !== '' && $wgEmailAuthentication ) {
3160 // Send a confirmation request to the new address if needed
3161 $result = $this->sendConfirmationMail( $type );
3162
3163 if ( $notificationResult !== null ) {
3164 $result->merge( $notificationResult );
3165 }
3166
3167 if ( $result->isGood() ) {
3168 // Say to the caller that a confirmation and notification mail has been sent
3169 $result->value = 'eauth';
3170 }
3171 } else {
3172 $result = Status::newGood( true );
3173 }
3174
3175 return $result;
3176 }
3177
3178 /**
3179 * Get the user's real name
3180 * @return string User's real name
3181 */
3182 public function getRealName() {
3183 if ( !$this->isItemLoaded( 'realname' ) ) {
3184 $this->load();
3185 }
3186
3187 return $this->mRealName;
3188 }
3189
3190 /**
3191 * Set the user's real name
3192 * @param string $str New real name
3193 */
3194 public function setRealName( $str ) {
3195 $this->load();
3196 $this->mRealName = $str;
3197 }
3198
3199 /**
3200 * Get the user's current setting for a given option.
3201 *
3202 * @param string $oname The option to check
3203 * @param string|array|null $defaultOverride A default value returned if the option does not exist
3204 * @param bool $ignoreHidden Whether to ignore the effects of $wgHiddenPrefs
3205 * @return string|array|int|null User's current value for the option
3206 * @see getBoolOption()
3207 * @see getIntOption()
3208 */
3209 public function getOption( $oname, $defaultOverride = null, $ignoreHidden = false ) {
3210 global $wgHiddenPrefs;
3211 $this->loadOptions();
3212
3213 # We want 'disabled' preferences to always behave as the default value for
3214 # users, even if they have set the option explicitly in their settings (ie they
3215 # set it, and then it was disabled removing their ability to change it). But
3216 # we don't want to erase the preferences in the database in case the preference
3217 # is re-enabled again. So don't touch $mOptions, just override the returned value
3218 if ( !$ignoreHidden && in_array( $oname, $wgHiddenPrefs ) ) {
3219 return self::getDefaultOption( $oname );
3220 }
3221
3222 if ( array_key_exists( $oname, $this->mOptions ) ) {
3223 return $this->mOptions[$oname];
3224 }
3225
3226 return $defaultOverride;
3227 }
3228
3229 /**
3230 * Get all user's options
3231 *
3232 * @param int $flags Bitwise combination of:
3233 * User::GETOPTIONS_EXCLUDE_DEFAULTS Exclude user options that are set
3234 * to the default value. (Since 1.25)
3235 * @return array
3236 */
3237 public function getOptions( $flags = 0 ) {
3238 global $wgHiddenPrefs;
3239 $this->loadOptions();
3240 $options = $this->mOptions;
3241
3242 # We want 'disabled' preferences to always behave as the default value for
3243 # users, even if they have set the option explicitly in their settings (ie they
3244 # set it, and then it was disabled removing their ability to change it). But
3245 # we don't want to erase the preferences in the database in case the preference
3246 # is re-enabled again. So don't touch $mOptions, just override the returned value
3247 foreach ( $wgHiddenPrefs as $pref ) {
3248 $default = self::getDefaultOption( $pref );
3249 if ( $default !== null ) {
3250 $options[$pref] = $default;
3251 }
3252 }
3253
3254 if ( $flags & self::GETOPTIONS_EXCLUDE_DEFAULTS ) {
3255 $options = array_diff_assoc( $options, self::getDefaultOptions() );
3256 }
3257
3258 return $options;
3259 }
3260
3261 /**
3262 * Get the user's current setting for a given option, as a boolean value.
3263 *
3264 * @param string $oname The option to check
3265 * @return bool User's current value for the option
3266 * @see getOption()
3267 */
3268 public function getBoolOption( $oname ) {
3269 return (bool)$this->getOption( $oname );
3270 }
3271
3272 /**
3273 * Get the user's current setting for a given option, as an integer value.
3274 *
3275 * @param string $oname The option to check
3276 * @param int $defaultOverride A default value returned if the option does not exist
3277 * @return int User's current value for the option
3278 * @see getOption()
3279 */
3280 public function getIntOption( $oname, $defaultOverride = 0 ) {
3281 $val = $this->getOption( $oname );
3282 if ( $val == '' ) {
3283 $val = $defaultOverride;
3284 }
3285 return intval( $val );
3286 }
3287
3288 /**
3289 * Set the given option for a user.
3290 *
3291 * You need to call saveSettings() to actually write to the database.
3292 *
3293 * @param string $oname The option to set
3294 * @param mixed $val New value to set
3295 */
3296 public function setOption( $oname, $val ) {
3297 $this->loadOptions();
3298
3299 // Explicitly NULL values should refer to defaults
3300 if ( is_null( $val ) ) {
3301 $val = self::getDefaultOption( $oname );
3302 }
3303
3304 $this->mOptions[$oname] = $val;
3305 }
3306
3307 /**
3308 * Get a token stored in the preferences (like the watchlist one),
3309 * resetting it if it's empty (and saving changes).
3310 *
3311 * @param string $oname The option name to retrieve the token from
3312 * @return string|bool User's current value for the option, or false if this option is disabled.
3313 * @see resetTokenFromOption()
3314 * @see getOption()
3315 * @deprecated since 1.26 Applications should use the OAuth extension
3316 */
3317 public function getTokenFromOption( $oname ) {
3318 global $wgHiddenPrefs;
3319
3320 $id = $this->getId();
3321 if ( !$id || in_array( $oname, $wgHiddenPrefs ) ) {
3322 return false;
3323 }
3324
3325 $token = $this->getOption( $oname );
3326 if ( !$token ) {
3327 // Default to a value based on the user token to avoid space
3328 // wasted on storing tokens for all users. When this option
3329 // is set manually by the user, only then is it stored.
3330 $token = hash_hmac( 'sha1', "$oname:$id", $this->getToken() );
3331 }
3332
3333 return $token;
3334 }
3335
3336 /**
3337 * Reset a token stored in the preferences (like the watchlist one).
3338 * *Does not* save user's preferences (similarly to setOption()).
3339 *
3340 * @param string $oname The option name to reset the token in
3341 * @return string|bool New token value, or false if this option is disabled.
3342 * @see getTokenFromOption()
3343 * @see setOption()
3344 */
3345 public function resetTokenFromOption( $oname ) {
3346 global $wgHiddenPrefs;
3347 if ( in_array( $oname, $wgHiddenPrefs ) ) {
3348 return false;
3349 }
3350
3351 $token = MWCryptRand::generateHex( 40 );
3352 $this->setOption( $oname, $token );
3353 return $token;
3354 }
3355
3356 /**
3357 * Return a list of the types of user options currently returned by
3358 * User::getOptionKinds().
3359 *
3360 * Currently, the option kinds are:
3361 * - 'registered' - preferences which are registered in core MediaWiki or
3362 * by extensions using the UserGetDefaultOptions hook.
3363 * - 'registered-multiselect' - as above, using the 'multiselect' type.
3364 * - 'registered-checkmatrix' - as above, using the 'checkmatrix' type.
3365 * - 'userjs' - preferences with names starting with 'userjs-', intended to
3366 * be used by user scripts.
3367 * - 'special' - "preferences" that are not accessible via User::getOptions
3368 * or User::setOptions.
3369 * - 'unused' - preferences about which MediaWiki doesn't know anything.
3370 * These are usually legacy options, removed in newer versions.
3371 *
3372 * The API (and possibly others) use this function to determine the possible
3373 * option types for validation purposes, so make sure to update this when a
3374 * new option kind is added.
3375 *
3376 * @see User::getOptionKinds
3377 * @return array Option kinds
3378 */
3379 public static function listOptionKinds() {
3380 return [
3381 'registered',
3382 'registered-multiselect',
3383 'registered-checkmatrix',
3384 'userjs',
3385 'special',
3386 'unused'
3387 ];
3388 }
3389
3390 /**
3391 * Return an associative array mapping preferences keys to the kind of a preference they're
3392 * used for. Different kinds are handled differently when setting or reading preferences.
3393 *
3394 * See User::listOptionKinds for the list of valid option types that can be provided.
3395 *
3396 * @see User::listOptionKinds
3397 * @param IContextSource $context
3398 * @param array|null $options Assoc. array with options keys to check as keys.
3399 * Defaults to $this->mOptions.
3400 * @return array The key => kind mapping data
3401 */
3402 public function getOptionKinds( IContextSource $context, $options = null ) {
3403 $this->loadOptions();
3404 if ( $options === null ) {
3405 $options = $this->mOptions;
3406 }
3407
3408 $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory();
3409 $prefs = $preferencesFactory->getFormDescriptor( $this, $context );
3410 $mapping = [];
3411
3412 // Pull out the "special" options, so they don't get converted as
3413 // multiselect or checkmatrix.
3414 $specialOptions = array_fill_keys( $preferencesFactory->getSaveBlacklist(), true );
3415 foreach ( $specialOptions as $name => $value ) {
3416 unset( $prefs[$name] );
3417 }
3418
3419 // Multiselect and checkmatrix options are stored in the database with
3420 // one key per option, each having a boolean value. Extract those keys.
3421 $multiselectOptions = [];
3422 foreach ( $prefs as $name => $info ) {
3423 if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
3424 ( isset( $info['class'] ) && $info['class'] == HTMLMultiSelectField::class ) ) {
3425 $opts = HTMLFormField::flattenOptions( $info['options'] );
3426 $prefix = $info['prefix'] ?? $name;
3427
3428 foreach ( $opts as $value ) {
3429 $multiselectOptions["$prefix$value"] = true;
3430 }
3431
3432 unset( $prefs[$name] );
3433 }
3434 }
3435 $checkmatrixOptions = [];
3436 foreach ( $prefs as $name => $info ) {
3437 if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
3438 ( isset( $info['class'] ) && $info['class'] == HTMLCheckMatrix::class ) ) {
3439 $columns = HTMLFormField::flattenOptions( $info['columns'] );
3440 $rows = HTMLFormField::flattenOptions( $info['rows'] );
3441 $prefix = $info['prefix'] ?? $name;
3442
3443 foreach ( $columns as $column ) {
3444 foreach ( $rows as $row ) {
3445 $checkmatrixOptions["$prefix$column-$row"] = true;
3446 }
3447 }
3448
3449 unset( $prefs[$name] );
3450 }
3451 }
3452
3453 // $value is ignored
3454 foreach ( $options as $key => $value ) {
3455 if ( isset( $prefs[$key] ) ) {
3456 $mapping[$key] = 'registered';
3457 } elseif ( isset( $multiselectOptions[$key] ) ) {
3458 $mapping[$key] = 'registered-multiselect';
3459 } elseif ( isset( $checkmatrixOptions[$key] ) ) {
3460 $mapping[$key] = 'registered-checkmatrix';
3461 } elseif ( isset( $specialOptions[$key] ) ) {
3462 $mapping[$key] = 'special';
3463 } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
3464 $mapping[$key] = 'userjs';
3465 } else {
3466 $mapping[$key] = 'unused';
3467 }
3468 }
3469
3470 return $mapping;
3471 }
3472
3473 /**
3474 * Reset certain (or all) options to the site defaults
3475 *
3476 * The optional parameter determines which kinds of preferences will be reset.
3477 * Supported values are everything that can be reported by getOptionKinds()
3478 * and 'all', which forces a reset of *all* preferences and overrides everything else.
3479 *
3480 * @param array|string $resetKinds Which kinds of preferences to reset. Defaults to
3481 * array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' )
3482 * for backwards-compatibility.
3483 * @param IContextSource|null $context Context source used when $resetKinds
3484 * does not contain 'all', passed to getOptionKinds().
3485 * Defaults to RequestContext::getMain() when null.
3486 */
3487 public function resetOptions(
3488 $resetKinds = [ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ],
3489 IContextSource $context = null
3490 ) {
3491 $this->load();
3492 $defaultOptions = self::getDefaultOptions();
3493
3494 if ( !is_array( $resetKinds ) ) {
3495 $resetKinds = [ $resetKinds ];
3496 }
3497
3498 if ( in_array( 'all', $resetKinds ) ) {
3499 $newOptions = $defaultOptions;
3500 } else {
3501 if ( $context === null ) {
3502 $context = RequestContext::getMain();
3503 }
3504
3505 $optionKinds = $this->getOptionKinds( $context );
3506 $resetKinds = array_intersect( $resetKinds, self::listOptionKinds() );
3507 $newOptions = [];
3508
3509 // Use default values for the options that should be deleted, and
3510 // copy old values for the ones that shouldn't.
3511 foreach ( $this->mOptions as $key => $value ) {
3512 if ( in_array( $optionKinds[$key], $resetKinds ) ) {
3513 if ( array_key_exists( $key, $defaultOptions ) ) {
3514 $newOptions[$key] = $defaultOptions[$key];
3515 }
3516 } else {
3517 $newOptions[$key] = $value;
3518 }
3519 }
3520 }
3521
3522 Hooks::run( 'UserResetAllOptions', [ $this, &$newOptions, $this->mOptions, $resetKinds ] );
3523
3524 $this->mOptions = $newOptions;
3525 $this->mOptionsLoaded = true;
3526 }
3527
3528 /**
3529 * Get the user's preferred date format.
3530 * @return string User's preferred date format
3531 */
3532 public function getDatePreference() {
3533 // Important migration for old data rows
3534 if ( is_null( $this->mDatePreference ) ) {
3535 global $wgLang;
3536 $value = $this->getOption( 'date' );
3537 $map = $wgLang->getDatePreferenceMigrationMap();
3538 if ( isset( $map[$value] ) ) {
3539 $value = $map[$value];
3540 }
3541 $this->mDatePreference = $value;
3542 }
3543 return $this->mDatePreference;
3544 }
3545
3546 /**
3547 * Determine based on the wiki configuration and the user's options,
3548 * whether this user must be over HTTPS no matter what.
3549 *
3550 * @return bool
3551 */
3552 public function requiresHTTPS() {
3553 global $wgSecureLogin;
3554 if ( !$wgSecureLogin ) {
3555 return false;
3556 }
3557
3558 $https = $this->getBoolOption( 'prefershttps' );
3559 Hooks::run( 'UserRequiresHTTPS', [ $this, &$https ] );
3560 if ( $https ) {
3561 $https = wfCanIPUseHTTPS( $this->getRequest()->getIP() );
3562 }
3563
3564 return $https;
3565 }
3566
3567 /**
3568 * Get the user preferred stub threshold
3569 *
3570 * @return int
3571 */
3572 public function getStubThreshold() {
3573 global $wgMaxArticleSize; # Maximum article size, in Kb
3574 $threshold = $this->getIntOption( 'stubthreshold' );
3575 if ( $threshold > $wgMaxArticleSize * 1024 ) {
3576 // If they have set an impossible value, disable the preference
3577 // so we can use the parser cache again.
3578 $threshold = 0;
3579 }
3580 return $threshold;
3581 }
3582
3583 /**
3584 * Get the permissions this user has.
3585 * @return string[] permission names
3586 */
3587 public function getRights() {
3588 if ( is_null( $this->mRights ) ) {
3589 $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
3590 Hooks::run( 'UserGetRights', [ $this, &$this->mRights ] );
3591
3592 // Deny any rights denied by the user's session, unless this
3593 // endpoint has no sessions.
3594 if ( !defined( 'MW_NO_SESSION' ) ) {
3595 $allowedRights = $this->getRequest()->getSession()->getAllowedUserRights();
3596 if ( $allowedRights !== null ) {
3597 $this->mRights = array_intersect( $this->mRights, $allowedRights );
3598 }
3599 }
3600
3601 Hooks::run( 'UserGetRightsRemove', [ $this, &$this->mRights ] );
3602 // Force reindexation of rights when a hook has unset one of them
3603 $this->mRights = array_values( array_unique( $this->mRights ) );
3604
3605 // If block disables login, we should also remove any
3606 // extra rights blocked users might have, in case the
3607 // blocked user has a pre-existing session (T129738).
3608 // This is checked here for cases where people only call
3609 // $user->isAllowed(). It is also checked in Title::checkUserBlock()
3610 // to give a better error message in the common case.
3611 $config = RequestContext::getMain()->getConfig();
3612 if (
3613 $this->isLoggedIn() &&
3614 $config->get( 'BlockDisablesLogin' ) &&
3615 $this->isBlocked()
3616 ) {
3617 $anon = new User;
3618 $this->mRights = array_intersect( $this->mRights, $anon->getRights() );
3619 }
3620 }
3621 return $this->mRights;
3622 }
3623
3624 /**
3625 * Get the list of explicit group memberships this user has.
3626 * The implicit * and user groups are not included.
3627 *
3628 * @return string[] Array of internal group names (sorted since 1.33)
3629 */
3630 public function getGroups() {
3631 $this->load();
3632 $this->loadGroups();
3633 return array_keys( $this->mGroupMemberships );
3634 }
3635
3636 /**
3637 * Get the list of explicit group memberships this user has, stored as
3638 * UserGroupMembership objects. Implicit groups are not included.
3639 *
3640 * @return UserGroupMembership[] Associative array of (group name => UserGroupMembership object)
3641 * @since 1.29
3642 */
3643 public function getGroupMemberships() {
3644 $this->load();
3645 $this->loadGroups();
3646 return $this->mGroupMemberships;
3647 }
3648
3649 /**
3650 * Get the list of implicit group memberships this user has.
3651 * This includes all explicit groups, plus 'user' if logged in,
3652 * '*' for all accounts, and autopromoted groups
3653 * @param bool $recache Whether to avoid the cache
3654 * @return array Array of String internal group names
3655 */
3656 public function getEffectiveGroups( $recache = false ) {
3657 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
3658 $this->mEffectiveGroups = array_unique( array_merge(
3659 $this->getGroups(), // explicit groups
3660 $this->getAutomaticGroups( $recache ) // implicit groups
3661 ) );
3662 // Avoid PHP 7.1 warning of passing $this by reference
3663 $user = $this;
3664 // Hook for additional groups
3665 Hooks::run( 'UserEffectiveGroups', [ &$user, &$this->mEffectiveGroups ] );
3666 // Force reindexation of groups when a hook has unset one of them
3667 $this->mEffectiveGroups = array_values( array_unique( $this->mEffectiveGroups ) );
3668 }
3669 return $this->mEffectiveGroups;
3670 }
3671
3672 /**
3673 * Get the list of implicit group memberships this user has.
3674 * This includes 'user' if logged in, '*' for all accounts,
3675 * and autopromoted groups
3676 * @param bool $recache Whether to avoid the cache
3677 * @return array Array of String internal group names
3678 */
3679 public function getAutomaticGroups( $recache = false ) {
3680 if ( $recache || is_null( $this->mImplicitGroups ) ) {
3681 $this->mImplicitGroups = [ '*' ];
3682 if ( $this->getId() ) {
3683 $this->mImplicitGroups[] = 'user';
3684
3685 $this->mImplicitGroups = array_unique( array_merge(
3686 $this->mImplicitGroups,
3687 Autopromote::getAutopromoteGroups( $this )
3688 ) );
3689 }
3690 if ( $recache ) {
3691 // Assure data consistency with rights/groups,
3692 // as getEffectiveGroups() depends on this function
3693 $this->mEffectiveGroups = null;
3694 }
3695 }
3696 return $this->mImplicitGroups;
3697 }
3698
3699 /**
3700 * Returns the groups the user has belonged to.
3701 *
3702 * The user may still belong to the returned groups. Compare with getGroups().
3703 *
3704 * The function will not return groups the user had belonged to before MW 1.17
3705 *
3706 * @return array Names of the groups the user has belonged to.
3707 */
3708 public function getFormerGroups() {
3709 $this->load();
3710
3711 if ( is_null( $this->mFormerGroups ) ) {
3712 $db = ( $this->queryFlagsUsed & self::READ_LATEST )
3713 ? wfGetDB( DB_MASTER )
3714 : wfGetDB( DB_REPLICA );
3715 $res = $db->select( 'user_former_groups',
3716 [ 'ufg_group' ],
3717 [ 'ufg_user' => $this->mId ],
3718 __METHOD__ );
3719 $this->mFormerGroups = [];
3720 foreach ( $res as $row ) {
3721 $this->mFormerGroups[] = $row->ufg_group;
3722 }
3723 }
3724
3725 return $this->mFormerGroups;
3726 }
3727
3728 /**
3729 * Get the user's edit count.
3730 * @return int|null Null for anonymous users
3731 */
3732 public function getEditCount() {
3733 if ( !$this->getId() ) {
3734 return null;
3735 }
3736
3737 if ( $this->mEditCount === null ) {
3738 /* Populate the count, if it has not been populated yet */
3739 $dbr = wfGetDB( DB_REPLICA );
3740 // check if the user_editcount field has been initialized
3741 $count = $dbr->selectField(
3742 'user', 'user_editcount',
3743 [ 'user_id' => $this->mId ],
3744 __METHOD__
3745 );
3746
3747 if ( $count === null ) {
3748 // it has not been initialized. do so.
3749 $count = $this->initEditCountInternal();
3750 }
3751 $this->mEditCount = $count;
3752 }
3753 return (int)$this->mEditCount;
3754 }
3755
3756 /**
3757 * Add the user to the given group. This takes immediate effect.
3758 * If the user is already in the group, the expiry time will be updated to the new
3759 * expiry time. (If $expiry is omitted or null, the membership will be altered to
3760 * never expire.)
3761 *
3762 * @param string $group Name of the group to add
3763 * @param string|null $expiry Optional expiry timestamp in any format acceptable to
3764 * wfTimestamp(), or null if the group assignment should not expire
3765 * @return bool
3766 */
3767 public function addGroup( $group, $expiry = null ) {
3768 $this->load();
3769 $this->loadGroups();
3770
3771 if ( $expiry ) {
3772 $expiry = wfTimestamp( TS_MW, $expiry );
3773 }
3774
3775 if ( !Hooks::run( 'UserAddGroup', [ $this, &$group, &$expiry ] ) ) {
3776 return false;
3777 }
3778
3779 // create the new UserGroupMembership and put it in the DB
3780 $ugm = new UserGroupMembership( $this->mId, $group, $expiry );
3781 if ( !$ugm->insert( true ) ) {
3782 return false;
3783 }
3784
3785 $this->mGroupMemberships[$group] = $ugm;
3786
3787 // Refresh the groups caches, and clear the rights cache so it will be
3788 // refreshed on the next call to $this->getRights().
3789 $this->getEffectiveGroups( true );
3790 $this->mRights = null;
3791
3792 $this->invalidateCache();
3793
3794 return true;
3795 }
3796
3797 /**
3798 * Remove the user from the given group.
3799 * This takes immediate effect.
3800 * @param string $group Name of the group to remove
3801 * @return bool
3802 */
3803 public function removeGroup( $group ) {
3804 $this->load();
3805
3806 if ( !Hooks::run( 'UserRemoveGroup', [ $this, &$group ] ) ) {
3807 return false;
3808 }
3809
3810 $ugm = UserGroupMembership::getMembership( $this->mId, $group );
3811 // delete the membership entry
3812 if ( !$ugm || !$ugm->delete() ) {
3813 return false;
3814 }
3815
3816 $this->loadGroups();
3817 unset( $this->mGroupMemberships[$group] );
3818
3819 // Refresh the groups caches, and clear the rights cache so it will be
3820 // refreshed on the next call to $this->getRights().
3821 $this->getEffectiveGroups( true );
3822 $this->mRights = null;
3823
3824 $this->invalidateCache();
3825
3826 return true;
3827 }
3828
3829 /**
3830 * Get whether the user is logged in
3831 * @return bool
3832 */
3833 public function isLoggedIn() {
3834 return $this->getId() != 0;
3835 }
3836
3837 /**
3838 * Get whether the user is anonymous
3839 * @return bool
3840 */
3841 public function isAnon() {
3842 return !$this->isLoggedIn();
3843 }
3844
3845 /**
3846 * @return bool Whether this user is flagged as being a bot role account
3847 * @since 1.28
3848 */
3849 public function isBot() {
3850 if ( in_array( 'bot', $this->getGroups() ) && $this->isAllowed( 'bot' ) ) {
3851 return true;
3852 }
3853
3854 $isBot = false;
3855 Hooks::run( "UserIsBot", [ $this, &$isBot ] );
3856
3857 return $isBot;
3858 }
3859
3860 /**
3861 * Check if user is allowed to access a feature / make an action
3862 *
3863 * @param string $permissions,... Permissions to test
3864 * @return bool True if user is allowed to perform *any* of the given actions
3865 */
3866 public function isAllowedAny() {
3867 $permissions = func_get_args();
3868 foreach ( $permissions as $permission ) {
3869 if ( $this->isAllowed( $permission ) ) {
3870 return true;
3871 }
3872 }
3873 return false;
3874 }
3875
3876 /**
3877 *
3878 * @param string $permissions,... Permissions to test
3879 * @return bool True if the user is allowed to perform *all* of the given actions
3880 */
3881 public function isAllowedAll() {
3882 $permissions = func_get_args();
3883 foreach ( $permissions as $permission ) {
3884 if ( !$this->isAllowed( $permission ) ) {
3885 return false;
3886 }
3887 }
3888 return true;
3889 }
3890
3891 /**
3892 * Internal mechanics of testing a permission
3893 * @param string $action
3894 * @return bool
3895 */
3896 public function isAllowed( $action = '' ) {
3897 if ( $action === '' ) {
3898 return true; // In the spirit of DWIM
3899 }
3900 // Use strict parameter to avoid matching numeric 0 accidentally inserted
3901 // by misconfiguration: 0 == 'foo'
3902 return in_array( $action, $this->getRights(), true );
3903 }
3904
3905 /**
3906 * Check whether to enable recent changes patrol features for this user
3907 * @return bool True or false
3908 */
3909 public function useRCPatrol() {
3910 global $wgUseRCPatrol;
3911 return $wgUseRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' );
3912 }
3913
3914 /**
3915 * Check whether to enable new pages patrol features for this user
3916 * @return bool True or false
3917 */
3918 public function useNPPatrol() {
3919 global $wgUseRCPatrol, $wgUseNPPatrol;
3920 return (
3921 ( $wgUseRCPatrol || $wgUseNPPatrol )
3922 && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
3923 );
3924 }
3925
3926 /**
3927 * Check whether to enable new files patrol features for this user
3928 * @return bool True or false
3929 */
3930 public function useFilePatrol() {
3931 global $wgUseRCPatrol, $wgUseFilePatrol;
3932 return (
3933 ( $wgUseRCPatrol || $wgUseFilePatrol )
3934 && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
3935 );
3936 }
3937
3938 /**
3939 * Get the WebRequest object to use with this object
3940 *
3941 * @return WebRequest
3942 */
3943 public function getRequest() {
3944 if ( $this->mRequest ) {
3945 return $this->mRequest;
3946 }
3947
3948 global $wgRequest;
3949 return $wgRequest;
3950 }
3951
3952 /**
3953 * Check the watched status of an article.
3954 * @since 1.22 $checkRights parameter added
3955 * @param Title $title Title of the article to look at
3956 * @param bool $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
3957 * Pass User::CHECK_USER_RIGHTS or User::IGNORE_USER_RIGHTS.
3958 * @return bool
3959 */
3960 public function isWatched( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
3961 if ( $title->isWatchable() && ( !$checkRights || $this->isAllowed( 'viewmywatchlist' ) ) ) {
3962 return MediaWikiServices::getInstance()->getWatchedItemStore()->isWatched( $this, $title );
3963 }
3964 return false;
3965 }
3966
3967 /**
3968 * Watch an article.
3969 * @since 1.22 $checkRights parameter added
3970 * @param Title $title Title of the article to look at
3971 * @param bool $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
3972 * Pass User::CHECK_USER_RIGHTS or User::IGNORE_USER_RIGHTS.
3973 */
3974 public function addWatch( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
3975 if ( !$checkRights || $this->isAllowed( 'editmywatchlist' ) ) {
3976 MediaWikiServices::getInstance()->getWatchedItemStore()->addWatchBatchForUser(
3977 $this,
3978 [ $title->getSubjectPage(), $title->getTalkPage() ]
3979 );
3980 }
3981 $this->invalidateCache();
3982 }
3983
3984 /**
3985 * Stop watching an article.
3986 * @since 1.22 $checkRights parameter added
3987 * @param Title $title Title of the article to look at
3988 * @param bool $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
3989 * Pass User::CHECK_USER_RIGHTS or User::IGNORE_USER_RIGHTS.
3990 */
3991 public function removeWatch( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
3992 if ( !$checkRights || $this->isAllowed( 'editmywatchlist' ) ) {
3993 $store = MediaWikiServices::getInstance()->getWatchedItemStore();
3994 $store->removeWatch( $this, $title->getSubjectPage() );
3995 $store->removeWatch( $this, $title->getTalkPage() );
3996 }
3997 $this->invalidateCache();
3998 }
3999
4000 /**
4001 * Clear the user's notification timestamp for the given title.
4002 * If e-notif e-mails are on, they will receive notification mails on
4003 * the next change of the page if it's watched etc.
4004 * @note If the user doesn't have 'editmywatchlist', this will do nothing.
4005 * @param Title &$title Title of the article to look at
4006 * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed.
4007 */
4008 public function clearNotification( &$title, $oldid = 0 ) {
4009 global $wgUseEnotif, $wgShowUpdatedMarker;
4010
4011 // Do nothing if the database is locked to writes
4012 if ( wfReadOnly() ) {
4013 return;
4014 }
4015
4016 // Do nothing if not allowed to edit the watchlist
4017 if ( !$this->isAllowed( 'editmywatchlist' ) ) {
4018 return;
4019 }
4020
4021 // If we're working on user's talk page, we should update the talk page message indicator
4022 if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
4023 // Avoid PHP 7.1 warning of passing $this by reference
4024 $user = $this;
4025 if ( !Hooks::run( 'UserClearNewTalkNotification', [ &$user, $oldid ] ) ) {
4026 return;
4027 }
4028
4029 // Try to update the DB post-send and only if needed...
4030 DeferredUpdates::addCallableUpdate( function () use ( $title, $oldid ) {
4031 if ( !$this->getNewtalk() ) {
4032 return; // no notifications to clear
4033 }
4034
4035 // Delete the last notifications (they stack up)
4036 $this->setNewtalk( false );
4037
4038 // If there is a new, unseen, revision, use its timestamp
4039 $nextid = $oldid
4040 ? $title->getNextRevisionID( $oldid, Title::GAID_FOR_UPDATE )
4041 : null;
4042 if ( $nextid ) {
4043 $this->setNewtalk( true, Revision::newFromId( $nextid ) );
4044 }
4045 } );
4046 }
4047
4048 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
4049 return;
4050 }
4051
4052 if ( $this->isAnon() ) {
4053 // Nothing else to do...
4054 return;
4055 }
4056
4057 // Only update the timestamp if the page is being watched.
4058 // The query to find out if it is watched is cached both in memcached and per-invocation,
4059 // and when it does have to be executed, it can be on a replica DB
4060 // If this is the user's newtalk page, we always update the timestamp
4061 $force = '';
4062 if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
4063 $force = 'force';
4064 }
4065
4066 MediaWikiServices::getInstance()->getWatchedItemStore()
4067 ->resetNotificationTimestamp( $this, $title, $force, $oldid );
4068 }
4069
4070 /**
4071 * Resets all of the given user's page-change notification timestamps.
4072 * If e-notif e-mails are on, they will receive notification mails on
4073 * the next change of any watched page.
4074 * @note If the user doesn't have 'editmywatchlist', this will do nothing.
4075 */
4076 public function clearAllNotifications() {
4077 global $wgUseEnotif, $wgShowUpdatedMarker;
4078 // Do nothing if not allowed to edit the watchlist
4079 if ( wfReadOnly() || !$this->isAllowed( 'editmywatchlist' ) ) {
4080 return;
4081 }
4082
4083 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
4084 $this->setNewtalk( false );
4085 return;
4086 }
4087
4088 $id = $this->getId();
4089 if ( !$id ) {
4090 return;
4091 }
4092
4093 $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
4094 $watchedItemStore->resetAllNotificationTimestampsForUser( $this );
4095
4096 // We also need to clear here the "you have new message" notification for the own
4097 // user_talk page; it's cleared one page view later in WikiPage::doViewUpdates().
4098 }
4099
4100 /**
4101 * Compute experienced level based on edit count and registration date.
4102 *
4103 * @return string 'newcomer', 'learner', or 'experienced'
4104 */
4105 public function getExperienceLevel() {
4106 global $wgLearnerEdits,
4107 $wgExperiencedUserEdits,
4108 $wgLearnerMemberSince,
4109 $wgExperiencedUserMemberSince;
4110
4111 if ( $this->isAnon() ) {
4112 return false;
4113 }
4114
4115 $editCount = $this->getEditCount();
4116 $registration = $this->getRegistration();
4117 $now = time();
4118 $learnerRegistration = wfTimestamp( TS_MW, $now - $wgLearnerMemberSince * 86400 );
4119 $experiencedRegistration = wfTimestamp( TS_MW, $now - $wgExperiencedUserMemberSince * 86400 );
4120
4121 if ( $editCount < $wgLearnerEdits ||
4122 $registration > $learnerRegistration ) {
4123 return 'newcomer';
4124 }
4125
4126 if ( $editCount > $wgExperiencedUserEdits &&
4127 $registration <= $experiencedRegistration
4128 ) {
4129 return 'experienced';
4130 }
4131
4132 return 'learner';
4133 }
4134
4135 /**
4136 * Persist this user's session (e.g. set cookies)
4137 *
4138 * @param WebRequest|null $request WebRequest object to use; $wgRequest will be used if null
4139 * is passed.
4140 * @param bool|null $secure Whether to force secure/insecure cookies or use default
4141 * @param bool $rememberMe Whether to add a Token cookie for elongated sessions
4142 */
4143 public function setCookies( $request = null, $secure = null, $rememberMe = false ) {
4144 $this->load();
4145 if ( $this->mId == 0 ) {
4146 return;
4147 }
4148
4149 $session = $this->getRequest()->getSession();
4150 if ( $request && $session->getRequest() !== $request ) {
4151 $session = $session->sessionWithRequest( $request );
4152 }
4153 $delay = $session->delaySave();
4154
4155 if ( !$session->getUser()->equals( $this ) ) {
4156 if ( !$session->canSetUser() ) {
4157 \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
4158 ->warning( __METHOD__ .
4159 ": Cannot save user \"$this\" to a user \"{$session->getUser()}\"'s immutable session"
4160 );
4161 return;
4162 }
4163 $session->setUser( $this );
4164 }
4165
4166 $session->setRememberUser( $rememberMe );
4167 if ( $secure !== null ) {
4168 $session->setForceHTTPS( $secure );
4169 }
4170
4171 $session->persist();
4172
4173 ScopedCallback::consume( $delay );
4174 }
4175
4176 /**
4177 * Log this user out.
4178 */
4179 public function logout() {
4180 // Avoid PHP 7.1 warning of passing $this by reference
4181 $user = $this;
4182 if ( Hooks::run( 'UserLogout', [ &$user ] ) ) {
4183 $this->doLogout();
4184 }
4185 }
4186
4187 /**
4188 * Clear the user's session, and reset the instance cache.
4189 * @see logout()
4190 */
4191 public function doLogout() {
4192 $session = $this->getRequest()->getSession();
4193 if ( !$session->canSetUser() ) {
4194 \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
4195 ->warning( __METHOD__ . ": Cannot log out of an immutable session" );
4196 $error = 'immutable';
4197 } elseif ( !$session->getUser()->equals( $this ) ) {
4198 \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
4199 ->warning( __METHOD__ .
4200 ": Cannot log user \"$this\" out of a user \"{$session->getUser()}\"'s session"
4201 );
4202 // But we still may as well make this user object anon
4203 $this->clearInstanceCache( 'defaults' );
4204 $error = 'wronguser';
4205 } else {
4206 $this->clearInstanceCache( 'defaults' );
4207 $delay = $session->delaySave();
4208 $session->unpersist(); // Clear cookies (T127436)
4209 $session->setLoggedOutTimestamp( time() );
4210 $session->setUser( new User );
4211 $session->set( 'wsUserID', 0 ); // Other code expects this
4212 $session->resetAllTokens();
4213 ScopedCallback::consume( $delay );
4214 $error = false;
4215 }
4216 \MediaWiki\Logger\LoggerFactory::getInstance( 'authevents' )->info( 'Logout', [
4217 'event' => 'logout',
4218 'successful' => $error === false,
4219 'status' => $error ?: 'success',
4220 ] );
4221 }
4222
4223 /**
4224 * Save this user's settings into the database.
4225 * @todo Only rarely do all these fields need to be set!
4226 */
4227 public function saveSettings() {
4228 if ( wfReadOnly() ) {
4229 // @TODO: caller should deal with this instead!
4230 // This should really just be an exception.
4231 MWExceptionHandler::logException( new DBExpectedError(
4232 null,
4233 "Could not update user with ID '{$this->mId}'; DB is read-only."
4234 ) );
4235 return;
4236 }
4237
4238 $this->load();
4239 if ( $this->mId == 0 ) {
4240 return; // anon
4241 }
4242
4243 // Get a new user_touched that is higher than the old one.
4244 // This will be used for a CAS check as a last-resort safety
4245 // check against race conditions and replica DB lag.
4246 $newTouched = $this->newTouchedTimestamp();
4247
4248 $dbw = wfGetDB( DB_MASTER );
4249 $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $newTouched ) {
4250 global $wgActorTableSchemaMigrationStage;
4251
4252 $dbw->update( 'user',
4253 [ /* SET */
4254 'user_name' => $this->mName,
4255 'user_real_name' => $this->mRealName,
4256 'user_email' => $this->mEmail,
4257 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
4258 'user_touched' => $dbw->timestamp( $newTouched ),
4259 'user_token' => strval( $this->mToken ),
4260 'user_email_token' => $this->mEmailToken,
4261 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
4262 ], $this->makeUpdateConditions( $dbw, [ /* WHERE */
4263 'user_id' => $this->mId,
4264 ] ), $fname
4265 );
4266
4267 if ( !$dbw->affectedRows() ) {
4268 // Maybe the problem was a missed cache update; clear it to be safe
4269 $this->clearSharedCache( 'refresh' );
4270 // User was changed in the meantime or loaded with stale data
4271 $from = ( $this->queryFlagsUsed & self::READ_LATEST ) ? 'master' : 'replica';
4272 LoggerFactory::getInstance( 'preferences' )->warning(
4273 "CAS update failed on user_touched for user ID '{user_id}' ({db_flag} read)",
4274 [ 'user_id' => $this->mId, 'db_flag' => $from ]
4275 );
4276 throw new MWException( "CAS update failed on user_touched. " .
4277 "The version of the user to be saved is older than the current version."
4278 );
4279 }
4280
4281 if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
4282 $dbw->update(
4283 'actor',
4284 [ 'actor_name' => $this->mName ],
4285 [ 'actor_user' => $this->mId ],
4286 $fname
4287 );
4288 }
4289 } );
4290
4291 $this->mTouched = $newTouched;
4292 $this->saveOptions();
4293
4294 Hooks::run( 'UserSaveSettings', [ $this ] );
4295 $this->clearSharedCache();
4296 $this->getUserPage()->invalidateCache();
4297 }
4298
4299 /**
4300 * If only this user's username is known, and it exists, return the user ID.
4301 *
4302 * @param int $flags Bitfield of User:READ_* constants; useful for existence checks
4303 * @return int
4304 */
4305 public function idForName( $flags = 0 ) {
4306 $s = trim( $this->getName() );
4307 if ( $s === '' ) {
4308 return 0;
4309 }
4310
4311 $db = ( ( $flags & self::READ_LATEST ) == self::READ_LATEST )
4312 ? wfGetDB( DB_MASTER )
4313 : wfGetDB( DB_REPLICA );
4314
4315 $options = ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING )
4316 ? [ 'LOCK IN SHARE MODE' ]
4317 : [];
4318
4319 $id = $db->selectField( 'user',
4320 'user_id', [ 'user_name' => $s ], __METHOD__, $options );
4321
4322 return (int)$id;
4323 }
4324
4325 /**
4326 * Add a user to the database, return the user object
4327 *
4328 * @param string $name Username to add
4329 * @param array $params Array of Strings Non-default parameters to save to
4330 * the database as user_* fields:
4331 * - email: The user's email address.
4332 * - email_authenticated: The email authentication timestamp.
4333 * - real_name: The user's real name.
4334 * - options: An associative array of non-default options.
4335 * - token: Random authentication token. Do not set.
4336 * - registration: Registration timestamp. Do not set.
4337 *
4338 * @return User|null User object, or null if the username already exists.
4339 */
4340 public static function createNew( $name, $params = [] ) {
4341 foreach ( [ 'password', 'newpassword', 'newpass_time', 'password_expires' ] as $field ) {
4342 if ( isset( $params[$field] ) ) {
4343 wfDeprecated( __METHOD__ . " with param '$field'", '1.27' );
4344 unset( $params[$field] );
4345 }
4346 }
4347
4348 $user = new User;
4349 $user->load();
4350 $user->setToken(); // init token
4351 if ( isset( $params['options'] ) ) {
4352 $user->mOptions = $params['options'] + (array)$user->mOptions;
4353 unset( $params['options'] );
4354 }
4355 $dbw = wfGetDB( DB_MASTER );
4356
4357 $noPass = PasswordFactory::newInvalidPassword()->toString();
4358
4359 $fields = [
4360 'user_name' => $name,
4361 'user_password' => $noPass,
4362 'user_newpassword' => $noPass,
4363 'user_email' => $user->mEmail,
4364 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
4365 'user_real_name' => $user->mRealName,
4366 'user_token' => strval( $user->mToken ),
4367 'user_registration' => $dbw->timestamp( $user->mRegistration ),
4368 'user_editcount' => 0,
4369 'user_touched' => $dbw->timestamp( $user->newTouchedTimestamp() ),
4370 ];
4371 foreach ( $params as $name => $value ) {
4372 $fields["user_$name"] = $value;
4373 }
4374
4375 return $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $fields ) {
4376 $dbw->insert( 'user', $fields, $fname, [ 'IGNORE' ] );
4377 if ( $dbw->affectedRows() ) {
4378 $newUser = self::newFromId( $dbw->insertId() );
4379 $newUser->mName = $fields['user_name'];
4380 $newUser->updateActorId( $dbw );
4381 // Load the user from master to avoid replica lag
4382 $newUser->load( self::READ_LATEST );
4383 } else {
4384 $newUser = null;
4385 }
4386 return $newUser;
4387 } );
4388 }
4389
4390 /**
4391 * Add this existing user object to the database. If the user already
4392 * exists, a fatal status object is returned, and the user object is
4393 * initialised with the data from the database.
4394 *
4395 * Previously, this function generated a DB error due to a key conflict
4396 * if the user already existed. Many extension callers use this function
4397 * in code along the lines of:
4398 *
4399 * $user = User::newFromName( $name );
4400 * if ( !$user->isLoggedIn() ) {
4401 * $user->addToDatabase();
4402 * }
4403 * // do something with $user...
4404 *
4405 * However, this was vulnerable to a race condition (T18020). By
4406 * initialising the user object if the user exists, we aim to support this
4407 * calling sequence as far as possible.
4408 *
4409 * Note that if the user exists, this function will acquire a write lock,
4410 * so it is still advisable to make the call conditional on isLoggedIn(),
4411 * and to commit the transaction after calling.
4412 *
4413 * @throws MWException
4414 * @return Status
4415 */
4416 public function addToDatabase() {
4417 $this->load();
4418 if ( !$this->mToken ) {
4419 $this->setToken(); // init token
4420 }
4421
4422 if ( !is_string( $this->mName ) ) {
4423 throw new RuntimeException( "User name field is not set." );
4424 }
4425
4426 $this->mTouched = $this->newTouchedTimestamp();
4427
4428 $dbw = wfGetDB( DB_MASTER );
4429 $status = $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) {
4430 $noPass = PasswordFactory::newInvalidPassword()->toString();
4431 $dbw->insert( 'user',
4432 [
4433 'user_name' => $this->mName,
4434 'user_password' => $noPass,
4435 'user_newpassword' => $noPass,
4436 'user_email' => $this->mEmail,
4437 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
4438 'user_real_name' => $this->mRealName,
4439 'user_token' => strval( $this->mToken ),
4440 'user_registration' => $dbw->timestamp( $this->mRegistration ),
4441 'user_editcount' => 0,
4442 'user_touched' => $dbw->timestamp( $this->mTouched ),
4443 ], $fname,
4444 [ 'IGNORE' ]
4445 );
4446 if ( !$dbw->affectedRows() ) {
4447 // Use locking reads to bypass any REPEATABLE-READ snapshot.
4448 $this->mId = $dbw->selectField(
4449 'user',
4450 'user_id',
4451 [ 'user_name' => $this->mName ],
4452 $fname,
4453 [ 'LOCK IN SHARE MODE' ]
4454 );
4455 $loaded = false;
4456 if ( $this->mId ) {
4457 if ( $this->loadFromDatabase( self::READ_LOCKING ) ) {
4458 $loaded = true;
4459 }
4460 }
4461 if ( !$loaded ) {
4462 throw new MWException( $fname . ": hit a key conflict attempting " .
4463 "to insert user '{$this->mName}' row, but it was not present in select!" );
4464 }
4465 return Status::newFatal( 'userexists' );
4466 }
4467 $this->mId = $dbw->insertId();
4468 self::$idCacheByName[$this->mName] = $this->mId;
4469 $this->updateActorId( $dbw );
4470
4471 return Status::newGood();
4472 } );
4473 if ( !$status->isGood() ) {
4474 return $status;
4475 }
4476
4477 // Clear instance cache other than user table data and actor, which is already accurate
4478 $this->clearInstanceCache();
4479
4480 $this->saveOptions();
4481 return Status::newGood();
4482 }
4483
4484 /**
4485 * Update the actor ID after an insert
4486 * @param IDatabase $dbw Writable database handle
4487 */
4488 private function updateActorId( IDatabase $dbw ) {
4489 global $wgActorTableSchemaMigrationStage;
4490
4491 if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
4492 $dbw->insert(
4493 'actor',
4494 [ 'actor_user' => $this->mId, 'actor_name' => $this->mName ],
4495 __METHOD__
4496 );
4497 $this->mActorId = (int)$dbw->insertId();
4498 }
4499 }
4500
4501 /**
4502 * If this user is logged-in and blocked,
4503 * block any IP address they've successfully logged in from.
4504 * @return bool A block was spread
4505 */
4506 public function spreadAnyEditBlock() {
4507 if ( $this->isLoggedIn() && $this->isBlocked() ) {
4508 return $this->spreadBlock();
4509 }
4510
4511 return false;
4512 }
4513
4514 /**
4515 * If this (non-anonymous) user is blocked,
4516 * block the IP address they've successfully logged in from.
4517 * @return bool A block was spread
4518 */
4519 protected function spreadBlock() {
4520 wfDebug( __METHOD__ . "()\n" );
4521 $this->load();
4522 if ( $this->mId == 0 ) {
4523 return false;
4524 }
4525
4526 $userblock = Block::newFromTarget( $this->getName() );
4527 if ( !$userblock ) {
4528 return false;
4529 }
4530
4531 return (bool)$userblock->doAutoblock( $this->getRequest()->getIP() );
4532 }
4533
4534 /**
4535 * Get whether the user is explicitly blocked from account creation.
4536 * @return bool|Block
4537 */
4538 public function isBlockedFromCreateAccount() {
4539 $this->getBlockedStatus();
4540 if ( $this->mBlock && $this->mBlock->prevents( 'createaccount' ) ) {
4541 return $this->mBlock;
4542 }
4543
4544 # T15611: if the IP address the user is trying to create an account from is
4545 # blocked with createaccount disabled, prevent new account creation there even
4546 # when the user is logged in
4547 if ( $this->mBlockedFromCreateAccount === false && !$this->isAllowed( 'ipblock-exempt' ) ) {
4548 $this->mBlockedFromCreateAccount = Block::newFromTarget( null, $this->getRequest()->getIP() );
4549 }
4550 return $this->mBlockedFromCreateAccount instanceof Block
4551 && $this->mBlockedFromCreateAccount->prevents( 'createaccount' )
4552 ? $this->mBlockedFromCreateAccount
4553 : false;
4554 }
4555
4556 /**
4557 * Get whether the user is blocked from using Special:Emailuser.
4558 * @return bool
4559 */
4560 public function isBlockedFromEmailuser() {
4561 $this->getBlockedStatus();
4562 return $this->mBlock && $this->mBlock->prevents( 'sendemail' );
4563 }
4564
4565 /**
4566 * Get whether the user is blocked from using Special:Upload
4567 *
4568 * @since 1.33
4569 * @return bool
4570 */
4571 public function isBlockedFromUpload() {
4572 $this->getBlockedStatus();
4573 return $this->mBlock && $this->mBlock->prevents( 'upload' );
4574 }
4575
4576 /**
4577 * Get whether the user is allowed to create an account.
4578 * @return bool
4579 */
4580 public function isAllowedToCreateAccount() {
4581 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
4582 }
4583
4584 /**
4585 * Get this user's personal page title.
4586 *
4587 * @return Title User's personal page title
4588 */
4589 public function getUserPage() {
4590 return Title::makeTitle( NS_USER, $this->getName() );
4591 }
4592
4593 /**
4594 * Get this user's talk page title.
4595 *
4596 * @return Title User's talk page title
4597 */
4598 public function getTalkPage() {
4599 $title = $this->getUserPage();
4600 return $title->getTalkPage();
4601 }
4602
4603 /**
4604 * Determine whether the user is a newbie. Newbies are either
4605 * anonymous IPs, or the most recently created accounts.
4606 * @return bool
4607 */
4608 public function isNewbie() {
4609 return !$this->isAllowed( 'autoconfirmed' );
4610 }
4611
4612 /**
4613 * Check to see if the given clear-text password is one of the accepted passwords
4614 * @deprecated since 1.27, use AuthManager instead
4615 * @param string $password User password
4616 * @return bool True if the given password is correct, otherwise False
4617 */
4618 public function checkPassword( $password ) {
4619 wfDeprecated( __METHOD__, '1.27' );
4620
4621 $manager = AuthManager::singleton();
4622 $reqs = AuthenticationRequest::loadRequestsFromSubmission(
4623 $manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN ),
4624 [
4625 'username' => $this->getName(),
4626 'password' => $password,
4627 ]
4628 );
4629 $res = AuthManager::singleton()->beginAuthentication( $reqs, 'null:' );
4630 switch ( $res->status ) {
4631 case AuthenticationResponse::PASS:
4632 return true;
4633 case AuthenticationResponse::FAIL:
4634 // Hope it's not a PreAuthenticationProvider that failed...
4635 \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
4636 ->info( __METHOD__ . ': Authentication failed: ' . $res->message->plain() );
4637 return false;
4638 default:
4639 throw new BadMethodCallException(
4640 'AuthManager returned a response unsupported by ' . __METHOD__
4641 );
4642 }
4643 }
4644
4645 /**
4646 * Check if the given clear-text password matches the temporary password
4647 * sent by e-mail for password reset operations.
4648 *
4649 * @deprecated since 1.27, use AuthManager instead
4650 * @param string $plaintext
4651 * @return bool True if matches, false otherwise
4652 */
4653 public function checkTemporaryPassword( $plaintext ) {
4654 wfDeprecated( __METHOD__, '1.27' );
4655 // Can't check the temporary password individually.
4656 return $this->checkPassword( $plaintext );
4657 }
4658
4659 /**
4660 * Initialize (if necessary) and return a session token value
4661 * which can be used in edit forms to show that the user's
4662 * login credentials aren't being hijacked with a foreign form
4663 * submission.
4664 *
4665 * @since 1.27
4666 * @param string|array $salt Array of Strings Optional function-specific data for hashing
4667 * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest
4668 * @return MediaWiki\Session\Token The new edit token
4669 */
4670 public function getEditTokenObject( $salt = '', $request = null ) {
4671 if ( $this->isAnon() ) {
4672 return new LoggedOutEditToken();
4673 }
4674
4675 if ( !$request ) {
4676 $request = $this->getRequest();
4677 }
4678 return $request->getSession()->getToken( $salt );
4679 }
4680
4681 /**
4682 * Initialize (if necessary) and return a session token value
4683 * which can be used in edit forms to show that the user's
4684 * login credentials aren't being hijacked with a foreign form
4685 * submission.
4686 *
4687 * The $salt for 'edit' and 'csrf' tokens is the default (empty string).
4688 *
4689 * @since 1.19
4690 * @param string|array $salt Array of Strings Optional function-specific data for hashing
4691 * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest
4692 * @return string The new edit token
4693 */
4694 public function getEditToken( $salt = '', $request = null ) {
4695 return $this->getEditTokenObject( $salt, $request )->toString();
4696 }
4697
4698 /**
4699 * Check given value against the token value stored in the session.
4700 * A match should confirm that the form was submitted from the
4701 * user's own login session, not a form submission from a third-party
4702 * site.
4703 *
4704 * @param string $val Input value to compare
4705 * @param string|array $salt Optional function-specific data for hashing
4706 * @param WebRequest|null $request Object to use or null to use $wgRequest
4707 * @param int|null $maxage Fail tokens older than this, in seconds
4708 * @return bool Whether the token matches
4709 */
4710 public function matchEditToken( $val, $salt = '', $request = null, $maxage = null ) {
4711 return $this->getEditTokenObject( $salt, $request )->match( $val, $maxage );
4712 }
4713
4714 /**
4715 * Check given value against the token value stored in the session,
4716 * ignoring the suffix.
4717 *
4718 * @param string $val Input value to compare
4719 * @param string|array $salt Optional function-specific data for hashing
4720 * @param WebRequest|null $request Object to use or null to use $wgRequest
4721 * @param int|null $maxage Fail tokens older than this, in seconds
4722 * @return bool Whether the token matches
4723 */
4724 public function matchEditTokenNoSuffix( $val, $salt = '', $request = null, $maxage = null ) {
4725 $val = substr( $val, 0, strspn( $val, '0123456789abcdef' ) ) . Token::SUFFIX;
4726 return $this->matchEditToken( $val, $salt, $request, $maxage );
4727 }
4728
4729 /**
4730 * Generate a new e-mail confirmation token and send a confirmation/invalidation
4731 * mail to the user's given address.
4732 *
4733 * @param string $type Message to send, either "created", "changed" or "set"
4734 * @return Status
4735 */
4736 public function sendConfirmationMail( $type = 'created' ) {
4737 global $wgLang;
4738 $expiration = null; // gets passed-by-ref and defined in next line.
4739 $token = $this->confirmationToken( $expiration );
4740 $url = $this->confirmationTokenUrl( $token );
4741 $invalidateURL = $this->invalidationTokenUrl( $token );
4742 $this->saveSettings();
4743
4744 if ( $type == 'created' || $type === false ) {
4745 $message = 'confirmemail_body';
4746 } elseif ( $type === true ) {
4747 $message = 'confirmemail_body_changed';
4748 } else {
4749 // Messages: confirmemail_body_changed, confirmemail_body_set
4750 $message = 'confirmemail_body_' . $type;
4751 }
4752
4753 return $this->sendMail( wfMessage( 'confirmemail_subject' )->text(),
4754 wfMessage( $message,
4755 $this->getRequest()->getIP(),
4756 $this->getName(),
4757 $url,
4758 $wgLang->userTimeAndDate( $expiration, $this ),
4759 $invalidateURL,
4760 $wgLang->userDate( $expiration, $this ),
4761 $wgLang->userTime( $expiration, $this ) )->text() );
4762 }
4763
4764 /**
4765 * Send an e-mail to this user's account. Does not check for
4766 * confirmed status or validity.
4767 *
4768 * @param string $subject Message subject
4769 * @param string $body Message body
4770 * @param User|null $from Optional sending user; if unspecified, default
4771 * $wgPasswordSender will be used.
4772 * @param string|null $replyto Reply-To address
4773 * @return Status
4774 */
4775 public function sendMail( $subject, $body, $from = null, $replyto = null ) {
4776 global $wgPasswordSender;
4777
4778 if ( $from instanceof User ) {
4779 $sender = MailAddress::newFromUser( $from );
4780 } else {
4781 $sender = new MailAddress( $wgPasswordSender,
4782 wfMessage( 'emailsender' )->inContentLanguage()->text() );
4783 }
4784 $to = MailAddress::newFromUser( $this );
4785
4786 return UserMailer::send( $to, $sender, $subject, $body, [
4787 'replyTo' => $replyto,
4788 ] );
4789 }
4790
4791 /**
4792 * Generate, store, and return a new e-mail confirmation code.
4793 * A hash (unsalted, since it's used as a key) is stored.
4794 *
4795 * @note Call saveSettings() after calling this function to commit
4796 * this change to the database.
4797 *
4798 * @param string &$expiration Accepts the expiration time
4799 * @return string New token
4800 */
4801 protected function confirmationToken( &$expiration ) {
4802 global $wgUserEmailConfirmationTokenExpiry;
4803 $now = time();
4804 $expires = $now + $wgUserEmailConfirmationTokenExpiry;
4805 $expiration = wfTimestamp( TS_MW, $expires );
4806 $this->load();
4807 $token = MWCryptRand::generateHex( 32 );
4808 $hash = md5( $token );
4809 $this->mEmailToken = $hash;
4810 $this->mEmailTokenExpires = $expiration;
4811 return $token;
4812 }
4813
4814 /**
4815 * Return a URL the user can use to confirm their email address.
4816 * @param string $token Accepts the email confirmation token
4817 * @return string New token URL
4818 */
4819 protected function confirmationTokenUrl( $token ) {
4820 return $this->getTokenUrl( 'ConfirmEmail', $token );
4821 }
4822
4823 /**
4824 * Return a URL the user can use to invalidate their email address.
4825 * @param string $token Accepts the email confirmation token
4826 * @return string New token URL
4827 */
4828 protected function invalidationTokenUrl( $token ) {
4829 return $this->getTokenUrl( 'InvalidateEmail', $token );
4830 }
4831
4832 /**
4833 * Internal function to format the e-mail validation/invalidation URLs.
4834 * This uses a quickie hack to use the
4835 * hardcoded English names of the Special: pages, for ASCII safety.
4836 *
4837 * @note Since these URLs get dropped directly into emails, using the
4838 * short English names avoids insanely long URL-encoded links, which
4839 * also sometimes can get corrupted in some browsers/mailers
4840 * (T8957 with Gmail and Internet Explorer).
4841 *
4842 * @param string $page Special page
4843 * @param string $token
4844 * @return string Formatted URL
4845 */
4846 protected function getTokenUrl( $page, $token ) {
4847 // Hack to bypass localization of 'Special:'
4848 $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" );
4849 return $title->getCanonicalURL();
4850 }
4851
4852 /**
4853 * Mark the e-mail address confirmed.
4854 *
4855 * @note Call saveSettings() after calling this function to commit the change.
4856 *
4857 * @return bool
4858 */
4859 public function confirmEmail() {
4860 // Check if it's already confirmed, so we don't touch the database
4861 // and fire the ConfirmEmailComplete hook on redundant confirmations.
4862 if ( !$this->isEmailConfirmed() ) {
4863 $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
4864 Hooks::run( 'ConfirmEmailComplete', [ $this ] );
4865 }
4866 return true;
4867 }
4868
4869 /**
4870 * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
4871 * address if it was already confirmed.
4872 *
4873 * @note Call saveSettings() after calling this function to commit the change.
4874 * @return bool Returns true
4875 */
4876 public function invalidateEmail() {
4877 $this->load();
4878 $this->mEmailToken = null;
4879 $this->mEmailTokenExpires = null;
4880 $this->setEmailAuthenticationTimestamp( null );
4881 $this->mEmail = '';
4882 Hooks::run( 'InvalidateEmailComplete', [ $this ] );
4883 return true;
4884 }
4885
4886 /**
4887 * Set the e-mail authentication timestamp.
4888 * @param string $timestamp TS_MW timestamp
4889 */
4890 public function setEmailAuthenticationTimestamp( $timestamp ) {
4891 $this->load();
4892 $this->mEmailAuthenticated = $timestamp;
4893 Hooks::run( 'UserSetEmailAuthenticationTimestamp', [ $this, &$this->mEmailAuthenticated ] );
4894 }
4895
4896 /**
4897 * Is this user allowed to send e-mails within limits of current
4898 * site configuration?
4899 * @return bool
4900 */
4901 public function canSendEmail() {
4902 global $wgEnableEmail, $wgEnableUserEmail;
4903 if ( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
4904 return false;
4905 }
4906 $canSend = $this->isEmailConfirmed();
4907 // Avoid PHP 7.1 warning of passing $this by reference
4908 $user = $this;
4909 Hooks::run( 'UserCanSendEmail', [ &$user, &$canSend ] );
4910 return $canSend;
4911 }
4912
4913 /**
4914 * Is this user allowed to receive e-mails within limits of current
4915 * site configuration?
4916 * @return bool
4917 */
4918 public function canReceiveEmail() {
4919 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
4920 }
4921
4922 /**
4923 * Is this user's e-mail address valid-looking and confirmed within
4924 * limits of the current site configuration?
4925 *
4926 * @note If $wgEmailAuthentication is on, this may require the user to have
4927 * confirmed their address by returning a code or using a password
4928 * sent to the address from the wiki.
4929 *
4930 * @return bool
4931 */
4932 public function isEmailConfirmed() {
4933 global $wgEmailAuthentication;
4934 $this->load();
4935 // Avoid PHP 7.1 warning of passing $this by reference
4936 $user = $this;
4937 $confirmed = true;
4938 if ( Hooks::run( 'EmailConfirmed', [ &$user, &$confirmed ] ) ) {
4939 if ( $this->isAnon() ) {
4940 return false;
4941 }
4942 if ( !Sanitizer::validateEmail( $this->mEmail ) ) {
4943 return false;
4944 }
4945 if ( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) {
4946 return false;
4947 }
4948 return true;
4949 }
4950
4951 return $confirmed;
4952 }
4953
4954 /**
4955 * Check whether there is an outstanding request for e-mail confirmation.
4956 * @return bool
4957 */
4958 public function isEmailConfirmationPending() {
4959 global $wgEmailAuthentication;
4960 return $wgEmailAuthentication &&
4961 !$this->isEmailConfirmed() &&
4962 $this->mEmailToken &&
4963 $this->mEmailTokenExpires > wfTimestamp();
4964 }
4965
4966 /**
4967 * Get the timestamp of account creation.
4968 *
4969 * @return string|bool|null Timestamp of account creation, false for
4970 * non-existent/anonymous user accounts, or null if existing account
4971 * but information is not in database.
4972 */
4973 public function getRegistration() {
4974 if ( $this->isAnon() ) {
4975 return false;
4976 }
4977 $this->load();
4978 return $this->mRegistration;
4979 }
4980
4981 /**
4982 * Get the timestamp of the first edit
4983 *
4984 * @return string|bool Timestamp of first edit, or false for
4985 * non-existent/anonymous user accounts.
4986 */
4987 public function getFirstEditTimestamp() {
4988 if ( $this->getId() == 0 ) {
4989 return false; // anons
4990 }
4991 $dbr = wfGetDB( DB_REPLICA );
4992 $actorWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $this );
4993 $tsField = isset( $actorWhere['tables']['temp_rev_user'] )
4994 ? 'revactor_timestamp' : 'rev_timestamp';
4995 $time = $dbr->selectField(
4996 [ 'revision' ] + $actorWhere['tables'],
4997 $tsField,
4998 [ $actorWhere['conds'] ],
4999 __METHOD__,
5000 [ 'ORDER BY' => "$tsField ASC" ],
5001 $actorWhere['joins']
5002 );
5003 if ( !$time ) {
5004 return false; // no edits
5005 }
5006 return wfTimestamp( TS_MW, $time );
5007 }
5008
5009 /**
5010 * Get the permissions associated with a given list of groups
5011 *
5012 * @param array $groups Array of Strings List of internal group names
5013 * @return array Array of Strings List of permission key names for given groups combined
5014 */
5015 public static function getGroupPermissions( $groups ) {
5016 global $wgGroupPermissions, $wgRevokePermissions;
5017 $rights = [];
5018 // grant every granted permission first
5019 foreach ( $groups as $group ) {
5020 if ( isset( $wgGroupPermissions[$group] ) ) {
5021 $rights = array_merge( $rights,
5022 // array_filter removes empty items
5023 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
5024 }
5025 }
5026 // now revoke the revoked permissions
5027 foreach ( $groups as $group ) {
5028 if ( isset( $wgRevokePermissions[$group] ) ) {
5029 $rights = array_diff( $rights,
5030 array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
5031 }
5032 }
5033 return array_unique( $rights );
5034 }
5035
5036 /**
5037 * Get all the groups who have a given permission
5038 *
5039 * @param string $role Role to check
5040 * @return array Array of Strings List of internal group names with the given permission
5041 */
5042 public static function getGroupsWithPermission( $role ) {
5043 global $wgGroupPermissions;
5044 $allowedGroups = [];
5045 foreach ( array_keys( $wgGroupPermissions ) as $group ) {
5046 if ( self::groupHasPermission( $group, $role ) ) {
5047 $allowedGroups[] = $group;
5048 }
5049 }
5050 return $allowedGroups;
5051 }
5052
5053 /**
5054 * Check, if the given group has the given permission
5055 *
5056 * If you're wanting to check whether all users have a permission, use
5057 * User::isEveryoneAllowed() instead. That properly checks if it's revoked
5058 * from anyone.
5059 *
5060 * @since 1.21
5061 * @param string $group Group to check
5062 * @param string $role Role to check
5063 * @return bool
5064 */
5065 public static function groupHasPermission( $group, $role ) {
5066 global $wgGroupPermissions, $wgRevokePermissions;
5067 return isset( $wgGroupPermissions[$group][$role] ) && $wgGroupPermissions[$group][$role]
5068 && !( isset( $wgRevokePermissions[$group][$role] ) && $wgRevokePermissions[$group][$role] );
5069 }
5070
5071 /**
5072 * Check if all users may be assumed to have the given permission
5073 *
5074 * We generally assume so if the right is granted to '*' and isn't revoked
5075 * on any group. It doesn't attempt to take grants or other extension
5076 * limitations on rights into account in the general case, though, as that
5077 * would require it to always return false and defeat the purpose.
5078 * Specifically, session-based rights restrictions (such as OAuth or bot
5079 * passwords) are applied based on the current session.
5080 *
5081 * @since 1.22
5082 * @param string $right Right to check
5083 * @return bool
5084 */
5085 public static function isEveryoneAllowed( $right ) {
5086 global $wgGroupPermissions, $wgRevokePermissions;
5087 static $cache = [];
5088
5089 // Use the cached results, except in unit tests which rely on
5090 // being able change the permission mid-request
5091 if ( isset( $cache[$right] ) && !defined( 'MW_PHPUNIT_TEST' ) ) {
5092 return $cache[$right];
5093 }
5094
5095 if ( !isset( $wgGroupPermissions['*'][$right] ) || !$wgGroupPermissions['*'][$right] ) {
5096 $cache[$right] = false;
5097 return false;
5098 }
5099
5100 // If it's revoked anywhere, then everyone doesn't have it
5101 foreach ( $wgRevokePermissions as $rights ) {
5102 if ( isset( $rights[$right] ) && $rights[$right] ) {
5103 $cache[$right] = false;
5104 return false;
5105 }
5106 }
5107
5108 // Remove any rights that aren't allowed to the global-session user,
5109 // unless there are no sessions for this endpoint.
5110 if ( !defined( 'MW_NO_SESSION' ) ) {
5111 $allowedRights = SessionManager::getGlobalSession()->getAllowedUserRights();
5112 if ( $allowedRights !== null && !in_array( $right, $allowedRights, true ) ) {
5113 $cache[$right] = false;
5114 return false;
5115 }
5116 }
5117
5118 // Allow extensions to say false
5119 if ( !Hooks::run( 'UserIsEveryoneAllowed', [ $right ] ) ) {
5120 $cache[$right] = false;
5121 return false;
5122 }
5123
5124 $cache[$right] = true;
5125 return true;
5126 }
5127
5128 /**
5129 * Get the localized descriptive name for a group, if it exists
5130 * @deprecated since 1.29 Use UserGroupMembership::getGroupName instead
5131 *
5132 * @param string $group Internal group name
5133 * @return string Localized descriptive group name
5134 */
5135 public static function getGroupName( $group ) {
5136 wfDeprecated( __METHOD__, '1.29' );
5137 return UserGroupMembership::getGroupName( $group );
5138 }
5139
5140 /**
5141 * Get the localized descriptive name for a member of a group, if it exists
5142 * @deprecated since 1.29 Use UserGroupMembership::getGroupMemberName instead
5143 *
5144 * @param string $group Internal group name
5145 * @param string $username Username for gender (since 1.19)
5146 * @return string Localized name for group member
5147 */
5148 public static function getGroupMember( $group, $username = '#' ) {
5149 wfDeprecated( __METHOD__, '1.29' );
5150 return UserGroupMembership::getGroupMemberName( $group, $username );
5151 }
5152
5153 /**
5154 * Return the set of defined explicit groups.
5155 * The implicit groups (by default *, 'user' and 'autoconfirmed')
5156 * are not included, as they are defined automatically, not in the database.
5157 * @return array Array of internal group names
5158 */
5159 public static function getAllGroups() {
5160 global $wgGroupPermissions, $wgRevokePermissions;
5161 return array_values( array_diff(
5162 array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
5163 self::getImplicitGroups()
5164 ) );
5165 }
5166
5167 /**
5168 * Get a list of all available permissions.
5169 * @return string[] Array of permission names
5170 */
5171 public static function getAllRights() {
5172 if ( self::$mAllRights === false ) {
5173 global $wgAvailableRights;
5174 if ( count( $wgAvailableRights ) ) {
5175 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
5176 } else {
5177 self::$mAllRights = self::$mCoreRights;
5178 }
5179 Hooks::run( 'UserGetAllRights', [ &self::$mAllRights ] );
5180 }
5181 return self::$mAllRights;
5182 }
5183
5184 /**
5185 * Get a list of implicit groups
5186 * TODO: Should we deprecate this? It's trivial, but we don't want to encourage use of globals.
5187 *
5188 * @return array Array of Strings Array of internal group names
5189 */
5190 public static function getImplicitGroups() {
5191 global $wgImplicitGroups;
5192 return $wgImplicitGroups;
5193 }
5194
5195 /**
5196 * Get the title of a page describing a particular group
5197 * @deprecated since 1.29 Use UserGroupMembership::getGroupPage instead
5198 *
5199 * @param string $group Internal group name
5200 * @return Title|bool Title of the page if it exists, false otherwise
5201 */
5202 public static function getGroupPage( $group ) {
5203 wfDeprecated( __METHOD__, '1.29' );
5204 return UserGroupMembership::getGroupPage( $group );
5205 }
5206
5207 /**
5208 * Create a link to the group in HTML, if available;
5209 * else return the group name.
5210 * @deprecated since 1.29 Use UserGroupMembership::getLink instead, or
5211 * make the link yourself if you need custom text
5212 *
5213 * @param string $group Internal name of the group
5214 * @param string $text The text of the link
5215 * @return string HTML link to the group
5216 */
5217 public static function makeGroupLinkHTML( $group, $text = '' ) {
5218 wfDeprecated( __METHOD__, '1.29' );
5219
5220 if ( $text == '' ) {
5221 $text = UserGroupMembership::getGroupName( $group );
5222 }
5223 $title = UserGroupMembership::getGroupPage( $group );
5224 if ( $title ) {
5225 return MediaWikiServices::getInstance()
5226 ->getLinkRenderer()->makeLink( $title, $text );
5227 }
5228
5229 return htmlspecialchars( $text );
5230 }
5231
5232 /**
5233 * Create a link to the group in Wikitext, if available;
5234 * else return the group name.
5235 * @deprecated since 1.29 Use UserGroupMembership::getLink instead, or
5236 * make the link yourself if you need custom text
5237 *
5238 * @param string $group Internal name of the group
5239 * @param string $text The text of the link
5240 * @return string Wikilink to the group
5241 */
5242 public static function makeGroupLinkWiki( $group, $text = '' ) {
5243 wfDeprecated( __METHOD__, '1.29' );
5244
5245 if ( $text == '' ) {
5246 $text = UserGroupMembership::getGroupName( $group );
5247 }
5248 $title = UserGroupMembership::getGroupPage( $group );
5249 if ( $title ) {
5250 $page = $title->getFullText();
5251 return "[[$page|$text]]";
5252 }
5253
5254 return $text;
5255 }
5256
5257 /**
5258 * Returns an array of the groups that a particular group can add/remove.
5259 *
5260 * @param string $group The group to check for whether it can add/remove
5261 * @return array Array( 'add' => array( addablegroups ),
5262 * 'remove' => array( removablegroups ),
5263 * 'add-self' => array( addablegroups to self),
5264 * 'remove-self' => array( removable groups from self) )
5265 */
5266 public static function changeableByGroup( $group ) {
5267 global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
5268
5269 $groups = [
5270 'add' => [],
5271 'remove' => [],
5272 'add-self' => [],
5273 'remove-self' => []
5274 ];
5275
5276 if ( empty( $wgAddGroups[$group] ) ) {
5277 // Don't add anything to $groups
5278 } elseif ( $wgAddGroups[$group] === true ) {
5279 // You get everything
5280 $groups['add'] = self::getAllGroups();
5281 } elseif ( is_array( $wgAddGroups[$group] ) ) {
5282 $groups['add'] = $wgAddGroups[$group];
5283 }
5284
5285 // Same thing for remove
5286 if ( empty( $wgRemoveGroups[$group] ) ) {
5287 // Do nothing
5288 } elseif ( $wgRemoveGroups[$group] === true ) {
5289 $groups['remove'] = self::getAllGroups();
5290 } elseif ( is_array( $wgRemoveGroups[$group] ) ) {
5291 $groups['remove'] = $wgRemoveGroups[$group];
5292 }
5293
5294 // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
5295 if ( empty( $wgGroupsAddToSelf['user'] ) || $wgGroupsAddToSelf['user'] !== true ) {
5296 foreach ( $wgGroupsAddToSelf as $key => $value ) {
5297 if ( is_int( $key ) ) {
5298 $wgGroupsAddToSelf['user'][] = $value;
5299 }
5300 }
5301 }
5302
5303 if ( empty( $wgGroupsRemoveFromSelf['user'] ) || $wgGroupsRemoveFromSelf['user'] !== true ) {
5304 foreach ( $wgGroupsRemoveFromSelf as $key => $value ) {
5305 if ( is_int( $key ) ) {
5306 $wgGroupsRemoveFromSelf['user'][] = $value;
5307 }
5308 }
5309 }
5310
5311 // Now figure out what groups the user can add to him/herself
5312 if ( empty( $wgGroupsAddToSelf[$group] ) ) {
5313 // Do nothing
5314 } elseif ( $wgGroupsAddToSelf[$group] === true ) {
5315 // No idea WHY this would be used, but it's there
5316 $groups['add-self'] = self::getAllGroups();
5317 } elseif ( is_array( $wgGroupsAddToSelf[$group] ) ) {
5318 $groups['add-self'] = $wgGroupsAddToSelf[$group];
5319 }
5320
5321 if ( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
5322 // Do nothing
5323 } elseif ( $wgGroupsRemoveFromSelf[$group] === true ) {
5324 $groups['remove-self'] = self::getAllGroups();
5325 } elseif ( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
5326 $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
5327 }
5328
5329 return $groups;
5330 }
5331
5332 /**
5333 * Returns an array of groups that this user can add and remove
5334 * @return array Array( 'add' => array( addablegroups ),
5335 * 'remove' => array( removablegroups ),
5336 * 'add-self' => array( addablegroups to self),
5337 * 'remove-self' => array( removable groups from self) )
5338 */
5339 public function changeableGroups() {
5340 if ( $this->isAllowed( 'userrights' ) ) {
5341 // This group gives the right to modify everything (reverse-
5342 // compatibility with old "userrights lets you change
5343 // everything")
5344 // Using array_merge to make the groups reindexed
5345 $all = array_merge( self::getAllGroups() );
5346 return [
5347 'add' => $all,
5348 'remove' => $all,
5349 'add-self' => [],
5350 'remove-self' => []
5351 ];
5352 }
5353
5354 // Okay, it's not so simple, we will have to go through the arrays
5355 $groups = [
5356 'add' => [],
5357 'remove' => [],
5358 'add-self' => [],
5359 'remove-self' => []
5360 ];
5361 $addergroups = $this->getEffectiveGroups();
5362
5363 foreach ( $addergroups as $addergroup ) {
5364 $groups = array_merge_recursive(
5365 $groups, $this->changeableByGroup( $addergroup )
5366 );
5367 $groups['add'] = array_unique( $groups['add'] );
5368 $groups['remove'] = array_unique( $groups['remove'] );
5369 $groups['add-self'] = array_unique( $groups['add-self'] );
5370 $groups['remove-self'] = array_unique( $groups['remove-self'] );
5371 }
5372 return $groups;
5373 }
5374
5375 /**
5376 * Schedule a deferred update to update the user's edit count
5377 */
5378 public function incEditCount() {
5379 if ( $this->isAnon() ) {
5380 return; // sanity
5381 }
5382
5383 DeferredUpdates::addUpdate(
5384 new UserEditCountUpdate( $this, 1 ),
5385 DeferredUpdates::POSTSEND
5386 );
5387 }
5388
5389 /**
5390 * This method should not be called outside User/UserEditCountUpdate
5391 *
5392 * @param int $count
5393 */
5394 public function setEditCountInternal( $count ) {
5395 $this->mEditCount = $count;
5396 }
5397
5398 /**
5399 * Initialize user_editcount from data out of the revision table
5400 *
5401 * This method should not be called outside User/UserEditCountUpdate
5402 *
5403 * @return int Number of edits
5404 */
5405 public function initEditCountInternal() {
5406 // Pull from a replica DB to be less cruel to servers
5407 // Accuracy isn't the point anyway here
5408 $dbr = wfGetDB( DB_REPLICA );
5409 $actorWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $this );
5410 $count = (int)$dbr->selectField(
5411 [ 'revision' ] + $actorWhere['tables'],
5412 'COUNT(*)',
5413 [ $actorWhere['conds'] ],
5414 __METHOD__,
5415 [],
5416 $actorWhere['joins']
5417 );
5418
5419 $dbw = wfGetDB( DB_MASTER );
5420 $dbw->update(
5421 'user',
5422 [ 'user_editcount' => $count ],
5423 [
5424 'user_id' => $this->getId(),
5425 'user_editcount IS NULL OR user_editcount < ' . (int)$count
5426 ],
5427 __METHOD__
5428 );
5429
5430 return $count;
5431 }
5432
5433 /**
5434 * Get the description of a given right
5435 *
5436 * @since 1.29
5437 * @param string $right Right to query
5438 * @return string Localized description of the right
5439 */
5440 public static function getRightDescription( $right ) {
5441 $key = "right-$right";
5442 $msg = wfMessage( $key );
5443 return $msg->isDisabled() ? $right : $msg->text();
5444 }
5445
5446 /**
5447 * Get the name of a given grant
5448 *
5449 * @since 1.29
5450 * @param string $grant Grant to query
5451 * @return string Localized name of the grant
5452 */
5453 public static function getGrantName( $grant ) {
5454 $key = "grant-$grant";
5455 $msg = wfMessage( $key );
5456 return $msg->isDisabled() ? $grant : $msg->text();
5457 }
5458
5459 /**
5460 * Add a newuser log entry for this user.
5461 * Before 1.19 the return value was always true.
5462 *
5463 * @deprecated since 1.27, AuthManager handles logging
5464 * @param string|bool $action Account creation type.
5465 * - String, one of the following values:
5466 * - 'create' for an anonymous user creating an account for himself.
5467 * This will force the action's performer to be the created user itself,
5468 * no matter the value of $wgUser
5469 * - 'create2' for a logged in user creating an account for someone else
5470 * - 'byemail' when the created user will receive its password by e-mail
5471 * - 'autocreate' when the user is automatically created (such as by CentralAuth).
5472 * - Boolean means whether the account was created by e-mail (deprecated):
5473 * - true will be converted to 'byemail'
5474 * - false will be converted to 'create' if this object is the same as
5475 * $wgUser and to 'create2' otherwise
5476 * @param string $reason User supplied reason
5477 * @return bool true
5478 */
5479 public function addNewUserLogEntry( $action = false, $reason = '' ) {
5480 return true; // disabled
5481 }
5482
5483 /**
5484 * Add an autocreate newuser log entry for this user
5485 * Used by things like CentralAuth and perhaps other authplugins.
5486 * Consider calling addNewUserLogEntry() directly instead.
5487 *
5488 * @deprecated since 1.27, AuthManager handles logging
5489 * @return bool
5490 */
5491 public function addNewUserLogEntryAutoCreate() {
5492 $this->addNewUserLogEntry( 'autocreate' );
5493
5494 return true;
5495 }
5496
5497 /**
5498 * Load the user options either from cache, the database or an array
5499 *
5500 * @param array|null $data Rows for the current user out of the user_properties table
5501 */
5502 protected function loadOptions( $data = null ) {
5503 $this->load();
5504
5505 if ( $this->mOptionsLoaded ) {
5506 return;
5507 }
5508
5509 $this->mOptions = self::getDefaultOptions();
5510
5511 if ( !$this->getId() ) {
5512 // For unlogged-in users, load language/variant options from request.
5513 // There's no need to do it for logged-in users: they can set preferences,
5514 // and handling of page content is done by $pageLang->getPreferredVariant() and such,
5515 // so don't override user's choice (especially when the user chooses site default).
5516 $variant = MediaWikiServices::getInstance()->getContentLanguage()->getDefaultVariant();
5517 $this->mOptions['variant'] = $variant;
5518 $this->mOptions['language'] = $variant;
5519 $this->mOptionsLoaded = true;
5520 return;
5521 }
5522
5523 // Maybe load from the object
5524 if ( !is_null( $this->mOptionOverrides ) ) {
5525 wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" );
5526 foreach ( $this->mOptionOverrides as $key => $value ) {
5527 $this->mOptions[$key] = $value;
5528 }
5529 } else {
5530 if ( !is_array( $data ) ) {
5531 wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" );
5532 // Load from database
5533 $dbr = ( $this->queryFlagsUsed & self::READ_LATEST )
5534 ? wfGetDB( DB_MASTER )
5535 : wfGetDB( DB_REPLICA );
5536
5537 $res = $dbr->select(
5538 'user_properties',
5539 [ 'up_property', 'up_value' ],
5540 [ 'up_user' => $this->getId() ],
5541 __METHOD__
5542 );
5543
5544 $this->mOptionOverrides = [];
5545 $data = [];
5546 foreach ( $res as $row ) {
5547 // Convert '0' to 0. PHP's boolean conversion considers them both
5548 // false, but e.g. JavaScript considers the former as true.
5549 // @todo: T54542 Somehow determine the desired type (string/int/bool)
5550 // and convert all values here.
5551 if ( $row->up_value === '0' ) {
5552 $row->up_value = 0;
5553 }
5554 $data[$row->up_property] = $row->up_value;
5555 }
5556 }
5557
5558 foreach ( $data as $property => $value ) {
5559 $this->mOptionOverrides[$property] = $value;
5560 $this->mOptions[$property] = $value;
5561 }
5562 }
5563
5564 // Replace deprecated language codes
5565 $this->mOptions['language'] = LanguageCode::replaceDeprecatedCodes(
5566 $this->mOptions['language']
5567 );
5568
5569 $this->mOptionsLoaded = true;
5570
5571 Hooks::run( 'UserLoadOptions', [ $this, &$this->mOptions ] );
5572 }
5573
5574 /**
5575 * Saves the non-default options for this user, as previously set e.g. via
5576 * setOption(), in the database's "user_properties" (preferences) table.
5577 * Usually used via saveSettings().
5578 */
5579 protected function saveOptions() {
5580 $this->loadOptions();
5581
5582 // Not using getOptions(), to keep hidden preferences in database
5583 $saveOptions = $this->mOptions;
5584
5585 // Allow hooks to abort, for instance to save to a global profile.
5586 // Reset options to default state before saving.
5587 if ( !Hooks::run( 'UserSaveOptions', [ $this, &$saveOptions ] ) ) {
5588 return;
5589 }
5590
5591 $userId = $this->getId();
5592
5593 $insert_rows = []; // all the new preference rows
5594 foreach ( $saveOptions as $key => $value ) {
5595 // Don't bother storing default values
5596 $defaultOption = self::getDefaultOption( $key );
5597 if ( ( $defaultOption === null && $value !== false && $value !== null )
5598 || $value != $defaultOption
5599 ) {
5600 $insert_rows[] = [
5601 'up_user' => $userId,
5602 'up_property' => $key,
5603 'up_value' => $value,
5604 ];
5605 }
5606 }
5607
5608 $dbw = wfGetDB( DB_MASTER );
5609
5610 $res = $dbw->select( 'user_properties',
5611 [ 'up_property', 'up_value' ], [ 'up_user' => $userId ], __METHOD__ );
5612
5613 // Find prior rows that need to be removed or updated. These rows will
5614 // all be deleted (the latter so that INSERT IGNORE applies the new values).
5615 $keysDelete = [];
5616 foreach ( $res as $row ) {
5617 if ( !isset( $saveOptions[$row->up_property] )
5618 || strcmp( $saveOptions[$row->up_property], $row->up_value ) != 0
5619 ) {
5620 $keysDelete[] = $row->up_property;
5621 }
5622 }
5623
5624 if ( count( $keysDelete ) ) {
5625 // Do the DELETE by PRIMARY KEY for prior rows.
5626 // In the past a very large portion of calls to this function are for setting
5627 // 'rememberpassword' for new accounts (a preference that has since been removed).
5628 // Doing a blanket per-user DELETE for new accounts with no rows in the table
5629 // caused gap locks on [max user ID,+infinity) which caused high contention since
5630 // updates would pile up on each other as they are for higher (newer) user IDs.
5631 // It might not be necessary these days, but it shouldn't hurt either.
5632 $dbw->delete( 'user_properties',
5633 [ 'up_user' => $userId, 'up_property' => $keysDelete ], __METHOD__ );
5634 }
5635 // Insert the new preference rows
5636 $dbw->insert( 'user_properties', $insert_rows, __METHOD__, [ 'IGNORE' ] );
5637 }
5638
5639 /**
5640 * Return the list of user fields that should be selected to create
5641 * a new user object.
5642 * @deprecated since 1.31, use self::getQueryInfo() instead.
5643 * @return array
5644 */
5645 public static function selectFields() {
5646 wfDeprecated( __METHOD__, '1.31' );
5647 return [
5648 'user_id',
5649 'user_name',
5650 'user_real_name',
5651 'user_email',
5652 'user_touched',
5653 'user_token',
5654 'user_email_authenticated',
5655 'user_email_token',
5656 'user_email_token_expires',
5657 'user_registration',
5658 'user_editcount',
5659 ];
5660 }
5661
5662 /**
5663 * Return the tables, fields, and join conditions to be selected to create
5664 * a new user object.
5665 * @since 1.31
5666 * @return array With three keys:
5667 * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
5668 * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
5669 * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
5670 */
5671 public static function getQueryInfo() {
5672 global $wgActorTableSchemaMigrationStage;
5673
5674 $ret = [
5675 'tables' => [ 'user' ],
5676 'fields' => [
5677 'user_id',
5678 'user_name',
5679 'user_real_name',
5680 'user_email',
5681 'user_touched',
5682 'user_token',
5683 'user_email_authenticated',
5684 'user_email_token',
5685 'user_email_token_expires',
5686 'user_registration',
5687 'user_editcount',
5688 ],
5689 'joins' => [],
5690 ];
5691
5692 // Technically we shouldn't allow this without SCHEMA_COMPAT_READ_NEW,
5693 // but it does little harm and might be needed for write callers loading a User.
5694 if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_NEW ) {
5695 $ret['tables']['user_actor'] = 'actor';
5696 $ret['fields'][] = 'user_actor.actor_id';
5697 $ret['joins']['user_actor'] = [
5698 ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) ? 'JOIN' : 'LEFT JOIN',
5699 [ 'user_actor.actor_user = user_id' ]
5700 ];
5701 }
5702
5703 return $ret;
5704 }
5705
5706 /**
5707 * Factory function for fatal permission-denied errors
5708 *
5709 * @since 1.22
5710 * @param string $permission User right required
5711 * @return Status
5712 */
5713 static function newFatalPermissionDeniedStatus( $permission ) {
5714 global $wgLang;
5715
5716 $groups = [];
5717 foreach ( self::getGroupsWithPermission( $permission ) as $group ) {
5718 $groups[] = UserGroupMembership::getLink( $group, RequestContext::getMain(), 'wiki' );
5719 }
5720
5721 if ( $groups ) {
5722 return Status::newFatal( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) );
5723 }
5724
5725 return Status::newFatal( 'badaccess-group0' );
5726 }
5727
5728 /**
5729 * Get a new instance of this user that was loaded from the master via a locking read
5730 *
5731 * Use this instead of the main context User when updating that user. This avoids races
5732 * where that user was loaded from a replica DB or even the master but without proper locks.
5733 *
5734 * @return User|null Returns null if the user was not found in the DB
5735 * @since 1.27
5736 */
5737 public function getInstanceForUpdate() {
5738 if ( !$this->getId() ) {
5739 return null; // anon
5740 }
5741
5742 $user = self::newFromId( $this->getId() );
5743 if ( !$user->loadFromId( self::READ_EXCLUSIVE ) ) {
5744 return null;
5745 }
5746
5747 return $user;
5748 }
5749
5750 /**
5751 * Checks if two user objects point to the same user.
5752 *
5753 * @since 1.25 ; takes a UserIdentity instead of a User since 1.32
5754 * @param UserIdentity $user
5755 * @return bool
5756 */
5757 public function equals( UserIdentity $user ) {
5758 // XXX it's not clear whether central ID providers are supposed to obey this
5759 return $this->getName() === $user->getName();
5760 }
5761 }