From: jenkins-bot Date: Fri, 5 Feb 2016 20:52:05 +0000 (+0000) Subject: Merge "Add $wgRateLimits types ip-all and subnet-all" X-Git-Tag: 1.31.0-rc.0~8071 X-Git-Url: http://git.cyclocoop.org/?a=commitdiff_plain;h=4b069cd1b88877fbb253af8780e358d96ba2587c;hp=-c;p=lhc%2Fweb%2Fwiklou.git Merge "Add $wgRateLimits types ip-all and subnet-all" --- 4b069cd1b88877fbb253af8780e358d96ba2587c diff --combined includes/user/User.php index 3635f5c0d6,1fb0b8a9e4..01afd97871 --- a/includes/user/User.php +++ b/includes/user/User.php @@@ -24,10 -24,9 +24,10 @@@ use MediaWiki\Session\SessionManager /** * 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, @@@ -48,7 -47,6 +48,7 @@@ class User implements IDBAccessObject /** * 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; @@@ -309,15 -307,6 +309,15 @@@ return $this->getName(); } + /** + * Test if it's safe to load this User object + * @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. * @@@ -336,11 -325,9 +336,11 @@@ $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' ); + ->warning( 'User::loadFromSession called before the end of Setup.php', array( + 'exception' => new Exception( 'User::loadFromSession called before the end of Setup.php' ), + ) ); $this->loadDefaults(); $this->mLoadedItems = $oldLoadedItems; return; @@@ -1165,7 -1152,7 +1165,7 @@@ // 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; } @@@ -1270,20 -1257,12 +1270,20 @@@ $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 ); @@@ -1535,16 -1514,10 +1535,16 @@@ # 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 @@@ -1753,40 -1726,43 +1753,43 @@@ $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 @@@ -1796,6 -1772,7 +1799,7 @@@ } } } + // Set the user limit key if ( $userLimit !== false ) { list( $max, $period ) = $userLimit; @@@ -1803,6 -1780,30 +1807,30 @@@ $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; @@@ -2444,33 -2445,14 +2472,33 @@@ * 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 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 ) { + return null; + } elseif ( $wgAuthenticationTokenVersion === null ) { + return $this->mToken; + } else { + $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 ); + } } /** @@@ -3475,7 -3457,7 +3503,7 @@@ } $this->getWatchedItem( $title )->resetNotificationTimestamp( - $force, $oldid, WatchedItem::DEFERRED + $force, $oldid ); } @@@ -4110,25 -4092,30 +4138,25 @@@ } /** - * 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 ); } /** @@@ -4138,23 -4125,29 +4166,23 @@@ * 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 ); } /** @@@ -4170,7 -4163,28 +4198,7 @@@ * @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 ); } /**