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