MediaWiki REL1_34
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 $this->useCrossSiteCookies = strcasecmp( $config->get( 'CookieSameSite' ), 'none' ) === 0;
102
103 // @codeCoverageIgnoreStart
104 $this->cookieOptions += [
105 // @codeCoverageIgnoreEnd
106 'prefix' => $config->get( 'CookiePrefix' ),
107 'path' => $config->get( 'CookiePath' ),
108 'domain' => $config->get( 'CookieDomain' ),
109 'secure' => $config->get( 'CookieSecure' ) || $this->config->get( 'ForceHTTPS' ),
110 'httpOnly' => $config->get( 'CookieHttpOnly' ),
111 'sameSite' => $config->get( 'CookieSameSite' ),
112 ];
113 }
114
115 public function provideSessionInfo( WebRequest $request ) {
116 $sessionId = $this->getCookie( $request, $this->params['sessionName'], '' );
117 $info = [
118 'provider' => $this,
119 'forceHTTPS' => $this->getCookie( $request, 'forceHTTPS', '', false )
120 ];
121 if ( SessionManager::validateSessionId( $sessionId ) ) {
122 $info['id'] = $sessionId;
123 $info['persisted'] = true;
124 }
125
126 list( $userId, $userName, $token ) = $this->getUserInfoFromCookies( $request );
127 if ( $userId !== null ) {
128 try {
129 $userInfo = UserInfo::newFromId( $userId );
130 } catch ( \InvalidArgumentException $ex ) {
131 return null;
132 }
133
134 // Sanity check
135 if ( $userName !== null && $userInfo->getName() !== $userName ) {
136 $this->logger->warning(
137 'Session "{session}" requested with mismatched UserID and UserName cookies.',
138 [
139 'session' => $sessionId,
140 'mismatch' => [
141 'userid' => $userId,
142 'cookie_username' => $userName,
143 'username' => $userInfo->getName(),
144 ],
145 ] );
146 return null;
147 }
148
149 if ( $token !== null ) {
150 if ( !hash_equals( $userInfo->getToken(), $token ) ) {
151 $this->logger->warning(
152 'Session "{session}" requested with invalid Token cookie.',
153 [
154 'session' => $sessionId,
155 'userid' => $userId,
156 'username' => $userInfo->getName(),
157 ] );
158 return null;
159 }
160 $info['userInfo'] = $userInfo->verified();
161 $info['persisted'] = true; // If we have user+token, it should be
162 } elseif ( isset( $info['id'] ) ) {
163 $info['userInfo'] = $userInfo;
164 } else {
165 // No point in returning, loadSessionInfoFromStore() will
166 // reject it anyway.
167 return null;
168 }
169 } elseif ( isset( $info['id'] ) ) {
170 // No UserID cookie, so insist that the session is anonymous.
171 // Note: this event occurs for several normal activities:
172 // * anon visits Special:UserLogin
173 // * anon browsing after seeing Special:UserLogin
174 // * anon browsing after edit or preview
175 $this->logger->debug(
176 'Session "{session}" requested without UserID cookie',
177 [
178 'session' => $info['id'],
179 ] );
180 $info['userInfo'] = UserInfo::newAnonymous();
181 } else {
182 // No session ID and no user is the same as an empty session, so
183 // there's no point.
184 return null;
185 }
186
187 return new SessionInfo( $this->priority, $info );
188 }
189
190 public function persistsSessionId() {
191 return true;
192 }
193
194 public function canChangeUser() {
195 return true;
196 }
197
198 public function persistSession( SessionBackend $session, WebRequest $request ) {
199 $response = $request->response();
200 if ( $response->headersSent() ) {
201 // Can't do anything now
202 $this->logger->debug( __METHOD__ . ': Headers already sent' );
203 return;
204 }
205
206 $user = $session->getUser();
207
208 $cookies = $this->cookieDataToExport( $user, $session->shouldRememberUser() );
209 $sessionData = $this->sessionDataToExport( $user );
210
211 // Legacy hook
212 if ( $this->params['callUserSetCookiesHook'] && !$user->isAnon() ) {
213 \Hooks::run( 'UserSetCookies', [ $user, &$sessionData, &$cookies ] );
214 }
215
216 $options = $this->cookieOptions;
217
218 $forceHTTPS = $session->shouldForceHTTPS() || $user->requiresHTTPS();
219 if ( $forceHTTPS ) {
220 $options['secure'] = $this->config->get( 'CookieSecure' )
221 || $this->config->get( 'ForceHTTPS' );
222 }
223
224 $response->setCookie( $this->params['sessionName'], $session->getId(), null,
225 [ 'prefix' => '' ] + $options
226 );
227
228 foreach ( $cookies as $key => $value ) {
229 if ( $value === false ) {
230 $response->clearCookie( $key, $options );
231 } else {
232 $expirationDuration = $this->getLoginCookieExpiration( $key, $session->shouldRememberUser() );
233 $expiration = $expirationDuration ? $expirationDuration + time() : null;
234 $response->setCookie( $key, (string)$value, $expiration, $options );
235 }
236 }
237
238 $this->setForceHTTPSCookie( $forceHTTPS, $session, $request );
239 $this->setLoggedOutCookie( $session->getLoggedOutTimestamp(), $request );
240
241 if ( $sessionData ) {
242 $session->addData( $sessionData );
243 }
244 }
245
246 public function unpersistSession( WebRequest $request ) {
247 $response = $request->response();
248 if ( $response->headersSent() ) {
249 // Can't do anything now
250 $this->logger->debug( __METHOD__ . ': Headers already sent' );
251 return;
252 }
253
254 $cookies = [
255 'UserID' => false,
256 'Token' => false,
257 ];
258
259 $response->clearCookie(
260 $this->params['sessionName'], [ 'prefix' => '' ] + $this->cookieOptions
261 );
262
263 foreach ( $cookies as $key => $value ) {
264 $response->clearCookie( $key, $this->cookieOptions );
265 }
266
267 $this->setForceHTTPSCookie( false, null, $request );
268 }
269
277 protected function setForceHTTPSCookie(
278 $set, SessionBackend $backend = null, WebRequest $request
279 ) {
280 if ( $this->config->get( 'ForceHTTPS' ) ) {
281 // No need to send a cookie if the wiki is always HTTPS (T256095)
282 return;
283 }
284 $response = $request->response();
285 if ( $set ) {
286 if ( $backend->shouldRememberUser() ) {
287 $expirationDuration = $this->getLoginCookieExpiration(
288 'forceHTTPS',
289 true
290 );
291 $expiration = $expirationDuration ? $expirationDuration + time() : null;
292 } else {
293 $expiration = null;
294 }
295 $response->setCookie( 'forceHTTPS', 'true', $expiration,
296 [ 'prefix' => '', 'secure' => false ] + $this->cookieOptions );
297 } else {
298 $response->clearCookie( 'forceHTTPS',
299 [ 'prefix' => '', 'secure' => false ] + $this->cookieOptions );
300 }
301 }
302
308 protected function setLoggedOutCookie( $loggedOut, WebRequest $request ) {
309 if ( $loggedOut + 86400 > time() &&
310 $loggedOut !== (int)$this->getCookie( $request, 'LoggedOut', $this->cookieOptions['prefix'] )
311 ) {
312 $request->response()->setCookie( 'LoggedOut', $loggedOut, $loggedOut + 86400,
313 $this->cookieOptions );
314 }
315 }
316
317 public function getVaryCookies() {
318 return [
319 // Vary on token and session because those are the real authn
320 // determiners. UserID and UserName don't matter without those.
321 $this->cookieOptions['prefix'] . 'Token',
322 $this->cookieOptions['prefix'] . 'LoggedOut',
323 $this->params['sessionName'],
324 'forceHTTPS',
325 ];
326 }
327
328 public function suggestLoginUsername( WebRequest $request ) {
329 $name = $this->getCookie( $request, 'UserName', $this->cookieOptions['prefix'] );
330 if ( $name !== null ) {
331 $name = User::getCanonicalName( $name, 'usable' );
332 }
333 return $name === false ? null : $name;
334 }
335
341 protected function getUserInfoFromCookies( $request ) {
342 $prefix = $this->cookieOptions['prefix'];
343 return [
344 $this->getCookie( $request, 'UserID', $prefix ),
345 $this->getCookie( $request, 'UserName', $prefix ),
346 $this->getCookie( $request, 'Token', $prefix ),
347 ];
348 }
349
358 protected function getCookie( $request, $key, $prefix, $default = null ) {
359 if ( $this->useCrossSiteCookies ) {
360 $value = $request->getCrossSiteCookie( $key, $prefix, $default );
361 } else {
362 $value = $request->getCookie( $key, $prefix, $default );
363 }
364 if ( $value === 'deleted' ) {
365 // PHP uses this value when deleting cookies. A legitimate cookie will never have
366 // this value (usernames start with uppercase, token is longer, other auth cookies
367 // are booleans or integers). Seeing this means that in a previous request we told the
368 // client to delete the cookie, but it has poor cookie handling. Pretend the cookie is
369 // not there to avoid invalidating the session.
370 return null;
371 }
372 return $value;
373 }
374
381 protected function cookieDataToExport( $user, $remember ) {
382 if ( $user->isAnon() ) {
383 return [
384 'UserID' => false,
385 'Token' => false,
386 ];
387 } else {
388 return [
389 'UserID' => $user->getId(),
390 'UserName' => $user->getName(),
391 'Token' => $remember ? (string)$user->getToken() : false,
392 ];
393 }
394 }
395
401 protected function sessionDataToExport( $user ) {
402 // If we're calling the legacy hook, we should populate $session
403 // like User::setCookies() did.
404 if ( !$user->isAnon() && $this->params['callUserSetCookiesHook'] ) {
405 return [
406 'wsUserID' => $user->getId(),
407 'wsToken' => $user->getToken(),
408 'wsUserName' => $user->getName(),
409 ];
410 }
411
412 return [];
413 }
414
415 public function whyNoSession() {
416 return wfMessage( 'sessionprovider-nocookies' );
417 }
418
419 public function getRememberUserDuration() {
420 return min( $this->getLoginCookieExpiration( 'UserID', true ),
421 $this->getLoginCookieExpiration( 'Token', true ) ) ?: null;
422 }
423
430 protected function getExtendedLoginCookies() {
431 return [ 'UserID', 'UserName', 'Token' ];
432 }
433
444 protected function getLoginCookieExpiration( $cookieName, $shouldRememberUser ) {
445 $extendedCookies = $this->getExtendedLoginCookies();
446 $normalExpiration = $this->config->get( 'CookieExpiration' );
447
448 if ( $shouldRememberUser && in_array( $cookieName, $extendedCookies, true ) ) {
449 $extendedExpiration = $this->config->get( 'ExtendedLoginCookieExpiration' );
450
451 return ( $extendedExpiration !== null ) ? (int)$extendedExpiration : (int)$normalExpiration;
452 } else {
453 return (int)$normalExpiration;
454 }
455 }
456}
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.
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.
setForceHTTPSCookie( $set, SessionBackend $backend=null, WebRequest $request)
Set the "forceHTTPS" cookie, unless $wgForceHTTPS prevents it.
getUserInfoFromCookies( $request)
Fetch the user identity from cookies.
whyNoSession()
Return a Message for why sessions might not be being persisted.
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...
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:51
static getCanonicalName( $name, $validate='valid')
Given unvalidated user input, return a canonical username, or false if the username is invalid.
Definition User.php:1180
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:28
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".