*/
const TOKEN_LENGTH = 32;
+ /**
+ * @const string An invalid value for user_token
+ */
+ const INVALID_TOKEN = '*** INVALID ***';
+
/**
* Global constant made accessible as class constants so that autoloader
* magic can be used.
}
/**
- * Test if it's safe to load this User object
+ * Test if it's safe to load this User object. You should typically check this before using
+ * $wgUser or RequestContext::getUser in a method that might be called before the system has
+ * been fully initialized. If the object is unsafe, you should use an anonymous user:
+ * \code
+ * $user = $wgUser->isSafeToLoad() ? $wgUser : new User;
+ * \endcode
+ *
+ * @since 1.27
* @return bool
*/
public function isSafeToLoad() {
$user = self::newFromRow( $row );
// A user is considered to exist as a non-system user if it has a
- // password set, or a temporary password set, or an email set.
+ // password set, or a temporary password set, or an email set, or a
+ // non-invalid token.
$passwordFactory = new PasswordFactory();
$passwordFactory->init( RequestContext::getMain()->getConfig() );
try {
$newpassword = PasswordFactory::newInvalidPassword();
}
if ( !$password instanceof InvalidPassword || !$newpassword instanceof InvalidPassword
- || $user->mEmail
+ || $user->mEmail || $user->mToken !== self::INVALID_TOKEN
) {
// User exists. Steal it?
if ( !$options['steal'] ) {
__METHOD__
);
$user->invalidateEmail();
+ $user->mToken = self::INVALID_TOKEN;
$user->saveSettings();
+ SessionManager::singleton()->preventSessionsForUser( $user->getName() );
}
- SessionManager::singleton()->preventSessionsForUser( $user->getName() );
-
return $user;
}
$keys = array();
$id = $this->getId();
$userLimit = false;
+ $isNewbie = $this->isNewbie();
- if ( isset( $limits['anon'] ) && $id == 0 ) {
- $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
- }
-
- if ( isset( $limits['user'] ) && $id != 0 ) {
- $userLimit = $limits['user'];
- }
- if ( $this->isNewbie() ) {
- if ( isset( $limits['newbie'] ) && $id != 0 ) {
+ if ( $id == 0 ) {
+ // limits for anons
+ if ( isset( $limits['anon'] ) ) {
+ $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
+ }
+ } else {
+ // limits for logged-in users
+ if ( isset( $limits['user'] ) ) {
+ $userLimit = $limits['user'];
+ }
+ // limits for newbie logged-in users
+ if ( $isNewbie && isset( $limits['newbie'] ) ) {
$keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
}
+ }
+
+ // limits for anons and for newbie logged-in users
+ if ( $isNewbie ) {
+ // ip-based limits
if ( isset( $limits['ip'] ) ) {
$ip = $this->getRequest()->getIP();
$keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
}
+ // subnet-based limits
if ( isset( $limits['subnet'] ) ) {
$ip = $this->getRequest()->getIP();
- $matches = array();
- $subnet = false;
- if ( IP::isIPv6( $ip ) ) {
- $parts = IP::parseRange( "$ip/64" );
- $subnet = $parts[0];
- } elseif ( preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
- // IPv4
- $subnet = $matches[1];
- }
+ $subnet = IP::getSubnet( $ip );
if ( $subnet !== false ) {
$keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
}
}
}
+
// Check for group-specific permissions
- // If more than one group applies, use the group with the highest limit
+ // If more than one group applies, use the group with the highest limit ratio (max/period)
foreach ( $this->getGroups() as $group ) {
if ( isset( $limits[$group] ) ) {
if ( $userLimit === false
}
}
}
+
// Set the user limit key
if ( $userLimit !== false ) {
list( $max, $period ) = $userLimit;
$keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $userLimit;
}
+ // ip-based limits for all ping-limitable users
+ if ( isset( $limits['ip-all'] ) ) {
+ $ip = $this->getRequest()->getIP();
+ // ignore if user limit is more permissive
+ if ( $isNewbie || $userLimit === false
+ || $limits['ip-all'][0] / $limits['ip-all'][1] > $userLimit[0] / $userLimit[1] ) {
+ $keys["mediawiki:limiter:$action:ip-all:$ip"] = $limits['ip-all'];
+ }
+ }
+
+ // subnet-based limits for all ping-limitable users
+ if ( isset( $limits['subnet-all'] ) ) {
+ $ip = $this->getRequest()->getIP();
+ $subnet = IP::getSubnet( $ip );
+ if ( $subnet !== false ) {
+ // ignore if user limit is more permissive
+ if ( $isNewbie || $userLimit === false
+ || $limits['ip-all'][0] / $limits['ip-all'][1]
+ > $userLimit[0] / $userLimit[1] ) {
+ $keys["mediawiki:limiter:$action:subnet-all:$subnet"] = $limits['subnet-all'];
+ }
+ }
+ }
+
$cache = ObjectCache::getLocalClusterInstance();
$triggered = false;
$this->setToken();
}
- // If the user doesn't have a token, return null to indicate that.
- // Otherwise, hmac the version with the secret if we have a version.
if ( !$this->mToken ) {
+ // The user doesn't have a token, return null to indicate that.
return null;
+ } elseif ( $this->mToken === self::INVALID_TOKEN ) {
+ // We return a random value here so existing token checks are very
+ // likely to fail.
+ return MWCryptRand::generateHex( self::TOKEN_LENGTH );
} elseif ( $wgAuthenticationTokenVersion === null ) {
+ // $wgAuthenticationTokenVersion not in use, so return the raw secret
return $this->mToken;
} else {
+ // $wgAuthenticationTokenVersion in use, so hmac it.
$ret = MWCryptHash::hmac( $wgAuthenticationTokenVersion, $this->mToken, false );
// The raw hash can be overly long. Shorten it up.
*/
public function setToken( $token = false ) {
$this->load();
- if ( !$token ) {
+ if ( $this->mToken === self::INVALID_TOKEN ) {
+ \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
+ ->debug( __METHOD__ . ": Ignoring attempt to set token for system user \"$this\"" );
+ } elseif ( !$token ) {
$this->mToken = MWCryptRand::generateHex( self::TOKEN_LENGTH );
} else {
$this->mToken = $token;
return;
}
- $that = $this;
// Try to update the DB post-send and only if needed...
- DeferredUpdates::addCallableUpdate( function() use ( $that, $title, $oldid ) {
- if ( !$that->getNewtalk() ) {
+ DeferredUpdates::addCallableUpdate( function() use ( $title, $oldid ) {
+ if ( !$this->getNewtalk() ) {
return; // no notifications to clear
}
// Delete the last notifications (they stack up)
- $that->setNewtalk( false );
+ $this->setNewtalk( false );
// If there is a new, unseen, revision, use its timestamp
$nextid = $oldid
? $title->getNextRevisionID( $oldid, Title::GAID_FOR_UPDATE )
: null;
if ( $nextid ) {
- $that->setNewtalk( true, Revision::newFromId( $nextid ) );
+ $this->setNewtalk( true, Revision::newFromId( $nextid ) );
}
} );
}
* Deferred version of incEditCountImmediate()
*/
public function incEditCount() {
- $that = $this;
- wfGetDB( DB_MASTER )->onTransactionPreCommitOrIdle( function() use ( $that ) {
- $that->incEditCountImmediate();
+ wfGetDB( DB_MASTER )->onTransactionPreCommitOrIdle( function() {
+ $this->incEditCountImmediate();
} );
}