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