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