Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.85% covered (warning)
80.85%
152 / 188
92.00% covered (success)
92.00%
46 / 50
CRAP
0.00% covered (danger)
0.00%
0 / 1
Session
80.85% covered (warning)
80.85%
152 / 188
92.00% covered (success)
92.00%
46 / 50
137.93
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 __destruct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSessionId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resetId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getProvider
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isPersistent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 persist
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 unpersist
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 shouldRememberUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRememberUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRequest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedUserRights
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRestrictions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 canSetUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 suggestLoginUsername
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 shouldForceHTTPS
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setForceHTTPS
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLoggedOutTimestamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setLoggedOutTimestamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getProviderMetadata
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 clear
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 renew
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sessionWithRequest
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 exists
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 set
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 remove
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 hasToken
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getToken
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 resetToken
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 resetAllTokens
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSecretKeys
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
4
 getEncryptionAlgorithm
19.05% covered (danger)
19.05%
4 / 21
0.00% covered (danger)
0.00%
0 / 1
25.10
 setSecret
65.00% covered (warning)
65.00%
13 / 20
0.00% covered (danger)
0.00%
0 / 1
6.07
 getSecret
70.27% covered (warning)
70.27%
26 / 37
0.00% covered (danger)
0.00%
0 / 1
12.63
 delaySave
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 save
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 count
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 current
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 key
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 next
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 rewind
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 valid
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 offsetExists
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 offsetGet
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 offsetSet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetUnset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * MediaWiki session
4 *
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.
9 *
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.
14 *
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
19 *
20 * @file
21 * @ingroup Session
22 */
23
24namespace MediaWiki\Session;
25
26use BadMethodCallException;
27use LogicException;
28use MediaWiki\MainConfigNames;
29use MediaWiki\MediaWikiServices;
30use MediaWiki\Request\WebRequest;
31use MediaWiki\User\User;
32use MWRestrictions;
33use Psr\Log\LoggerInterface;
34use RuntimeException;
35
36/**
37 * Manages data for an authenticated session
38 *
39 * A Session represents the fact that the current HTTP request is part of a
40 * session. There are two broad types of Sessions, based on whether they
41 * return true or false from self::canSetUser():
42 * * When true (mutable), the Session identifies multiple requests as part of
43 *   a session generically, with no tie to a particular user.
44 * * When false (immutable), the Session identifies multiple requests as part
45 *   of a session by identifying and authenticating the request itself as
46 *   belonging to a particular user.
47 *
48 * The Session object also serves as a replacement for PHP's $_SESSION,
49 * managing access to per-session data.
50 *
51 * @ingroup Session
52 * @since 1.27
53 */
54class Session implements \Countable, \Iterator, \ArrayAccess {
55    /** @var null|string[] Encryption algorithm to use */
56    private static $encryptionAlgorithm = null;
57
58    /** @var SessionBackend Session backend */
59    private $backend;
60
61    /** @var int Session index */
62    private $index;
63
64    /** @var LoggerInterface */
65    private $logger;
66
67    /**
68     * @param SessionBackend $backend
69     * @param int $index
70     * @param LoggerInterface $logger
71     */
72    public function __construct( SessionBackend $backend, $index, LoggerInterface $logger ) {
73        $this->backend = $backend;
74        $this->index = $index;
75        $this->logger = $logger;
76    }
77
78    public function __destruct() {
79        $this->backend->deregisterSession( $this->index );
80    }
81
82    /**
83     * Returns the session ID
84     * @return string
85     */
86    public function getId() {
87        return $this->backend->getId();
88    }
89
90    /**
91     * Returns the SessionId object
92     * @internal For internal use by WebRequest
93     * @return SessionId
94     */
95    public function getSessionId() {
96        return $this->backend->getSessionId();
97    }
98
99    /**
100     * Changes the session ID
101     * @return string New ID (might be the same as the old)
102     */
103    public function resetId() {
104        return $this->backend->resetId();
105    }
106
107    /**
108     * Fetch the SessionProvider for this session
109     * @return SessionProviderInterface
110     */
111    public function getProvider() {
112        return $this->backend->getProvider();
113    }
114
115    /**
116     * Indicate whether this session is persisted across requests
117     *
118     * For example, if cookies are set.
119     *
120     * @return bool
121     */
122    public function isPersistent() {
123        return $this->backend->isPersistent();
124    }
125
126    /**
127     * Make this session persisted across requests
128     *
129     * If the session is already persistent, equivalent to calling
130     * $this->renew().
131     */
132    public function persist() {
133        $this->backend->persist();
134    }
135
136    /**
137     * Make this session not be persisted across requests
138     *
139     * This will remove persistence information (e.g. delete cookies)
140     * from the associated WebRequest(s), and delete session data in the
141     * backend. The session data will still be available via get() until
142     * the end of the request.
143     */
144    public function unpersist() {
145        $this->backend->unpersist();
146    }
147
148    /**
149     * Indicate whether the user should be remembered independently of the
150     * session ID.
151     * @return bool
152     */
153    public function shouldRememberUser() {
154        return $this->backend->shouldRememberUser();
155    }
156
157    /**
158     * Set whether the user should be remembered independently of the session
159     * ID.
160     * @param bool $remember
161     */
162    public function setRememberUser( $remember ) {
163        $this->backend->setRememberUser( $remember );
164    }
165
166    /**
167     * Returns the request associated with this session
168     * @return WebRequest
169     */
170    public function getRequest() {
171        return $this->backend->getRequest( $this->index );
172    }
173
174    /**
175     * Returns the authenticated user for this session
176     * @return User
177     */
178    public function getUser(): User {
179        return $this->backend->getUser();
180    }
181
182    /**
183     * Fetch the rights allowed the user when this session is active.
184     * @return null|string[] Allowed user rights, or null to allow all.
185     */
186    public function getAllowedUserRights() {
187        return $this->backend->getAllowedUserRights();
188    }
189
190    /**
191     * Fetch any restrictions imposed on logins or actions when this
192     * session is active.
193     * @return MWRestrictions|null
194     */
195    public function getRestrictions(): ?MWRestrictions {
196        return $this->backend->getRestrictions();
197    }
198
199    /**
200     * Indicate whether the session user info can be changed
201     * @return bool
202     */
203    public function canSetUser() {
204        return $this->backend->canSetUser();
205    }
206
207    /**
208     * Set a new user for this session
209     * @note This should only be called when the user has been authenticated
210     * @param User $user User to set on the session.
211     *   This may become a "UserValue" in the future, or User may be refactored
212     *   into such.
213     */
214    public function setUser( $user ) {
215        $this->backend->setUser( $user );
216    }
217
218    /**
219     * Get a suggested username for the login form
220     * @return string|null
221     */
222    public function suggestLoginUsername() {
223        return $this->backend->suggestLoginUsername( $this->index );
224    }
225
226    /**
227     * Get the expected value of the forceHTTPS cookie. This reflects whether
228     * session cookies were sent with the Secure attribute. If $wgForceHTTPS
229     * is true, the forceHTTPS cookie is not sent and this value is ignored.
230     *
231     * @return bool
232     */
233    public function shouldForceHTTPS() {
234        return $this->backend->shouldForceHTTPS();
235    }
236
237    /**
238     * Set the value of the forceHTTPS cookie. This reflects whether session
239     * cookies were sent with the Secure attribute. If $wgForceHTTPS is true,
240     * the forceHTTPS cookie is not sent, and this value is ignored.
241     *
242     * @param bool $force
243     */
244    public function setForceHTTPS( $force ) {
245        $this->backend->setForceHTTPS( $force );
246    }
247
248    /**
249     * Fetch the "logged out" timestamp
250     * @return int
251     */
252    public function getLoggedOutTimestamp() {
253        return $this->backend->getLoggedOutTimestamp();
254    }
255
256    /**
257     * @param int $ts
258     */
259    public function setLoggedOutTimestamp( $ts ) {
260        $this->backend->setLoggedOutTimestamp( $ts );
261    }
262
263    /**
264     * Fetch provider metadata
265     * @note For use by SessionProvider subclasses only
266     * @return mixed
267     */
268    public function getProviderMetadata() {
269        return $this->backend->getProviderMetadata();
270    }
271
272    /**
273     * Delete all session data and clear the user (if possible)
274     */
275    public function clear() {
276        $data = &$this->backend->getData();
277        if ( $data ) {
278            $data = [];
279            $this->backend->dirty();
280        }
281        if ( $this->backend->canSetUser() ) {
282            $this->backend->setUser( MediaWikiServices::getInstance()->getUserFactory()->newAnonymous() );
283        }
284        $this->backend->save();
285    }
286
287    /**
288     * Resets the TTL in the backend store if the session is near expiring, and
289     * re-persists the session to any active WebRequests if persistent.
290     */
291    public function renew() {
292        $this->backend->renew();
293    }
294
295    /**
296     * Fetch a copy of this session attached to an alternative WebRequest
297     *
298     * Actions on the copy will affect this session too, and vice versa.
299     *
300     * @param WebRequest $request Any existing session associated with this
301     *  WebRequest object will be overwritten.
302     * @return Session
303     */
304    public function sessionWithRequest( WebRequest $request ) {
305        $request->setSessionId( $this->backend->getSessionId() );
306        return $this->backend->getSession( $request );
307    }
308
309    /**
310     * Fetch a value from the session
311     * @param string|int $key
312     * @param mixed|null $default Returned if $this->exists( $key ) would be false
313     * @return mixed
314     */
315    public function get( $key, $default = null ) {
316        $data = &$this->backend->getData();
317        return array_key_exists( $key, $data ) ? $data[$key] : $default;
318    }
319
320    /**
321     * Test if a value exists in the session
322     * @note Unlike isset(), null values are considered to exist.
323     * @param string|int $key
324     * @return bool
325     */
326    public function exists( $key ) {
327        $data = &$this->backend->getData();
328        return array_key_exists( $key, $data );
329    }
330
331    /**
332     * Set a value in the session
333     * @param string|int $key
334     * @param mixed $value
335     */
336    public function set( $key, $value ) {
337        $data = &$this->backend->getData();
338        if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) {
339            $data[$key] = $value;
340            $this->backend->dirty();
341        }
342    }
343
344    /**
345     * Remove a value from the session
346     * @param string|int $key
347     */
348    public function remove( $key ) {
349        $data = &$this->backend->getData();
350        if ( array_key_exists( $key, $data ) ) {
351            unset( $data[$key] );
352            $this->backend->dirty();
353        }
354    }
355
356    /**
357     * Check if a CSRF token is set for the session
358     *
359     * @since 1.37
360     * @param string $key Token key
361     * @return bool
362     */
363    public function hasToken( string $key = 'default' ): bool {
364        $secrets = $this->get( 'wsTokenSecrets' );
365        if ( !is_array( $secrets ) ) {
366            return false;
367        }
368        return isset( $secrets[$key] ) && is_string( $secrets[$key] );
369    }
370
371    /**
372     * Fetch a CSRF token from the session
373     *
374     * Note that this does not persist the session, which you'll probably want
375     * to do if you want the token to actually be useful.
376     *
377     * @param string|string[] $salt Token salt
378     * @param string $key Token key
379     * @return Token
380     */
381    public function getToken( $salt = '', $key = 'default' ) {
382        $new = false;
383        $secrets = $this->get( 'wsTokenSecrets' );
384        if ( !is_array( $secrets ) ) {
385            $secrets = [];
386        }
387        if ( isset( $secrets[$key] ) && is_string( $secrets[$key] ) ) {
388            $secret = $secrets[$key];
389        } else {
390            $secret = \MWCryptRand::generateHex( 32 );
391            $secrets[$key] = $secret;
392            $this->set( 'wsTokenSecrets', $secrets );
393            $new = true;
394        }
395        if ( is_array( $salt ) ) {
396            $salt = implode( '|', $salt );
397        }
398        return new Token( $secret, (string)$salt, $new );
399    }
400
401    /**
402     * Remove a CSRF token from the session
403     *
404     * The next call to self::getToken() with $key will generate a new secret.
405     *
406     * @param string $key Token key
407     */
408    public function resetToken( $key = 'default' ) {
409        $secrets = $this->get( 'wsTokenSecrets' );
410        if ( is_array( $secrets ) && isset( $secrets[$key] ) ) {
411            unset( $secrets[$key] );
412            $this->set( 'wsTokenSecrets', $secrets );
413        }
414    }
415
416    /**
417     * Remove all CSRF tokens from the session
418     */
419    public function resetAllTokens() {
420        $this->remove( 'wsTokenSecrets' );
421    }
422
423    /**
424     * Fetch the secret keys for self::setSecret() and self::getSecret().
425     * @return string[] Encryption key, HMAC key
426     */
427    private function getSecretKeys() {
428        $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
429        $sessionSecret = $mainConfig->get( MainConfigNames::SessionSecret );
430        $secretKey = $mainConfig->get( MainConfigNames::SecretKey );
431        $sessionPbkdf2Iterations = $mainConfig->get( MainConfigNames::SessionPbkdf2Iterations );
432        $wikiSecret = $sessionSecret ?: $secretKey;
433        $userSecret = $this->get( 'wsSessionSecret', null );
434        if ( $userSecret === null ) {
435            $userSecret = \MWCryptRand::generateHex( 32 );
436            $this->set( 'wsSessionSecret', $userSecret );
437        }
438        $iterations = $this->get( 'wsSessionPbkdf2Iterations', null );
439        if ( $iterations === null ) {
440            $iterations = $sessionPbkdf2Iterations;
441            $this->set( 'wsSessionPbkdf2Iterations', $iterations );
442        }
443
444        $keymats = hash_pbkdf2( 'sha256', $wikiSecret, $userSecret, $iterations, 64, true );
445        return [
446            substr( $keymats, 0, 32 ),
447            substr( $keymats, 32, 32 ),
448        ];
449    }
450
451    /**
452     * Decide what type of encryption to use, based on system capabilities.
453     * @return array
454     */
455    private static function getEncryptionAlgorithm() {
456        $sessionInsecureSecrets = MediaWikiServices::getInstance()->getMainConfig()
457            ->get( MainConfigNames::SessionInsecureSecrets );
458
459        if ( self::$encryptionAlgorithm === null ) {
460            if ( function_exists( 'openssl_encrypt' ) ) {
461                $methods = openssl_get_cipher_methods();
462                if ( in_array( 'aes-256-ctr', $methods, true ) ) {
463                    self::$encryptionAlgorithm = [ 'openssl', 'aes-256-ctr' ];
464                    return self::$encryptionAlgorithm;
465                }
466                if ( in_array( 'aes-256-cbc', $methods, true ) ) {
467                    self::$encryptionAlgorithm = [ 'openssl', 'aes-256-cbc' ];
468                    return self::$encryptionAlgorithm;
469                }
470            }
471
472            if ( $sessionInsecureSecrets ) {
473                // @todo: import a pure-PHP library for AES instead of this
474                self::$encryptionAlgorithm = [ 'insecure' ];
475                return self::$encryptionAlgorithm;
476            }
477
478            throw new BadMethodCallException(
479                'Encryption is not available. You really should install the PHP OpenSSL extension. ' .
480                'But if you really can\'t and you\'re willing ' .
481                'to accept insecure storage of sensitive session data, set ' .
482                '$wgSessionInsecureSecrets = true in LocalSettings.php to make this exception go away.'
483            );
484        }
485
486        return self::$encryptionAlgorithm;
487    }
488
489    /**
490     * Set a value in the session, encrypted
491     *
492     * This relies on the secrecy of $wgSecretKey (by default), or $wgSessionSecret.
493     *
494     * @param string|int $key
495     * @param mixed $value
496     */
497    public function setSecret( $key, $value ) {
498        [ $encKey, $hmacKey ] = $this->getSecretKeys();
499        $serialized = serialize( $value );
500
501        // The code for encryption (with OpenSSL) and sealing is taken from
502        // Chris Steipp's OATHAuthUtils class in Extension::OATHAuth.
503
504        // Encrypt
505        // @todo: import a pure-PHP library for AES instead of doing $wgSessionInsecureSecrets
506        $iv = random_bytes( 16 );
507        $algorithm = self::getEncryptionAlgorithm();
508        switch ( $algorithm[0] ) {
509            case 'openssl':
510                $ciphertext = openssl_encrypt( $serialized, $algorithm[1], $encKey, OPENSSL_RAW_DATA, $iv );
511                if ( $ciphertext === false ) {
512                    throw new \UnexpectedValueException( 'Encryption failed: ' . openssl_error_string() );
513                }
514                break;
515            case 'insecure':
516                $ex = new RuntimeException( 'No encryption is available, storing data as plain text' );
517                $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
518                $ciphertext = $serialized;
519                break;
520            default:
521                throw new LogicException( 'invalid algorithm' );
522        }
523
524        // Seal
525        $sealed = base64_encode( $iv ) . '.' . base64_encode( $ciphertext );
526        $hmac = hash_hmac( 'sha256', $sealed, $hmacKey, true );
527        $encrypted = base64_encode( $hmac ) . '.' . $sealed;
528
529        // Store
530        $this->set( $key, $encrypted );
531    }
532
533    /**
534     * Fetch a value from the session that was set with self::setSecret()
535     * @param string|int $key
536     * @param mixed|null $default Returned if $this->exists( $key ) would be false or decryption fails
537     * @return mixed
538     */
539    public function getSecret( $key, $default = null ) {
540        // Fetch
541        $encrypted = $this->get( $key, null );
542        if ( $encrypted === null ) {
543            return $default;
544        }
545
546        // The code for unsealing, checking, and decrypting (with OpenSSL) is
547        // taken from Chris Steipp's OATHAuthUtils class in
548        // Extension::OATHAuth.
549
550        // Unseal and check
551        $pieces = explode( '.', $encrypted, 4 );
552        if ( count( $pieces ) !== 3 ) {
553            $ex = new RuntimeException( 'Invalid sealed-secret format' );
554            $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
555            return $default;
556        }
557        [ $hmac, $iv, $ciphertext ] = $pieces;
558        [ $encKey, $hmacKey ] = $this->getSecretKeys();
559        $integCalc = hash_hmac( 'sha256', $iv . '.' . $ciphertext, $hmacKey, true );
560        if ( !hash_equals( $integCalc, base64_decode( $hmac ) ) ) {
561            $ex = new RuntimeException( 'Sealed secret has been tampered with, aborting.' );
562            $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
563            return $default;
564        }
565
566        // Decrypt
567        $algorithm = self::getEncryptionAlgorithm();
568        switch ( $algorithm[0] ) {
569            case 'openssl':
570                $serialized = openssl_decrypt( base64_decode( $ciphertext ), $algorithm[1], $encKey,
571                    OPENSSL_RAW_DATA, base64_decode( $iv ) );
572                if ( $serialized === false ) {
573                    $ex = new RuntimeException( 'Decyption failed: ' . openssl_error_string() );
574                    $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
575                    return $default;
576                }
577                break;
578            case 'insecure':
579                $ex = new RuntimeException(
580                    'No encryption is available, retrieving data that was stored as plain text'
581                );
582                $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
583                $serialized = base64_decode( $ciphertext );
584                break;
585            default:
586                throw new \LogicException( 'invalid algorithm' );
587        }
588
589        $value = unserialize( $serialized );
590        if ( $value === false && $serialized !== serialize( false ) ) {
591            $value = $default;
592        }
593        return $value;
594    }
595
596    /**
597     * Delay automatic saving while multiple updates are being made
598     *
599     * Calls to save() or clear() will not be delayed.
600     *
601     * @return \Wikimedia\ScopedCallback When this goes out of scope, a save will be triggered
602     */
603    public function delaySave() {
604        return $this->backend->delaySave();
605    }
606
607    /**
608     * This will update the backend data and might re-persist the session
609     * if needed.
610     */
611    public function save() {
612        $this->backend->save();
613    }
614
615    // region   Interface methods
616    /** @name   Interface methods
617     * @{
618     */
619
620    /** @inheritDoc */
621    public function count(): int {
622        $data = &$this->backend->getData();
623        return count( $data );
624    }
625
626    /** @inheritDoc */
627    #[\ReturnTypeWillChange]
628    public function current() {
629        $data = &$this->backend->getData();
630        return current( $data );
631    }
632
633    /** @inheritDoc */
634    #[\ReturnTypeWillChange]
635    public function key() {
636        $data = &$this->backend->getData();
637        return key( $data );
638    }
639
640    /** @inheritDoc */
641    public function next(): void {
642        $data = &$this->backend->getData();
643        next( $data );
644    }
645
646    /** @inheritDoc */
647    public function rewind(): void {
648        $data = &$this->backend->getData();
649        reset( $data );
650    }
651
652    /** @inheritDoc */
653    public function valid(): bool {
654        $data = &$this->backend->getData();
655        return key( $data ) !== null;
656    }
657
658    /**
659     * @note Despite the name, this seems to be intended to implement isset()
660     *  rather than array_key_exists(). So do that.
661     * @inheritDoc
662     */
663    public function offsetExists( $offset ): bool {
664        $data = &$this->backend->getData();
665        return isset( $data[$offset] );
666    }
667
668    /**
669     * @note This supports indirect modifications but can't mark the session
670     *  dirty when those happen. SessionBackend::save() checks the hash of the
671     *  data to detect such changes.
672     * @note Accessing a nonexistent key via this mechanism causes that key to
673     *  be created with a null value, and does not raise a PHP warning.
674     * @inheritDoc
675     */
676    #[\ReturnTypeWillChange]
677    public function &offsetGet( $offset ) {
678        $data = &$this->backend->getData();
679        if ( !array_key_exists( $offset, $data ) ) {
680            $ex = new LogicException( "Undefined index (auto-adds to session with a null value): $offset" );
681            $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
682        }
683        return $data[$offset];
684    }
685
686    /** @inheritDoc */
687    public function offsetSet( $offset, $value ): void {
688        $this->set( $offset, $value );
689    }
690
691    /** @inheritDoc */
692    public function offsetUnset( $offset ): void {
693        $this->remove( $offset );
694    }
695
696    /** @} */
697    // endregion  -- end of Interface methods
698}