MediaWiki  1.34.4
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 
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 }
MediaWiki\Session\CookieSessionProvider\canChangeUser
canChangeUser()
Indicate whether the user associated with the request can be changed.
Definition: CookieSessionProvider.php:194
MediaWiki\Session\CookieSessionProvider\getExtendedLoginCookies
getExtendedLoginCookies()
Gets the list of cookies that must be set to the 'remember me' duration, if $wgExtendedLoginCookieExp...
Definition: CookieSessionProvider.php:430
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:358
MediaWiki\Session\SessionBackend\shouldRememberUser
shouldRememberUser()
Indicate whether the user should be remembered independently of the session ID.
Definition: SessionBackend.php:353
MediaWiki\Session\SessionBackend\getUser
getUser()
Returns the authenticated user for this session.
Definition: SessionBackend.php:391
MediaWiki\Session\CookieSessionProvider\setForceHTTPSCookie
setForceHTTPSCookie( $set, SessionBackend $backend=null, WebRequest $request)
Set the "forceHTTPS" cookie, unless $wgForceHTTPS prevents it.
Definition: CookieSessionProvider.php:277
$response
$response
Definition: opensearch_desc.php:38
MediaWiki\Session\CookieSessionProvider\sessionDataToExport
sessionDataToExport( $user)
Return extra data to store in the session.
Definition: CookieSessionProvider.php:401
MediaWiki\Session\SessionBackend\getId
getId()
Returns the session ID.
Definition: SessionBackend.php:225
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1263
MediaWiki\Session\CookieSessionProvider\provideSessionInfo
provideSessionInfo(WebRequest $request)
Provide session info for a request.
Definition: CookieSessionProvider.php:115
MediaWiki\Session\CookieSessionProvider\unpersistSession
unpersistSession(WebRequest $request)
Remove any persisted session from a request/response.
Definition: CookieSessionProvider.php:246
Config
Interface for configuration instances.
Definition: Config.php:28
MediaWiki\Session\CookieSessionProvider\getUserInfoFromCookies
getUserInfoFromCookies( $request)
Fetch the user identity from cookies.
Definition: CookieSessionProvider.php:341
MediaWiki\Session\CookieSessionProvider\$useCrossSiteCookies
bool $useCrossSiteCookies
Definition: CookieSessionProvider.php:45
MediaWiki\Session\CookieSessionProvider\persistSession
persistSession(SessionBackend $session, WebRequest $request)
Persist a session into a request/response.
Definition: CookieSessionProvider.php:198
MediaWiki\Session\CookieSessionProvider\whyNoSession
whyNoSession()
Return a Message for why sessions might not be being persisted.
Definition: CookieSessionProvider.php:415
MediaWiki\Session\SessionManager\validateSessionId
static validateSessionId( $id)
Validate a session ID.
Definition: SessionManager.php:365
MediaWiki\Session\SessionProvider
A SessionProvider provides SessionInfo and support for Session.
Definition: SessionProvider.php:78
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:90
MediaWiki\Session\SessionBackend\addData
addData(array $newData)
Add data to the session.
Definition: SessionBackend.php:548
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:444
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:1069
MediaWiki\Session\CookieSessionProvider\__construct
__construct( $params=[])
Definition: CookieSessionProvider.php:61
MediaWiki\Session\SessionBackend\shouldForceHTTPS
shouldForceHTTPS()
Whether HTTPS should be forced.
Definition: SessionBackend.php:451
MediaWiki\Session\SessionProvider\$config
Config $config
Definition: SessionProvider.php:84
WebRequest
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:42
MediaWiki\Session\CookieSessionProvider\getVaryCookies
getVaryCookies()
Return the list of cookies that need varying on.
Definition: CookieSessionProvider.php:317
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:381
MediaWiki\Session\SessionBackend\getLoggedOutTimestamp
getLoggedOutTimestamp()
Fetch the "logged out" timestamp.
Definition: SessionBackend.php:476
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:328
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:1180
MediaWiki\Session\CookieSessionProvider\getRememberUserDuration
getRememberUserDuration()
Returns the duration (in seconds) for which users will be remembered when Session::setRememberUser() ...
Definition: CookieSessionProvider.php:419
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:190
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:51
MediaWiki\Session\SessionInfo\MIN_PRIORITY
const MIN_PRIORITY
Minimum allowed priority.
Definition: SessionInfo.php:36
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
MediaWiki\Session\CookieSessionProvider\setLoggedOutCookie
setLoggedOutCookie( $loggedOut, WebRequest $request)
Set the "logged out" cookie.
Definition: CookieSessionProvider.php:308
MediaWiki\Session\SessionBackend
This is the actual workhorse for Session.
Definition: SessionBackend.php:50