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