/**
* String Some punctuation to prevent editing from broken text-mangling proxies.
+ * @deprecated since 1.27, use \\MediaWiki\\Session\\Token::SUFFIX
* @ingroup Constants
*/
-define( 'EDIT_TOKEN_SUFFIX', '+\\' );
+define( 'EDIT_TOKEN_SUFFIX', MediaWiki\Session\Token::SUFFIX );
/**
* The User object encapsulates all of the user-specific settings (user_id,
*/
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.
+ * @deprecated since 1.27, use \\MediaWiki\\Session\\Token::SUFFIX
*/
const EDIT_TOKEN_SUFFIX = EDIT_TOKEN_SUFFIX;
return $this->getName();
}
+ /**
+ * 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() {
+ global $wgFullyInitialised;
+ return $wgFullyInitialised || $this->mLoadedItems === true || $this->mFrom !== 'session';
+ }
+
/**
* Load the user table data for this object from the source given by mFrom.
*
$this->queryFlagsUsed = $flags;
// If this is called too early, things are likely to break.
- if ( $this->mFrom === 'session' && empty( $wgFullyInitialised ) ) {
+ if ( !$wgFullyInitialised && $this->mFrom === 'session' ) {
\MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
->warning( 'User::loadFromSession called before the end of Setup.php', array(
'exception' => new Exception( 'User::loadFromSession called before the end of Setup.php' ),
$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;
}
// Other code expects these to be set in the session, so set them.
$session->set( 'wsUserID', $this->getId() );
$session->set( 'wsUserName', $this->getName() );
- $session->set( 'wsToken', $this->mToken );
+ $session->set( 'wsToken', $this->getToken() );
return true;
}
$all = false;
}
- if ( isset( $row->user_email ) ) {
- $this->mEmail = $row->user_email;
- $this->mToken = $row->user_token;
- if ( $this->mToken == '' ) {
+ if ( isset( $row->user_token ) ) {
+ // The definition for the column is binary(32), so trim the NULs
+ // that appends. The previous definition was char(32), so trim
+ // spaces too.
+ $this->mToken = rtrim( $row->user_token, " \0" );
+ if ( $this->mToken === '' ) {
$this->mToken = null;
}
+ } else {
+ $all = false;
+ }
+
+ if ( isset( $row->user_email ) ) {
+ $this->mEmail = $row->user_email;
$this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
$this->mEmailToken = $row->user_email_token;
$this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
# We only need to worry about passing the IP address to the Block generator if the
# user is not immune to autoblocks/hardblocks, and they are the current user so we
# know which IP address they're actually coming from
- if ( !$this->isAllowed( 'ipblock-exempt' ) && $this->equals( $wgUser ) ) {
- $ip = $this->getRequest()->getIP();
- } else {
- $ip = null;
+ $ip = null;
+ if ( !$this->isAllowed( 'ipblock-exempt' ) ) {
+ // $wgUser->getName() only works after the end of Setup.php. Until
+ // then, assume it's a logged-out user.
+ $globalUserName = $wgUser->isSafeToLoad()
+ ? $wgUser->getName()
+ : IP::sanitizeIP( $wgUser->getRequest()->getIP() );
+ if ( $this->getName() === $globalUserName ) {
+ $ip = $this->getRequest()->getIP();
+ }
}
// User/IP blocking
$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'];
+ $keys[wfMemcKey( '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'];
+ $keys[wfMemcKey( '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;
* Get the user's current token.
* @param bool $forceCreation Force the generation of a new token if the
* user doesn't have one (default=true for backwards compatibility).
- * @return string Token
+ * @return string|null Token
*/
public function getToken( $forceCreation = true ) {
+ global $wgAuthenticationTokenVersion;
+
$this->load();
if ( !$this->mToken && $forceCreation ) {
$this->setToken();
}
- return $this->mToken;
+
+ 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.
+ $len = max( 32, self::TOKEN_LENGTH );
+ if ( strlen( $ret ) < $len ) {
+ // Should never happen, even md5 is 128 bits
+ throw new \UnexpectedValueException( 'Hmac returned less than 128 bits' );
+ }
+ return substr( $ret, -$len );
+ }
}
/**
*/
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;
}
$this->getWatchedItem( $title )->resetNotificationTimestamp(
- $force, $oldid, WatchedItem::DEFERRED
+ $force, $oldid
);
}
}
/**
- * Internal implementation for self::getEditToken() and
- * self::matchEditToken().
+ * Initialize (if necessary) and return a session token value
+ * which can be used in edit forms to show that the user's
+ * login credentials aren't being hijacked with a foreign form
+ * submission.
*
- * @param string|array $salt
- * @param WebRequest $request
- * @param string|int $timestamp
- * @return string
+ * @since 1.27
+ * @param string|array $salt Array of Strings Optional function-specific data for hashing
+ * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest
+ * @return MediaWiki\\Session\\Token The new edit token
*/
- private function getEditTokenAtTimestamp( $salt, $request, $timestamp ) {
+ public function getEditTokenObject( $salt = '', $request = null ) {
if ( $this->isAnon() ) {
- return self::EDIT_TOKEN_SUFFIX;
- } else {
- $token = $request->getSessionData( 'wsEditToken' );
- if ( $token === null ) {
- $token = MWCryptRand::generateHex( 32 );
- $request->setSessionData( 'wsEditToken', $token );
- }
- if ( is_array( $salt ) ) {
- $salt = implode( '|', $salt );
- }
- return hash_hmac( 'md5', $timestamp . $salt, $token, false ) .
- dechex( $timestamp ) .
- self::EDIT_TOKEN_SUFFIX;
+ return new LoggedOutEditToken();
}
+
+ if ( !$request ) {
+ $request = $this->getRequest();
+ }
+ return $request->getSession()->getToken( $salt );
}
/**
* submission.
*
* @since 1.19
- *
* @param string|array $salt Array of Strings Optional function-specific data for hashing
* @param WebRequest|null $request WebRequest object to use or null to use $wgRequest
* @return string The new edit token
*/
public function getEditToken( $salt = '', $request = null ) {
- return $this->getEditTokenAtTimestamp(
- $salt, $request ?: $this->getRequest(), wfTimestamp()
- );
+ return $this->getEditTokenObject( $salt, $request )->toString();
}
/**
* Get the embedded timestamp from a token.
+ * @deprecated since 1.27, use \\MediaWiki\\Session\\Token::getTimestamp instead.
* @param string $val Input token
* @return int|null
*/
public static function getEditTokenTimestamp( $val ) {
- $suffixLen = strlen( self::EDIT_TOKEN_SUFFIX );
- if ( strlen( $val ) <= 32 + $suffixLen ) {
- return null;
- }
-
- return hexdec( substr( $val, 32, -$suffixLen ) );
+ wfDeprecated( __METHOD__, '1.27' );
+ return MediaWiki\Session\Token::getTimestamp( $val );
}
/**
* @return bool Whether the token matches
*/
public function matchEditToken( $val, $salt = '', $request = null, $maxage = null ) {
- if ( $this->isAnon() ) {
- return $val === self::EDIT_TOKEN_SUFFIX;
- }
-
- $timestamp = self::getEditTokenTimestamp( $val );
- if ( $timestamp === null ) {
- return false;
- }
- if ( $maxage !== null && $timestamp < wfTimestamp() - $maxage ) {
- // Expired token
- return false;
- }
-
- $sessionToken = $this->getEditTokenAtTimestamp(
- $salt, $request ?: $this->getRequest(), $timestamp
- );
-
- if ( !hash_equals( $sessionToken, $val ) ) {
- wfDebug( "User::matchEditToken: broken session data\n" );
- }
-
- return hash_equals( $sessionToken, $val );
+ return $this->getEditTokenObject( $salt, $request )->match( $val, $maxage );
}
/**