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