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