3 * MediaWiki cookie-based session provider interface
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
24 namespace MediaWiki\Session
;
31 * A CookieSessionProvider persists sessions using cookies
36 class CookieSessionProvider
extends SessionProvider
{
38 protected $params = array();
39 protected $cookieOptions = array();
42 * @param array $params Keys include:
43 * - priority: (required) Priority of the returned sessions
44 * - callUserSetCookiesHook: Whether to call the deprecated hook
45 * - sessionName: Session cookie name. Doesn't honor 'prefix'. Defaults to
46 * $wgSessionName, or $wgCookiePrefix . '_session' if that is unset.
47 * - cookieOptions: Options to pass to WebRequest::setCookie():
48 * - prefix: Cookie prefix, defaults to $wgCookiePrefix
49 * - path: Cookie path, defaults to $wgCookiePath
50 * - domain: Cookie domain, defaults to $wgCookieDomain
51 * - secure: Cookie secure flag, defaults to $wgCookieSecure
52 * - httpOnly: Cookie httpOnly flag, defaults to $wgCookieHttpOnly
54 public function __construct( $params = array() ) {
55 parent
::__construct();
58 'cookieOptions' => array(),
59 // @codeCoverageIgnoreStart
61 // @codeCoverageIgnoreEnd
63 if ( !isset( $params['priority'] ) ) {
64 throw new \
InvalidArgumentException( __METHOD__
. ': priority must be specified' );
66 if ( $params['priority'] < SessionInfo
::MIN_PRIORITY ||
67 $params['priority'] > SessionInfo
::MAX_PRIORITY
69 throw new \
InvalidArgumentException( __METHOD__
. ': Invalid priority' );
72 if ( !is_array( $params['cookieOptions'] ) ) {
73 throw new \
InvalidArgumentException( __METHOD__
. ': cookieOptions must be an array' );
76 $this->priority
= $params['priority'];
77 $this->cookieOptions
= $params['cookieOptions'];
78 $this->params
= $params;
79 unset( $this->params
['priority'] );
80 unset( $this->params
['cookieOptions'] );
83 public function setConfig( Config
$config ) {
84 parent
::setConfig( $config );
86 // @codeCoverageIgnoreStart
87 $this->params +
= array(
88 // @codeCoverageIgnoreEnd
89 'callUserSetCookiesHook' => false,
91 $config->get( 'SessionName' ) ?
: $config->get( 'CookiePrefix' ) . '_session',
94 // @codeCoverageIgnoreStart
95 $this->cookieOptions +
= array(
96 // @codeCoverageIgnoreEnd
97 'prefix' => $config->get( 'CookiePrefix' ),
98 'path' => $config->get( 'CookiePath' ),
99 'domain' => $config->get( 'CookieDomain' ),
100 'secure' => $config->get( 'CookieSecure' ),
101 'httpOnly' => $config->get( 'CookieHttpOnly' ),
105 public function provideSessionInfo( WebRequest
$request ) {
107 'id' => $this->getCookie( $request, $this->params
['sessionName'], '' ),
109 'forceHTTPS' => $this->getCookie( $request, 'forceHTTPS', '', false )
111 if ( !SessionManager
::validateSessionId( $info['id'] ) ) {
112 unset( $info['id'] );
114 $info['persisted'] = isset( $info['id'] );
116 list( $userId, $userName, $token ) = $this->getUserInfoFromCookies( $request );
117 if ( $userId !== null ) {
119 $userInfo = UserInfo
::newFromId( $userId );
120 } catch ( \InvalidArgumentException
$ex ) {
125 if ( $userName !== null && $userInfo->getName() !== $userName ) {
126 $this->logger
->warning(
127 'Session "{session}" requested with mismatched UserID and UserName cookies.',
129 'session' => $info['id'],
132 'cookie_username' => $userName,
133 'username' => $userInfo->getName(),
139 if ( $token !== null ) {
140 if ( !hash_equals( $userInfo->getToken(), $token ) ) {
141 $this->logger
->warning(
142 'Session "{session}" requested with invalid Token cookie.',
144 'session' => $info['id'],
146 'username' => $userInfo->getName(),
150 $info['userInfo'] = $userInfo->verified();
151 } elseif ( isset( $info['id'] ) ) {
152 $info['userInfo'] = $userInfo;
154 // No point in returning, loadSessionInfoFromStore() will
158 } elseif ( isset( $info['id'] ) ) {
159 // No UserID cookie, so insist that the session is anonymous.
160 // Note: this event occurs for several normal activities:
161 // * anon visits Special:UserLogin
162 // * anon browsing after seeing Special:UserLogin
163 // * anon browsing after edit or preview
164 $this->logger
->debug(
165 'Session "{session}" requested without UserID cookie',
167 'session' => $info['id'],
169 $info['userInfo'] = UserInfo
::newAnonymous();
171 // No session ID and no user is the same as an empty session, so
176 return new SessionInfo( $this->priority
, $info );
179 public function persistsSessionId() {
183 public function canChangeUser() {
187 public function persistSession( SessionBackend
$session, WebRequest
$request ) {
188 $response = $request->response();
189 if ( $response->headersSent() ) {
190 // Can't do anything now
191 $this->logger
->debug( __METHOD__
. ': Headers already sent' );
195 $user = $session->getUser();
197 $cookies = $this->cookieDataToExport( $user, $session->shouldRememberUser() );
198 $sessionData = $this->sessionDataToExport( $user );
201 if ( $this->params
['callUserSetCookiesHook'] && !$user->isAnon() ) {
202 \Hooks
::run( 'UserSetCookies', array( $user, &$sessionData, &$cookies ) );
205 $options = $this->cookieOptions
;
207 $forceHTTPS = $session->shouldForceHTTPS() ||
$user->requiresHTTPS();
209 // Don't set the secure flag if the request came in
210 // over "http", for backwards compat.
211 // @todo Break that backwards compat properly.
212 $options['secure'] = $this->config
->get( 'CookieSecure' );
215 $response->setCookie( $this->params
['sessionName'], $session->getId(), null,
216 array( 'prefix' => '' ) +
$options
219 $extendedCookies = $this->config
->get( 'ExtendedLoginCookies' );
220 $extendedExpiry = $this->config
->get( 'ExtendedLoginCookieExpiration' );
222 foreach ( $cookies as $key => $value ) {
223 if ( $value === false ) {
224 $response->clearCookie( $key, $options );
226 if ( $extendedExpiry !== null && in_array( $key, $extendedCookies ) ) {
227 $expiry = time() +
(int)$extendedExpiry;
229 $expiry = 0; // Default cookie expiration
231 $response->setCookie( $key, (string)$value, $expiry, $options );
235 $this->setForceHTTPSCookie( $forceHTTPS, $session, $request );
236 $this->setLoggedOutCookie( $session->getLoggedOutTimestamp(), $request );
238 if ( $sessionData ) {
239 $session->addData( $sessionData );
243 public function unpersistSession( WebRequest
$request ) {
244 $response = $request->response();
245 if ( $response->headersSent() ) {
246 // Can't do anything now
247 $this->logger
->debug( __METHOD__
. ': Headers already sent' );
256 $response->clearCookie(
257 $this->params
['sessionName'], array( 'prefix' => '' ) +
$this->cookieOptions
260 foreach ( $cookies as $key => $value ) {
261 $response->clearCookie( $key, $this->cookieOptions
);
264 $this->setForceHTTPSCookie( false, null, $request );
268 * Set the "forceHTTPS" cookie
269 * @param bool $set Whether the cookie should be set or not
270 * @param SessionBackend|null $backend
271 * @param WebRequest $request
273 protected function setForceHTTPSCookie(
274 $set, SessionBackend
$backend = null, WebRequest
$request
276 $response = $request->response();
278 $response->setCookie( 'forceHTTPS', 'true', $backend->shouldRememberUser() ?
0 : null,
279 array( 'prefix' => '', 'secure' => false ) +
$this->cookieOptions
);
281 $response->clearCookie( 'forceHTTPS',
282 array( 'prefix' => '', 'secure' => false ) +
$this->cookieOptions
);
287 * Set the "logged out" cookie
288 * @param int $loggedOut timestamp
289 * @param WebRequest $request
291 protected function setLoggedOutCookie( $loggedOut, WebRequest
$request ) {
292 if ( $loggedOut +
86400 > time() &&
293 $loggedOut !== (int)$this->getCookie( $request, 'LoggedOut', $this->cookieOptions
['prefix'] )
295 $request->response()->setCookie( 'LoggedOut', $loggedOut, $loggedOut +
86400,
296 $this->cookieOptions
);
300 public function getVaryCookies() {
302 // Vary on token and session because those are the real authn
303 // determiners. UserID and UserName don't matter without those.
304 $this->cookieOptions
['prefix'] . 'Token',
305 $this->cookieOptions
['prefix'] . 'LoggedOut',
306 $this->params
['sessionName'],
311 public function suggestLoginUsername( WebRequest
$request ) {
312 $name = $this->getCookie( $request, 'UserName', $this->cookieOptions
['prefix'] );
313 if ( $name !== null ) {
314 $name = User
::getCanonicalName( $name, 'usable' );
316 return $name === false ?
null : $name;
320 * Fetch the user identity from cookies
321 * @param \WebRequest $request
322 * @return array (string|null $id, string|null $username, string|null $token)
324 protected function getUserInfoFromCookies( $request ) {
325 $prefix = $this->cookieOptions
['prefix'];
327 $this->getCookie( $request, 'UserID', $prefix ),
328 $this->getCookie( $request, 'UserName', $prefix ),
329 $this->getCookie( $request, 'Token', $prefix ),
334 * Get a cookie. Contains an auth-specific hack.
335 * @param \WebRequest $request
337 * @param string $prefix
338 * @param mixed $default
341 protected function getCookie( $request, $key, $prefix, $default = null ) {
342 $value = $request->getCookie( $key, $prefix, $default );
343 if ( $value === 'deleted' ) {
344 // PHP uses this value when deleting cookies. A legitimate cookie will never have
345 // this value (usernames start with uppercase, token is longer, other auth cookies
346 // are booleans or integers). Seeing this means that in a previous request we told the
347 // client to delete the cookie, but it has poor cookie handling. Pretend the cookie is
348 // not there to avoid invalidating the session.
355 * Return the data to store in cookies
357 * @param bool $remember
358 * @return array $cookies Set value false to unset the cookie
360 protected function cookieDataToExport( $user, $remember ) {
361 if ( $user->isAnon() ) {
368 'UserID' => $user->getId(),
369 'UserName' => $user->getName(),
370 'Token' => $remember ?
(string)$user->getToken() : false,
376 * Return extra data to store in the session
378 * @return array $session
380 protected function sessionDataToExport( $user ) {
381 // If we're calling the legacy hook, we should populate $session
382 // like User::setCookies() did.
383 if ( !$user->isAnon() && $this->params
['callUserSetCookiesHook'] ) {
385 'wsUserID' => $user->getId(),
386 'wsToken' => $user->getToken(),
387 'wsUserName' => $user->getName(),
394 public function whyNoSession() {
395 return wfMessage( 'sessionprovider-nocookies' );