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