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