Merge "mediawiki.mixins.rotation: Remove unnecessary @-o-keyframes"
[lhc/web/wiklou.git] / includes / user / User.php
index b406bac..6ec0ff0 100644 (file)
@@ -20,6 +20,8 @@
  * @file
  */
 
+use MediaWiki\Session\SessionManager;
+
 /**
  * String Some punctuation to prevent editing from broken text-mangling proxies.
  * @ingroup Constants
@@ -99,6 +101,7 @@ class User implements IDBAccessObject {
                'apihighlimits',
                'applychangetags',
                'autoconfirmed',
+               'autocreateaccount',
                'autopatrol',
                'bigdelete',
                'block',
@@ -148,7 +151,6 @@ class User implements IDBAccessObject {
                'patrol',
                'patrolmarks',
                'protect',
-               'proxyunbannable',
                'purge',
                'read',
                'reupload',
@@ -227,7 +229,7 @@ class User implements IDBAccessObject {
         *  - 'defaults'   anonymous user initialised from class defaults
         *  - 'name'       initialise from mName
         *  - 'id'         initialise from mId
-        *  - 'session'    log in from cookies or session if possible
+        *  - 'session'    log in from session if possible
         *
         * Use the User::newFrom*() family of functions to set this.
         */
@@ -311,14 +313,26 @@ class User implements IDBAccessObject {
         * @param integer $flags User::READ_* constant bitfield
         */
        public function load( $flags = self::READ_NORMAL ) {
+               global $wgFullyInitialised;
+
                if ( $this->mLoadedItems === true ) {
                        return;
                }
 
                // Set it now to avoid infinite recursion in accessors
+               $oldLoadedItems = $this->mLoadedItems;
                $this->mLoadedItems = true;
                $this->queryFlagsUsed = $flags;
 
+               // If this is called too early, things are likely to break.
+               if ( $this->mFrom === 'session' && empty( $wgFullyInitialised ) ) {
+                       \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
+                               ->warning( 'User::loadFromSession called before the end of Setup.php' );
+                       $this->loadDefaults();
+                       $this->mLoadedItems = $oldLoadedItems;
+                       return;
+               }
+
                switch ( $this->mFrom ) {
                        case 'defaults':
                                $this->loadDefaults();
@@ -541,8 +555,8 @@ class User implements IDBAccessObject {
        }
 
        /**
-        * Create a new user object using data from session or cookies. If the
-        * login credentials are invalid, the result is an anonymous user.
+        * Create a new user object using data from session. If the login
+        * credentials are invalid, the result is an anonymous user.
         *
         * @param WebRequest|null $request Object to use; $wgRequest will be used if omitted.
         * @return User
@@ -662,6 +676,8 @@ class User implements IDBAccessObject {
                        $user->saveSettings();
                }
 
+               SessionManager::singleton()->preventSessionsForUser( $user->getName() );
+
                return $user;
        }
 
@@ -1069,8 +1085,9 @@ class User implements IDBAccessObject {
                $this->mOptionOverrides = null;
                $this->mOptionsLoaded = false;
 
-               $loggedOut = $this->getRequest()->getCookie( 'LoggedOut' );
-               if ( $loggedOut !== null ) {
+               $request = $this->getRequest();
+               $loggedOut = $request ? $request->getSession()->getLoggedOutTimestamp() : 0;
+               if ( $loggedOut !== 0 ) {
                        $this->mTouched = wfTimestamp( TS_MW, $loggedOut );
                } else {
                        $this->mTouched = '1'; # Allow any pages to be cached
@@ -1115,84 +1132,32 @@ class User implements IDBAccessObject {
        }
 
        /**
-        * Load user data from the session or login cookie.
+        * Load user data from the session.
         *
         * @return bool True if the user is logged in, false otherwise.
         */
        private function loadFromSession() {
+               // Deprecated hook
                $result = null;
-               Hooks::run( 'UserLoadFromSession', array( $this, &$result ) );
+               Hooks::run( 'UserLoadFromSession', array( $this, &$result ), '1.27' );
                if ( $result !== null ) {
                        return $result;
                }
 
-               $request = $this->getRequest();
-
-               $cookieId = $request->getCookie( 'UserID' );
-               $sessId = $request->getSessionData( 'wsUserID' );
-
-               if ( $cookieId !== null ) {
-                       $sId = intval( $cookieId );
-                       if ( $sessId !== null && $cookieId != $sessId ) {
-                               wfDebugLog( 'loginSessions', "Session user ID ($sessId) and
-                                       cookie user ID ($sId) don't match!" );
-                               return false;
-                       }
-                       $request->setSessionData( 'wsUserID', $sId );
-               } elseif ( $sessId !== null && $sessId != 0 ) {
-                       $sId = $sessId;
-               } else {
-                       return false;
-               }
-
-               if ( $request->getSessionData( 'wsUserName' ) !== null ) {
-                       $sName = $request->getSessionData( 'wsUserName' );
-               } elseif ( $request->getCookie( 'UserName' ) !== null ) {
-                       $sName = $request->getCookie( 'UserName' );
-                       $request->setSessionData( 'wsUserName', $sName );
-               } else {
-                       return false;
-               }
-
-               $proposedUser = User::newFromId( $sId );
-               if ( !$proposedUser->isLoggedIn() ) {
-                       // Not a valid ID
-                       return false;
-               }
-
-               global $wgBlockDisablesLogin;
-               if ( $wgBlockDisablesLogin && $proposedUser->isBlocked() ) {
-                       // User blocked and we've disabled blocked user logins
-                       return false;
-               }
-
-               if ( $request->getSessionData( 'wsToken' ) ) {
-                       $passwordCorrect =
-                               ( $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' ) );
-                       $from = 'session';
-               } elseif ( $request->getCookie( 'Token' ) ) {
-                       # Get the token from DB/cache and clean it up to remove garbage padding.
-                       # This deals with historical problems with bugs and the default column value.
-                       $token = rtrim( $proposedUser->getToken( false ) ); // correct token
-                       // Make comparison in constant time (bug 61346)
-                       $passwordCorrect = strlen( $token )
-                               && hash_equals( $token, $request->getCookie( 'Token' ) );
-                       $from = 'cookie';
-               } else {
-                       // No session or persistent login cookie
-                       return false;
-               }
-
-               if ( ( $sName === $proposedUser->getName() ) && $passwordCorrect ) {
-                       $this->loadFromUserObject( $proposedUser );
-                       $request->setSessionData( 'wsToken', $this->mToken );
-                       wfDebug( "User: logged in from $from\n" );
+               // MediaWiki\Session\Session already did the necessary authentication of the user
+               // returned here, so just use it if applicable.
+               $session = $this->getRequest()->getSession();
+               $user = $session->getUser();
+               if ( $user->isLoggedIn() ) {
+                       $this->loadFromUserObject( $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 );
                        return true;
-               } else {
-                       // Invalid credentials
-                       wfDebug( "User: can't log in from $from, invalid credentials\n" );
-                       return false;
                }
+
+               return false;
        }
 
        /**
@@ -1555,9 +1520,7 @@ class User implements IDBAccessObject {
                $block = Block::newFromTarget( $this, $ip, !$bFromSlave );
 
                // Proxy blocking
-               if ( !$block instanceof Block && $ip !== null && !$this->isAllowed( 'proxyunbannable' )
-                       && !in_array( $ip, $wgProxyWhitelist )
-               ) {
+               if ( !$block instanceof Block && $ip !== null && !in_array( $ip, $wgProxyWhitelist ) ) {
                        // Local list
                        if ( self::isLocallyBlockedProxy( $ip ) ) {
                                $block = new Block;
@@ -1576,7 +1539,6 @@ class User implements IDBAccessObject {
                if ( !$block instanceof Block
                        && $wgApplyIpBlocksToXff
                        && $ip !== null
-                       && !$this->isAllowed( 'proxyunbannable' )
                        && !in_array( $ip, $wgProxyWhitelist )
                ) {
                        $xff = $this->getRequest()->getHeader( 'X-Forwarded-For' );
@@ -2442,6 +2404,9 @@ class User implements IDBAccessObject {
                        ),
                        __METHOD__
                );
+
+               // When the main password is changed, invalidate all bot passwords too
+               BotPassword::invalidateAllPasswordsForUser( $this->getName() );
        }
 
        /**
@@ -3015,6 +2980,12 @@ class User implements IDBAccessObject {
        public function getRights() {
                if ( is_null( $this->mRights ) ) {
                        $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
+
+                       $allowedRights = $this->getRequest()->getSession()->getAllowedUserRights();
+                       if ( $allowedRights !== null ) {
+                               $this->mRights = array_intersect( $this->mRights, $allowedRights );
+                       }
+
                        Hooks::run( 'UserGetRights', array( $this, &$this->mRights ) );
                        // Force reindexation of rights when a hook has unset one of them
                        $this->mRights = array_values( array_unique( $this->mRights ) );
@@ -3276,13 +3247,6 @@ class User implements IDBAccessObject {
                if ( $action === '' ) {
                        return true; // In the spirit of DWIM
                }
-               // Patrolling may not be enabled
-               if ( $action === 'patrol' || $action === 'autopatrol' ) {
-                       global $wgUseRCPatrol, $wgUseNPPatrol;
-                       if ( !$wgUseRCPatrol && !$wgUseNPPatrol ) {
-                               return false;
-                       }
-               }
                // Use strict parameter to avoid matching numeric 0 accidentally inserted
                // by misconfiguration: 0 == 'foo'
                return in_array( $action, $this->getRights(), true );
@@ -3502,6 +3466,7 @@ class User implements IDBAccessObject {
        /**
         * Set a cookie on the user's client. Wrapper for
         * WebResponse::setCookie
+        * @deprecated since 1.27
         * @param string $name Name of the cookie to set
         * @param string $value Value to set
         * @param int $exp Expiration time, as a UNIX time value;
@@ -3517,6 +3482,7 @@ class User implements IDBAccessObject {
        protected function setCookie(
                $name, $value, $exp = 0, $secure = null, $params = array(), $request = null
        ) {
+               wfDeprecated( __METHOD__, '1.27' );
                if ( $request === null ) {
                        $request = $this->getRequest();
                }
@@ -3526,6 +3492,7 @@ class User implements IDBAccessObject {
 
        /**
         * Clear a cookie on the user's client
+        * @deprecated since 1.27
         * @param string $name Name of the cookie to clear
         * @param bool $secure
         *  true: Force setting the secure attribute when setting the cookie
@@ -3534,6 +3501,7 @@ class User implements IDBAccessObject {
         * @param array $params Array of options sent passed to WebResponse::setcookie()
         */
        protected function clearCookie( $name, $secure = null, $params = array() ) {
+               wfDeprecated( __METHOD__, '1.27' );
                $this->setCookie( $name, '', time() - 86400, $secure, $params );
        }
 
@@ -3544,6 +3512,7 @@ class User implements IDBAccessObject {
         *
         * @see User::setCookie
         *
+        * @deprecated since 1.27
         * @param string $name Name of the cookie to set
         * @param string $value Value to set
         * @param bool $secure
@@ -3554,6 +3523,8 @@ class User implements IDBAccessObject {
        protected function setExtendedLoginCookie( $name, $value, $secure ) {
                global $wgExtendedLoginCookieExpiration, $wgCookieExpiration;
 
+               wfDeprecated( __METHOD__, '1.27' );
+
                $exp = time();
                $exp += $wgExtendedLoginCookieExpiration !== null
                        ? $wgExtendedLoginCookieExpiration
@@ -3563,7 +3534,7 @@ class User implements IDBAccessObject {
        }
 
        /**
-        * Set the default cookies for this session on the user's client.
+        * Persist this user's session (e.g. set cookies)
         *
         * @param WebRequest|null $request WebRequest object to use; $wgRequest will be used if null
         *        is passed.
@@ -3571,72 +3542,36 @@ class User implements IDBAccessObject {
         * @param bool $rememberMe Whether to add a Token cookie for elongated sessions
         */
        public function setCookies( $request = null, $secure = null, $rememberMe = false ) {
-               global $wgExtendedLoginCookies;
-
-               if ( $request === null ) {
-                       $request = $this->getRequest();
-               }
-
                $this->load();
                if ( 0 == $this->mId ) {
                        return;
                }
-               if ( !$this->mToken ) {
-                       // When token is empty or NULL generate a new one and then save it to the database
-                       // This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey
-                       // Simply by setting every cell in the user_token column to NULL and letting them be
-                       // regenerated as users log back into the wiki.
-                       $this->setToken();
-                       if ( !wfReadOnly() ) {
-                               $this->saveSettings();
-                       }
-               }
-               $session = array(
-                       'wsUserID' => $this->mId,
-                       'wsToken' => $this->mToken,
-                       'wsUserName' => $this->getName()
-               );
-               $cookies = array(
-                       'UserID' => $this->mId,
-                       'UserName' => $this->getName(),
-               );
-               if ( $rememberMe ) {
-                       $cookies['Token'] = $this->mToken;
-               } else {
-                       $cookies['Token'] = false;
-               }
 
-               Hooks::run( 'UserSetCookies', array( $this, &$session, &$cookies ) );
-
-               foreach ( $session as $name => $value ) {
-                       $request->setSessionData( $name, $value );
+               $session = $this->getRequest()->getSession();
+               if ( $request && $session->getRequest() !== $request ) {
+                       $session = $session->sessionWithRequest( $request );
                }
-               foreach ( $cookies as $name => $value ) {
-                       if ( $value === false ) {
-                               $this->clearCookie( $name );
-                       } elseif ( $rememberMe && in_array( $name, $wgExtendedLoginCookies ) ) {
-                               $this->setExtendedLoginCookie( $name, $value, $secure );
-                       } else {
-                               $this->setCookie( $name, $value, 0, $secure, array(), $request );
+               $delay = $session->delaySave();
+
+               if ( !$session->getUser()->equals( $this ) ) {
+                       if ( !$session->canSetUser() ) {
+                               \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
+                                       ->warning( __METHOD__ .
+                                               ": Cannot save user \"$this\" to a user \"{$session->getUser()}\"'s immutable session"
+                                       );
+                               return;
                        }
+                       $session->setUser( $this );
                }
 
-               /**
-                * If wpStickHTTPS was selected, also set an insecure cookie that
-                * will cause the site to redirect the user to HTTPS, if they access
-                * it over HTTP. Bug 29898. Use an un-prefixed cookie, so it's the same
-                * as the one set by centralauth (bug 53538). Also set it to session, or
-                * standard time setting, based on if rememberme was set.
-                */
-               if ( $request->getCheck( 'wpStickHTTPS' ) || $this->requiresHTTPS() ) {
-                       $this->setCookie(
-                               'forceHTTPS',
-                               'true',
-                               $rememberMe ? 0 : null,
-                               false,
-                               array( 'prefix' => '' ) // no prefix
-                       );
+               $session->setRememberUser( $rememberMe );
+               if ( $secure !== null ) {
+                       $session->setForceHTTPS( $secure );
                }
+
+               $session->persist();
+
+               ScopedCallback::consume( $delay );
        }
 
        /**
@@ -3649,20 +3584,29 @@ class User implements IDBAccessObject {
        }
 
        /**
-        * Clear the user's cookies and session, and reset the instance cache.
+        * Clear the user's session, and reset the instance cache.
         * @see logout()
         */
        public function doLogout() {
-               $this->clearInstanceCache( 'defaults' );
-
-               $this->getRequest()->setSessionData( 'wsUserID', 0 );
-
-               $this->clearCookie( 'UserID' );
-               $this->clearCookie( 'Token' );
-               $this->clearCookie( 'forceHTTPS', false, array( 'prefix' => '' ) );
-
-               // Remember when user logged out, to prevent seeing cached pages
-               $this->setCookie( 'LoggedOut', time(), time() + 86400 );
+               $session = $this->getRequest()->getSession();
+               if ( !$session->canSetUser() ) {
+                       \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
+                               ->warning( __METHOD__ . ": Cannot log out of an immutable session" );
+               } elseif ( !$session->getUser()->equals( $this ) ) {
+                       \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
+                               ->warning( __METHOD__ .
+                                       ": Cannot log user \"$this\" out of a user \"{$session->getUser()}\"'s session"
+                               );
+                       // But we still may as well make this user object anon
+                       $this->clearInstanceCache( 'defaults' );
+               } else {
+                       $this->clearInstanceCache( 'defaults' );
+                       $delay = $session->delaySave();
+                       $session->setLoggedOutTimestamp( time() );
+                       $session->setUser( new User );
+                       $session->set( 'wsUserID', 0 ); // Other code expects this
+                       ScopedCallback::consume( $delay );
+               }
        }
 
        /**
@@ -4160,17 +4104,6 @@ class User implements IDBAccessObject {
                );
        }
 
-       /**
-        * Generate a looking random token for various uses.
-        *
-        * @return string The new random token
-        * @deprecated since 1.20: Use MWCryptRand for secure purposes or
-        *   wfRandomString for pseudo-randomness.
-        */
-       public static function generateToken() {
-               return MWCryptRand::generateHex( 32 );
-       }
-
        /**
         * Get the embedded timestamp from a token.
         * @param string $val Input token
@@ -4570,7 +4503,14 @@ class User implements IDBAccessObject {
        }
 
        /**
-        * Check if all users have the given permission
+        * Check if all users may be assumed to have the given permission
+        *
+        * We generally assume so if the right is granted to '*' and isn't revoked
+        * on any group. It doesn't attempt to take grants or other extension
+        * limitations on rights into account in the general case, though, as that
+        * would require it to always return false and defeat the purpose.
+        * Specifically, session-based rights restrictions (such as OAuth or bot
+        * passwords) are applied based on the current session.
         *
         * @since 1.22
         * @param string $right Right to check
@@ -4599,7 +4539,14 @@ class User implements IDBAccessObject {
                        }
                }
 
-               // Allow extensions (e.g. OAuth) to say false
+               // Remove any rights that aren't allowed to the global-session user
+               $allowedRights = SessionManager::getGlobalSession()->getAllowedUserRights();
+               if ( $allowedRights !== null && !in_array( $right, $allowedRights, true ) ) {
+                       $cache[$right] = false;
+                       return false;
+               }
+
+               // Allow extensions to say false
                if ( !Hooks::run( 'UserIsEveryoneAllowed', array( $right ) ) ) {
                        $cache[$right] = false;
                        return false;