ApiQueryBase: Fix addWhereFld for PHP 7.2
[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 array 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 $prefs = Preferences::getPreferences( $this, $context );
3092 $mapping = [];
3093
3094 // Pull out the "special" options, so they don't get converted as
3095 // multiselect or checkmatrix.
3096 $specialOptions = array_fill_keys( Preferences::getSaveBlacklist(), true );
3097 foreach ( $specialOptions as $name => $value ) {
3098 unset( $prefs[$name] );
3099 }
3100
3101 // Multiselect and checkmatrix options are stored in the database with
3102 // one key per option, each having a boolean value. Extract those keys.
3103 $multiselectOptions = [];
3104 foreach ( $prefs as $name => $info ) {
3105 if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
3106 ( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) {
3107 $opts = HTMLFormField::flattenOptions( $info['options'] );
3108 $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
3109
3110 foreach ( $opts as $value ) {
3111 $multiselectOptions["$prefix$value"] = true;
3112 }
3113
3114 unset( $prefs[$name] );
3115 }
3116 }
3117 $checkmatrixOptions = [];
3118 foreach ( $prefs as $name => $info ) {
3119 if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
3120 ( isset( $info['class'] ) && $info['class'] == 'HTMLCheckMatrix' ) ) {
3121 $columns = HTMLFormField::flattenOptions( $info['columns'] );
3122 $rows = HTMLFormField::flattenOptions( $info['rows'] );
3123 $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
3124
3125 foreach ( $columns as $column ) {
3126 foreach ( $rows as $row ) {
3127 $checkmatrixOptions["$prefix$column-$row"] = true;
3128 }
3129 }
3130
3131 unset( $prefs[$name] );
3132 }
3133 }
3134
3135 // $value is ignored
3136 foreach ( $options as $key => $value ) {
3137 if ( isset( $prefs[$key] ) ) {
3138 $mapping[$key] = 'registered';
3139 } elseif ( isset( $multiselectOptions[$key] ) ) {
3140 $mapping[$key] = 'registered-multiselect';
3141 } elseif ( isset( $checkmatrixOptions[$key] ) ) {
3142 $mapping[$key] = 'registered-checkmatrix';
3143 } elseif ( isset( $specialOptions[$key] ) ) {
3144 $mapping[$key] = 'special';
3145 } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
3146 $mapping[$key] = 'userjs';
3147 } else {
3148 $mapping[$key] = 'unused';
3149 }
3150 }
3151
3152 return $mapping;
3153 }
3154
3155 /**
3156 * Reset certain (or all) options to the site defaults
3157 *
3158 * The optional parameter determines which kinds of preferences will be reset.
3159 * Supported values are everything that can be reported by getOptionKinds()
3160 * and 'all', which forces a reset of *all* preferences and overrides everything else.
3161 *
3162 * @param array|string $resetKinds Which kinds of preferences to reset. Defaults to
3163 * array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' )
3164 * for backwards-compatibility.
3165 * @param IContextSource|null $context Context source used when $resetKinds
3166 * does not contain 'all', passed to getOptionKinds().
3167 * Defaults to RequestContext::getMain() when null.
3168 */
3169 public function resetOptions(
3170 $resetKinds = [ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ],
3171 IContextSource $context = null
3172 ) {
3173 $this->load();
3174 $defaultOptions = self::getDefaultOptions();
3175
3176 if ( !is_array( $resetKinds ) ) {
3177 $resetKinds = [ $resetKinds ];
3178 }
3179
3180 if ( in_array( 'all', $resetKinds ) ) {
3181 $newOptions = $defaultOptions;
3182 } else {
3183 if ( $context === null ) {
3184 $context = RequestContext::getMain();
3185 }
3186
3187 $optionKinds = $this->getOptionKinds( $context );
3188 $resetKinds = array_intersect( $resetKinds, self::listOptionKinds() );
3189 $newOptions = [];
3190
3191 // Use default values for the options that should be deleted, and
3192 // copy old values for the ones that shouldn't.
3193 foreach ( $this->mOptions as $key => $value ) {
3194 if ( in_array( $optionKinds[$key], $resetKinds ) ) {
3195 if ( array_key_exists( $key, $defaultOptions ) ) {
3196 $newOptions[$key] = $defaultOptions[$key];
3197 }
3198 } else {
3199 $newOptions[$key] = $value;
3200 }
3201 }
3202 }
3203
3204 Hooks::run( 'UserResetAllOptions', [ $this, &$newOptions, $this->mOptions, $resetKinds ] );
3205
3206 $this->mOptions = $newOptions;
3207 $this->mOptionsLoaded = true;
3208 }
3209
3210 /**
3211 * Get the user's preferred date format.
3212 * @return string User's preferred date format
3213 */
3214 public function getDatePreference() {
3215 // Important migration for old data rows
3216 if ( is_null( $this->mDatePreference ) ) {
3217 global $wgLang;
3218 $value = $this->getOption( 'date' );
3219 $map = $wgLang->getDatePreferenceMigrationMap();
3220 if ( isset( $map[$value] ) ) {
3221 $value = $map[$value];
3222 }
3223 $this->mDatePreference = $value;
3224 }
3225 return $this->mDatePreference;
3226 }
3227
3228 /**
3229 * Determine based on the wiki configuration and the user's options,
3230 * whether this user must be over HTTPS no matter what.
3231 *
3232 * @return bool
3233 */
3234 public function requiresHTTPS() {
3235 global $wgSecureLogin;
3236 if ( !$wgSecureLogin ) {
3237 return false;
3238 } else {
3239 $https = $this->getBoolOption( 'prefershttps' );
3240 Hooks::run( 'UserRequiresHTTPS', [ $this, &$https ] );
3241 if ( $https ) {
3242 $https = wfCanIPUseHTTPS( $this->getRequest()->getIP() );
3243 }
3244 return $https;
3245 }
3246 }
3247
3248 /**
3249 * Get the user preferred stub threshold
3250 *
3251 * @return int
3252 */
3253 public function getStubThreshold() {
3254 global $wgMaxArticleSize; # Maximum article size, in Kb
3255 $threshold = $this->getIntOption( 'stubthreshold' );
3256 if ( $threshold > $wgMaxArticleSize * 1024 ) {
3257 // If they have set an impossible value, disable the preference
3258 // so we can use the parser cache again.
3259 $threshold = 0;
3260 }
3261 return $threshold;
3262 }
3263
3264 /**
3265 * Get the permissions this user has.
3266 * @return string[] permission names
3267 */
3268 public function getRights() {
3269 if ( is_null( $this->mRights ) ) {
3270 $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
3271 Hooks::run( 'UserGetRights', [ $this, &$this->mRights ] );
3272
3273 // Deny any rights denied by the user's session, unless this
3274 // endpoint has no sessions.
3275 if ( !defined( 'MW_NO_SESSION' ) ) {
3276 $allowedRights = $this->getRequest()->getSession()->getAllowedUserRights();
3277 if ( $allowedRights !== null ) {
3278 $this->mRights = array_intersect( $this->mRights, $allowedRights );
3279 }
3280 }
3281
3282 // Force reindexation of rights when a hook has unset one of them
3283 $this->mRights = array_values( array_unique( $this->mRights ) );
3284
3285 // If block disables login, we should also remove any
3286 // extra rights blocked users might have, in case the
3287 // blocked user has a pre-existing session (T129738).
3288 // This is checked here for cases where people only call
3289 // $user->isAllowed(). It is also checked in Title::checkUserBlock()
3290 // to give a better error message in the common case.
3291 $config = RequestContext::getMain()->getConfig();
3292 if (
3293 $this->isLoggedIn() &&
3294 $config->get( 'BlockDisablesLogin' ) &&
3295 $this->isBlocked()
3296 ) {
3297 $anon = new User;
3298 $this->mRights = array_intersect( $this->mRights, $anon->getRights() );
3299 }
3300 }
3301 return $this->mRights;
3302 }
3303
3304 /**
3305 * Get the list of explicit group memberships this user has.
3306 * The implicit * and user groups are not included.
3307 * @return array Array of String internal group names
3308 */
3309 public function getGroups() {
3310 $this->load();
3311 $this->loadGroups();
3312 return array_keys( $this->mGroupMemberships );
3313 }
3314
3315 /**
3316 * Get the list of explicit group memberships this user has, stored as
3317 * UserGroupMembership objects. Implicit groups are not included.
3318 *
3319 * @return array Associative array of (group name as string => UserGroupMembership object)
3320 * @since 1.29
3321 */
3322 public function getGroupMemberships() {
3323 $this->load();
3324 $this->loadGroups();
3325 return $this->mGroupMemberships;
3326 }
3327
3328 /**
3329 * Get the list of implicit group memberships this user has.
3330 * This includes all explicit groups, plus 'user' if logged in,
3331 * '*' for all accounts, and autopromoted groups
3332 * @param bool $recache Whether to avoid the cache
3333 * @return array Array of String internal group names
3334 */
3335 public function getEffectiveGroups( $recache = false ) {
3336 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
3337 $this->mEffectiveGroups = array_unique( array_merge(
3338 $this->getGroups(), // explicit groups
3339 $this->getAutomaticGroups( $recache ) // implicit groups
3340 ) );
3341 // Avoid PHP 7.1 warning of passing $this by reference
3342 $user = $this;
3343 // Hook for additional groups
3344 Hooks::run( 'UserEffectiveGroups', [ &$user, &$this->mEffectiveGroups ] );
3345 // Force reindexation of groups when a hook has unset one of them
3346 $this->mEffectiveGroups = array_values( array_unique( $this->mEffectiveGroups ) );
3347 }
3348 return $this->mEffectiveGroups;
3349 }
3350
3351 /**
3352 * Get the list of implicit group memberships this user has.
3353 * This includes 'user' if logged in, '*' for all accounts,
3354 * and autopromoted groups
3355 * @param bool $recache Whether to avoid the cache
3356 * @return array Array of String internal group names
3357 */
3358 public function getAutomaticGroups( $recache = false ) {
3359 if ( $recache || is_null( $this->mImplicitGroups ) ) {
3360 $this->mImplicitGroups = [ '*' ];
3361 if ( $this->getId() ) {
3362 $this->mImplicitGroups[] = 'user';
3363
3364 $this->mImplicitGroups = array_unique( array_merge(
3365 $this->mImplicitGroups,
3366 Autopromote::getAutopromoteGroups( $this )
3367 ) );
3368 }
3369 if ( $recache ) {
3370 // Assure data consistency with rights/groups,
3371 // as getEffectiveGroups() depends on this function
3372 $this->mEffectiveGroups = null;
3373 }
3374 }
3375 return $this->mImplicitGroups;
3376 }
3377
3378 /**
3379 * Returns the groups the user has belonged to.
3380 *
3381 * The user may still belong to the returned groups. Compare with getGroups().
3382 *
3383 * The function will not return groups the user had belonged to before MW 1.17
3384 *
3385 * @return array Names of the groups the user has belonged to.
3386 */
3387 public function getFormerGroups() {
3388 $this->load();
3389
3390 if ( is_null( $this->mFormerGroups ) ) {
3391 $db = ( $this->queryFlagsUsed & self::READ_LATEST )
3392 ? wfGetDB( DB_MASTER )
3393 : wfGetDB( DB_REPLICA );
3394 $res = $db->select( 'user_former_groups',
3395 [ 'ufg_group' ],
3396 [ 'ufg_user' => $this->mId ],
3397 __METHOD__ );
3398 $this->mFormerGroups = [];
3399 foreach ( $res as $row ) {
3400 $this->mFormerGroups[] = $row->ufg_group;
3401 }
3402 }
3403
3404 return $this->mFormerGroups;
3405 }
3406
3407 /**
3408 * Get the user's edit count.
3409 * @return int|null Null for anonymous users
3410 */
3411 public function getEditCount() {
3412 if ( !$this->getId() ) {
3413 return null;
3414 }
3415
3416 if ( $this->mEditCount === null ) {
3417 /* Populate the count, if it has not been populated yet */
3418 $dbr = wfGetDB( DB_REPLICA );
3419 // check if the user_editcount field has been initialized
3420 $count = $dbr->selectField(
3421 'user', 'user_editcount',
3422 [ 'user_id' => $this->mId ],
3423 __METHOD__
3424 );
3425
3426 if ( $count === null ) {
3427 // it has not been initialized. do so.
3428 $count = $this->initEditCount();
3429 }
3430 $this->mEditCount = $count;
3431 }
3432 return (int)$this->mEditCount;
3433 }
3434
3435 /**
3436 * Add the user to the given group. This takes immediate effect.
3437 * If the user is already in the group, the expiry time will be updated to the new
3438 * expiry time. (If $expiry is omitted or null, the membership will be altered to
3439 * never expire.)
3440 *
3441 * @param string $group Name of the group to add
3442 * @param string $expiry Optional expiry timestamp in any format acceptable to
3443 * wfTimestamp(), or null if the group assignment should not expire
3444 * @return bool
3445 */
3446 public function addGroup( $group, $expiry = null ) {
3447 $this->load();
3448 $this->loadGroups();
3449
3450 if ( $expiry ) {
3451 $expiry = wfTimestamp( TS_MW, $expiry );
3452 }
3453
3454 if ( !Hooks::run( 'UserAddGroup', [ $this, &$group, &$expiry ] ) ) {
3455 return false;
3456 }
3457
3458 // create the new UserGroupMembership and put it in the DB
3459 $ugm = new UserGroupMembership( $this->mId, $group, $expiry );
3460 if ( !$ugm->insert( true ) ) {
3461 return false;
3462 }
3463
3464 $this->mGroupMemberships[$group] = $ugm;
3465
3466 // Refresh the groups caches, and clear the rights cache so it will be
3467 // refreshed on the next call to $this->getRights().
3468 $this->getEffectiveGroups( true );
3469 $this->mRights = null;
3470
3471 $this->invalidateCache();
3472
3473 return true;
3474 }
3475
3476 /**
3477 * Remove the user from the given group.
3478 * This takes immediate effect.
3479 * @param string $group Name of the group to remove
3480 * @return bool
3481 */
3482 public function removeGroup( $group ) {
3483 $this->load();
3484
3485 if ( !Hooks::run( 'UserRemoveGroup', [ $this, &$group ] ) ) {
3486 return false;
3487 }
3488
3489 $ugm = UserGroupMembership::getMembership( $this->mId, $group );
3490 // delete the membership entry
3491 if ( !$ugm || !$ugm->delete() ) {
3492 return false;
3493 }
3494
3495 $this->loadGroups();
3496 unset( $this->mGroupMemberships[$group] );
3497
3498 // Refresh the groups caches, and clear the rights cache so it will be
3499 // refreshed on the next call to $this->getRights().
3500 $this->getEffectiveGroups( true );
3501 $this->mRights = null;
3502
3503 $this->invalidateCache();
3504
3505 return true;
3506 }
3507
3508 /**
3509 * Get whether the user is logged in
3510 * @return bool
3511 */
3512 public function isLoggedIn() {
3513 return $this->getId() != 0;
3514 }
3515
3516 /**
3517 * Get whether the user is anonymous
3518 * @return bool
3519 */
3520 public function isAnon() {
3521 return !$this->isLoggedIn();
3522 }
3523
3524 /**
3525 * @return bool Whether this user is flagged as being a bot role account
3526 * @since 1.28
3527 */
3528 public function isBot() {
3529 if ( in_array( 'bot', $this->getGroups() ) && $this->isAllowed( 'bot' ) ) {
3530 return true;
3531 }
3532
3533 $isBot = false;
3534 Hooks::run( "UserIsBot", [ $this, &$isBot ] );
3535
3536 return $isBot;
3537 }
3538
3539 /**
3540 * Check if user is allowed to access a feature / make an action
3541 *
3542 * @param string $permissions,... Permissions to test
3543 * @return bool True if user is allowed to perform *any* of the given actions
3544 */
3545 public function isAllowedAny() {
3546 $permissions = func_get_args();
3547 foreach ( $permissions as $permission ) {
3548 if ( $this->isAllowed( $permission ) ) {
3549 return true;
3550 }
3551 }
3552 return false;
3553 }
3554
3555 /**
3556 *
3557 * @param string $permissions,... Permissions to test
3558 * @return bool True if the user is allowed to perform *all* of the given actions
3559 */
3560 public function isAllowedAll() {
3561 $permissions = func_get_args();
3562 foreach ( $permissions as $permission ) {
3563 if ( !$this->isAllowed( $permission ) ) {
3564 return false;
3565 }
3566 }
3567 return true;
3568 }
3569
3570 /**
3571 * Internal mechanics of testing a permission
3572 * @param string $action
3573 * @return bool
3574 */
3575 public function isAllowed( $action = '' ) {
3576 if ( $action === '' ) {
3577 return true; // In the spirit of DWIM
3578 }
3579 // Use strict parameter to avoid matching numeric 0 accidentally inserted
3580 // by misconfiguration: 0 == 'foo'
3581 return in_array( $action, $this->getRights(), true );
3582 }
3583
3584 /**
3585 * Check whether to enable recent changes patrol features for this user
3586 * @return bool True or false
3587 */
3588 public function useRCPatrol() {
3589 global $wgUseRCPatrol;
3590 return $wgUseRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' );
3591 }
3592
3593 /**
3594 * Check whether to enable new pages patrol features for this user
3595 * @return bool True or false
3596 */
3597 public function useNPPatrol() {
3598 global $wgUseRCPatrol, $wgUseNPPatrol;
3599 return (
3600 ( $wgUseRCPatrol || $wgUseNPPatrol )
3601 && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
3602 );
3603 }
3604
3605 /**
3606 * Check whether to enable new files patrol features for this user
3607 * @return bool True or false
3608 */
3609 public function useFilePatrol() {
3610 global $wgUseRCPatrol, $wgUseFilePatrol;
3611 return (
3612 ( $wgUseRCPatrol || $wgUseFilePatrol )
3613 && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
3614 );
3615 }
3616
3617 /**
3618 * Get the WebRequest object to use with this object
3619 *
3620 * @return WebRequest
3621 */
3622 public function getRequest() {
3623 if ( $this->mRequest ) {
3624 return $this->mRequest;
3625 } else {
3626 global $wgRequest;
3627 return $wgRequest;
3628 }
3629 }
3630
3631 /**
3632 * Check the watched status of an article.
3633 * @since 1.22 $checkRights parameter added
3634 * @param Title $title Title of the article to look at
3635 * @param bool $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
3636 * Pass User::CHECK_USER_RIGHTS or User::IGNORE_USER_RIGHTS.
3637 * @return bool
3638 */
3639 public function isWatched( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
3640 if ( $title->isWatchable() && ( !$checkRights || $this->isAllowed( 'viewmywatchlist' ) ) ) {
3641 return MediaWikiServices::getInstance()->getWatchedItemStore()->isWatched( $this, $title );
3642 }
3643 return false;
3644 }
3645
3646 /**
3647 * Watch an article.
3648 * @since 1.22 $checkRights parameter added
3649 * @param Title $title Title of the article to look at
3650 * @param bool $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
3651 * Pass User::CHECK_USER_RIGHTS or User::IGNORE_USER_RIGHTS.
3652 */
3653 public function addWatch( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
3654 if ( !$checkRights || $this->isAllowed( 'editmywatchlist' ) ) {
3655 MediaWikiServices::getInstance()->getWatchedItemStore()->addWatchBatchForUser(
3656 $this,
3657 [ $title->getSubjectPage(), $title->getTalkPage() ]
3658 );
3659 }
3660 $this->invalidateCache();
3661 }
3662
3663 /**
3664 * Stop watching an article.
3665 * @since 1.22 $checkRights parameter added
3666 * @param Title $title Title of the article to look at
3667 * @param bool $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
3668 * Pass User::CHECK_USER_RIGHTS or User::IGNORE_USER_RIGHTS.
3669 */
3670 public function removeWatch( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
3671 if ( !$checkRights || $this->isAllowed( 'editmywatchlist' ) ) {
3672 $store = MediaWikiServices::getInstance()->getWatchedItemStore();
3673 $store->removeWatch( $this, $title->getSubjectPage() );
3674 $store->removeWatch( $this, $title->getTalkPage() );
3675 }
3676 $this->invalidateCache();
3677 }
3678
3679 /**
3680 * Clear the user's notification timestamp for the given title.
3681 * If e-notif e-mails are on, they will receive notification mails on
3682 * the next change of the page if it's watched etc.
3683 * @note If the user doesn't have 'editmywatchlist', this will do nothing.
3684 * @param Title &$title Title of the article to look at
3685 * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed.
3686 */
3687 public function clearNotification( &$title, $oldid = 0 ) {
3688 global $wgUseEnotif, $wgShowUpdatedMarker;
3689
3690 // Do nothing if the database is locked to writes
3691 if ( wfReadOnly() ) {
3692 return;
3693 }
3694
3695 // Do nothing if not allowed to edit the watchlist
3696 if ( !$this->isAllowed( 'editmywatchlist' ) ) {
3697 return;
3698 }
3699
3700 // If we're working on user's talk page, we should update the talk page message indicator
3701 if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
3702 // Avoid PHP 7.1 warning of passing $this by reference
3703 $user = $this;
3704 if ( !Hooks::run( 'UserClearNewTalkNotification', [ &$user, $oldid ] ) ) {
3705 return;
3706 }
3707
3708 // Try to update the DB post-send and only if needed...
3709 DeferredUpdates::addCallableUpdate( function () use ( $title, $oldid ) {
3710 if ( !$this->getNewtalk() ) {
3711 return; // no notifications to clear
3712 }
3713
3714 // Delete the last notifications (they stack up)
3715 $this->setNewtalk( false );
3716
3717 // If there is a new, unseen, revision, use its timestamp
3718 $nextid = $oldid
3719 ? $title->getNextRevisionID( $oldid, Title::GAID_FOR_UPDATE )
3720 : null;
3721 if ( $nextid ) {
3722 $this->setNewtalk( true, Revision::newFromId( $nextid ) );
3723 }
3724 } );
3725 }
3726
3727 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
3728 return;
3729 }
3730
3731 if ( $this->isAnon() ) {
3732 // Nothing else to do...
3733 return;
3734 }
3735
3736 // Only update the timestamp if the page is being watched.
3737 // The query to find out if it is watched is cached both in memcached and per-invocation,
3738 // and when it does have to be executed, it can be on a replica DB
3739 // If this is the user's newtalk page, we always update the timestamp
3740 $force = '';
3741 if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
3742 $force = 'force';
3743 }
3744
3745 MediaWikiServices::getInstance()->getWatchedItemStore()
3746 ->resetNotificationTimestamp( $this, $title, $force, $oldid );
3747 }
3748
3749 /**
3750 * Resets all of the given user's page-change notification timestamps.
3751 * If e-notif e-mails are on, they will receive notification mails on
3752 * the next change of any watched page.
3753 * @note If the user doesn't have 'editmywatchlist', this will do nothing.
3754 */
3755 public function clearAllNotifications() {
3756 global $wgUseEnotif, $wgShowUpdatedMarker;
3757 // Do nothing if not allowed to edit the watchlist
3758 if ( wfReadOnly() || !$this->isAllowed( 'editmywatchlist' ) ) {
3759 return;
3760 }
3761
3762 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
3763 $this->setNewtalk( false );
3764 return;
3765 }
3766
3767 $id = $this->getId();
3768 if ( !$id ) {
3769 return;
3770 }
3771
3772 $dbw = wfGetDB( DB_MASTER );
3773 $asOfTimes = array_unique( $dbw->selectFieldValues(
3774 'watchlist',
3775 'wl_notificationtimestamp',
3776 [ 'wl_user' => $id, 'wl_notificationtimestamp IS NOT NULL' ],
3777 __METHOD__,
3778 [ 'ORDER BY' => 'wl_notificationtimestamp DESC', 'LIMIT' => 500 ]
3779 ) );
3780 if ( !$asOfTimes ) {
3781 return;
3782 }
3783 // Immediately update the most recent touched rows, which hopefully covers what
3784 // the user sees on the watchlist page before pressing "mark all pages visited"....
3785 $dbw->update(
3786 'watchlist',
3787 [ 'wl_notificationtimestamp' => null ],
3788 [ 'wl_user' => $id, 'wl_notificationtimestamp' => $asOfTimes ],
3789 __METHOD__
3790 );
3791 // ...and finish the older ones in a post-send update with lag checks...
3792 DeferredUpdates::addUpdate( new AutoCommitUpdate(
3793 $dbw,
3794 __METHOD__,
3795 function () use ( $dbw, $id ) {
3796 global $wgUpdateRowsPerQuery;
3797
3798 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
3799 $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
3800 $asOfTimes = array_unique( $dbw->selectFieldValues(
3801 'watchlist',
3802 'wl_notificationtimestamp',
3803 [ 'wl_user' => $id, 'wl_notificationtimestamp IS NOT NULL' ],
3804 __METHOD__
3805 ) );
3806 foreach ( array_chunk( $asOfTimes, $wgUpdateRowsPerQuery ) as $asOfTimeBatch ) {
3807 $dbw->update(
3808 'watchlist',
3809 [ 'wl_notificationtimestamp' => null ],
3810 [ 'wl_user' => $id, 'wl_notificationtimestamp' => $asOfTimeBatch ],
3811 __METHOD__
3812 );
3813 $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
3814 }
3815 }
3816 ) );
3817 // We also need to clear here the "you have new message" notification for the own
3818 // user_talk page; it's cleared one page view later in WikiPage::doViewUpdates().
3819 }
3820
3821 /**
3822 * Compute experienced level based on edit count and registration date.
3823 *
3824 * @return string 'newcomer', 'learner', or 'experienced'
3825 */
3826 public function getExperienceLevel() {
3827 global $wgLearnerEdits,
3828 $wgExperiencedUserEdits,
3829 $wgLearnerMemberSince,
3830 $wgExperiencedUserMemberSince;
3831
3832 if ( $this->isAnon() ) {
3833 return false;
3834 }
3835
3836 $editCount = $this->getEditCount();
3837 $registration = $this->getRegistration();
3838 $now = time();
3839 $learnerRegistration = wfTimestamp( TS_MW, $now - $wgLearnerMemberSince * 86400 );
3840 $experiencedRegistration = wfTimestamp( TS_MW, $now - $wgExperiencedUserMemberSince * 86400 );
3841
3842 if (
3843 $editCount < $wgLearnerEdits ||
3844 $registration > $learnerRegistration
3845 ) {
3846 return 'newcomer';
3847 } elseif (
3848 $editCount > $wgExperiencedUserEdits &&
3849 $registration <= $experiencedRegistration
3850 ) {
3851 return 'experienced';
3852 } else {
3853 return 'learner';
3854 }
3855 }
3856
3857 /**
3858 * Set a cookie on the user's client. Wrapper for
3859 * WebResponse::setCookie
3860 * @deprecated since 1.27
3861 * @param string $name Name of the cookie to set
3862 * @param string $value Value to set
3863 * @param int $exp Expiration time, as a UNIX time value;
3864 * if 0 or not specified, use the default $wgCookieExpiration
3865 * @param bool $secure
3866 * true: Force setting the secure attribute when setting the cookie
3867 * false: Force NOT setting the secure attribute when setting the cookie
3868 * null (default): Use the default ($wgCookieSecure) to set the secure attribute
3869 * @param array $params Array of options sent passed to WebResponse::setcookie()
3870 * @param WebRequest|null $request WebRequest object to use; $wgRequest will be used if null
3871 * is passed.
3872 */
3873 protected function setCookie(
3874 $name, $value, $exp = 0, $secure = null, $params = [], $request = null
3875 ) {
3876 wfDeprecated( __METHOD__, '1.27' );
3877 if ( $request === null ) {
3878 $request = $this->getRequest();
3879 }
3880 $params['secure'] = $secure;
3881 $request->response()->setCookie( $name, $value, $exp, $params );
3882 }
3883
3884 /**
3885 * Clear a cookie on the user's client
3886 * @deprecated since 1.27
3887 * @param string $name Name of the cookie to clear
3888 * @param bool $secure
3889 * true: Force setting the secure attribute when setting the cookie
3890 * false: Force NOT setting the secure attribute when setting the cookie
3891 * null (default): Use the default ($wgCookieSecure) to set the secure attribute
3892 * @param array $params Array of options sent passed to WebResponse::setcookie()
3893 */
3894 protected function clearCookie( $name, $secure = null, $params = [] ) {
3895 wfDeprecated( __METHOD__, '1.27' );
3896 $this->setCookie( $name, '', time() - 86400, $secure, $params );
3897 }
3898
3899 /**
3900 * Set an extended login cookie on the user's client. The expiry of the cookie
3901 * is controlled by the $wgExtendedLoginCookieExpiration configuration
3902 * variable.
3903 *
3904 * @see User::setCookie
3905 *
3906 * @deprecated since 1.27
3907 * @param string $name Name of the cookie to set
3908 * @param string $value Value to set
3909 * @param bool $secure
3910 * true: Force setting the secure attribute when setting the cookie
3911 * false: Force NOT setting the secure attribute when setting the cookie
3912 * null (default): Use the default ($wgCookieSecure) to set the secure attribute
3913 */
3914 protected function setExtendedLoginCookie( $name, $value, $secure ) {
3915 global $wgExtendedLoginCookieExpiration, $wgCookieExpiration;
3916
3917 wfDeprecated( __METHOD__, '1.27' );
3918
3919 $exp = time();
3920 $exp += $wgExtendedLoginCookieExpiration !== null
3921 ? $wgExtendedLoginCookieExpiration
3922 : $wgCookieExpiration;
3923
3924 $this->setCookie( $name, $value, $exp, $secure );
3925 }
3926
3927 /**
3928 * Persist this user's session (e.g. set cookies)
3929 *
3930 * @param WebRequest|null $request WebRequest object to use; $wgRequest will be used if null
3931 * is passed.
3932 * @param bool $secure Whether to force secure/insecure cookies or use default
3933 * @param bool $rememberMe Whether to add a Token cookie for elongated sessions
3934 */
3935 public function setCookies( $request = null, $secure = null, $rememberMe = false ) {
3936 $this->load();
3937 if ( 0 == $this->mId ) {
3938 return;
3939 }
3940
3941 $session = $this->getRequest()->getSession();
3942 if ( $request && $session->getRequest() !== $request ) {
3943 $session = $session->sessionWithRequest( $request );
3944 }
3945 $delay = $session->delaySave();
3946
3947 if ( !$session->getUser()->equals( $this ) ) {
3948 if ( !$session->canSetUser() ) {
3949 \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
3950 ->warning( __METHOD__ .
3951 ": Cannot save user \"$this\" to a user \"{$session->getUser()}\"'s immutable session"
3952 );
3953 return;
3954 }
3955 $session->setUser( $this );
3956 }
3957
3958 $session->setRememberUser( $rememberMe );
3959 if ( $secure !== null ) {
3960 $session->setForceHTTPS( $secure );
3961 }
3962
3963 $session->persist();
3964
3965 ScopedCallback::consume( $delay );
3966 }
3967
3968 /**
3969 * Log this user out.
3970 */
3971 public function logout() {
3972 // Avoid PHP 7.1 warning of passing $this by reference
3973 $user = $this;
3974 if ( Hooks::run( 'UserLogout', [ &$user ] ) ) {
3975 $this->doLogout();
3976 }
3977 }
3978
3979 /**
3980 * Clear the user's session, and reset the instance cache.
3981 * @see logout()
3982 */
3983 public function doLogout() {
3984 $session = $this->getRequest()->getSession();
3985 if ( !$session->canSetUser() ) {
3986 \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
3987 ->warning( __METHOD__ . ": Cannot log out of an immutable session" );
3988 $error = 'immutable';
3989 } elseif ( !$session->getUser()->equals( $this ) ) {
3990 \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
3991 ->warning( __METHOD__ .
3992 ": Cannot log user \"$this\" out of a user \"{$session->getUser()}\"'s session"
3993 );
3994 // But we still may as well make this user object anon
3995 $this->clearInstanceCache( 'defaults' );
3996 $error = 'wronguser';
3997 } else {
3998 $this->clearInstanceCache( 'defaults' );
3999 $delay = $session->delaySave();
4000 $session->unpersist(); // Clear cookies (T127436)
4001 $session->setLoggedOutTimestamp( time() );
4002 $session->setUser( new User );
4003 $session->set( 'wsUserID', 0 ); // Other code expects this
4004 $session->resetAllTokens();
4005 ScopedCallback::consume( $delay );
4006 $error = false;
4007 }
4008 \MediaWiki\Logger\LoggerFactory::getInstance( 'authevents' )->info( 'Logout', [
4009 'event' => 'logout',
4010 'successful' => $error === false,
4011 'status' => $error ?: 'success',
4012 ] );
4013 }
4014
4015 /**
4016 * Save this user's settings into the database.
4017 * @todo Only rarely do all these fields need to be set!
4018 */
4019 public function saveSettings() {
4020 if ( wfReadOnly() ) {
4021 // @TODO: caller should deal with this instead!
4022 // This should really just be an exception.
4023 MWExceptionHandler::logException( new DBExpectedError(
4024 null,
4025 "Could not update user with ID '{$this->mId}'; DB is read-only."
4026 ) );
4027 return;
4028 }
4029
4030 $this->load();
4031 if ( 0 == $this->mId ) {
4032 return; // anon
4033 }
4034
4035 // Get a new user_touched that is higher than the old one.
4036 // This will be used for a CAS check as a last-resort safety
4037 // check against race conditions and replica DB lag.
4038 $newTouched = $this->newTouchedTimestamp();
4039
4040 $dbw = wfGetDB( DB_MASTER );
4041 $dbw->update( 'user',
4042 [ /* SET */
4043 'user_name' => $this->mName,
4044 'user_real_name' => $this->mRealName,
4045 'user_email' => $this->mEmail,
4046 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
4047 'user_touched' => $dbw->timestamp( $newTouched ),
4048 'user_token' => strval( $this->mToken ),
4049 'user_email_token' => $this->mEmailToken,
4050 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
4051 ], $this->makeUpdateConditions( $dbw, [ /* WHERE */
4052 'user_id' => $this->mId,
4053 ] ), __METHOD__
4054 );
4055
4056 if ( !$dbw->affectedRows() ) {
4057 // Maybe the problem was a missed cache update; clear it to be safe
4058 $this->clearSharedCache( 'refresh' );
4059 // User was changed in the meantime or loaded with stale data
4060 $from = ( $this->queryFlagsUsed & self::READ_LATEST ) ? 'master' : 'replica';
4061 throw new MWException(
4062 "CAS update failed on user_touched for user ID '{$this->mId}' (read from $from);" .
4063 " the version of the user to be saved is older than the current version."
4064 );
4065 }
4066
4067 $this->mTouched = $newTouched;
4068 $this->saveOptions();
4069
4070 Hooks::run( 'UserSaveSettings', [ $this ] );
4071 $this->clearSharedCache();
4072 $this->getUserPage()->invalidateCache();
4073 }
4074
4075 /**
4076 * If only this user's username is known, and it exists, return the user ID.
4077 *
4078 * @param int $flags Bitfield of User:READ_* constants; useful for existence checks
4079 * @return int
4080 */
4081 public function idForName( $flags = 0 ) {
4082 $s = trim( $this->getName() );
4083 if ( $s === '' ) {
4084 return 0;
4085 }
4086
4087 $db = ( ( $flags & self::READ_LATEST ) == self::READ_LATEST )
4088 ? wfGetDB( DB_MASTER )
4089 : wfGetDB( DB_REPLICA );
4090
4091 $options = ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING )
4092 ? [ 'LOCK IN SHARE MODE' ]
4093 : [];
4094
4095 $id = $db->selectField( 'user',
4096 'user_id', [ 'user_name' => $s ], __METHOD__, $options );
4097
4098 return (int)$id;
4099 }
4100
4101 /**
4102 * Add a user to the database, return the user object
4103 *
4104 * @param string $name Username to add
4105 * @param array $params Array of Strings Non-default parameters to save to
4106 * the database as user_* fields:
4107 * - email: The user's email address.
4108 * - email_authenticated: The email authentication timestamp.
4109 * - real_name: The user's real name.
4110 * - options: An associative array of non-default options.
4111 * - token: Random authentication token. Do not set.
4112 * - registration: Registration timestamp. Do not set.
4113 *
4114 * @return User|null User object, or null if the username already exists.
4115 */
4116 public static function createNew( $name, $params = [] ) {
4117 foreach ( [ 'password', 'newpassword', 'newpass_time', 'password_expires' ] as $field ) {
4118 if ( isset( $params[$field] ) ) {
4119 wfDeprecated( __METHOD__ . " with param '$field'", '1.27' );
4120 unset( $params[$field] );
4121 }
4122 }
4123
4124 $user = new User;
4125 $user->load();
4126 $user->setToken(); // init token
4127 if ( isset( $params['options'] ) ) {
4128 $user->mOptions = $params['options'] + (array)$user->mOptions;
4129 unset( $params['options'] );
4130 }
4131 $dbw = wfGetDB( DB_MASTER );
4132
4133 $noPass = PasswordFactory::newInvalidPassword()->toString();
4134
4135 $fields = [
4136 'user_name' => $name,
4137 'user_password' => $noPass,
4138 'user_newpassword' => $noPass,
4139 'user_email' => $user->mEmail,
4140 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
4141 'user_real_name' => $user->mRealName,
4142 'user_token' => strval( $user->mToken ),
4143 'user_registration' => $dbw->timestamp( $user->mRegistration ),
4144 'user_editcount' => 0,
4145 'user_touched' => $dbw->timestamp( $user->newTouchedTimestamp() ),
4146 ];
4147 foreach ( $params as $name => $value ) {
4148 $fields["user_$name"] = $value;
4149 }
4150 $dbw->insert( 'user', $fields, __METHOD__, [ 'IGNORE' ] );
4151 if ( $dbw->affectedRows() ) {
4152 $newUser = self::newFromId( $dbw->insertId() );
4153 } else {
4154 $newUser = null;
4155 }
4156 return $newUser;
4157 }
4158
4159 /**
4160 * Add this existing user object to the database. If the user already
4161 * exists, a fatal status object is returned, and the user object is
4162 * initialised with the data from the database.
4163 *
4164 * Previously, this function generated a DB error due to a key conflict
4165 * if the user already existed. Many extension callers use this function
4166 * in code along the lines of:
4167 *
4168 * $user = User::newFromName( $name );
4169 * if ( !$user->isLoggedIn() ) {
4170 * $user->addToDatabase();
4171 * }
4172 * // do something with $user...
4173 *
4174 * However, this was vulnerable to a race condition (T18020). By
4175 * initialising the user object if the user exists, we aim to support this
4176 * calling sequence as far as possible.
4177 *
4178 * Note that if the user exists, this function will acquire a write lock,
4179 * so it is still advisable to make the call conditional on isLoggedIn(),
4180 * and to commit the transaction after calling.
4181 *
4182 * @throws MWException
4183 * @return Status
4184 */
4185 public function addToDatabase() {
4186 $this->load();
4187 if ( !$this->mToken ) {
4188 $this->setToken(); // init token
4189 }
4190
4191 if ( !is_string( $this->mName ) ) {
4192 throw new RuntimeException( "User name field is not set." );
4193 }
4194
4195 $this->mTouched = $this->newTouchedTimestamp();
4196
4197 $noPass = PasswordFactory::newInvalidPassword()->toString();
4198
4199 $dbw = wfGetDB( DB_MASTER );
4200 $dbw->insert( 'user',
4201 [
4202 'user_name' => $this->mName,
4203 'user_password' => $noPass,
4204 'user_newpassword' => $noPass,
4205 'user_email' => $this->mEmail,
4206 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
4207 'user_real_name' => $this->mRealName,
4208 'user_token' => strval( $this->mToken ),
4209 'user_registration' => $dbw->timestamp( $this->mRegistration ),
4210 'user_editcount' => 0,
4211 'user_touched' => $dbw->timestamp( $this->mTouched ),
4212 ], __METHOD__,
4213 [ 'IGNORE' ]
4214 );
4215 if ( !$dbw->affectedRows() ) {
4216 // Use locking reads to bypass any REPEATABLE-READ snapshot.
4217 $this->mId = $dbw->selectField(
4218 'user',
4219 'user_id',
4220 [ 'user_name' => $this->mName ],
4221 __METHOD__,
4222 [ 'LOCK IN SHARE MODE' ]
4223 );
4224 $loaded = false;
4225 if ( $this->mId ) {
4226 if ( $this->loadFromDatabase( self::READ_LOCKING ) ) {
4227 $loaded = true;
4228 }
4229 }
4230 if ( !$loaded ) {
4231 throw new MWException( __METHOD__ . ": hit a key conflict attempting " .
4232 "to insert user '{$this->mName}' row, but it was not present in select!" );
4233 }
4234 return Status::newFatal( 'userexists' );
4235 }
4236 $this->mId = $dbw->insertId();
4237 self::$idCacheByName[$this->mName] = $this->mId;
4238
4239 // Clear instance cache other than user table data, which is already accurate
4240 $this->clearInstanceCache();
4241
4242 $this->saveOptions();
4243 return Status::newGood();
4244 }
4245
4246 /**
4247 * If this user is logged-in and blocked,
4248 * block any IP address they've successfully logged in from.
4249 * @return bool A block was spread
4250 */
4251 public function spreadAnyEditBlock() {
4252 if ( $this->isLoggedIn() && $this->isBlocked() ) {
4253 return $this->spreadBlock();
4254 }
4255
4256 return false;
4257 }
4258
4259 /**
4260 * If this (non-anonymous) user is blocked,
4261 * block the IP address they've successfully logged in from.
4262 * @return bool A block was spread
4263 */
4264 protected function spreadBlock() {
4265 wfDebug( __METHOD__ . "()\n" );
4266 $this->load();
4267 if ( $this->mId == 0 ) {
4268 return false;
4269 }
4270
4271 $userblock = Block::newFromTarget( $this->getName() );
4272 if ( !$userblock ) {
4273 return false;
4274 }
4275
4276 return (bool)$userblock->doAutoblock( $this->getRequest()->getIP() );
4277 }
4278
4279 /**
4280 * Get whether the user is explicitly blocked from account creation.
4281 * @return bool|Block
4282 */
4283 public function isBlockedFromCreateAccount() {
4284 $this->getBlockedStatus();
4285 if ( $this->mBlock && $this->mBlock->prevents( 'createaccount' ) ) {
4286 return $this->mBlock;
4287 }
4288
4289 # T15611: if the IP address the user is trying to create an account from is
4290 # blocked with createaccount disabled, prevent new account creation there even
4291 # when the user is logged in
4292 if ( $this->mBlockedFromCreateAccount === false && !$this->isAllowed( 'ipblock-exempt' ) ) {
4293 $this->mBlockedFromCreateAccount = Block::newFromTarget( null, $this->getRequest()->getIP() );
4294 }
4295 return $this->mBlockedFromCreateAccount instanceof Block
4296 && $this->mBlockedFromCreateAccount->prevents( 'createaccount' )
4297 ? $this->mBlockedFromCreateAccount
4298 : false;
4299 }
4300
4301 /**
4302 * Get whether the user is blocked from using Special:Emailuser.
4303 * @return bool
4304 */
4305 public function isBlockedFromEmailuser() {
4306 $this->getBlockedStatus();
4307 return $this->mBlock && $this->mBlock->prevents( 'sendemail' );
4308 }
4309
4310 /**
4311 * Get whether the user is allowed to create an account.
4312 * @return bool
4313 */
4314 public function isAllowedToCreateAccount() {
4315 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
4316 }
4317
4318 /**
4319 * Get this user's personal page title.
4320 *
4321 * @return Title User's personal page title
4322 */
4323 public function getUserPage() {
4324 return Title::makeTitle( NS_USER, $this->getName() );
4325 }
4326
4327 /**
4328 * Get this user's talk page title.
4329 *
4330 * @return Title User's talk page title
4331 */
4332 public function getTalkPage() {
4333 $title = $this->getUserPage();
4334 return $title->getTalkPage();
4335 }
4336
4337 /**
4338 * Determine whether the user is a newbie. Newbies are either
4339 * anonymous IPs, or the most recently created accounts.
4340 * @return bool
4341 */
4342 public function isNewbie() {
4343 return !$this->isAllowed( 'autoconfirmed' );
4344 }
4345
4346 /**
4347 * Check to see if the given clear-text password is one of the accepted passwords
4348 * @deprecated since 1.27, use AuthManager instead
4349 * @param string $password User password
4350 * @return bool True if the given password is correct, otherwise False
4351 */
4352 public function checkPassword( $password ) {
4353 $manager = AuthManager::singleton();
4354 $reqs = AuthenticationRequest::loadRequestsFromSubmission(
4355 $manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN ),
4356 [
4357 'username' => $this->getName(),
4358 'password' => $password,
4359 ]
4360 );
4361 $res = AuthManager::singleton()->beginAuthentication( $reqs, 'null:' );
4362 switch ( $res->status ) {
4363 case AuthenticationResponse::PASS:
4364 return true;
4365 case AuthenticationResponse::FAIL:
4366 // Hope it's not a PreAuthenticationProvider that failed...
4367 \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
4368 ->info( __METHOD__ . ': Authentication failed: ' . $res->message->plain() );
4369 return false;
4370 default:
4371 throw new BadMethodCallException(
4372 'AuthManager returned a response unsupported by ' . __METHOD__
4373 );
4374 }
4375 }
4376
4377 /**
4378 * Check if the given clear-text password matches the temporary password
4379 * sent by e-mail for password reset operations.
4380 *
4381 * @deprecated since 1.27, use AuthManager instead
4382 * @param string $plaintext
4383 * @return bool True if matches, false otherwise
4384 */
4385 public function checkTemporaryPassword( $plaintext ) {
4386 // Can't check the temporary password individually.
4387 return $this->checkPassword( $plaintext );
4388 }
4389
4390 /**
4391 * Initialize (if necessary) and return a session token value
4392 * which can be used in edit forms to show that the user's
4393 * login credentials aren't being hijacked with a foreign form
4394 * submission.
4395 *
4396 * @since 1.27
4397 * @param string|array $salt Array of Strings Optional function-specific data for hashing
4398 * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest
4399 * @return MediaWiki\Session\Token The new edit token
4400 */
4401 public function getEditTokenObject( $salt = '', $request = null ) {
4402 if ( $this->isAnon() ) {
4403 return new LoggedOutEditToken();
4404 }
4405
4406 if ( !$request ) {
4407 $request = $this->getRequest();
4408 }
4409 return $request->getSession()->getToken( $salt );
4410 }
4411
4412 /**
4413 * Initialize (if necessary) and return a session token value
4414 * which can be used in edit forms to show that the user's
4415 * login credentials aren't being hijacked with a foreign form
4416 * submission.
4417 *
4418 * The $salt for 'edit' and 'csrf' tokens is the default (empty string).
4419 *
4420 * @since 1.19
4421 * @param string|array $salt Array of Strings Optional function-specific data for hashing
4422 * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest
4423 * @return string The new edit token
4424 */
4425 public function getEditToken( $salt = '', $request = null ) {
4426 return $this->getEditTokenObject( $salt, $request )->toString();
4427 }
4428
4429 /**
4430 * Get the embedded timestamp from a token.
4431 * @deprecated since 1.27, use \MediaWiki\Session\Token::getTimestamp instead.
4432 * @param string $val Input token
4433 * @return int|null
4434 */
4435 public static function getEditTokenTimestamp( $val ) {
4436 wfDeprecated( __METHOD__, '1.27' );
4437 return MediaWiki\Session\Token::getTimestamp( $val );
4438 }
4439
4440 /**
4441 * Check given value against the token value stored in the session.
4442 * A match should confirm that the form was submitted from the
4443 * user's own login session, not a form submission from a third-party
4444 * site.
4445 *
4446 * @param string $val Input value to compare
4447 * @param string $salt Optional function-specific data for hashing
4448 * @param WebRequest|null $request Object to use or null to use $wgRequest
4449 * @param int $maxage Fail tokens older than this, in seconds
4450 * @return bool Whether the token matches
4451 */
4452 public function matchEditToken( $val, $salt = '', $request = null, $maxage = null ) {
4453 return $this->getEditTokenObject( $salt, $request )->match( $val, $maxage );
4454 }
4455
4456 /**
4457 * Check given value against the token value stored in the session,
4458 * ignoring the suffix.
4459 *
4460 * @param string $val Input value to compare
4461 * @param string $salt Optional function-specific data for hashing
4462 * @param WebRequest|null $request Object to use or null to use $wgRequest
4463 * @param int $maxage Fail tokens older than this, in seconds
4464 * @return bool Whether the token matches
4465 */
4466 public function matchEditTokenNoSuffix( $val, $salt = '', $request = null, $maxage = null ) {
4467 $val = substr( $val, 0, strspn( $val, '0123456789abcdef' ) ) . Token::SUFFIX;
4468 return $this->matchEditToken( $val, $salt, $request, $maxage );
4469 }
4470
4471 /**
4472 * Generate a new e-mail confirmation token and send a confirmation/invalidation
4473 * mail to the user's given address.
4474 *
4475 * @param string $type Message to send, either "created", "changed" or "set"
4476 * @return Status
4477 */
4478 public function sendConfirmationMail( $type = 'created' ) {
4479 global $wgLang;
4480 $expiration = null; // gets passed-by-ref and defined in next line.
4481 $token = $this->confirmationToken( $expiration );
4482 $url = $this->confirmationTokenUrl( $token );
4483 $invalidateURL = $this->invalidationTokenUrl( $token );
4484 $this->saveSettings();
4485
4486 if ( $type == 'created' || $type === false ) {
4487 $message = 'confirmemail_body';
4488 } elseif ( $type === true ) {
4489 $message = 'confirmemail_body_changed';
4490 } else {
4491 // Messages: confirmemail_body_changed, confirmemail_body_set
4492 $message = 'confirmemail_body_' . $type;
4493 }
4494
4495 return $this->sendMail( wfMessage( 'confirmemail_subject' )->text(),
4496 wfMessage( $message,
4497 $this->getRequest()->getIP(),
4498 $this->getName(),
4499 $url,
4500 $wgLang->userTimeAndDate( $expiration, $this ),
4501 $invalidateURL,
4502 $wgLang->userDate( $expiration, $this ),
4503 $wgLang->userTime( $expiration, $this ) )->text() );
4504 }
4505
4506 /**
4507 * Send an e-mail to this user's account. Does not check for
4508 * confirmed status or validity.
4509 *
4510 * @param string $subject Message subject
4511 * @param string $body Message body
4512 * @param User|null $from Optional sending user; if unspecified, default
4513 * $wgPasswordSender will be used.
4514 * @param string $replyto Reply-To address
4515 * @return Status
4516 */
4517 public function sendMail( $subject, $body, $from = null, $replyto = null ) {
4518 global $wgPasswordSender;
4519
4520 if ( $from instanceof User ) {
4521 $sender = MailAddress::newFromUser( $from );
4522 } else {
4523 $sender = new MailAddress( $wgPasswordSender,
4524 wfMessage( 'emailsender' )->inContentLanguage()->text() );
4525 }
4526 $to = MailAddress::newFromUser( $this );
4527
4528 return UserMailer::send( $to, $sender, $subject, $body, [
4529 'replyTo' => $replyto,
4530 ] );
4531 }
4532
4533 /**
4534 * Generate, store, and return a new e-mail confirmation code.
4535 * A hash (unsalted, since it's used as a key) is stored.
4536 *
4537 * @note Call saveSettings() after calling this function to commit
4538 * this change to the database.
4539 *
4540 * @param string &$expiration Accepts the expiration time
4541 * @return string New token
4542 */
4543 protected function confirmationToken( &$expiration ) {
4544 global $wgUserEmailConfirmationTokenExpiry;
4545 $now = time();
4546 $expires = $now + $wgUserEmailConfirmationTokenExpiry;
4547 $expiration = wfTimestamp( TS_MW, $expires );
4548 $this->load();
4549 $token = MWCryptRand::generateHex( 32 );
4550 $hash = md5( $token );
4551 $this->mEmailToken = $hash;
4552 $this->mEmailTokenExpires = $expiration;
4553 return $token;
4554 }
4555
4556 /**
4557 * Return a URL the user can use to confirm their email address.
4558 * @param string $token Accepts the email confirmation token
4559 * @return string New token URL
4560 */
4561 protected function confirmationTokenUrl( $token ) {
4562 return $this->getTokenUrl( 'ConfirmEmail', $token );
4563 }
4564
4565 /**
4566 * Return a URL the user can use to invalidate their email address.
4567 * @param string $token Accepts the email confirmation token
4568 * @return string New token URL
4569 */
4570 protected function invalidationTokenUrl( $token ) {
4571 return $this->getTokenUrl( 'InvalidateEmail', $token );
4572 }
4573
4574 /**
4575 * Internal function to format the e-mail validation/invalidation URLs.
4576 * This uses a quickie hack to use the
4577 * hardcoded English names of the Special: pages, for ASCII safety.
4578 *
4579 * @note Since these URLs get dropped directly into emails, using the
4580 * short English names avoids insanely long URL-encoded links, which
4581 * also sometimes can get corrupted in some browsers/mailers
4582 * (T8957 with Gmail and Internet Explorer).
4583 *
4584 * @param string $page Special page
4585 * @param string $token Token
4586 * @return string Formatted URL
4587 */
4588 protected function getTokenUrl( $page, $token ) {
4589 // Hack to bypass localization of 'Special:'
4590 $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" );
4591 return $title->getCanonicalURL();
4592 }
4593
4594 /**
4595 * Mark the e-mail address confirmed.
4596 *
4597 * @note Call saveSettings() after calling this function to commit the change.
4598 *
4599 * @return bool
4600 */
4601 public function confirmEmail() {
4602 // Check if it's already confirmed, so we don't touch the database
4603 // and fire the ConfirmEmailComplete hook on redundant confirmations.
4604 if ( !$this->isEmailConfirmed() ) {
4605 $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
4606 Hooks::run( 'ConfirmEmailComplete', [ $this ] );
4607 }
4608 return true;
4609 }
4610
4611 /**
4612 * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
4613 * address if it was already confirmed.
4614 *
4615 * @note Call saveSettings() after calling this function to commit the change.
4616 * @return bool Returns true
4617 */
4618 public function invalidateEmail() {
4619 $this->load();
4620 $this->mEmailToken = null;
4621 $this->mEmailTokenExpires = null;
4622 $this->setEmailAuthenticationTimestamp( null );
4623 $this->mEmail = '';
4624 Hooks::run( 'InvalidateEmailComplete', [ $this ] );
4625 return true;
4626 }
4627
4628 /**
4629 * Set the e-mail authentication timestamp.
4630 * @param string $timestamp TS_MW timestamp
4631 */
4632 public function setEmailAuthenticationTimestamp( $timestamp ) {
4633 $this->load();
4634 $this->mEmailAuthenticated = $timestamp;
4635 Hooks::run( 'UserSetEmailAuthenticationTimestamp', [ $this, &$this->mEmailAuthenticated ] );
4636 }
4637
4638 /**
4639 * Is this user allowed to send e-mails within limits of current
4640 * site configuration?
4641 * @return bool
4642 */
4643 public function canSendEmail() {
4644 global $wgEnableEmail, $wgEnableUserEmail;
4645 if ( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
4646 return false;
4647 }
4648 $canSend = $this->isEmailConfirmed();
4649 // Avoid PHP 7.1 warning of passing $this by reference
4650 $user = $this;
4651 Hooks::run( 'UserCanSendEmail', [ &$user, &$canSend ] );
4652 return $canSend;
4653 }
4654
4655 /**
4656 * Is this user allowed to receive e-mails within limits of current
4657 * site configuration?
4658 * @return bool
4659 */
4660 public function canReceiveEmail() {
4661 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
4662 }
4663
4664 /**
4665 * Is this user's e-mail address valid-looking and confirmed within
4666 * limits of the current site configuration?
4667 *
4668 * @note If $wgEmailAuthentication is on, this may require the user to have
4669 * confirmed their address by returning a code or using a password
4670 * sent to the address from the wiki.
4671 *
4672 * @return bool
4673 */
4674 public function isEmailConfirmed() {
4675 global $wgEmailAuthentication;
4676 $this->load();
4677 // Avoid PHP 7.1 warning of passing $this by reference
4678 $user = $this;
4679 $confirmed = true;
4680 if ( Hooks::run( 'EmailConfirmed', [ &$user, &$confirmed ] ) ) {
4681 if ( $this->isAnon() ) {
4682 return false;
4683 }
4684 if ( !Sanitizer::validateEmail( $this->mEmail ) ) {
4685 return false;
4686 }
4687 if ( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) {
4688 return false;
4689 }
4690 return true;
4691 } else {
4692 return $confirmed;
4693 }
4694 }
4695
4696 /**
4697 * Check whether there is an outstanding request for e-mail confirmation.
4698 * @return bool
4699 */
4700 public function isEmailConfirmationPending() {
4701 global $wgEmailAuthentication;
4702 return $wgEmailAuthentication &&
4703 !$this->isEmailConfirmed() &&
4704 $this->mEmailToken &&
4705 $this->mEmailTokenExpires > wfTimestamp();
4706 }
4707
4708 /**
4709 * Get the timestamp of account creation.
4710 *
4711 * @return string|bool|null Timestamp of account creation, false for
4712 * non-existent/anonymous user accounts, or null if existing account
4713 * but information is not in database.
4714 */
4715 public function getRegistration() {
4716 if ( $this->isAnon() ) {
4717 return false;
4718 }
4719 $this->load();
4720 return $this->mRegistration;
4721 }
4722
4723 /**
4724 * Get the timestamp of the first edit
4725 *
4726 * @return string|bool Timestamp of first edit, or false for
4727 * non-existent/anonymous user accounts.
4728 */
4729 public function getFirstEditTimestamp() {
4730 if ( $this->getId() == 0 ) {
4731 return false; // anons
4732 }
4733 $dbr = wfGetDB( DB_REPLICA );
4734 $time = $dbr->selectField( 'revision', 'rev_timestamp',
4735 [ 'rev_user' => $this->getId() ],
4736 __METHOD__,
4737 [ 'ORDER BY' => 'rev_timestamp ASC' ]
4738 );
4739 if ( !$time ) {
4740 return false; // no edits
4741 }
4742 return wfTimestamp( TS_MW, $time );
4743 }
4744
4745 /**
4746 * Get the permissions associated with a given list of groups
4747 *
4748 * @param array $groups Array of Strings List of internal group names
4749 * @return array Array of Strings List of permission key names for given groups combined
4750 */
4751 public static function getGroupPermissions( $groups ) {
4752 global $wgGroupPermissions, $wgRevokePermissions;
4753 $rights = [];
4754 // grant every granted permission first
4755 foreach ( $groups as $group ) {
4756 if ( isset( $wgGroupPermissions[$group] ) ) {
4757 $rights = array_merge( $rights,
4758 // array_filter removes empty items
4759 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
4760 }
4761 }
4762 // now revoke the revoked permissions
4763 foreach ( $groups as $group ) {
4764 if ( isset( $wgRevokePermissions[$group] ) ) {
4765 $rights = array_diff( $rights,
4766 array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
4767 }
4768 }
4769 return array_unique( $rights );
4770 }
4771
4772 /**
4773 * Get all the groups who have a given permission
4774 *
4775 * @param string $role Role to check
4776 * @return array Array of Strings List of internal group names with the given permission
4777 */
4778 public static function getGroupsWithPermission( $role ) {
4779 global $wgGroupPermissions;
4780 $allowedGroups = [];
4781 foreach ( array_keys( $wgGroupPermissions ) as $group ) {
4782 if ( self::groupHasPermission( $group, $role ) ) {
4783 $allowedGroups[] = $group;
4784 }
4785 }
4786 return $allowedGroups;
4787 }
4788
4789 /**
4790 * Check, if the given group has the given permission
4791 *
4792 * If you're wanting to check whether all users have a permission, use
4793 * User::isEveryoneAllowed() instead. That properly checks if it's revoked
4794 * from anyone.
4795 *
4796 * @since 1.21
4797 * @param string $group Group to check
4798 * @param string $role Role to check
4799 * @return bool
4800 */
4801 public static function groupHasPermission( $group, $role ) {
4802 global $wgGroupPermissions, $wgRevokePermissions;
4803 return isset( $wgGroupPermissions[$group][$role] ) && $wgGroupPermissions[$group][$role]
4804 && !( isset( $wgRevokePermissions[$group][$role] ) && $wgRevokePermissions[$group][$role] );
4805 }
4806
4807 /**
4808 * Check if all users may be assumed to have the given permission
4809 *
4810 * We generally assume so if the right is granted to '*' and isn't revoked
4811 * on any group. It doesn't attempt to take grants or other extension
4812 * limitations on rights into account in the general case, though, as that
4813 * would require it to always return false and defeat the purpose.
4814 * Specifically, session-based rights restrictions (such as OAuth or bot
4815 * passwords) are applied based on the current session.
4816 *
4817 * @since 1.22
4818 * @param string $right Right to check
4819 * @return bool
4820 */
4821 public static function isEveryoneAllowed( $right ) {
4822 global $wgGroupPermissions, $wgRevokePermissions;
4823 static $cache = [];
4824
4825 // Use the cached results, except in unit tests which rely on
4826 // being able change the permission mid-request
4827 if ( isset( $cache[$right] ) && !defined( 'MW_PHPUNIT_TEST' ) ) {
4828 return $cache[$right];
4829 }
4830
4831 if ( !isset( $wgGroupPermissions['*'][$right] ) || !$wgGroupPermissions['*'][$right] ) {
4832 $cache[$right] = false;
4833 return false;
4834 }
4835
4836 // If it's revoked anywhere, then everyone doesn't have it
4837 foreach ( $wgRevokePermissions as $rights ) {
4838 if ( isset( $rights[$right] ) && $rights[$right] ) {
4839 $cache[$right] = false;
4840 return false;
4841 }
4842 }
4843
4844 // Remove any rights that aren't allowed to the global-session user,
4845 // unless there are no sessions for this endpoint.
4846 if ( !defined( 'MW_NO_SESSION' ) ) {
4847 $allowedRights = SessionManager::getGlobalSession()->getAllowedUserRights();
4848 if ( $allowedRights !== null && !in_array( $right, $allowedRights, true ) ) {
4849 $cache[$right] = false;
4850 return false;
4851 }
4852 }
4853
4854 // Allow extensions to say false
4855 if ( !Hooks::run( 'UserIsEveryoneAllowed', [ $right ] ) ) {
4856 $cache[$right] = false;
4857 return false;
4858 }
4859
4860 $cache[$right] = true;
4861 return true;
4862 }
4863
4864 /**
4865 * Get the localized descriptive name for a group, if it exists
4866 * @deprecated since 1.29 Use UserGroupMembership::getGroupName instead
4867 *
4868 * @param string $group Internal group name
4869 * @return string Localized descriptive group name
4870 */
4871 public static function getGroupName( $group ) {
4872 wfDeprecated( __METHOD__, '1.29' );
4873 return UserGroupMembership::getGroupName( $group );
4874 }
4875
4876 /**
4877 * Get the localized descriptive name for a member of a group, if it exists
4878 * @deprecated since 1.29 Use UserGroupMembership::getGroupMemberName instead
4879 *
4880 * @param string $group Internal group name
4881 * @param string $username Username for gender (since 1.19)
4882 * @return string Localized name for group member
4883 */
4884 public static function getGroupMember( $group, $username = '#' ) {
4885 wfDeprecated( __METHOD__, '1.29' );
4886 return UserGroupMembership::getGroupMemberName( $group, $username );
4887 }
4888
4889 /**
4890 * Return the set of defined explicit groups.
4891 * The implicit groups (by default *, 'user' and 'autoconfirmed')
4892 * are not included, as they are defined automatically, not in the database.
4893 * @return array Array of internal group names
4894 */
4895 public static function getAllGroups() {
4896 global $wgGroupPermissions, $wgRevokePermissions;
4897 return array_diff(
4898 array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
4899 self::getImplicitGroups()
4900 );
4901 }
4902
4903 /**
4904 * Get a list of all available permissions.
4905 * @return string[] Array of permission names
4906 */
4907 public static function getAllRights() {
4908 if ( self::$mAllRights === false ) {
4909 global $wgAvailableRights;
4910 if ( count( $wgAvailableRights ) ) {
4911 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
4912 } else {
4913 self::$mAllRights = self::$mCoreRights;
4914 }
4915 Hooks::run( 'UserGetAllRights', [ &self::$mAllRights ] );
4916 }
4917 return self::$mAllRights;
4918 }
4919
4920 /**
4921 * Get a list of implicit groups
4922 * @return array Array of Strings Array of internal group names
4923 */
4924 public static function getImplicitGroups() {
4925 global $wgImplicitGroups;
4926
4927 $groups = $wgImplicitGroups;
4928 # Deprecated, use $wgImplicitGroups instead
4929 Hooks::run( 'UserGetImplicitGroups', [ &$groups ], '1.25' );
4930
4931 return $groups;
4932 }
4933
4934 /**
4935 * Get the title of a page describing a particular group
4936 * @deprecated since 1.29 Use UserGroupMembership::getGroupPage instead
4937 *
4938 * @param string $group Internal group name
4939 * @return Title|bool Title of the page if it exists, false otherwise
4940 */
4941 public static function getGroupPage( $group ) {
4942 wfDeprecated( __METHOD__, '1.29' );
4943 return UserGroupMembership::getGroupPage( $group );
4944 }
4945
4946 /**
4947 * Create a link to the group in HTML, if available;
4948 * else return the group name.
4949 * @deprecated since 1.29 Use UserGroupMembership::getLink instead, or
4950 * make the link yourself if you need custom text
4951 *
4952 * @param string $group Internal name of the group
4953 * @param string $text The text of the link
4954 * @return string HTML link to the group
4955 */
4956 public static function makeGroupLinkHTML( $group, $text = '' ) {
4957 wfDeprecated( __METHOD__, '1.29' );
4958
4959 if ( $text == '' ) {
4960 $text = UserGroupMembership::getGroupName( $group );
4961 }
4962 $title = UserGroupMembership::getGroupPage( $group );
4963 if ( $title ) {
4964 return MediaWikiServices::getInstance()
4965 ->getLinkRenderer()->makeLink( $title, $text );
4966 } else {
4967 return htmlspecialchars( $text );
4968 }
4969 }
4970
4971 /**
4972 * Create a link to the group in Wikitext, if available;
4973 * else return the group name.
4974 * @deprecated since 1.29 Use UserGroupMembership::getLink instead, or
4975 * make the link yourself if you need custom text
4976 *
4977 * @param string $group Internal name of the group
4978 * @param string $text The text of the link
4979 * @return string Wikilink to the group
4980 */
4981 public static function makeGroupLinkWiki( $group, $text = '' ) {
4982 wfDeprecated( __METHOD__, '1.29' );
4983
4984 if ( $text == '' ) {
4985 $text = UserGroupMembership::getGroupName( $group );
4986 }
4987 $title = UserGroupMembership::getGroupPage( $group );
4988 if ( $title ) {
4989 $page = $title->getFullText();
4990 return "[[$page|$text]]";
4991 } else {
4992 return $text;
4993 }
4994 }
4995
4996 /**
4997 * Returns an array of the groups that a particular group can add/remove.
4998 *
4999 * @param string $group The group to check for whether it can add/remove
5000 * @return array Array( 'add' => array( addablegroups ),
5001 * 'remove' => array( removablegroups ),
5002 * 'add-self' => array( addablegroups to self),
5003 * 'remove-self' => array( removable groups from self) )
5004 */
5005 public static function changeableByGroup( $group ) {
5006 global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
5007
5008 $groups = [
5009 'add' => [],
5010 'remove' => [],
5011 'add-self' => [],
5012 'remove-self' => []
5013 ];
5014
5015 if ( empty( $wgAddGroups[$group] ) ) {
5016 // Don't add anything to $groups
5017 } elseif ( $wgAddGroups[$group] === true ) {
5018 // You get everything
5019 $groups['add'] = self::getAllGroups();
5020 } elseif ( is_array( $wgAddGroups[$group] ) ) {
5021 $groups['add'] = $wgAddGroups[$group];
5022 }
5023
5024 // Same thing for remove
5025 if ( empty( $wgRemoveGroups[$group] ) ) {
5026 // Do nothing
5027 } elseif ( $wgRemoveGroups[$group] === true ) {
5028 $groups['remove'] = self::getAllGroups();
5029 } elseif ( is_array( $wgRemoveGroups[$group] ) ) {
5030 $groups['remove'] = $wgRemoveGroups[$group];
5031 }
5032
5033 // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
5034 if ( empty( $wgGroupsAddToSelf['user'] ) || $wgGroupsAddToSelf['user'] !== true ) {
5035 foreach ( $wgGroupsAddToSelf as $key => $value ) {
5036 if ( is_int( $key ) ) {
5037 $wgGroupsAddToSelf['user'][] = $value;
5038 }
5039 }
5040 }
5041
5042 if ( empty( $wgGroupsRemoveFromSelf['user'] ) || $wgGroupsRemoveFromSelf['user'] !== true ) {
5043 foreach ( $wgGroupsRemoveFromSelf as $key => $value ) {
5044 if ( is_int( $key ) ) {
5045 $wgGroupsRemoveFromSelf['user'][] = $value;
5046 }
5047 }
5048 }
5049
5050 // Now figure out what groups the user can add to him/herself
5051 if ( empty( $wgGroupsAddToSelf[$group] ) ) {
5052 // Do nothing
5053 } elseif ( $wgGroupsAddToSelf[$group] === true ) {
5054 // No idea WHY this would be used, but it's there
5055 $groups['add-self'] = self::getAllGroups();
5056 } elseif ( is_array( $wgGroupsAddToSelf[$group] ) ) {
5057 $groups['add-self'] = $wgGroupsAddToSelf[$group];
5058 }
5059
5060 if ( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
5061 // Do nothing
5062 } elseif ( $wgGroupsRemoveFromSelf[$group] === true ) {
5063 $groups['remove-self'] = self::getAllGroups();
5064 } elseif ( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
5065 $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
5066 }
5067
5068 return $groups;
5069 }
5070
5071 /**
5072 * Returns an array of groups that this user can add and remove
5073 * @return array Array( 'add' => array( addablegroups ),
5074 * 'remove' => array( removablegroups ),
5075 * 'add-self' => array( addablegroups to self),
5076 * 'remove-self' => array( removable groups from self) )
5077 */
5078 public function changeableGroups() {
5079 if ( $this->isAllowed( 'userrights' ) ) {
5080 // This group gives the right to modify everything (reverse-
5081 // compatibility with old "userrights lets you change
5082 // everything")
5083 // Using array_merge to make the groups reindexed
5084 $all = array_merge( self::getAllGroups() );
5085 return [
5086 'add' => $all,
5087 'remove' => $all,
5088 'add-self' => [],
5089 'remove-self' => []
5090 ];
5091 }
5092
5093 // Okay, it's not so simple, we will have to go through the arrays
5094 $groups = [
5095 'add' => [],
5096 'remove' => [],
5097 'add-self' => [],
5098 'remove-self' => []
5099 ];
5100 $addergroups = $this->getEffectiveGroups();
5101
5102 foreach ( $addergroups as $addergroup ) {
5103 $groups = array_merge_recursive(
5104 $groups, $this->changeableByGroup( $addergroup )
5105 );
5106 $groups['add'] = array_unique( $groups['add'] );
5107 $groups['remove'] = array_unique( $groups['remove'] );
5108 $groups['add-self'] = array_unique( $groups['add-self'] );
5109 $groups['remove-self'] = array_unique( $groups['remove-self'] );
5110 }
5111 return $groups;
5112 }
5113
5114 /**
5115 * Deferred version of incEditCountImmediate()
5116 *
5117 * This function, rather than incEditCountImmediate(), should be used for
5118 * most cases as it avoids potential deadlocks caused by concurrent editing.
5119 */
5120 public function incEditCount() {
5121 wfGetDB( DB_MASTER )->onTransactionPreCommitOrIdle(
5122 function () {
5123 $this->incEditCountImmediate();
5124 },
5125 __METHOD__
5126 );
5127 }
5128
5129 /**
5130 * Increment the user's edit-count field.
5131 * Will have no effect for anonymous users.
5132 * @since 1.26
5133 */
5134 public function incEditCountImmediate() {
5135 if ( $this->isAnon() ) {
5136 return;
5137 }
5138
5139 $dbw = wfGetDB( DB_MASTER );
5140 // No rows will be "affected" if user_editcount is NULL
5141 $dbw->update(
5142 'user',
5143 [ 'user_editcount=user_editcount+1' ],
5144 [ 'user_id' => $this->getId(), 'user_editcount IS NOT NULL' ],
5145 __METHOD__
5146 );
5147 // Lazy initialization check...
5148 if ( $dbw->affectedRows() == 0 ) {
5149 // Now here's a goddamn hack...
5150 $dbr = wfGetDB( DB_REPLICA );
5151 if ( $dbr !== $dbw ) {
5152 // If we actually have a replica DB server, the count is
5153 // at least one behind because the current transaction
5154 // has not been committed and replicated.
5155 $this->mEditCount = $this->initEditCount( 1 );
5156 } else {
5157 // But if DB_REPLICA is selecting the master, then the
5158 // count we just read includes the revision that was
5159 // just added in the working transaction.
5160 $this->mEditCount = $this->initEditCount();
5161 }
5162 } else {
5163 if ( $this->mEditCount === null ) {
5164 $this->getEditCount();
5165 $dbr = wfGetDB( DB_REPLICA );
5166 $this->mEditCount += ( $dbr !== $dbw ) ? 1 : 0;
5167 } else {
5168 $this->mEditCount++;
5169 }
5170 }
5171 // Edit count in user cache too
5172 $this->invalidateCache();
5173 }
5174
5175 /**
5176 * Initialize user_editcount from data out of the revision table
5177 *
5178 * @param int $add Edits to add to the count from the revision table
5179 * @return int Number of edits
5180 */
5181 protected function initEditCount( $add = 0 ) {
5182 // Pull from a replica DB to be less cruel to servers
5183 // Accuracy isn't the point anyway here
5184 $dbr = wfGetDB( DB_REPLICA );
5185 $count = (int)$dbr->selectField(
5186 'revision',
5187 'COUNT(rev_user)',
5188 [ 'rev_user' => $this->getId() ],
5189 __METHOD__
5190 );
5191 $count = $count + $add;
5192
5193 $dbw = wfGetDB( DB_MASTER );
5194 $dbw->update(
5195 'user',
5196 [ 'user_editcount' => $count ],
5197 [ 'user_id' => $this->getId() ],
5198 __METHOD__
5199 );
5200
5201 return $count;
5202 }
5203
5204 /**
5205 * Get the description of a given right
5206 *
5207 * @since 1.29
5208 * @param string $right Right to query
5209 * @return string Localized description of the right
5210 */
5211 public static function getRightDescription( $right ) {
5212 $key = "right-$right";
5213 $msg = wfMessage( $key );
5214 return $msg->isDisabled() ? $right : $msg->text();
5215 }
5216
5217 /**
5218 * Get the name of a given grant
5219 *
5220 * @since 1.29
5221 * @param string $grant Grant to query
5222 * @return string Localized name of the grant
5223 */
5224 public static function getGrantName( $grant ) {
5225 $key = "grant-$grant";
5226 $msg = wfMessage( $key );
5227 return $msg->isDisabled() ? $grant : $msg->text();
5228 }
5229
5230 /**
5231 * Add a newuser log entry for this user.
5232 * Before 1.19 the return value was always true.
5233 *
5234 * @deprecated since 1.27, AuthManager handles logging
5235 * @param string|bool $action Account creation type.
5236 * - String, one of the following values:
5237 * - 'create' for an anonymous user creating an account for himself.
5238 * This will force the action's performer to be the created user itself,
5239 * no matter the value of $wgUser
5240 * - 'create2' for a logged in user creating an account for someone else
5241 * - 'byemail' when the created user will receive its password by e-mail
5242 * - 'autocreate' when the user is automatically created (such as by CentralAuth).
5243 * - Boolean means whether the account was created by e-mail (deprecated):
5244 * - true will be converted to 'byemail'
5245 * - false will be converted to 'create' if this object is the same as
5246 * $wgUser and to 'create2' otherwise
5247 * @param string $reason User supplied reason
5248 * @return bool true
5249 */
5250 public function addNewUserLogEntry( $action = false, $reason = '' ) {
5251 return true; // disabled
5252 }
5253
5254 /**
5255 * Add an autocreate newuser log entry for this user
5256 * Used by things like CentralAuth and perhaps other authplugins.
5257 * Consider calling addNewUserLogEntry() directly instead.
5258 *
5259 * @deprecated since 1.27, AuthManager handles logging
5260 * @return bool
5261 */
5262 public function addNewUserLogEntryAutoCreate() {
5263 $this->addNewUserLogEntry( 'autocreate' );
5264
5265 return true;
5266 }
5267
5268 /**
5269 * Load the user options either from cache, the database or an array
5270 *
5271 * @param array $data Rows for the current user out of the user_properties table
5272 */
5273 protected function loadOptions( $data = null ) {
5274 global $wgContLang;
5275
5276 $this->load();
5277
5278 if ( $this->mOptionsLoaded ) {
5279 return;
5280 }
5281
5282 $this->mOptions = self::getDefaultOptions();
5283
5284 if ( !$this->getId() ) {
5285 // For unlogged-in users, load language/variant options from request.
5286 // There's no need to do it for logged-in users: they can set preferences,
5287 // and handling of page content is done by $pageLang->getPreferredVariant() and such,
5288 // so don't override user's choice (especially when the user chooses site default).
5289 $variant = $wgContLang->getDefaultVariant();
5290 $this->mOptions['variant'] = $variant;
5291 $this->mOptions['language'] = $variant;
5292 $this->mOptionsLoaded = true;
5293 return;
5294 }
5295
5296 // Maybe load from the object
5297 if ( !is_null( $this->mOptionOverrides ) ) {
5298 wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" );
5299 foreach ( $this->mOptionOverrides as $key => $value ) {
5300 $this->mOptions[$key] = $value;
5301 }
5302 } else {
5303 if ( !is_array( $data ) ) {
5304 wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" );
5305 // Load from database
5306 $dbr = ( $this->queryFlagsUsed & self::READ_LATEST )
5307 ? wfGetDB( DB_MASTER )
5308 : wfGetDB( DB_REPLICA );
5309
5310 $res = $dbr->select(
5311 'user_properties',
5312 [ 'up_property', 'up_value' ],
5313 [ 'up_user' => $this->getId() ],
5314 __METHOD__
5315 );
5316
5317 $this->mOptionOverrides = [];
5318 $data = [];
5319 foreach ( $res as $row ) {
5320 // Convert '0' to 0. PHP's boolean conversion considers them both
5321 // false, but e.g. JavaScript considers the former as true.
5322 // @todo: T54542 Somehow determine the desired type (string/int/bool)
5323 // and convert all values here.
5324 if ( $row->up_value === '0' ) {
5325 $row->up_value = 0;
5326 }
5327 $data[$row->up_property] = $row->up_value;
5328 }
5329 }
5330
5331 // Convert the email blacklist from a new line delimited string
5332 // to an array of ids.
5333 if ( isset( $data['email-blacklist'] ) && $data['email-blacklist'] ) {
5334 $data['email-blacklist'] = array_map( 'intval', explode( "\n", $data['email-blacklist'] ) );
5335 }
5336
5337 foreach ( $data as $property => $value ) {
5338 $this->mOptionOverrides[$property] = $value;
5339 $this->mOptions[$property] = $value;
5340 }
5341 }
5342
5343 $this->mOptionsLoaded = true;
5344
5345 Hooks::run( 'UserLoadOptions', [ $this, &$this->mOptions ] );
5346 }
5347
5348 /**
5349 * Saves the non-default options for this user, as previously set e.g. via
5350 * setOption(), in the database's "user_properties" (preferences) table.
5351 * Usually used via saveSettings().
5352 */
5353 protected function saveOptions() {
5354 $this->loadOptions();
5355
5356 // Not using getOptions(), to keep hidden preferences in database
5357 $saveOptions = $this->mOptions;
5358
5359 // Convert usernames to ids.
5360 if ( isset( $this->mOptions['email-blacklist'] ) ) {
5361 if ( $this->mOptions['email-blacklist'] ) {
5362 $value = $this->mOptions['email-blacklist'];
5363 // Email Blacklist may be an array of ids or a string of new line
5364 // delimnated user names.
5365 if ( is_array( $value ) ) {
5366 $ids = array_filter( $value, 'is_numeric' );
5367 } else {
5368 $lookup = CentralIdLookup::factory();
5369 $ids = $lookup->centralIdsFromNames( explode( "\n", $value ), $this );
5370 }
5371 $this->mOptions['email-blacklist'] = $ids;
5372 $saveOptions['email-blacklist'] = implode( "\n", $this->mOptions['email-blacklist'] );
5373 } else {
5374 // If the blacklist is empty, set it to null rather than an empty string.
5375 $this->mOptions['email-blacklist'] = null;
5376 }
5377 }
5378
5379 // Allow hooks to abort, for instance to save to a global profile.
5380 // Reset options to default state before saving.
5381 if ( !Hooks::run( 'UserSaveOptions', [ $this, &$saveOptions ] ) ) {
5382 return;
5383 }
5384
5385 $userId = $this->getId();
5386
5387 $insert_rows = []; // all the new preference rows
5388 foreach ( $saveOptions as $key => $value ) {
5389 // Don't bother storing default values
5390 $defaultOption = self::getDefaultOption( $key );
5391 if ( ( $defaultOption === null && $value !== false && $value !== null )
5392 || $value != $defaultOption
5393 ) {
5394 $insert_rows[] = [
5395 'up_user' => $userId,
5396 'up_property' => $key,
5397 'up_value' => $value,
5398 ];
5399 }
5400 }
5401
5402 $dbw = wfGetDB( DB_MASTER );
5403
5404 $res = $dbw->select( 'user_properties',
5405 [ 'up_property', 'up_value' ], [ 'up_user' => $userId ], __METHOD__ );
5406
5407 // Find prior rows that need to be removed or updated. These rows will
5408 // all be deleted (the latter so that INSERT IGNORE applies the new values).
5409 $keysDelete = [];
5410 foreach ( $res as $row ) {
5411 if ( !isset( $saveOptions[$row->up_property] )
5412 || strcmp( $saveOptions[$row->up_property], $row->up_value ) != 0
5413 ) {
5414 $keysDelete[] = $row->up_property;
5415 }
5416 }
5417
5418 if ( count( $keysDelete ) ) {
5419 // Do the DELETE by PRIMARY KEY for prior rows.
5420 // In the past a very large portion of calls to this function are for setting
5421 // 'rememberpassword' for new accounts (a preference that has since been removed).
5422 // Doing a blanket per-user DELETE for new accounts with no rows in the table
5423 // caused gap locks on [max user ID,+infinity) which caused high contention since
5424 // updates would pile up on each other as they are for higher (newer) user IDs.
5425 // It might not be necessary these days, but it shouldn't hurt either.
5426 $dbw->delete( 'user_properties',
5427 [ 'up_user' => $userId, 'up_property' => $keysDelete ], __METHOD__ );
5428 }
5429 // Insert the new preference rows
5430 $dbw->insert( 'user_properties', $insert_rows, __METHOD__, [ 'IGNORE' ] );
5431 }
5432
5433 /**
5434 * Lazily instantiate and return a factory object for making passwords
5435 *
5436 * @deprecated since 1.27, create a PasswordFactory directly instead
5437 * @return PasswordFactory
5438 */
5439 public static function getPasswordFactory() {
5440 wfDeprecated( __METHOD__, '1.27' );
5441 $ret = new PasswordFactory();
5442 $ret->init( RequestContext::getMain()->getConfig() );
5443 return $ret;
5444 }
5445
5446 /**
5447 * Provide an array of HTML5 attributes to put on an input element
5448 * intended for the user to enter a new password. This may include
5449 * required, title, and/or pattern, depending on $wgMinimalPasswordLength.
5450 *
5451 * Do *not* use this when asking the user to enter his current password!
5452 * Regardless of configuration, users may have invalid passwords for whatever
5453 * reason (e.g., they were set before requirements were tightened up).
5454 * Only use it when asking for a new password, like on account creation or
5455 * ResetPass.
5456 *
5457 * Obviously, you still need to do server-side checking.
5458 *
5459 * NOTE: A combination of bugs in various browsers means that this function
5460 * actually just returns array() unconditionally at the moment. May as
5461 * well keep it around for when the browser bugs get fixed, though.
5462 *
5463 * @todo FIXME: This does not belong here; put it in Html or Linker or somewhere
5464 *
5465 * @deprecated since 1.27
5466 * @return array Array of HTML attributes suitable for feeding to
5467 * Html::element(), directly or indirectly. (Don't feed to Xml::*()!
5468 * That will get confused by the boolean attribute syntax used.)
5469 */
5470 public static function passwordChangeInputAttribs() {
5471 global $wgMinimalPasswordLength;
5472
5473 if ( $wgMinimalPasswordLength == 0 ) {
5474 return [];
5475 }
5476
5477 # Note that the pattern requirement will always be satisfied if the
5478 # input is empty, so we need required in all cases.
5479
5480 # @todo FIXME: T25769: This needs to not claim the password is required
5481 # if e-mail confirmation is being used. Since HTML5 input validation
5482 # is b0rked anyway in some browsers, just return nothing. When it's
5483 # re-enabled, fix this code to not output required for e-mail
5484 # registration.
5485 # $ret = array( 'required' );
5486 $ret = [];
5487
5488 # We can't actually do this right now, because Opera 9.6 will print out
5489 # the entered password visibly in its error message! When other
5490 # browsers add support for this attribute, or Opera fixes its support,
5491 # we can add support with a version check to avoid doing this on Opera
5492 # versions where it will be a problem. Reported to Opera as
5493 # DSK-262266, but they don't have a public bug tracker for us to follow.
5494 /*
5495 if ( $wgMinimalPasswordLength > 1 ) {
5496 $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}';
5497 $ret['title'] = wfMessage( 'passwordtooshort' )
5498 ->numParams( $wgMinimalPasswordLength )->text();
5499 }
5500 */
5501
5502 return $ret;
5503 }
5504
5505 /**
5506 * Return the list of user fields that should be selected to create
5507 * a new user object.
5508 * @deprecated since 1.31, use self::getQueryInfo() instead.
5509 * @return array
5510 */
5511 public static function selectFields() {
5512 wfDeprecated( __METHOD__, '1.31' );
5513 return [
5514 'user_id',
5515 'user_name',
5516 'user_real_name',
5517 'user_email',
5518 'user_touched',
5519 'user_token',
5520 'user_email_authenticated',
5521 'user_email_token',
5522 'user_email_token_expires',
5523 'user_registration',
5524 'user_editcount',
5525 ];
5526 }
5527
5528 /**
5529 * Return the tables, fields, and join conditions to be selected to create
5530 * a new user object.
5531 * @since 1.31
5532 * @return array With three keys:
5533 * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
5534 * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
5535 * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
5536 */
5537 public static function getQueryInfo() {
5538 return [
5539 'tables' => [ 'user' ],
5540 'fields' => [
5541 'user_id',
5542 'user_name',
5543 'user_real_name',
5544 'user_email',
5545 'user_touched',
5546 'user_token',
5547 'user_email_authenticated',
5548 'user_email_token',
5549 'user_email_token_expires',
5550 'user_registration',
5551 'user_editcount',
5552 ],
5553 'joins' => [],
5554 ];
5555 }
5556
5557 /**
5558 * Factory function for fatal permission-denied errors
5559 *
5560 * @since 1.22
5561 * @param string $permission User right required
5562 * @return Status
5563 */
5564 static function newFatalPermissionDeniedStatus( $permission ) {
5565 global $wgLang;
5566
5567 $groups = [];
5568 foreach ( self::getGroupsWithPermission( $permission ) as $group ) {
5569 $groups[] = UserGroupMembership::getLink( $group, RequestContext::getMain(), 'wiki' );
5570 }
5571
5572 if ( $groups ) {
5573 return Status::newFatal( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) );
5574 } else {
5575 return Status::newFatal( 'badaccess-group0' );
5576 }
5577 }
5578
5579 /**
5580 * Get a new instance of this user that was loaded from the master via a locking read
5581 *
5582 * Use this instead of the main context User when updating that user. This avoids races
5583 * where that user was loaded from a replica DB or even the master but without proper locks.
5584 *
5585 * @return User|null Returns null if the user was not found in the DB
5586 * @since 1.27
5587 */
5588 public function getInstanceForUpdate() {
5589 if ( !$this->getId() ) {
5590 return null; // anon
5591 }
5592
5593 $user = self::newFromId( $this->getId() );
5594 if ( !$user->loadFromId( self::READ_EXCLUSIVE ) ) {
5595 return null;
5596 }
5597
5598 return $user;
5599 }
5600
5601 /**
5602 * Checks if two user objects point to the same user.
5603 *
5604 * @since 1.25
5605 * @param User $user
5606 * @return bool
5607 */
5608 public function equals( User $user ) {
5609 return $this->getName() === $user->getName();
5610 }
5611 }