MediaWiki REL1_37
CookieSessionProvider.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Session;
25
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 protected function postInitSetup() {
91 // @codeCoverageIgnoreStart
92 $this->params += [
93 // @codeCoverageIgnoreEnd
94 'callUserSetCookiesHook' => false,
95 'sessionName' =>
96 $this->getConfig()->get( 'SessionName' ) ?: $this->getConfig()->get( 'CookiePrefix' ) . '_session',
97 ];
98
99 $sameSite = $this->getConfig()->get( 'CookieSameSite' );
100 $this->useCrossSiteCookies = $sameSite !== null && strcasecmp( $sameSite, 'none' ) === 0;
101
102 // @codeCoverageIgnoreStart
103 $this->cookieOptions += [
104 // @codeCoverageIgnoreEnd
105 'prefix' => $this->getConfig()->get( 'CookiePrefix' ),
106 'path' => $this->getConfig()->get( 'CookiePath' ),
107 'domain' => $this->getConfig()->get( 'CookieDomain' ),
108 'secure' => $this->getConfig()->get( 'CookieSecure' ) || $this->getConfig()->get( 'ForceHTTPS' ),
109 'httpOnly' => $this->getConfig()->get( 'CookieHttpOnly' ),
110 'sameSite' => $sameSite
111 ];
112 }
113
114 public function provideSessionInfo( WebRequest $request ) {
115 $sessionId = $this->getCookie( $request, $this->params['sessionName'], '' );
116 $info = [
117 'provider' => $this,
118 'forceHTTPS' => $this->getCookie( $request, 'forceHTTPS', '', false )
119 ];
120 if ( SessionManager::validateSessionId( $sessionId ) ) {
121 $info['id'] = $sessionId;
122 $info['persisted'] = true;
123 }
124
125 list( $userId, $userName, $token ) = $this->getUserInfoFromCookies( $request );
126 if ( $userId !== null ) {
127 try {
128 $userInfo = UserInfo::newFromId( $userId );
129 } catch ( \InvalidArgumentException $ex ) {
130 return null;
131 }
132
133 // Sanity check
134 if ( $userName !== null && $userInfo->getName() !== $userName ) {
135 $this->logger->warning(
136 'Session "{session}" requested with mismatched UserID and UserName cookies.',
137 [
138 'session' => $sessionId,
139 'mismatch' => [
140 'userid' => $userId,
141 'cookie_username' => $userName,
142 'username' => $userInfo->getName(),
143 ],
144 ] );
145 return null;
146 }
147
148 if ( $token !== null ) {
149 if ( !hash_equals( $userInfo->getToken(), $token ) ) {
150 $this->logger->warning(
151 'Session "{session}" requested with invalid Token cookie.',
152 [
153 'session' => $sessionId,
154 'userid' => $userId,
155 'username' => $userInfo->getName(),
156 ] );
157 return null;
158 }
159 $info['userInfo'] = $userInfo->verified();
160 $info['persisted'] = true; // If we have user+token, it should be
161 } elseif ( isset( $info['id'] ) ) {
162 $info['userInfo'] = $userInfo;
163 } else {
164 // No point in returning, loadSessionInfoFromStore() will
165 // reject it anyway.
166 return null;
167 }
168 } elseif ( isset( $info['id'] ) ) {
169 // No UserID cookie, so insist that the session is anonymous.
170 // Note: this event occurs for several normal activities:
171 // * anon visits Special:UserLogin
172 // * anon browsing after seeing Special:UserLogin
173 // * anon browsing after edit or preview
174 $this->logger->debug(
175 'Session "{session}" requested without UserID cookie',
176 [
177 'session' => $info['id'],
178 ] );
179 $info['userInfo'] = UserInfo::newAnonymous();
180 } else {
181 // No session ID and no user is the same as an empty session, so
182 // there's no point.
183 return null;
184 }
185
186 return new SessionInfo( $this->priority, $info );
187 }
188
189 public function persistsSessionId() {
190 return true;
191 }
192
193 public function canChangeUser() {
194 return true;
195 }
196
197 public function persistSession( SessionBackend $session, WebRequest $request ) {
198 $response = $request->response();
199 if ( $response->headersSent() ) {
200 // Can't do anything now
201 $this->logger->debug( __METHOD__ . ': Headers already sent' );
202 return;
203 }
204
205 $user = $session->getUser();
206
207 $cookies = $this->cookieDataToExport( $user, $session->shouldRememberUser() );
208 $sessionData = $this->sessionDataToExport( $user );
209
210 // Legacy hook
211 if ( $this->params['callUserSetCookiesHook'] && !$user->isAnon() ) {
212 $this->getHookRunner()->onUserSetCookies( $user, $sessionData, $cookies );
213 }
214
215 $options = $this->cookieOptions;
216
217 $forceHTTPS = $session->shouldForceHTTPS() || $user->requiresHTTPS();
218 if ( $forceHTTPS ) {
219 $options['secure'] = $this->getConfig()->get( 'CookieSecure' )
220 || $this->getConfig()->get( 'ForceHTTPS' );
221 }
222
223 $response->setCookie( $this->params['sessionName'], $session->getId(), null,
224 [ 'prefix' => '' ] + $options
225 );
226
227 foreach ( $cookies as $key => $value ) {
228 if ( $value === false ) {
229 $response->clearCookie( $key, $options );
230 } else {
231 $expirationDuration = $this->getLoginCookieExpiration( $key, $session->shouldRememberUser() );
232 $expiration = $expirationDuration ? $expirationDuration + time() : null;
233 $response->setCookie( $key, (string)$value, $expiration, $options );
234 }
235 }
236
237 $this->setForceHTTPSCookie( $forceHTTPS, $session, $request );
238 $this->setLoggedOutCookie( $session->getLoggedOutTimestamp(), $request );
239
240 if ( $sessionData ) {
241 $session->addData( $sessionData );
242 }
243 }
244
245 public function unpersistSession( WebRequest $request ) {
246 $response = $request->response();
247 if ( $response->headersSent() ) {
248 // Can't do anything now
249 $this->logger->debug( __METHOD__ . ': Headers already sent' );
250 return;
251 }
252
253 $cookies = [
254 'UserID' => false,
255 'Token' => false,
256 ];
257
258 $response->clearCookie(
259 $this->params['sessionName'], [ 'prefix' => '' ] + $this->cookieOptions
260 );
261
262 foreach ( $cookies as $key => $value ) {
263 $response->clearCookie( $key, $this->cookieOptions );
264 }
265
266 $this->setForceHTTPSCookie( false, null, $request );
267 }
268
276 protected function setForceHTTPSCookie( $set, ?SessionBackend $backend, WebRequest $request ) {
277 if ( $this->getConfig()->get( 'ForceHTTPS' ) ) {
278 // No need to send a cookie if the wiki is always HTTPS (T256095)
279 return;
280 }
281 $response = $request->response();
282 if ( $set ) {
283 if ( $backend->shouldRememberUser() ) {
284 $expirationDuration = $this->getLoginCookieExpiration(
285 'forceHTTPS',
286 true
287 );
288 $expiration = $expirationDuration ? $expirationDuration + time() : null;
289 } else {
290 $expiration = null;
291 }
292 $response->setCookie( 'forceHTTPS', 'true', $expiration,
293 [ 'prefix' => '', 'secure' => false ] + $this->cookieOptions );
294 } else {
295 $response->clearCookie( 'forceHTTPS',
296 [ 'prefix' => '', 'secure' => false ] + $this->cookieOptions );
297 }
298 }
299
304 protected function setLoggedOutCookie( $loggedOut, WebRequest $request ) {
305 if ( $loggedOut + 86400 > time() &&
306 $loggedOut !== (int)$this->getCookie( $request, 'LoggedOut', $this->cookieOptions['prefix'] )
307 ) {
308 $request->response()->setCookie( 'LoggedOut', $loggedOut, $loggedOut + 86400,
309 $this->cookieOptions );
310 }
311 }
312
313 public function getVaryCookies() {
314 return [
315 // Vary on token and session because those are the real authn
316 // determiners. UserID and UserName don't matter without those.
317 $this->cookieOptions['prefix'] . 'Token',
318 $this->cookieOptions['prefix'] . 'LoggedOut',
319 $this->params['sessionName'],
320 'forceHTTPS',
321 ];
322 }
323
324 public function suggestLoginUsername( WebRequest $request ) {
325 $name = $this->getCookie( $request, 'UserName', $this->cookieOptions['prefix'] );
326 if ( $name !== null ) {
327 $name = $this->userNameUtils->getCanonical( $name, UserNameUtils::RIGOR_USABLE );
328 }
329 return $name === false ? null : $name;
330 }
331
337 protected function getUserInfoFromCookies( $request ) {
338 $prefix = $this->cookieOptions['prefix'];
339 return [
340 $this->getCookie( $request, 'UserID', $prefix ),
341 $this->getCookie( $request, 'UserName', $prefix ),
342 $this->getCookie( $request, 'Token', $prefix ),
343 ];
344 }
345
354 protected function getCookie( $request, $key, $prefix, $default = null ) {
355 if ( $this->useCrossSiteCookies ) {
356 $value = $request->getCrossSiteCookie( $key, $prefix, $default );
357 } else {
358 $value = $request->getCookie( $key, $prefix, $default );
359 }
360 if ( $value === 'deleted' ) {
361 // PHP uses this value when deleting cookies. A legitimate cookie will never have
362 // this value (usernames start with uppercase, token is longer, other auth cookies
363 // are booleans or integers). Seeing this means that in a previous request we told the
364 // client to delete the cookie, but it has poor cookie handling. Pretend the cookie is
365 // not there to avoid invalidating the session.
366 return null;
367 }
368 return $value;
369 }
370
377 protected function cookieDataToExport( $user, $remember ) {
378 if ( $user->isAnon() ) {
379 return [
380 'UserID' => false,
381 'Token' => false,
382 ];
383 } else {
384 return [
385 'UserID' => $user->getId(),
386 'UserName' => $user->getName(),
387 'Token' => $remember ? (string)$user->getToken() : false,
388 ];
389 }
390 }
391
397 protected function sessionDataToExport( $user ) {
398 // If we're calling the legacy hook, we should populate $session
399 // like User::setCookies() did.
400 if ( !$user->isAnon() && $this->params['callUserSetCookiesHook'] ) {
401 return [
402 'wsUserID' => $user->getId(),
403 'wsToken' => $user->getToken(),
404 'wsUserName' => $user->getName(),
405 ];
406 }
407
408 return [];
409 }
410
411 public function whyNoSession() {
412 return wfMessage( 'sessionprovider-nocookies' );
413 }
414
415 public function getRememberUserDuration() {
416 return min( $this->getLoginCookieExpiration( 'UserID', true ),
417 $this->getLoginCookieExpiration( 'Token', true ) ) ?: null;
418 }
419
426 protected function getExtendedLoginCookies() {
427 return [ 'UserID', 'UserName', 'Token' ];
428 }
429
440 protected function getLoginCookieExpiration( $cookieName, $shouldRememberUser ) {
441 $extendedCookies = $this->getExtendedLoginCookies();
442 $normalExpiration = $this->getConfig()->get( 'CookieExpiration' );
443
444 if ( $shouldRememberUser && in_array( $cookieName, $extendedCookies, true ) ) {
445 $extendedExpiration = $this->getConfig()->get( 'ExtendedLoginCookieExpiration' );
446
447 return ( $extendedExpiration !== null ) ? (int)$extendedExpiration : (int)$normalExpiration;
448 } else {
449 return (int)$normalExpiration;
450 }
451 }
452}
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.
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)
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.
postInitSetup()
A provider can override this to do any necessary setup after init() is called.
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:78
static newFromId( $id, $verified=false)
Create an instance for a logged-in user by ID.
Definition UserInfo.php:88
UserNameUtils service.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:69
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,...