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
24 namespace MediaWiki\Session
;
26 use Psr\Log\LoggerInterface
;
31 * Manages data for an an authenticated session
33 * A Session represents the fact that the current HTTP request is part of a
34 * session. There are two broad types of Sessions, based on whether they
35 * return true or false from self::canSetUser():
36 * * When true (mutable), the Session identifies multiple requests as part of
37 * a session generically, with no tie to a particular user.
38 * * When false (immutable), the Session identifies multiple requests as part
39 * of a session by identifying and authenticating the request itself as
40 * belonging to a particular user.
42 * The Session object also serves as a replacement for PHP's $_SESSION,
43 * managing access to per-session data.
48 final class Session
implements \Countable
, \Iterator
, \ArrayAccess
{
49 /** @var SessionBackend Session backend */
52 /** @var int Session index */
55 /** @var LoggerInterface */
59 * @param SessionBackend $backend
61 * @param LoggerInterface $logger
63 public function __construct( SessionBackend
$backend, $index, LoggerInterface
$logger ) {
64 $this->backend
= $backend;
65 $this->index
= $index;
66 $this->logger
= $logger;
69 public function __destruct() {
70 $this->backend
->deregisterSession( $this->index
);
74 * Returns the session ID
77 public function getId() {
78 return $this->backend
->getId();
82 * Returns the SessionId object
83 * @private For internal use by WebRequest
86 public function getSessionId() {
87 return $this->backend
->getSessionId();
91 * Changes the session ID
92 * @return string New ID (might be the same as the old)
94 public function resetId() {
95 return $this->backend
->resetId();
99 * Fetch the SessionProvider for this session
100 * @return SessionProviderInterface
102 public function getProvider() {
103 return $this->backend
->getProvider();
107 * Indicate whether this session is persisted across requests
109 * For example, if cookies are set.
113 public function isPersistent() {
114 return $this->backend
->isPersistent();
118 * Make this session persisted across requests
120 * If the session is already persistent, equivalent to calling
123 public function persist() {
124 $this->backend
->persist();
128 * Indicate whether the user should be remembered independently of the
132 public function shouldRememberUser() {
133 return $this->backend
->shouldRememberUser();
137 * Set whether the user should be remembered independently of the session
139 * @param bool $remember
141 public function setRememberUser( $remember ) {
142 $this->backend
->setRememberUser( $remember );
146 * Returns the request associated with this session
149 public function getRequest() {
150 return $this->backend
->getRequest( $this->index
);
154 * Returns the authenticated user for this session
157 public function getUser() {
158 return $this->backend
->getUser();
162 * Fetch the rights allowed the user when this session is active.
163 * @return null|string[] Allowed user rights, or null to allow all.
165 public function getAllowedUserRights() {
166 return $this->backend
->getAllowedUserRights();
170 * Indicate whether the session user info can be changed
173 public function canSetUser() {
174 return $this->backend
->canSetUser();
178 * Set a new user for this session
179 * @note This should only be called when the user has been authenticated
180 * @param User $user User to set on the session.
181 * This may become a "UserValue" in the future, or User may be refactored
184 public function setUser( $user ) {
185 $this->backend
->setUser( $user );
189 * Get a suggested username for the login form
190 * @return string|null
192 public function suggestLoginUsername() {
193 return $this->backend
->suggestLoginUsername( $this->index
);
197 * Whether HTTPS should be forced
200 public function shouldForceHTTPS() {
201 return $this->backend
->shouldForceHTTPS();
205 * Set whether HTTPS should be forced
208 public function setForceHTTPS( $force ) {
209 $this->backend
->setForceHTTPS( $force );
213 * Fetch the "logged out" timestamp
216 public function getLoggedOutTimestamp() {
217 return $this->backend
->getLoggedOutTimestamp();
221 * Set the "logged out" timestamp
224 public function setLoggedOutTimestamp( $ts ) {
225 $this->backend
->setLoggedOutTimestamp( $ts );
229 * Fetch provider metadata
230 * @protected For use by SessionProvider subclasses only
233 public function getProviderMetadata() {
234 return $this->backend
->getProviderMetadata();
238 * Delete all session data and clear the user (if possible)
240 public function clear() {
241 $data = &$this->backend
->getData();
244 $this->backend
->dirty();
246 if ( $this->backend
->canSetUser() ) {
247 $this->backend
->setUser( new User
);
249 $this->backend
->save();
255 * Resets the TTL in the backend store if the session is near expiring, and
256 * re-persists the session to any active WebRequests if persistent.
258 public function renew() {
259 $this->backend
->renew();
263 * Fetch a copy of this session attached to an alternative WebRequest
265 * Actions on the copy will affect this session too, and vice versa.
267 * @param WebRequest $request Any existing session associated with this
268 * WebRequest object will be overwritten.
271 public function sessionWithRequest( WebRequest
$request ) {
272 $request->setSessionId( $this->backend
->getSessionId() );
273 return $this->backend
->getSession( $request );
277 * Fetch a value from the session
278 * @param string|int $key
279 * @param mixed $default Returned if $this->exists( $key ) would be false
282 public function get( $key, $default = null ) {
283 $data = &$this->backend
->getData();
284 return array_key_exists( $key, $data ) ?
$data[$key] : $default;
288 * Test if a value exists in the session
289 * @note Unlike isset(), null values are considered to exist.
290 * @param string|int $key
293 public function exists( $key ) {
294 $data = &$this->backend
->getData();
295 return array_key_exists( $key, $data );
299 * Set a value in the session
300 * @param string|int $key
301 * @param mixed $value
303 public function set( $key, $value ) {
304 $data = &$this->backend
->getData();
305 if ( !array_key_exists( $key, $data ) ||
$data[$key] !== $value ) {
306 $data[$key] = $value;
307 $this->backend
->dirty();
312 * Remove a value from the session
313 * @param string|int $key
315 public function remove( $key ) {
316 $data = &$this->backend
->getData();
317 if ( array_key_exists( $key, $data ) ) {
318 unset( $data[$key] );
319 $this->backend
->dirty();
324 * Fetch a CSRF token from the session
326 * Note that this does not persist the session, which you'll probably want
327 * to do if you want the token to actually be useful.
329 * @param string|string[] $salt Token salt
330 * @param string $key Token key
331 * @return MediaWiki\\Session\\SessionToken
333 public function getToken( $salt = '', $key = 'default' ) {
335 $secrets = $this->get( 'wsTokenSecrets' );
336 if ( !is_array( $secrets ) ) {
339 if ( isset( $secrets[$key] ) && is_string( $secrets[$key] ) ) {
340 $secret = $secrets[$key];
342 $secret = \MWCryptRand
::generateHex( 32 );
343 $secrets[$key] = $secret;
344 $this->set( 'wsTokenSecrets', $secrets );
347 if ( is_array( $salt ) ) {
348 $salt = join( '|', $salt );
350 return new Token( $secret, (string)$salt, $new );
354 * Remove a CSRF token from the session
356 * The next call to self::getToken() with $key will generate a new secret.
358 * @param string $key Token key
360 public function resetToken( $key = 'default' ) {
361 $secrets = $this->get( 'wsTokenSecrets' );
362 if ( is_array( $secrets ) && isset( $secrets[$key] ) ) {
363 unset( $secrets[$key] );
364 $this->set( 'wsTokenSecrets', $secrets );
369 * Remove all CSRF tokens from the session
371 public function resetAllTokens() {
372 $this->remove( 'wsTokenSecrets' );
376 * Delay automatic saving while multiple updates are being made
378 * Calls to save() or clear() will not be delayed.
380 * @return \ScopedCallback When this goes out of scope, a save will be triggered
382 public function delaySave() {
383 return $this->backend
->delaySave();
389 public function save() {
390 $this->backend
->save();
394 * @name Interface methods
398 public function count() {
399 $data = &$this->backend
->getData();
400 return count( $data );
403 public function current() {
404 $data = &$this->backend
->getData();
405 return current( $data );
408 public function key() {
409 $data = &$this->backend
->getData();
413 public function next() {
414 $data = &$this->backend
->getData();
418 public function rewind() {
419 $data = &$this->backend
->getData();
423 public function valid() {
424 $data = &$this->backend
->getData();
425 return key( $data ) !== null;
429 * @note Despite the name, this seems to be intended to implement isset()
430 * rather than array_key_exists(). So do that.
432 public function offsetExists( $offset ) {
433 $data = &$this->backend
->getData();
434 return isset( $data[$offset] );
438 * @note This supports indirect modifications but can't mark the session
439 * dirty when those happen. SessionBackend::save() checks the hash of the
440 * data to detect such changes.
441 * @note Accessing a nonexistent key via this mechanism causes that key to
442 * be created with a null value, and does not raise a PHP warning.
444 public function &offsetGet( $offset ) {
445 $data = &$this->backend
->getData();
446 if ( !array_key_exists( $offset, $data ) ) {
447 $ex = new \
Exception( "Undefined index (auto-adds to session with a null value): $offset" );
448 $this->logger
->debug( $ex->getMessage(), [ 'exception' => $ex ] );
450 return $data[$offset];
453 public function offsetSet( $offset, $value ) {
454 $this->set( $offset, $value );
457 public function offsetUnset( $offset ) {
458 $this->remove( $offset );