MediaWiki REL1_40
CookieSessionProvider.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Session;
25
28use User;
29use WebRequest;
30
38
40 protected $params = [];
41
43 protected $cookieOptions = [];
44
47
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 $this->params += [
92 'sessionName' =>
94 ?: $this->getConfig()->get( MainConfigNames::CookiePrefix ) . '_session',
95 ];
96
97 $sameSite = $this->getConfig()->get( MainConfigNames::CookieSameSite );
98 $this->useCrossSiteCookies = $sameSite !== null && strcasecmp( $sameSite, 'none' ) === 0;
99
100 // @codeCoverageIgnoreStart
101 $this->cookieOptions += [
102 // @codeCoverageIgnoreEnd
103 'prefix' => $this->getConfig()->get( MainConfigNames::CookiePrefix ),
104 'path' => $this->getConfig()->get( MainConfigNames::CookiePath ),
105 'domain' => $this->getConfig()->get( MainConfigNames::CookieDomain ),
106 'secure' => $this->getConfig()->get( MainConfigNames::CookieSecure )
107 || $this->getConfig()->get( MainConfigNames::ForceHTTPS ),
108 'httpOnly' => $this->getConfig()->get( MainConfigNames::CookieHttpOnly ),
109 'sameSite' => $sameSite,
110 ];
111 }
112
113 public function provideSessionInfo( WebRequest $request ) {
114 $sessionId = $this->getCookie( $request, $this->params['sessionName'], '' );
115 $info = [
116 'provider' => $this,
117 'forceHTTPS' => $this->getCookie( $request, 'forceHTTPS', '', false )
118 ];
119 if ( SessionManager::validateSessionId( $sessionId ) ) {
120 $info['id'] = $sessionId;
121 $info['persisted'] = true;
122 }
123
124 [ $userId, $userName, $token ] = $this->getUserInfoFromCookies( $request );
125 if ( $userId !== null ) {
126 try {
127 $userInfo = UserInfo::newFromId( $userId );
128 } catch ( \InvalidArgumentException $ex ) {
129 return null;
130 }
131
132 if ( $userName !== null && $userInfo->getName() !== $userName ) {
133 $this->logger->warning(
134 'Session "{session}" requested with mismatched UserID and UserName cookies.',
135 [
136 'session' => $sessionId,
137 'mismatch' => [
138 'userid' => $userId,
139 'cookie_username' => $userName,
140 'username' => $userInfo->getName(),
141 ],
142 ] );
143 return null;
144 }
145
146 if ( $token !== null ) {
147 if ( !hash_equals( $userInfo->getToken(), $token ) ) {
148 $this->logger->warning(
149 'Session "{session}" requested with invalid Token cookie.',
150 [
151 'session' => $sessionId,
152 'userid' => $userId,
153 'username' => $userInfo->getName(),
154 ] );
155 return null;
156 }
157 $info['userInfo'] = $userInfo->verified();
158 $info['persisted'] = true; // If we have user+token, it should be
159 } elseif ( isset( $info['id'] ) ) {
160 $info['userInfo'] = $userInfo;
161 } else {
162 // No point in returning, loadSessionInfoFromStore() will
163 // reject it anyway.
164 return null;
165 }
166 } elseif ( isset( $info['id'] ) ) {
167 // No UserID cookie, so insist that the session is anonymous.
168 // Note: this event occurs for several normal activities:
169 // * anon visits Special:UserLogin
170 // * anon browsing after seeing Special:UserLogin
171 // * anon browsing after edit or preview
172 $this->logger->debug(
173 'Session "{session}" requested without UserID cookie',
174 [
175 'session' => $info['id'],
176 ] );
177 $info['userInfo'] = UserInfo::newAnonymous();
178 } else {
179 // No session ID and no user is the same as an empty session, so
180 // there's no point.
181 return null;
182 }
183
184 return new SessionInfo( $this->priority, $info );
185 }
186
187 public function persistsSessionId() {
188 return true;
189 }
190
191 public function canChangeUser() {
192 return true;
193 }
194
195 public function persistSession( SessionBackend $session, WebRequest $request ) {
196 $response = $request->response();
197 if ( $response->headersSent() ) {
198 // Can't do anything now
199 $this->logger->debug( __METHOD__ . ': Headers already sent' );
200 return;
201 }
202
203 $user = $session->getUser();
204
205 $cookies = $this->cookieDataToExport( $user, $session->shouldRememberUser() );
206 $sessionData = $this->sessionDataToExport( $user );
207
208 $options = $this->cookieOptions;
209
210 $forceHTTPS = $session->shouldForceHTTPS() || $user->requiresHTTPS();
211 if ( $forceHTTPS ) {
212 $options['secure'] = $this->getConfig()->get( MainConfigNames::CookieSecure )
213 || $this->getConfig()->get( MainConfigNames::ForceHTTPS );
214 }
215
216 $response->setCookie( $this->params['sessionName'], $session->getId(), null,
217 [ 'prefix' => '' ] + $options
218 );
219
220 foreach ( $cookies as $key => $value ) {
221 if ( $value === false ) {
222 $response->clearCookie( $key, $options );
223 } else {
224 $expirationDuration = $this->getLoginCookieExpiration( $key, $session->shouldRememberUser() );
225 $expiration = $expirationDuration ? $expirationDuration + time() : null;
226 $response->setCookie( $key, (string)$value, $expiration, $options );
227 }
228 }
229
230 $this->setForceHTTPSCookie( $forceHTTPS, $session, $request );
231 $this->setLoggedOutCookie( $session->getLoggedOutTimestamp(), $request );
232
233 if ( $sessionData ) {
234 $session->addData( $sessionData );
235 }
236 }
237
238 public function unpersistSession( WebRequest $request ) {
239 $response = $request->response();
240 if ( $response->headersSent() ) {
241 // Can't do anything now
242 $this->logger->debug( __METHOD__ . ': Headers already sent' );
243 return;
244 }
245
246 $cookies = [
247 'UserID' => false,
248 'Token' => false,
249 ];
250
251 $response->clearCookie(
252 $this->params['sessionName'], [ 'prefix' => '' ] + $this->cookieOptions
253 );
254
255 foreach ( $cookies as $key => $value ) {
256 $response->clearCookie( $key, $this->cookieOptions );
257 }
258
259 $this->setForceHTTPSCookie( false, null, $request );
260 }
261
269 protected function setForceHTTPSCookie( $set, ?SessionBackend $backend, WebRequest $request ) {
270 if ( $this->getConfig()->get( MainConfigNames::ForceHTTPS ) ) {
271 // No need to send a cookie if the wiki is always HTTPS (T256095)
272 return;
273 }
274 $response = $request->response();
275 if ( $set ) {
276 if ( $backend->shouldRememberUser() ) {
277 $expirationDuration = $this->getLoginCookieExpiration(
278 'forceHTTPS',
279 true
280 );
281 $expiration = $expirationDuration ? $expirationDuration + time() : null;
282 } else {
283 $expiration = null;
284 }
285 $response->setCookie( 'forceHTTPS', 'true', $expiration,
286 [ 'prefix' => '', 'secure' => false ] + $this->cookieOptions );
287 } else {
288 $response->clearCookie( 'forceHTTPS',
289 [ 'prefix' => '', 'secure' => false ] + $this->cookieOptions );
290 }
291 }
292
297 protected function setLoggedOutCookie( $loggedOut, WebRequest $request ) {
298 if ( $loggedOut + 86400 > time() &&
299 $loggedOut !== (int)$this->getCookie( $request, 'LoggedOut', $this->cookieOptions['prefix'] )
300 ) {
301 $request->response()->setCookie( 'LoggedOut', (string)$loggedOut, $loggedOut + 86400,
302 $this->cookieOptions );
303 }
304 }
305
306 public function getVaryCookies() {
307 return [
308 // Vary on token and session because those are the real authn
309 // determiners. UserID and UserName don't matter without those.
310 $this->cookieOptions['prefix'] . 'Token',
311 $this->cookieOptions['prefix'] . 'LoggedOut',
312 $this->params['sessionName'],
313 'forceHTTPS',
314 ];
315 }
316
317 public function suggestLoginUsername( WebRequest $request ) {
318 $name = $this->getCookie( $request, 'UserName', $this->cookieOptions['prefix'] );
319 if ( $name !== null ) {
320 if ( $this->userNameUtils->isTemp( $name ) ) {
321 $name = false;
322 } else {
323 $name = $this->userNameUtils->getCanonical( $name, UserRigorOptions::RIGOR_USABLE );
324 }
325 }
326 return $name === false ? null : $name;
327 }
328
334 protected function getUserInfoFromCookies( $request ) {
335 $prefix = $this->cookieOptions['prefix'];
336 return [
337 $this->getCookie( $request, 'UserID', $prefix ),
338 $this->getCookie( $request, 'UserName', $prefix ),
339 $this->getCookie( $request, 'Token', $prefix ),
340 ];
341 }
342
351 protected function getCookie( $request, $key, $prefix, $default = null ) {
352 if ( $this->useCrossSiteCookies ) {
353 $value = $request->getCrossSiteCookie( $key, $prefix, $default );
354 } else {
355 $value = $request->getCookie( $key, $prefix, $default );
356 }
357 if ( $value === 'deleted' ) {
358 // PHP uses this value when deleting cookies. A legitimate cookie will never have
359 // this value (usernames start with uppercase, token is longer, other auth cookies
360 // are booleans or integers). Seeing this means that in a previous request we told the
361 // client to delete the cookie, but it has poor cookie handling. Pretend the cookie is
362 // not there to avoid invalidating the session.
363 return null;
364 }
365 return $value;
366 }
367
374 protected function cookieDataToExport( $user, $remember ) {
375 if ( $user->isAnon() ) {
376 return [
377 'UserID' => false,
378 'Token' => false,
379 ];
380 } else {
381 return [
382 'UserID' => $user->getId(),
383 'UserName' => $user->getName(),
384 'Token' => $remember ? (string)$user->getToken() : false,
385 ];
386 }
387 }
388
394 protected function sessionDataToExport( $user ) {
395 return [];
396 }
397
398 public function whyNoSession() {
399 return wfMessage( 'sessionprovider-nocookies' );
400 }
401
402 public function getRememberUserDuration() {
403 return min( $this->getLoginCookieExpiration( 'UserID', true ),
404 $this->getLoginCookieExpiration( 'Token', true ) ) ?: null;
405 }
406
413 protected function getExtendedLoginCookies() {
414 return [ 'UserID', 'UserName', 'Token' ];
415 }
416
427 protected function getLoginCookieExpiration( $cookieName, $shouldRememberUser ) {
428 $extendedCookies = $this->getExtendedLoginCookies();
429 $normalExpiration = $this->getConfig()->get( MainConfigNames::CookieExpiration );
430
431 if ( $shouldRememberUser && in_array( $cookieName, $extendedCookies, true ) ) {
432 $extendedExpiration = $this->getConfig()->get( MainConfigNames::ExtendedLoginCookieExpiration );
433
434 return ( $extendedExpiration !== null ) ? (int)$extendedExpiration : (int)$normalExpiration;
435 } else {
436 return (int)$normalExpiration;
437 }
438 }
439}
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
A class containing constants representing the names of configuration variables.
const ForceHTTPS
Name constant for the ForceHTTPS setting, for use with Config::get()
const CookieExpiration
Name constant for the CookieExpiration setting, for use with Config::get()
const CookieDomain
Name constant for the CookieDomain setting, for use with Config::get()
const CookiePath
Name constant for the CookiePath setting, for use with Config::get()
const CookieSameSite
Name constant for the CookieSameSite setting, for use with Config::get()
const CookieSecure
Name constant for the CookieSecure setting, for use with Config::get()
const SessionName
Name constant for the SessionName setting, for use with Config::get()
const ExtendedLoginCookieExpiration
Name constant for the ExtendedLoginCookieExpiration setting, for use with Config::get()
const CookiePrefix
Name constant for the CookiePrefix setting, for use with Config::get()
const CookieHttpOnly
Name constant for the CookieHttpOnly setting, for use with Config::get()
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
internal since 1.36
Definition User.php:71
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,...
Shared interface for rigor levels when dealing with User methods.