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