SessionManager: Change behavior of getSessionById()
[mediawiki.git] / includes / session / CookieSessionProvider.php
blobf92a519ac29ac116b20f2d822ea1a69a6715d300
1 <?php
2 /**
3 * MediaWiki cookie-based session provider interface
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
20 * @file
21 * @ingroup Session
24 namespace MediaWiki\Session;
26 use Config;
27 use User;
28 use WebRequest;
30 /**
31 * A CookieSessionProvider persists sessions using cookies
33 * @ingroup Session
34 * @since 1.27
36 class CookieSessionProvider extends SessionProvider {
38 protected $params = array();
39 protected $cookieOptions = array();
41 /**
42 * @param array $params Keys include:
43 * - priority: (required) Priority of the returned sessions
44 * - callUserSetCookiesHook: Whether to call the deprecated hook
45 * - sessionName: Session cookie name. Doesn't honor 'prefix'. Defaults to
46 * $wgSessionName, or $wgCookiePrefix . '_session' if that is unset.
47 * - cookieOptions: Options to pass to WebRequest::setCookie():
48 * - prefix: Cookie prefix, defaults to $wgCookiePrefix
49 * - path: Cookie path, defaults to $wgCookiePath
50 * - domain: Cookie domain, defaults to $wgCookieDomain
51 * - secure: Cookie secure flag, defaults to $wgCookieSecure
52 * - httpOnly: Cookie httpOnly flag, defaults to $wgCookieHttpOnly
54 public function __construct( $params = array() ) {
55 parent::__construct();
57 $params += array(
58 'cookieOptions' => array(),
59 // @codeCoverageIgnoreStart
61 // @codeCoverageIgnoreEnd
63 if ( !isset( $params['priority'] ) ) {
64 throw new \InvalidArgumentException( __METHOD__ . ': priority must be specified' );
66 if ( $params['priority'] < SessionInfo::MIN_PRIORITY ||
67 $params['priority'] > SessionInfo::MAX_PRIORITY
68 ) {
69 throw new \InvalidArgumentException( __METHOD__ . ': Invalid priority' );
72 if ( !is_array( $params['cookieOptions'] ) ) {
73 throw new \InvalidArgumentException( __METHOD__ . ': cookieOptions must be an array' );
76 $this->priority = $params['priority'];
77 $this->cookieOptions = $params['cookieOptions'];
78 $this->params = $params;
79 unset( $this->params['priority'] );
80 unset( $this->params['cookieOptions'] );
83 public function setConfig( Config $config ) {
84 parent::setConfig( $config );
86 // @codeCoverageIgnoreStart
87 $this->params += array(
88 // @codeCoverageIgnoreEnd
89 'callUserSetCookiesHook' => false,
90 'sessionName' =>
91 $config->get( 'SessionName' ) ?: $config->get( 'CookiePrefix' ) . '_session',
94 // @codeCoverageIgnoreStart
95 $this->cookieOptions += array(
96 // @codeCoverageIgnoreEnd
97 'prefix' => $config->get( 'CookiePrefix' ),
98 'path' => $config->get( 'CookiePath' ),
99 'domain' => $config->get( 'CookieDomain' ),
100 'secure' => $config->get( 'CookieSecure' ),
101 'httpOnly' => $config->get( 'CookieHttpOnly' ),
105 public function provideSessionInfo( WebRequest $request ) {
106 $info = array(
107 'id' => $request->getCookie( $this->params['sessionName'], '' )
109 if ( !SessionManager::validateSessionId( $info['id'] ) ) {
110 unset( $info['id'] );
113 list( $userId, $userName, $token ) = $this->getUserInfoFromCookies( $request );
114 if ( $userId !== null ) {
115 try {
116 $userInfo = UserInfo::newFromId( $userId );
117 } catch ( \InvalidArgumentException $ex ) {
118 return null;
121 // Sanity check
122 if ( $userName !== null && $userInfo->getName() !== $userName ) {
123 return null;
126 if ( $token !== null ) {
127 if ( !hash_equals( $userInfo->getToken(), $token ) ) {
128 return null;
130 $info['userInfo'] = $userInfo->verified();
131 } elseif ( isset( $info['id'] ) ) { // No point if no session ID
132 $info['userInfo'] = $userInfo;
136 if ( !$info ) {
137 return null;
140 $info += array(
141 'provider' => $this,
142 'persisted' => isset( $info['id'] ),
143 'forceHTTPS' => $request->getCookie( 'forceHTTPS', '', false )
146 return new SessionInfo( $this->priority, $info );
149 public function persistsSessionId() {
150 return true;
153 public function canChangeUser() {
154 return true;
157 public function persistSession( SessionBackend $session, WebRequest $request ) {
158 $response = $request->response();
159 if ( $response->headersSent() ) {
160 // Can't do anything now
161 $this->logger->debug( __METHOD__ . ': Headers already sent' );
162 return;
165 $user = $session->getUser();
167 $cookies = $this->cookieDataToExport( $user, $session->shouldRememberUser() );
168 $sessionData = $this->sessionDataToExport( $user );
170 // Legacy hook
171 if ( $this->params['callUserSetCookiesHook'] && !$user->isAnon() ) {
172 \Hooks::run( 'UserSetCookies', array( $user, &$sessionData, &$cookies ) );
175 $options = $this->cookieOptions;
176 if ( $session->shouldForceHTTPS() || $user->requiresHTTPS() ) {
177 $response->setCookie( 'forceHTTPS', 'true', $session->shouldRememberUser() ? 0 : null,
178 array( 'prefix' => '', 'secure' => false ) + $options );
179 $options['secure'] = true;
182 $response->setCookie( $this->params['sessionName'], $session->getId(), null,
183 array( 'prefix' => '' ) + $options
186 $extendedCookies = $this->config->get( 'ExtendedLoginCookies' );
187 $extendedExpiry = $this->config->get( 'ExtendedLoginCookieExpiration' );
189 foreach ( $cookies as $key => $value ) {
190 if ( $value === false ) {
191 $response->clearCookie( $key, $options );
192 } else {
193 if ( $extendedExpiry !== null && in_array( $key, $extendedCookies ) ) {
194 $expiry = time() + (int)$extendedExpiry;
195 } else {
196 $expiry = 0; // Default cookie expiration
198 $response->setCookie( $key, (string)$value, $expiry, $options );
202 $this->setLoggedOutCookie( $session->getLoggedOutTimestamp(), $request );
204 if ( $sessionData ) {
205 $session->addData( $sessionData );
209 public function unpersistSession( WebRequest $request ) {
210 $response = $request->response();
211 if ( $response->headersSent() ) {
212 // Can't do anything now
213 $this->logger->debug( __METHOD__ . ': Headers already sent' );
214 return;
217 $cookies = array(
218 'UserID' => false,
219 'Token' => false,
222 $response->clearCookie(
223 $this->params['sessionName'], array( 'prefix' => '' ) + $this->cookieOptions
226 foreach ( $cookies as $key => $value ) {
227 $response->clearCookie( $key, $this->cookieOptions );
230 $response->clearCookie( 'forceHTTPS',
231 array( 'prefix' => '', 'secure' => false ) + $this->cookieOptions );
235 * Set the "logged out" cookie
236 * @param int $loggedOut timestamp
237 * @param WebRequest $request
239 protected function setLoggedOutCookie( $loggedOut, WebRequest $request ) {
240 if ( $loggedOut + 86400 > time() &&
241 $loggedOut !== (int)$request->getCookie( 'LoggedOut', $this->cookieOptions['prefix'] )
243 $request->response()->setCookie( 'LoggedOut', $loggedOut, $loggedOut + 86400,
244 $this->cookieOptions );
248 public function getVaryCookies() {
249 return array(
250 // Vary on token and session because those are the real authn
251 // determiners. UserID and UserName don't matter without those.
252 $this->cookieOptions['prefix'] . 'Token',
253 $this->cookieOptions['prefix'] . 'LoggedOut',
254 $this->params['sessionName'],
255 'forceHTTPS',
259 public function suggestLoginUsername( WebRequest $request ) {
260 $name = $request->getCookie( 'UserName', $this->cookieOptions['prefix'] );
261 if ( $name !== null ) {
262 $name = User::getCanonicalName( $name, 'usable' );
264 return $name === false ? null : $name;
268 * Fetch the user identity from cookies
269 * @return array (int|null $id, string|null $token)
271 protected function getUserInfoFromCookies( $request ) {
272 $prefix = $this->cookieOptions['prefix'];
273 return array(
274 $request->getCookie( 'UserID', $prefix ),
275 $request->getCookie( 'UserName', $prefix ),
276 $request->getCookie( 'Token', $prefix ),
281 * Return the data to store in cookies
282 * @param User $user
283 * @param bool $remember
284 * @return array $cookies Set value false to unset the cookie
286 protected function cookieDataToExport( $user, $remember ) {
287 if ( $user->isAnon() ) {
288 return array(
289 'UserID' => false,
290 'Token' => false,
292 } else {
293 return array(
294 'UserID' => $user->getId(),
295 'UserName' => $user->getName(),
296 'Token' => $remember ? (string)$user->getToken() : false,
302 * Return extra data to store in the session
303 * @param User $user
304 * @return array $session
306 protected function sessionDataToExport( $user ) {
307 // If we're calling the legacy hook, we should populate $session
308 // like User::setCookies() did.
309 if ( !$user->isAnon() && $this->params['callUserSetCookiesHook'] ) {
310 return array(
311 'wsUserID' => $user->getId(),
312 'wsToken' => $user->getToken(),
313 'wsUserName' => $user->getName(),
317 return array();
320 public function whyNoSession() {
321 return wfMessage( 'sessionprovider-nocookies' );