MediaWiki REL1_35
CookieSessionProvider.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Session;
25
26use Config;
27use User;
28use WebRequest;
29
37
39 protected $params = [];
40
42 protected $cookieOptions = [];
43
46
61 public function __construct( $params = [] ) {
62 parent::__construct();
63
64 $params += [
65 'cookieOptions' => [],
66 // @codeCoverageIgnoreStart
67 ];
68 // @codeCoverageIgnoreEnd
69
70 if ( !isset( $params['priority'] ) ) {
71 throw new \InvalidArgumentException( __METHOD__ . ': priority must be specified' );
72 }
73 if ( $params['priority'] < SessionInfo::MIN_PRIORITY ||
75 ) {
76 throw new \InvalidArgumentException( __METHOD__ . ': Invalid priority' );
77 }
78
79 if ( !is_array( $params['cookieOptions'] ) ) {
80 throw new \InvalidArgumentException( __METHOD__ . ': cookieOptions must be an array' );
81 }
82
83 $this->priority = $params['priority'];
84 $this->cookieOptions = $params['cookieOptions'];
85 $this->params = $params;
86 unset( $this->params['priority'] );
87 unset( $this->params['cookieOptions'] );
88 }
89
90 public function setConfig( Config $config ) {
91 parent::setConfig( $config );
92
93 // @codeCoverageIgnoreStart
94 $this->params += [
95 // @codeCoverageIgnoreEnd
96 'callUserSetCookiesHook' => false,
97 'sessionName' =>
98 $config->get( 'SessionName' ) ?: $config->get( 'CookiePrefix' ) . '_session',
99 ];
100
101 $sameSite = $config->get( 'CookieSameSite' );
102 $this->useCrossSiteCookies = $sameSite !== null && strcasecmp( $sameSite, 'none' ) === 0;
103
104 // @codeCoverageIgnoreStart
105 $this->cookieOptions += [
106 // @codeCoverageIgnoreEnd
107 'prefix' => $config->get( 'CookiePrefix' ),
108 'path' => $config->get( 'CookiePath' ),
109 'domain' => $config->get( 'CookieDomain' ),
110 'secure' => $config->get( 'CookieSecure' ) || $this->config->get( 'ForceHTTPS' ),
111 'httpOnly' => $config->get( 'CookieHttpOnly' ),
112 'sameSite' => $sameSite
113 ];
114 }
115
116 public function provideSessionInfo( WebRequest $request ) {
117 $sessionId = $this->getCookie( $request, $this->params['sessionName'], '' );
118 $info = [
119 'provider' => $this,
120 'forceHTTPS' => $this->getCookie( $request, 'forceHTTPS', '', false )
121 ];
122 if ( SessionManager::validateSessionId( $sessionId ) ) {
123 $info['id'] = $sessionId;
124 $info['persisted'] = true;
125 }
126
127 list( $userId, $userName, $token ) = $this->getUserInfoFromCookies( $request );
128 if ( $userId !== null ) {
129 try {
130 $userInfo = UserInfo::newFromId( $userId );
131 } catch ( \InvalidArgumentException $ex ) {
132 return null;
133 }
134
135 // Sanity check
136 if ( $userName !== null && $userInfo->getName() !== $userName ) {
137 $this->logger->warning(
138 'Session "{session}" requested with mismatched UserID and UserName cookies.',
139 [
140 'session' => $sessionId,
141 'mismatch' => [
142 'userid' => $userId,
143 'cookie_username' => $userName,
144 'username' => $userInfo->getName(),
145 ],
146 ] );
147 return null;
148 }
149
150 if ( $token !== null ) {
151 if ( !hash_equals( $userInfo->getToken(), $token ) ) {
152 $this->logger->warning(
153 'Session "{session}" requested with invalid Token cookie.',
154 [
155 'session' => $sessionId,
156 'userid' => $userId,
157 'username' => $userInfo->getName(),
158 ] );
159 return null;
160 }
161 $info['userInfo'] = $userInfo->verified();
162 $info['persisted'] = true; // If we have user+token, it should be
163 } elseif ( isset( $info['id'] ) ) {
164 $info['userInfo'] = $userInfo;
165 } else {
166 // No point in returning, loadSessionInfoFromStore() will
167 // reject it anyway.
168 return null;
169 }
170 } elseif ( isset( $info['id'] ) ) {
171 // No UserID cookie, so insist that the session is anonymous.
172 // Note: this event occurs for several normal activities:
173 // * anon visits Special:UserLogin
174 // * anon browsing after seeing Special:UserLogin
175 // * anon browsing after edit or preview
176 $this->logger->debug(
177 'Session "{session}" requested without UserID cookie',
178 [
179 'session' => $info['id'],
180 ] );
181 $info['userInfo'] = UserInfo::newAnonymous();
182 } else {
183 // No session ID and no user is the same as an empty session, so
184 // there's no point.
185 return null;
186 }
187
188 return new SessionInfo( $this->priority, $info );
189 }
190
191 public function persistsSessionId() {
192 return true;
193 }
194
195 public function canChangeUser() {
196 return true;
197 }
198
199 public function persistSession( SessionBackend $session, WebRequest $request ) {
200 $response = $request->response();
201 if ( $response->headersSent() ) {
202 // Can't do anything now
203 $this->logger->debug( __METHOD__ . ': Headers already sent' );
204 return;
205 }
206
207 $user = $session->getUser();
208
209 $cookies = $this->cookieDataToExport( $user, $session->shouldRememberUser() );
210 $sessionData = $this->sessionDataToExport( $user );
211
212 // Legacy hook
213 if ( $this->params['callUserSetCookiesHook'] && !$user->isAnon() ) {
214 $this->getHookRunner()->onUserSetCookies( $user, $sessionData, $cookies );
215 }
216
217 $options = $this->cookieOptions;
218
219 $forceHTTPS = $session->shouldForceHTTPS() || $user->requiresHTTPS();
220 if ( $forceHTTPS ) {
221 $options['secure'] = $this->config->get( 'CookieSecure' )
222 || $this->config->get( 'ForceHTTPS' );
223 }
224
225 $response->setCookie( $this->params['sessionName'], $session->getId(), null,
226 [ 'prefix' => '' ] + $options
227 );
228
229 foreach ( $cookies as $key => $value ) {
230 if ( $value === false ) {
231 $response->clearCookie( $key, $options );
232 } else {
233 $expirationDuration = $this->getLoginCookieExpiration( $key, $session->shouldRememberUser() );
234 $expiration = $expirationDuration ? $expirationDuration + time() : null;
235 $response->setCookie( $key, (string)$value, $expiration, $options );
236 }
237 }
238
239 $this->setForceHTTPSCookie( $forceHTTPS, $session, $request );
240 $this->setLoggedOutCookie( $session->getLoggedOutTimestamp(), $request );
241
242 if ( $sessionData ) {
243 $session->addData( $sessionData );
244 }
245 }
246
247 public function unpersistSession( WebRequest $request ) {
248 $response = $request->response();
249 if ( $response->headersSent() ) {
250 // Can't do anything now
251 $this->logger->debug( __METHOD__ . ': Headers already sent' );
252 return;
253 }
254
255 $cookies = [
256 'UserID' => false,
257 'Token' => false,
258 ];
259
260 $response->clearCookie(
261 $this->params['sessionName'], [ 'prefix' => '' ] + $this->cookieOptions
262 );
263
264 foreach ( $cookies as $key => $value ) {
265 $response->clearCookie( $key, $this->cookieOptions );
266 }
267
268 $this->setForceHTTPSCookie( false, null, $request );
269 }
270
278 protected function setForceHTTPSCookie( $set, ?SessionBackend $backend, WebRequest $request ) {
279 if ( $this->config->get( 'ForceHTTPS' ) ) {
280 // No need to send a cookie if the wiki is always HTTPS (T256095)
281 return;
282 }
283 $response = $request->response();
284 if ( $set ) {
285 if ( $backend->shouldRememberUser() ) {
286 $expirationDuration = $this->getLoginCookieExpiration(
287 'forceHTTPS',
288 true
289 );
290 $expiration = $expirationDuration ? $expirationDuration + time() : null;
291 } else {
292 $expiration = null;
293 }
294 $response->setCookie( 'forceHTTPS', 'true', $expiration,
295 [ 'prefix' => '', 'secure' => false ] + $this->cookieOptions );
296 } else {
297 $response->clearCookie( 'forceHTTPS',
298 [ 'prefix' => '', 'secure' => false ] + $this->cookieOptions );
299 }
300 }
301
307 protected function setLoggedOutCookie( $loggedOut, WebRequest $request ) {
308 if ( $loggedOut + 86400 > time() &&
309 $loggedOut !== (int)$this->getCookie( $request, 'LoggedOut', $this->cookieOptions['prefix'] )
310 ) {
311 $request->response()->setCookie( 'LoggedOut', $loggedOut, $loggedOut + 86400,
312 $this->cookieOptions );
313 }
314 }
315
316 public function getVaryCookies() {
317 return [
318 // Vary on token and session because those are the real authn
319 // determiners. UserID and UserName don't matter without those.
320 $this->cookieOptions['prefix'] . 'Token',
321 $this->cookieOptions['prefix'] . 'LoggedOut',
322 $this->params['sessionName'],
323 'forceHTTPS',
324 ];
325 }
326
327 public function suggestLoginUsername( WebRequest $request ) {
328 $name = $this->getCookie( $request, 'UserName', $this->cookieOptions['prefix'] );
329 if ( $name !== null ) {
330 $name = User::getCanonicalName( $name, 'usable' );
331 }
332 return $name === false ? null : $name;
333 }
334
340 protected function getUserInfoFromCookies( $request ) {
341 $prefix = $this->cookieOptions['prefix'];
342 return [
343 $this->getCookie( $request, 'UserID', $prefix ),
344 $this->getCookie( $request, 'UserName', $prefix ),
345 $this->getCookie( $request, 'Token', $prefix ),
346 ];
347 }
348
357 protected function getCookie( $request, $key, $prefix, $default = null ) {
358 if ( $this->useCrossSiteCookies ) {
359 $value = $request->getCrossSiteCookie( $key, $prefix, $default );
360 } else {
361 $value = $request->getCookie( $key, $prefix, $default );
362 }
363 if ( $value === 'deleted' ) {
364 // PHP uses this value when deleting cookies. A legitimate cookie will never have
365 // this value (usernames start with uppercase, token is longer, other auth cookies
366 // are booleans or integers). Seeing this means that in a previous request we told the
367 // client to delete the cookie, but it has poor cookie handling. Pretend the cookie is
368 // not there to avoid invalidating the session.
369 return null;
370 }
371 return $value;
372 }
373
380 protected function cookieDataToExport( $user, $remember ) {
381 if ( $user->isAnon() ) {
382 return [
383 'UserID' => false,
384 'Token' => false,
385 ];
386 } else {
387 return [
388 'UserID' => $user->getId(),
389 'UserName' => $user->getName(),
390 'Token' => $remember ? (string)$user->getToken() : false,
391 ];
392 }
393 }
394
400 protected function sessionDataToExport( $user ) {
401 // If we're calling the legacy hook, we should populate $session
402 // like User::setCookies() did.
403 if ( !$user->isAnon() && $this->params['callUserSetCookiesHook'] ) {
404 return [
405 'wsUserID' => $user->getId(),
406 'wsToken' => $user->getToken(),
407 'wsUserName' => $user->getName(),
408 ];
409 }
410
411 return [];
412 }
413
414 public function whyNoSession() {
415 return wfMessage( 'sessionprovider-nocookies' );
416 }
417
418 public function getRememberUserDuration() {
419 return min( $this->getLoginCookieExpiration( 'UserID', true ),
420 $this->getLoginCookieExpiration( 'Token', true ) ) ?: null;
421 }
422
429 protected function getExtendedLoginCookies() {
430 return [ 'UserID', 'UserName', 'Token' ];
431 }
432
443 protected function getLoginCookieExpiration( $cookieName, $shouldRememberUser ) {
444 $extendedCookies = $this->getExtendedLoginCookies();
445 $normalExpiration = $this->config->get( 'CookieExpiration' );
446
447 if ( $shouldRememberUser && in_array( $cookieName, $extendedCookies, true ) ) {
448 $extendedExpiration = $this->config->get( 'ExtendedLoginCookieExpiration' );
449
450 return ( $extendedExpiration !== null ) ? (int)$extendedExpiration : (int)$normalExpiration;
451 } else {
452 return (int)$normalExpiration;
453 }
454 }
455}
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
A CookieSessionProvider persists sessions using cookies.
suggestLoginUsername(WebRequest $request)
Get a suggested username for the login form Stable to override.
canChangeUser()
Indicate whether the user associated with the request can be changed.
sessionDataToExport( $user)
Return extra data to store in the session.
persistSession(SessionBackend $session, WebRequest $request)
Persist a session into a request/response.
setConfig(Config $config)
Set configuration.
getUserInfoFromCookies( $request)
Fetch the user identity from cookies.
whyNoSession()
Return a Message for why sessions might not be being persisted.For example, "check whether you're blo...
setLoggedOutCookie( $loggedOut, WebRequest $request)
Set the "logged out" cookie.
getExtendedLoginCookies()
Gets the list of cookies that must be set to the 'remember me' duration, if $wgExtendedLoginCookieExp...
setForceHTTPSCookie( $set, ?SessionBackend $backend, WebRequest $request)
Set the "forceHTTPS" cookie, unless $wgForceHTTPS prevents it.
getVaryCookies()
Return the list of cookies that need varying on.
getCookie( $request, $key, $prefix, $default=null)
Get a cookie.
persistsSessionId()
Indicate whether self::persistSession() can save arbitrary session IDs.
provideSessionInfo(WebRequest $request)
Provide session info for a request.
cookieDataToExport( $user, $remember)
Return the data to store in cookies.
getRememberUserDuration()
Returns the duration (in seconds) for which users will be remembered when Session::setRememberUser() ...
unpersistSession(WebRequest $request)
Remove any persisted session from a request/response.
getLoginCookieExpiration( $cookieName, $shouldRememberUser)
Returns the lifespan of the login cookies, in seconds.
This is the actual workhorse for Session.
addData(array $newData)
Add data to the session.
shouldForceHTTPS()
Whether HTTPS should be forced.
getId()
Returns the session ID.
getLoggedOutTimestamp()
Fetch the "logged out" timestamp.
getUser()
Returns the authenticated user for this session.
shouldRememberUser()
Indicate whether the user should be remembered independently of the session ID.
Value object returned by SessionProvider.
const MIN_PRIORITY
Minimum allowed priority.
const MAX_PRIORITY
Maximum allowed priority.
static validateSessionId( $id)
Validate a session ID.
A SessionProvider provides SessionInfo and support for Session.
static newAnonymous()
Create an instance for an anonymous (i.e.
Definition UserInfo.php:75
static newFromId( $id, $verified=false)
Create an instance for a logged-in user by ID.
Definition UserInfo.php:85
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:60
static getCanonicalName( $name, $validate='valid')
Given unvalidated user input, return a canonical username, or false if the username is invalid.
Definition User.php:1130
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
response()
Return a handle to WebResponse style object, for setting cookies, headers and other stuff,...
Interface for configuration instances.
Definition Config.php:30
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".