MediaWiki  master
CookieSessionProvider.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Session;
25 
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  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  $this->useCrossSiteCookies = strcasecmp( $this->getConfig()->get( 'CookieSameSite' ), 'none' ) === 0;
100 
101  // @codeCoverageIgnoreStart
102  $this->cookieOptions += [
103  // @codeCoverageIgnoreEnd
104  'prefix' => $this->getConfig()->get( 'CookiePrefix' ),
105  'path' => $this->getConfig()->get( 'CookiePath' ),
106  'domain' => $this->getConfig()->get( 'CookieDomain' ),
107  'secure' => $this->getConfig()->get( 'CookieSecure' ) || $this->getConfig()->get( 'ForceHTTPS' ),
108  'httpOnly' => $this->getConfig()->get( 'CookieHttpOnly' ),
109  'sameSite' => $this->getConfig()->get( 'CookieSameSite' ),
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  list( $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  // Sanity check
133  if ( $userName !== null && $userInfo->getName() !== $userName ) {
134  $this->logger->warning(
135  'Session "{session}" requested with mismatched UserID and UserName cookies.',
136  [
137  'session' => $sessionId,
138  'mismatch' => [
139  'userid' => $userId,
140  'cookie_username' => $userName,
141  'username' => $userInfo->getName(),
142  ],
143  ] );
144  return null;
145  }
146 
147  if ( $token !== null ) {
148  if ( !hash_equals( $userInfo->getToken(), $token ) ) {
149  $this->logger->warning(
150  'Session "{session}" requested with invalid Token cookie.',
151  [
152  'session' => $sessionId,
153  'userid' => $userId,
154  'username' => $userInfo->getName(),
155  ] );
156  return null;
157  }
158  $info['userInfo'] = $userInfo->verified();
159  $info['persisted'] = true; // If we have user+token, it should be
160  } elseif ( isset( $info['id'] ) ) {
161  $info['userInfo'] = $userInfo;
162  } else {
163  // No point in returning, loadSessionInfoFromStore() will
164  // reject it anyway.
165  return null;
166  }
167  } elseif ( isset( $info['id'] ) ) {
168  // No UserID cookie, so insist that the session is anonymous.
169  // Note: this event occurs for several normal activities:
170  // * anon visits Special:UserLogin
171  // * anon browsing after seeing Special:UserLogin
172  // * anon browsing after edit or preview
173  $this->logger->debug(
174  'Session "{session}" requested without UserID cookie',
175  [
176  'session' => $info['id'],
177  ] );
178  $info['userInfo'] = UserInfo::newAnonymous();
179  } else {
180  // No session ID and no user is the same as an empty session, so
181  // there's no point.
182  return null;
183  }
184 
185  return new SessionInfo( $this->priority, $info );
186  }
187 
188  public function persistsSessionId() {
189  return true;
190  }
191 
192  public function canChangeUser() {
193  return true;
194  }
195 
196  public function persistSession( SessionBackend $session, WebRequest $request ) {
197  $response = $request->response();
198  if ( $response->headersSent() ) {
199  // Can't do anything now
200  $this->logger->debug( __METHOD__ . ': Headers already sent' );
201  return;
202  }
203 
204  $user = $session->getUser();
205 
206  $cookies = $this->cookieDataToExport( $user, $session->shouldRememberUser() );
207  $sessionData = $this->sessionDataToExport( $user );
208 
209  // Legacy hook
210  if ( $this->params['callUserSetCookiesHook'] && !$user->isAnon() ) {
211  $this->getHookRunner()->onUserSetCookies( $user, $sessionData, $cookies );
212  }
213 
214  $options = $this->cookieOptions;
215 
216  $forceHTTPS = $session->shouldForceHTTPS() || $user->requiresHTTPS();
217  if ( $forceHTTPS ) {
218  $options['secure'] = $this->getConfig()->get( 'CookieSecure' )
219  || $this->getConfig()->get( 'ForceHTTPS' );
220  }
221 
222  $response->setCookie( $this->params['sessionName'], $session->getId(), null,
223  [ 'prefix' => '' ] + $options
224  );
225 
226  foreach ( $cookies as $key => $value ) {
227  if ( $value === false ) {
228  $response->clearCookie( $key, $options );
229  } else {
230  $expirationDuration = $this->getLoginCookieExpiration( $key, $session->shouldRememberUser() );
231  $expiration = $expirationDuration ? $expirationDuration + time() : null;
232  $response->setCookie( $key, (string)$value, $expiration, $options );
233  }
234  }
235 
236  $this->setForceHTTPSCookie( $forceHTTPS, $session, $request );
237  $this->setLoggedOutCookie( $session->getLoggedOutTimestamp(), $request );
238 
239  if ( $sessionData ) {
240  $session->addData( $sessionData );
241  }
242  }
243 
244  public function unpersistSession( WebRequest $request ) {
245  $response = $request->response();
246  if ( $response->headersSent() ) {
247  // Can't do anything now
248  $this->logger->debug( __METHOD__ . ': Headers already sent' );
249  return;
250  }
251 
252  $cookies = [
253  'UserID' => false,
254  'Token' => false,
255  ];
256 
257  $response->clearCookie(
258  $this->params['sessionName'], [ 'prefix' => '' ] + $this->cookieOptions
259  );
260 
261  foreach ( $cookies as $key => $value ) {
262  $response->clearCookie( $key, $this->cookieOptions );
263  }
264 
265  $this->setForceHTTPSCookie( false, null, $request );
266  }
267 
275  protected function setForceHTTPSCookie( $set, ?SessionBackend $backend, WebRequest $request ) {
276  if ( $this->getConfig()->get( 'ForceHTTPS' ) ) {
277  // No need to send a cookie if the wiki is always HTTPS (T256095)
278  return;
279  }
280  $response = $request->response();
281  if ( $set ) {
282  if ( $backend->shouldRememberUser() ) {
283  $expirationDuration = $this->getLoginCookieExpiration(
284  'forceHTTPS',
285  true
286  );
287  $expiration = $expirationDuration ? $expirationDuration + time() : null;
288  } else {
289  $expiration = null;
290  }
291  $response->setCookie( 'forceHTTPS', 'true', $expiration,
292  [ 'prefix' => '', 'secure' => false ] + $this->cookieOptions );
293  } else {
294  $response->clearCookie( 'forceHTTPS',
295  [ 'prefix' => '', 'secure' => false ] + $this->cookieOptions );
296  }
297  }
298 
303  protected function setLoggedOutCookie( $loggedOut, WebRequest $request ) {
304  if ( $loggedOut + 86400 > time() &&
305  $loggedOut !== (int)$this->getCookie( $request, 'LoggedOut', $this->cookieOptions['prefix'] )
306  ) {
307  $request->response()->setCookie( 'LoggedOut', $loggedOut, $loggedOut + 86400,
308  $this->cookieOptions );
309  }
310  }
311 
312  public function getVaryCookies() {
313  return [
314  // Vary on token and session because those are the real authn
315  // determiners. UserID and UserName don't matter without those.
316  $this->cookieOptions['prefix'] . 'Token',
317  $this->cookieOptions['prefix'] . 'LoggedOut',
318  $this->params['sessionName'],
319  'forceHTTPS',
320  ];
321  }
322 
323  public function suggestLoginUsername( WebRequest $request ) {
324  $name = $this->getCookie( $request, 'UserName', $this->cookieOptions['prefix'] );
325  if ( $name !== null ) {
326  $name = $this->userNameUtils->getCanonical( $name, UserNameUtils::RIGOR_USABLE );
327  }
328  return $name === false ? null : $name;
329  }
330 
336  protected function getUserInfoFromCookies( $request ) {
337  $prefix = $this->cookieOptions['prefix'];
338  return [
339  $this->getCookie( $request, 'UserID', $prefix ),
340  $this->getCookie( $request, 'UserName', $prefix ),
341  $this->getCookie( $request, 'Token', $prefix ),
342  ];
343  }
344 
353  protected function getCookie( $request, $key, $prefix, $default = null ) {
354  if ( $this->useCrossSiteCookies ) {
355  $value = $request->getCrossSiteCookie( $key, $prefix, $default );
356  } else {
357  $value = $request->getCookie( $key, $prefix, $default );
358  }
359  if ( $value === 'deleted' ) {
360  // PHP uses this value when deleting cookies. A legitimate cookie will never have
361  // this value (usernames start with uppercase, token is longer, other auth cookies
362  // are booleans or integers). Seeing this means that in a previous request we told the
363  // client to delete the cookie, but it has poor cookie handling. Pretend the cookie is
364  // not there to avoid invalidating the session.
365  return null;
366  }
367  return $value;
368  }
369 
376  protected function cookieDataToExport( $user, $remember ) {
377  if ( $user->isAnon() ) {
378  return [
379  'UserID' => false,
380  'Token' => false,
381  ];
382  } else {
383  return [
384  'UserID' => $user->getId(),
385  'UserName' => $user->getName(),
386  'Token' => $remember ? (string)$user->getToken() : false,
387  ];
388  }
389  }
390 
396  protected function sessionDataToExport( $user ) {
397  // If we're calling the legacy hook, we should populate $session
398  // like User::setCookies() did.
399  if ( !$user->isAnon() && $this->params['callUserSetCookiesHook'] ) {
400  return [
401  'wsUserID' => $user->getId(),
402  'wsToken' => $user->getToken(),
403  'wsUserName' => $user->getName(),
404  ];
405  }
406 
407  return [];
408  }
409 
410  public function whyNoSession() {
411  return wfMessage( 'sessionprovider-nocookies' );
412  }
413 
414  public function getRememberUserDuration() {
415  return min( $this->getLoginCookieExpiration( 'UserID', true ),
416  $this->getLoginCookieExpiration( 'Token', true ) ) ?: null;
417  }
418 
425  protected function getExtendedLoginCookies() {
426  return [ 'UserID', 'UserName', 'Token' ];
427  }
428 
439  protected function getLoginCookieExpiration( $cookieName, $shouldRememberUser ) {
440  $extendedCookies = $this->getExtendedLoginCookies();
441  $normalExpiration = $this->getConfig()->get( 'CookieExpiration' );
442 
443  if ( $shouldRememberUser && in_array( $cookieName, $extendedCookies, true ) ) {
444  $extendedExpiration = $this->getConfig()->get( 'ExtendedLoginCookieExpiration' );
445 
446  return ( $extendedExpiration !== null ) ? (int)$extendedExpiration : (int)$normalExpiration;
447  } else {
448  return (int)$normalExpiration;
449  }
450  }
451 }
MediaWiki\Session\CookieSessionProvider\canChangeUser
canChangeUser()
Indicate whether the user associated with the request can be changed.
Definition: CookieSessionProvider.php:192
MediaWiki\Session\CookieSessionProvider\getExtendedLoginCookies
getExtendedLoginCookies()
Gets the list of cookies that must be set to the 'remember me' duration, if $wgExtendedLoginCookieExp...
Definition: CookieSessionProvider.php:425
MediaWiki\Session\UserInfo\newAnonymous
static newAnonymous()
Create an instance for an anonymous (i.e.
Definition: UserInfo.php:78
MediaWiki\Session\CookieSessionProvider\getCookie
getCookie( $request, $key, $prefix, $default=null)
Get a cookie.
Definition: CookieSessionProvider.php:353
MediaWiki\Session\SessionBackend\shouldRememberUser
shouldRememberUser()
Indicate whether the user should be remembered independently of the session ID.
Definition: SessionBackend.php:380
MediaWiki\Session\SessionBackend\getUser
getUser()
Returns the authenticated user for this session.
Definition: SessionBackend.php:418
MediaWiki\Session\CookieSessionProvider\sessionDataToExport
sessionDataToExport( $user)
Return extra data to store in the session.
Definition: CookieSessionProvider.php:396
MediaWiki\Session\SessionBackend\getId
getId()
Returns the session ID.
Definition: SessionBackend.php:252
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1186
MediaWiki\Session\CookieSessionProvider\provideSessionInfo
provideSessionInfo(WebRequest $request)
Provide session info for a request.
Definition: CookieSessionProvider.php:113
MediaWiki\Session\CookieSessionProvider\unpersistSession
unpersistSession(WebRequest $request)
Remove any persisted session from a request/response.
Definition: CookieSessionProvider.php:244
MediaWiki\Session\CookieSessionProvider\getUserInfoFromCookies
getUserInfoFromCookies( $request)
Fetch the user identity from cookies.
Definition: CookieSessionProvider.php:336
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:196
MediaWiki\Session\CookieSessionProvider\whyNoSession
whyNoSession()
Return a Message for why sessions might not be being persisted.For example, "check whether you're blo...
Definition: CookieSessionProvider.php:410
MediaWiki\Session\SessionManager\validateSessionId
static validateSessionId( $id)
Validate a session ID.
Definition: SessionManager.php:420
MediaWiki\Session\SessionProvider
A SessionProvider provides SessionInfo and support for Session.
Definition: SessionProvider.php:81
MediaWiki\Session\CookieSessionProvider\$cookieOptions
mixed[] $cookieOptions
Definition: CookieSessionProvider.php:42
MediaWiki\Session\SessionBackend\addData
addData(array $newData)
Add data to the session.
Definition: SessionBackend.php:573
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:439
MediaWiki\Session\SessionInfo\MAX_PRIORITY
const MAX_PRIORITY
Maximum allowed priority.
Definition: SessionInfo.php:42
WebRequest\response
response()
Return a handle to WebResponse style object, for setting cookies, headers and other stuff,...
Definition: WebRequest.php:1091
MediaWiki\Session\CookieSessionProvider\__construct
__construct( $params=[])
Definition: CookieSessionProvider.php:61
MediaWiki\Session\CookieSessionProvider\setForceHTTPSCookie
setForceHTTPSCookie( $set, ?SessionBackend $backend, WebRequest $request)
Set the "forceHTTPS" cookie, unless $wgForceHTTPS prevents it.
Definition: CookieSessionProvider.php:275
MediaWiki\Session\SessionBackend\shouldForceHTTPS
shouldForceHTTPS()
Whether HTTPS should be forced.
Definition: SessionBackend.php:478
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:312
MediaWiki\Session\SessionInfo
Value object returned by SessionProvider.
Definition: SessionInfo.php:37
MediaWiki\Session\CookieSessionProvider\$params
mixed[] $params
Definition: CookieSessionProvider.php:39
MediaWiki\Session\SessionProvider\getConfig
getConfig()
Get the config.
Definition: SessionProvider.php:190
MediaWiki\Session\CookieSessionProvider\cookieDataToExport
cookieDataToExport( $user, $remember)
Return the data to store in cookies.
Definition: CookieSessionProvider.php:376
MediaWiki\Session\SessionBackend\getLoggedOutTimestamp
getLoggedOutTimestamp()
Fetch the "logged out" timestamp.
Definition: SessionBackend.php:503
MediaWiki\User\UserNameUtils
UserNameUtils service.
Definition: UserNameUtils.php:42
MediaWiki\Session\UserInfo\newFromId
static newFromId( $id, $verified=false)
Create an instance for a logged-in user by ID.
Definition: UserInfo.php:88
MediaWiki\Session\CookieSessionProvider\suggestLoginUsername
suggestLoginUsername(WebRequest $request)
Get a suggested username for the login form.
Definition: CookieSessionProvider.php:323
MediaWiki\Session\CookieSessionProvider\getRememberUserDuration
getRememberUserDuration()
Returns the duration (in seconds) for which users will be remembered when Session::setRememberUser() ...
Definition: CookieSessionProvider.php:414
MediaWiki\Session\CookieSessionProvider\postInitSetup
postInitSetup()
A provider can override this to do any necessary setup after init() is called.
Definition: CookieSessionProvider.php:90
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:188
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:68
MediaWiki\Session\SessionInfo\MIN_PRIORITY
const MIN_PRIORITY
Minimum allowed priority.
Definition: SessionInfo.php:39
MediaWiki\Session\CookieSessionProvider\setLoggedOutCookie
setLoggedOutCookie( $loggedOut, WebRequest $request)
Definition: CookieSessionProvider.php:303
MediaWiki\Session\SessionBackend
This is the actual workhorse for Session.
Definition: SessionBackend.php:53
MediaWiki\Session\SessionProvider\getHookRunner
getHookRunner()
Get the HookRunner.
Definition: SessionProvider.php:251