[], // @codeCoverageIgnoreStart ]; // @codeCoverageIgnoreEnd if ( !isset( $params['priority'] ) ) { throw new \InvalidArgumentException( __METHOD__ . ': priority must be specified' ); } if ( $params['priority'] < SessionInfo::MIN_PRIORITY || $params['priority'] > SessionInfo::MAX_PRIORITY ) { throw new \InvalidArgumentException( __METHOD__ . ': Invalid priority' ); } if ( !is_array( $params['cookieOptions'] ) ) { throw new \InvalidArgumentException( __METHOD__ . ': cookieOptions must be an array' ); } $this->priority = $params['priority']; $this->cookieOptions = $params['cookieOptions']; $this->params = $params; unset( $this->params['priority'] ); unset( $this->params['cookieOptions'] ); } public function setConfig( Config $config ) { parent::setConfig( $config ); // @codeCoverageIgnoreStart $this->params += [ // @codeCoverageIgnoreEnd 'callUserSetCookiesHook' => false, 'sessionName' => $config->get( 'SessionName' ) ?: $config->get( 'CookiePrefix' ) . '_session', ]; // @codeCoverageIgnoreStart $this->cookieOptions += [ // @codeCoverageIgnoreEnd 'prefix' => $config->get( 'CookiePrefix' ), 'path' => $config->get( 'CookiePath' ), 'domain' => $config->get( 'CookieDomain' ), 'secure' => $config->get( 'CookieSecure' ), 'httpOnly' => $config->get( 'CookieHttpOnly' ), ]; } public function provideSessionInfo( WebRequest $request ) { $sessionId = $this->getCookie( $request, $this->params['sessionName'], '' ); $info = [ 'provider' => $this, 'forceHTTPS' => $this->getCookie( $request, 'forceHTTPS', '', false ) ]; if ( SessionManager::validateSessionId( $sessionId ) ) { $info['id'] = $sessionId; $info['persisted'] = true; } list( $userId, $userName, $token ) = $this->getUserInfoFromCookies( $request ); if ( $userId !== null ) { try { $userInfo = UserInfo::newFromId( $userId ); } catch ( \InvalidArgumentException $ex ) { return null; } // Sanity check if ( $userName !== null && $userInfo->getName() !== $userName ) { $this->logger->warning( 'Session "{session}" requested with mismatched UserID and UserName cookies.', [ 'session' => $sessionId, 'mismatch' => [ 'userid' => $userId, 'cookie_username' => $userName, 'username' => $userInfo->getName(), ], ] ); return null; } if ( $token !== null ) { if ( !hash_equals( $userInfo->getToken(), $token ) ) { $this->logger->warning( 'Session "{session}" requested with invalid Token cookie.', [ 'session' => $sessionId, 'userid' => $userId, 'username' => $userInfo->getName(), ] ); return null; } $info['userInfo'] = $userInfo->verified(); $info['persisted'] = true; // If we have user+token, it should be } elseif ( isset( $info['id'] ) ) { $info['userInfo'] = $userInfo; } else { // No point in returning, loadSessionInfoFromStore() will // reject it anyway. return null; } } elseif ( isset( $info['id'] ) ) { // No UserID cookie, so insist that the session is anonymous. // Note: this event occurs for several normal activities: // * anon visits Special:UserLogin // * anon browsing after seeing Special:UserLogin // * anon browsing after edit or preview $this->logger->debug( 'Session "{session}" requested without UserID cookie', [ 'session' => $info['id'], ] ); $info['userInfo'] = UserInfo::newAnonymous(); } else { // No session ID and no user is the same as an empty session, so // there's no point. return null; } return new SessionInfo( $this->priority, $info ); } public function persistsSessionId() { return true; } public function canChangeUser() { return true; } public function persistSession( SessionBackend $session, WebRequest $request ) { $response = $request->response(); if ( $response->headersSent() ) { // Can't do anything now $this->logger->debug( __METHOD__ . ': Headers already sent' ); return; } $user = $session->getUser(); $cookies = $this->cookieDataToExport( $user, $session->shouldRememberUser() ); $sessionData = $this->sessionDataToExport( $user ); // Legacy hook if ( $this->params['callUserSetCookiesHook'] && !$user->isAnon() ) { \Hooks::run( 'UserSetCookies', [ $user, &$sessionData, &$cookies ] ); } $options = $this->cookieOptions; $forceHTTPS = $session->shouldForceHTTPS() || $user->requiresHTTPS(); if ( $forceHTTPS ) { // Don't set the secure flag if the request came in // over "http", for backwards compat. // @todo Break that backwards compat properly. $options['secure'] = $this->config->get( 'CookieSecure' ); } $response->setCookie( $this->params['sessionName'], $session->getId(), null, [ 'prefix' => '' ] + $options ); foreach ( $cookies as $key => $value ) { if ( $value === false ) { $response->clearCookie( $key, $options ); } else { $expirationDuration = $this->getLoginCookieExpiration( $key, $session->shouldRememberUser() ); $expiration = $expirationDuration ? $expirationDuration + time() : null; $response->setCookie( $key, (string)$value, $expiration, $options ); } } $this->setForceHTTPSCookie( $forceHTTPS, $session, $request ); $this->setLoggedOutCookie( $session->getLoggedOutTimestamp(), $request ); if ( $sessionData ) { $session->addData( $sessionData ); } } public function unpersistSession( WebRequest $request ) { $response = $request->response(); if ( $response->headersSent() ) { // Can't do anything now $this->logger->debug( __METHOD__ . ': Headers already sent' ); return; } $cookies = [ 'UserID' => false, 'Token' => false, ]; $response->clearCookie( $this->params['sessionName'], [ 'prefix' => '' ] + $this->cookieOptions ); foreach ( $cookies as $key => $value ) { $response->clearCookie( $key, $this->cookieOptions ); } $this->setForceHTTPSCookie( false, null, $request ); } /** * Set the "forceHTTPS" cookie * @param bool $set Whether the cookie should be set or not * @param SessionBackend|null $backend * @param WebRequest $request */ protected function setForceHTTPSCookie( $set, SessionBackend $backend = null, WebRequest $request ) { $response = $request->response(); if ( $set ) { if ( $backend->shouldRememberUser() ) { $expirationDuration = $this->getLoginCookieExpiration( 'forceHTTPS', true ); $expiration = $expirationDuration ? $expirationDuration + time() : null; } else { $expiration = null; } $response->setCookie( 'forceHTTPS', 'true', $expiration, [ 'prefix' => '', 'secure' => false ] + $this->cookieOptions ); } else { $response->clearCookie( 'forceHTTPS', [ 'prefix' => '', 'secure' => false ] + $this->cookieOptions ); } } /** * Set the "logged out" cookie * @param int $loggedOut timestamp * @param WebRequest $request */ protected function setLoggedOutCookie( $loggedOut, WebRequest $request ) { if ( $loggedOut + 86400 > time() && $loggedOut !== (int)$this->getCookie( $request, 'LoggedOut', $this->cookieOptions['prefix'] ) ) { $request->response()->setCookie( 'LoggedOut', $loggedOut, $loggedOut + 86400, $this->cookieOptions ); } } public function getVaryCookies() { return [ // Vary on token and session because those are the real authn // determiners. UserID and UserName don't matter without those. $this->cookieOptions['prefix'] . 'Token', $this->cookieOptions['prefix'] . 'LoggedOut', $this->params['sessionName'], 'forceHTTPS', ]; } public function suggestLoginUsername( WebRequest $request ) { $name = $this->getCookie( $request, 'UserName', $this->cookieOptions['prefix'] ); if ( $name !== null ) { $name = User::getCanonicalName( $name, 'usable' ); } return $name === false ? null : $name; } /** * Fetch the user identity from cookies * @param \WebRequest $request * @return array (string|null $id, string|null $username, string|null $token) */ protected function getUserInfoFromCookies( $request ) { $prefix = $this->cookieOptions['prefix']; return [ $this->getCookie( $request, 'UserID', $prefix ), $this->getCookie( $request, 'UserName', $prefix ), $this->getCookie( $request, 'Token', $prefix ), ]; } /** * Get a cookie. Contains an auth-specific hack. * @param \WebRequest $request * @param string $key * @param string $prefix * @param mixed $default * @return mixed */ protected function getCookie( $request, $key, $prefix, $default = null ) { $value = $request->getCookie( $key, $prefix, $default ); if ( $value === 'deleted' ) { // PHP uses this value when deleting cookies. A legitimate cookie will never have // this value (usernames start with uppercase, token is longer, other auth cookies // are booleans or integers). Seeing this means that in a previous request we told the // client to delete the cookie, but it has poor cookie handling. Pretend the cookie is // not there to avoid invalidating the session. return null; } return $value; } /** * Return the data to store in cookies * @param User $user * @param bool $remember * @return array $cookies Set value false to unset the cookie */ protected function cookieDataToExport( $user, $remember ) { if ( $user->isAnon() ) { return [ 'UserID' => false, 'Token' => false, ]; } else { return [ 'UserID' => $user->getId(), 'UserName' => $user->getName(), 'Token' => $remember ? (string)$user->getToken() : false, ]; } } /** * Return extra data to store in the session * @param User $user * @return array $session */ protected function sessionDataToExport( $user ) { // If we're calling the legacy hook, we should populate $session // like User::setCookies() did. if ( !$user->isAnon() && $this->params['callUserSetCookiesHook'] ) { return [ 'wsUserID' => $user->getId(), 'wsToken' => $user->getToken(), 'wsUserName' => $user->getName(), ]; } return []; } public function whyNoSession() { return wfMessage( 'sessionprovider-nocookies' ); } public function getRememberUserDuration() { return min( $this->getLoginCookieExpiration( 'UserID', true ), $this->getLoginCookieExpiration( 'Token', true ) ) ?: null; } /** * Gets the list of cookies that must be set to the 'remember me' duration, * if $wgExtendedLoginCookieExpiration is in use. * * @return string[] Array of unprefixed cookie keys */ protected function getExtendedLoginCookies() { return [ 'UserID', 'UserName', 'Token' ]; } /** * Returns the lifespan of the login cookies, in seconds. 0 means until the end of the session. * * Cookies that are session-length do not call this function. * * @param string $cookieName * @param bool $shouldRememberUser Whether the user should be remembered * long-term * @return int Cookie expiration time in seconds; 0 for session cookies */ protected function getLoginCookieExpiration( $cookieName, $shouldRememberUser ) { $extendedCookies = $this->getExtendedLoginCookies(); $normalExpiration = $this->config->get( 'CookieExpiration' ); if ( $shouldRememberUser && in_array( $cookieName, $extendedCookies, true ) ) { $extendedExpiration = $this->config->get( 'ExtendedLoginCookieExpiration' ); return ( $extendedExpiration !== null ) ? (int)$extendedExpiration : (int)$normalExpiration; } else { return (int)$normalExpiration; } } }