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