Merge "Add $wgRateLimits types ip-all and subnet-all"
[lhc/web/wiklou.git] / includes / user / User.php
index 1fb0b8a..01afd97 100644 (file)
@@ -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,
@@ -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;
 
@@ -307,6 +309,15 @@ class User implements IDBAccessObject {
                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.
         *
@@ -325,9 +336,11 @@ class User implements IDBAccessObject {
                $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;
@@ -1152,7 +1165,7 @@ class User implements IDBAccessObject {
                        // 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;
                }
 
@@ -1257,12 +1270,20 @@ class User implements IDBAccessObject {
                        $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 );
@@ -1514,10 +1535,16 @@ class User implements IDBAccessObject {
                # 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
@@ -2445,14 +2472,33 @@ class User implements IDBAccessObject {
         * 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 );
+               }
        }
 
        /**
@@ -3457,7 +3503,7 @@ class User implements IDBAccessObject {
                }
 
                $this->getWatchedItem( $title )->resetNotificationTimestamp(
-                       $force, $oldid, WatchedItem::DEFERRED
+                       $force, $oldid
                );
        }
 
@@ -4092,30 +4138,25 @@ class User implements IDBAccessObject {
        }
 
        /**
-        * 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 );
        }
 
        /**
@@ -4125,29 +4166,23 @@ class User implements IDBAccessObject {
         * 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 );
        }
 
        /**
@@ -4163,28 +4198,7 @@ class User implements IDBAccessObject {
         * @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 );
        }
 
        /**