MediaWiki REL1_34
User.php
Go to the documentation of this file.
1<?php
34use Wikimedia\Assert\Assert;
35use Wikimedia\IPSet;
36use Wikimedia\ScopedCallback;
40
52
56 const TOKEN_LENGTH = 32;
57
61 const INVALID_TOKEN = '*** INVALID ***';
62
67 const VERSION = 14;
68
74
78 const CHECK_USER_RIGHTS = true;
79
83 const IGNORE_USER_RIGHTS = false;
84
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 'mGroupMemberships',
107 // user_properties table
108 'mOptionOverrides',
109 // actor table
110 'mActorId',
111 ];
112
114 private static $reservedUsernames = false;
115
117 // @{
119 public $mId;
121 public $mName;
123 protected $mActorId;
126
128 public $mEmail;
130 public $mTouched;
132 protected $mQuickTouched;
134 protected $mToken;
138 protected $mEmailToken;
142 protected $mRegistration;
144 protected $mEditCount;
149 // @}
150
151 // @{
156
160 protected $mLoadedItems = [];
161 // @}
162
173 public $mFrom;
174
179 protected $mNewtalk;
185 protected $mHash;
187 protected $mBlockreason;
193 protected $mFormerGroups;
195 protected $mGlobalBlock;
197 protected $mLocked;
201 public $mOptions;
202
204 private $mRequest;
205
207 public $mBlock;
208
211
214
216 protected $queryFlagsUsed = self::READ_NORMAL;
217
219 public static $idCacheByName = [];
220
232 public function __construct() {
233 $this->clearInstanceCache( 'defaults' );
234 }
235
239 public function __toString() {
240 return (string)$this->getName();
241 }
242
243 public function &__get( $name ) {
244 // A shortcut for $mRights deprecation phase
245 if ( $name === 'mRights' ) {
246 $copy = $this->getRights();
247 return $copy;
248 } elseif ( !property_exists( $this, $name ) ) {
249 // T227688 - do not break $u->foo['bar'] = 1
250 wfLogWarning( 'tried to get non-existent property' );
251 $this->$name = null;
252 return $this->$name;
253 } else {
254 wfLogWarning( 'tried to get non-visible property' );
255 $null = null;
256 return $null;
257 }
258 }
259
260 public function __set( $name, $value ) {
261 // A shortcut for $mRights deprecation phase, only known legitimate use was for
262 // testing purposes, other uses seem bad in principle
263 if ( $name === 'mRights' ) {
264 MediaWikiServices::getInstance()->getPermissionManager()->overrideUserRightsForTesting(
265 $this,
266 is_null( $value ) ? [] : $value
267 );
268 } elseif ( !property_exists( $this, $name ) ) {
269 $this->$name = $value;
270 } else {
271 wfLogWarning( 'tried to set non-visible property' );
272 }
273 }
274
289 public function isSafeToLoad() {
290 global $wgFullyInitialised;
291
292 // The user is safe to load if:
293 // * MW_NO_SESSION is undefined AND $wgFullyInitialised is true (safe to use session data)
294 // * mLoadedItems === true (already loaded)
295 // * mFrom !== 'session' (sessions not involved at all)
296
297 return ( !defined( 'MW_NO_SESSION' ) && $wgFullyInitialised ) ||
298 $this->mLoadedItems === true || $this->mFrom !== 'session';
299 }
300
306 public function load( $flags = self::READ_NORMAL ) {
307 global $wgFullyInitialised;
308
309 if ( $this->mLoadedItems === true ) {
310 return;
311 }
312
313 // Set it now to avoid infinite recursion in accessors
314 $oldLoadedItems = $this->mLoadedItems;
315 $this->mLoadedItems = true;
316 $this->queryFlagsUsed = $flags;
317
318 // If this is called too early, things are likely to break.
319 if ( !$wgFullyInitialised && $this->mFrom === 'session' ) {
320 \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
321 ->warning( 'User::loadFromSession called before the end of Setup.php', [
322 'exception' => new Exception( 'User::loadFromSession called before the end of Setup.php' ),
323 ] );
324 $this->loadDefaults();
325 $this->mLoadedItems = $oldLoadedItems;
326 return;
327 }
328
329 switch ( $this->mFrom ) {
330 case 'defaults':
331 $this->loadDefaults();
332 break;
333 case 'id':
334 // Make sure this thread sees its own changes, if the ID isn't 0
335 if ( $this->mId != 0 ) {
336 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
337 if ( $lb->hasOrMadeRecentMasterChanges() ) {
338 $flags |= self::READ_LATEST;
339 $this->queryFlagsUsed = $flags;
340 }
341 }
342
343 $this->loadFromId( $flags );
344 break;
345 case 'actor':
346 case 'name':
347 // Make sure this thread sees its own changes
348 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
349 if ( $lb->hasOrMadeRecentMasterChanges() ) {
350 $flags |= self::READ_LATEST;
351 $this->queryFlagsUsed = $flags;
352 }
353
354 list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
355 $row = wfGetDB( $index )->selectRow(
356 'actor',
357 [ 'actor_id', 'actor_user', 'actor_name' ],
358 $this->mFrom === 'name' ? [ 'actor_name' => $this->mName ] : [ 'actor_id' => $this->mActorId ],
359 __METHOD__,
360 $options
361 );
362
363 if ( !$row ) {
364 // Ugh.
365 $this->loadDefaults( $this->mFrom === 'name' ? $this->mName : false );
366 } elseif ( $row->actor_user ) {
367 $this->mId = $row->actor_user;
368 $this->loadFromId( $flags );
369 } else {
370 $this->loadDefaults( $row->actor_name, $row->actor_id );
371 }
372 break;
373 case 'session':
374 if ( !$this->loadFromSession() ) {
375 // Loading from session failed. Load defaults.
376 $this->loadDefaults();
377 }
378 Hooks::run( 'UserLoadAfterLoadFromSession', [ $this ] );
379 break;
380 default:
381 throw new UnexpectedValueException(
382 "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
383 }
384 }
385
391 public function loadFromId( $flags = self::READ_NORMAL ) {
392 if ( $this->mId == 0 ) {
393 // Anonymous users are not in the database (don't need cache)
394 $this->loadDefaults();
395 return false;
396 }
397
398 // Try cache (unless this needs data from the master DB).
399 // NOTE: if this thread called saveSettings(), the cache was cleared.
400 $latest = DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST );
401 if ( $latest ) {
402 if ( !$this->loadFromDatabase( $flags ) ) {
403 // Can't load from ID
404 return false;
405 }
406 } else {
407 $this->loadFromCache();
408 }
409
410 $this->mLoadedItems = true;
411 $this->queryFlagsUsed = $flags;
412
413 return true;
414 }
415
421 public static function purge( $dbDomain, $userId ) {
422 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
423 $key = $cache->makeGlobalKey( 'user', 'id', $dbDomain, $userId );
424 $cache->delete( $key );
425 }
426
432 protected function getCacheKey( WANObjectCache $cache ) {
433 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
434
435 return $cache->makeGlobalKey( 'user', 'id', $lbFactory->getLocalDomainID(), $this->mId );
436 }
437
444 $id = $this->getId();
445
446 return $id ? [ $this->getCacheKey( $cache ) ] : [];
447 }
448
455 protected function loadFromCache() {
456 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
457 $data = $cache->getWithSetCallback(
458 $this->getCacheKey( $cache ),
459 $cache::TTL_HOUR,
460 function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache ) {
461 $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
462 wfDebug( "User: cache miss for user {$this->mId}\n" );
463
464 $this->loadFromDatabase( self::READ_NORMAL );
465 $this->loadGroups();
466 $this->loadOptions();
467
468 $data = [];
469 foreach ( self::$mCacheVars as $name ) {
470 $data[$name] = $this->$name;
471 }
472
473 $ttl = $cache->adaptiveTTL( wfTimestamp( TS_UNIX, $this->mTouched ), $ttl );
474
475 // if a user group membership is about to expire, the cache needs to
476 // expire at that time (T163691)
477 foreach ( $this->mGroupMemberships as $ugm ) {
478 if ( $ugm->getExpiry() ) {
479 $secondsUntilExpiry = wfTimestamp( TS_UNIX, $ugm->getExpiry() ) - time();
480 if ( $secondsUntilExpiry > 0 && $secondsUntilExpiry < $ttl ) {
481 $ttl = $secondsUntilExpiry;
482 }
483 }
484 }
485
486 return $data;
487 },
488 [ 'pcTTL' => $cache::TTL_PROC_LONG, 'version' => self::VERSION ]
489 );
490
491 // Restore from cache
492 foreach ( self::$mCacheVars as $name ) {
493 $this->$name = $data[$name];
494 }
495
496 return true;
497 }
498
500 // @{
501
518 public static function newFromName( $name, $validate = 'valid' ) {
519 if ( $validate === true ) {
520 $validate = 'valid';
521 }
522 $name = self::getCanonicalName( $name, $validate );
523 if ( $name === false ) {
524 return false;
525 }
526
527 // Create unloaded user object
528 $u = new User;
529 $u->mName = $name;
530 $u->mFrom = 'name';
531 $u->setItemLoaded( 'name' );
532
533 return $u;
534 }
535
542 public static function newFromId( $id ) {
543 $u = new User;
544 $u->mId = $id;
545 $u->mFrom = 'id';
546 $u->setItemLoaded( 'id' );
547 return $u;
548 }
549
557 public static function newFromActorId( $id ) {
558 $u = new User;
559 $u->mActorId = $id;
560 $u->mFrom = 'actor';
561 $u->setItemLoaded( 'actor' );
562 return $u;
563 }
564
574 public static function newFromIdentity( UserIdentity $identity ) {
575 if ( $identity instanceof User ) {
576 return $identity;
577 }
578
579 return self::newFromAnyId(
580 $identity->getId() === 0 ? null : $identity->getId(),
581 $identity->getName() === '' ? null : $identity->getName(),
582 $identity->getActorId() === 0 ? null : $identity->getActorId()
583 );
584 }
585
599 public static function newFromAnyId( $userId, $userName, $actorId, $dbDomain = false ) {
600 // Stop-gap solution for the problem described in T222212.
601 // Force the User ID and Actor ID to zero for users loaded from the database
602 // of another wiki, to prevent subtle data corruption and confusing failure modes.
603 if ( $dbDomain !== false ) {
604 $userId = 0;
605 $actorId = 0;
606 }
607
608 $user = new User;
609 $user->mFrom = 'defaults';
610
611 if ( $actorId !== null ) {
612 $user->mActorId = (int)$actorId;
613 if ( $user->mActorId !== 0 ) {
614 $user->mFrom = 'actor';
615 }
616 $user->setItemLoaded( 'actor' );
617 }
618
619 if ( $userName !== null && $userName !== '' ) {
620 $user->mName = $userName;
621 $user->mFrom = 'name';
622 $user->setItemLoaded( 'name' );
623 }
624
625 if ( $userId !== null ) {
626 $user->mId = (int)$userId;
627 if ( $user->mId !== 0 ) {
628 $user->mFrom = 'id';
629 }
630 $user->setItemLoaded( 'id' );
631 }
632
633 if ( $user->mFrom === 'defaults' ) {
634 throw new InvalidArgumentException(
635 'Cannot create a user with no name, no ID, and no actor ID'
636 );
637 }
638
639 return $user;
640 }
641
653 public static function newFromConfirmationCode( $code, $flags = 0 ) {
654 $db = ( $flags & self::READ_LATEST ) == self::READ_LATEST
655 ? wfGetDB( DB_MASTER )
656 : wfGetDB( DB_REPLICA );
657
658 $id = $db->selectField(
659 'user',
660 'user_id',
661 [
662 'user_email_token' => md5( $code ),
663 'user_email_token_expires > ' . $db->addQuotes( $db->timestamp() ),
664 ]
665 );
666
667 return $id ? self::newFromId( $id ) : null;
668 }
669
677 public static function newFromSession( WebRequest $request = null ) {
678 $user = new User;
679 $user->mFrom = 'session';
680 $user->mRequest = $request;
681 return $user;
682 }
683
699 public static function newFromRow( $row, $data = null ) {
700 $user = new User;
701 $user->loadFromRow( $row, $data );
702 return $user;
703 }
704
740 public static function newSystemUser( $name, $options = [] ) {
741 $options += [
742 'validate' => 'valid',
743 'create' => true,
744 'steal' => false,
745 ];
746
747 $name = self::getCanonicalName( $name, $options['validate'] );
748 if ( $name === false ) {
749 return null;
750 }
751
753 $userQuery = self::getQueryInfo();
754 $row = $dbr->selectRow(
755 $userQuery['tables'],
756 $userQuery['fields'],
757 [ 'user_name' => $name ],
758 __METHOD__,
759 [],
760 $userQuery['joins']
761 );
762 if ( !$row ) {
763 // Try the master database...
764 $dbw = wfGetDB( DB_MASTER );
765 $row = $dbw->selectRow(
766 $userQuery['tables'],
767 $userQuery['fields'],
768 [ 'user_name' => $name ],
769 __METHOD__,
770 [],
771 $userQuery['joins']
772 );
773 }
774
775 if ( !$row ) {
776 // No user. Create it?
777 if ( !$options['create'] ) {
778 // No.
779 return null;
780 }
781
782 // If it's a reserved user that had an anonymous actor created for it at
783 // some point, we need special handling.
784 if ( !self::isValidUserName( $name ) || self::isUsableName( $name ) ) {
785 // Not reserved, so just create it.
786 return self::createNew( $name, [ 'token' => self::INVALID_TOKEN ] );
787 }
788
789 // It is reserved. Check for an anonymous actor row.
790 $dbw = wfGetDB( DB_MASTER );
791 return $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) use ( $name ) {
792 $row = $dbw->selectRow(
793 'actor',
794 [ 'actor_id' ],
795 [ 'actor_name' => $name, 'actor_user' => null ],
796 $fname,
797 [ 'FOR UPDATE' ]
798 );
799 if ( !$row ) {
800 // No anonymous actor.
801 return self::createNew( $name, [ 'token' => self::INVALID_TOKEN ] );
802 }
803
804 // There is an anonymous actor. Delete the actor row so we can create the user,
805 // then restore the old actor_id so as to not break existing references.
806 // @todo If MediaWiki ever starts using foreign keys for `actor`, this will break things.
807 $dbw->delete( 'actor', [ 'actor_id' => $row->actor_id ], $fname );
808 $user = self::createNew( $name, [ 'token' => self::INVALID_TOKEN ] );
809 $dbw->update(
810 'actor',
811 [ 'actor_id' => $row->actor_id ],
812 [ 'actor_id' => $user->getActorId() ],
813 $fname
814 );
815 $user->clearInstanceCache( 'id' );
816 $user->invalidateCache();
817 return $user;
818 } );
819 }
820
821 $user = self::newFromRow( $row );
822
823 // A user is considered to exist as a non-system user if it can
824 // authenticate, or has an email set, or has a non-invalid token.
825 if ( $user->mEmail || $user->mToken !== self::INVALID_TOKEN ||
826 AuthManager::singleton()->userCanAuthenticate( $name )
827 ) {
828 // User exists. Steal it?
829 if ( !$options['steal'] ) {
830 return null;
831 }
832
833 AuthManager::singleton()->revokeAccessForUser( $name );
834
835 $user->invalidateEmail();
836 $user->mToken = self::INVALID_TOKEN;
837 $user->saveSettings();
838 SessionManager::singleton()->preventSessionsForUser( $user->getName() );
839 }
840
841 return $user;
842 }
843
844 // @}
845
851 public static function whoIs( $id ) {
852 return UserCache::singleton()->getProp( $id, 'name' );
853 }
854
861 public static function whoIsReal( $id ) {
862 return UserCache::singleton()->getProp( $id, 'real_name' );
863 }
864
871 public static function idFromName( $name, $flags = self::READ_NORMAL ) {
872 // Don't explode on self::$idCacheByName[$name] if $name is not a string but e.g. a User object
873 $name = (string)$name;
874 $nt = Title::makeTitleSafe( NS_USER, $name );
875 if ( is_null( $nt ) ) {
876 // Illegal name
877 return null;
878 }
879
880 if ( !( $flags & self::READ_LATEST ) && array_key_exists( $name, self::$idCacheByName ) ) {
881 return is_null( self::$idCacheByName[$name] ) ? null : (int)self::$idCacheByName[$name];
882 }
883
884 list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
885 $db = wfGetDB( $index );
886
887 $s = $db->selectRow(
888 'user',
889 [ 'user_id' ],
890 [ 'user_name' => $nt->getText() ],
891 __METHOD__,
892 $options
893 );
894
895 if ( $s === false ) {
896 $result = null;
897 } else {
898 $result = (int)$s->user_id;
899 }
900
901 if ( count( self::$idCacheByName ) >= 1000 ) {
902 self::$idCacheByName = [];
903 }
904
905 self::$idCacheByName[$name] = $result;
906
907 return $result;
908 }
909
913 public static function resetIdByNameCache() {
914 self::$idCacheByName = [];
915 }
916
933 public static function isIP( $name ) {
934 return preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/', $name )
935 || IP::isIPv6( $name );
936 }
937
944 public function isIPRange() {
945 return IP::isValidRange( $this->mName );
946 }
947
959 public static function isValidUserName( $name ) {
960 global $wgMaxNameChars;
961
962 if ( $name == ''
963 || self::isIP( $name )
964 || strpos( $name, '/' ) !== false
965 || strlen( $name ) > $wgMaxNameChars
966 || $name != MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $name )
967 ) {
968 return false;
969 }
970
971 // Ensure that the name can't be misresolved as a different title,
972 // such as with extra namespace keys at the start.
973 $parsed = Title::newFromText( $name );
974 if ( is_null( $parsed )
975 || $parsed->getNamespace()
976 || strcmp( $name, $parsed->getPrefixedText() ) ) {
977 return false;
978 }
979
980 // Check an additional blacklist of troublemaker characters.
981 // Should these be merged into the title char list?
982 $unicodeBlacklist = '/[' .
983 '\x{0080}-\x{009f}' . # iso-8859-1 control chars
984 '\x{00a0}' . # non-breaking space
985 '\x{2000}-\x{200f}' . # various whitespace
986 '\x{2028}-\x{202f}' . # breaks and control chars
987 '\x{3000}' . # ideographic space
988 '\x{e000}-\x{f8ff}' . # private use
989 ']/u';
990 if ( preg_match( $unicodeBlacklist, $name ) ) {
991 return false;
992 }
993
994 return true;
995 }
996
1008 public static function isUsableName( $name ) {
1009 global $wgReservedUsernames;
1010 // Must be a valid username, obviously ;)
1011 if ( !self::isValidUserName( $name ) ) {
1012 return false;
1013 }
1014
1015 if ( !self::$reservedUsernames ) {
1016 self::$reservedUsernames = $wgReservedUsernames;
1017 Hooks::run( 'UserGetReservedNames', [ &self::$reservedUsernames ] );
1018 }
1019
1020 // Certain names may be reserved for batch processes.
1021 foreach ( self::$reservedUsernames as $reserved ) {
1022 if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
1023 $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->plain();
1024 }
1025 if ( $reserved == $name ) {
1026 return false;
1027 }
1028 }
1029 return true;
1030 }
1031
1042 public static function findUsersByGroup( $groups, $limit = 5000, $after = null ) {
1043 if ( $groups === [] ) {
1045 }
1046
1047 $groups = array_unique( (array)$groups );
1048 $limit = min( 5000, $limit );
1049
1050 $conds = [ 'ug_group' => $groups ];
1051 if ( $after !== null ) {
1052 $conds[] = 'ug_user > ' . (int)$after;
1053 }
1054
1055 $dbr = wfGetDB( DB_REPLICA );
1056 $ids = $dbr->selectFieldValues(
1057 'user_groups',
1058 'ug_user',
1059 $conds,
1060 __METHOD__,
1061 [
1062 'DISTINCT' => true,
1063 'ORDER BY' => 'ug_user',
1064 'LIMIT' => $limit,
1065 ]
1066 ) ?: [];
1067 return UserArray::newFromIDs( $ids );
1068 }
1069
1082 public static function isCreatableName( $name ) {
1084
1085 // Ensure that the username isn't longer than 235 bytes, so that
1086 // (at least for the builtin skins) user javascript and css files
1087 // will work. (T25080)
1088 if ( strlen( $name ) > 235 ) {
1089 wfDebugLog( 'username', __METHOD__ .
1090 ": '$name' invalid due to length" );
1091 return false;
1092 }
1093
1094 // Preg yells if you try to give it an empty string
1095 if ( $wgInvalidUsernameCharacters !== '' &&
1096 preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name )
1097 ) {
1098 wfDebugLog( 'username', __METHOD__ .
1099 ": '$name' invalid due to wgInvalidUsernameCharacters" );
1100 return false;
1101 }
1102
1103 return self::isUsableName( $name );
1104 }
1105
1112 public function isValidPassword( $password ) {
1113 // simple boolean wrapper for checkPasswordValidity
1114 return $this->checkPasswordValidity( $password )->isGood();
1115 }
1116
1138 public function checkPasswordValidity( $password ) {
1139 global $wgPasswordPolicy;
1140
1141 $upp = new UserPasswordPolicy(
1142 $wgPasswordPolicy['policies'],
1143 $wgPasswordPolicy['checks']
1144 );
1145
1146 $status = Status::newGood( [] );
1147 $result = false; // init $result to false for the internal checks
1148
1149 if ( !Hooks::run( 'isValidPassword', [ $password, &$result, $this ] ) ) {
1150 $status->error( $result );
1151 return $status;
1152 }
1153
1154 if ( $result === false ) {
1155 $status->merge( $upp->checkUserPassword( $this, $password ), true );
1156 return $status;
1157 }
1158
1159 if ( $result === true ) {
1160 return $status;
1161 }
1162
1163 $status->error( $result );
1164 return $status; // the isValidPassword hook set a string $result and returned true
1165 }
1166
1180 public static function getCanonicalName( $name, $validate = 'valid' ) {
1181 // Force usernames to capital
1182 $name = MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $name );
1183
1184 # Reject names containing '#'; these will be cleaned up
1185 # with title normalisation, but then it's too late to
1186 # check elsewhere
1187 if ( strpos( $name, '#' ) !== false ) {
1188 return false;
1189 }
1190
1191 // Clean up name according to title rules,
1192 // but only when validation is requested (T14654)
1193 $t = ( $validate !== false ) ?
1194 Title::newFromText( $name, NS_USER ) : Title::makeTitle( NS_USER, $name );
1195 // Check for invalid titles
1196 if ( is_null( $t ) || $t->getNamespace() !== NS_USER || $t->isExternal() ) {
1197 return false;
1198 }
1199
1200 $name = $t->getText();
1201
1202 switch ( $validate ) {
1203 case false:
1204 break;
1205 case 'valid':
1206 if ( !self::isValidUserName( $name ) ) {
1207 $name = false;
1208 }
1209 break;
1210 case 'usable':
1211 if ( !self::isUsableName( $name ) ) {
1212 $name = false;
1213 }
1214 break;
1215 case 'creatable':
1216 if ( !self::isCreatableName( $name ) ) {
1217 $name = false;
1218 }
1219 break;
1220 default:
1221 throw new InvalidArgumentException(
1222 'Invalid parameter value for $validate in ' . __METHOD__ );
1223 }
1224 return $name;
1225 }
1226
1236 public function loadDefaults( $name = false, $actorId = null ) {
1237 $this->mId = 0;
1238 $this->mName = $name;
1239 $this->mActorId = $actorId;
1240 $this->mRealName = '';
1241 $this->mEmail = '';
1242 $this->mOptionOverrides = null;
1243 $this->mOptionsLoaded = false;
1244
1245 $loggedOut = $this->mRequest && !defined( 'MW_NO_SESSION' )
1246 ? $this->mRequest->getSession()->getLoggedOutTimestamp() : 0;
1247 if ( $loggedOut !== 0 ) {
1248 $this->mTouched = wfTimestamp( TS_MW, $loggedOut );
1249 } else {
1250 $this->mTouched = '1'; # Allow any pages to be cached
1251 }
1252
1253 $this->mToken = null; // Don't run cryptographic functions till we need a token
1254 $this->mEmailAuthenticated = null;
1255 $this->mEmailToken = '';
1256 $this->mEmailTokenExpires = null;
1257 $this->mRegistration = wfTimestamp( TS_MW );
1258 $this->mGroupMemberships = [];
1259
1260 Hooks::run( 'UserLoadDefaults', [ $this, $name ] );
1261 }
1262
1275 public function isItemLoaded( $item, $all = 'all' ) {
1276 return ( $this->mLoadedItems === true && $all === 'all' ) ||
1277 ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true );
1278 }
1279
1285 protected function setItemLoaded( $item ) {
1286 if ( is_array( $this->mLoadedItems ) ) {
1287 $this->mLoadedItems[$item] = true;
1288 }
1289 }
1290
1296 private function loadFromSession() {
1297 // MediaWiki\Session\Session already did the necessary authentication of the user
1298 // returned here, so just use it if applicable.
1299 $session = $this->getRequest()->getSession();
1300 $user = $session->getUser();
1301 if ( $user->isLoggedIn() ) {
1302 $this->loadFromUserObject( $user );
1303
1304 // If this user is autoblocked, set a cookie to track the block. This has to be done on
1305 // every session load, because an autoblocked editor might not edit again from the same
1306 // IP address after being blocked.
1307 MediaWikiServices::getInstance()->getBlockManager()->trackBlockWithCookie( $this );
1308
1309 // Other code expects these to be set in the session, so set them.
1310 $session->set( 'wsUserID', $this->getId() );
1311 $session->set( 'wsUserName', $this->getName() );
1312 $session->set( 'wsToken', $this->getToken() );
1313
1314 return true;
1315 }
1316
1317 return false;
1318 }
1319
1325 public function trackBlockWithCookie() {
1326 MediaWikiServices::getInstance()->getBlockManager()->trackBlockWithCookie( $this );
1327 }
1328
1336 public function loadFromDatabase( $flags = self::READ_LATEST ) {
1337 // Paranoia
1338 $this->mId = intval( $this->mId );
1339
1340 if ( !$this->mId ) {
1341 // Anonymous users are not in the database
1342 $this->loadDefaults();
1343 return false;
1344 }
1345
1346 list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
1347 $db = wfGetDB( $index );
1348
1349 $userQuery = self::getQueryInfo();
1350 $s = $db->selectRow(
1351 $userQuery['tables'],
1352 $userQuery['fields'],
1353 [ 'user_id' => $this->mId ],
1354 __METHOD__,
1355 $options,
1356 $userQuery['joins']
1357 );
1358
1359 $this->queryFlagsUsed = $flags;
1360 Hooks::run( 'UserLoadFromDatabase', [ $this, &$s ] );
1361
1362 if ( $s !== false ) {
1363 // Initialise user table data
1364 $this->loadFromRow( $s );
1365 $this->mGroupMemberships = null; // deferred
1366 $this->getEditCount(); // revalidation for nulls
1367 return true;
1368 }
1369
1370 // Invalid user_id
1371 $this->mId = 0;
1372 $this->loadDefaults();
1373
1374 return false;
1375 }
1376
1389 protected function loadFromRow( $row, $data = null ) {
1390 if ( !is_object( $row ) ) {
1391 throw new InvalidArgumentException( '$row must be an object' );
1392 }
1393
1394 $all = true;
1395
1396 $this->mGroupMemberships = null; // deferred
1397
1398 if ( isset( $row->actor_id ) ) {
1399 $this->mActorId = (int)$row->actor_id;
1400 if ( $this->mActorId !== 0 ) {
1401 $this->mFrom = 'actor';
1402 }
1403 $this->setItemLoaded( 'actor' );
1404 } else {
1405 $all = false;
1406 }
1407
1408 if ( isset( $row->user_name ) && $row->user_name !== '' ) {
1409 $this->mName = $row->user_name;
1410 $this->mFrom = 'name';
1411 $this->setItemLoaded( 'name' );
1412 } else {
1413 $all = false;
1414 }
1415
1416 if ( isset( $row->user_real_name ) ) {
1417 $this->mRealName = $row->user_real_name;
1418 $this->setItemLoaded( 'realname' );
1419 } else {
1420 $all = false;
1421 }
1422
1423 if ( isset( $row->user_id ) ) {
1424 $this->mId = intval( $row->user_id );
1425 if ( $this->mId !== 0 ) {
1426 $this->mFrom = 'id';
1427 }
1428 $this->setItemLoaded( 'id' );
1429 } else {
1430 $all = false;
1431 }
1432
1433 if ( isset( $row->user_id ) && isset( $row->user_name ) && $row->user_name !== '' ) {
1434 self::$idCacheByName[$row->user_name] = $row->user_id;
1435 }
1436
1437 if ( isset( $row->user_editcount ) ) {
1438 $this->mEditCount = $row->user_editcount;
1439 } else {
1440 $all = false;
1441 }
1442
1443 if ( isset( $row->user_touched ) ) {
1444 $this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
1445 } else {
1446 $all = false;
1447 }
1448
1449 if ( isset( $row->user_token ) ) {
1450 // The definition for the column is binary(32), so trim the NULs
1451 // that appends. The previous definition was char(32), so trim
1452 // spaces too.
1453 $this->mToken = rtrim( $row->user_token, " \0" );
1454 if ( $this->mToken === '' ) {
1455 $this->mToken = null;
1456 }
1457 } else {
1458 $all = false;
1459 }
1460
1461 if ( isset( $row->user_email ) ) {
1462 $this->mEmail = $row->user_email;
1463 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
1464 $this->mEmailToken = $row->user_email_token;
1465 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
1466 $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
1467 } else {
1468 $all = false;
1469 }
1470
1471 if ( $all ) {
1472 $this->mLoadedItems = true;
1473 }
1474
1475 if ( is_array( $data ) ) {
1476 if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) {
1477 if ( $data['user_groups'] === [] ) {
1478 $this->mGroupMemberships = [];
1479 } else {
1480 $firstGroup = reset( $data['user_groups'] );
1481 if ( is_array( $firstGroup ) || is_object( $firstGroup ) ) {
1482 $this->mGroupMemberships = [];
1483 foreach ( $data['user_groups'] as $row ) {
1484 $ugm = UserGroupMembership::newFromRow( (object)$row );
1485 $this->mGroupMemberships[$ugm->getGroup()] = $ugm;
1486 }
1487 }
1488 }
1489 }
1490 if ( isset( $data['user_properties'] ) && is_array( $data['user_properties'] ) ) {
1491 $this->loadOptions( $data['user_properties'] );
1492 }
1493 }
1494 }
1495
1501 protected function loadFromUserObject( $user ) {
1502 $user->load();
1503 foreach ( self::$mCacheVars as $var ) {
1504 $this->$var = $user->$var;
1505 }
1506 }
1507
1511 private function loadGroups() {
1512 if ( is_null( $this->mGroupMemberships ) ) {
1513 $db = ( $this->queryFlagsUsed & self::READ_LATEST )
1514 ? wfGetDB( DB_MASTER )
1515 : wfGetDB( DB_REPLICA );
1516 $this->mGroupMemberships = UserGroupMembership::getMembershipsForUser(
1517 $this->mId, $db );
1518 }
1519 }
1520
1535 public function addAutopromoteOnceGroups( $event ) {
1537
1538 if ( wfReadOnly() || !$this->getId() ) {
1539 return [];
1540 }
1541
1542 $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
1543 if ( $toPromote === [] ) {
1544 return [];
1545 }
1546
1547 if ( !$this->checkAndSetTouched() ) {
1548 return []; // raced out (bug T48834)
1549 }
1550
1551 $oldGroups = $this->getGroups(); // previous groups
1552 $oldUGMs = $this->getGroupMemberships();
1553 foreach ( $toPromote as $group ) {
1554 $this->addGroup( $group );
1555 }
1556 $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
1557 $newUGMs = $this->getGroupMemberships();
1558
1559 // update groups in external authentication database
1560 Hooks::run( 'UserGroupsChanged', [ $this, $toPromote, [], false, false, $oldUGMs, $newUGMs ] );
1561
1562 $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
1563 $logEntry->setPerformer( $this );
1564 $logEntry->setTarget( $this->getUserPage() );
1565 $logEntry->setParameters( [
1566 '4::oldgroups' => $oldGroups,
1567 '5::newgroups' => $newGroups,
1568 ] );
1569 $logid = $logEntry->insert();
1571 $logEntry->publish( $logid );
1572 }
1573
1574 return $toPromote;
1575 }
1576
1586 protected function makeUpdateConditions( IDatabase $db, array $conditions ) {
1587 if ( $this->mTouched ) {
1588 // CAS check: only update if the row wasn't changed sicne it was loaded.
1589 $conditions['user_touched'] = $db->timestamp( $this->mTouched );
1590 }
1591
1592 return $conditions;
1593 }
1594
1604 protected function checkAndSetTouched() {
1605 $this->load();
1606
1607 if ( !$this->mId ) {
1608 return false; // anon
1609 }
1610
1611 // Get a new user_touched that is higher than the old one
1612 $newTouched = $this->newTouchedTimestamp();
1613
1614 $dbw = wfGetDB( DB_MASTER );
1615 $dbw->update( 'user',
1616 [ 'user_touched' => $dbw->timestamp( $newTouched ) ],
1617 $this->makeUpdateConditions( $dbw, [
1618 'user_id' => $this->mId,
1619 ] ),
1620 __METHOD__
1621 );
1622 $success = ( $dbw->affectedRows() > 0 );
1623
1624 if ( $success ) {
1625 $this->mTouched = $newTouched;
1626 $this->clearSharedCache( 'changed' );
1627 } else {
1628 // Clears on failure too since that is desired if the cache is stale
1629 $this->clearSharedCache( 'refresh' );
1630 }
1631
1632 return $success;
1633 }
1634
1642 public function clearInstanceCache( $reloadFrom = false ) {
1643 global $wgFullyInitialised;
1644
1645 $this->mNewtalk = -1;
1646 $this->mDatePreference = null;
1647 $this->mBlockedby = -1; # Unset
1648 $this->mHash = false;
1649 $this->mEffectiveGroups = null;
1650 $this->mImplicitGroups = null;
1651 $this->mGroupMemberships = null;
1652 $this->mOptions = null;
1653 $this->mOptionsLoaded = false;
1654 $this->mEditCount = null;
1655
1656 // Replacement of former `$this->mRights = null` line
1657 if ( $wgFullyInitialised && $this->mFrom ) {
1658 MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache(
1659 $this
1660 );
1661 }
1662
1663 if ( $reloadFrom ) {
1664 $this->mLoadedItems = [];
1665 $this->mFrom = $reloadFrom;
1666 }
1667 }
1668
1670 private static $defOpt = null;
1672 private static $defOptLang = null;
1673
1680 public static function resetGetDefaultOptionsForTestsOnly() {
1681 Assert::invariant( defined( 'MW_PHPUNIT_TEST' ), 'Unit tests only' );
1682 self::$defOpt = null;
1683 self::$defOptLang = null;
1684 }
1685
1692 public static function getDefaultOptions() {
1694
1695 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1696 if ( self::$defOpt !== null && self::$defOptLang === $contLang->getCode() ) {
1697 // The content language does not change (and should not change) mid-request, but the
1698 // unit tests change it anyway, and expect this method to return values relevant to the
1699 // current content language.
1700 return self::$defOpt;
1701 }
1702
1703 self::$defOpt = $wgDefaultUserOptions;
1704 // Default language setting
1705 self::$defOptLang = $contLang->getCode();
1706 self::$defOpt['language'] = self::$defOptLang;
1707 foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
1708 if ( $langCode === $contLang->getCode() ) {
1709 self::$defOpt['variant'] = $langCode;
1710 } else {
1711 self::$defOpt["variant-$langCode"] = $langCode;
1712 }
1713 }
1714
1715 // NOTE: don't use SearchEngineConfig::getSearchableNamespaces here,
1716 // since extensions may change the set of searchable namespaces depending
1717 // on user groups/permissions.
1718 foreach ( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
1719 self::$defOpt['searchNs' . $nsnum] = (bool)$val;
1720 }
1721 self::$defOpt['skin'] = Skin::normalizeKey( $wgDefaultSkin );
1722
1723 Hooks::run( 'UserGetDefaultOptions', [ &self::$defOpt ] );
1724
1725 return self::$defOpt;
1726 }
1727
1734 public static function getDefaultOption( $opt ) {
1735 $defOpts = self::getDefaultOptions();
1736 return $defOpts[$opt] ?? null;
1737 }
1738
1748 private function getBlockedStatus( $fromReplica = true ) {
1749 if ( $this->mBlockedby != -1 ) {
1750 return;
1751 }
1752
1753 wfDebug( __METHOD__ . ": checking...\n" );
1754
1755 // Initialize data...
1756 // Otherwise something ends up stomping on $this->mBlockedby when
1757 // things get lazy-loaded later, causing false positive block hits
1758 // due to -1 !== 0. Probably session-related... Nothing should be
1759 // overwriting mBlockedby, surely?
1760 $this->load();
1761
1762 // TODO: Block checking shouldn't really be done from the User object. Block
1763 // checking can involve checking for IP blocks, cookie blocks, and/or XFF blocks,
1764 // which need more knowledge of the request context than the User should have.
1765 // Since we do currently check blocks from the User, we have to do the following
1766 // here:
1767 // - Check if this is the user associated with the main request
1768 // - If so, pass the relevant request information to the block manager
1769 $request = null;
1770
1771 // The session user is set up towards the end of Setup.php. Until then,
1772 // assume it's a logged-out user.
1773 $sessionUser = RequestContext::getMain()->getUser();
1774 $globalUserName = $sessionUser->isSafeToLoad()
1775 ? $sessionUser->getName()
1776 : IP::sanitizeIP( $sessionUser->getRequest()->getIP() );
1777
1778 if ( $this->getName() === $globalUserName ) {
1779 // This is the global user, so we need to pass the request
1780 $request = $this->getRequest();
1781 }
1782
1783 // @phan-suppress-next-line PhanAccessMethodInternal It's the only allowed use
1784 $block = MediaWikiServices::getInstance()->getBlockManager()->getUserBlock(
1785 $this,
1786 $request,
1787 $fromReplica
1788 );
1789
1790 if ( $block ) {
1791 $this->mBlock = $block;
1792 $this->mBlockedby = $block->getByName();
1793 $this->mBlockreason = $block->getReason();
1794 $this->mHideName = $block->getHideName();
1795 $this->mAllowUsertalk = $block->isUsertalkEditAllowed();
1796 } else {
1797 $this->mBlock = null;
1798 $this->mBlockedby = '';
1799 $this->mBlockreason = '';
1800 $this->mHideName = 0;
1801 $this->mAllowUsertalk = false;
1802 }
1803
1804 // Avoid PHP 7.1 warning of passing $this by reference
1805 $thisUser = $this;
1806 // Extensions
1807 Hooks::run( 'GetBlockedStatus', [ &$thisUser ], '1.34' );
1808 }
1809
1818 public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
1819 return MediaWikiServices::getInstance()->getBlockManager()
1820 ->isDnsBlacklisted( $ip, $checkWhitelist );
1821 }
1822
1831 public function inDnsBlacklist( $ip, $bases ) {
1832 wfDeprecated( __METHOD__, '1.34' );
1833
1834 $found = false;
1835 // @todo FIXME: IPv6 ??? (https://bugs.php.net/bug.php?id=33170)
1836 if ( IP::isIPv4( $ip ) ) {
1837 // Reverse IP, T23255
1838 $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
1839
1840 foreach ( (array)$bases as $base ) {
1841 // Make hostname
1842 // If we have an access key, use that too (ProjectHoneypot, etc.)
1843 $basename = $base;
1844 if ( is_array( $base ) ) {
1845 if ( count( $base ) >= 2 ) {
1846 // Access key is 1, base URL is 0
1847 $host = "{$base[1]}.$ipReversed.{$base[0]}";
1848 } else {
1849 $host = "$ipReversed.{$base[0]}";
1850 }
1851 $basename = $base[0];
1852 } else {
1853 $host = "$ipReversed.$base";
1854 }
1855
1856 // Send query
1857 $ipList = gethostbynamel( $host );
1858
1859 if ( $ipList ) {
1860 wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $basename!" );
1861 $found = true;
1862 break;
1863 }
1864
1865 wfDebugLog( 'dnsblacklist', "Requested $host, not found in $basename." );
1866 }
1867 }
1868
1869 return $found;
1870 }
1871
1879 public static function isLocallyBlockedProxy( $ip ) {
1880 wfDeprecated( __METHOD__, '1.34' );
1881
1882 global $wgProxyList;
1883
1884 if ( !$wgProxyList ) {
1885 return false;
1886 }
1887
1888 if ( !is_array( $wgProxyList ) ) {
1889 // Load values from the specified file
1890 $wgProxyList = array_map( 'trim', file( $wgProxyList ) );
1891 }
1892
1893 $resultProxyList = [];
1894 $deprecatedIPEntries = [];
1895
1896 // backward compatibility: move all ip addresses in keys to values
1897 foreach ( $wgProxyList as $key => $value ) {
1898 $keyIsIP = IP::isIPAddress( $key );
1899 $valueIsIP = IP::isIPAddress( $value );
1900 if ( $keyIsIP && !$valueIsIP ) {
1901 $deprecatedIPEntries[] = $key;
1902 $resultProxyList[] = $key;
1903 } elseif ( $keyIsIP && $valueIsIP ) {
1904 $deprecatedIPEntries[] = $key;
1905 $resultProxyList[] = $key;
1906 $resultProxyList[] = $value;
1907 } else {
1908 $resultProxyList[] = $value;
1909 }
1910 }
1911
1912 if ( $deprecatedIPEntries ) {
1914 'IP addresses in the keys of $wgProxyList (found the following IP addresses in keys: ' .
1915 implode( ', ', $deprecatedIPEntries ) . ', please move them to values)', '1.30' );
1916 }
1917
1918 $proxyListIPSet = new IPSet( $resultProxyList );
1919 return $proxyListIPSet->match( $ip );
1920 }
1921
1927 public function isPingLimitable() {
1929 if ( IP::isInRanges( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) {
1930 // No other good way currently to disable rate limits
1931 // for specific IPs. :P
1932 // But this is a crappy hack and should die.
1933 return false;
1934 }
1935 return !$this->isAllowed( 'noratelimit' );
1936 }
1937
1954 public function pingLimiter( $action = 'edit', $incrBy = 1 ) {
1955 // Avoid PHP 7.1 warning of passing $this by reference
1956 $user = $this;
1957
1958 $logger = \MediaWiki\Logger\LoggerFactory::getInstance( 'ratelimit' );
1959
1960 // Call the 'PingLimiter' hook
1961 $result = false;
1962 if ( !Hooks::run( 'PingLimiter', [ &$user, $action, &$result, $incrBy ] ) ) {
1963 return $result;
1964 }
1965
1966 global $wgRateLimits;
1967 if ( !isset( $wgRateLimits[$action] ) ) {
1968 return false;
1969 }
1970
1971 $limits = array_merge(
1972 [ '&can-bypass' => true ],
1973 $wgRateLimits[$action]
1974 );
1975
1976 // Some groups shouldn't trigger the ping limiter, ever
1977 if ( $limits['&can-bypass'] && !$this->isPingLimitable() ) {
1978 return false;
1979 }
1980
1981 $logger->debug( __METHOD__ . ": limiting $action rate for {$this->getName()}" );
1982
1983 $keys = [];
1984 $id = $this->getId();
1985 $isNewbie = $this->isNewbie();
1986 $cache = ObjectCache::getLocalClusterInstance();
1987
1988 if ( $id == 0 ) {
1989 // "shared anon" limit, for all anons combined
1990 if ( isset( $limits['anon'] ) ) {
1991 $keys[$cache->makeKey( 'limiter', $action, 'anon' )] = $limits['anon'];
1992 }
1993 } else {
1994 // "global per name" limit, across sites
1995 if ( isset( $limits['user-global'] ) ) {
1996 $lookup = CentralIdLookup::factoryNonLocal();
1997
1998 $centralId = $lookup
1999 ? $lookup->centralIdFromLocalUser( $this, CentralIdLookup::AUDIENCE_RAW )
2000 : 0;
2001
2002 if ( $centralId ) {
2003 // We don't have proper realms, use provider ID.
2004 $realm = $lookup->getProviderId();
2005
2006 $globalKey = $cache->makeGlobalKey( 'limiter', $action, 'user-global',
2007 $realm, $centralId );
2008 } else {
2009 // Fall back to a local key for a local ID
2010 $globalKey = $cache->makeKey( 'limiter', $action, 'user-global',
2011 'local', $id );
2012 }
2013 $keys[$globalKey] = $limits['user-global'];
2014 }
2015 }
2016
2017 if ( $isNewbie ) {
2018 // "per ip" limit for anons and newbie users
2019 if ( isset( $limits['ip'] ) ) {
2020 $ip = $this->getRequest()->getIP();
2021 $keys[$cache->makeGlobalKey( 'limiter', $action, 'ip', $ip )] = $limits['ip'];
2022 }
2023 // "per subnet" limit for anons and newbie users
2024 if ( isset( $limits['subnet'] ) ) {
2025 $ip = $this->getRequest()->getIP();
2026 $subnet = IP::getSubnet( $ip );
2027 if ( $subnet !== false ) {
2028 $keys[$cache->makeGlobalKey( 'limiter', $action, 'subnet', $subnet )] = $limits['subnet'];
2029 }
2030 }
2031 }
2032
2033 // determine the "per user account" limit
2034 $userLimit = false;
2035 if ( $id !== 0 && isset( $limits['user'] ) ) {
2036 // default limit for logged-in users
2037 $userLimit = $limits['user'];
2038 }
2039 // limits for newbie logged-in users (overrides all the normal user limits)
2040 if ( $id !== 0 && $isNewbie && isset( $limits['newbie'] ) ) {
2041 $userLimit = $limits['newbie'];
2042 } else {
2043 // Check for group-specific limits
2044 // If more than one group applies, use the highest allowance (if higher than the default)
2045 foreach ( $this->getGroups() as $group ) {
2046 if ( isset( $limits[$group] ) ) {
2047 if ( $userLimit === false
2048 || $limits[$group][0] / $limits[$group][1] > $userLimit[0] / $userLimit[1]
2049 ) {
2050 $userLimit = $limits[$group];
2051 }
2052 }
2053 }
2054 }
2055
2056 // Set the user limit key
2057 if ( $userLimit !== false ) {
2058 // phan is confused because &can-bypass's value is a bool, so it assumes
2059 // that $userLimit is also a bool here.
2060 // @phan-suppress-next-line PhanTypeInvalidExpressionArrayDestructuring
2061 list( $max, $period ) = $userLimit;
2062 $logger->debug( __METHOD__ . ": effective user limit: $max in {$period}s" );
2063 $keys[$cache->makeKey( 'limiter', $action, 'user', $id )] = $userLimit;
2064 }
2065
2066 // ip-based limits for all ping-limitable users
2067 if ( isset( $limits['ip-all'] ) ) {
2068 $ip = $this->getRequest()->getIP();
2069 // ignore if user limit is more permissive
2070 if ( $isNewbie || $userLimit === false
2071 || $limits['ip-all'][0] / $limits['ip-all'][1] > $userLimit[0] / $userLimit[1] ) {
2072 $keys[$cache->makeGlobalKey( 'limiter', $action, 'ip-all', $ip )] = $limits['ip-all'];
2073 }
2074 }
2075
2076 // subnet-based limits for all ping-limitable users
2077 if ( isset( $limits['subnet-all'] ) ) {
2078 $ip = $this->getRequest()->getIP();
2079 $subnet = IP::getSubnet( $ip );
2080 if ( $subnet !== false ) {
2081 // ignore if user limit is more permissive
2082 if ( $isNewbie || $userLimit === false
2083 || $limits['ip-all'][0] / $limits['ip-all'][1]
2084 > $userLimit[0] / $userLimit[1] ) {
2085 $keys[$cache->makeGlobalKey( 'limiter', $action, 'subnet-all', $subnet )]
2086 = $limits['subnet-all'];
2087 }
2088 }
2089 }
2090
2091 // XXX: We may want to use $cache->getCurrentTime() here, but that would make it
2092 // harder to test for T246991. Also $cache->getCurrentTime() is documented
2093 // as being for testing only, so it apparently should not be called here.
2094 $now = MWTimestamp::time();
2095 $clockFudge = 3; // avoid log spam when a clock is slightly off
2096
2097 $triggered = false;
2098 foreach ( $keys as $key => $limit ) {
2099 // Do the update in a merge callback, for atomicity.
2100 // To use merge(), we need to explicitly track the desired expiry timestamp.
2101 // This tracking was introduced to investigate T246991. Once it is no longer needed,
2102 // we could go back to incrWithInit(), though that has more potential for race
2103 // conditions between the get() and incrWithInit() calls.
2104 $cache->merge(
2105 $key,
2106 function ( $cache, $key, $data, &$expiry )
2107 use ( $action, $logger, &$triggered, $now, $clockFudge, $limit, $incrBy )
2108 {
2109 // phan is confused because &can-bypass's value is a bool, so it assumes
2110 // that $userLimit is also a bool here.
2111 // @phan-suppress-next-line PhanTypeInvalidExpressionArrayDestructuring
2112 list( $max, $period ) = $limit;
2113
2114 $expiry = $now + (int)$period;
2115 $count = 0;
2116
2117 // Already pinged?
2118 if ( $data ) {
2119 // NOTE: in order to investigate T246991, we write the expiry time
2120 // into the payload, along with the count.
2121 $fields = explode( '|', $data );
2122 $storedCount = (int)( $fields[0] ?? 0 );
2123 $storedExpiry = (int)( $fields[1] ?? PHP_INT_MAX );
2124
2125 // Found a stale entry. This should not happen!
2126 if ( $storedExpiry < ( $now + $clockFudge ) ) {
2127 $logger->info(
2128 'User::pingLimiter: '
2129 . 'Stale rate limit entry, cache key failed to expire (T246991)',
2130 [
2131 'action' => $action,
2132 'user' => $this->getName(),
2133 'limit' => $max,
2134 'period' => $period,
2135 'count' => $storedCount,
2136 'key' => $key,
2137 'expiry' => MWTimestamp::convert( TS_DB, $storedExpiry ),
2138 ]
2139 );
2140 } else {
2141 // NOTE: We'll keep the original expiry when bumping counters,
2142 // resulting in a kind of fixed-window throttle.
2143 $expiry = min( $storedExpiry, $now + (int)$period );
2144 $count = $storedCount;
2145 }
2146 }
2147
2148 // Limit exceeded!
2149 if ( $count >= $max ) {
2150 if ( !$triggered ) {
2151 $logger->info(
2152 'User::pingLimiter: User tripped rate limit',
2153 [
2154 'action' => $action,
2155 'user' => $this->getName(),
2156 'ip' => $this->getRequest()->getIP(),
2157 'limit' => $max,
2158 'period' => $period,
2159 'count' => $count,
2160 'key' => $key
2161 ]
2162 );
2163 }
2164
2165 $triggered = true;
2166 }
2167
2168 $count += $incrBy;
2169 $data = "$count|$expiry";
2170 return $data;
2171 }
2172 );
2173 }
2174
2175 return $triggered;
2176 }
2177
2189 public function isBlocked( $fromReplica = true ) {
2190 return $this->getBlock( $fromReplica ) instanceof AbstractBlock &&
2191 $this->getBlock()->appliesToRight( 'edit' );
2192 }
2193
2200 public function getBlock( $fromReplica = true ) {
2201 $this->getBlockedStatus( $fromReplica );
2202 return $this->mBlock instanceof AbstractBlock ? $this->mBlock : null;
2203 }
2204
2216 public function isBlockedFrom( $title, $fromReplica = false ) {
2217 return MediaWikiServices::getInstance()->getPermissionManager()
2218 ->isBlockedFrom( $this, $title, $fromReplica );
2219 }
2220
2225 public function blockedBy() {
2226 $this->getBlockedStatus();
2227 return $this->mBlockedby;
2228 }
2229
2234 public function blockedFor() {
2235 $this->getBlockedStatus();
2236 return $this->mBlockreason;
2237 }
2238
2243 public function getBlockId() {
2244 $this->getBlockedStatus();
2245 return ( $this->mBlock ? $this->mBlock->getId() : false );
2246 }
2247
2256 public function isBlockedGlobally( $ip = '' ) {
2257 return $this->getGlobalBlock( $ip ) instanceof AbstractBlock;
2258 }
2259
2270 public function getGlobalBlock( $ip = '' ) {
2271 if ( $this->mGlobalBlock !== null ) {
2272 return $this->mGlobalBlock ?: null;
2273 }
2274 // User is already an IP?
2275 if ( IP::isIPAddress( $this->getName() ) ) {
2276 $ip = $this->getName();
2277 } elseif ( !$ip ) {
2278 $ip = $this->getRequest()->getIP();
2279 }
2280 // Avoid PHP 7.1 warning of passing $this by reference
2281 $user = $this;
2282 $blocked = false;
2283 $block = null;
2284 Hooks::run( 'UserIsBlockedGlobally', [ &$user, $ip, &$blocked, &$block ] );
2285
2286 if ( $blocked && $block === null ) {
2287 // back-compat: UserIsBlockedGlobally didn't have $block param first
2288 $block = new SystemBlock( [
2289 'address' => $ip,
2290 'systemBlock' => 'global-block'
2291 ] );
2292 }
2293
2294 $this->mGlobalBlock = $blocked ? $block : false;
2295 return $this->mGlobalBlock ?: null;
2296 }
2297
2303 public function isLocked() {
2304 if ( $this->mLocked !== null ) {
2305 return $this->mLocked;
2306 }
2307 // Reset for hook
2308 $this->mLocked = false;
2309 Hooks::run( 'UserIsLocked', [ $this, &$this->mLocked ] );
2310 return $this->mLocked;
2311 }
2312
2318 public function isHidden() {
2319 if ( $this->mHideName !== null ) {
2320 return (bool)$this->mHideName;
2321 }
2322 $this->getBlockedStatus();
2323 if ( !$this->mHideName ) {
2324 // Reset for hook
2325 $this->mHideName = false;
2326 Hooks::run( 'UserIsHidden', [ $this, &$this->mHideName ], '1.34' );
2327 }
2328 return (bool)$this->mHideName;
2329 }
2330
2335 public function getId() {
2336 if ( $this->mId === null && $this->mName !== null &&
2337 ( self::isIP( $this->mName ) || ExternalUserNames::isExternal( $this->mName ) )
2338 ) {
2339 // Special case, we know the user is anonymous
2340 return 0;
2341 }
2342
2343 if ( !$this->isItemLoaded( 'id' ) ) {
2344 // Don't load if this was initialized from an ID
2345 $this->load();
2346 }
2347
2348 return (int)$this->mId;
2349 }
2350
2355 public function setId( $v ) {
2356 $this->mId = $v;
2357 $this->clearInstanceCache( 'id' );
2358 }
2359
2364 public function getName() {
2365 if ( $this->isItemLoaded( 'name', 'only' ) ) {
2366 // Special case optimisation
2367 return $this->mName;
2368 }
2369
2370 $this->load();
2371 if ( $this->mName === false ) {
2372 // Clean up IPs
2373 $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() );
2374 }
2375
2376 return $this->mName;
2377 }
2378
2392 public function setName( $str ) {
2393 $this->load();
2394 $this->mName = $str;
2395 }
2396
2403 public function getActorId( IDatabase $dbw = null ) {
2404 if ( !$this->isItemLoaded( 'actor' ) ) {
2405 $this->load();
2406 }
2407
2408 if ( !$this->mActorId && $dbw ) {
2409 $migration = MediaWikiServices::getInstance()->getActorMigration();
2410 $this->mActorId = $migration->getNewActorId( $dbw, $this );
2411
2412 $this->invalidateCache();
2413 $this->setItemLoaded( 'actor' );
2414 }
2415
2416 return (int)$this->mActorId;
2417 }
2418
2423 public function getTitleKey() {
2424 return str_replace( ' ', '_', $this->getName() );
2425 }
2426
2431 public function getNewtalk() {
2432 $this->load();
2433
2434 // Load the newtalk status if it is unloaded (mNewtalk=-1)
2435 if ( $this->mNewtalk === -1 ) {
2436 $this->mNewtalk = false; # reset talk page status
2437
2438 // Check memcached separately for anons, who have no
2439 // entire User object stored in there.
2440 if ( !$this->mId ) {
2441 global $wgDisableAnonTalk;
2442 if ( $wgDisableAnonTalk ) {
2443 // Anon newtalk disabled by configuration.
2444 $this->mNewtalk = false;
2445 } else {
2446 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
2447 }
2448 } else {
2449 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
2450 }
2451 }
2452
2453 return (bool)$this->mNewtalk;
2454 }
2455
2469 public function getNewMessageLinks() {
2470 // Avoid PHP 7.1 warning of passing $this by reference
2471 $user = $this;
2472 $talks = [];
2473 if ( !Hooks::run( 'UserRetrieveNewTalks', [ &$user, &$talks ] ) ) {
2474 return $talks;
2475 }
2476
2477 if ( !$this->getNewtalk() ) {
2478 return [];
2479 }
2480 $utp = $this->getTalkPage();
2481 $dbr = wfGetDB( DB_REPLICA );
2482 // Get the "last viewed rev" timestamp from the oldest message notification
2483 $timestamp = $dbr->selectField( 'user_newtalk',
2484 'MIN(user_last_timestamp)',
2485 $this->isAnon() ? [ 'user_ip' => $this->getName() ] : [ 'user_id' => $this->getId() ],
2486 __METHOD__ );
2487 $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null;
2488 return [
2489 [
2490 'wiki' => WikiMap::getWikiIdFromDbDomain( WikiMap::getCurrentWikiDbDomain() ),
2491 'link' => $utp->getLocalURL(),
2492 'rev' => $rev
2493 ]
2494 ];
2495 }
2496
2502 public function getNewMessageRevisionId() {
2503 $newMessageRevisionId = null;
2504 $newMessageLinks = $this->getNewMessageLinks();
2505
2506 // Note: getNewMessageLinks() never returns more than a single link
2507 // and it is always for the same wiki, but we double-check here in
2508 // case that changes some time in the future.
2509 if ( $newMessageLinks && count( $newMessageLinks ) === 1
2510 && WikiMap::isCurrentWikiId( $newMessageLinks[0]['wiki'] )
2511 && $newMessageLinks[0]['rev']
2512 ) {
2514 $newMessageRevision = $newMessageLinks[0]['rev'];
2515 $newMessageRevisionId = $newMessageRevision->getId();
2516 }
2517
2518 return $newMessageRevisionId;
2519 }
2520
2529 protected function checkNewtalk( $field, $id ) {
2530 $dbr = wfGetDB( DB_REPLICA );
2531
2532 $ok = $dbr->selectField( 'user_newtalk', $field, [ $field => $id ], __METHOD__ );
2533
2534 return $ok !== false;
2535 }
2536
2544 protected function updateNewtalk( $field, $id, $curRev = null ) {
2545 // Get timestamp of the talk page revision prior to the current one
2546 $prevRev = $curRev ? $curRev->getPrevious() : false;
2547 $ts = $prevRev ? $prevRev->getTimestamp() : null;
2548 // Mark the user as having new messages since this revision
2549 $dbw = wfGetDB( DB_MASTER );
2550 $dbw->insert( 'user_newtalk',
2551 [ $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ],
2552 __METHOD__,
2553 [ 'IGNORE' ] );
2554 if ( $dbw->affectedRows() ) {
2555 wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
2556 return true;
2557 }
2558
2559 wfDebug( __METHOD__ . " already set ($field, $id)\n" );
2560 return false;
2561 }
2562
2569 protected function deleteNewtalk( $field, $id ) {
2570 $dbw = wfGetDB( DB_MASTER );
2571 $dbw->delete( 'user_newtalk',
2572 [ $field => $id ],
2573 __METHOD__ );
2574 if ( $dbw->affectedRows() ) {
2575 wfDebug( __METHOD__ . ": killed on ($field, $id)\n" );
2576 return true;
2577 }
2578
2579 wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
2580 return false;
2581 }
2582
2589 public function setNewtalk( $val, $curRev = null ) {
2590 if ( wfReadOnly() ) {
2591 return;
2592 }
2593
2594 $this->load();
2595 $this->mNewtalk = $val;
2596
2597 if ( $this->isAnon() ) {
2598 $field = 'user_ip';
2599 $id = $this->getName();
2600 } else {
2601 $field = 'user_id';
2602 $id = $this->getId();
2603 }
2604
2605 if ( $val ) {
2606 $changed = $this->updateNewtalk( $field, $id, $curRev );
2607 } else {
2608 $changed = $this->deleteNewtalk( $field, $id );
2609 }
2610
2611 if ( $changed ) {
2612 $this->invalidateCache();
2613 }
2614 }
2615
2622 private function newTouchedTimestamp() {
2623 $time = time();
2624 if ( $this->mTouched ) {
2625 $time = max( $time, wfTimestamp( TS_UNIX, $this->mTouched ) + 1 );
2626 }
2627
2628 return wfTimestamp( TS_MW, $time );
2629 }
2630
2641 public function clearSharedCache( $mode = 'refresh' ) {
2642 if ( !$this->getId() ) {
2643 return;
2644 }
2645
2646 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
2647 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
2648 $key = $this->getCacheKey( $cache );
2649
2650 if ( $mode === 'refresh' ) {
2651 $cache->delete( $key, 1 ); // low tombstone/"hold-off" TTL
2652 } else {
2653 $lb->getConnectionRef( DB_MASTER )->onTransactionPreCommitOrIdle(
2654 function () use ( $cache, $key ) {
2655 $cache->delete( $key );
2656 },
2657 __METHOD__
2658 );
2659 }
2660 }
2661
2667 public function invalidateCache() {
2668 $this->touch();
2669 $this->clearSharedCache( 'changed' );
2670 }
2671
2684 public function touch() {
2685 $id = $this->getId();
2686 if ( $id ) {
2687 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
2688 $key = $cache->makeKey( 'user-quicktouched', 'id', $id );
2689 $cache->touchCheckKey( $key );
2690 $this->mQuickTouched = null;
2691 }
2692 }
2693
2699 public function validateCache( $timestamp ) {
2700 return ( $timestamp >= $this->getTouched() );
2701 }
2702
2711 public function getTouched() {
2712 $this->load();
2713
2714 if ( $this->mId ) {
2715 if ( $this->mQuickTouched === null ) {
2716 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
2717 $key = $cache->makeKey( 'user-quicktouched', 'id', $this->mId );
2718
2719 $this->mQuickTouched = wfTimestamp( TS_MW, $cache->getCheckKeyTime( $key ) );
2720 }
2721
2722 return max( $this->mTouched, $this->mQuickTouched );
2723 }
2724
2725 return $this->mTouched;
2726 }
2727
2733 public function getDBTouched() {
2734 $this->load();
2735
2736 return $this->mTouched;
2737 }
2738
2755 public function setPassword( $str ) {
2756 wfDeprecated( __METHOD__, '1.27' );
2757 return $this->setPasswordInternal( $str );
2758 }
2759
2768 public function setInternalPassword( $str ) {
2769 wfDeprecated( __METHOD__, '1.27' );
2770 $this->setPasswordInternal( $str );
2771 }
2772
2781 private function setPasswordInternal( $str ) {
2782 $manager = AuthManager::singleton();
2783
2784 // If the user doesn't exist yet, fail
2785 if ( !$manager->userExists( $this->getName() ) ) {
2786 throw new LogicException( 'Cannot set a password for a user that is not in the database.' );
2787 }
2788
2789 $status = $this->changeAuthenticationData( [
2790 'username' => $this->getName(),
2791 'password' => $str,
2792 'retype' => $str,
2793 ] );
2794 if ( !$status->isGood() ) {
2795 \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
2796 ->info( __METHOD__ . ': Password change rejected: '
2797 . $status->getWikiText( null, null, 'en' ) );
2798 return false;
2799 }
2800
2801 $this->setOption( 'watchlisttoken', false );
2802 SessionManager::singleton()->invalidateSessionsForUser( $this );
2803
2804 return true;
2805 }
2806
2819 public function changeAuthenticationData( array $data ) {
2820 $manager = AuthManager::singleton();
2821 $reqs = $manager->getAuthenticationRequests( AuthManager::ACTION_CHANGE, $this );
2822 $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
2823
2824 $status = Status::newGood( 'ignored' );
2825 foreach ( $reqs as $req ) {
2826 $status->merge( $manager->allowsAuthenticationDataChange( $req ), true );
2827 }
2828 if ( $status->getValue() === 'ignored' ) {
2829 $status->warning( 'authenticationdatachange-ignored' );
2830 }
2831
2832 if ( $status->isGood() ) {
2833 foreach ( $reqs as $req ) {
2834 $manager->changeAuthenticationData( $req );
2835 }
2836 }
2837 return $status;
2838 }
2839
2846 public function getToken( $forceCreation = true ) {
2848
2849 $this->load();
2850 if ( !$this->mToken && $forceCreation ) {
2851 $this->setToken();
2852 }
2853
2854 if ( !$this->mToken ) {
2855 // The user doesn't have a token, return null to indicate that.
2856 return null;
2857 }
2858
2859 if ( $this->mToken === self::INVALID_TOKEN ) {
2860 // We return a random value here so existing token checks are very
2861 // likely to fail.
2862 return MWCryptRand::generateHex( self::TOKEN_LENGTH );
2863 }
2864
2865 if ( $wgAuthenticationTokenVersion === null ) {
2866 // $wgAuthenticationTokenVersion not in use, so return the raw secret
2867 return $this->mToken;
2868 }
2869
2870 // $wgAuthenticationTokenVersion in use, so hmac it.
2871 $ret = MWCryptHash::hmac( $wgAuthenticationTokenVersion, $this->mToken, false );
2872
2873 // The raw hash can be overly long. Shorten it up.
2874 $len = max( 32, self::TOKEN_LENGTH );
2875 if ( strlen( $ret ) < $len ) {
2876 // Should never happen, even md5 is 128 bits
2877 throw new \UnexpectedValueException( 'Hmac returned less than 128 bits' );
2878 }
2879
2880 return substr( $ret, -$len );
2881 }
2882
2889 public function setToken( $token = false ) {
2890 $this->load();
2891 if ( $this->mToken === self::INVALID_TOKEN ) {
2892 \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
2893 ->debug( __METHOD__ . ": Ignoring attempt to set token for system user \"$this\"" );
2894 } elseif ( !$token ) {
2895 $this->mToken = MWCryptRand::generateHex( self::TOKEN_LENGTH );
2896 } else {
2897 $this->mToken = $token;
2898 }
2899 }
2900
2905 public function getEmail() {
2906 $this->load();
2907 Hooks::run( 'UserGetEmail', [ $this, &$this->mEmail ] );
2908 return $this->mEmail;
2909 }
2910
2916 $this->load();
2917 Hooks::run( 'UserGetEmailAuthenticationTimestamp', [ $this, &$this->mEmailAuthenticated ] );
2918 return $this->mEmailAuthenticated;
2919 }
2920
2925 public function setEmail( $str ) {
2926 $this->load();
2927 if ( $str == $this->mEmail ) {
2928 return;
2929 }
2930 $this->invalidateEmail();
2931 $this->mEmail = $str;
2932 Hooks::run( 'UserSetEmail', [ $this, &$this->mEmail ] );
2933 }
2934
2942 public function setEmailWithConfirmation( $str ) {
2944
2945 if ( !$wgEnableEmail ) {
2946 return Status::newFatal( 'emaildisabled' );
2947 }
2948
2949 $oldaddr = $this->getEmail();
2950 if ( $str === $oldaddr ) {
2951 return Status::newGood( true );
2952 }
2953
2954 $type = $oldaddr != '' ? 'changed' : 'set';
2955 $notificationResult = null;
2956
2957 if ( $wgEmailAuthentication && $type === 'changed' ) {
2958 // Send the user an email notifying the user of the change in registered
2959 // email address on their previous email address
2960 $change = $str != '' ? 'changed' : 'removed';
2961 $notificationResult = $this->sendMail(
2962 wfMessage( 'notificationemail_subject_' . $change )->text(),
2963 wfMessage( 'notificationemail_body_' . $change,
2964 $this->getRequest()->getIP(),
2965 $this->getName(),
2966 $str )->text()
2967 );
2968 }
2969
2970 $this->setEmail( $str );
2971
2972 if ( $str !== '' && $wgEmailAuthentication ) {
2973 // Send a confirmation request to the new address if needed
2974 $result = $this->sendConfirmationMail( $type );
2975
2976 if ( $notificationResult !== null ) {
2977 $result->merge( $notificationResult );
2978 }
2979
2980 if ( $result->isGood() ) {
2981 // Say to the caller that a confirmation and notification mail has been sent
2982 $result->value = 'eauth';
2983 }
2984 } else {
2985 $result = Status::newGood( true );
2986 }
2987
2988 return $result;
2989 }
2990
2995 public function getRealName() {
2996 if ( !$this->isItemLoaded( 'realname' ) ) {
2997 $this->load();
2998 }
2999
3000 return $this->mRealName;
3001 }
3002
3007 public function setRealName( $str ) {
3008 $this->load();
3009 $this->mRealName = $str;
3010 }
3011
3022 public function getOption( $oname, $defaultOverride = null, $ignoreHidden = false ) {
3023 global $wgHiddenPrefs;
3024 $this->loadOptions();
3025
3026 # We want 'disabled' preferences to always behave as the default value for
3027 # users, even if they have set the option explicitly in their settings (ie they
3028 # set it, and then it was disabled removing their ability to change it). But
3029 # we don't want to erase the preferences in the database in case the preference
3030 # is re-enabled again. So don't touch $mOptions, just override the returned value
3031 if ( !$ignoreHidden && in_array( $oname, $wgHiddenPrefs ) ) {
3032 return self::getDefaultOption( $oname );
3033 }
3034
3035 if ( array_key_exists( $oname, $this->mOptions ) ) {
3036 return $this->mOptions[$oname];
3037 }
3038
3039 return $defaultOverride;
3040 }
3041
3050 public function getOptions( $flags = 0 ) {
3051 global $wgHiddenPrefs;
3052 $this->loadOptions();
3053 $options = $this->mOptions;
3054
3055 # We want 'disabled' preferences to always behave as the default value for
3056 # users, even if they have set the option explicitly in their settings (ie they
3057 # set it, and then it was disabled removing their ability to change it). But
3058 # we don't want to erase the preferences in the database in case the preference
3059 # is re-enabled again. So don't touch $mOptions, just override the returned value
3060 foreach ( $wgHiddenPrefs as $pref ) {
3061 $default = self::getDefaultOption( $pref );
3062 if ( $default !== null ) {
3063 $options[$pref] = $default;
3064 }
3065 }
3066
3067 if ( $flags & self::GETOPTIONS_EXCLUDE_DEFAULTS ) {
3068 $options = array_diff_assoc( $options, self::getDefaultOptions() );
3069 }
3070
3071 return $options;
3072 }
3073
3081 public function getBoolOption( $oname ) {
3082 return (bool)$this->getOption( $oname );
3083 }
3084
3093 public function getIntOption( $oname, $defaultOverride = 0 ) {
3094 $val = $this->getOption( $oname );
3095 if ( $val == '' ) {
3096 $val = $defaultOverride;
3097 }
3098 return intval( $val );
3099 }
3100
3109 public function setOption( $oname, $val ) {
3110 $this->loadOptions();
3111
3112 // Explicitly NULL values should refer to defaults
3113 if ( is_null( $val ) ) {
3114 $val = self::getDefaultOption( $oname );
3115 }
3116
3117 $this->mOptions[$oname] = $val;
3118 }
3119
3130 public function getTokenFromOption( $oname ) {
3131 global $wgHiddenPrefs;
3132
3133 $id = $this->getId();
3134 if ( !$id || in_array( $oname, $wgHiddenPrefs ) ) {
3135 return false;
3136 }
3137
3138 $token = $this->getOption( $oname );
3139 if ( !$token ) {
3140 // Default to a value based on the user token to avoid space
3141 // wasted on storing tokens for all users. When this option
3142 // is set manually by the user, only then is it stored.
3143 $token = hash_hmac( 'sha1', "$oname:$id", $this->getToken() );
3144 }
3145
3146 return $token;
3147 }
3148
3158 public function resetTokenFromOption( $oname ) {
3159 global $wgHiddenPrefs;
3160 if ( in_array( $oname, $wgHiddenPrefs ) ) {
3161 return false;
3162 }
3163
3164 $token = MWCryptRand::generateHex( 40 );
3165 $this->setOption( $oname, $token );
3166 return $token;
3167 }
3168
3192 public static function listOptionKinds() {
3193 return [
3194 'registered',
3195 'registered-multiselect',
3196 'registered-checkmatrix',
3197 'userjs',
3198 'special',
3199 'unused'
3200 ];
3201 }
3202
3215 public function getOptionKinds( IContextSource $context, $options = null ) {
3216 $this->loadOptions();
3217 if ( $options === null ) {
3218 $options = $this->mOptions;
3219 }
3220
3221 $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory();
3222 $prefs = $preferencesFactory->getFormDescriptor( $this, $context );
3223 $mapping = [];
3224
3225 // Pull out the "special" options, so they don't get converted as
3226 // multiselect or checkmatrix.
3227 $specialOptions = array_fill_keys( $preferencesFactory->getSaveBlacklist(), true );
3228 foreach ( $specialOptions as $name => $value ) {
3229 unset( $prefs[$name] );
3230 }
3231
3232 // Multiselect and checkmatrix options are stored in the database with
3233 // one key per option, each having a boolean value. Extract those keys.
3234 $multiselectOptions = [];
3235 foreach ( $prefs as $name => $info ) {
3236 if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
3237 ( isset( $info['class'] ) && $info['class'] == HTMLMultiSelectField::class ) ) {
3238 $opts = HTMLFormField::flattenOptions( $info['options'] );
3239 $prefix = $info['prefix'] ?? $name;
3240
3241 foreach ( $opts as $value ) {
3242 $multiselectOptions["$prefix$value"] = true;
3243 }
3244
3245 unset( $prefs[$name] );
3246 }
3247 }
3248 $checkmatrixOptions = [];
3249 foreach ( $prefs as $name => $info ) {
3250 if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
3251 ( isset( $info['class'] ) && $info['class'] == HTMLCheckMatrix::class ) ) {
3252 $columns = HTMLFormField::flattenOptions( $info['columns'] );
3253 $rows = HTMLFormField::flattenOptions( $info['rows'] );
3254 $prefix = $info['prefix'] ?? $name;
3255
3256 foreach ( $columns as $column ) {
3257 foreach ( $rows as $row ) {
3258 $checkmatrixOptions["$prefix$column-$row"] = true;
3259 }
3260 }
3261
3262 unset( $prefs[$name] );
3263 }
3264 }
3265
3266 // $value is ignored
3267 foreach ( $options as $key => $value ) {
3268 if ( isset( $prefs[$key] ) ) {
3269 $mapping[$key] = 'registered';
3270 } elseif ( isset( $multiselectOptions[$key] ) ) {
3271 $mapping[$key] = 'registered-multiselect';
3272 } elseif ( isset( $checkmatrixOptions[$key] ) ) {
3273 $mapping[$key] = 'registered-checkmatrix';
3274 } elseif ( isset( $specialOptions[$key] ) ) {
3275 $mapping[$key] = 'special';
3276 } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
3277 $mapping[$key] = 'userjs';
3278 } else {
3279 $mapping[$key] = 'unused';
3280 }
3281 }
3282
3283 return $mapping;
3284 }
3285
3300 public function resetOptions(
3301 $resetKinds = [ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ],
3303 ) {
3304 $this->load();
3305 $defaultOptions = self::getDefaultOptions();
3306
3307 if ( !is_array( $resetKinds ) ) {
3308 $resetKinds = [ $resetKinds ];
3309 }
3310
3311 if ( in_array( 'all', $resetKinds ) ) {
3312 $newOptions = $defaultOptions;
3313 } else {
3314 if ( $context === null ) {
3315 $context = RequestContext::getMain();
3316 }
3317
3318 $optionKinds = $this->getOptionKinds( $context );
3319 $resetKinds = array_intersect( $resetKinds, self::listOptionKinds() );
3320 $newOptions = [];
3321
3322 // Use default values for the options that should be deleted, and
3323 // copy old values for the ones that shouldn't.
3324 foreach ( $this->mOptions as $key => $value ) {
3325 if ( in_array( $optionKinds[$key], $resetKinds ) ) {
3326 if ( array_key_exists( $key, $defaultOptions ) ) {
3327 $newOptions[$key] = $defaultOptions[$key];
3328 }
3329 } else {
3330 $newOptions[$key] = $value;
3331 }
3332 }
3333 }
3334
3335 Hooks::run( 'UserResetAllOptions', [ $this, &$newOptions, $this->mOptions, $resetKinds ] );
3336
3337 $this->mOptions = $newOptions;
3338 $this->mOptionsLoaded = true;
3339 }
3340
3345 public function getDatePreference() {
3346 // Important migration for old data rows
3347 if ( is_null( $this->mDatePreference ) ) {
3348 global $wgLang;
3349 $value = $this->getOption( 'date' );
3350 $map = $wgLang->getDatePreferenceMigrationMap();
3351 if ( isset( $map[$value] ) ) {
3352 $value = $map[$value];
3353 }
3354 $this->mDatePreference = $value;
3355 }
3356 return $this->mDatePreference;
3357 }
3358
3365 public function requiresHTTPS() {
3367 if ( $wgForceHTTPS ) {
3368 return true;
3369 }
3370 if ( !$wgSecureLogin ) {
3371 return false;
3372 }
3373 $https = $this->getBoolOption( 'prefershttps' );
3374 Hooks::run( 'UserRequiresHTTPS', [ $this, &$https ] );
3375 if ( $https ) {
3376 $https = wfCanIPUseHTTPS( $this->getRequest()->getIP() );
3377 }
3378
3379 return $https;
3380 }
3381
3387 public function getStubThreshold() {
3388 global $wgMaxArticleSize; # Maximum article size, in Kb
3389 $threshold = $this->getIntOption( 'stubthreshold' );
3390 if ( $threshold > $wgMaxArticleSize * 1024 ) {
3391 // If they have set an impossible value, disable the preference
3392 // so we can use the parser cache again.
3393 $threshold = 0;
3394 }
3395 return $threshold;
3396 }
3397
3406 public function getRights() {
3407 return MediaWikiServices::getInstance()->getPermissionManager()->getUserPermissions( $this );
3408 }
3409
3416 public function getGroups() {
3417 $this->load();
3418 $this->loadGroups();
3419 return array_keys( $this->mGroupMemberships );
3420 }
3421
3429 public function getGroupMemberships() {
3430 $this->load();
3431 $this->loadGroups();
3432 return $this->mGroupMemberships;
3433 }
3434
3442 public function getEffectiveGroups( $recache = false ) {
3443 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
3444 $this->mEffectiveGroups = array_unique( array_merge(
3445 $this->getGroups(), // explicit groups
3446 $this->getAutomaticGroups( $recache ) // implicit groups
3447 ) );
3448 // Avoid PHP 7.1 warning of passing $this by reference
3449 $user = $this;
3450 // Hook for additional groups
3451 Hooks::run( 'UserEffectiveGroups', [ &$user, &$this->mEffectiveGroups ] );
3452 // Force reindexation of groups when a hook has unset one of them
3453 $this->mEffectiveGroups = array_values( array_unique( $this->mEffectiveGroups ) );
3454 }
3455 return $this->mEffectiveGroups;
3456 }
3457
3465 public function getAutomaticGroups( $recache = false ) {
3466 if ( $recache || is_null( $this->mImplicitGroups ) ) {
3467 $this->mImplicitGroups = [ '*' ];
3468 if ( $this->getId() ) {
3469 $this->mImplicitGroups[] = 'user';
3470
3471 $this->mImplicitGroups = array_unique( array_merge(
3472 $this->mImplicitGroups,
3474 ) );
3475 }
3476 if ( $recache ) {
3477 // Assure data consistency with rights/groups,
3478 // as getEffectiveGroups() depends on this function
3479 $this->mEffectiveGroups = null;
3480 }
3481 }
3482 return $this->mImplicitGroups;
3483 }
3484
3494 public function getFormerGroups() {
3495 $this->load();
3496
3497 if ( is_null( $this->mFormerGroups ) ) {
3498 $db = ( $this->queryFlagsUsed & self::READ_LATEST )
3499 ? wfGetDB( DB_MASTER )
3500 : wfGetDB( DB_REPLICA );
3501 $res = $db->select( 'user_former_groups',
3502 [ 'ufg_group' ],
3503 [ 'ufg_user' => $this->mId ],
3504 __METHOD__ );
3505 $this->mFormerGroups = [];
3506 foreach ( $res as $row ) {
3507 $this->mFormerGroups[] = $row->ufg_group;
3508 }
3509 }
3510
3511 return $this->mFormerGroups;
3512 }
3513
3518 public function getEditCount() {
3519 if ( !$this->getId() ) {
3520 return null;
3521 }
3522
3523 if ( $this->mEditCount === null ) {
3524 /* Populate the count, if it has not been populated yet */
3525 $dbr = wfGetDB( DB_REPLICA );
3526 // check if the user_editcount field has been initialized
3527 $count = $dbr->selectField(
3528 'user', 'user_editcount',
3529 [ 'user_id' => $this->mId ],
3530 __METHOD__
3531 );
3532
3533 if ( $count === null ) {
3534 // it has not been initialized. do so.
3535 $count = $this->initEditCountInternal( $dbr );
3536 }
3537 $this->mEditCount = $count;
3538 }
3539 return (int)$this->mEditCount;
3540 }
3541
3553 public function addGroup( $group, $expiry = null ) {
3554 $this->load();
3555 $this->loadGroups();
3556
3557 if ( $expiry ) {
3558 $expiry = wfTimestamp( TS_MW, $expiry );
3559 }
3560
3561 if ( !Hooks::run( 'UserAddGroup', [ $this, &$group, &$expiry ] ) ) {
3562 return false;
3563 }
3564
3565 // create the new UserGroupMembership and put it in the DB
3566 $ugm = new UserGroupMembership( $this->mId, $group, $expiry );
3567 if ( !$ugm->insert( true ) ) {
3568 return false;
3569 }
3570
3571 $this->mGroupMemberships[$group] = $ugm;
3572
3573 // Refresh the groups caches, and clear the rights cache so it will be
3574 // refreshed on the next call to $this->getRights().
3575 $this->getEffectiveGroups( true );
3576 MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache( $this );
3577 $this->invalidateCache();
3578
3579 return true;
3580 }
3581
3588 public function removeGroup( $group ) {
3589 $this->load();
3590
3591 if ( !Hooks::run( 'UserRemoveGroup', [ $this, &$group ] ) ) {
3592 return false;
3593 }
3594
3595 $ugm = UserGroupMembership::getMembership( $this->mId, $group );
3596 // delete the membership entry
3597 if ( !$ugm || !$ugm->delete() ) {
3598 return false;
3599 }
3600
3601 $this->loadGroups();
3602 unset( $this->mGroupMemberships[$group] );
3603
3604 // Refresh the groups caches, and clear the rights cache so it will be
3605 // refreshed on the next call to $this->getRights().
3606 $this->getEffectiveGroups( true );
3607 MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache( $this );
3608 $this->invalidateCache();
3609
3610 return true;
3611 }
3612
3622 public function isRegistered() {
3623 return $this->getId() != 0;
3624 }
3625
3630 public function isLoggedIn() {
3631 return $this->isRegistered();
3632 }
3633
3638 public function isAnon() {
3639 return !$this->isRegistered();
3640 }
3641
3646 public function isBot() {
3647 if ( in_array( 'bot', $this->getGroups() ) && $this->isAllowed( 'bot' ) ) {
3648 return true;
3649 }
3650
3651 $isBot = false;
3652 Hooks::run( "UserIsBot", [ $this, &$isBot ] );
3653
3654 return $isBot;
3655 }
3656
3666 public function isAllowedAny( ...$permissions ) {
3667 return MediaWikiServices::getInstance()
3668 ->getPermissionManager()
3669 ->userHasAnyRight( $this, ...$permissions );
3670 }
3671
3678 public function isAllowedAll( ...$permissions ) {
3679 return MediaWikiServices::getInstance()
3680 ->getPermissionManager()
3681 ->userHasAllRights( $this, ...$permissions );
3682 }
3683
3694 public function isAllowed( $action = '' ) {
3695 return MediaWikiServices::getInstance()->getPermissionManager()
3696 ->userHasRight( $this, $action );
3697 }
3698
3703 public function useRCPatrol() {
3704 global $wgUseRCPatrol;
3705 return $wgUseRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' );
3706 }
3707
3712 public function useNPPatrol() {
3714 return (
3716 && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
3717 );
3718 }
3719
3724 public function useFilePatrol() {
3726 return (
3728 && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
3729 );
3730 }
3731
3737 public function getRequest() {
3738 if ( $this->mRequest ) {
3739 return $this->mRequest;
3740 }
3741
3742 global $wgRequest;
3743 return $wgRequest;
3744 }
3745
3754 public function isWatched( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
3755 if ( $title->isWatchable() && ( !$checkRights || $this->isAllowed( 'viewmywatchlist' ) ) ) {
3756 return MediaWikiServices::getInstance()->getWatchedItemStore()->isWatched( $this, $title );
3757 }
3758 return false;
3759 }
3760
3768 public function addWatch( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
3769 if ( !$checkRights || $this->isAllowed( 'editmywatchlist' ) ) {
3770 MediaWikiServices::getInstance()->getWatchedItemStore()->addWatchBatchForUser(
3771 $this,
3772 [ $title->getSubjectPage(), $title->getTalkPage() ]
3773 );
3774 }
3775 $this->invalidateCache();
3776 }
3777
3785 public function removeWatch( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
3786 if ( !$checkRights || $this->isAllowed( 'editmywatchlist' ) ) {
3787 $store = MediaWikiServices::getInstance()->getWatchedItemStore();
3788 $store->removeWatch( $this, $title->getSubjectPage() );
3789 $store->removeWatch( $this, $title->getTalkPage() );
3790 }
3791 $this->invalidateCache();
3792 }
3793
3802 public function clearNotification( &$title, $oldid = 0 ) {
3804
3805 // Do nothing if the database is locked to writes
3806 if ( wfReadOnly() ) {
3807 return;
3808 }
3809
3810 // Do nothing if not allowed to edit the watchlist
3811 if ( !$this->isAllowed( 'editmywatchlist' ) ) {
3812 return;
3813 }
3814
3815 // If we're working on user's talk page, we should update the talk page message indicator
3816 if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
3817 // Avoid PHP 7.1 warning of passing $this by reference
3818 $user = $this;
3819 if ( !Hooks::run( 'UserClearNewTalkNotification', [ &$user, $oldid ] ) ) {
3820 return;
3821 }
3822
3823 // Try to update the DB post-send and only if needed...
3824 DeferredUpdates::addCallableUpdate( function () use ( $title, $oldid ) {
3825 if ( !$this->getNewtalk() ) {
3826 return; // no notifications to clear
3827 }
3828
3829 // Delete the last notifications (they stack up)
3830 $this->setNewtalk( false );
3831
3832 // If there is a new, unseen, revision, use its timestamp
3833 if ( $oldid ) {
3834 $rl = MediaWikiServices::getInstance()->getRevisionLookup();
3835 $oldRev = $rl->getRevisionById( $oldid, Title::READ_LATEST );
3836 if ( $oldRev ) {
3837 $newRev = $rl->getNextRevision( $oldRev );
3838 if ( $newRev ) {
3839 // TODO: actually no need to wrap in a revision,
3840 // setNewtalk really only needs a RevRecord
3841 $this->setNewtalk( true, new Revision( $newRev ) );
3842 }
3843 }
3844 }
3845 } );
3846 }
3847
3849 return;
3850 }
3851
3852 if ( $this->isAnon() ) {
3853 // Nothing else to do...
3854 return;
3855 }
3856
3857 // Only update the timestamp if the page is being watched.
3858 // The query to find out if it is watched is cached both in memcached and per-invocation,
3859 // and when it does have to be executed, it can be on a replica DB
3860 // If this is the user's newtalk page, we always update the timestamp
3861 $force = '';
3862 if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
3863 $force = 'force';
3864 }
3865
3866 MediaWikiServices::getInstance()->getWatchedItemStore()
3867 ->resetNotificationTimestamp( $this, $title, $force, $oldid );
3868 }
3869
3876 public function clearAllNotifications() {
3878 // Do nothing if not allowed to edit the watchlist
3879 if ( wfReadOnly() || !$this->isAllowed( 'editmywatchlist' ) ) {
3880 return;
3881 }
3882
3884 $this->setNewtalk( false );
3885 return;
3886 }
3887
3888 $id = $this->getId();
3889 if ( !$id ) {
3890 return;
3891 }
3892
3893 $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
3894 $watchedItemStore->resetAllNotificationTimestampsForUser( $this );
3895
3896 // We also need to clear here the "you have new message" notification for the own
3897 // user_talk page; it's cleared one page view later in WikiPage::doViewUpdates().
3898 }
3899
3905 public function getExperienceLevel() {
3906 global $wgLearnerEdits,
3910
3911 if ( $this->isAnon() ) {
3912 return false;
3913 }
3914
3915 $editCount = $this->getEditCount();
3916 $registration = $this->getRegistration();
3917 $now = time();
3918 $learnerRegistration = wfTimestamp( TS_MW, $now - $wgLearnerMemberSince * 86400 );
3919 $experiencedRegistration = wfTimestamp( TS_MW, $now - $wgExperiencedUserMemberSince * 86400 );
3920
3921 if ( $editCount < $wgLearnerEdits ||
3922 $registration > $learnerRegistration ) {
3923 return 'newcomer';
3924 }
3925
3926 if ( $editCount > $wgExperiencedUserEdits &&
3927 $registration <= $experiencedRegistration
3928 ) {
3929 return 'experienced';
3930 }
3931
3932 return 'learner';
3933 }
3934
3943 public function setCookies( $request = null, $secure = null, $rememberMe = false ) {
3944 $this->load();
3945 if ( $this->mId == 0 ) {
3946 return;
3947 }
3948
3949 $session = $this->getRequest()->getSession();
3950 if ( $request && $session->getRequest() !== $request ) {
3951 $session = $session->sessionWithRequest( $request );
3952 }
3953 $delay = $session->delaySave();
3954
3955 if ( !$session->getUser()->equals( $this ) ) {
3956 if ( !$session->canSetUser() ) {
3957 \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
3958 ->warning( __METHOD__ .
3959 ": Cannot save user \"$this\" to a user \"{$session->getUser()}\"'s immutable session"
3960 );
3961 return;
3962 }
3963 $session->setUser( $this );
3964 }
3965
3966 $session->setRememberUser( $rememberMe );
3967 if ( $secure !== null ) {
3968 $session->setForceHTTPS( $secure );
3969 }
3970
3971 $session->persist();
3972
3973 ScopedCallback::consume( $delay );
3974 }
3975
3979 public function logout() {
3980 // Avoid PHP 7.1 warning of passing $this by reference
3981 $user = $this;
3982 if ( Hooks::run( 'UserLogout', [ &$user ] ) ) {
3983 $this->doLogout();
3984 }
3985 }
3986
3991 public function doLogout() {
3992 $session = $this->getRequest()->getSession();
3993 if ( !$session->canSetUser() ) {
3994 \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
3995 ->warning( __METHOD__ . ": Cannot log out of an immutable session" );
3996 $error = 'immutable';
3997 } elseif ( !$session->getUser()->equals( $this ) ) {
3998 \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
3999 ->warning( __METHOD__ .
4000 ": Cannot log user \"$this\" out of a user \"{$session->getUser()}\"'s session"
4001 );
4002 // But we still may as well make this user object anon
4003 $this->clearInstanceCache( 'defaults' );
4004 $error = 'wronguser';
4005 } else {
4006 $this->clearInstanceCache( 'defaults' );
4007 $delay = $session->delaySave();
4008 $session->unpersist(); // Clear cookies (T127436)
4009 $session->setLoggedOutTimestamp( time() );
4010 $session->setUser( new User );
4011 $session->set( 'wsUserID', 0 ); // Other code expects this
4012 $session->resetAllTokens();
4013 ScopedCallback::consume( $delay );
4014 $error = false;
4015 }
4016 \MediaWiki\Logger\LoggerFactory::getInstance( 'authevents' )->info( 'Logout', [
4017 'event' => 'logout',
4018 'successful' => $error === false,
4019 'status' => $error ?: 'success',
4020 ] );
4021 }
4022
4027 public function saveSettings() {
4028 if ( wfReadOnly() ) {
4029 // @TODO: caller should deal with this instead!
4030 // This should really just be an exception.
4031 MWExceptionHandler::logException( new DBExpectedError(
4032 null,
4033 "Could not update user with ID '{$this->mId}'; DB is read-only."
4034 ) );
4035 return;
4036 }
4037
4038 $this->load();
4039 if ( $this->mId == 0 ) {
4040 return; // anon
4041 }
4042
4043 // Get a new user_touched that is higher than the old one.
4044 // This will be used for a CAS check as a last-resort safety
4045 // check against race conditions and replica DB lag.
4046 $newTouched = $this->newTouchedTimestamp();
4047
4048 $dbw = wfGetDB( DB_MASTER );
4049 $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) use ( $newTouched ) {
4050 $dbw->update( 'user',
4051 [ /* SET */
4052 'user_name' => $this->mName,
4053 'user_real_name' => $this->mRealName,
4054 'user_email' => $this->mEmail,
4055 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
4056 'user_touched' => $dbw->timestamp( $newTouched ),
4057 'user_token' => strval( $this->mToken ),
4058 'user_email_token' => $this->mEmailToken,
4059 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
4060 ], $this->makeUpdateConditions( $dbw, [ /* WHERE */
4061 'user_id' => $this->mId,
4062 ] ), $fname
4063 );
4064
4065 if ( !$dbw->affectedRows() ) {
4066 // Maybe the problem was a missed cache update; clear it to be safe
4067 $this->clearSharedCache( 'refresh' );
4068 // User was changed in the meantime or loaded with stale data
4069 $from = ( $this->queryFlagsUsed & self::READ_LATEST ) ? 'master' : 'replica';
4070 LoggerFactory::getInstance( 'preferences' )->warning(
4071 "CAS update failed on user_touched for user ID '{user_id}' ({db_flag} read)",
4072 [ 'user_id' => $this->mId, 'db_flag' => $from ]
4073 );
4074 throw new MWException( "CAS update failed on user_touched. " .
4075 "The version of the user to be saved is older than the current version."
4076 );
4077 }
4078
4079 $dbw->update(
4080 'actor',
4081 [ 'actor_name' => $this->mName ],
4082 [ 'actor_user' => $this->mId ],
4083 $fname
4084 );
4085 } );
4086
4087 $this->mTouched = $newTouched;
4088 $this->saveOptions();
4089
4090 Hooks::run( 'UserSaveSettings', [ $this ] );
4091 $this->clearSharedCache( 'changed' );
4092 $this->getUserPage()->purgeSquid();
4093 }
4094
4101 public function idForName( $flags = 0 ) {
4102 $s = trim( $this->getName() );
4103 if ( $s === '' ) {
4104 return 0;
4105 }
4106
4107 $db = ( ( $flags & self::READ_LATEST ) == self::READ_LATEST )
4108 ? wfGetDB( DB_MASTER )
4109 : wfGetDB( DB_REPLICA );
4110
4111 $options = ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING )
4112 ? [ 'LOCK IN SHARE MODE' ]
4113 : [];
4114
4115 $id = $db->selectField( 'user',
4116 'user_id', [ 'user_name' => $s ], __METHOD__, $options );
4117
4118 return (int)$id;
4119 }
4120
4136 public static function createNew( $name, $params = [] ) {
4137 foreach ( [ 'password', 'newpassword', 'newpass_time', 'password_expires' ] as $field ) {
4138 if ( isset( $params[$field] ) ) {
4139 wfDeprecated( __METHOD__ . " with param '$field'", '1.27' );
4140 unset( $params[$field] );
4141 }
4142 }
4143
4144 $user = new User;
4145 $user->load();
4146 $user->setToken(); // init token
4147 if ( isset( $params['options'] ) ) {
4148 $user->mOptions = $params['options'] + (array)$user->mOptions;
4149 unset( $params['options'] );
4150 }
4151 $dbw = wfGetDB( DB_MASTER );
4152
4153 $noPass = PasswordFactory::newInvalidPassword()->toString();
4154
4155 $fields = [
4156 'user_name' => $name,
4157 'user_password' => $noPass,
4158 'user_newpassword' => $noPass,
4159 'user_email' => $user->mEmail,
4160 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
4161 'user_real_name' => $user->mRealName,
4162 'user_token' => strval( $user->mToken ),
4163 'user_registration' => $dbw->timestamp( $user->mRegistration ),
4164 'user_editcount' => 0,
4165 'user_touched' => $dbw->timestamp( $user->newTouchedTimestamp() ),
4166 ];
4167 foreach ( $params as $name => $value ) {
4168 $fields["user_$name"] = $value;
4169 }
4170
4171 return $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) use ( $fields ) {
4172 $dbw->insert( 'user', $fields, $fname, [ 'IGNORE' ] );
4173 if ( $dbw->affectedRows() ) {
4174 $newUser = self::newFromId( $dbw->insertId() );
4175 $newUser->mName = $fields['user_name'];
4176 $newUser->updateActorId( $dbw );
4177 // Load the user from master to avoid replica lag
4178 $newUser->load( self::READ_LATEST );
4179 } else {
4180 $newUser = null;
4181 }
4182 return $newUser;
4183 } );
4184 }
4185
4212 public function addToDatabase() {
4213 $this->load();
4214 if ( !$this->mToken ) {
4215 $this->setToken(); // init token
4216 }
4217
4218 if ( !is_string( $this->mName ) ) {
4219 throw new RuntimeException( "User name field is not set." );
4220 }
4221
4222 $this->mTouched = $this->newTouchedTimestamp();
4223
4224 $dbw = wfGetDB( DB_MASTER );
4225 $status = $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) {
4226 $noPass = PasswordFactory::newInvalidPassword()->toString();
4227 $dbw->insert( 'user',
4228 [
4229 'user_name' => $this->mName,
4230 'user_password' => $noPass,
4231 'user_newpassword' => $noPass,
4232 'user_email' => $this->mEmail,
4233 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
4234 'user_real_name' => $this->mRealName,
4235 'user_token' => strval( $this->mToken ),
4236 'user_registration' => $dbw->timestamp( $this->mRegistration ),
4237 'user_editcount' => 0,
4238 'user_touched' => $dbw->timestamp( $this->mTouched ),
4239 ], $fname,
4240 [ 'IGNORE' ]
4241 );
4242 if ( !$dbw->affectedRows() ) {
4243 // Use locking reads to bypass any REPEATABLE-READ snapshot.
4244 $this->mId = $dbw->selectField(
4245 'user',
4246 'user_id',
4247 [ 'user_name' => $this->mName ],
4248 $fname,
4249 [ 'LOCK IN SHARE MODE' ]
4250 );
4251 $loaded = false;
4252 if ( $this->mId && $this->loadFromDatabase( self::READ_LOCKING ) ) {
4253 $loaded = true;
4254 }
4255 if ( !$loaded ) {
4256 throw new MWException( $fname . ": hit a key conflict attempting " .
4257 "to insert user '{$this->mName}' row, but it was not present in select!" );
4258 }
4259 return Status::newFatal( 'userexists' );
4260 }
4261 $this->mId = $dbw->insertId();
4262 self::$idCacheByName[$this->mName] = $this->mId;
4263 $this->updateActorId( $dbw );
4264
4265 return Status::newGood();
4266 } );
4267 if ( !$status->isGood() ) {
4268 return $status;
4269 }
4270
4271 // Clear instance cache other than user table data and actor, which is already accurate
4272 $this->clearInstanceCache();
4273
4274 $this->saveOptions();
4275 return Status::newGood();
4276 }
4277
4282 private function updateActorId( IDatabase $dbw ) {
4283 $dbw->insert(
4284 'actor',
4285 [ 'actor_user' => $this->mId, 'actor_name' => $this->mName ],
4286 __METHOD__
4287 );
4288 $this->mActorId = (int)$dbw->insertId();
4289 }
4290
4296 public function spreadAnyEditBlock() {
4297 if ( $this->isLoggedIn() && $this->getBlock() ) {
4298 return $this->spreadBlock();
4299 }
4300
4301 return false;
4302 }
4303
4309 protected function spreadBlock() {
4310 wfDebug( __METHOD__ . "()\n" );
4311 $this->load();
4312 if ( $this->mId == 0 ) {
4313 return false;
4314 }
4315
4316 $userblock = DatabaseBlock::newFromTarget( $this->getName() );
4317 if ( !$userblock ) {
4318 return false;
4319 }
4320
4321 return (bool)$userblock->doAutoblock( $this->getRequest()->getIP() );
4322 }
4323
4328 public function isBlockedFromCreateAccount() {
4329 $this->getBlockedStatus();
4330 if ( $this->mBlock && $this->mBlock->appliesToRight( 'createaccount' ) ) {
4331 return $this->mBlock;
4332 }
4333
4334 # T15611: if the IP address the user is trying to create an account from is
4335 # blocked with createaccount disabled, prevent new account creation there even
4336 # when the user is logged in
4337 if ( $this->mBlockedFromCreateAccount === false && !$this->isAllowed( 'ipblock-exempt' ) ) {
4338 $this->mBlockedFromCreateAccount = DatabaseBlock::newFromTarget(
4339 null, $this->getRequest()->getIP()
4340 );
4341 }
4342 return $this->mBlockedFromCreateAccount instanceof AbstractBlock
4343 && $this->mBlockedFromCreateAccount->appliesToRight( 'createaccount' )
4344 ? $this->mBlockedFromCreateAccount
4345 : false;
4346 }
4347
4352 public function isBlockedFromEmailuser() {
4353 $this->getBlockedStatus();
4354 return $this->mBlock && $this->mBlock->appliesToRight( 'sendemail' );
4355 }
4356
4363 public function isBlockedFromUpload() {
4364 $this->getBlockedStatus();
4365 return $this->mBlock && $this->mBlock->appliesToRight( 'upload' );
4366 }
4367
4372 public function isAllowedToCreateAccount() {
4373 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
4374 }
4375
4381 public function getUserPage() {
4382 return Title::makeTitle( NS_USER, $this->getName() );
4383 }
4384
4390 public function getTalkPage() {
4391 $title = $this->getUserPage();
4392 return $title->getTalkPage();
4393 }
4394
4400 public function isNewbie() {
4401 return !$this->isAllowed( 'autoconfirmed' );
4402 }
4403
4410 public function checkPassword( $password ) {
4411 wfDeprecated( __METHOD__, '1.27' );
4412
4413 $manager = AuthManager::singleton();
4414 $reqs = AuthenticationRequest::loadRequestsFromSubmission(
4415 $manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN ),
4416 [
4417 'username' => $this->getName(),
4418 'password' => $password,
4419 ]
4420 );
4421 $res = $manager->beginAuthentication( $reqs, 'null:' );
4422 switch ( $res->status ) {
4423 case AuthenticationResponse::PASS:
4424 return true;
4425 case AuthenticationResponse::FAIL:
4426 // Hope it's not a PreAuthenticationProvider that failed...
4427 LoggerFactory::getInstance( 'authentication' )
4428 ->info( __METHOD__ . ': Authentication failed: ' . $res->message->plain() );
4429 return false;
4430 default:
4431 throw new BadMethodCallException(
4432 'AuthManager returned a response unsupported by ' . __METHOD__
4433 );
4434 }
4435 }
4436
4445 public function checkTemporaryPassword( $plaintext ) {
4446 wfDeprecated( __METHOD__, '1.27' );
4447 // Can't check the temporary password individually.
4448 return $this->checkPassword( $plaintext );
4449 }
4450
4462 public function getEditTokenObject( $salt = '', $request = null ) {
4463 if ( $this->isAnon() ) {
4464 return new LoggedOutEditToken();
4465 }
4466
4467 if ( !$request ) {
4468 $request = $this->getRequest();
4469 }
4470 return $request->getSession()->getToken( $salt );
4471 }
4472
4486 public function getEditToken( $salt = '', $request = null ) {
4487 return $this->getEditTokenObject( $salt, $request )->toString();
4488 }
4489
4502 public function matchEditToken( $val, $salt = '', $request = null, $maxage = null ) {
4503 return $this->getEditTokenObject( $salt, $request )->match( $val, $maxage );
4504 }
4505
4516 public function matchEditTokenNoSuffix( $val, $salt = '', $request = null, $maxage = null ) {
4517 $val = substr( $val, 0, strspn( $val, '0123456789abcdef' ) ) . Token::SUFFIX;
4518 return $this->matchEditToken( $val, $salt, $request, $maxage );
4519 }
4520
4528 public function sendConfirmationMail( $type = 'created' ) {
4529 global $wgLang;
4530 $expiration = null; // gets passed-by-ref and defined in next line.
4531 $token = $this->confirmationToken( $expiration );
4532 $url = $this->confirmationTokenUrl( $token );
4533 $invalidateURL = $this->invalidationTokenUrl( $token );
4534 $this->saveSettings();
4535
4536 if ( $type == 'created' || $type === false ) {
4537 $message = 'confirmemail_body';
4538 $type = 'created';
4539 } elseif ( $type === true ) {
4540 $message = 'confirmemail_body_changed';
4541 $type = 'changed';
4542 } else {
4543 // Messages: confirmemail_body_changed, confirmemail_body_set
4544 $message = 'confirmemail_body_' . $type;
4545 }
4546
4547 $mail = [
4548 'subject' => wfMessage( 'confirmemail_subject' )->text(),
4549 'body' => wfMessage( $message,
4550 $this->getRequest()->getIP(),
4551 $this->getName(),
4552 $url,
4553 $wgLang->userTimeAndDate( $expiration, $this ),
4554 $invalidateURL,
4555 $wgLang->userDate( $expiration, $this ),
4556 $wgLang->userTime( $expiration, $this ) )->text(),
4557 'from' => null,
4558 'replyTo' => null,
4559 ];
4560 $info = [
4561 'type' => $type,
4562 'ip' => $this->getRequest()->getIP(),
4563 'confirmURL' => $url,
4564 'invalidateURL' => $invalidateURL,
4565 'expiration' => $expiration
4566 ];
4567
4568 Hooks::run( 'UserSendConfirmationMail', [ $this, &$mail, $info ] );
4569 return $this->sendMail( $mail['subject'], $mail['body'], $mail['from'], $mail['replyTo'] );
4570 }
4571
4583 public function sendMail( $subject, $body, $from = null, $replyto = null ) {
4584 global $wgPasswordSender;
4585
4586 if ( $from instanceof User ) {
4587 $sender = MailAddress::newFromUser( $from );
4588 } else {
4589 $sender = new MailAddress( $wgPasswordSender,
4590 wfMessage( 'emailsender' )->inContentLanguage()->text() );
4591 }
4592 $to = MailAddress::newFromUser( $this );
4593
4594 return UserMailer::send( $to, $sender, $subject, $body, [
4595 'replyTo' => $replyto,
4596 ] );
4597 }
4598
4609 protected function confirmationToken( &$expiration ) {
4611 $now = time();
4612 $expires = $now + $wgUserEmailConfirmationTokenExpiry;
4613 $expiration = wfTimestamp( TS_MW, $expires );
4614 $this->load();
4615 $token = MWCryptRand::generateHex( 32 );
4616 $hash = md5( $token );
4617 $this->mEmailToken = $hash;
4618 $this->mEmailTokenExpires = $expiration;
4619 return $token;
4620 }
4621
4627 protected function confirmationTokenUrl( $token ) {
4628 return $this->getTokenUrl( 'ConfirmEmail', $token );
4629 }
4630
4636 protected function invalidationTokenUrl( $token ) {
4637 return $this->getTokenUrl( 'InvalidateEmail', $token );
4638 }
4639
4654 protected function getTokenUrl( $page, $token ) {
4655 // Hack to bypass localization of 'Special:'
4656 $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" );
4657 return $title->getCanonicalURL();
4658 }
4659
4667 public function confirmEmail() {
4668 // Check if it's already confirmed, so we don't touch the database
4669 // and fire the ConfirmEmailComplete hook on redundant confirmations.
4670 if ( !$this->isEmailConfirmed() ) {
4672 Hooks::run( 'ConfirmEmailComplete', [ $this ] );
4673 }
4674 return true;
4675 }
4676
4684 public function invalidateEmail() {
4685 $this->load();
4686 $this->mEmailToken = null;
4687 $this->mEmailTokenExpires = null;
4688 $this->setEmailAuthenticationTimestamp( null );
4689 $this->mEmail = '';
4690 Hooks::run( 'InvalidateEmailComplete', [ $this ] );
4691 return true;
4692 }
4693
4698 public function setEmailAuthenticationTimestamp( $timestamp ) {
4699 $this->load();
4700 $this->mEmailAuthenticated = $timestamp;
4701 Hooks::run( 'UserSetEmailAuthenticationTimestamp', [ $this, &$this->mEmailAuthenticated ] );
4702 }
4703
4709 public function canSendEmail() {
4711 if ( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
4712 return false;
4713 }
4714 $canSend = $this->isEmailConfirmed();
4715 // Avoid PHP 7.1 warning of passing $this by reference
4716 $user = $this;
4717 Hooks::run( 'UserCanSendEmail', [ &$user, &$canSend ] );
4718 return $canSend;
4719 }
4720
4726 public function canReceiveEmail() {
4727 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
4728 }
4729
4740 public function isEmailConfirmed() {
4742 $this->load();
4743 // Avoid PHP 7.1 warning of passing $this by reference
4744 $user = $this;
4745 $confirmed = true;
4746 if ( Hooks::run( 'EmailConfirmed', [ &$user, &$confirmed ] ) ) {
4747 if ( $this->isAnon() ) {
4748 return false;
4749 }
4750 if ( !Sanitizer::validateEmail( $this->mEmail ) ) {
4751 return false;
4752 }
4754 return false;
4755 }
4756 return true;
4757 }
4758
4759 return $confirmed;
4760 }
4761
4766 public function isEmailConfirmationPending() {
4768 return $wgEmailAuthentication &&
4769 !$this->isEmailConfirmed() &&
4770 $this->mEmailToken &&
4771 $this->mEmailTokenExpires > wfTimestamp();
4772 }
4773
4781 public function getRegistration() {
4782 if ( $this->isAnon() ) {
4783 return false;
4784 }
4785 $this->load();
4786 return $this->mRegistration;
4787 }
4788
4795 public function getFirstEditTimestamp() {
4796 return $this->getEditTimestamp( true );
4797 }
4798
4806 public function getLatestEditTimestamp() {
4807 return $this->getEditTimestamp( false );
4808 }
4809
4817 private function getEditTimestamp( $first ) {
4818 if ( $this->getId() == 0 ) {
4819 return false; // anons
4820 }
4821 $dbr = wfGetDB( DB_REPLICA );
4822 $actorWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $this );
4823 $tsField = isset( $actorWhere['tables']['temp_rev_user'] )
4824 ? 'revactor_timestamp' : 'rev_timestamp';
4825 $sortOrder = $first ? 'ASC' : 'DESC';
4826 $time = $dbr->selectField(
4827 [ 'revision' ] + $actorWhere['tables'],
4828 $tsField,
4829 [ $actorWhere['conds'] ],
4830 __METHOD__,
4831 [ 'ORDER BY' => "$tsField $sortOrder" ],
4832 $actorWhere['joins']
4833 );
4834 if ( !$time ) {
4835 return false; // no edits
4836 }
4837 return wfTimestamp( TS_MW, $time );
4838 }
4839
4849 public static function getGroupPermissions( $groups ) {
4850 return MediaWikiServices::getInstance()->getPermissionManager()->getGroupPermissions( $groups );
4851 }
4852
4862 public static function getGroupsWithPermission( $role ) {
4863 return MediaWikiServices::getInstance()->getPermissionManager()->getGroupsWithPermission( $role );
4864 }
4865
4881 public static function groupHasPermission( $group, $role ) {
4882 return MediaWikiServices::getInstance()->getPermissionManager()
4883 ->groupHasPermission( $group, $role );
4884 }
4885
4904 public static function isEveryoneAllowed( $right ) {
4905 return MediaWikiServices::getInstance()->getPermissionManager()->isEveryoneAllowed( $right );
4906 }
4907
4914 public static function getAllGroups() {
4916 return array_values( array_diff(
4917 array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
4918 self::getImplicitGroups()
4919 ) );
4920 }
4921
4929 public static function getAllRights() {
4930 return MediaWikiServices::getInstance()->getPermissionManager()->getAllPermissions();
4931 }
4932
4939 public static function getImplicitGroups() {
4940 global $wgImplicitGroups;
4941 return $wgImplicitGroups;
4942 }
4943
4953 public static function changeableByGroup( $group ) {
4955
4956 $groups = [
4957 'add' => [],
4958 'remove' => [],
4959 'add-self' => [],
4960 'remove-self' => []
4961 ];
4962
4963 if ( empty( $wgAddGroups[$group] ) ) {
4964 // Don't add anything to $groups
4965 } elseif ( $wgAddGroups[$group] === true ) {
4966 // You get everything
4967 $groups['add'] = self::getAllGroups();
4968 } elseif ( is_array( $wgAddGroups[$group] ) ) {
4969 $groups['add'] = $wgAddGroups[$group];
4970 }
4971
4972 // Same thing for remove
4973 if ( empty( $wgRemoveGroups[$group] ) ) {
4974 // Do nothing
4975 } elseif ( $wgRemoveGroups[$group] === true ) {
4976 $groups['remove'] = self::getAllGroups();
4977 } elseif ( is_array( $wgRemoveGroups[$group] ) ) {
4978 $groups['remove'] = $wgRemoveGroups[$group];
4979 }
4980
4981 // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
4982 if ( empty( $wgGroupsAddToSelf['user'] ) || $wgGroupsAddToSelf['user'] !== true ) {
4983 foreach ( $wgGroupsAddToSelf as $key => $value ) {
4984 if ( is_int( $key ) ) {
4985 $wgGroupsAddToSelf['user'][] = $value;
4986 }
4987 }
4988 }
4989
4990 if ( empty( $wgGroupsRemoveFromSelf['user'] ) || $wgGroupsRemoveFromSelf['user'] !== true ) {
4991 foreach ( $wgGroupsRemoveFromSelf as $key => $value ) {
4992 if ( is_int( $key ) ) {
4993 $wgGroupsRemoveFromSelf['user'][] = $value;
4994 }
4995 }
4996 }
4997
4998 // Now figure out what groups the user can add to him/herself
4999 if ( empty( $wgGroupsAddToSelf[$group] ) ) {
5000 // Do nothing
5001 } elseif ( $wgGroupsAddToSelf[$group] === true ) {
5002 // No idea WHY this would be used, but it's there
5003 $groups['add-self'] = self::getAllGroups();
5004 } elseif ( is_array( $wgGroupsAddToSelf[$group] ) ) {
5005 $groups['add-self'] = $wgGroupsAddToSelf[$group];
5006 }
5007
5008 if ( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
5009 // Do nothing
5010 } elseif ( $wgGroupsRemoveFromSelf[$group] === true ) {
5011 $groups['remove-self'] = self::getAllGroups();
5012 } elseif ( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
5013 $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
5014 }
5015
5016 return $groups;
5017 }
5018
5026 public function changeableGroups() {
5027 if ( $this->isAllowed( 'userrights' ) ) {
5028 // This group gives the right to modify everything (reverse-
5029 // compatibility with old "userrights lets you change
5030 // everything")
5031 // Using array_merge to make the groups reindexed
5032 $all = array_merge( self::getAllGroups() );
5033 return [
5034 'add' => $all,
5035 'remove' => $all,
5036 'add-self' => [],
5037 'remove-self' => []
5038 ];
5039 }
5040
5041 // Okay, it's not so simple, we will have to go through the arrays
5042 $groups = [
5043 'add' => [],
5044 'remove' => [],
5045 'add-self' => [],
5046 'remove-self' => []
5047 ];
5048 $addergroups = $this->getEffectiveGroups();
5049
5050 foreach ( $addergroups as $addergroup ) {
5051 $groups = array_merge_recursive(
5052 $groups, $this->changeableByGroup( $addergroup )
5053 );
5054 $groups['add'] = array_unique( $groups['add'] );
5055 $groups['remove'] = array_unique( $groups['remove'] );
5056 $groups['add-self'] = array_unique( $groups['add-self'] );
5057 $groups['remove-self'] = array_unique( $groups['remove-self'] );
5058 }
5059 return $groups;
5060 }
5061
5065 public function incEditCount() {
5066 if ( $this->isAnon() ) {
5067 return; // sanity
5068 }
5069
5070 DeferredUpdates::addUpdate(
5071 new UserEditCountUpdate( $this, 1 ),
5072 DeferredUpdates::POSTSEND
5073 );
5074 }
5075
5081 public function setEditCountInternal( $count ) {
5082 $this->mEditCount = $count;
5083 }
5084
5093 // Pull from a replica DB to be less cruel to servers
5094 // Accuracy isn't the point anyway here
5095 $actorWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $this );
5096 $count = (int)$dbr->selectField(
5097 [ 'revision' ] + $actorWhere['tables'],
5098 'COUNT(*)',
5099 [ $actorWhere['conds'] ],
5100 __METHOD__,
5101 [],
5102 $actorWhere['joins']
5103 );
5104
5105 $dbw = wfGetDB( DB_MASTER );
5106 $dbw->update(
5107 'user',
5108 [ 'user_editcount' => $count ],
5109 [
5110 'user_id' => $this->getId(),
5111 'user_editcount IS NULL OR user_editcount < ' . (int)$count
5112 ],
5113 __METHOD__
5114 );
5115
5116 return $count;
5117 }
5118
5126 public static function getRightDescription( $right ) {
5127 $key = "right-$right";
5128 $msg = wfMessage( $key );
5129 return $msg->isDisabled() ? $right : $msg->text();
5130 }
5131
5139 public static function getGrantName( $grant ) {
5140 $key = "grant-$grant";
5141 $msg = wfMessage( $key );
5142 return $msg->isDisabled() ? $grant : $msg->text();
5143 }
5144
5165 public function addNewUserLogEntry( $action = false, $reason = '' ) {
5166 return true; // disabled
5167 }
5168
5178 wfDeprecated( __METHOD__, '1.27' );
5179 $this->addNewUserLogEntry( 'autocreate' );
5180
5181 return true;
5182 }
5183
5189 protected function loadOptions( $data = null ) {
5190 $this->load();
5191
5192 if ( $this->mOptionsLoaded ) {
5193 return;
5194 }
5195
5196 $this->mOptions = self::getDefaultOptions();
5197
5198 if ( !$this->getId() ) {
5199 // For unlogged-in users, load language/variant options from request.
5200 // There's no need to do it for logged-in users: they can set preferences,
5201 // and handling of page content is done by $pageLang->getPreferredVariant() and such,
5202 // so don't override user's choice (especially when the user chooses site default).
5203 $variant = MediaWikiServices::getInstance()->getContentLanguage()->getDefaultVariant();
5204 $this->mOptions['variant'] = $variant;
5205 $this->mOptions['language'] = $variant;
5206 $this->mOptionsLoaded = true;
5207 return;
5208 }
5209
5210 // Maybe load from the object
5211 if ( !is_null( $this->mOptionOverrides ) ) {
5212 wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" );
5213 foreach ( $this->mOptionOverrides as $key => $value ) {
5214 $this->mOptions[$key] = $value;
5215 }
5216 } else {
5217 if ( !is_array( $data ) ) {
5218 wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" );
5219 // Load from database
5220 $dbr = ( $this->queryFlagsUsed & self::READ_LATEST )
5221 ? wfGetDB( DB_MASTER )
5222 : wfGetDB( DB_REPLICA );
5223
5224 $res = $dbr->select(
5225 'user_properties',
5226 [ 'up_property', 'up_value' ],
5227 [ 'up_user' => $this->getId() ],
5228 __METHOD__
5229 );
5230
5231 $this->mOptionOverrides = [];
5232 $data = [];
5233 foreach ( $res as $row ) {
5234 // Convert '0' to 0. PHP's boolean conversion considers them both
5235 // false, but e.g. JavaScript considers the former as true.
5236 // @todo: T54542 Somehow determine the desired type (string/int/bool)
5237 // and convert all values here.
5238 if ( $row->up_value === '0' ) {
5239 $row->up_value = 0;
5240 }
5241 $data[$row->up_property] = $row->up_value;
5242 }
5243 }
5244
5245 foreach ( $data as $property => $value ) {
5246 $this->mOptionOverrides[$property] = $value;
5247 $this->mOptions[$property] = $value;
5248 }
5249 }
5250
5251 // Replace deprecated language codes
5252 $this->mOptions['language'] = LanguageCode::replaceDeprecatedCodes(
5253 $this->mOptions['language']
5254 );
5255
5256 $this->mOptionsLoaded = true;
5257
5258 Hooks::run( 'UserLoadOptions', [ $this, &$this->mOptions ] );
5259 }
5260
5266 protected function saveOptions() {
5267 $this->loadOptions();
5268
5269 // Not using getOptions(), to keep hidden preferences in database
5270 $saveOptions = $this->mOptions;
5271
5272 // Allow hooks to abort, for instance to save to a global profile.
5273 // Reset options to default state before saving.
5274 if ( !Hooks::run( 'UserSaveOptions', [ $this, &$saveOptions ] ) ) {
5275 return;
5276 }
5277
5278 $userId = $this->getId();
5279
5280 $insert_rows = []; // all the new preference rows
5281 foreach ( $saveOptions as $key => $value ) {
5282 // Don't bother storing default values
5283 $defaultOption = self::getDefaultOption( $key );
5284 if ( ( $defaultOption === null && $value !== false && $value !== null )
5285 || $value != $defaultOption
5286 ) {
5287 $insert_rows[] = [
5288 'up_user' => $userId,
5289 'up_property' => $key,
5290 'up_value' => $value,
5291 ];
5292 }
5293 }
5294
5295 $dbw = wfGetDB( DB_MASTER );
5296
5297 $res = $dbw->select( 'user_properties',
5298 [ 'up_property', 'up_value' ], [ 'up_user' => $userId ], __METHOD__ );
5299
5300 // Find prior rows that need to be removed or updated. These rows will
5301 // all be deleted (the latter so that INSERT IGNORE applies the new values).
5302 $keysDelete = [];
5303 foreach ( $res as $row ) {
5304 if ( !isset( $saveOptions[$row->up_property] )
5305 || strcmp( $saveOptions[$row->up_property], $row->up_value ) != 0
5306 ) {
5307 $keysDelete[] = $row->up_property;
5308 }
5309 }
5310
5311 if ( count( $keysDelete ) ) {
5312 // Do the DELETE by PRIMARY KEY for prior rows.
5313 // In the past a very large portion of calls to this function are for setting
5314 // 'rememberpassword' for new accounts (a preference that has since been removed).
5315 // Doing a blanket per-user DELETE for new accounts with no rows in the table
5316 // caused gap locks on [max user ID,+infinity) which caused high contention since
5317 // updates would pile up on each other as they are for higher (newer) user IDs.
5318 // It might not be necessary these days, but it shouldn't hurt either.
5319 $dbw->delete( 'user_properties',
5320 [ 'up_user' => $userId, 'up_property' => $keysDelete ], __METHOD__ );
5321 }
5322 // Insert the new preference rows
5323 $dbw->insert( 'user_properties', $insert_rows, __METHOD__, [ 'IGNORE' ] );
5324 }
5325
5332 public static function selectFields() {
5333 wfDeprecated( __METHOD__, '1.31' );
5334 return [
5335 'user_id',
5336 'user_name',
5337 'user_real_name',
5338 'user_email',
5339 'user_touched',
5340 'user_token',
5341 'user_email_authenticated',
5342 'user_email_token',
5343 'user_email_token_expires',
5344 'user_registration',
5345 'user_editcount',
5346 ];
5347 }
5348
5358 public static function getQueryInfo() {
5359 $ret = [
5360 'tables' => [ 'user', 'user_actor' => 'actor' ],
5361 'fields' => [
5362 'user_id',
5363 'user_name',
5364 'user_real_name',
5365 'user_email',
5366 'user_touched',
5367 'user_token',
5368 'user_email_authenticated',
5369 'user_email_token',
5370 'user_email_token_expires',
5371 'user_registration',
5372 'user_editcount',
5373 'user_actor.actor_id',
5374 ],
5375 'joins' => [
5376 'user_actor' => [ 'JOIN', 'user_actor.actor_user = user_id' ],
5377 ],
5378 ];
5379
5380 return $ret;
5381 }
5382
5390 static function newFatalPermissionDeniedStatus( $permission ) {
5391 global $wgLang;
5392
5393 $groups = [];
5394 foreach ( MediaWikiServices::getInstance()
5396 ->getGroupsWithPermission( $permission ) as $group ) {
5397 $groups[] = UserGroupMembership::getLink( $group, RequestContext::getMain(), 'wiki' );
5398 }
5399
5400 if ( $groups ) {
5401 return Status::newFatal( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) );
5402 }
5403
5404 return Status::newFatal( 'badaccess-group0' );
5405 }
5406
5416 public function getInstanceForUpdate() {
5417 if ( !$this->getId() ) {
5418 return null; // anon
5419 }
5420
5421 $user = self::newFromId( $this->getId() );
5422 if ( !$user->loadFromId( self::READ_EXCLUSIVE ) ) {
5423 return null;
5424 }
5425
5426 return $user;
5427 }
5428
5436 public function equals( UserIdentity $user ) {
5437 // XXX it's not clear whether central ID providers are supposed to obey this
5438 return $this->getName() === $user->getName();
5439 }
5440
5446 public function isAllowUsertalk() {
5447 return $this->mAllowUsertalk;
5448 }
5449
5450}
getPermissionManager()
$wgRateLimitsExcludedIPs
Array of IPs / CIDR ranges which should be excluded from rate limits.
$wgGroupsRemoveFromSelf
$wgUserEmailConfirmationTokenExpiry
The time, in seconds, when an email confirmation email expires.
$wgRemoveGroups
$wgMaxArticleSize
Maximum article size in kilobytes.
$wgLearnerMemberSince
Specify the difference engine to use.
$wgProxyList
Big list of banned IP addresses.
$wgHiddenPrefs
An array of preferences to not show for the user.
$wgDefaultUserOptions
Settings added to this array will override the default globals for the user preferences used by anony...
$wgDisableAnonTalk
Disable links to talk pages of anonymous users (IPs) in listings on special pages like page history,...
$wgAutopromoteOnceLogInRC
Put user rights log entries for autopromotion in recent changes?
$wgEnableUserEmail
Set to true to enable user-to-user e-mail.
$wgPasswordPolicy
Password policy for the wiki.
$wgExperiencedUserMemberSince
Specify the difference engine to use.
$wgUseFilePatrol
Use file patrolling to check new files on Special:Newfiles.
string null $wgAuthenticationTokenVersion
Versioning for authentication tokens.
$wgGroupsAddToSelf
A map of group names that the user is in, to group names that those users are allowed to add or revok...
$wgShowUpdatedMarker
Show "Updated (since my last visit)" marker in RC view, watchlist and history view for watched pages ...
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
$wgLearnerEdits
The following variables define 3 user experience levels:
$wgUseNPPatrol
Use new page patrolling to check new pages on Special:Newpages.
$wgInvalidUsernameCharacters
Characters to prevent during new account creations.
$wgEnableEmail
Set to true to enable the e-mail basic features: Password reminders, etc.
$wgMaxNameChars
Maximum number of bytes in username.
$wgSecureLogin
This is to let user authenticate using https when they come from http.
$wgImplicitGroups
Implicit groups, aren't shown on Special:Listusers or somewhere else.
$wgAddGroups
$wgAddGroups and $wgRemoveGroups can be used to give finer control over who can assign which groups a...
$wgExperiencedUserEdits
Specify the difference engine to use.
$wgReservedUsernames
Array of usernames which may not be registered or logged in from Maintenance scripts can still use th...
$wgDefaultSkin
Default skin, for new users and anonymous visitors.
$wgRevokePermissions
Permission keys revoked from users in each group.
bool $wgForceHTTPS
If this is true, when an insecure HTTP request is received, always redirect to HTTPS.
$wgEmailAuthentication
Require email authentication before sending mail to an email address.
$wgPasswordSender
Sender email address for e-mail notifications.
$wgRateLimits
Simple rate limiter options to brake edit floods.
$wgNamespacesToBeSearchedDefault
List of namespaces which are searched by default.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfTimestampOrNull( $outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfReadOnly()
Check whether the wiki is in read-only mode.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfCanIPUseHTTPS( $ip)
Determine whether the client at a given source IP is likely to be able to access the wiki via HTTPS.
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
$wgGroupPermissions['sysop']['replacetext']
foreach( $wgExtensionFunctions as $func) if(!defined('MW_NO_SESSION') &&! $wgCommandLineMode) if(! $wgCommandLineMode) $wgFullyInitialised
Definition Setup.php:931
$wgUseEnotif
Definition Setup.php:437
if(! $wgDBerrorLogTZ) $wgRequest
Definition Setup.php:751
$wgLang
Definition Setup.php:880
static getAutopromoteOnceGroups(User $user, $event)
Get the groups for the given user based on the given criteria.
static getAutopromoteGroups(User $user)
Get the groups for the given user based on $wgAutopromote.
static isExternal( $username)
Tells whether the username is external or not.
Value object representing a logged-out user's edit token.
static generateHex( $chars)
Generate a run of cryptographically random data and return it in hexadecimal string format.
MediaWiki exception.
Stores a single person's name and email address.
static newFromUser(User $user)
Create a new MailAddress object for the given user.
Class for creating new log entries and inserting them into the database.
This serves as the entry point to the authentication system.
This is a value object for authentication requests.
This is a value object to hold authentication response data.
appliesToRight( $right)
Determine whether the block prevents a given right.
A DatabaseBlock (unlike a SystemBlock) is stored in the database, may give rise to autoblocks and may...
System blocks are temporary blocks that are created on enforcement (e.g.
PSR-3 logger instance factory.
MediaWikiServices is the service locator for the application scope of MediaWiki.
This serves as the entry point to the MediaWiki session handling system.
Value object representing a CSRF token.
Definition Token.php:32
static newFromIDs( $ids)
Definition UserArray.php:42
static singleton()
Definition UserCache.php:34
Handles increment the edit count for a given set of users.
Represents a "user group membership" – a specific instance of a user belonging to a group.
static send( $to, $from, $subject, $body, $options=[])
This function will perform a direct (authenticated) login to a SMTP Server to use for mail relaying i...
Check if a user's password complies with any password policies that apply to that user,...
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
getEditTimestamp( $first)
Get the timestamp of the first or latest edit.
Definition User.php:4817
loadFromSession()
Load user data from the session.
Definition User.php:1296
addWatch( $title, $checkRights=self::CHECK_USER_RIGHTS)
Watch an article.
Definition User.php:3768
setEditCountInternal( $count)
This method should not be called outside User/UserEditCountUpdate.
Definition User.php:5081
string $mEmailToken
Definition User.php:138
string $mToken
Definition User.php:134
string $mTouched
TS_MW timestamp from the DB.
Definition User.php:130
logout()
Log this user out.
Definition User.php:3979
getOptions( $flags=0)
Get all user's options.
Definition User.php:3050
getRequest()
Get the WebRequest object to use with this object.
Definition User.php:3737
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2364
addToDatabase()
Add this existing user object to the database.
Definition User.php:4212
requiresHTTPS()
Determine based on the wiki configuration and the user's options, whether this user must be over HTTP...
Definition User.php:3365
updateActorId(IDatabase $dbw)
Update the actor ID after an insert.
Definition User.php:4282
getExperienceLevel()
Compute experienced level based on edit count and registration date.
Definition User.php:3905
static isEveryoneAllowed( $right)
Check if all users may be assumed to have the given permission.
Definition User.php:4904
static string null $defOptLang
Is the user an IP range?
Definition User.php:1672
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:518
resetOptions( $resetKinds=[ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused'], IContextSource $context=null)
Reset certain (or all) options to the site defaults.
Definition User.php:3300
static whoIsReal( $id)
Get the real name of a user given their user ID.
Definition User.php:861
invalidateCache()
Immediately touch the user data cache for this account.
Definition User.php:2667
getEmailAuthenticationTimestamp()
Get the timestamp of the user's e-mail authentication.
Definition User.php:2915
array $mOptionOverrides
Definition User.php:148
isBlockedFromEmailuser()
Get whether the user is blocked from using Special:Emailuser.
Definition User.php:4352
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new user object.
Definition User.php:5358
static isCreatableName( $name)
Usernames which fail to pass this function will be blocked from new account registrations,...
Definition User.php:1082
getFirstEditTimestamp()
Get the timestamp of the first edit.
Definition User.php:4795
getOptionKinds(IContextSource $context, $options=null)
Return an associative array mapping preferences keys to the kind of a preference they're used for.
Definition User.php:3215
static purge( $dbDomain, $userId)
Definition User.php:421
int null $mActorId
Definition User.php:123
static changeableByGroup( $group)
Returns an array of the groups that a particular group can add/remove.
Definition User.php:4953
const VERSION
Version number to tag cached versions of serialized User objects.
Definition User.php:67
getEditTokenObject( $salt='', $request=null)
Initialize (if necessary) and return a session token value which can be used in edit forms to show th...
Definition User.php:4462
static getAllGroups()
Return the set of defined explicit groups.
Definition User.php:4914
bool $mAllowUsertalk
Definition User.php:210
string $mEmailTokenExpires
Definition User.php:140
isAllowUsertalk()
Checks if usertalk is allowed.
Definition User.php:5446
string $mFrom
Initialization data source if mLoadedItems!==true.
Definition User.php:173
setCookies( $request=null, $secure=null, $rememberMe=false)
Persist this user's session (e.g.
Definition User.php:3943
getEditToken( $salt='', $request=null)
Initialize (if necessary) and return a session token value which can be used in edit forms to show th...
Definition User.php:4486
isDnsBlacklisted( $ip, $checkWhitelist=false)
Whether the given IP is in a DNS blacklist.
Definition User.php:1818
int $queryFlagsUsed
User::READ_* constant bitfield used to load data.
Definition User.php:216
equals(UserIdentity $user)
Checks if two user objects point to the same user.
Definition User.php:5436
getTokenUrl( $page, $token)
Internal function to format the e-mail validation/invalidation URLs.
Definition User.php:4654
addGroup( $group, $expiry=null)
Add the user to the given group.
Definition User.php:3553
isRegistered()
Alias of isLoggedIn() with a name that describes its actual functionality.
Definition User.php:3622
array $mEffectiveGroups
Definition User.php:189
isBlockedGlobally( $ip='')
Check if user is blocked on all wikis.
Definition User.php:2256
string $mQuickTouched
TS_MW timestamp from cache.
Definition User.php:132
const INVALID_TOKEN
An invalid string value for the user_token field.
Definition User.php:61
isSafeToLoad()
Test if it's safe to load this User object.
Definition User.php:289
static groupHasPermission( $group, $role)
Check, if the given group has the given permission.
Definition User.php:4881
isEmailConfirmed()
Is this user's e-mail address valid-looking and confirmed within limits of the current site configura...
Definition User.php:4740
spreadAnyEditBlock()
If this user is logged-in and blocked, block any IP address they've successfully logged in from.
Definition User.php:4296
useFilePatrol()
Check whether to enable new files patrol features for this user.
Definition User.php:3724
loadFromId( $flags=self::READ_NORMAL)
Load user table data, given mId has already been set.
Definition User.php:391
isAllowed( $action='')
Internal mechanics of testing a permission.
Definition User.php:3694
getDBTouched()
Get the user_touched timestamp field (time of last DB updates)
Definition User.php:2733
setName( $str)
Set the user name.
Definition User.php:2392
changeAuthenticationData(array $data)
Changes credentials of the user.
Definition User.php:2819
getId()
Get the user's ID.
Definition User.php:2335
getRealName()
Get the user's real name.
Definition User.php:2995
loadOptions( $data=null)
Load the user options either from cache, the database or an array.
Definition User.php:5189
getBoolOption( $oname)
Get the user's current setting for a given option, as a boolean value.
Definition User.php:3081
getBlockedStatus( $fromReplica=true)
Get blocking information.
Definition User.php:1748
getRegistration()
Get the timestamp of account creation.
Definition User.php:4781
static isLocallyBlockedProxy( $ip)
Check if an IP address is in the local proxy list.
Definition User.php:1879
getGlobalBlock( $ip='')
Check if user is blocked on all wikis.
Definition User.php:2270
string $mRealName
Definition User.php:125
isAllowedAny(... $permissions)
Check if user is allowed to access a feature / make an action.
Definition User.php:3666
isItemLoaded( $item, $all='all')
Return whether an item has been loaded.
Definition User.php:1275
getMutableCacheKeys(WANObjectCache $cache)
Definition User.php:443
getTokenFromOption( $oname)
Get a token stored in the preferences (like the watchlist one), resetting it if it's empty (and savin...
Definition User.php:3130
isNewbie()
Determine whether the user is a newbie.
Definition User.php:4400
clearNotification(&$title, $oldid=0)
Clear the user's notification timestamp for the given title.
Definition User.php:3802
static resetIdByNameCache()
Reset the cache used in idFromName().
Definition User.php:913
deleteNewtalk( $field, $id)
Clear the new messages flag for the given user.
Definition User.php:2569
loadFromDatabase( $flags=self::READ_LATEST)
Load user and user_group data from the database.
Definition User.php:1336
invalidationTokenUrl( $token)
Return a URL the user can use to invalidate their email address.
Definition User.php:4636
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition User.php:3712
getIntOption( $oname, $defaultOverride=0)
Get the user's current setting for a given option, as an integer value.
Definition User.php:3093
clearInstanceCache( $reloadFrom=false)
Clear various cached data stored in this object.
Definition User.php:1642
canReceiveEmail()
Is this user allowed to receive e-mails within limits of current site configuration?
Definition User.php:4726
string $mEmail
Definition User.php:128
trackBlockWithCookie()
Set the 'BlockID' cookie depending on block type and user authentication status.
Definition User.php:1325
getOption( $oname, $defaultOverride=null, $ignoreHidden=false)
Get the user's current setting for a given option.
Definition User.php:3022
touch()
Update the "touched" timestamp for the user.
Definition User.php:2684
checkTemporaryPassword( $plaintext)
Check if the given clear-text password matches the temporary password sent by e-mail for password res...
Definition User.php:4445
isPingLimitable()
Is this user subject to rate limiting?
Definition User.php:1927
static string[] $mCacheVars
Array of Strings List of member variables which are saved to the shared cache (memcached).
Definition User.php:92
static getCanonicalName( $name, $validate='valid')
Given unvalidated user input, return a canonical username, or false if the username is invalid.
Definition User.php:1180
static newFromRow( $row, $data=null)
Create a new user object from a user row.
Definition User.php:699
getToken( $forceCreation=true)
Get the user's current token.
Definition User.php:2846
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition User.php:542
array bool $mLoadedItems
Array with already loaded items or true if all items have been loaded.
Definition User.php:160
setInternalPassword( $str)
Set the password and reset the random token unconditionally.
Definition User.php:2768
confirmEmail()
Mark the e-mail address confirmed.
Definition User.php:4667
static getGroupsWithPermission( $role)
Get all the groups who have a given permission.
Definition User.php:4862
setId( $v)
Set the user and reload all fields according to a given ID.
Definition User.php:2355
static getGroupPermissions( $groups)
Get the permissions associated with a given list of groups.
Definition User.php:4849
static getDefaultOptions()
Combine the language default options with any site-specific options and add the default language vari...
Definition User.php:1692
static findUsersByGroup( $groups, $limit=5000, $after=null)
Return the users who are members of the given group(s).
Definition User.php:1042
bool $mLocked
Definition User.php:197
getEffectiveGroups( $recache=false)
Get the list of implicit group memberships this user has.
Definition User.php:3442
static selectFields()
Return the list of user fields that should be selected to create a new user object.
Definition User.php:5332
isHidden()
Check if user account is hidden.
Definition User.php:2318
static array null $defOpt
Is the user an IP range?
Definition User.php:1670
static newFromConfirmationCode( $code, $flags=0)
Factory method to fetch whichever user has a given email confirmation code.
Definition User.php:653
setEmailWithConfirmation( $str)
Set the user's e-mail address and a confirmation mail if needed.
Definition User.php:2942
static string[] false $reservedUsernames
Cache for self::isUsableName()
Definition User.php:114
newTouchedTimestamp()
Generate a current or new-future timestamp to be stored in the user_touched field when we update thin...
Definition User.php:2622
inDnsBlacklist( $ip, $bases)
Whether the given IP is in a given DNS blacklist.
Definition User.php:1831
loadFromUserObject( $user)
Load the data for this user object from another user object.
Definition User.php:1501
static isUsableName( $name)
Usernames which fail to pass this function will be blocked from user login and new account registrati...
Definition User.php:1008
static getDefaultOption( $opt)
Get a given default option value.
Definition User.php:1734
const IGNORE_USER_RIGHTS
Definition User.php:83
getDatePreference()
Get the user's preferred date format.
Definition User.php:3345
string $mHash
Definition User.php:185
checkPasswordValidity( $password)
Check if this is a valid password for this user.
Definition User.php:1138
static resetGetDefaultOptionsForTestsOnly()
Reset the process cache of default user options.
Definition User.php:1680
idForName( $flags=0)
If only this user's username is known, and it exists, return the user ID.
Definition User.php:4101
getGroupMemberships()
Get the list of explicit group memberships this user has, stored as UserGroupMembership objects.
Definition User.php:3429
matchEditTokenNoSuffix( $val, $salt='', $request=null, $maxage=null)
Check given value against the token value stored in the session, ignoring the suffix.
Definition User.php:4516
isWatched( $title, $checkRights=self::CHECK_USER_RIGHTS)
Check the watched status of an article.
Definition User.php:3754
loadFromRow( $row, $data=null)
Initialize this object from a row from the user table.
Definition User.php:1389
setPassword( $str)
Set the password and reset the random token.
Definition User.php:2755
string $mBlockedby
Definition User.php:183
static newFromAnyId( $userId, $userName, $actorId, $dbDomain=false)
Static factory method for creation from an ID, name, and/or actor ID.
Definition User.php:599
confirmationTokenUrl( $token)
Return a URL the user can use to confirm their email address.
Definition User.php:4627
isAllowedToCreateAccount()
Get whether the user is allowed to create an account.
Definition User.php:4372
static newFromSession(WebRequest $request=null)
Create a new user object using data from session.
Definition User.php:677
static getAllRights()
Get a list of all available permissions.
Definition User.php:4929
getNewtalk()
Check if the user has new messages.
Definition User.php:2431
getGroups()
Get the list of explicit group memberships this user has.
Definition User.php:3416
addNewUserLogEntry( $action=false, $reason='')
Add a newuser log entry for this user.
Definition User.php:5165
validateCache( $timestamp)
Validate the cache for this account.
Definition User.php:2699
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition User.php:3703
makeUpdateConditions(IDatabase $db, array $conditions)
Builds update conditions.
Definition User.php:1586
loadGroups()
Load the groups from the database if they aren't already loaded.
Definition User.php:1511
static whoIs( $id)
Get the username corresponding to a given user ID.
Definition User.php:851
invalidateEmail()
Invalidate the user's e-mail confirmation, and unauthenticate the e-mail address if it was already co...
Definition User.php:4684
setEmailAuthenticationTimestamp( $timestamp)
Set the e-mail authentication timestamp.
Definition User.php:4698
static newSystemUser( $name, $options=[])
Static factory method for creation of a "system" user from username.
Definition User.php:740
setOption( $oname, $val)
Set the given option for a user.
Definition User.php:3109
bool $mOptionsLoaded
Whether the cache variables have been loaded.
Definition User.php:155
saveOptions()
Saves the non-default options for this user, as previously set e.g.
Definition User.php:5266
static getRightDescription( $right)
Get the description of a given right.
Definition User.php:5126
isAllowedAll(... $permissions)
Definition User.php:3678
getEditCount()
Get the user's edit count.
Definition User.php:3518
getActorId(IDatabase $dbw=null)
Get the user's actor ID.
Definition User.php:2403
const CHECK_USER_RIGHTS
Definition User.php:78
UserGroupMembership[] $mGroupMemberships
Associative array of (group name => UserGroupMembership object)
Definition User.php:146
addAutopromoteOnceGroups( $event)
Add the user to the group if he/she meets given criteria.
Definition User.php:1535
clearSharedCache( $mode='refresh')
Clear user data from memcached.
Definition User.php:2641
isValidPassword( $password)
Is the input a valid password for this user?
Definition User.php:1112
array $mFormerGroups
Definition User.php:193
isBlockedFromCreateAccount()
Get whether the user is explicitly blocked from account creation.
Definition User.php:4328
spreadBlock()
If this (non-anonymous) user is blocked, block the IP address they've successfully logged in from.
Definition User.php:4309
getFormerGroups()
Returns the groups the user has belonged to.
Definition User.php:3494
setRealName( $str)
Set the user's real name.
Definition User.php:3007
getLatestEditTimestamp()
Get the timestamp of the latest edit.
Definition User.php:4806
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition User.php:574
AbstractBlock $mGlobalBlock
Definition User.php:195
int $mId
Cache variables.
Definition User.php:119
getTitleKey()
Get the user's name escaped by underscores.
Definition User.php:2423
static isIP( $name)
Does the string match an anonymous IP address?
Definition User.php:933
getTouched()
Get the user touched timestamp.
Definition User.php:2711
sendMail( $subject, $body, $from=null, $replyto=null)
Send an e-mail to this user's account.
Definition User.php:4583
isLocked()
Check if user account is locked.
Definition User.php:2303
array $mOptions
Definition User.php:201
checkAndSetTouched()
Bump user_touched if it didn't change since this object was loaded.
Definition User.php:1604
removeWatch( $title, $checkRights=self::CHECK_USER_RIGHTS)
Stop watching an article.
Definition User.php:3785
setPasswordInternal( $str)
Actually set the password and such.
Definition User.php:2781
isEmailConfirmationPending()
Check whether there is an outstanding request for e-mail confirmation.
Definition User.php:4766
checkNewtalk( $field, $id)
Internal uncached check for new messages.
Definition User.php:2529
__toString()
Definition User.php:239
getUserPage()
Get this user's personal page title.
Definition User.php:4381
isIPRange()
Is the user an IP range?
Definition User.php:944
confirmationToken(&$expiration)
Generate, store, and return a new e-mail confirmation code.
Definition User.php:4609
canSendEmail()
Is this user allowed to send e-mails within limits of current site configuration?
Definition User.php:4709
getBlockId()
If user is blocked, return the ID for the block.
Definition User.php:2243
isBot()
Definition User.php:3646
string $mRegistration
Definition User.php:142
__construct()
Lightweight constructor for an anonymous user.
Definition User.php:232
setNewtalk( $val, $curRev=null)
Update the 'You have new messages!' status.
Definition User.php:2589
getStubThreshold()
Get the user preferred stub threshold.
Definition User.php:3387
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition User.php:1954
string $mDatePreference
Definition User.php:181
static isValidUserName( $name)
Is the input a valid username?
Definition User.php:959
sendConfirmationMail( $type='created')
Generate a new e-mail confirmation token and send a confirmation/invalidation mail to the user's give...
Definition User.php:4528
getTalkPage()
Get this user's talk page title.
Definition User.php:4390
isLoggedIn()
Get whether the user is logged in.
Definition User.php:3630
static idFromName( $name, $flags=self::READ_NORMAL)
Get database id given a user name.
Definition User.php:871
loadFromCache()
Load user data from shared cache, given mId has already been set.
Definition User.php:455
getCacheKey(WANObjectCache $cache)
Definition User.php:432
AbstractBlock $mBlock
Definition User.php:207
saveSettings()
Save this user's settings into the database.
Definition User.php:4027
getNewMessageRevisionId()
Get the revision ID for the last talk page revision viewed by the talk page owner.
Definition User.php:2502
static getGrantName( $grant)
Get the name of a given grant.
Definition User.php:5139
getEmail()
Get the user's e-mail address.
Definition User.php:2905
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition User.php:5390
static int[] $idCacheByName
Definition User.php:219
getAutomaticGroups( $recache=false)
Get the list of implicit group memberships this user has.
Definition User.php:3465
isBlockedFromUpload()
Get whether the user is blocked from using Special:Upload.
Definition User.php:4363
load( $flags=self::READ_NORMAL)
Load the user table data for this object from the source given by mFrom.
Definition User.php:306
blockedBy()
If user is blocked, return the name of the user who placed the block.
Definition User.php:2225
getRights()
Get the permissions this user has.
Definition User.php:3406
& __get( $name)
Definition User.php:243
matchEditToken( $val, $salt='', $request=null, $maxage=null)
Check given value against the token value stored in the session.
Definition User.php:4502
string $mEmailAuthenticated
Definition User.php:136
loadDefaults( $name=false, $actorId=null)
Set cached properties to default.
Definition User.php:1236
const GETOPTIONS_EXCLUDE_DEFAULTS
Exclude user options that are set to their default value.
Definition User.php:73
const TOKEN_LENGTH
Number of characters required for the user_token field.
Definition User.php:56
setToken( $token=false)
Set the random token (used for persistent authentication) Called from loadDefaults() among other plac...
Definition User.php:2889
updateNewtalk( $field, $id, $curRev=null)
Add or update the new messages flag.
Definition User.php:2544
static createNew( $name, $params=[])
Add a user to the database, return the user object.
Definition User.php:4136
addNewUserLogEntryAutoCreate()
Add an autocreate newuser log entry for this user Used by things like CentralAuth and perhaps other a...
Definition User.php:5177
doLogout()
Clear the user's session, and reset the instance cache.
Definition User.php:3991
setItemLoaded( $item)
Set that an item has been loaded.
Definition User.php:1285
AbstractBlock bool $mBlockedFromCreateAccount
Definition User.php:213
incEditCount()
Schedule a deferred update to update the user's edit count.
Definition User.php:5065
blockedFor()
If user is blocked, return the specified reason for the block.
Definition User.php:2234
static listOptionKinds()
Return a list of the types of user options currently returned by User::getOptionKinds().
Definition User.php:3192
getBlock( $fromReplica=true)
Get the block affecting the user, or null if the user is not blocked.
Definition User.php:2200
int $mEditCount
Definition User.php:144
resetTokenFromOption( $oname)
Reset a token stored in the preferences (like the watchlist one).
Definition User.php:3158
bool $mHideName
Definition User.php:199
static getImplicitGroups()
Get a list of implicit groups TODO: Should we deprecate this? It's trivial, but we don't want to enco...
Definition User.php:4939
changeableGroups()
Returns an array of groups that this user can add and remove.
Definition User.php:5026
int bool $mNewtalk
Lazy-initialized variables, invalidated with clearInstanceCache.
Definition User.php:179
isBlocked( $fromReplica=true)
Check if user is blocked.
Definition User.php:2189
string $mName
Definition User.php:121
getNewMessageLinks()
Return the data needed to construct links for new talk page message alerts.
Definition User.php:2469
isAnon()
Get whether the user is anonymous.
Definition User.php:3638
setEmail( $str)
Set the user's e-mail address.
Definition User.php:2925
string $mBlockreason
Definition User.php:187
__set( $name, $value)
Definition User.php:260
WebRequest $mRequest
Definition User.php:204
checkPassword( $password)
Check to see if the given clear-text password is one of the accepted passwords.
Definition User.php:4410
getInstanceForUpdate()
Get a new instance of this user that was loaded from the master via a locking read.
Definition User.php:5416
isBlockedFrom( $title, $fromReplica=false)
Check if user is blocked from editing a particular article.
Definition User.php:2216
static newFromActorId( $id)
Static factory method for creation from a given actor ID.
Definition User.php:557
clearAllNotifications()
Resets all of the given user's page-change notification timestamps.
Definition User.php:3876
array $mImplicitGroups
Definition User.php:191
initEditCountInternal(IDatabase $dbr)
Initialize user_editcount from data out of the revision table.
Definition User.php:5092
removeGroup( $group)
Remove the user from the given group.
Definition User.php:3588
Multi-datacenter aware caching interface.
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Base class for the more common types of database errors.
Relational database abstraction object.
Definition Database.php:49
const NS_USER
Definition Defines.php:71
const NS_MAIN
Definition Defines.php:69
const NS_USER_TALK
Definition Defines.php:72
Interface for objects which can provide a MediaWiki context on request.
Interface for database access objects.
Interface for objects representing user identity.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
affectedRows()
Get the number of rows affected by the last write query.
delete( $table, $conds, $fname=__METHOD__)
DELETE query wrapper.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a single field from a single result row.
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
insert( $table, $a, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
insertId()
Get the inserted value of an auto-increment row.
timestampOrNull( $ts=null)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
$context
Definition load.php:45
$cache
Definition mcc.php:33
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:26
return true
Definition router.php:94