MediaWiki REL1_31
User.php
Go to the documentation of this file.
1<?php
30use Wikimedia\IPSet;
31use Wikimedia\ScopedCallback;
35
41define( 'EDIT_TOKEN_SUFFIX', Token::SUFFIX );
42
57 const TOKEN_LENGTH = 32;
58
62 const INVALID_TOKEN = '*** INVALID ***';
63
70
74 const VERSION = 12;
75
81
85 const CHECK_USER_RIGHTS = true;
86
90 const IGNORE_USER_RIGHTS = false;
91
98 protected static $mCacheVars = [
99 // user table
100 'mId',
101 'mName',
102 'mRealName',
103 'mEmail',
104 'mTouched',
105 'mToken',
106 'mEmailAuthenticated',
107 'mEmailToken',
108 'mEmailTokenExpires',
109 'mRegistration',
110 'mEditCount',
111 // user_groups table
112 'mGroupMemberships',
113 // user_properties table
114 'mOptionOverrides',
115 // actor table
116 'mActorId',
117 ];
118
125 protected static $mCoreRights = [
126 'apihighlimits',
127 'applychangetags',
128 'autoconfirmed',
129 'autocreateaccount',
130 'autopatrol',
131 'bigdelete',
132 'block',
133 'blockemail',
134 'bot',
135 'browsearchive',
136 'changetags',
137 'createaccount',
138 'createpage',
139 'createtalk',
140 'delete',
141 'deletechangetags',
142 'deletedhistory',
143 'deletedtext',
144 'deletelogentry',
145 'deleterevision',
146 'edit',
147 'editcontentmodel',
148 'editinterface',
149 'editprotected',
150 'editmyoptions',
151 'editmyprivateinfo',
152 'editmyusercss',
153 'editmyuserjson',
154 'editmyuserjs',
155 'editmywatchlist',
156 'editsemiprotected',
157 'editusercss',
158 'edituserjson',
159 'edituserjs',
160 'hideuser',
161 'import',
162 'importupload',
163 'ipblock-exempt',
164 'managechangetags',
165 'markbotedits',
166 'mergehistory',
167 'minoredit',
168 'move',
169 'movefile',
170 'move-categorypages',
171 'move-rootuserpages',
172 'move-subpages',
173 'nominornewtalk',
174 'noratelimit',
175 'override-export-depth',
176 'pagelang',
177 'patrol',
178 'patrolmarks',
179 'protect',
180 'purge',
181 'read',
182 'reupload',
183 'reupload-own',
184 'reupload-shared',
185 'rollback',
186 'sendemail',
187 'siteadmin',
188 'suppressionlog',
189 'suppressredirect',
190 'suppressrevision',
191 'unblockself',
192 'undelete',
193 'unwatchedpages',
194 'upload',
195 'upload_by_url',
196 'userrights',
197 'userrights-interwiki',
198 'viewmyprivateinfo',
199 'viewmywatchlist',
200 'viewsuppressed',
201 'writeapi',
202 ];
203
207 protected static $mAllRights = false;
208
210 // @{
212 public $mId;
214 public $mName;
216 protected $mActorId;
219
221 public $mEmail;
223 public $mTouched;
225 protected $mQuickTouched;
227 protected $mToken;
231 protected $mEmailToken;
235 protected $mRegistration;
237 protected $mEditCount;
242 // @}
243
247 // @{
249
253 protected $mLoadedItems = [];
254 // @}
255
266 public $mFrom;
267
271 protected $mNewtalk;
277 protected $mHash;
279 public $mRights;
281 protected $mBlockreason;
287 protected $mFormerGroups;
289 protected $mGlobalBlock;
291 protected $mLocked;
295 public $mOptions;
296
298 private $mRequest;
299
301 public $mBlock;
302
305
308
310 protected $queryFlagsUsed = self::READ_NORMAL;
311
312 public static $idCacheByName = [];
313
325 public function __construct() {
326 $this->clearInstanceCache( 'defaults' );
327 }
328
332 public function __toString() {
333 return (string)$this->getName();
334 }
335
350 public function isSafeToLoad() {
352
353 // The user is safe to load if:
354 // * MW_NO_SESSION is undefined AND $wgFullyInitialised is true (safe to use session data)
355 // * mLoadedItems === true (already loaded)
356 // * mFrom !== 'session' (sessions not involved at all)
357
358 return ( !defined( 'MW_NO_SESSION' ) && $wgFullyInitialised ) ||
359 $this->mLoadedItems === true || $this->mFrom !== 'session';
360 }
361
367 public function load( $flags = self::READ_NORMAL ) {
369
370 if ( $this->mLoadedItems === true ) {
371 return;
372 }
373
374 // Set it now to avoid infinite recursion in accessors
375 $oldLoadedItems = $this->mLoadedItems;
376 $this->mLoadedItems = true;
377 $this->queryFlagsUsed = $flags;
378
379 // If this is called too early, things are likely to break.
380 if ( !$wgFullyInitialised && $this->mFrom === 'session' ) {
381 \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
382 ->warning( 'User::loadFromSession called before the end of Setup.php', [
383 'exception' => new Exception( 'User::loadFromSession called before the end of Setup.php' ),
384 ] );
385 $this->loadDefaults();
386 $this->mLoadedItems = $oldLoadedItems;
387 return;
388 }
389
390 switch ( $this->mFrom ) {
391 case 'defaults':
392 $this->loadDefaults();
393 break;
394 case 'name':
395 // Make sure this thread sees its own changes
396 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
397 if ( $lb->hasOrMadeRecentMasterChanges() ) {
398 $flags |= self::READ_LATEST;
399 $this->queryFlagsUsed = $flags;
400 }
401
402 $this->mId = self::idFromName( $this->mName, $flags );
403 if ( !$this->mId ) {
404 // Nonexistent user placeholder object
405 $this->loadDefaults( $this->mName );
406 } else {
407 $this->loadFromId( $flags );
408 }
409 break;
410 case 'id':
411 // Make sure this thread sees its own changes, if the ID isn't 0
412 if ( $this->mId != 0 ) {
413 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
414 if ( $lb->hasOrMadeRecentMasterChanges() ) {
415 $flags |= self::READ_LATEST;
416 $this->queryFlagsUsed = $flags;
417 }
418 }
419
420 $this->loadFromId( $flags );
421 break;
422 case 'actor':
423 // Make sure this thread sees its own changes
424 if ( wfGetLB()->hasOrMadeRecentMasterChanges() ) {
425 $flags |= self::READ_LATEST;
426 $this->queryFlagsUsed = $flags;
427 }
428
429 list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
430 $row = wfGetDB( $index )->selectRow(
431 'actor',
432 [ 'actor_user', 'actor_name' ],
433 [ 'actor_id' => $this->mActorId ],
434 __METHOD__,
436 );
437
438 if ( !$row ) {
439 // Ugh.
440 $this->loadDefaults();
441 } elseif ( $row->actor_user ) {
442 $this->mId = $row->actor_user;
443 $this->loadFromId( $flags );
444 } else {
445 $this->loadDefaults( $row->actor_name );
446 }
447 break;
448 case 'session':
449 if ( !$this->loadFromSession() ) {
450 // Loading from session failed. Load defaults.
451 $this->loadDefaults();
452 }
453 Hooks::run( 'UserLoadAfterLoadFromSession', [ $this ] );
454 break;
455 default:
456 throw new UnexpectedValueException(
457 "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
458 }
459 }
460
466 public function loadFromId( $flags = self::READ_NORMAL ) {
467 if ( $this->mId == 0 ) {
468 // Anonymous users are not in the database (don't need cache)
469 $this->loadDefaults();
470 return false;
471 }
472
473 // Try cache (unless this needs data from the master DB).
474 // NOTE: if this thread called saveSettings(), the cache was cleared.
475 $latest = DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST );
476 if ( $latest ) {
477 if ( !$this->loadFromDatabase( $flags ) ) {
478 // Can't load from ID
479 return false;
480 }
481 } else {
482 $this->loadFromCache();
483 }
484
485 $this->mLoadedItems = true;
486 $this->queryFlagsUsed = $flags;
487
488 return true;
489 }
490
496 public static function purge( $wikiId, $userId ) {
497 $cache = ObjectCache::getMainWANInstance();
498 $key = $cache->makeGlobalKey( 'user', 'id', $wikiId, $userId );
499 $cache->delete( $key );
500 }
501
507 protected function getCacheKey( WANObjectCache $cache ) {
508 return $cache->makeGlobalKey( 'user', 'id', wfWikiID(), $this->mId );
509 }
510
517 $id = $this->getId();
518
519 return $id ? [ $this->getCacheKey( $cache ) ] : [];
520 }
521
528 protected function loadFromCache() {
529 $cache = ObjectCache::getMainWANInstance();
530 $data = $cache->getWithSetCallback(
531 $this->getCacheKey( $cache ),
532 $cache::TTL_HOUR,
533 function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache ) {
534 $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
535 wfDebug( "User: cache miss for user {$this->mId}\n" );
536
537 $this->loadFromDatabase( self::READ_NORMAL );
538 $this->loadGroups();
539 $this->loadOptions();
540
541 $data = [];
542 foreach ( self::$mCacheVars as $name ) {
543 $data[$name] = $this->$name;
544 }
545
546 $ttl = $cache->adaptiveTTL( wfTimestamp( TS_UNIX, $this->mTouched ), $ttl );
547
548 // if a user group membership is about to expire, the cache needs to
549 // expire at that time (T163691)
550 foreach ( $this->mGroupMemberships as $ugm ) {
551 if ( $ugm->getExpiry() ) {
552 $secondsUntilExpiry = wfTimestamp( TS_UNIX, $ugm->getExpiry() ) - time();
553 if ( $secondsUntilExpiry > 0 && $secondsUntilExpiry < $ttl ) {
554 $ttl = $secondsUntilExpiry;
555 }
556 }
557 }
558
559 return $data;
560 },
561 [ 'pcTTL' => $cache::TTL_PROC_LONG, 'version' => self::VERSION ]
562 );
563
564 // Restore from cache
565 foreach ( self::$mCacheVars as $name ) {
566 $this->$name = $data[$name];
567 }
568
569 return true;
570 }
571
573 // @{
574
591 public static function newFromName( $name, $validate = 'valid' ) {
592 if ( $validate === true ) {
593 $validate = 'valid';
594 }
595 $name = self::getCanonicalName( $name, $validate );
596 if ( $name === false ) {
597 return false;
598 } else {
599 // Create unloaded user object
600 $u = new User;
601 $u->mName = $name;
602 $u->mFrom = 'name';
603 $u->setItemLoaded( 'name' );
604 return $u;
605 }
606 }
607
614 public static function newFromId( $id ) {
615 $u = new User;
616 $u->mId = $id;
617 $u->mFrom = 'id';
618 $u->setItemLoaded( 'id' );
619 return $u;
620 }
621
629 public static function newFromActorId( $id ) {
631
633 throw new BadMethodCallException(
634 'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage is MIGRATION_OLD'
635 );
636 }
637
638 $u = new User;
639 $u->mActorId = $id;
640 $u->mFrom = 'actor';
641 $u->setItemLoaded( 'actor' );
642 return $u;
643 }
644
657 public static function newFromAnyId( $userId, $userName, $actorId ) {
659
660 $user = new User;
661 $user->mFrom = 'defaults';
662
663 if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD && $actorId !== null ) {
664 $user->mActorId = (int)$actorId;
665 if ( $user->mActorId !== 0 ) {
666 $user->mFrom = 'actor';
667 }
668 $user->setItemLoaded( 'actor' );
669 }
670
671 if ( $userName !== null && $userName !== '' ) {
672 $user->mName = $userName;
673 $user->mFrom = 'name';
674 $user->setItemLoaded( 'name' );
675 }
676
677 if ( $userId !== null ) {
678 $user->mId = (int)$userId;
679 if ( $user->mId !== 0 ) {
680 $user->mFrom = 'id';
681 }
682 $user->setItemLoaded( 'id' );
683 }
684
685 if ( $user->mFrom === 'defaults' ) {
686 throw new InvalidArgumentException(
687 'Cannot create a user with no name, no ID, and no actor ID'
688 );
689 }
690
691 return $user;
692 }
693
705 public static function newFromConfirmationCode( $code, $flags = 0 ) {
706 $db = ( $flags & self::READ_LATEST ) == self::READ_LATEST
707 ? wfGetDB( DB_MASTER )
708 : wfGetDB( DB_REPLICA );
709
710 $id = $db->selectField(
711 'user',
712 'user_id',
713 [
714 'user_email_token' => md5( $code ),
715 'user_email_token_expires > ' . $db->addQuotes( $db->timestamp() ),
716 ]
717 );
718
719 return $id ? self::newFromId( $id ) : null;
720 }
721
729 public static function newFromSession( WebRequest $request = null ) {
730 $user = new User;
731 $user->mFrom = 'session';
732 $user->mRequest = $request;
733 return $user;
734 }
735
750 public static function newFromRow( $row, $data = null ) {
751 $user = new User;
752 $user->loadFromRow( $row, $data );
753 return $user;
754 }
755
791 public static function newSystemUser( $name, $options = [] ) {
792 $options += [
793 'validate' => 'valid',
794 'create' => true,
795 'steal' => false,
796 ];
797
798 $name = self::getCanonicalName( $name, $options['validate'] );
799 if ( $name === false ) {
800 return null;
801 }
802
804 $userQuery = self::getQueryInfo();
805 $row = $dbr->selectRow(
806 $userQuery['tables'],
807 $userQuery['fields'],
808 [ 'user_name' => $name ],
809 __METHOD__,
810 [],
811 $userQuery['joins']
812 );
813 if ( !$row ) {
814 // Try the master database...
815 $dbw = wfGetDB( DB_MASTER );
816 $row = $dbw->selectRow(
817 $userQuery['tables'],
818 $userQuery['fields'],
819 [ 'user_name' => $name ],
820 __METHOD__,
821 [],
822 $userQuery['joins']
823 );
824 }
825
826 if ( !$row ) {
827 // No user. Create it?
828 return $options['create']
829 ? self::createNew( $name, [ 'token' => self::INVALID_TOKEN ] )
830 : null;
831 }
832
833 $user = self::newFromRow( $row );
834
835 // A user is considered to exist as a non-system user if it can
836 // authenticate, or has an email set, or has a non-invalid token.
837 if ( $user->mEmail || $user->mToken !== self::INVALID_TOKEN ||
838 AuthManager::singleton()->userCanAuthenticate( $name )
839 ) {
840 // User exists. Steal it?
841 if ( !$options['steal'] ) {
842 return null;
843 }
844
845 AuthManager::singleton()->revokeAccessForUser( $name );
846
847 $user->invalidateEmail();
848 $user->mToken = self::INVALID_TOKEN;
849 $user->saveSettings();
850 SessionManager::singleton()->preventSessionsForUser( $user->getName() );
851 }
852
853 return $user;
854 }
855
856 // @}
857
863 public static function whoIs( $id ) {
864 return UserCache::singleton()->getProp( $id, 'name' );
865 }
866
873 public static function whoIsReal( $id ) {
874 return UserCache::singleton()->getProp( $id, 'real_name' );
875 }
876
883 public static function idFromName( $name, $flags = self::READ_NORMAL ) {
884 $nt = Title::makeTitleSafe( NS_USER, $name );
885 if ( is_null( $nt ) ) {
886 // Illegal name
887 return null;
888 }
889
890 if ( !( $flags & self::READ_LATEST ) && array_key_exists( $name, self::$idCacheByName ) ) {
891 return self::$idCacheByName[$name];
892 }
893
894 list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
895 $db = wfGetDB( $index );
896
897 $s = $db->selectRow(
898 'user',
899 [ 'user_id' ],
900 [ 'user_name' => $nt->getText() ],
901 __METHOD__,
903 );
904
905 if ( $s === false ) {
906 $result = null;
907 } else {
908 $result = $s->user_id;
909 }
910
911 self::$idCacheByName[$name] = $result;
912
913 if ( count( self::$idCacheByName ) > 1000 ) {
914 self::$idCacheByName = [];
915 }
916
917 return $result;
918 }
919
923 public static function resetIdByNameCache() {
924 self::$idCacheByName = [];
925 }
926
943 public static function isIP( $name ) {
944 return preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/', $name )
945 || IP::isIPv6( $name );
946 }
947
954 public function isIPRange() {
955 return IP::isValidRange( $this->mName );
956 }
957
969 public static function isValidUserName( $name ) {
971
972 if ( $name == ''
973 || self::isIP( $name )
974 || strpos( $name, '/' ) !== false
975 || strlen( $name ) > $wgMaxNameChars
976 || $name != $wgContLang->ucfirst( $name )
977 ) {
978 return false;
979 }
980
981 // Ensure that the name can't be misresolved as a different title,
982 // such as with extra namespace keys at the start.
983 $parsed = Title::newFromText( $name );
984 if ( is_null( $parsed )
985 || $parsed->getNamespace()
986 || strcmp( $name, $parsed->getPrefixedText() ) ) {
987 return false;
988 }
989
990 // Check an additional blacklist of troublemaker characters.
991 // Should these be merged into the title char list?
992 $unicodeBlacklist = '/[' .
993 '\x{0080}-\x{009f}' . # iso-8859-1 control chars
994 '\x{00a0}' . # non-breaking space
995 '\x{2000}-\x{200f}' . # various whitespace
996 '\x{2028}-\x{202f}' . # breaks and control chars
997 '\x{3000}' . # ideographic space
998 '\x{e000}-\x{f8ff}' . # private use
999 ']/u';
1000 if ( preg_match( $unicodeBlacklist, $name ) ) {
1001 return false;
1002 }
1003
1004 return true;
1005 }
1006
1018 public static function isUsableName( $name ) {
1020 // Must be a valid username, obviously ;)
1021 if ( !self::isValidUserName( $name ) ) {
1022 return false;
1023 }
1024
1025 static $reservedUsernames = false;
1026 if ( !$reservedUsernames ) {
1027 $reservedUsernames = $wgReservedUsernames;
1028 Hooks::run( 'UserGetReservedNames', [ &$reservedUsernames ] );
1029 }
1030
1031 // Certain names may be reserved for batch processes.
1032 foreach ( $reservedUsernames as $reserved ) {
1033 if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
1034 $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->text();
1035 }
1036 if ( $reserved == $name ) {
1037 return false;
1038 }
1039 }
1040 return true;
1041 }
1042
1053 public static function findUsersByGroup( $groups, $limit = 5000, $after = null ) {
1054 if ( $groups === [] ) {
1056 }
1057
1058 $groups = array_unique( (array)$groups );
1059 $limit = min( 5000, $limit );
1060
1061 $conds = [ 'ug_group' => $groups ];
1062 if ( $after !== null ) {
1063 $conds[] = 'ug_user > ' . (int)$after;
1064 }
1065
1066 $dbr = wfGetDB( DB_REPLICA );
1067 $ids = $dbr->selectFieldValues(
1068 'user_groups',
1069 'ug_user',
1070 $conds,
1071 __METHOD__,
1072 [
1073 'DISTINCT' => true,
1074 'ORDER BY' => 'ug_user',
1075 'LIMIT' => $limit,
1076 ]
1077 ) ?: [];
1078 return UserArray::newFromIDs( $ids );
1079 }
1080
1093 public static function isCreatableName( $name ) {
1095
1096 // Ensure that the username isn't longer than 235 bytes, so that
1097 // (at least for the builtin skins) user javascript and css files
1098 // will work. (T25080)
1099 if ( strlen( $name ) > 235 ) {
1100 wfDebugLog( 'username', __METHOD__ .
1101 ": '$name' invalid due to length" );
1102 return false;
1103 }
1104
1105 // Preg yells if you try to give it an empty string
1106 if ( $wgInvalidUsernameCharacters !== '' ) {
1107 if ( preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name ) ) {
1108 wfDebugLog( 'username', __METHOD__ .
1109 ": '$name' invalid due to wgInvalidUsernameCharacters" );
1110 return false;
1111 }
1112 }
1113
1114 return self::isUsableName( $name );
1115 }
1116
1123 public function isValidPassword( $password ) {
1124 // simple boolean wrapper for getPasswordValidity
1125 return $this->getPasswordValidity( $password ) === true;
1126 }
1127
1134 public function getPasswordValidity( $password ) {
1135 $result = $this->checkPasswordValidity( $password );
1136 if ( $result->isGood() ) {
1137 return true;
1138 } else {
1139 $messages = [];
1140 foreach ( $result->getErrorsByType( 'error' ) as $error ) {
1141 $messages[] = $error['message'];
1142 }
1143 foreach ( $result->getErrorsByType( 'warning' ) as $warning ) {
1144 $messages[] = $warning['message'];
1145 }
1146 if ( count( $messages ) === 1 ) {
1147 return $messages[0];
1148 }
1149 return $messages;
1150 }
1151 }
1152
1170 public function checkPasswordValidity( $password ) {
1172
1173 $upp = new UserPasswordPolicy(
1174 $wgPasswordPolicy['policies'],
1175 $wgPasswordPolicy['checks']
1176 );
1177
1178 $status = Status::newGood();
1179 $result = false; // init $result to false for the internal checks
1180
1181 if ( !Hooks::run( 'isValidPassword', [ $password, &$result, $this ] ) ) {
1182 $status->error( $result );
1183 return $status;
1184 }
1185
1186 if ( $result === false ) {
1187 $status->merge( $upp->checkUserPassword( $this, $password ) );
1188 return $status;
1189 } elseif ( $result === true ) {
1190 return $status;
1191 } else {
1192 $status->error( $result );
1193 return $status; // the isValidPassword hook set a string $result and returned true
1194 }
1195 }
1196
1210 public static function getCanonicalName( $name, $validate = 'valid' ) {
1211 // Force usernames to capital
1213 $name = $wgContLang->ucfirst( $name );
1214
1215 # Reject names containing '#'; these will be cleaned up
1216 # with title normalisation, but then it's too late to
1217 # check elsewhere
1218 if ( strpos( $name, '#' ) !== false ) {
1219 return false;
1220 }
1221
1222 // Clean up name according to title rules,
1223 // but only when validation is requested (T14654)
1224 $t = ( $validate !== false ) ?
1225 Title::newFromText( $name, NS_USER ) : Title::makeTitle( NS_USER, $name );
1226 // Check for invalid titles
1227 if ( is_null( $t ) || $t->getNamespace() !== NS_USER || $t->isExternal() ) {
1228 return false;
1229 }
1230
1231 // Reject various classes of invalid names
1232 $name = AuthManager::callLegacyAuthPlugin(
1233 'getCanonicalName', [ $t->getText() ], $t->getText()
1234 );
1235
1236 switch ( $validate ) {
1237 case false:
1238 break;
1239 case 'valid':
1240 if ( !self::isValidUserName( $name ) ) {
1241 $name = false;
1242 }
1243 break;
1244 case 'usable':
1245 if ( !self::isUsableName( $name ) ) {
1246 $name = false;
1247 }
1248 break;
1249 case 'creatable':
1250 if ( !self::isCreatableName( $name ) ) {
1251 $name = false;
1252 }
1253 break;
1254 default:
1255 throw new InvalidArgumentException(
1256 'Invalid parameter value for $validate in ' . __METHOD__ );
1257 }
1258 return $name;
1259 }
1260
1267 public static function randomPassword() {
1269 return PasswordFactory::generateRandomPasswordString( $wgMinimalPasswordLength );
1270 }
1271
1280 public function loadDefaults( $name = false ) {
1281 $this->mId = 0;
1282 $this->mName = $name;
1283 $this->mActorId = null;
1284 $this->mRealName = '';
1285 $this->mEmail = '';
1286 $this->mOptionOverrides = null;
1287 $this->mOptionsLoaded = false;
1288
1289 $loggedOut = $this->mRequest && !defined( 'MW_NO_SESSION' )
1290 ? $this->mRequest->getSession()->getLoggedOutTimestamp() : 0;
1291 if ( $loggedOut !== 0 ) {
1292 $this->mTouched = wfTimestamp( TS_MW, $loggedOut );
1293 } else {
1294 $this->mTouched = '1'; # Allow any pages to be cached
1295 }
1296
1297 $this->mToken = null; // Don't run cryptographic functions till we need a token
1298 $this->mEmailAuthenticated = null;
1299 $this->mEmailToken = '';
1300 $this->mEmailTokenExpires = null;
1301 $this->mRegistration = wfTimestamp( TS_MW );
1302 $this->mGroupMemberships = [];
1303
1304 Hooks::run( 'UserLoadDefaults', [ $this, $name ] );
1305 }
1306
1319 public function isItemLoaded( $item, $all = 'all' ) {
1320 return ( $this->mLoadedItems === true && $all === 'all' ) ||
1321 ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true );
1322 }
1323
1329 protected function setItemLoaded( $item ) {
1330 if ( is_array( $this->mLoadedItems ) ) {
1331 $this->mLoadedItems[$item] = true;
1332 }
1333 }
1334
1340 private function loadFromSession() {
1341 // Deprecated hook
1342 $result = null;
1343 Hooks::run( 'UserLoadFromSession', [ $this, &$result ], '1.27' );
1344 if ( $result !== null ) {
1345 return $result;
1346 }
1347
1348 // MediaWiki\Session\Session already did the necessary authentication of the user
1349 // returned here, so just use it if applicable.
1350 $session = $this->getRequest()->getSession();
1351 $user = $session->getUser();
1352 if ( $user->isLoggedIn() ) {
1353 $this->loadFromUserObject( $user );
1354
1355 // If this user is autoblocked, set a cookie to track the Block. This has to be done on
1356 // every session load, because an autoblocked editor might not edit again from the same
1357 // IP address after being blocked.
1358 $config = RequestContext::getMain()->getConfig();
1359 if ( $config->get( 'CookieSetOnAutoblock' ) === true ) {
1360 $block = $this->getBlock();
1361 $shouldSetCookie = $this->getRequest()->getCookie( 'BlockID' ) === null
1362 && $block
1363 && $block->getType() === Block::TYPE_USER
1364 && $block->isAutoblocking();
1365 if ( $shouldSetCookie ) {
1366 wfDebug( __METHOD__ . ': User is autoblocked, setting cookie to track' );
1367 $block->setCookie( $this->getRequest()->response() );
1368 }
1369 }
1370
1371 // Other code expects these to be set in the session, so set them.
1372 $session->set( 'wsUserID', $this->getId() );
1373 $session->set( 'wsUserName', $this->getName() );
1374 $session->set( 'wsToken', $this->getToken() );
1375 return true;
1376 }
1377 return false;
1378 }
1379
1387 public function loadFromDatabase( $flags = self::READ_LATEST ) {
1388 // Paranoia
1389 $this->mId = intval( $this->mId );
1390
1391 if ( !$this->mId ) {
1392 // Anonymous users are not in the database
1393 $this->loadDefaults();
1394 return false;
1395 }
1396
1397 list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
1398 $db = wfGetDB( $index );
1399
1400 $userQuery = self::getQueryInfo();
1401 $s = $db->selectRow(
1402 $userQuery['tables'],
1403 $userQuery['fields'],
1404 [ 'user_id' => $this->mId ],
1405 __METHOD__,
1406 $options,
1407 $userQuery['joins']
1408 );
1409
1410 $this->queryFlagsUsed = $flags;
1411 Hooks::run( 'UserLoadFromDatabase', [ $this, &$s ] );
1412
1413 if ( $s !== false ) {
1414 // Initialise user table data
1415 $this->loadFromRow( $s );
1416 $this->mGroupMemberships = null; // deferred
1417 $this->getEditCount(); // revalidation for nulls
1418 return true;
1419 } else {
1420 // Invalid user_id
1421 $this->mId = 0;
1422 $this->loadDefaults();
1423 return false;
1424 }
1425 }
1426
1439 protected function loadFromRow( $row, $data = null ) {
1441
1442 if ( !is_object( $row ) ) {
1443 throw new InvalidArgumentException( '$row must be an object' );
1444 }
1445
1446 $all = true;
1447
1448 $this->mGroupMemberships = null; // deferred
1449
1451 if ( isset( $row->actor_id ) ) {
1452 $this->mActorId = (int)$row->actor_id;
1453 if ( $this->mActorId !== 0 ) {
1454 $this->mFrom = 'actor';
1455 }
1456 $this->setItemLoaded( 'actor' );
1457 } else {
1458 $all = false;
1459 }
1460 }
1461
1462 if ( isset( $row->user_name ) && $row->user_name !== '' ) {
1463 $this->mName = $row->user_name;
1464 $this->mFrom = 'name';
1465 $this->setItemLoaded( 'name' );
1466 } else {
1467 $all = false;
1468 }
1469
1470 if ( isset( $row->user_real_name ) ) {
1471 $this->mRealName = $row->user_real_name;
1472 $this->setItemLoaded( 'realname' );
1473 } else {
1474 $all = false;
1475 }
1476
1477 if ( isset( $row->user_id ) ) {
1478 $this->mId = intval( $row->user_id );
1479 if ( $this->mId !== 0 ) {
1480 $this->mFrom = 'id';
1481 }
1482 $this->setItemLoaded( 'id' );
1483 } else {
1484 $all = false;
1485 }
1486
1487 if ( isset( $row->user_id ) && isset( $row->user_name ) && $row->user_name !== '' ) {
1488 self::$idCacheByName[$row->user_name] = $row->user_id;
1489 }
1490
1491 if ( isset( $row->user_editcount ) ) {
1492 $this->mEditCount = $row->user_editcount;
1493 } else {
1494 $all = false;
1495 }
1496
1497 if ( isset( $row->user_touched ) ) {
1498 $this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
1499 } else {
1500 $all = false;
1501 }
1502
1503 if ( isset( $row->user_token ) ) {
1504 // The definition for the column is binary(32), so trim the NULs
1505 // that appends. The previous definition was char(32), so trim
1506 // spaces too.
1507 $this->mToken = rtrim( $row->user_token, " \0" );
1508 if ( $this->mToken === '' ) {
1509 $this->mToken = null;
1510 }
1511 } else {
1512 $all = false;
1513 }
1514
1515 if ( isset( $row->user_email ) ) {
1516 $this->mEmail = $row->user_email;
1517 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
1518 $this->mEmailToken = $row->user_email_token;
1519 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
1520 $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
1521 } else {
1522 $all = false;
1523 }
1524
1525 if ( $all ) {
1526 $this->mLoadedItems = true;
1527 }
1528
1529 if ( is_array( $data ) ) {
1530 if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) {
1531 if ( !count( $data['user_groups'] ) ) {
1532 $this->mGroupMemberships = [];
1533 } else {
1534 $firstGroup = reset( $data['user_groups'] );
1535 if ( is_array( $firstGroup ) || is_object( $firstGroup ) ) {
1536 $this->mGroupMemberships = [];
1537 foreach ( $data['user_groups'] as $row ) {
1538 $ugm = UserGroupMembership::newFromRow( (object)$row );
1539 $this->mGroupMemberships[$ugm->getGroup()] = $ugm;
1540 }
1541 }
1542 }
1543 }
1544 if ( isset( $data['user_properties'] ) && is_array( $data['user_properties'] ) ) {
1545 $this->loadOptions( $data['user_properties'] );
1546 }
1547 }
1548 }
1549
1555 protected function loadFromUserObject( $user ) {
1556 $user->load();
1557 foreach ( self::$mCacheVars as $var ) {
1558 $this->$var = $user->$var;
1559 }
1560 }
1561
1565 private function loadGroups() {
1566 if ( is_null( $this->mGroupMemberships ) ) {
1567 $db = ( $this->queryFlagsUsed & self::READ_LATEST )
1568 ? wfGetDB( DB_MASTER )
1569 : wfGetDB( DB_REPLICA );
1570 $this->mGroupMemberships = UserGroupMembership::getMembershipsForUser(
1571 $this->mId, $db );
1572 }
1573 }
1574
1589 public function addAutopromoteOnceGroups( $event ) {
1591
1592 if ( wfReadOnly() || !$this->getId() ) {
1593 return [];
1594 }
1595
1596 $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
1597 if ( !count( $toPromote ) ) {
1598 return [];
1599 }
1600
1601 if ( !$this->checkAndSetTouched() ) {
1602 return []; // raced out (bug T48834)
1603 }
1604
1605 $oldGroups = $this->getGroups(); // previous groups
1606 $oldUGMs = $this->getGroupMemberships();
1607 foreach ( $toPromote as $group ) {
1608 $this->addGroup( $group );
1609 }
1610 $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
1611 $newUGMs = $this->getGroupMemberships();
1612
1613 // update groups in external authentication database
1614 Hooks::run( 'UserGroupsChanged', [ $this, $toPromote, [], false, false, $oldUGMs, $newUGMs ] );
1615 AuthManager::callLegacyAuthPlugin( 'updateExternalDBGroups', [ $this, $toPromote ] );
1616
1617 $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
1618 $logEntry->setPerformer( $this );
1619 $logEntry->setTarget( $this->getUserPage() );
1620 $logEntry->setParameters( [
1621 '4::oldgroups' => $oldGroups,
1622 '5::newgroups' => $newGroups,
1623 ] );
1624 $logid = $logEntry->insert();
1626 $logEntry->publish( $logid );
1627 }
1628
1629 return $toPromote;
1630 }
1631
1641 protected function makeUpdateConditions( Database $db, array $conditions ) {
1642 if ( $this->mTouched ) {
1643 // CAS check: only update if the row wasn't changed sicne it was loaded.
1644 $conditions['user_touched'] = $db->timestamp( $this->mTouched );
1645 }
1646
1647 return $conditions;
1648 }
1649
1659 protected function checkAndSetTouched() {
1660 $this->load();
1661
1662 if ( !$this->mId ) {
1663 return false; // anon
1664 }
1665
1666 // Get a new user_touched that is higher than the old one
1667 $newTouched = $this->newTouchedTimestamp();
1668
1669 $dbw = wfGetDB( DB_MASTER );
1670 $dbw->update( 'user',
1671 [ 'user_touched' => $dbw->timestamp( $newTouched ) ],
1672 $this->makeUpdateConditions( $dbw, [
1673 'user_id' => $this->mId,
1674 ] ),
1675 __METHOD__
1676 );
1677 $success = ( $dbw->affectedRows() > 0 );
1678
1679 if ( $success ) {
1680 $this->mTouched = $newTouched;
1681 $this->clearSharedCache();
1682 } else {
1683 // Clears on failure too since that is desired if the cache is stale
1684 $this->clearSharedCache( 'refresh' );
1685 }
1686
1687 return $success;
1688 }
1689
1697 public function clearInstanceCache( $reloadFrom = false ) {
1698 $this->mNewtalk = -1;
1699 $this->mDatePreference = null;
1700 $this->mBlockedby = -1; # Unset
1701 $this->mHash = false;
1702 $this->mRights = null;
1703 $this->mEffectiveGroups = null;
1704 $this->mImplicitGroups = null;
1705 $this->mGroupMemberships = null;
1706 $this->mOptions = null;
1707 $this->mOptionsLoaded = false;
1708 $this->mEditCount = null;
1709
1710 if ( $reloadFrom ) {
1711 $this->mLoadedItems = [];
1712 $this->mFrom = $reloadFrom;
1713 }
1714 }
1715
1722 public static function getDefaultOptions() {
1724
1725 static $defOpt = null;
1726 static $defOptLang = null;
1727
1728 if ( $defOpt !== null && $defOptLang === $wgContLang->getCode() ) {
1729 // $wgContLang does not change (and should not change) mid-request,
1730 // but the unit tests change it anyway, and expect this method to
1731 // return values relevant to the current $wgContLang.
1732 return $defOpt;
1733 }
1734
1735 $defOpt = $wgDefaultUserOptions;
1736 // Default language setting
1737 $defOptLang = $wgContLang->getCode();
1738 $defOpt['language'] = $defOptLang;
1739 foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
1740 $defOpt[$langCode == $wgContLang->getCode() ? 'variant' : "variant-$langCode"] = $langCode;
1741 }
1742
1743 // NOTE: don't use SearchEngineConfig::getSearchableNamespaces here,
1744 // since extensions may change the set of searchable namespaces depending
1745 // on user groups/permissions.
1746 foreach ( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
1747 $defOpt['searchNs' . $nsnum] = (bool)$val;
1748 }
1749 $defOpt['skin'] = Skin::normalizeKey( $wgDefaultSkin );
1750
1751 Hooks::run( 'UserGetDefaultOptions', [ &$defOpt ] );
1752
1753 return $defOpt;
1754 }
1755
1762 public static function getDefaultOption( $opt ) {
1763 $defOpts = self::getDefaultOptions();
1764 if ( isset( $defOpts[$opt] ) ) {
1765 return $defOpts[$opt];
1766 } else {
1767 return null;
1768 }
1769 }
1770
1777 private function getBlockedStatus( $bFromSlave = true ) {
1779
1780 if ( -1 != $this->mBlockedby ) {
1781 return;
1782 }
1783
1784 wfDebug( __METHOD__ . ": checking...\n" );
1785
1786 // Initialize data...
1787 // Otherwise something ends up stomping on $this->mBlockedby when
1788 // things get lazy-loaded later, causing false positive block hits
1789 // due to -1 !== 0. Probably session-related... Nothing should be
1790 // overwriting mBlockedby, surely?
1791 $this->load();
1792
1793 # We only need to worry about passing the IP address to the Block generator if the
1794 # user is not immune to autoblocks/hardblocks, and they are the current user so we
1795 # know which IP address they're actually coming from
1796 $ip = null;
1797 $sessionUser = RequestContext::getMain()->getUser();
1798 // the session user is set up towards the end of Setup.php. Until then,
1799 // assume it's a logged-out user.
1800 $globalUserName = $sessionUser->isSafeToLoad()
1801 ? $sessionUser->getName()
1802 : IP::sanitizeIP( $sessionUser->getRequest()->getIP() );
1803 if ( $this->getName() === $globalUserName && !$this->isAllowed( 'ipblock-exempt' ) ) {
1804 $ip = $this->getRequest()->getIP();
1805 }
1806
1807 // User/IP blocking
1808 $block = Block::newFromTarget( $this, $ip, !$bFromSlave );
1809
1810 // Cookie blocking
1811 if ( !$block instanceof Block ) {
1812 $block = $this->getBlockFromCookieValue( $this->getRequest()->getCookie( 'BlockID' ) );
1813 }
1814
1815 // Proxy blocking
1816 if ( !$block instanceof Block && $ip !== null && !in_array( $ip, $wgProxyWhitelist ) ) {
1817 // Local list
1818 if ( self::isLocallyBlockedProxy( $ip ) ) {
1819 $block = new Block( [
1820 'byText' => wfMessage( 'proxyblocker' )->text(),
1821 'reason' => wfMessage( 'proxyblockreason' )->text(),
1822 'address' => $ip,
1823 'systemBlock' => 'proxy',
1824 ] );
1825 } elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) {
1826 $block = new Block( [
1827 'byText' => wfMessage( 'sorbs' )->text(),
1828 'reason' => wfMessage( 'sorbsreason' )->text(),
1829 'address' => $ip,
1830 'systemBlock' => 'dnsbl',
1831 ] );
1832 }
1833 }
1834
1835 // (T25343) Apply IP blocks to the contents of XFF headers, if enabled
1836 if ( !$block instanceof Block
1838 && $ip !== null
1839 && !in_array( $ip, $wgProxyWhitelist )
1840 ) {
1841 $xff = $this->getRequest()->getHeader( 'X-Forwarded-For' );
1842 $xff = array_map( 'trim', explode( ',', $xff ) );
1843 $xff = array_diff( $xff, [ $ip ] );
1844 $xffblocks = Block::getBlocksForIPList( $xff, $this->isAnon(), !$bFromSlave );
1845 $block = Block::chooseBlock( $xffblocks, $xff );
1846 if ( $block instanceof Block ) {
1847 # Mangle the reason to alert the user that the block
1848 # originated from matching the X-Forwarded-For header.
1849 $block->mReason = wfMessage( 'xffblockreason', $block->mReason )->text();
1850 }
1851 }
1852
1853 if ( !$block instanceof Block
1854 && $ip !== null
1855 && $this->isAnon()
1856 && IP::isInRanges( $ip, $wgSoftBlockRanges )
1857 ) {
1858 $block = new Block( [
1859 'address' => $ip,
1860 'byText' => 'MediaWiki default',
1861 'reason' => wfMessage( 'softblockrangesreason', $ip )->text(),
1862 'anonOnly' => true,
1863 'systemBlock' => 'wgSoftBlockRanges',
1864 ] );
1865 }
1866
1867 if ( $block instanceof Block ) {
1868 wfDebug( __METHOD__ . ": Found block.\n" );
1869 $this->mBlock = $block;
1870 $this->mBlockedby = $block->getByName();
1871 $this->mBlockreason = $block->mReason;
1872 $this->mHideName = $block->mHideName;
1873 $this->mAllowUsertalk = !$block->prevents( 'editownusertalk' );
1874 } else {
1875 $this->mBlock = null;
1876 $this->mBlockedby = '';
1877 $this->mBlockreason = '';
1878 $this->mHideName = 0;
1879 $this->mAllowUsertalk = false;
1880 }
1881
1882 // Avoid PHP 7.1 warning of passing $this by reference
1883 $thisUser = $this;
1884 // Extensions
1885 Hooks::run( 'GetBlockedStatus', [ &$thisUser ] );
1886 }
1887
1893 protected function getBlockFromCookieValue( $blockCookieVal ) {
1894 // Make sure there's something to check. The cookie value must start with a number.
1895 if ( strlen( $blockCookieVal ) < 1 || !is_numeric( substr( $blockCookieVal, 0, 1 ) ) ) {
1896 return false;
1897 }
1898 // Load the Block from the ID in the cookie.
1899 $blockCookieId = Block::getIdFromCookieValue( $blockCookieVal );
1900 if ( $blockCookieId !== null ) {
1901 // An ID was found in the cookie.
1902 $tmpBlock = Block::newFromID( $blockCookieId );
1903 if ( $tmpBlock instanceof Block ) {
1904 // Check the validity of the block.
1905 $blockIsValid = $tmpBlock->getType() == Block::TYPE_USER
1906 && !$tmpBlock->isExpired()
1907 && $tmpBlock->isAutoblocking();
1908 $config = RequestContext::getMain()->getConfig();
1909 $useBlockCookie = ( $config->get( 'CookieSetOnAutoblock' ) === true );
1910 if ( $blockIsValid && $useBlockCookie ) {
1911 // Use the block.
1912 return $tmpBlock;
1913 } else {
1914 // If the block is not valid, remove the cookie.
1915 Block::clearCookie( $this->getRequest()->response() );
1916 }
1917 } else {
1918 // If the block doesn't exist, remove the cookie.
1919 Block::clearCookie( $this->getRequest()->response() );
1920 }
1921 }
1922 return false;
1923 }
1924
1932 public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
1934
1935 if ( !$wgEnableDnsBlacklist ) {
1936 return false;
1937 }
1938
1939 if ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) ) {
1940 return false;
1941 }
1942
1943 return $this->inDnsBlacklist( $ip, $wgDnsBlacklistUrls );
1944 }
1945
1953 public function inDnsBlacklist( $ip, $bases ) {
1954 $found = false;
1955 // @todo FIXME: IPv6 ??? (https://bugs.php.net/bug.php?id=33170)
1956 if ( IP::isIPv4( $ip ) ) {
1957 // Reverse IP, T23255
1958 $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
1959
1960 foreach ( (array)$bases as $base ) {
1961 // Make hostname
1962 // If we have an access key, use that too (ProjectHoneypot, etc.)
1963 $basename = $base;
1964 if ( is_array( $base ) ) {
1965 if ( count( $base ) >= 2 ) {
1966 // Access key is 1, base URL is 0
1967 $host = "{$base[1]}.$ipReversed.{$base[0]}";
1968 } else {
1969 $host = "$ipReversed.{$base[0]}";
1970 }
1971 $basename = $base[0];
1972 } else {
1973 $host = "$ipReversed.$base";
1974 }
1975
1976 // Send query
1977 $ipList = gethostbynamel( $host );
1978
1979 if ( $ipList ) {
1980 wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $basename!" );
1981 $found = true;
1982 break;
1983 } else {
1984 wfDebugLog( 'dnsblacklist', "Requested $host, not found in $basename." );
1985 }
1986 }
1987 }
1988
1989 return $found;
1990 }
1991
1999 public static function isLocallyBlockedProxy( $ip ) {
2001
2002 if ( !$wgProxyList ) {
2003 return false;
2004 }
2005
2006 if ( !is_array( $wgProxyList ) ) {
2007 // Load values from the specified file
2008 $wgProxyList = array_map( 'trim', file( $wgProxyList ) );
2009 }
2010
2011 $resultProxyList = [];
2012 $deprecatedIPEntries = [];
2013
2014 // backward compatibility: move all ip addresses in keys to values
2015 foreach ( $wgProxyList as $key => $value ) {
2016 $keyIsIP = IP::isIPAddress( $key );
2017 $valueIsIP = IP::isIPAddress( $value );
2018 if ( $keyIsIP && !$valueIsIP ) {
2019 $deprecatedIPEntries[] = $key;
2020 $resultProxyList[] = $key;
2021 } elseif ( $keyIsIP && $valueIsIP ) {
2022 $deprecatedIPEntries[] = $key;
2023 $resultProxyList[] = $key;
2024 $resultProxyList[] = $value;
2025 } else {
2026 $resultProxyList[] = $value;
2027 }
2028 }
2029
2030 if ( $deprecatedIPEntries ) {
2032 'IP addresses in the keys of $wgProxyList (found the following IP addresses in keys: ' .
2033 implode( ', ', $deprecatedIPEntries ) . ', please move them to values)', '1.30' );
2034 }
2035
2036 $proxyListIPSet = new IPSet( $resultProxyList );
2037 return $proxyListIPSet->match( $ip );
2038 }
2039
2045 public function isPingLimitable() {
2047 if ( IP::isInRanges( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) {
2048 // No other good way currently to disable rate limits
2049 // for specific IPs. :P
2050 // But this is a crappy hack and should die.
2051 return false;
2052 }
2053 return !$this->isAllowed( 'noratelimit' );
2054 }
2055
2072 public function pingLimiter( $action = 'edit', $incrBy = 1 ) {
2073 // Avoid PHP 7.1 warning of passing $this by reference
2074 $user = $this;
2075
2076 $logger = \MediaWiki\Logger\LoggerFactory::getInstance( 'ratelimit' );
2077
2078 // Call the 'PingLimiter' hook
2079 $result = false;
2080 if ( !Hooks::run( 'PingLimiter', [ &$user, $action, &$result, $incrBy ] ) ) {
2081 return $result;
2082 }
2083
2085 if ( !isset( $wgRateLimits[$action] ) ) {
2086 return false;
2087 }
2088
2089 $limits = array_merge(
2090 [ '&can-bypass' => true ],
2091 $wgRateLimits[$action]
2092 );
2093
2094 // Some groups shouldn't trigger the ping limiter, ever
2095 if ( $limits['&can-bypass'] && !$this->isPingLimitable() ) {
2096 return false;
2097 }
2098
2099 $logger->debug( __METHOD__ . ": limiting $action rate for {$this->getName()}" );
2100
2101 $keys = [];
2102 $id = $this->getId();
2103 $isNewbie = $this->isNewbie();
2104 $cache = ObjectCache::getLocalClusterInstance();
2105
2106 if ( $id == 0 ) {
2107 // "shared anon" limit, for all anons combined
2108 if ( isset( $limits['anon'] ) ) {
2109 $keys[$cache->makeKey( 'limiter', $action, 'anon' )] = $limits['anon'];
2110 }
2111 } else {
2112 // "global per name" limit, across sites
2113 if ( isset( $limits['user-global'] ) ) {
2114 $lookup = CentralIdLookup::factoryNonLocal();
2115
2116 $centralId = $lookup
2117 ? $lookup->centralIdFromLocalUser( $this, CentralIdLookup::AUDIENCE_RAW )
2118 : 0;
2119
2120 if ( $centralId ) {
2121 // We don't have proper realms, use provider ID.
2122 $realm = $lookup->getProviderId();
2123
2124 $globalKey = $cache->makeGlobalKey( 'limiter', $action, 'user-global',
2125 $realm, $centralId );
2126 } else {
2127 // Fall back to a local key for a local ID
2128 $globalKey = $cache->makeKey( 'limiter', $action, 'user-global',
2129 'local', $id );
2130 }
2131 $keys[$globalKey] = $limits['user-global'];
2132 }
2133 }
2134
2135 if ( $isNewbie ) {
2136 // "per ip" limit for anons and newbie users
2137 if ( isset( $limits['ip'] ) ) {
2138 $ip = $this->getRequest()->getIP();
2139 $keys[$cache->makeGlobalKey( 'limiter', $action, 'ip', $ip )] = $limits['ip'];
2140 }
2141 // "per subnet" limit for anons and newbie users
2142 if ( isset( $limits['subnet'] ) ) {
2143 $ip = $this->getRequest()->getIP();
2144 $subnet = IP::getSubnet( $ip );
2145 if ( $subnet !== false ) {
2146 $keys[$cache->makeGlobalKey( 'limiter', $action, 'subnet', $subnet )] = $limits['subnet'];
2147 }
2148 }
2149 }
2150
2151 // determine the "per user account" limit
2152 $userLimit = false;
2153 if ( $id !== 0 && isset( $limits['user'] ) ) {
2154 // default limit for logged-in users
2155 $userLimit = $limits['user'];
2156 }
2157 // limits for newbie logged-in users (overrides all the normal user limits)
2158 if ( $id !== 0 && $isNewbie && isset( $limits['newbie'] ) ) {
2159 $userLimit = $limits['newbie'];
2160 } else {
2161 // Check for group-specific limits
2162 // If more than one group applies, use the highest allowance (if higher than the default)
2163 foreach ( $this->getGroups() as $group ) {
2164 if ( isset( $limits[$group] ) ) {
2165 if ( $userLimit === false
2166 || $limits[$group][0] / $limits[$group][1] > $userLimit[0] / $userLimit[1]
2167 ) {
2168 $userLimit = $limits[$group];
2169 }
2170 }
2171 }
2172 }
2173
2174 // Set the user limit key
2175 if ( $userLimit !== false ) {
2176 list( $max, $period ) = $userLimit;
2177 $logger->debug( __METHOD__ . ": effective user limit: $max in {$period}s" );
2178 $keys[$cache->makeKey( 'limiter', $action, 'user', $id )] = $userLimit;
2179 }
2180
2181 // ip-based limits for all ping-limitable users
2182 if ( isset( $limits['ip-all'] ) ) {
2183 $ip = $this->getRequest()->getIP();
2184 // ignore if user limit is more permissive
2185 if ( $isNewbie || $userLimit === false
2186 || $limits['ip-all'][0] / $limits['ip-all'][1] > $userLimit[0] / $userLimit[1] ) {
2187 $keys[$cache->makeGlobalKey( 'limiter', $action, 'ip-all', $ip )] = $limits['ip-all'];
2188 }
2189 }
2190
2191 // subnet-based limits for all ping-limitable users
2192 if ( isset( $limits['subnet-all'] ) ) {
2193 $ip = $this->getRequest()->getIP();
2194 $subnet = IP::getSubnet( $ip );
2195 if ( $subnet !== false ) {
2196 // ignore if user limit is more permissive
2197 if ( $isNewbie || $userLimit === false
2198 || $limits['ip-all'][0] / $limits['ip-all'][1]
2199 > $userLimit[0] / $userLimit[1] ) {
2200 $keys[$cache->makeGlobalKey( 'limiter', $action, 'subnet-all', $subnet )]
2201 = $limits['subnet-all'];
2202 }
2203 }
2204 }
2205
2206 // XXX: We may want to use $cache->getCurrentTime() here, but that would make it
2207 // harder to test for T246991. Also $cache->getCurrentTime() is documented
2208 // as being for testing only, so it apparently should not be called here.
2209 $now = (int)wfTimestamp( TS_UNIX );
2210 $clockFudge = 3; // avoid log spam when a clock is slightly off
2211
2212 $triggered = false;
2213 foreach ( $keys as $key => $limit ) {
2214 // Do the update in a merge callback, for atomicity.
2215 // To use merge(), we need to explicitly track the desired expiry timestamp.
2216 // This tracking was introduced to investigate T246991. Once it is no longer needed,
2217 // we could go back to incrWithInit(), though that has more potential for race
2218 // conditions between the get() and incrWithInit() calls.
2219 $cache->merge(
2220 $key,
2221 function ( $cache, $key, $data, &$expiry )
2222 use ( $action, $logger, &$triggered, $now, $clockFudge, $limit, $incrBy )
2223 {
2224 // phan is confused because &can-bypass's value is a bool, so it assumes
2225 // that $userLimit is also a bool here.
2226 // @phan-suppress-next-line PhanTypeInvalidExpressionArrayDestructuring
2227 list( $max, $period ) = $limit;
2228
2229 $expiry = $now + (int)$period;
2230 $count = 0;
2231
2232 // Already pinged?
2233 if ( $data ) {
2234 // NOTE: in order to investigate T246991, we write the expiry time
2235 // into the payload, along with the count.
2236 $fields = explode( '|', $data );
2237 $storedCount = (int)( $fields[0] ?? 0 );
2238 $storedExpiry = (int)( $fields[1] ?? PHP_INT_MAX );
2239
2240 // Found a stale entry. This should not happen!
2241 if ( $storedExpiry < ( $now + $clockFudge ) ) {
2242 $logger->info(
2243 'User::pingLimiter: '
2244 . 'Stale rate limit entry, cache key failed to expire (T246991)',
2245 [
2246 'action' => $action,
2247 'user' => $this->getName(),
2248 'limit' => $max,
2249 'period' => $period,
2250 'count' => $storedCount,
2251 'key' => $key,
2252 'expiry' => MWTimestamp::convert( TS_DB, $storedExpiry ),
2253 ]
2254 );
2255 } else {
2256 // NOTE: We'll keep the original expiry when bumping counters,
2257 // resulting in a kind of fixed-window throttle.
2258 $expiry = min( $storedExpiry, $now + (int)$period );
2259 $count = $storedCount;
2260 }
2261 }
2262
2263 // Limit exceeded!
2264 if ( $count >= $max ) {
2265 if ( !$triggered ) {
2266 $logger->info(
2267 'User::pingLimiter: User tripped rate limit',
2268 [
2269 'action' => $action,
2270 'user' => $this->getName(),
2271 'ip' => $this->getRequest()->getIP(),
2272 'limit' => $max,
2273 'period' => $period,
2274 'count' => $count,
2275 'key' => $key
2276 ]
2277 );
2278 }
2279
2280 $triggered = true;
2281 }
2282
2283 $count += $incrBy;
2284 $data = "$count|$expiry";
2285 return $data;
2286 }
2287 );
2288 }
2289
2290 return $triggered;
2291 }
2292
2300 public function isBlocked( $bFromSlave = true ) {
2301 return $this->getBlock( $bFromSlave ) instanceof Block && $this->getBlock()->prevents( 'edit' );
2302 }
2303
2310 public function getBlock( $bFromSlave = true ) {
2311 $this->getBlockedStatus( $bFromSlave );
2312 return $this->mBlock instanceof Block ? $this->mBlock : null;
2313 }
2314
2322 public function isBlockedFrom( $title, $bFromSlave = false ) {
2324
2325 $blocked = $this->isBlocked( $bFromSlave );
2326 $allowUsertalk = ( $wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false );
2327 // If a user's name is suppressed, they cannot make edits anywhere
2328 if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName()
2329 && $title->getNamespace() == NS_USER_TALK ) {
2330 $blocked = false;
2331 wfDebug( __METHOD__ . ": self-talk page, ignoring any blocks\n" );
2332 }
2333
2334 Hooks::run( 'UserIsBlockedFrom', [ $this, $title, &$blocked, &$allowUsertalk ] );
2335
2336 return $blocked;
2337 }
2338
2343 public function blockedBy() {
2344 $this->getBlockedStatus();
2345 return $this->mBlockedby;
2346 }
2347
2352 public function blockedFor() {
2353 $this->getBlockedStatus();
2354 return $this->mBlockreason;
2355 }
2356
2361 public function getBlockId() {
2362 $this->getBlockedStatus();
2363 return ( $this->mBlock ? $this->mBlock->getId() : false );
2364 }
2365
2374 public function isBlockedGlobally( $ip = '' ) {
2375 return $this->getGlobalBlock( $ip ) instanceof Block;
2376 }
2377
2388 public function getGlobalBlock( $ip = '' ) {
2389 if ( $this->mGlobalBlock !== null ) {
2390 return $this->mGlobalBlock ?: null;
2391 }
2392 // User is already an IP?
2393 if ( IP::isIPAddress( $this->getName() ) ) {
2394 $ip = $this->getName();
2395 } elseif ( !$ip ) {
2396 $ip = $this->getRequest()->getIP();
2397 }
2398 // Avoid PHP 7.1 warning of passing $this by reference
2399 $user = $this;
2400 $blocked = false;
2401 $block = null;
2402 Hooks::run( 'UserIsBlockedGlobally', [ &$user, $ip, &$blocked, &$block ] );
2403
2404 if ( $blocked && $block === null ) {
2405 // back-compat: UserIsBlockedGlobally didn't have $block param first
2406 $block = new Block( [
2407 'address' => $ip,
2408 'systemBlock' => 'global-block'
2409 ] );
2410 }
2411
2412 $this->mGlobalBlock = $blocked ? $block : false;
2413 return $this->mGlobalBlock ?: null;
2414 }
2415
2421 public function isLocked() {
2422 if ( $this->mLocked !== null ) {
2423 return $this->mLocked;
2424 }
2425 // Avoid PHP 7.1 warning of passing $this by reference
2426 $user = $this;
2427 $authUser = AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$user ], null );
2428 $this->mLocked = $authUser && $authUser->isLocked();
2429 Hooks::run( 'UserIsLocked', [ $this, &$this->mLocked ] );
2430 return $this->mLocked;
2431 }
2432
2438 public function isHidden() {
2439 if ( $this->mHideName !== null ) {
2440 return $this->mHideName;
2441 }
2442 $this->getBlockedStatus();
2443 if ( !$this->mHideName ) {
2444 // Avoid PHP 7.1 warning of passing $this by reference
2445 $user = $this;
2446 $authUser = AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$user ], null );
2447 $this->mHideName = $authUser && $authUser->isHidden();
2448 Hooks::run( 'UserIsHidden', [ $this, &$this->mHideName ] );
2449 }
2450 return $this->mHideName;
2451 }
2452
2457 public function getId() {
2458 if ( $this->mId === null && $this->mName !== null && self::isIP( $this->mName ) ) {
2459 // Special case, we know the user is anonymous
2460 return 0;
2461 } elseif ( !$this->isItemLoaded( 'id' ) ) {
2462 // Don't load if this was initialized from an ID
2463 $this->load();
2464 }
2465
2466 return (int)$this->mId;
2467 }
2468
2473 public function setId( $v ) {
2474 $this->mId = $v;
2475 $this->clearInstanceCache( 'id' );
2476 }
2477
2482 public function getName() {
2483 if ( $this->isItemLoaded( 'name', 'only' ) ) {
2484 // Special case optimisation
2485 return $this->mName;
2486 } else {
2487 $this->load();
2488 if ( $this->mName === false ) {
2489 // Clean up IPs
2490 $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() );
2491 }
2492 return $this->mName;
2493 }
2494 }
2495
2509 public function setName( $str ) {
2510 $this->load();
2511 $this->mName = $str;
2512 }
2513
2520 public function getActorId( IDatabase $dbw = null ) {
2522
2524 return 0;
2525 }
2526
2527 if ( !$this->isItemLoaded( 'actor' ) ) {
2528 $this->load();
2529 }
2530
2531 if ( $this->mActorId === null || !$this->mActorId && $dbw ) {
2532 $migration = MediaWikiServices::getInstance()->getActorMigration();
2533 if ( !$dbw ) {
2534 // Read from a database, flags are used for wfGetDB()
2535 $dbw = $this->queryFlagsUsed;
2536 }
2537 $this->mActorId = $migration->getNewActorId( $dbw, $this );
2538
2539 if ( $dbw instanceof IDatabase ) {
2540 $this->invalidateCache();
2541 }
2542 $this->setItemLoaded( 'actor' );
2543 }
2544
2545 return (int)$this->mActorId;
2546 }
2547
2552 public function getTitleKey() {
2553 return str_replace( ' ', '_', $this->getName() );
2554 }
2555
2560 public function getNewtalk() {
2561 $this->load();
2562
2563 // Load the newtalk status if it is unloaded (mNewtalk=-1)
2564 if ( $this->mNewtalk === -1 ) {
2565 $this->mNewtalk = false; # reset talk page status
2566
2567 // Check memcached separately for anons, who have no
2568 // entire User object stored in there.
2569 if ( !$this->mId ) {
2571 if ( $wgDisableAnonTalk ) {
2572 // Anon newtalk disabled by configuration.
2573 $this->mNewtalk = false;
2574 } else {
2575 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
2576 }
2577 } else {
2578 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
2579 }
2580 }
2581
2582 return (bool)$this->mNewtalk;
2583 }
2584
2598 public function getNewMessageLinks() {
2599 // Avoid PHP 7.1 warning of passing $this by reference
2600 $user = $this;
2601 $talks = [];
2602 if ( !Hooks::run( 'UserRetrieveNewTalks', [ &$user, &$talks ] ) ) {
2603 return $talks;
2604 } elseif ( !$this->getNewtalk() ) {
2605 return [];
2606 }
2607 $utp = $this->getTalkPage();
2608 $dbr = wfGetDB( DB_REPLICA );
2609 // Get the "last viewed rev" timestamp from the oldest message notification
2610 $timestamp = $dbr->selectField( 'user_newtalk',
2611 'MIN(user_last_timestamp)',
2612 $this->isAnon() ? [ 'user_ip' => $this->getName() ] : [ 'user_id' => $this->getId() ],
2613 __METHOD__ );
2614 $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null;
2615 return [ [ 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL(), 'rev' => $rev ] ];
2616 }
2617
2623 public function getNewMessageRevisionId() {
2624 $newMessageRevisionId = null;
2625 $newMessageLinks = $this->getNewMessageLinks();
2626 if ( $newMessageLinks ) {
2627 // Note: getNewMessageLinks() never returns more than a single link
2628 // and it is always for the same wiki, but we double-check here in
2629 // case that changes some time in the future.
2630 if ( count( $newMessageLinks ) === 1
2631 && $newMessageLinks[0]['wiki'] === wfWikiID()
2632 && $newMessageLinks[0]['rev']
2633 ) {
2635 $newMessageRevision = $newMessageLinks[0]['rev'];
2636 $newMessageRevisionId = $newMessageRevision->getId();
2637 }
2638 }
2639 return $newMessageRevisionId;
2640 }
2641
2650 protected function checkNewtalk( $field, $id ) {
2651 $dbr = wfGetDB( DB_REPLICA );
2652
2653 $ok = $dbr->selectField( 'user_newtalk', $field, [ $field => $id ], __METHOD__ );
2654
2655 return $ok !== false;
2656 }
2657
2665 protected function updateNewtalk( $field, $id, $curRev = null ) {
2666 // Get timestamp of the talk page revision prior to the current one
2667 $prevRev = $curRev ? $curRev->getPrevious() : false;
2668 $ts = $prevRev ? $prevRev->getTimestamp() : null;
2669 // Mark the user as having new messages since this revision
2670 $dbw = wfGetDB( DB_MASTER );
2671 $dbw->insert( 'user_newtalk',
2672 [ $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ],
2673 __METHOD__,
2674 'IGNORE' );
2675 if ( $dbw->affectedRows() ) {
2676 wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
2677 return true;
2678 } else {
2679 wfDebug( __METHOD__ . " already set ($field, $id)\n" );
2680 return false;
2681 }
2682 }
2683
2690 protected function deleteNewtalk( $field, $id ) {
2691 $dbw = wfGetDB( DB_MASTER );
2692 $dbw->delete( 'user_newtalk',
2693 [ $field => $id ],
2694 __METHOD__ );
2695 if ( $dbw->affectedRows() ) {
2696 wfDebug( __METHOD__ . ": killed on ($field, $id)\n" );
2697 return true;
2698 } else {
2699 wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
2700 return false;
2701 }
2702 }
2703
2710 public function setNewtalk( $val, $curRev = null ) {
2711 if ( wfReadOnly() ) {
2712 return;
2713 }
2714
2715 $this->load();
2716 $this->mNewtalk = $val;
2717
2718 if ( $this->isAnon() ) {
2719 $field = 'user_ip';
2720 $id = $this->getName();
2721 } else {
2722 $field = 'user_id';
2723 $id = $this->getId();
2724 }
2725
2726 if ( $val ) {
2727 $changed = $this->updateNewtalk( $field, $id, $curRev );
2728 } else {
2729 $changed = $this->deleteNewtalk( $field, $id );
2730 }
2731
2732 if ( $changed ) {
2733 $this->invalidateCache();
2734 }
2735 }
2736
2742 private function newTouchedTimestamp() {
2744
2745 $time = wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
2746 if ( $this->mTouched && $time <= $this->mTouched ) {
2747 $time = wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $this->mTouched ) + 1 );
2748 }
2749
2750 return $time;
2751 }
2752
2763 public function clearSharedCache( $mode = 'changed' ) {
2764 if ( !$this->getId() ) {
2765 return;
2766 }
2767
2768 $cache = ObjectCache::getMainWANInstance();
2769 $key = $this->getCacheKey( $cache );
2770 if ( $mode === 'refresh' ) {
2771 $cache->delete( $key, 1 );
2772 } else {
2773 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
2774 if ( $lb->hasOrMadeRecentMasterChanges() ) {
2775 $lb->getConnection( DB_MASTER )->onTransactionPreCommitOrIdle(
2776 function () use ( $cache, $key ) {
2777 $cache->delete( $key );
2778 },
2779 __METHOD__
2780 );
2781 } else {
2782 $cache->delete( $key );
2783 }
2784 }
2785 }
2786
2792 public function invalidateCache() {
2793 $this->touch();
2794 $this->clearSharedCache();
2795 }
2796
2809 public function touch() {
2810 $id = $this->getId();
2811 if ( $id ) {
2812 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
2813 $key = $cache->makeKey( 'user-quicktouched', 'id', $id );
2814 $cache->touchCheckKey( $key );
2815 $this->mQuickTouched = null;
2816 }
2817 }
2818
2824 public function validateCache( $timestamp ) {
2825 return ( $timestamp >= $this->getTouched() );
2826 }
2827
2836 public function getTouched() {
2837 $this->load();
2838
2839 if ( $this->mId ) {
2840 if ( $this->mQuickTouched === null ) {
2841 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
2842 $key = $cache->makeKey( 'user-quicktouched', 'id', $this->mId );
2843
2844 $this->mQuickTouched = wfTimestamp( TS_MW, $cache->getCheckKeyTime( $key ) );
2845 }
2846
2847 return max( $this->mTouched, $this->mQuickTouched );
2848 }
2849
2850 return $this->mTouched;
2851 }
2852
2858 public function getDBTouched() {
2859 $this->load();
2860
2861 return $this->mTouched;
2862 }
2863
2880 public function setPassword( $str ) {
2881 return $this->setPasswordInternal( $str );
2882 }
2883
2892 public function setInternalPassword( $str ) {
2893 $this->setPasswordInternal( $str );
2894 }
2895
2904 private function setPasswordInternal( $str ) {
2905 $manager = AuthManager::singleton();
2906
2907 // If the user doesn't exist yet, fail
2908 if ( !$manager->userExists( $this->getName() ) ) {
2909 throw new LogicException( 'Cannot set a password for a user that is not in the database.' );
2910 }
2911
2913 'username' => $this->getName(),
2914 'password' => $str,
2915 'retype' => $str,
2916 ] );
2917 if ( !$status->isGood() ) {
2918 \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
2919 ->info( __METHOD__ . ': Password change rejected: '
2920 . $status->getWikiText( null, null, 'en' ) );
2921 return false;
2922 }
2923
2924 $this->setOption( 'watchlisttoken', false );
2925 SessionManager::singleton()->invalidateSessionsForUser( $this );
2926
2927 return true;
2928 }
2929
2942 public function changeAuthenticationData( array $data ) {
2943 $manager = AuthManager::singleton();
2944 $reqs = $manager->getAuthenticationRequests( AuthManager::ACTION_CHANGE, $this );
2945 $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
2946
2947 $status = Status::newGood( 'ignored' );
2948 foreach ( $reqs as $req ) {
2949 $status->merge( $manager->allowsAuthenticationDataChange( $req ), true );
2950 }
2951 if ( $status->getValue() === 'ignored' ) {
2952 $status->warning( 'authenticationdatachange-ignored' );
2953 }
2954
2955 if ( $status->isGood() ) {
2956 foreach ( $reqs as $req ) {
2957 $manager->changeAuthenticationData( $req );
2958 }
2959 }
2960 return $status;
2961 }
2962
2969 public function getToken( $forceCreation = true ) {
2971
2972 $this->load();
2973 if ( !$this->mToken && $forceCreation ) {
2974 $this->setToken();
2975 }
2976
2977 if ( !$this->mToken ) {
2978 // The user doesn't have a token, return null to indicate that.
2979 return null;
2980 } elseif ( $this->mToken === self::INVALID_TOKEN ) {
2981 // We return a random value here so existing token checks are very
2982 // likely to fail.
2983 return MWCryptRand::generateHex( self::TOKEN_LENGTH );
2984 } elseif ( $wgAuthenticationTokenVersion === null ) {
2985 // $wgAuthenticationTokenVersion not in use, so return the raw secret
2986 return $this->mToken;
2987 } else {
2988 // $wgAuthenticationTokenVersion in use, so hmac it.
2989 $ret = MWCryptHash::hmac( $wgAuthenticationTokenVersion, $this->mToken, false );
2990
2991 // The raw hash can be overly long. Shorten it up.
2992 $len = max( 32, self::TOKEN_LENGTH );
2993 if ( strlen( $ret ) < $len ) {
2994 // Should never happen, even md5 is 128 bits
2995 throw new \UnexpectedValueException( 'Hmac returned less than 128 bits' );
2996 }
2997 return substr( $ret, -$len );
2998 }
2999 }
3000
3007 public function setToken( $token = false ) {
3008 $this->load();
3009 if ( $this->mToken === self::INVALID_TOKEN ) {
3010 \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
3011 ->debug( __METHOD__ . ": Ignoring attempt to set token for system user \"$this\"" );
3012 } elseif ( !$token ) {
3013 $this->mToken = MWCryptRand::generateHex( self::TOKEN_LENGTH );
3014 } else {
3015 $this->mToken = $token;
3016 }
3017 }
3018
3027 public function setNewpassword( $str, $throttle = true ) {
3028 throw new BadMethodCallException( __METHOD__ . ' has been removed in 1.27' );
3029 }
3030
3035 public function getEmail() {
3036 $this->load();
3037 Hooks::run( 'UserGetEmail', [ $this, &$this->mEmail ] );
3038 return $this->mEmail;
3039 }
3040
3046 $this->load();
3047 Hooks::run( 'UserGetEmailAuthenticationTimestamp', [ $this, &$this->mEmailAuthenticated ] );
3049 }
3050
3055 public function setEmail( $str ) {
3056 $this->load();
3057 if ( $str == $this->mEmail ) {
3058 return;
3059 }
3060 $this->invalidateEmail();
3061 $this->mEmail = $str;
3062 Hooks::run( 'UserSetEmail', [ $this, &$this->mEmail ] );
3063 }
3064
3072 public function setEmailWithConfirmation( $str ) {
3074
3075 if ( !$wgEnableEmail ) {
3076 return Status::newFatal( 'emaildisabled' );
3077 }
3078
3079 $oldaddr = $this->getEmail();
3080 if ( $str === $oldaddr ) {
3081 return Status::newGood( true );
3082 }
3083
3084 $type = $oldaddr != '' ? 'changed' : 'set';
3085 $notificationResult = null;
3086
3087 if ( $wgEmailAuthentication ) {
3088 // Send the user an email notifying the user of the change in registered
3089 // email address on their previous email address
3090 if ( $type == 'changed' ) {
3091 $change = $str != '' ? 'changed' : 'removed';
3092 $notificationResult = $this->sendMail(
3093 wfMessage( 'notificationemail_subject_' . $change )->text(),
3094 wfMessage( 'notificationemail_body_' . $change,
3095 $this->getRequest()->getIP(),
3096 $this->getName(),
3097 $str )->text()
3098 );
3099 }
3100 }
3101
3102 $this->setEmail( $str );
3103
3104 if ( $str !== '' && $wgEmailAuthentication ) {
3105 // Send a confirmation request to the new address if needed
3107
3108 if ( $notificationResult !== null ) {
3109 $result->merge( $notificationResult );
3110 }
3111
3112 if ( $result->isGood() ) {
3113 // Say to the caller that a confirmation and notification mail has been sent
3114 $result->value = 'eauth';
3115 }
3116 } else {
3117 $result = Status::newGood( true );
3118 }
3119
3120 return $result;
3121 }
3122
3127 public function getRealName() {
3128 if ( !$this->isItemLoaded( 'realname' ) ) {
3129 $this->load();
3130 }
3131
3132 return $this->mRealName;
3133 }
3134
3139 public function setRealName( $str ) {
3140 $this->load();
3141 $this->mRealName = $str;
3142 }
3143
3154 public function getOption( $oname, $defaultOverride = null, $ignoreHidden = false ) {
3156 $this->loadOptions();
3157
3158 # We want 'disabled' preferences to always behave as the default value for
3159 # users, even if they have set the option explicitly in their settings (ie they
3160 # set it, and then it was disabled removing their ability to change it). But
3161 # we don't want to erase the preferences in the database in case the preference
3162 # is re-enabled again. So don't touch $mOptions, just override the returned value
3163 if ( !$ignoreHidden && in_array( $oname, $wgHiddenPrefs ) ) {
3164 return self::getDefaultOption( $oname );
3165 }
3166
3167 if ( array_key_exists( $oname, $this->mOptions ) ) {
3168 return $this->mOptions[$oname];
3169 } else {
3170 return $defaultOverride;
3171 }
3172 }
3173
3182 public function getOptions( $flags = 0 ) {
3184 $this->loadOptions();
3186
3187 # We want 'disabled' preferences to always behave as the default value for
3188 # users, even if they have set the option explicitly in their settings (ie they
3189 # set it, and then it was disabled removing their ability to change it). But
3190 # we don't want to erase the preferences in the database in case the preference
3191 # is re-enabled again. So don't touch $mOptions, just override the returned value
3192 foreach ( $wgHiddenPrefs as $pref ) {
3193 $default = self::getDefaultOption( $pref );
3194 if ( $default !== null ) {
3195 $options[$pref] = $default;
3196 }
3197 }
3198
3199 if ( $flags & self::GETOPTIONS_EXCLUDE_DEFAULTS ) {
3200 $options = array_diff_assoc( $options, self::getDefaultOptions() );
3201 }
3202
3203 return $options;
3204 }
3205
3213 public function getBoolOption( $oname ) {
3214 return (bool)$this->getOption( $oname );
3215 }
3216
3225 public function getIntOption( $oname, $defaultOverride = 0 ) {
3226 $val = $this->getOption( $oname );
3227 if ( $val == '' ) {
3228 $val = $defaultOverride;
3229 }
3230 return intval( $val );
3231 }
3232
3241 public function setOption( $oname, $val ) {
3242 $this->loadOptions();
3243
3244 // Explicitly NULL values should refer to defaults
3245 if ( is_null( $val ) ) {
3246 $val = self::getDefaultOption( $oname );
3247 }
3248
3249 $this->mOptions[$oname] = $val;
3250 }
3251
3262 public function getTokenFromOption( $oname ) {
3264
3265 $id = $this->getId();
3266 if ( !$id || in_array( $oname, $wgHiddenPrefs ) ) {
3267 return false;
3268 }
3269
3270 $token = $this->getOption( $oname );
3271 if ( !$token ) {
3272 // Default to a value based on the user token to avoid space
3273 // wasted on storing tokens for all users. When this option
3274 // is set manually by the user, only then is it stored.
3275 $token = hash_hmac( 'sha1', "$oname:$id", $this->getToken() );
3276 }
3277
3278 return $token;
3279 }
3280
3290 public function resetTokenFromOption( $oname ) {
3292 if ( in_array( $oname, $wgHiddenPrefs ) ) {
3293 return false;
3294 }
3295
3296 $token = MWCryptRand::generateHex( 40 );
3297 $this->setOption( $oname, $token );
3298 return $token;
3299 }
3300
3324 public static function listOptionKinds() {
3325 return [
3326 'registered',
3327 'registered-multiselect',
3328 'registered-checkmatrix',
3329 'userjs',
3330 'special',
3331 'unused'
3332 ];
3333 }
3334
3347 public function getOptionKinds( IContextSource $context, $options = null ) {
3348 $this->loadOptions();
3349 if ( $options === null ) {
3351 }
3352
3353 $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory();
3354 $prefs = $preferencesFactory->getFormDescriptor( $this, $context );
3355 $mapping = [];
3356
3357 // Pull out the "special" options, so they don't get converted as
3358 // multiselect or checkmatrix.
3359 $specialOptions = array_fill_keys( $preferencesFactory->getSaveBlacklist(), true );
3360 foreach ( $specialOptions as $name => $value ) {
3361 unset( $prefs[$name] );
3362 }
3363
3364 // Multiselect and checkmatrix options are stored in the database with
3365 // one key per option, each having a boolean value. Extract those keys.
3366 $multiselectOptions = [];
3367 foreach ( $prefs as $name => $info ) {
3368 if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
3369 ( isset( $info['class'] ) && $info['class'] == HTMLMultiSelectField::class ) ) {
3370 $opts = HTMLFormField::flattenOptions( $info['options'] );
3371 $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
3372
3373 foreach ( $opts as $value ) {
3374 $multiselectOptions["$prefix$value"] = true;
3375 }
3376
3377 unset( $prefs[$name] );
3378 }
3379 }
3380 $checkmatrixOptions = [];
3381 foreach ( $prefs as $name => $info ) {
3382 if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
3383 ( isset( $info['class'] ) && $info['class'] == HTMLCheckMatrix::class ) ) {
3384 $columns = HTMLFormField::flattenOptions( $info['columns'] );
3385 $rows = HTMLFormField::flattenOptions( $info['rows'] );
3386 $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
3387
3388 foreach ( $columns as $column ) {
3389 foreach ( $rows as $row ) {
3390 $checkmatrixOptions["$prefix$column-$row"] = true;
3391 }
3392 }
3393
3394 unset( $prefs[$name] );
3395 }
3396 }
3397
3398 // $value is ignored
3399 foreach ( $options as $key => $value ) {
3400 if ( isset( $prefs[$key] ) ) {
3401 $mapping[$key] = 'registered';
3402 } elseif ( isset( $multiselectOptions[$key] ) ) {
3403 $mapping[$key] = 'registered-multiselect';
3404 } elseif ( isset( $checkmatrixOptions[$key] ) ) {
3405 $mapping[$key] = 'registered-checkmatrix';
3406 } elseif ( isset( $specialOptions[$key] ) ) {
3407 $mapping[$key] = 'special';
3408 } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
3409 $mapping[$key] = 'userjs';
3410 } else {
3411 $mapping[$key] = 'unused';
3412 }
3413 }
3414
3415 return $mapping;
3416 }
3417
3432 public function resetOptions(
3433 $resetKinds = [ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ],
3435 ) {
3436 $this->load();
3437 $defaultOptions = self::getDefaultOptions();
3438
3439 if ( !is_array( $resetKinds ) ) {
3440 $resetKinds = [ $resetKinds ];
3441 }
3442
3443 if ( in_array( 'all', $resetKinds ) ) {
3444 $newOptions = $defaultOptions;
3445 } else {
3446 if ( $context === null ) {
3448 }
3449
3450 $optionKinds = $this->getOptionKinds( $context );
3451 $resetKinds = array_intersect( $resetKinds, self::listOptionKinds() );
3452 $newOptions = [];
3453
3454 // Use default values for the options that should be deleted, and
3455 // copy old values for the ones that shouldn't.
3456 foreach ( $this->mOptions as $key => $value ) {
3457 if ( in_array( $optionKinds[$key], $resetKinds ) ) {
3458 if ( array_key_exists( $key, $defaultOptions ) ) {
3459 $newOptions[$key] = $defaultOptions[$key];
3460 }
3461 } else {
3462 $newOptions[$key] = $value;
3463 }
3464 }
3465 }
3466
3467 Hooks::run( 'UserResetAllOptions', [ $this, &$newOptions, $this->mOptions, $resetKinds ] );
3468
3469 $this->mOptions = $newOptions;
3470 $this->mOptionsLoaded = true;
3471 }
3472
3477 public function getDatePreference() {
3478 // Important migration for old data rows
3479 if ( is_null( $this->mDatePreference ) ) {
3481 $value = $this->getOption( 'date' );
3482 $map = $wgLang->getDatePreferenceMigrationMap();
3483 if ( isset( $map[$value] ) ) {
3484 $value = $map[$value];
3485 }
3486 $this->mDatePreference = $value;
3487 }
3489 }
3490
3497 public function requiresHTTPS() {
3499 if ( $wgForceHTTPS ) {
3500 return true;
3501 }
3502 if ( !$wgSecureLogin ) {
3503 return false;
3504 } else {
3505 $https = $this->getBoolOption( 'prefershttps' );
3506 Hooks::run( 'UserRequiresHTTPS', [ $this, &$https ] );
3507 if ( $https ) {
3508 $https = wfCanIPUseHTTPS( $this->getRequest()->getIP() );
3509 }
3510 return $https;
3511 }
3512 }
3513
3519 public function getStubThreshold() {
3520 global $wgMaxArticleSize; # Maximum article size, in Kb
3521 $threshold = $this->getIntOption( 'stubthreshold' );
3522 if ( $threshold > $wgMaxArticleSize * 1024 ) {
3523 // If they have set an impossible value, disable the preference
3524 // so we can use the parser cache again.
3525 $threshold = 0;
3526 }
3527 return $threshold;
3528 }
3529
3534 public function getRights() {
3535 if ( is_null( $this->mRights ) ) {
3536 $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
3537 Hooks::run( 'UserGetRights', [ $this, &$this->mRights ] );
3538
3539 // Deny any rights denied by the user's session, unless this
3540 // endpoint has no sessions.
3541 if ( !defined( 'MW_NO_SESSION' ) ) {
3542 $allowedRights = $this->getRequest()->getSession()->getAllowedUserRights();
3543 if ( $allowedRights !== null ) {
3544 $this->mRights = array_intersect( $this->mRights, $allowedRights );
3545 }
3546 }
3547
3548 // Force reindexation of rights when a hook has unset one of them
3549 $this->mRights = array_values( array_unique( $this->mRights ) );
3550
3551 // If block disables login, we should also remove any
3552 // extra rights blocked users might have, in case the
3553 // blocked user has a pre-existing session (T129738).
3554 // This is checked here for cases where people only call
3555 // $user->isAllowed(). It is also checked in Title::checkUserBlock()
3556 // to give a better error message in the common case.
3557 $config = RequestContext::getMain()->getConfig();
3558 if (
3559 $this->isLoggedIn() &&
3560 $config->get( 'BlockDisablesLogin' ) &&
3561 $this->isBlocked()
3562 ) {
3563 $anon = new User;
3564 $this->mRights = array_intersect( $this->mRights, $anon->getRights() );
3565 }
3566 }
3567 return $this->mRights;
3568 }
3569
3575 public function getGroups() {
3576 $this->load();
3577 $this->loadGroups();
3578 return array_keys( $this->mGroupMemberships );
3579 }
3580
3588 public function getGroupMemberships() {
3589 $this->load();
3590 $this->loadGroups();
3592 }
3593
3601 public function getEffectiveGroups( $recache = false ) {
3602 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
3603 $this->mEffectiveGroups = array_unique( array_merge(
3604 $this->getGroups(), // explicit groups
3605 $this->getAutomaticGroups( $recache ) // implicit groups
3606 ) );
3607 // Avoid PHP 7.1 warning of passing $this by reference
3608 $user = $this;
3609 // Hook for additional groups
3610 Hooks::run( 'UserEffectiveGroups', [ &$user, &$this->mEffectiveGroups ] );
3611 // Force reindexation of groups when a hook has unset one of them
3612 $this->mEffectiveGroups = array_values( array_unique( $this->mEffectiveGroups ) );
3613 }
3615 }
3616
3624 public function getAutomaticGroups( $recache = false ) {
3625 if ( $recache || is_null( $this->mImplicitGroups ) ) {
3626 $this->mImplicitGroups = [ '*' ];
3627 if ( $this->getId() ) {
3628 $this->mImplicitGroups[] = 'user';
3629
3630 $this->mImplicitGroups = array_unique( array_merge(
3631 $this->mImplicitGroups,
3633 ) );
3634 }
3635 if ( $recache ) {
3636 // Assure data consistency with rights/groups,
3637 // as getEffectiveGroups() depends on this function
3638 $this->mEffectiveGroups = null;
3639 }
3640 }
3642 }
3643
3653 public function getFormerGroups() {
3654 $this->load();
3655
3656 if ( is_null( $this->mFormerGroups ) ) {
3657 $db = ( $this->queryFlagsUsed & self::READ_LATEST )
3658 ? wfGetDB( DB_MASTER )
3659 : wfGetDB( DB_REPLICA );
3660 $res = $db->select( 'user_former_groups',
3661 [ 'ufg_group' ],
3662 [ 'ufg_user' => $this->mId ],
3663 __METHOD__ );
3664 $this->mFormerGroups = [];
3665 foreach ( $res as $row ) {
3666 $this->mFormerGroups[] = $row->ufg_group;
3667 }
3668 }
3669
3670 return $this->mFormerGroups;
3671 }
3672
3677 public function getEditCount() {
3678 if ( !$this->getId() ) {
3679 return null;
3680 }
3681
3682 if ( $this->mEditCount === null ) {
3683 /* Populate the count, if it has not been populated yet */
3684 $dbr = wfGetDB( DB_REPLICA );
3685 // check if the user_editcount field has been initialized
3686 $count = $dbr->selectField(
3687 'user', 'user_editcount',
3688 [ 'user_id' => $this->mId ],
3689 __METHOD__
3690 );
3691
3692 if ( $count === null ) {
3693 // it has not been initialized. do so.
3694 $count = $this->initEditCount();
3695 }
3696 $this->mEditCount = $count;
3697 }
3698 return (int)$this->mEditCount;
3699 }
3700
3712 public function addGroup( $group, $expiry = null ) {
3713 $this->load();
3714 $this->loadGroups();
3715
3716 if ( $expiry ) {
3717 $expiry = wfTimestamp( TS_MW, $expiry );
3718 }
3719
3720 if ( !Hooks::run( 'UserAddGroup', [ $this, &$group, &$expiry ] ) ) {
3721 return false;
3722 }
3723
3724 // create the new UserGroupMembership and put it in the DB
3725 $ugm = new UserGroupMembership( $this->mId, $group, $expiry );
3726 if ( !$ugm->insert( true ) ) {
3727 return false;
3728 }
3729
3730 $this->mGroupMemberships[$group] = $ugm;
3731
3732 // Refresh the groups caches, and clear the rights cache so it will be
3733 // refreshed on the next call to $this->getRights().
3734 $this->getEffectiveGroups( true );
3735 $this->mRights = null;
3736
3737 $this->invalidateCache();
3738
3739 return true;
3740 }
3741
3748 public function removeGroup( $group ) {
3749 $this->load();
3750
3751 if ( !Hooks::run( 'UserRemoveGroup', [ $this, &$group ] ) ) {
3752 return false;
3753 }
3754
3755 $ugm = UserGroupMembership::getMembership( $this->mId, $group );
3756 // delete the membership entry
3757 if ( !$ugm || !$ugm->delete() ) {
3758 return false;
3759 }
3760
3761 $this->loadGroups();
3762 unset( $this->mGroupMemberships[$group] );
3763
3764 // Refresh the groups caches, and clear the rights cache so it will be
3765 // refreshed on the next call to $this->getRights().
3766 $this->getEffectiveGroups( true );
3767 $this->mRights = null;
3768
3769 $this->invalidateCache();
3770
3771 return true;
3772 }
3773
3782 public function isRegistered() {
3783 return $this->getId() != 0;
3784 }
3785
3792 public function isLoggedIn() {
3793 return $this->isRegistered();
3794 }
3795
3800 public function isAnon() {
3801 return !$this->isLoggedIn();
3802 }
3803
3808 public function isBot() {
3809 if ( in_array( 'bot', $this->getGroups() ) && $this->isAllowed( 'bot' ) ) {
3810 return true;
3811 }
3812
3813 $isBot = false;
3814 Hooks::run( "UserIsBot", [ $this, &$isBot ] );
3815
3816 return $isBot;
3817 }
3818
3825 public function isAllowedAny() {
3826 $permissions = func_get_args();
3827 foreach ( $permissions as $permission ) {
3828 if ( $this->isAllowed( $permission ) ) {
3829 return true;
3830 }
3831 }
3832 return false;
3833 }
3834
3840 public function isAllowedAll() {
3841 $permissions = func_get_args();
3842 foreach ( $permissions as $permission ) {
3843 if ( !$this->isAllowed( $permission ) ) {
3844 return false;
3845 }
3846 }
3847 return true;
3848 }
3849
3855 public function isAllowed( $action = '' ) {
3856 if ( $action === '' ) {
3857 return true; // In the spirit of DWIM
3858 }
3859 // Use strict parameter to avoid matching numeric 0 accidentally inserted
3860 // by misconfiguration: 0 == 'foo'
3861 return in_array( $action, $this->getRights(), true );
3862 }
3863
3868 public function useRCPatrol() {
3870 return $wgUseRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' );
3871 }
3872
3877 public function useNPPatrol() {
3879 return (
3881 && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
3882 );
3883 }
3884
3889 public function useFilePatrol() {
3891 return (
3893 && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
3894 );
3895 }
3896
3902 public function getRequest() {
3903 if ( $this->mRequest ) {
3904 return $this->mRequest;
3905 } else {
3907 return $wgRequest;
3908 }
3909 }
3910
3919 public function isWatched( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
3920 if ( $title->isWatchable() && ( !$checkRights || $this->isAllowed( 'viewmywatchlist' ) ) ) {
3921 return MediaWikiServices::getInstance()->getWatchedItemStore()->isWatched( $this, $title );
3922 }
3923 return false;
3924 }
3925
3933 public function addWatch( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
3934 if ( !$checkRights || $this->isAllowed( 'editmywatchlist' ) ) {
3935 MediaWikiServices::getInstance()->getWatchedItemStore()->addWatchBatchForUser(
3936 $this,
3937 [ $title->getSubjectPage(), $title->getTalkPage() ]
3938 );
3939 }
3940 $this->invalidateCache();
3941 }
3942
3950 public function removeWatch( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
3951 if ( !$checkRights || $this->isAllowed( 'editmywatchlist' ) ) {
3952 $store = MediaWikiServices::getInstance()->getWatchedItemStore();
3953 $store->removeWatch( $this, $title->getSubjectPage() );
3954 $store->removeWatch( $this, $title->getTalkPage() );
3955 }
3956 $this->invalidateCache();
3957 }
3958
3967 public function clearNotification( &$title, $oldid = 0 ) {
3969
3970 // Do nothing if the database is locked to writes
3971 if ( wfReadOnly() ) {
3972 return;
3973 }
3974
3975 // Do nothing if not allowed to edit the watchlist
3976 if ( !$this->isAllowed( 'editmywatchlist' ) ) {
3977 return;
3978 }
3979
3980 // If we're working on user's talk page, we should update the talk page message indicator
3981 if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
3982 // Avoid PHP 7.1 warning of passing $this by reference
3983 $user = $this;
3984 if ( !Hooks::run( 'UserClearNewTalkNotification', [ &$user, $oldid ] ) ) {
3985 return;
3986 }
3987
3988 // Try to update the DB post-send and only if needed...
3989 DeferredUpdates::addCallableUpdate( function () use ( $title, $oldid ) {
3990 if ( !$this->getNewtalk() ) {
3991 return; // no notifications to clear
3992 }
3993
3994 // Delete the last notifications (they stack up)
3995 $this->setNewtalk( false );
3996
3997 // If there is a new, unseen, revision, use its timestamp
3998 $nextid = $oldid
3999 ? $title->getNextRevisionID( $oldid, Title::GAID_FOR_UPDATE )
4000 : null;
4001 if ( $nextid ) {
4002 $this->setNewtalk( true, Revision::newFromId( $nextid ) );
4003 }
4004 } );
4005 }
4006
4008 return;
4009 }
4010
4011 if ( $this->isAnon() ) {
4012 // Nothing else to do...
4013 return;
4014 }
4015
4016 // Only update the timestamp if the page is being watched.
4017 // The query to find out if it is watched is cached both in memcached and per-invocation,
4018 // and when it does have to be executed, it can be on a replica DB
4019 // If this is the user's newtalk page, we always update the timestamp
4020 $force = '';
4021 if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
4022 $force = 'force';
4023 }
4024
4025 MediaWikiServices::getInstance()->getWatchedItemStore()
4026 ->resetNotificationTimestamp( $this, $title, $force, $oldid );
4027 }
4028
4035 public function clearAllNotifications() {
4037 // Do nothing if not allowed to edit the watchlist
4038 if ( wfReadOnly() || !$this->isAllowed( 'editmywatchlist' ) ) {
4039 return;
4040 }
4041
4042 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
4043 $this->setNewtalk( false );
4044 return;
4045 }
4046
4047 $id = $this->getId();
4048 if ( !$id ) {
4049 return;
4050 }
4051
4052 $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
4053 $watchedItemStore->resetAllNotificationTimestampsForUser( $this );
4054
4055 // We also need to clear here the "you have new message" notification for the own
4056 // user_talk page; it's cleared one page view later in WikiPage::doViewUpdates().
4057 }
4058
4064 public function getExperienceLevel() {
4069
4070 if ( $this->isAnon() ) {
4071 return false;
4072 }
4073
4074 $editCount = $this->getEditCount();
4075 $registration = $this->getRegistration();
4076 $now = time();
4077 $learnerRegistration = wfTimestamp( TS_MW, $now - $wgLearnerMemberSince * 86400 );
4078 $experiencedRegistration = wfTimestamp( TS_MW, $now - $wgExperiencedUserMemberSince * 86400 );
4079
4080 if (
4081 $editCount < $wgLearnerEdits ||
4082 $registration > $learnerRegistration
4083 ) {
4084 return 'newcomer';
4085 } elseif (
4086 $editCount > $wgExperiencedUserEdits &&
4087 $registration <= $experiencedRegistration
4088 ) {
4089 return 'experienced';
4090 } else {
4091 return 'learner';
4092 }
4093 }
4094
4103 public function setCookies( $request = null, $secure = null, $rememberMe = false ) {
4104 $this->load();
4105 if ( 0 == $this->mId ) {
4106 return;
4107 }
4108
4109 $session = $this->getRequest()->getSession();
4110 if ( $request && $session->getRequest() !== $request ) {
4111 $session = $session->sessionWithRequest( $request );
4112 }
4113 $delay = $session->delaySave();
4114
4115 if ( !$session->getUser()->equals( $this ) ) {
4116 if ( !$session->canSetUser() ) {
4117 \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
4118 ->warning( __METHOD__ .
4119 ": Cannot save user \"$this\" to a user \"{$session->getUser()}\"'s immutable session"
4120 );
4121 return;
4122 }
4123 $session->setUser( $this );
4124 }
4125
4126 $session->setRememberUser( $rememberMe );
4127 if ( $secure !== null ) {
4128 $session->setForceHTTPS( $secure );
4129 }
4130
4131 $session->persist();
4132
4133 ScopedCallback::consume( $delay );
4134 }
4135
4139 public function logout() {
4140 // Avoid PHP 7.1 warning of passing $this by reference
4141 $user = $this;
4142 if ( Hooks::run( 'UserLogout', [ &$user ] ) ) {
4143 $this->doLogout();
4144 }
4145 }
4146
4151 public function doLogout() {
4152 $session = $this->getRequest()->getSession();
4153 if ( !$session->canSetUser() ) {
4154 \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
4155 ->warning( __METHOD__ . ": Cannot log out of an immutable session" );
4156 $error = 'immutable';
4157 } elseif ( !$session->getUser()->equals( $this ) ) {
4158 \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
4159 ->warning( __METHOD__ .
4160 ": Cannot log user \"$this\" out of a user \"{$session->getUser()}\"'s session"
4161 );
4162 // But we still may as well make this user object anon
4163 $this->clearInstanceCache( 'defaults' );
4164 $error = 'wronguser';
4165 } else {
4166 $this->clearInstanceCache( 'defaults' );
4167 $delay = $session->delaySave();
4168 $session->unpersist(); // Clear cookies (T127436)
4169 $session->setLoggedOutTimestamp( time() );
4170 $session->setUser( new User );
4171 $session->set( 'wsUserID', 0 ); // Other code expects this
4172 $session->resetAllTokens();
4173 ScopedCallback::consume( $delay );
4174 $error = false;
4175 }
4176 \MediaWiki\Logger\LoggerFactory::getInstance( 'authevents' )->info( 'Logout', [
4177 'event' => 'logout',
4178 'successful' => $error === false,
4179 'status' => $error ?: 'success',
4180 ] );
4181 }
4182
4187 public function saveSettings() {
4188 if ( wfReadOnly() ) {
4189 // @TODO: caller should deal with this instead!
4190 // This should really just be an exception.
4191 MWExceptionHandler::logException( new DBExpectedError(
4192 null,
4193 "Could not update user with ID '{$this->mId}'; DB is read-only."
4194 ) );
4195 return;
4196 }
4197
4198 $this->load();
4199 if ( 0 == $this->mId ) {
4200 return; // anon
4201 }
4202
4203 // Get a new user_touched that is higher than the old one.
4204 // This will be used for a CAS check as a last-resort safety
4205 // check against race conditions and replica DB lag.
4206 $newTouched = $this->newTouchedTimestamp();
4207
4208 $dbw = wfGetDB( DB_MASTER );
4209 $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $newTouched ) {
4211
4212 $dbw->update( 'user',
4213 [ /* SET */
4214 'user_name' => $this->mName,
4215 'user_real_name' => $this->mRealName,
4216 'user_email' => $this->mEmail,
4217 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
4218 'user_touched' => $dbw->timestamp( $newTouched ),
4219 'user_token' => strval( $this->mToken ),
4220 'user_email_token' => $this->mEmailToken,
4221 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
4222 ], $this->makeUpdateConditions( $dbw, [ /* WHERE */
4223 'user_id' => $this->mId,
4224 ] ), $fname
4225 );
4226
4227 if ( !$dbw->affectedRows() ) {
4228 // Maybe the problem was a missed cache update; clear it to be safe
4229 $this->clearSharedCache( 'refresh' );
4230 // User was changed in the meantime or loaded with stale data
4231 $from = ( $this->queryFlagsUsed & self::READ_LATEST ) ? 'master' : 'replica';
4232 throw new MWException(
4233 "CAS update failed on user_touched for user ID '{$this->mId}' (read from $from);" .
4234 " the version of the user to be saved is older than the current version."
4235 );
4236 }
4237
4239 $dbw->update(
4240 'actor',
4241 [ 'actor_name' => $this->mName ],
4242 [ 'actor_user' => $this->mId ],
4243 $fname
4244 );
4245 }
4246 } );
4247
4248 $this->mTouched = $newTouched;
4249 $this->saveOptions();
4250
4251 Hooks::run( 'UserSaveSettings', [ $this ] );
4252 $this->clearSharedCache();
4253 $this->getUserPage()->invalidateCache();
4254 }
4255
4262 public function idForName( $flags = 0 ) {
4263 $s = trim( $this->getName() );
4264 if ( $s === '' ) {
4265 return 0;
4266 }
4267
4268 $db = ( ( $flags & self::READ_LATEST ) == self::READ_LATEST )
4269 ? wfGetDB( DB_MASTER )
4270 : wfGetDB( DB_REPLICA );
4271
4272 $options = ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING )
4273 ? [ 'LOCK IN SHARE MODE' ]
4274 : [];
4275
4276 $id = $db->selectField( 'user',
4277 'user_id', [ 'user_name' => $s ], __METHOD__, $options );
4278
4279 return (int)$id;
4280 }
4281
4297 public static function createNew( $name, $params = [] ) {
4298 foreach ( [ 'password', 'newpassword', 'newpass_time', 'password_expires' ] as $field ) {
4299 if ( isset( $params[$field] ) ) {
4300 wfDeprecated( __METHOD__ . " with param '$field'", '1.27' );
4301 unset( $params[$field] );
4302 }
4303 }
4304
4305 $user = new User;
4306 $user->load();
4307 $user->setToken(); // init token
4308 if ( isset( $params['options'] ) ) {
4309 $user->mOptions = $params['options'] + (array)$user->mOptions;
4310 unset( $params['options'] );
4311 }
4312 $dbw = wfGetDB( DB_MASTER );
4313
4314 $noPass = PasswordFactory::newInvalidPassword()->toString();
4315
4316 $fields = [
4317 'user_name' => $name,
4318 'user_password' => $noPass,
4319 'user_newpassword' => $noPass,
4320 'user_email' => $user->mEmail,
4321 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
4322 'user_real_name' => $user->mRealName,
4323 'user_token' => strval( $user->mToken ),
4324 'user_registration' => $dbw->timestamp( $user->mRegistration ),
4325 'user_editcount' => 0,
4326 'user_touched' => $dbw->timestamp( $user->newTouchedTimestamp() ),
4327 ];
4328 foreach ( $params as $name => $value ) {
4329 $fields["user_$name"] = $value;
4330 }
4331
4332 return $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $fields ) {
4333 $dbw->insert( 'user', $fields, $fname, [ 'IGNORE' ] );
4334 if ( $dbw->affectedRows() ) {
4335 $newUser = self::newFromId( $dbw->insertId() );
4336 // Load the user from master to avoid replica lag
4337 $newUser->load( self::READ_LATEST );
4338 $newUser->updateActorId( $dbw );
4339 } else {
4340 $newUser = null;
4341 }
4342 return $newUser;
4343 } );
4344 }
4345
4372 public function addToDatabase() {
4373 $this->load();
4374 if ( !$this->mToken ) {
4375 $this->setToken(); // init token
4376 }
4377
4378 if ( !is_string( $this->mName ) ) {
4379 throw new RuntimeException( "User name field is not set." );
4380 }
4381
4382 $this->mTouched = $this->newTouchedTimestamp();
4383
4384 $dbw = wfGetDB( DB_MASTER );
4385 $status = $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) {
4386 $noPass = PasswordFactory::newInvalidPassword()->toString();
4387 $dbw->insert( 'user',
4388 [
4389 'user_name' => $this->mName,
4390 'user_password' => $noPass,
4391 'user_newpassword' => $noPass,
4392 'user_email' => $this->mEmail,
4393 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
4394 'user_real_name' => $this->mRealName,
4395 'user_token' => strval( $this->mToken ),
4396 'user_registration' => $dbw->timestamp( $this->mRegistration ),
4397 'user_editcount' => 0,
4398 'user_touched' => $dbw->timestamp( $this->mTouched ),
4399 ], $fname,
4400 [ 'IGNORE' ]
4401 );
4402 if ( !$dbw->affectedRows() ) {
4403 // Use locking reads to bypass any REPEATABLE-READ snapshot.
4404 $this->mId = $dbw->selectField(
4405 'user',
4406 'user_id',
4407 [ 'user_name' => $this->mName ],
4408 __METHOD__,
4409 [ 'LOCK IN SHARE MODE' ]
4410 );
4411 $loaded = false;
4412 if ( $this->mId ) {
4413 if ( $this->loadFromDatabase( self::READ_LOCKING ) ) {
4414 $loaded = true;
4415 }
4416 }
4417 if ( !$loaded ) {
4418 throw new MWException( __METHOD__ . ": hit a key conflict attempting " .
4419 "to insert user '{$this->mName}' row, but it was not present in select!" );
4420 }
4421 return Status::newFatal( 'userexists' );
4422 }
4423 $this->mId = $dbw->insertId();
4424 self::$idCacheByName[$this->mName] = $this->mId;
4425 $this->updateActorId( $dbw );
4426
4427 return Status::newGood();
4428 } );
4429 if ( !$status->isGood() ) {
4430 return $status;
4431 }
4432
4433 // Clear instance cache other than user table data and actor, which is already accurate
4434 $this->clearInstanceCache();
4435
4436 $this->saveOptions();
4437 return Status::newGood();
4438 }
4439
4444 private function updateActorId( IDatabase $dbw ) {
4446
4448 $dbw->insert(
4449 'actor',
4450 [ 'actor_user' => $this->mId, 'actor_name' => $this->mName ],
4451 __METHOD__
4452 );
4453 $this->mActorId = (int)$dbw->insertId();
4454 }
4455 }
4456
4462 public function spreadAnyEditBlock() {
4463 if ( $this->isLoggedIn() && $this->isBlocked() ) {
4464 return $this->spreadBlock();
4465 }
4466
4467 return false;
4468 }
4469
4475 protected function spreadBlock() {
4476 wfDebug( __METHOD__ . "()\n" );
4477 $this->load();
4478 if ( $this->mId == 0 ) {
4479 return false;
4480 }
4481
4482 $userblock = Block::newFromTarget( $this->getName() );
4483 if ( !$userblock ) {
4484 return false;
4485 }
4486
4487 return (bool)$userblock->doAutoblock( $this->getRequest()->getIP() );
4488 }
4489
4494 public function isBlockedFromCreateAccount() {
4495 $this->getBlockedStatus();
4496 if ( $this->mBlock && $this->mBlock->prevents( 'createaccount' ) ) {
4497 return $this->mBlock;
4498 }
4499
4500 # T15611: if the IP address the user is trying to create an account from is
4501 # blocked with createaccount disabled, prevent new account creation there even
4502 # when the user is logged in
4503 if ( $this->mBlockedFromCreateAccount === false && !$this->isAllowed( 'ipblock-exempt' ) ) {
4504 $this->mBlockedFromCreateAccount = Block::newFromTarget( null, $this->getRequest()->getIP() );
4505 }
4506 return $this->mBlockedFromCreateAccount instanceof Block
4507 && $this->mBlockedFromCreateAccount->prevents( 'createaccount' )
4508 ? $this->mBlockedFromCreateAccount
4509 : false;
4510 }
4511
4516 public function isBlockedFromEmailuser() {
4517 $this->getBlockedStatus();
4518 return $this->mBlock && $this->mBlock->prevents( 'sendemail' );
4519 }
4520
4525 public function isAllowedToCreateAccount() {
4526 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
4527 }
4528
4534 public function getUserPage() {
4535 return Title::makeTitle( NS_USER, $this->getName() );
4536 }
4537
4543 public function getTalkPage() {
4544 $title = $this->getUserPage();
4545 return $title->getTalkPage();
4546 }
4547
4553 public function isNewbie() {
4554 return !$this->isAllowed( 'autoconfirmed' );
4555 }
4556
4563 public function checkPassword( $password ) {
4564 $manager = AuthManager::singleton();
4565 $reqs = AuthenticationRequest::loadRequestsFromSubmission(
4566 $manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN ),
4567 [
4568 'username' => $this->getName(),
4569 'password' => $password,
4570 ]
4571 );
4572 $res = AuthManager::singleton()->beginAuthentication( $reqs, 'null:' );
4573 switch ( $res->status ) {
4574 case AuthenticationResponse::PASS:
4575 return true;
4576 case AuthenticationResponse::FAIL:
4577 // Hope it's not a PreAuthenticationProvider that failed...
4578 \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
4579 ->info( __METHOD__ . ': Authentication failed: ' . $res->message->plain() );
4580 return false;
4581 default:
4582 throw new BadMethodCallException(
4583 'AuthManager returned a response unsupported by ' . __METHOD__
4584 );
4585 }
4586 }
4587
4596 public function checkTemporaryPassword( $plaintext ) {
4597 // Can't check the temporary password individually.
4598 return $this->checkPassword( $plaintext );
4599 }
4600
4612 public function getEditTokenObject( $salt = '', $request = null ) {
4613 if ( $this->isAnon() ) {
4614 return new LoggedOutEditToken();
4615 }
4616
4617 if ( !$request ) {
4618 $request = $this->getRequest();
4619 }
4620 return $request->getSession()->getToken( $salt );
4621 }
4622
4636 public function getEditToken( $salt = '', $request = null ) {
4637 return $this->getEditTokenObject( $salt, $request )->toString();
4638 }
4639
4652 public function matchEditToken( $val, $salt = '', $request = null, $maxage = null ) {
4653 return $this->getEditTokenObject( $salt, $request )->match( $val, $maxage );
4654 }
4655
4666 public function matchEditTokenNoSuffix( $val, $salt = '', $request = null, $maxage = null ) {
4667 $val = substr( $val, 0, strspn( $val, '0123456789abcdef' ) ) . Token::SUFFIX;
4668 return $this->matchEditToken( $val, $salt, $request, $maxage );
4669 }
4670
4678 public function sendConfirmationMail( $type = 'created' ) {
4680 $expiration = null; // gets passed-by-ref and defined in next line.
4681 $token = $this->confirmationToken( $expiration );
4682 $url = $this->confirmationTokenUrl( $token );
4683 $invalidateURL = $this->invalidationTokenUrl( $token );
4684 $this->saveSettings();
4685
4686 if ( $type == 'created' || $type === false ) {
4687 $message = 'confirmemail_body';
4688 } elseif ( $type === true ) {
4689 $message = 'confirmemail_body_changed';
4690 } else {
4691 // Messages: confirmemail_body_changed, confirmemail_body_set
4692 $message = 'confirmemail_body_' . $type;
4693 }
4694
4695 return $this->sendMail( wfMessage( 'confirmemail_subject' )->text(),
4696 wfMessage( $message,
4697 $this->getRequest()->getIP(),
4698 $this->getName(),
4699 $url,
4700 $wgLang->userTimeAndDate( $expiration, $this ),
4701 $invalidateURL,
4702 $wgLang->userDate( $expiration, $this ),
4703 $wgLang->userTime( $expiration, $this ) )->text() );
4704 }
4705
4717 public function sendMail( $subject, $body, $from = null, $replyto = null ) {
4719
4720 if ( $from instanceof User ) {
4721 $sender = MailAddress::newFromUser( $from );
4722 } else {
4723 $sender = new MailAddress( $wgPasswordSender,
4724 wfMessage( 'emailsender' )->inContentLanguage()->text() );
4725 }
4726 $to = MailAddress::newFromUser( $this );
4727
4728 return UserMailer::send( $to, $sender, $subject, $body, [
4729 'replyTo' => $replyto,
4730 ] );
4731 }
4732
4743 protected function confirmationToken( &$expiration ) {
4745 $now = time();
4746 $expires = $now + $wgUserEmailConfirmationTokenExpiry;
4747 $expiration = wfTimestamp( TS_MW, $expires );
4748 $this->load();
4749 $token = MWCryptRand::generateHex( 32 );
4750 $hash = md5( $token );
4751 $this->mEmailToken = $hash;
4752 $this->mEmailTokenExpires = $expiration;
4753 return $token;
4754 }
4755
4761 protected function confirmationTokenUrl( $token ) {
4762 return $this->getTokenUrl( 'ConfirmEmail', $token );
4763 }
4764
4770 protected function invalidationTokenUrl( $token ) {
4771 return $this->getTokenUrl( 'InvalidateEmail', $token );
4772 }
4773
4788 protected function getTokenUrl( $page, $token ) {
4789 // Hack to bypass localization of 'Special:'
4790 $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" );
4791 return $title->getCanonicalURL();
4792 }
4793
4801 public function confirmEmail() {
4802 // Check if it's already confirmed, so we don't touch the database
4803 // and fire the ConfirmEmailComplete hook on redundant confirmations.
4804 if ( !$this->isEmailConfirmed() ) {
4806 Hooks::run( 'ConfirmEmailComplete', [ $this ] );
4807 }
4808 return true;
4809 }
4810
4818 public function invalidateEmail() {
4819 $this->load();
4820 $this->mEmailToken = null;
4821 $this->mEmailTokenExpires = null;
4822 $this->setEmailAuthenticationTimestamp( null );
4823 $this->mEmail = '';
4824 Hooks::run( 'InvalidateEmailComplete', [ $this ] );
4825 return true;
4826 }
4827
4832 public function setEmailAuthenticationTimestamp( $timestamp ) {
4833 $this->load();
4834 $this->mEmailAuthenticated = $timestamp;
4835 Hooks::run( 'UserSetEmailAuthenticationTimestamp', [ $this, &$this->mEmailAuthenticated ] );
4836 }
4837
4843 public function canSendEmail() {
4845 if ( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
4846 return false;
4847 }
4848 $canSend = $this->isEmailConfirmed();
4849 // Avoid PHP 7.1 warning of passing $this by reference
4850 $user = $this;
4851 Hooks::run( 'UserCanSendEmail', [ &$user, &$canSend ] );
4852 return $canSend;
4853 }
4854
4860 public function canReceiveEmail() {
4861 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
4862 }
4863
4874 public function isEmailConfirmed() {
4876 $this->load();
4877 // Avoid PHP 7.1 warning of passing $this by reference
4878 $user = $this;
4879 $confirmed = true;
4880 if ( Hooks::run( 'EmailConfirmed', [ &$user, &$confirmed ] ) ) {
4881 if ( $this->isAnon() ) {
4882 return false;
4883 }
4884 if ( !Sanitizer::validateEmail( $this->mEmail ) ) {
4885 return false;
4886 }
4888 return false;
4889 }
4890 return true;
4891 } else {
4892 return $confirmed;
4893 }
4894 }
4895
4900 public function isEmailConfirmationPending() {
4902 return $wgEmailAuthentication &&
4903 !$this->isEmailConfirmed() &&
4904 $this->mEmailToken &&
4905 $this->mEmailTokenExpires > wfTimestamp();
4906 }
4907
4915 public function getRegistration() {
4916 if ( $this->isAnon() ) {
4917 return false;
4918 }
4919 $this->load();
4920 return $this->mRegistration;
4921 }
4922
4929 public function getFirstEditTimestamp() {
4930 if ( $this->getId() == 0 ) {
4931 return false; // anons
4932 }
4933 $dbr = wfGetDB( DB_REPLICA );
4934 $actorWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $this );
4935 $time = $dbr->selectField(
4936 [ 'revision' ] + $actorWhere['tables'],
4937 'rev_timestamp',
4938 [ $actorWhere['conds'] ],
4939 __METHOD__,
4940 [ 'ORDER BY' => 'rev_timestamp ASC' ],
4941 $actorWhere['joins']
4942 );
4943 if ( !$time ) {
4944 return false; // no edits
4945 }
4946 return wfTimestamp( TS_MW, $time );
4947 }
4948
4955 public static function getGroupPermissions( $groups ) {
4957 $rights = [];
4958 // grant every granted permission first
4959 foreach ( $groups as $group ) {
4960 if ( isset( $wgGroupPermissions[$group] ) ) {
4961 $rights = array_merge( $rights,
4962 // array_filter removes empty items
4963 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
4964 }
4965 }
4966 // now revoke the revoked permissions
4967 foreach ( $groups as $group ) {
4968 if ( isset( $wgRevokePermissions[$group] ) ) {
4969 $rights = array_diff( $rights,
4970 array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
4971 }
4972 }
4973 return array_unique( $rights );
4974 }
4975
4982 public static function getGroupsWithPermission( $role ) {
4984 $allowedGroups = [];
4985 foreach ( array_keys( $wgGroupPermissions ) as $group ) {
4986 if ( self::groupHasPermission( $group, $role ) ) {
4987 $allowedGroups[] = $group;
4988 }
4989 }
4990 return $allowedGroups;
4991 }
4992
5005 public static function groupHasPermission( $group, $role ) {
5007 return isset( $wgGroupPermissions[$group][$role] ) && $wgGroupPermissions[$group][$role]
5008 && !( isset( $wgRevokePermissions[$group][$role] ) && $wgRevokePermissions[$group][$role] );
5009 }
5010
5025 public static function isEveryoneAllowed( $right ) {
5027 static $cache = [];
5028
5029 // Use the cached results, except in unit tests which rely on
5030 // being able change the permission mid-request
5031 if ( isset( $cache[$right] ) && !defined( 'MW_PHPUNIT_TEST' ) ) {
5032 return $cache[$right];
5033 }
5034
5035 if ( !isset( $wgGroupPermissions['*'][$right] ) || !$wgGroupPermissions['*'][$right] ) {
5036 $cache[$right] = false;
5037 return false;
5038 }
5039
5040 // If it's revoked anywhere, then everyone doesn't have it
5041 foreach ( $wgRevokePermissions as $rights ) {
5042 if ( isset( $rights[$right] ) && $rights[$right] ) {
5043 $cache[$right] = false;
5044 return false;
5045 }
5046 }
5047
5048 // Remove any rights that aren't allowed to the global-session user,
5049 // unless there are no sessions for this endpoint.
5050 if ( !defined( 'MW_NO_SESSION' ) ) {
5051 $allowedRights = SessionManager::getGlobalSession()->getAllowedUserRights();
5052 if ( $allowedRights !== null && !in_array( $right, $allowedRights, true ) ) {
5053 $cache[$right] = false;
5054 return false;
5055 }
5056 }
5057
5058 // Allow extensions to say false
5059 if ( !Hooks::run( 'UserIsEveryoneAllowed', [ $right ] ) ) {
5060 $cache[$right] = false;
5061 return false;
5062 }
5063
5064 $cache[$right] = true;
5065 return true;
5066 }
5067
5075 public static function getGroupName( $group ) {
5076 wfDeprecated( __METHOD__, '1.29' );
5077 return UserGroupMembership::getGroupName( $group );
5078 }
5079
5088 public static function getGroupMember( $group, $username = '#' ) {
5089 wfDeprecated( __METHOD__, '1.29' );
5090 return UserGroupMembership::getGroupMemberName( $group, $username );
5091 }
5092
5099 public static function getAllGroups() {
5101 return array_diff(
5102 array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
5103 self::getImplicitGroups()
5104 );
5105 }
5106
5111 public static function getAllRights() {
5112 if ( self::$mAllRights === false ) {
5114 if ( count( $wgAvailableRights ) ) {
5115 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
5116 } else {
5117 self::$mAllRights = self::$mCoreRights;
5118 }
5119 Hooks::run( 'UserGetAllRights', [ &self::$mAllRights ] );
5120 }
5121 return self::$mAllRights;
5122 }
5123
5128 public static function getImplicitGroups() {
5130
5131 $groups = $wgImplicitGroups;
5132 # Deprecated, use $wgImplicitGroups instead
5133 Hooks::run( 'UserGetImplicitGroups', [ &$groups ], '1.25' );
5134
5135 return $groups;
5136 }
5137
5145 public static function getGroupPage( $group ) {
5146 wfDeprecated( __METHOD__, '1.29' );
5147 return UserGroupMembership::getGroupPage( $group );
5148 }
5149
5160 public static function makeGroupLinkHTML( $group, $text = '' ) {
5161 wfDeprecated( __METHOD__, '1.29' );
5162
5163 if ( $text == '' ) {
5164 $text = UserGroupMembership::getGroupName( $group );
5165 }
5166 $title = UserGroupMembership::getGroupPage( $group );
5167 if ( $title ) {
5168 return MediaWikiServices::getInstance()
5169 ->getLinkRenderer()->makeLink( $title, $text );
5170 } else {
5171 return htmlspecialchars( $text );
5172 }
5173 }
5174
5185 public static function makeGroupLinkWiki( $group, $text = '' ) {
5186 wfDeprecated( __METHOD__, '1.29' );
5187
5188 if ( $text == '' ) {
5189 $text = UserGroupMembership::getGroupName( $group );
5190 }
5191 $title = UserGroupMembership::getGroupPage( $group );
5192 if ( $title ) {
5193 $page = $title->getFullText();
5194 return "[[$page|$text]]";
5195 } else {
5196 return $text;
5197 }
5198 }
5199
5209 public static function changeableByGroup( $group ) {
5211
5212 $groups = [
5213 'add' => [],
5214 'remove' => [],
5215 'add-self' => [],
5216 'remove-self' => []
5217 ];
5218
5219 if ( empty( $wgAddGroups[$group] ) ) {
5220 // Don't add anything to $groups
5221 } elseif ( $wgAddGroups[$group] === true ) {
5222 // You get everything
5223 $groups['add'] = self::getAllGroups();
5224 } elseif ( is_array( $wgAddGroups[$group] ) ) {
5225 $groups['add'] = $wgAddGroups[$group];
5226 }
5227
5228 // Same thing for remove
5229 if ( empty( $wgRemoveGroups[$group] ) ) {
5230 // Do nothing
5231 } elseif ( $wgRemoveGroups[$group] === true ) {
5232 $groups['remove'] = self::getAllGroups();
5233 } elseif ( is_array( $wgRemoveGroups[$group] ) ) {
5234 $groups['remove'] = $wgRemoveGroups[$group];
5235 }
5236
5237 // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
5238 if ( empty( $wgGroupsAddToSelf['user'] ) || $wgGroupsAddToSelf['user'] !== true ) {
5239 foreach ( $wgGroupsAddToSelf as $key => $value ) {
5240 if ( is_int( $key ) ) {
5241 $wgGroupsAddToSelf['user'][] = $value;
5242 }
5243 }
5244 }
5245
5246 if ( empty( $wgGroupsRemoveFromSelf['user'] ) || $wgGroupsRemoveFromSelf['user'] !== true ) {
5247 foreach ( $wgGroupsRemoveFromSelf as $key => $value ) {
5248 if ( is_int( $key ) ) {
5249 $wgGroupsRemoveFromSelf['user'][] = $value;
5250 }
5251 }
5252 }
5253
5254 // Now figure out what groups the user can add to him/herself
5255 if ( empty( $wgGroupsAddToSelf[$group] ) ) {
5256 // Do nothing
5257 } elseif ( $wgGroupsAddToSelf[$group] === true ) {
5258 // No idea WHY this would be used, but it's there
5259 $groups['add-self'] = self::getAllGroups();
5260 } elseif ( is_array( $wgGroupsAddToSelf[$group] ) ) {
5261 $groups['add-self'] = $wgGroupsAddToSelf[$group];
5262 }
5263
5264 if ( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
5265 // Do nothing
5266 } elseif ( $wgGroupsRemoveFromSelf[$group] === true ) {
5267 $groups['remove-self'] = self::getAllGroups();
5268 } elseif ( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
5269 $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
5270 }
5271
5272 return $groups;
5273 }
5274
5282 public function changeableGroups() {
5283 if ( $this->isAllowed( 'userrights' ) ) {
5284 // This group gives the right to modify everything (reverse-
5285 // compatibility with old "userrights lets you change
5286 // everything")
5287 // Using array_merge to make the groups reindexed
5288 $all = array_merge( self::getAllGroups() );
5289 return [
5290 'add' => $all,
5291 'remove' => $all,
5292 'add-self' => [],
5293 'remove-self' => []
5294 ];
5295 }
5296
5297 // Okay, it's not so simple, we will have to go through the arrays
5298 $groups = [
5299 'add' => [],
5300 'remove' => [],
5301 'add-self' => [],
5302 'remove-self' => []
5303 ];
5304 $addergroups = $this->getEffectiveGroups();
5305
5306 foreach ( $addergroups as $addergroup ) {
5307 $groups = array_merge_recursive(
5308 $groups, $this->changeableByGroup( $addergroup )
5309 );
5310 $groups['add'] = array_unique( $groups['add'] );
5311 $groups['remove'] = array_unique( $groups['remove'] );
5312 $groups['add-self'] = array_unique( $groups['add-self'] );
5313 $groups['remove-self'] = array_unique( $groups['remove-self'] );
5314 }
5315 return $groups;
5316 }
5317
5324 public function incEditCount() {
5325 wfGetDB( DB_MASTER )->onTransactionPreCommitOrIdle(
5326 function () {
5327 $this->incEditCountImmediate();
5328 },
5329 __METHOD__
5330 );
5331 }
5332
5338 public function incEditCountImmediate() {
5339 if ( $this->isAnon() ) {
5340 return;
5341 }
5342
5343 $dbw = wfGetDB( DB_MASTER );
5344 // No rows will be "affected" if user_editcount is NULL
5345 $dbw->update(
5346 'user',
5347 [ 'user_editcount=user_editcount+1' ],
5348 [ 'user_id' => $this->getId(), 'user_editcount IS NOT NULL' ],
5349 __METHOD__
5350 );
5351 // Lazy initialization check...
5352 if ( $dbw->affectedRows() == 0 ) {
5353 // Now here's a goddamn hack...
5354 $dbr = wfGetDB( DB_REPLICA );
5355 if ( $dbr !== $dbw ) {
5356 // If we actually have a replica DB server, the count is
5357 // at least one behind because the current transaction
5358 // has not been committed and replicated.
5359 $this->mEditCount = $this->initEditCount( 1 );
5360 } else {
5361 // But if DB_REPLICA is selecting the master, then the
5362 // count we just read includes the revision that was
5363 // just added in the working transaction.
5364 $this->mEditCount = $this->initEditCount();
5365 }
5366 } else {
5367 if ( $this->mEditCount === null ) {
5368 $this->getEditCount();
5369 $dbr = wfGetDB( DB_REPLICA );
5370 $this->mEditCount += ( $dbr !== $dbw ) ? 1 : 0;
5371 } else {
5372 $this->mEditCount++;
5373 }
5374 }
5375 // Edit count in user cache too
5376 $this->invalidateCache();
5377 }
5378
5385 protected function initEditCount( $add = 0 ) {
5386 // Pull from a replica DB to be less cruel to servers
5387 // Accuracy isn't the point anyway here
5388 $dbr = wfGetDB( DB_REPLICA );
5389 $actorWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $this );
5390 $count = (int)$dbr->selectField(
5391 [ 'revision' ] + $actorWhere['tables'],
5392 'COUNT(*)',
5393 [ $actorWhere['conds'] ],
5394 __METHOD__,
5395 [],
5396 $actorWhere['joins']
5397 );
5398 $count = $count + $add;
5399
5400 $dbw = wfGetDB( DB_MASTER );
5401 $dbw->update(
5402 'user',
5403 [ 'user_editcount' => $count ],
5404 [ 'user_id' => $this->getId() ],
5405 __METHOD__
5406 );
5407
5408 return $count;
5409 }
5410
5418 public static function getRightDescription( $right ) {
5419 $key = "right-$right";
5420 $msg = wfMessage( $key );
5421 return $msg->isDisabled() ? $right : $msg->text();
5422 }
5423
5431 public static function getGrantName( $grant ) {
5432 $key = "grant-$grant";
5433 $msg = wfMessage( $key );
5434 return $msg->isDisabled() ? $grant : $msg->text();
5435 }
5436
5457 public function addNewUserLogEntry( $action = false, $reason = '' ) {
5458 return true; // disabled
5459 }
5460
5470 $this->addNewUserLogEntry( 'autocreate' );
5471
5472 return true;
5473 }
5474
5480 protected function loadOptions( $data = null ) {
5482
5483 $this->load();
5484
5485 if ( $this->mOptionsLoaded ) {
5486 return;
5487 }
5488
5489 $this->mOptions = self::getDefaultOptions();
5490
5491 if ( !$this->getId() ) {
5492 // For unlogged-in users, load language/variant options from request.
5493 // There's no need to do it for logged-in users: they can set preferences,
5494 // and handling of page content is done by $pageLang->getPreferredVariant() and such,
5495 // so don't override user's choice (especially when the user chooses site default).
5496 $variant = $wgContLang->getDefaultVariant();
5497 $this->mOptions['variant'] = $variant;
5498 $this->mOptions['language'] = $variant;
5499 $this->mOptionsLoaded = true;
5500 return;
5501 }
5502
5503 // Maybe load from the object
5504 if ( !is_null( $this->mOptionOverrides ) ) {
5505 wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" );
5506 foreach ( $this->mOptionOverrides as $key => $value ) {
5507 $this->mOptions[$key] = $value;
5508 }
5509 } else {
5510 if ( !is_array( $data ) ) {
5511 wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" );
5512 // Load from database
5513 $dbr = ( $this->queryFlagsUsed & self::READ_LATEST )
5514 ? wfGetDB( DB_MASTER )
5515 : wfGetDB( DB_REPLICA );
5516
5517 $res = $dbr->select(
5518 'user_properties',
5519 [ 'up_property', 'up_value' ],
5520 [ 'up_user' => $this->getId() ],
5521 __METHOD__
5522 );
5523
5524 $this->mOptionOverrides = [];
5525 $data = [];
5526 foreach ( $res as $row ) {
5527 // Convert '0' to 0. PHP's boolean conversion considers them both
5528 // false, but e.g. JavaScript considers the former as true.
5529 // @todo: T54542 Somehow determine the desired type (string/int/bool)
5530 // and convert all values here.
5531 if ( $row->up_value === '0' ) {
5532 $row->up_value = 0;
5533 }
5534 $data[$row->up_property] = $row->up_value;
5535 }
5536 }
5537
5538 // Convert the email blacklist from a new line delimited string
5539 // to an array of ids.
5540 if ( isset( $data['email-blacklist'] ) && $data['email-blacklist'] ) {
5541 $data['email-blacklist'] = array_map( 'intval', explode( "\n", $data['email-blacklist'] ) );
5542 }
5543
5544 foreach ( $data as $property => $value ) {
5545 $this->mOptionOverrides[$property] = $value;
5546 $this->mOptions[$property] = $value;
5547 }
5548 }
5549
5550 $this->mOptionsLoaded = true;
5551
5552 Hooks::run( 'UserLoadOptions', [ $this, &$this->mOptions ] );
5553 }
5554
5560 protected function saveOptions() {
5561 $this->loadOptions();
5562
5563 // Not using getOptions(), to keep hidden preferences in database
5564 $saveOptions = $this->mOptions;
5565
5566 // Convert usernames to ids.
5567 if ( isset( $this->mOptions['email-blacklist'] ) ) {
5568 if ( $this->mOptions['email-blacklist'] ) {
5569 $value = $this->mOptions['email-blacklist'];
5570 // Email Blacklist may be an array of ids or a string of new line
5571 // delimnated user names.
5572 if ( is_array( $value ) ) {
5573 $ids = array_filter( $value, 'is_numeric' );
5574 } else {
5575 $lookup = CentralIdLookup::factory();
5576 $ids = $lookup->centralIdsFromNames( explode( "\n", $value ), $this );
5577 }
5578 $this->mOptions['email-blacklist'] = $ids;
5579 $saveOptions['email-blacklist'] = implode( "\n", $this->mOptions['email-blacklist'] );
5580 } else {
5581 // If the blacklist is empty, set it to null rather than an empty string.
5582 $this->mOptions['email-blacklist'] = null;
5583 }
5584 }
5585
5586 // Allow hooks to abort, for instance to save to a global profile.
5587 // Reset options to default state before saving.
5588 if ( !Hooks::run( 'UserSaveOptions', [ $this, &$saveOptions ] ) ) {
5589 return;
5590 }
5591
5592 $userId = $this->getId();
5593
5594 $insert_rows = []; // all the new preference rows
5595 foreach ( $saveOptions as $key => $value ) {
5596 // Don't bother storing default values
5597 $defaultOption = self::getDefaultOption( $key );
5598 if ( ( $defaultOption === null && $value !== false && $value !== null )
5599 || $value != $defaultOption
5600 ) {
5601 $insert_rows[] = [
5602 'up_user' => $userId,
5603 'up_property' => $key,
5604 'up_value' => $value,
5605 ];
5606 }
5607 }
5608
5609 $dbw = wfGetDB( DB_MASTER );
5610
5611 $res = $dbw->select( 'user_properties',
5612 [ 'up_property', 'up_value' ], [ 'up_user' => $userId ], __METHOD__ );
5613
5614 // Find prior rows that need to be removed or updated. These rows will
5615 // all be deleted (the latter so that INSERT IGNORE applies the new values).
5616 $keysDelete = [];
5617 foreach ( $res as $row ) {
5618 if ( !isset( $saveOptions[$row->up_property] )
5619 || strcmp( $saveOptions[$row->up_property], $row->up_value ) != 0
5620 ) {
5621 $keysDelete[] = $row->up_property;
5622 }
5623 }
5624
5625 if ( count( $keysDelete ) ) {
5626 // Do the DELETE by PRIMARY KEY for prior rows.
5627 // In the past a very large portion of calls to this function are for setting
5628 // 'rememberpassword' for new accounts (a preference that has since been removed).
5629 // Doing a blanket per-user DELETE for new accounts with no rows in the table
5630 // caused gap locks on [max user ID,+infinity) which caused high contention since
5631 // updates would pile up on each other as they are for higher (newer) user IDs.
5632 // It might not be necessary these days, but it shouldn't hurt either.
5633 $dbw->delete( 'user_properties',
5634 [ 'up_user' => $userId, 'up_property' => $keysDelete ], __METHOD__ );
5635 }
5636 // Insert the new preference rows
5637 $dbw->insert( 'user_properties', $insert_rows, __METHOD__, [ 'IGNORE' ] );
5638 }
5639
5646 public static function selectFields() {
5647 wfDeprecated( __METHOD__, '1.31' );
5648 return [
5649 'user_id',
5650 'user_name',
5651 'user_real_name',
5652 'user_email',
5653 'user_touched',
5654 'user_token',
5655 'user_email_authenticated',
5656 'user_email_token',
5657 'user_email_token_expires',
5658 'user_registration',
5659 'user_editcount',
5660 ];
5661 }
5662
5672 public static function getQueryInfo() {
5674
5675 $ret = [
5676 'tables' => [ 'user' ],
5677 'fields' => [
5678 'user_id',
5679 'user_name',
5680 'user_real_name',
5681 'user_email',
5682 'user_touched',
5683 'user_token',
5684 'user_email_authenticated',
5685 'user_email_token',
5686 'user_email_token_expires',
5687 'user_registration',
5688 'user_editcount',
5689 ],
5690 'joins' => [],
5691 ];
5693 $ret['tables']['user_actor'] = 'actor';
5694 $ret['fields'][] = 'user_actor.actor_id';
5695 $ret['joins']['user_actor'] = [
5696 $wgActorTableSchemaMigrationStage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN',
5697 [ 'user_actor.actor_user = user_id' ]
5698 ];
5699 }
5700 return $ret;
5701 }
5702
5710 static function newFatalPermissionDeniedStatus( $permission ) {
5712
5713 $groups = [];
5714 foreach ( self::getGroupsWithPermission( $permission ) as $group ) {
5715 $groups[] = UserGroupMembership::getLink( $group, RequestContext::getMain(), 'wiki' );
5716 }
5717
5718 if ( $groups ) {
5719 return Status::newFatal( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) );
5720 } else {
5721 return Status::newFatal( 'badaccess-group0' );
5722 }
5723 }
5724
5734 public function getInstanceForUpdate() {
5735 if ( !$this->getId() ) {
5736 return null; // anon
5737 }
5738
5739 $user = self::newFromId( $this->getId() );
5740 if ( !$user->loadFromId( self::READ_EXCLUSIVE ) ) {
5741 return null;
5742 }
5743
5744 return $user;
5745 }
5746
5754 public function equals( User $user ) {
5755 return $this->getName() === $user->getName();
5756 }
5757}
and(b) You must cause any modified files to carry prominent notices stating that You changed the files
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
target page
$wgRateLimitsExcludedIPs
Array of IPs / CIDR ranges which should be excluded from rate limits.
$wgGroupsRemoveFromSelf
$wgApplyIpBlocksToXff
Whether to look at the X-Forwarded-For header's list of (potentially spoofed) IPs and apply IP blocks...
$wgUserEmailConfirmationTokenExpiry
The time, in seconds, when an email confirmation email expires.
$wgRemoveGroups
$wgMaxArticleSize
Maximum article size in kilobytes.
$wgLearnerMemberSince
Name of the external diff 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 local wiki users.
$wgExperiencedUserMemberSince
Name of the external diff engine to use.
$wgUseFilePatrol
Use file patrolling to check new files on Special:Newfiles.
string null $wgAuthenticationTokenVersion
Versioning for authentication tokens.
string[] $wgSoftBlockRanges
IP ranges that should be considered soft-blocked (anon-only, account creation allowed).
$wgProxyWhitelist
Proxy whitelist, list of addresses that are assumed to be non-proxy despite what the other methods mi...
$wgClockSkewFudge
Clock skew or the one-second resolution of time() can occasionally cause cache problems when the user...
$wgGroupsAddToSelf
A map of group names that the user is in, to group names that those users are allowed to add or revok...
$wgAvailableRights
A list of available rights, in addition to the ones defined by the core.
$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...
$wgBlockAllowsUTEdit
Set this to true to allow blocked users to edit their own user talk page.
$wgGroupPermissions
Permission keys given to users in each group.
$wgExperiencedUserEdits
Name of the external diff 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.
$wgEnableDnsBlacklist
Whether to use DNS blacklists in $wgDnsBlacklistUrls to check for open proxies.
bool $wgForceHTTPS
If this is true, when an insecure HTTP request is received, always redirect to HTTPS.
$wgDnsBlacklistUrls
List of DNS blacklists to use, if $wgEnableDnsBlacklist is true.
$wgMinimalPasswordLength
Specifies the minimal length of a user password.
$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.
int $wgActorTableSchemaMigrationStage
Actor table schema migration stage.
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.
wfGetLB( $wiki=false)
Get a load balancer object.
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.
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.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
wfWikiID()
Get an ASCII string identifying this wiki This is used as a prefix in memcached keys.
$messages
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings,...
Definition Setup.php:112
foreach( $wgExtensionFunctions as $func) if(!defined('MW_NO_SESSION') &&! $wgCommandLineMode) if(! $wgCommandLineMode) $wgFullyInitialised
Definition Setup.php:977
$wgUseEnotif
Definition Setup.php:443
if(! $wgDBerrorLogTZ) $wgRequest
Definition Setup.php:737
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 clearCookie(WebResponse $response)
Unset the 'BlockID' cookie.
Definition Block.php:1552
prevents( $action, $x=null)
Get/set whether the Block prevents a given action.
Definition Block.php:1070
static newFromID( $id)
Load a blocked user from their block id.
Definition Block.php:184
static getBlocksForIPList(array $ipChain, $isAnon, $fromMaster=false)
Get all blocks that match any IP from an array of IP addresses.
Definition Block.php:1212
static chooseBlock(array $blocks, array $ipChain)
From a list of multiple blocks, find the most exact and strongest Block.
Definition Block.php:1293
static getIdFromCookieValue( $cookieValue)
Get the stored ID from the 'BlockID' cookie.
Definition Block.php:1588
const TYPE_USER
Definition Block.php:83
static newFromTarget( $specificTarget, $vagueTarget=null, $fromMaster=false)
Given a target and the target's type, get an existing Block object if possible.
Definition Block.php:1173
Value object representing a logged-out user's edit token.
static hmac( $data, $key, $raw=true)
Generate an acceptably unstable one-way-hmac of some text making use of the best hash algorithm that ...
static generateHex( $chars, $forceStrong=false)
Generate a run of (ideally) 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 log entries manually, to inject them into the database.
Definition LogEntry.php:432
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.
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 getMain()
Get the RequestContext object associated with the main request.
static newFromIDs( $ids)
Definition UserArray.php:45
static singleton()
Definition UserCache.php:34
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:53
loadFromSession()
Load user data from the session.
Definition User.php:1340
addWatch( $title, $checkRights=self::CHECK_USER_RIGHTS)
Watch an article.
Definition User.php:3933
string $mEmailToken
Definition User.php:231
string $mToken
Definition User.php:227
string $mTouched
TS_MW timestamp from the DB.
Definition User.php:223
logout()
Log this user out.
Definition User.php:4139
getOptions( $flags=0)
Get all user's options.
Definition User.php:3182
getRequest()
Get the WebRequest object to use with this object.
Definition User.php:3902
getPasswordValidity( $password)
Given unvalidated password input, return error message on failure.
Definition User.php:1134
Block $mBlockedFromCreateAccount
Definition User.php:307
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2482
addToDatabase()
Add this existing user object to the database.
Definition User.php:4372
requiresHTTPS()
Determine based on the wiki configuration and the user's options, whether this user must be over HTTP...
Definition User.php:3497
isBlocked( $bFromSlave=true)
Check if user is blocked.
Definition User.php:2300
updateActorId(IDatabase $dbw)
Update the actor ID after an insert.
Definition User.php:4444
getExperienceLevel()
Compute experienced level based on edit count and registration date.
Definition User.php:4064
static isEveryoneAllowed( $right)
Check if all users may be assumed to have the given permission.
Definition User.php:5025
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:591
resetOptions( $resetKinds=[ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused'], IContextSource $context=null)
Reset certain (or all) options to the site defaults.
Definition User.php:3432
static whoIsReal( $id)
Get the real name of a user given their user ID.
Definition User.php:873
invalidateCache()
Immediately touch the user data cache for this account.
Definition User.php:2792
static $mCacheVars
Array of Strings List of member variables which are saved to the shared cache (memcached).
Definition User.php:98
getEmailAuthenticationTimestamp()
Get the timestamp of the user's e-mail authentication.
Definition User.php:3045
array $mOptionOverrides
Definition User.php:241
isBlockedFromEmailuser()
Get whether the user is blocked from using Special:Emailuser.
Definition User.php:4516
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new user object.
Definition User.php:5672
$mOptionsLoaded
Bool Whether the cache variables have been loaded.
Definition User.php:248
static isCreatableName( $name)
Usernames which fail to pass this function will be blocked from new account registrations,...
Definition User.php:1093
getFirstEditTimestamp()
Get the timestamp of the first edit.
Definition User.php:4929
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:3347
getBlockedStatus( $bFromSlave=true)
Get blocking information.
Definition User.php:1777
int null $mActorId
Definition User.php:216
static changeableByGroup( $group)
Returns an array of the groups that a particular group can add/remove.
Definition User.php:5209
const VERSION
@const int Serialized record version.
Definition User.php:74
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:4612
static getAllGroups()
Return the set of defined explicit groups.
Definition User.php:5099
bool $mAllowUsertalk
Definition User.php:304
string $mEmailTokenExpires
Definition User.php:233
setCookies( $request=null, $secure=null, $rememberMe=false)
Persist this user's session (e.g.
Definition User.php:4103
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:4636
isDnsBlacklisted( $ip, $checkWhitelist=false)
Whether the given IP is in a DNS blacklist.
Definition User.php:1932
int $queryFlagsUsed
User::READ_* constant bitfield used to load data.
Definition User.php:310
static $mAllRights
String Cached results of getAllRights()
Definition User.php:207
getTokenUrl( $page, $token)
Internal function to format the e-mail validation/invalidation URLs.
Definition User.php:4788
addGroup( $group, $expiry=null)
Add the user to the given group.
Definition User.php:3712
isRegistered()
Alias of isLoggedIn() with a name that describes its actual functionality.
Definition User.php:3782
array $mEffectiveGroups
Definition User.php:283
isBlockedGlobally( $ip='')
Check if user is blocked on all wikis.
Definition User.php:2374
string $mQuickTouched
TS_MW timestamp from cache.
Definition User.php:225
const INVALID_TOKEN
@const string An invalid value for user_token
Definition User.php:62
isSafeToLoad()
Test if it's safe to load this User object.
Definition User.php:350
static groupHasPermission( $group, $role)
Check, if the given group has the given permission.
Definition User.php:5005
isEmailConfirmed()
Is this user's e-mail address valid-looking and confirmed within limits of the current site configura...
Definition User.php:4874
spreadAnyEditBlock()
If this user is logged-in and blocked, block any IP address they've successfully logged in from.
Definition User.php:4462
useFilePatrol()
Check whether to enable new files patrol features for this user.
Definition User.php:3889
loadFromId( $flags=self::READ_NORMAL)
Load user table data, given mId has already been set.
Definition User.php:466
isAllowed( $action='')
Internal mechanics of testing a permission.
Definition User.php:3855
getDBTouched()
Get the user_touched timestamp field (time of last DB updates)
Definition User.php:2858
setName( $str)
Set the user name.
Definition User.php:2509
changeAuthenticationData(array $data)
Changes credentials of the user.
Definition User.php:2942
Block $mGlobalBlock
Definition User.php:289
static $mCoreRights
Array of Strings Core rights.
Definition User.php:125
getId()
Get the user's ID.
Definition User.php:2457
getRealName()
Get the user's real name.
Definition User.php:3127
loadOptions( $data=null)
Load the user options either from cache, the database or an array.
Definition User.php:5480
getBoolOption( $oname)
Get the user's current setting for a given option, as a boolean value.
Definition User.php:3213
getRegistration()
Get the timestamp of account creation.
Definition User.php:4915
static isLocallyBlockedProxy( $ip)
Check if an IP address is in the local proxy list.
Definition User.php:1999
getGlobalBlock( $ip='')
Check if user is blocked on all wikis.
Definition User.php:2388
string $mRealName
Definition User.php:218
static newFromAnyId( $userId, $userName, $actorId)
Static factory method for creation from an ID, name, and/or actor ID.
Definition User.php:657
isItemLoaded( $item, $all='all')
Return whether an item has been loaded.
Definition User.php:1319
getMutableCacheKeys(WANObjectCache $cache)
Definition User.php:516
getTokenFromOption( $oname)
Get a token stored in the preferences (like the watchlist one), resetting it if it's empty (and savin...
Definition User.php:3262
isNewbie()
Determine whether the user is a newbie.
Definition User.php:4553
clearNotification(&$title, $oldid=0)
Clear the user's notification timestamp for the given title.
Definition User.php:3967
static resetIdByNameCache()
Reset the cache used in idFromName().
Definition User.php:923
deleteNewtalk( $field, $id)
Clear the new messages flag for the given user.
Definition User.php:2690
loadFromDatabase( $flags=self::READ_LATEST)
Load user and user_group data from the database.
Definition User.php:1387
invalidationTokenUrl( $token)
Return a URL the user can use to invalidate their email address.
Definition User.php:4770
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition User.php:3877
static randomPassword()
Return a random password.
Definition User.php:1267
static purge( $wikiId, $userId)
Definition User.php:496
getIntOption( $oname, $defaultOverride=0)
Get the user's current setting for a given option, as an integer value.
Definition User.php:3225
clearInstanceCache( $reloadFrom=false)
Clear various cached data stored in this object.
Definition User.php:1697
canReceiveEmail()
Is this user allowed to receive e-mails within limits of current site configuration?
Definition User.php:4860
string $mEmail
Definition User.php:221
loadDefaults( $name=false)
Set cached properties to default.
Definition User.php:1280
getOption( $oname, $defaultOverride=null, $ignoreHidden=false)
Get the user's current setting for a given option.
Definition User.php:3154
touch()
Update the "touched" timestamp for the user.
Definition User.php:2809
checkTemporaryPassword( $plaintext)
Check if the given clear-text password matches the temporary password sent by e-mail for password res...
Definition User.php:4596
isPingLimitable()
Is this user subject to rate limiting?
Definition User.php:2045
clearSharedCache( $mode='changed')
Clear user data from memcached.
Definition User.php:2763
static getCanonicalName( $name, $validate='valid')
Given unvalidated user input, return a canonical username, or false if the username is invalid.
Definition User.php:1210
static newFromRow( $row, $data=null)
Create a new user object from a user row.
Definition User.php:750
getToken( $forceCreation=true)
Get the user's current token.
Definition User.php:2969
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition User.php:614
setInternalPassword( $str)
Set the password and reset the random token unconditionally.
Definition User.php:2892
confirmEmail()
Mark the e-mail address confirmed.
Definition User.php:4801
static getGroupsWithPermission( $role)
Get all the groups who have a given permission.
Definition User.php:4982
setId( $v)
Set the user and reload all fields according to a given ID.
Definition User.php:2473
static getGroupPermissions( $groups)
Get the permissions associated with a given list of groups.
Definition User.php:4955
$mNewtalk
Lazy-initialized variables, invalidated with clearInstanceCache.
Definition User.php:271
static getDefaultOptions()
Combine the language default options with any site-specific options and add the default language vari...
Definition User.php:1722
static findUsersByGroup( $groups, $limit=5000, $after=null)
Return the users who are members of the given group(s).
Definition User.php:1053
bool $mLocked
Definition User.php:291
getEffectiveGroups( $recache=false)
Get the list of implicit group memberships this user has.
Definition User.php:3601
static selectFields()
Return the list of user fields that should be selected to create a new user object.
Definition User.php:5646
isHidden()
Check if user account is hidden.
Definition User.php:2438
static newFromConfirmationCode( $code, $flags=0)
Factory method to fetch whichever user has a given email confirmation code.
Definition User.php:705
setEmailWithConfirmation( $str)
Set the user's e-mail address and a confirmation mail if needed.
Definition User.php:3072
newTouchedTimestamp()
Generate a current or new-future timestamp to be stored in the user_touched field when we update thin...
Definition User.php:2742
inDnsBlacklist( $ip, $bases)
Whether the given IP is in a given DNS blacklist.
Definition User.php:1953
loadFromUserObject( $user)
Load the data for this user object from another user object.
Definition User.php:1555
static isUsableName( $name)
Usernames which fail to pass this function will be blocked from user login and new account registrati...
Definition User.php:1018
static getDefaultOption( $opt)
Get a given default option value.
Definition User.php:1762
const IGNORE_USER_RIGHTS
Definition User.php:90
getDatePreference()
Get the user's preferred date format.
Definition User.php:3477
string $mHash
Definition User.php:277
checkPasswordValidity( $password)
Check if this is a valid password for this user.
Definition User.php:1170
static getGroupPage( $group)
Get the title of a page describing a particular group.
Definition User.php:5145
array $mRights
Definition User.php:279
idForName( $flags=0)
If only this user's username is known, and it exists, return the user ID.
Definition User.php:4262
getGroupMemberships()
Get the list of explicit group memberships this user has, stored as UserGroupMembership objects.
Definition User.php:3588
matchEditTokenNoSuffix( $val, $salt='', $request=null, $maxage=null)
Check given value against the token value stored in the session, ignoring the suffix.
Definition User.php:4666
isWatched( $title, $checkRights=self::CHECK_USER_RIGHTS)
Check the watched status of an article.
Definition User.php:3919
loadFromRow( $row, $data=null)
Initialize this object from a row from the user table.
Definition User.php:1439
setPassword( $str)
Set the password and reset the random token.
Definition User.php:2880
string $mBlockedby
Definition User.php:275
confirmationTokenUrl( $token)
Return a URL the user can use to confirm their email address.
Definition User.php:4761
isAllowedToCreateAccount()
Get whether the user is allowed to create an account.
Definition User.php:4525
static newFromSession(WebRequest $request=null)
Create a new user object using data from session.
Definition User.php:729
incEditCountImmediate()
Increment the user's edit-count field.
Definition User.php:5338
static getAllRights()
Get a list of all available permissions.
Definition User.php:5111
getNewtalk()
Check if the user has new messages.
Definition User.php:2560
getGroups()
Get the list of explicit group memberships this user has.
Definition User.php:3575
addNewUserLogEntry( $action=false, $reason='')
Add a newuser log entry for this user.
Definition User.php:5457
validateCache( $timestamp)
Validate the cache for this account.
Definition User.php:2824
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition User.php:3868
loadGroups()
Load the groups from the database if they aren't already loaded.
Definition User.php:1565
static whoIs( $id)
Get the username corresponding to a given user ID.
Definition User.php:863
invalidateEmail()
Invalidate the user's e-mail confirmation, and unauthenticate the e-mail address if it was already co...
Definition User.php:4818
setEmailAuthenticationTimestamp( $timestamp)
Set the e-mail authentication timestamp.
Definition User.php:4832
static newSystemUser( $name, $options=[])
Static factory method for creation of a "system" user from username.
Definition User.php:791
setOption( $oname, $val)
Set the given option for a user.
Definition User.php:3241
saveOptions()
Saves the non-default options for this user, as previously set e.g.
Definition User.php:5560
static getRightDescription( $right)
Get the description of a given right.
Definition User.php:5418
getEditCount()
Get the user's edit count.
Definition User.php:3677
getActorId(IDatabase $dbw=null)
Get the user's actor ID.
Definition User.php:2520
const CHECK_USER_RIGHTS
Definition User.php:85
UserGroupMembership[] $mGroupMemberships
Associative array of (group name => UserGroupMembership object)
Definition User.php:239
addAutopromoteOnceGroups( $event)
Add the user to the group if he/she meets given criteria.
Definition User.php:1589
isValidPassword( $password)
Is the input a valid password for this user?
Definition User.php:1123
getBlock( $bFromSlave=true)
Get the block affecting the user, or null if the user is not blocked.
Definition User.php:2310
array $mFormerGroups
Definition User.php:287
isBlockedFromCreateAccount()
Get whether the user is explicitly blocked from account creation.
Definition User.php:4494
spreadBlock()
If this (non-anonymous) user is blocked, block the IP address they've successfully logged in from.
Definition User.php:4475
getFormerGroups()
Returns the groups the user has belonged to.
Definition User.php:3653
setRealName( $str)
Set the user's real name.
Definition User.php:3139
int $mId
Cache variables.
Definition User.php:212
getTitleKey()
Get the user's name escaped by underscores.
Definition User.php:2552
static isIP( $name)
Does the string match an anonymous IP address?
Definition User.php:943
static makeGroupLinkWiki( $group, $text='')
Create a link to the group in Wikitext, if available; else return the group name.
Definition User.php:5185
getTouched()
Get the user touched timestamp.
Definition User.php:2836
sendMail( $subject, $body, $from=null, $replyto=null)
Send an e-mail to this user's account.
Definition User.php:4717
Block $mBlock
Definition User.php:301
isLocked()
Check if user account is locked.
Definition User.php:2421
array $mOptions
Definition User.php:295
checkAndSetTouched()
Bump user_touched if it didn't change since this object was loaded.
Definition User.php:1659
removeWatch( $title, $checkRights=self::CHECK_USER_RIGHTS)
Stop watching an article.
Definition User.php:3950
setPasswordInternal( $str)
Actually set the password and such.
Definition User.php:2904
isEmailConfirmationPending()
Check whether there is an outstanding request for e-mail confirmation.
Definition User.php:4900
checkNewtalk( $field, $id)
Internal uncached check for new messages.
Definition User.php:2650
__toString()
Definition User.php:332
isBlockedFrom( $title, $bFromSlave=false)
Check if user is blocked from editing a particular article.
Definition User.php:2322
getUserPage()
Get this user's personal page title.
Definition User.php:4534
isIPRange()
Is the user an IP range?
Definition User.php:954
$mFrom
String Initialization data source if mLoadedItems!==true.
Definition User.php:266
confirmationToken(&$expiration)
Generate, store, and return a new e-mail confirmation code.
Definition User.php:4743
canSendEmail()
Is this user allowed to send e-mails within limits of current site configuration?
Definition User.php:4843
getBlockId()
If user is blocked, return the ID for the block.
Definition User.php:2361
isBot()
Definition User.php:3808
string $mRegistration
Definition User.php:235
__construct()
Lightweight constructor for an anonymous user.
Definition User.php:325
setNewtalk( $val, $curRev=null)
Update the 'You have new messages!' status.
Definition User.php:2710
getStubThreshold()
Get the user preferred stub threshold.
Definition User.php:3519
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition User.php:2072
string $mDatePreference
Definition User.php:273
static isValidUserName( $name)
Is the input a valid username?
Definition User.php:969
sendConfirmationMail( $type='created')
Generate a new e-mail confirmation token and send a confirmation/invalidation mail to the user's give...
Definition User.php:4678
getTalkPage()
Get this user's talk page title.
Definition User.php:4543
isLoggedIn()
Get whether the user is registered.
Definition User.php:3792
static idFromName( $name, $flags=self::READ_NORMAL)
Get database id given a user name.
Definition User.php:883
loadFromCache()
Load user data from shared cache, given mId has already been set.
Definition User.php:528
getCacheKey(WANObjectCache $cache)
Definition User.php:507
initEditCount( $add=0)
Initialize user_editcount from data out of the revision table.
Definition User.php:5385
saveSettings()
Save this user's settings into the database.
Definition User.php:4187
static $idCacheByName
Definition User.php:312
getNewMessageRevisionId()
Get the revision ID for the last talk page revision viewed by the talk page owner.
Definition User.php:2623
getBlockFromCookieValue( $blockCookieVal)
Try to load a Block from an ID given in a cookie value.
Definition User.php:1893
static getGrantName( $grant)
Get the name of a given grant.
Definition User.php:5431
isAllowedAny()
Check if user is allowed to access a feature / make an action.
Definition User.php:3825
getEmail()
Get the user's e-mail address.
Definition User.php:3035
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition User.php:5710
setNewpassword( $str, $throttle=true)
Set the password for a password reminder or new account email.
Definition User.php:3027
getAutomaticGroups( $recache=false)
Get the list of implicit group memberships this user has.
Definition User.php:3624
load( $flags=self::READ_NORMAL)
Load the user table data for this object from the source given by mFrom.
Definition User.php:367
blockedBy()
If user is blocked, return the name of the user who placed the block.
Definition User.php:2343
getRights()
Get the permissions this user has.
Definition User.php:3534
matchEditToken( $val, $salt='', $request=null, $maxage=null)
Check given value against the token value stored in the session.
Definition User.php:4652
string $mEmailAuthenticated
Definition User.php:229
const GETOPTIONS_EXCLUDE_DEFAULTS
Exclude user options that are set to their default value.
Definition User.php:80
const TOKEN_LENGTH
@const int Number of characters in user_token field.
Definition User.php:57
equals(User $user)
Checks if two user objects point to the same user.
Definition User.php:5754
isAllowedAll()
Definition User.php:3840
setToken( $token=false)
Set the random token (used for persistent authentication) Called from loadDefaults() among other plac...
Definition User.php:3007
updateNewtalk( $field, $id, $curRev=null)
Add or update the new messages flag.
Definition User.php:2665
static createNew( $name, $params=[])
Add a user to the database, return the user object.
Definition User.php:4297
static getGroupMember( $group, $username='#')
Get the localized descriptive name for a member of a group, if it exists.
Definition User.php:5088
addNewUserLogEntryAutoCreate()
Add an autocreate newuser log entry for this user Used by things like CentralAuth and perhaps other a...
Definition User.php:5469
doLogout()
Clear the user's session, and reset the instance cache.
Definition User.php:4151
setItemLoaded( $item)
Set that an item has been loaded.
Definition User.php:1329
incEditCount()
Deferred version of incEditCountImmediate()
Definition User.php:5324
$mLoadedItems
Array with already loaded items or true if all items have been loaded.
Definition User.php:253
blockedFor()
If user is blocked, return the specified reason for the block.
Definition User.php:2352
static getGroupName( $group)
Get the localized descriptive name for a group, if it exists.
Definition User.php:5075
static listOptionKinds()
Return a list of the types of user options currently returned by User::getOptionKinds().
Definition User.php:3324
int $mEditCount
Definition User.php:237
resetTokenFromOption( $oname)
Reset a token stored in the preferences (like the watchlist one).
Definition User.php:3290
static makeGroupLinkHTML( $group, $text='')
Create a link to the group in HTML, if available; else return the group name.
Definition User.php:5160
bool $mHideName
Definition User.php:293
static getImplicitGroups()
Get a list of implicit groups.
Definition User.php:5128
changeableGroups()
Returns an array of groups that this user can add and remove.
Definition User.php:5282
string $mName
Definition User.php:214
getNewMessageLinks()
Return the data needed to construct links for new talk page message alerts.
Definition User.php:2598
isAnon()
Get whether the user is anonymous.
Definition User.php:3800
setEmail( $str)
Set the user's e-mail address.
Definition User.php:3055
string $mBlockreason
Definition User.php:281
WebRequest $mRequest
Definition User.php:298
checkPassword( $password)
Check to see if the given clear-text password is one of the accepted passwords.
Definition User.php:4563
getInstanceForUpdate()
Get a new instance of this user that was loaded from the master via a locking read.
Definition User.php:5734
makeUpdateConditions(Database $db, array $conditions)
Builds update conditions.
Definition User.php:1641
const EDIT_TOKEN_SUFFIX
Global constant made accessible as class constants so that autoloader magic can be used.
Definition User.php:69
static newFromActorId( $id)
Static factory method for creation from a given actor ID.
Definition User.php:629
clearAllNotifications()
Resets all of the given user's page-change notification timestamps.
Definition User.php:4035
array $mImplicitGroups
Definition User.php:285
removeGroup( $group)
Remove the user from the given group.
Definition User.php:3748
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:48
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by wfTimestamp() to the format used for inserting ...
The ContentHandler facility adds support for arbitrary content types on wiki pages
$res
Definition database.txt:21
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add in any and then calling but I prefer the flexibility This should also do the output encoding The system allocates a global one in $wgOut Title Represents the title of an article
Definition design.txt:25
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as and the local content language as $wgContLang
Definition design.txt:57
when a variable name is used in a it is silently declared as a new local masking the global
Definition design.txt:95
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add text
Definition design.txt:18
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as $wgLang
Definition design.txt:56
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
const NS_USER
Definition Defines.php:76
const NS_MAIN
Definition Defines.php:74
const MIGRATION_NEW
Definition Defines.php:305
const NS_USER_TALK
Definition Defines.php:77
const MIGRATION_OLD
Definition Defines.php:302
this hook is for auditing only $req
Definition hooks.txt:990
the array() calling protocol came about after MediaWiki 1.4rc1.
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction $rows
Definition hooks.txt:2783
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on $request
Definition hooks.txt:2806
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition hooks.txt:1795
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. 'ImgAuthModifyHeaders':Executed just before a file is streamed to a user via img_auth.php, allowing headers to be modified beforehand. $title:LinkTarget object & $headers:HTTP headers(name=> value, names are case insensitive). Two headers get special handling:If-Modified-Since(value must be a valid HTTP date) and Range(must be of the form "bytes=(\d*-\d*)") will be honored when streaming the file. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUnknownUser':When a user doesn 't exist locally, this hook is called to give extensions an opportunity to auto-create it. If the auto-creation is successful, return false. $name:User name 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetMagic':DEPRECATED! Use $magicWords in a file listed in $wgExtensionMessagesFiles instead. Use this to define synonyms of magic words depending of the language & $magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetSpecialPageAliases':DEPRECATED! Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead. Use to define aliases of special pages names depending of the language & $specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links:Array with elements of the form "language:title" in the order that they will be output. & $linkFlags:Associative array mapping prefixed links to arrays of flags. Currently unused, but planned to provide support for marking individual language links in the UI, e.g. for featured articles. 'LanguageSelector':Hook to change the language selector available on a page. $out:The output page. $cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED! Use HtmlPageLinkRendererBegin instead. Used when generating internal and interwiki links in Linker::link(), before processing starts. Return false to skip default processing and return $ret. See documentation for Linker::link() for details on the expected meanings of parameters. $skin:the Skin object $target:the Title that the link is pointing to & $html:the contents that the< a > tag should have(raw HTML) $result
Definition hooks.txt:1993
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:2001
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable & $code
Definition hooks.txt:865
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on and they can depend only on the ResourceLoaderContext $context
Definition hooks.txt:2811
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:964
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt;div ...>$1&lt;/div>"). - flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException':Called before an exception(or PHP error) is logged. This is meant for integration with external error aggregation services
null for the local wiki Added in
Definition hooks.txt:1591
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition hooks.txt:2006
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition hooks.txt:2005
this hook is for auditing only or null if authentication failed before getting that far $username
Definition hooks.txt:785
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action or null $user:User who performed the tagging when the tagging is subsequent to the action or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1255
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
We ve cleaned up the code here by removing clumps of infrequently used code and moving them off somewhere else It s much easier for someone working with this code to see what s _really_ going and make changes or fix bugs In we can take all the code that deals with the little used title reversing we can concentrate it all in an extension file
Definition hooks.txt:106
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition hooks.txt:1777
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition hooks.txt:247
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition injection.txt:37
Interface for objects which can provide a MediaWiki context on request.
Interface for database access objects.
const READ_LOCKING
Constants for object loading bitfield flags (higher => higher QoS)
Interface for objects representing user identity.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
insert( $table, $a, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
insertId()
Get the inserted value of an auto-increment row.
$cache
Definition mcc.php:33
MediaWiki has optional support for a high distributed memory object caching system For general information on but for a larger site with heavy load
Definition memcached.txt:6
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:29
$property
$params