Issues (910)

framework/web/Session.php (2 issues)

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\web;
9
10
use Yii;
11
use yii\base\Component;
12
use yii\base\InvalidArgumentException;
13
use yii\base\InvalidConfigException;
14
15
/**
16
 * Session provides session data management and the related configurations.
17
 *
18
 * Session is a Web application component that can be accessed via `Yii::$app->session`.
19
 *
20
 * To start the session, call [[open()]]; To complete and send out session data, call [[close()]];
21
 * To destroy the session, call [[destroy()]].
22
 *
23
 * Session can be used like an array to set and get session data. For example,
24
 *
25
 * ```php
26
 * $session = new Session;
27
 * $session->open();
28
 * $value1 = $session['name1'];  // get session variable 'name1'
29
 * $value2 = $session['name2'];  // get session variable 'name2'
30
 * foreach ($session as $name => $value) // traverse all session variables
31
 * $session['name3'] = $value3;  // set session variable 'name3'
32
 * ```
33
 *
34
 * Session can be extended to support customized session storage.
35
 * To do so, override [[useCustomStorage]] so that it returns true, and
36
 * override these methods with the actual logic about using custom storage:
37
 * [[openSession()]], [[closeSession()]], [[readSession()]], [[writeSession()]],
38
 * [[destroySession()]] and [[gcSession()]].
39
 *
40
 * Session also supports a special type of session data, called *flash messages*.
41
 * A flash message is available only in the current request and the next request.
42
 * After that, it will be deleted automatically. Flash messages are particularly
43
 * useful for displaying confirmation messages. To use flash messages, simply
44
 * call methods such as [[setFlash()]], [[getFlash()]].
45
 *
46
 * For more details and usage information on Session, see the [guide article on sessions](guide:runtime-sessions-cookies).
47
 *
48
 * @property-read array $allFlashes Flash messages (key => message or key => [message1, message2]).
49
 * @property-read string $cacheLimiter Current cache limiter.
50
 * @property-read array $cookieParams The session cookie parameters.
51
 * @property-read int $count The number of session variables.
52
 * @property-write string $flash The key identifying the flash message. Note that flash messages and normal
53
 * session variables share the same name space. If you have a normal session variable using the same name, its
54
 * value will be overwritten by this method.
55
 * @property float $gCProbability The probability (percentage) that the GC (garbage collection) process is
56
 * started on every session initialization.
57
 * @property bool $hasSessionId Whether the current request has sent the session ID.
58
 * @property string $id The current session ID.
59
 * @property-read bool $isActive Whether the session has started.
60
 * @property string $name The current session name.
61
 * @property string $savePath The current session save path, defaults to '/tmp'.
62
 * @property int $timeout The number of seconds after which data will be seen as 'garbage' and cleaned up. The
63
 * default value is 1440 seconds (or the value of "session.gc_maxlifetime" set in php.ini).
64
 * @property bool|null $useCookies The value indicating whether cookies should be used to store session IDs.
65
 * @property-read bool $useCustomStorage Whether to use custom storage.
66
 * @property bool $useStrictMode Whether strict mode is enabled or not.
67
 * @property bool $useTransparentSessionID Whether transparent sid support is enabled or not, defaults to
68
 * false.
69
 *
70
 * @author Qiang Xue <[email protected]>
71
 * @since 2.0
72
 */
73
class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Countable
74
{
75
    /**
76
     * @var string|null Holds the original session module (before a custom handler is registered) so that it can be
77
     * restored when a Session component without custom handler is used after one that has.
78
     */
79
    protected static $_originalSessionModule = null;
80
    /**
81
     * Polyfill for ini directive session.use-strict-mode for PHP < 5.5.2.
82
     */
83
    private static $_useStrictModePolyfill = false;
84
    /**
85
     * @var string the name of the session variable that stores the flash message data.
86
     */
87
    public $flashParam = '__flash';
88
    /**
89
     * @var \SessionHandlerInterface|array an object implementing the SessionHandlerInterface or a configuration array. If set, will be used to provide persistency instead of build-in methods.
90
     */
91
    public $handler;
92
93
    /**
94
     * @var string|null Holds the session id in case useStrictMode is enabled and the session id needs to be regenerated
95
     */
96
    protected $_forceRegenerateId = null;
97
98
    /**
99
     * @var array parameter-value pairs to override default session cookie parameters that are used for session_set_cookie_params() function
100
     * Array may have the following possible keys: 'lifetime', 'path', 'domain', 'secure', 'httponly'
101
     * @see https://www.php.net/manual/en/function.session-set-cookie-params.php
102
     */
103
    private $_cookieParams = ['httponly' => true];
104
    /**
105
     * @var array|null is used for saving session between recreations due to session parameters update.
106
     */
107
    private $_frozenSessionData;
108
109
110
    /**
111
     * Initializes the application component.
112
     * This method is required by IApplicationComponent and is invoked by application.
113
     */
114 124
    public function init()
115
    {
116 124
        parent::init();
117 124
        register_shutdown_function([$this, 'close']);
118 124
        if ($this->getIsActive()) {
119 5
            Yii::warning('Session is already started', __METHOD__);
120 5
            $this->updateFlashCounters();
121
        }
122
    }
123
124
    /**
125
     * Returns a value indicating whether to use custom session storage.
126
     * This method should be overridden to return true by child classes that implement custom session storage.
127
     * To implement custom session storage, override these methods: [[openSession()]], [[closeSession()]],
128
     * [[readSession()]], [[writeSession()]], [[destroySession()]] and [[gcSession()]].
129
     * @return bool whether to use custom storage.
130
     */
131 68
    public function getUseCustomStorage()
132
    {
133 68
        return false;
134
    }
135
136
    /**
137
     * Starts the session.
138
     */
139 77
    public function open()
140
    {
141 77
        if ($this->getIsActive()) {
142 77
            return;
143
        }
144
145 76
        $this->registerSessionHandler();
146
147 76
        $this->setCookieParamsInternal();
148
149 76
        YII_DEBUG ? session_start() : @session_start();
150
151 76
        if ($this->getUseStrictMode() && $this->_forceRegenerateId) {
152 7
            $this->regenerateID();
153 7
            $this->_forceRegenerateId = null;
154
        }
155
156 76
        if ($this->getIsActive()) {
157 76
            Yii::info('Session started', __METHOD__);
158 76
            $this->updateFlashCounters();
159
        } else {
160
            $error = error_get_last();
161
            $message = isset($error['message']) ? $error['message'] : 'Failed to start session.';
162
            Yii::error($message, __METHOD__);
163
        }
164
    }
165
166
    /**
167
     * Registers session handler.
168
     * @throws \yii\base\InvalidConfigException
169
     */
170 76
    protected function registerSessionHandler()
171
    {
172 76
        $sessionModuleName = session_module_name();
173 76
        if (static::$_originalSessionModule === null) {
174 1
            static::$_originalSessionModule = $sessionModuleName;
175
        }
176
177 76
        if ($this->handler !== null) {
178
            if (!is_object($this->handler)) {
179
                $this->handler = Yii::createObject($this->handler);
180
            }
181
            if (!$this->handler instanceof \SessionHandlerInterface) {
182
                throw new InvalidConfigException('"' . get_class($this) . '::handler" must implement the SessionHandlerInterface.');
183
            }
184
            YII_DEBUG ? session_set_save_handler($this->handler, false) : @session_set_save_handler($this->handler, false);
185 76
        } elseif ($this->getUseCustomStorage()) {
186 8
            if (YII_DEBUG) {
187 8
                session_set_save_handler(
188 8
                    [$this, 'openSession'],
189 8
                    [$this, 'closeSession'],
190 8
                    [$this, 'readSession'],
191 8
                    [$this, 'writeSession'],
192 8
                    [$this, 'destroySession'],
193 8
                    [$this, 'gcSession']
194 8
                );
195
            } else {
196 8
                @session_set_save_handler(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for session_set_save_handler(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

196
                /** @scrutinizer ignore-unhandled */ @session_set_save_handler(

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
197 8
                    [$this, 'openSession'],
198 8
                    [$this, 'closeSession'],
199 8
                    [$this, 'readSession'],
200 8
                    [$this, 'writeSession'],
201 8
                    [$this, 'destroySession'],
202 8
                    [$this, 'gcSession']
203 8
                );
204
            }
205
        } elseif (
206 68
            $sessionModuleName !== static::$_originalSessionModule
207 68
            && static::$_originalSessionModule !== null
208 68
            && static::$_originalSessionModule !== 'user'
209
        ) {
210 1
            session_module_name(static::$_originalSessionModule);
211
        }
212
    }
213
214
    /**
215
     * Ends the current session and store session data.
216
     */
217 93
    public function close()
218
    {
219 93
        if ($this->getIsActive()) {
220 70
            YII_DEBUG ? session_write_close() : @session_write_close();
221
        }
222
223 93
        $this->_forceRegenerateId = null;
224
    }
225
226
    /**
227
     * Frees all session variables and destroys all data registered to a session.
228
     *
229
     * This method has no effect when session is not [[getIsActive()|active]].
230
     * Make sure to call [[open()]] before calling it.
231
     * @see open()
232
     * @see isActive
233
     */
234 7
    public function destroy()
235
    {
236 7
        if ($this->getIsActive()) {
237 7
            $sessionId = session_id();
238 7
            $this->close();
239 7
            $this->setId($sessionId);
240 7
            $this->open();
241 7
            session_unset();
242 7
            session_destroy();
243 7
            $this->setId($sessionId);
244
        }
245
    }
246
247
    /**
248
     * @return bool whether the session has started
249
     */
250 124
    public function getIsActive()
251
    {
252 124
        return session_status() === PHP_SESSION_ACTIVE;
253
    }
254
255
    private $_hasSessionId;
256
257
    /**
258
     * Returns a value indicating whether the current request has sent the session ID.
259
     * The default implementation will check cookie and $_GET using the session name.
260
     * If you send session ID via other ways, you may need to override this method
261
     * or call [[setHasSessionId()]] to explicitly set whether the session ID is sent.
262
     * @return bool whether the current request has sent the session ID.
263
     */
264 37
    public function getHasSessionId()
265
    {
266 37
        if ($this->_hasSessionId === null) {
267 37
            $name = $this->getName();
268 37
            $request = Yii::$app->getRequest();
269 37
            if (!empty($_COOKIE[$name]) && ini_get('session.use_cookies')) {
270
                $this->_hasSessionId = true;
271 37
            } elseif (!ini_get('session.use_only_cookies') && ini_get('session.use_trans_sid')) {
272
                $this->_hasSessionId = $request->get($name) != '';
273
            } else {
274 37
                $this->_hasSessionId = false;
275
            }
276
        }
277
278 37
        return $this->_hasSessionId;
279
    }
280
281
    /**
282
     * Sets the value indicating whether the current request has sent the session ID.
283
     * This method is provided so that you can override the default way of determining
284
     * whether the session ID is sent.
285
     * @param bool $value whether the current request has sent the session ID.
286
     */
287
    public function setHasSessionId($value)
288
    {
289
        $this->_hasSessionId = $value;
290
    }
291
292
    /**
293
     * Gets the session ID.
294
     * This is a wrapper for [PHP session_id()](https://www.php.net/manual/en/function.session-id.php).
295
     * @return string the current session ID
296
     */
297 21
    public function getId()
298
    {
299 21
        return session_id();
300
    }
301
302
    /**
303
     * Sets the session ID.
304
     * This is a wrapper for [PHP session_id()](https://www.php.net/manual/en/function.session-id.php).
305
     * @param string $value the session ID for the current session
306
     */
307 12
    public function setId($value)
308
    {
309 12
        session_id($value);
310
    }
311
312
    /**
313
     * Updates the current session ID with a newly generated one.
314
     *
315
     * Please refer to <https://www.php.net/session_regenerate_id> for more details.
316
     *
317
     * This method has no effect when session is not [[getIsActive()|active]].
318
     * Make sure to call [[open()]] before calling it.
319
     *
320
     * @param bool $deleteOldSession Whether to delete the old associated session file or not.
321
     * @see open()
322
     * @see isActive
323
     */
324 52
    public function regenerateID($deleteOldSession = false)
325
    {
326 52
        if ($this->getIsActive()) {
327
            // add @ to inhibit possible warning due to race condition
328
            // https://github.com/yiisoft/yii2/pull/1812
329 39
            if (YII_DEBUG && !headers_sent()) {
330 39
                session_regenerate_id($deleteOldSession);
331
            } else {
332
                @session_regenerate_id($deleteOldSession);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for session_regenerate_id(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

332
                /** @scrutinizer ignore-unhandled */ @session_regenerate_id($deleteOldSession);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
333
            }
334
        }
335
    }
336
337
    /**
338
     * Gets the name of the current session.
339
     * This is a wrapper for [PHP session_name()](https://www.php.net/manual/en/function.session-name.php).
340
     * @return string the current session name
341
     */
342 38
    public function getName()
343
    {
344 38
        return session_name();
345
    }
346
347
    /**
348
     * Sets the name for the current session.
349
     * This is a wrapper for [PHP session_name()](https://www.php.net/manual/en/function.session-name.php).
350
     * @param string $value the session name for the current session, must be an alphanumeric string.
351
     * It defaults to "PHPSESSID".
352
     */
353 1
    public function setName($value)
354
    {
355 1
        $this->freeze();
356 1
        session_name($value);
357 1
        $this->unfreeze();
358
    }
359
360
    /**
361
     * Gets the current session save path.
362
     * This is a wrapper for [PHP session_save_path()](https://www.php.net/manual/en/function.session-save-path.php).
363
     * @return string the current session save path, defaults to '/tmp'.
364
     */
365
    public function getSavePath()
366
    {
367
        return session_save_path();
368
    }
369
370
    /**
371
     * Sets the current session save path.
372
     * This is a wrapper for [PHP session_save_path()](https://www.php.net/manual/en/function.session-save-path.php).
373
     * @param string $value the current session save path. This can be either a directory name or a [path alias](guide:concept-aliases).
374
     * @throws InvalidArgumentException if the path is not a valid directory
375
     */
376
    public function setSavePath($value)
377
    {
378
        $path = Yii::getAlias($value);
379
        if (is_dir($path)) {
380
            session_save_path($path);
381
        } else {
382
            throw new InvalidArgumentException("Session save path is not a valid directory: $value");
383
        }
384
    }
385
386
    /**
387
     * @return array the session cookie parameters.
388
     * @see https://www.php.net/manual/en/function.session-get-cookie-params.php
389
     */
390 76
    public function getCookieParams()
391
    {
392 76
        return array_merge(session_get_cookie_params(), array_change_key_case($this->_cookieParams));
393
    }
394
395
    /**
396
     * Sets the session cookie parameters.
397
     * The cookie parameters passed to this method will be merged with the result
398
     * of `session_get_cookie_params()`.
399
     * @param array $value cookie parameters, valid keys include: `lifetime`, `path`, `domain`, `secure` and `httponly`.
400
     * Starting with Yii 2.0.21 `sameSite` is also supported. It requires PHP version 7.3.0 or higher.
401
     * For security, an exception will be thrown if `sameSite` is set while using an unsupported version of PHP.
402
     * To use this feature across different PHP versions check the version first. E.g.
403
     * ```php
404
     * [
405
     *     'sameSite' => PHP_VERSION_ID >= 70300 ? yii\web\Cookie::SAME_SITE_LAX : null,
406
     * ]
407
     * ```
408
     * See https://owasp.org/www-community/SameSite for more information about `sameSite`.
409
     *
410
     * @throws InvalidArgumentException if the parameters are incomplete.
411
     * @see https://www.php.net/manual/en/function.session-set-cookie-params.php
412
     */
413
    public function setCookieParams(array $value)
414
    {
415
        $this->_cookieParams = $value;
416
    }
417
418
    /**
419
     * Sets the session cookie parameters.
420
     * This method is called by [[open()]] when it is about to open the session.
421
     * @throws InvalidArgumentException if the parameters are incomplete.
422
     * @see https://www.php.net/manual/en/function.session-set-cookie-params.php
423
     */
424 76
    private function setCookieParamsInternal()
425
    {
426 76
        $data = $this->getCookieParams();
427 76
        if (isset($data['lifetime'], $data['path'], $data['domain'], $data['secure'], $data['httponly'])) {
428 76
            if (PHP_VERSION_ID >= 70300) {
429 76
                session_set_cookie_params($data);
430
            } else {
431
                if (!empty($data['samesite'])) {
432
                    $data['path'] .= '; samesite=' . $data['samesite'];
433
                }
434 76
                session_set_cookie_params($data['lifetime'], $data['path'], $data['domain'], $data['secure'], $data['httponly']);
435
            }
436
        } else {
437
            throw new InvalidArgumentException('Please make sure cookieParams contains these elements: lifetime, path, domain, secure and httponly.');
438
        }
439
    }
440
441
    /**
442
     * Returns the value indicating whether cookies should be used to store session IDs.
443
     * @return bool|null the value indicating whether cookies should be used to store session IDs.
444
     * @see setUseCookies()
445
     */
446 1
    public function getUseCookies()
447
    {
448 1
        if (ini_get('session.use_cookies') === '0') {
449 1
            return false;
450 1
        } elseif (ini_get('session.use_only_cookies') === '1') {
451 1
            return true;
452
        }
453
454
        return null;
455
    }
456
457
    /**
458
     * Sets the value indicating whether cookies should be used to store session IDs.
459
     *
460
     * Three states are possible:
461
     *
462
     * - true: cookies and only cookies will be used to store session IDs.
463
     * - false: cookies will not be used to store session IDs.
464
     * - null: if possible, cookies will be used to store session IDs; if not, other mechanisms will be used (e.g. GET parameter)
465
     *
466
     * @param bool|null $value the value indicating whether cookies should be used to store session IDs.
467
     */
468 4
    public function setUseCookies($value)
469
    {
470 4
        $this->freeze();
471 4
        if ($value === false) {
472 1
            ini_set('session.use_cookies', '0');
473 1
            ini_set('session.use_only_cookies', '0');
474 3
        } elseif ($value === true) {
475 3
            ini_set('session.use_cookies', '1');
476 3
            ini_set('session.use_only_cookies', '1');
477
        } else {
478
            ini_set('session.use_cookies', '1');
479
            ini_set('session.use_only_cookies', '0');
480
        }
481 4
        $this->unfreeze();
482
    }
483
484
    /**
485
     * @return float the probability (percentage) that the GC (garbage collection) process is started on every session initialization.
486
     */
487 1
    public function getGCProbability()
488
    {
489 1
        return (float) (ini_get('session.gc_probability') / ini_get('session.gc_divisor') * 100);
490
    }
491
492
    /**
493
     * @param float $value the probability (percentage) that the GC (garbage collection) process is started on every session initialization.
494
     * @throws InvalidArgumentException if the value is not between 0 and 100.
495
     */
496 1
    public function setGCProbability($value)
497
    {
498 1
        $this->freeze();
499 1
        if ($value >= 0 && $value <= 100) {
500
            // percent * 21474837 / 2147483647 ≈ percent * 0.01
501 1
            ini_set('session.gc_probability', floor($value * 21474836.47));
502 1
            ini_set('session.gc_divisor', 2147483647);
503
        } else {
504
            throw new InvalidArgumentException('GCProbability must be a value between 0 and 100.');
505
        }
506 1
        $this->unfreeze();
507
    }
508
509
    /**
510
     * @return bool whether transparent sid support is enabled or not, defaults to false.
511
     */
512 1
    public function getUseTransparentSessionID()
513
    {
514 1
        return ini_get('session.use_trans_sid') == 1;
515
    }
516
517
    /**
518
     * @param bool $value whether transparent sid support is enabled or not.
519
     */
520 1
    public function setUseTransparentSessionID($value)
521
    {
522 1
        $this->freeze();
523 1
        ini_set('session.use_trans_sid', $value ? '1' : '0');
524 1
        $this->unfreeze();
525
    }
526
527
    /**
528
     * @return int the number of seconds after which data will be seen as 'garbage' and cleaned up.
529
     * The default value is 1440 seconds (or the value of "session.gc_maxlifetime" set in php.ini).
530
     */
531 28
    public function getTimeout()
532
    {
533 28
        return (int) ini_get('session.gc_maxlifetime');
534
    }
535
536
    /**
537
     * @param int $value the number of seconds after which data will be seen as 'garbage' and cleaned up
538
     */
539 4
    public function setTimeout($value)
540
    {
541 4
        $this->freeze();
542 4
        ini_set('session.gc_maxlifetime', $value);
543 4
        $this->unfreeze();
544
    }
545
546
    /**
547
     * @param bool $value Whether strict mode is enabled or not.
548
     * When `true` this setting prevents the session component to use an uninitialized session ID.
549
     * Note: Enabling `useStrictMode` on PHP < 5.5.2 is only supported with custom storage classes.
550
     * Warning! Although enabling strict mode is mandatory for secure sessions, the default value of 'session.use-strict-mode' is `0`.
551
     * @see https://www.php.net/manual/en/session.configuration.php#ini.session.use-strict-mode
552
     * @since 2.0.38
553
     */
554 10
    public function setUseStrictMode($value)
555
    {
556 10
        if (PHP_VERSION_ID < 50502) {
557
            if ($this->getUseCustomStorage() || !$value) {
558
                self::$_useStrictModePolyfill = $value;
559
            } else {
560
                throw new InvalidConfigException('Enabling `useStrictMode` on PHP < 5.5.2 is only supported with custom storage classes.');
561
            }
562
        } else {
563 10
            $this->freeze();
564 10
            ini_set('session.use_strict_mode', $value ? '1' : '0');
565 10
            $this->unfreeze();
566
        }
567
    }
568
569
    /**
570
     * @return bool Whether strict mode is enabled or not.
571
     * @see setUseStrictMode()
572
     * @since 2.0.38
573
     */
574 97
    public function getUseStrictMode()
575
    {
576 97
        if (PHP_VERSION_ID < 50502) {
577
            return self::$_useStrictModePolyfill;
578
        }
579
580 97
        return (bool)ini_get('session.use_strict_mode');
581
    }
582
583
    /**
584
     * Session open handler.
585
     * This method should be overridden if [[useCustomStorage]] returns true.
586
     * @internal Do not call this method directly.
587
     * @param string $savePath session save path
588
     * @param string $sessionName session name
589
     * @return bool whether session is opened successfully
590
     */
591 9
    public function openSession($savePath, $sessionName)
592
    {
593 9
        return true;
594
    }
595
596
    /**
597
     * Session close handler.
598
     * This method should be overridden if [[useCustomStorage]] returns true.
599
     * @internal Do not call this method directly.
600
     * @return bool whether session is closed successfully
601
     */
602 8
    public function closeSession()
603
    {
604 8
        return true;
605
    }
606
607
    /**
608
     * Session read handler.
609
     * This method should be overridden if [[useCustomStorage]] returns true.
610
     * @internal Do not call this method directly.
611
     * @param string $id session ID
612
     * @return string the session data
613
     */
614
    public function readSession($id)
615
    {
616
        return '';
617
    }
618
619
    /**
620
     * Session write handler.
621
     * This method should be overridden if [[useCustomStorage]] returns true.
622
     * @internal Do not call this method directly.
623
     * @param string $id session ID
624
     * @param string $data session data
625
     * @return bool whether session write is successful
626
     */
627
    public function writeSession($id, $data)
628
    {
629
        return true;
630
    }
631
632
    /**
633
     * Session destroy handler.
634
     * This method should be overridden if [[useCustomStorage]] returns true.
635
     * @internal Do not call this method directly.
636
     * @param string $id session ID
637
     * @return bool whether session is destroyed successfully
638
     */
639 1
    public function destroySession($id)
640
    {
641 1
        return true;
642
    }
643
644
    /**
645
     * Session GC (garbage collection) handler.
646
     * This method should be overridden if [[useCustomStorage]] returns true.
647
     * @internal Do not call this method directly.
648
     * @param int $maxLifetime the number of seconds after which data will be seen as 'garbage' and cleaned up.
649
     * @return bool whether session is GCed successfully
650
     */
651
    public function gcSession($maxLifetime)
652
    {
653
        return true;
654
    }
655
656
    /**
657
     * Returns an iterator for traversing the session variables.
658
     * This method is required by the interface [[\IteratorAggregate]].
659
     * @return SessionIterator an iterator for traversing the session variables.
660
     */
661
    #[\ReturnTypeWillChange]
662
    public function getIterator()
663
    {
664
        $this->open();
665
        return new SessionIterator();
666
    }
667
668
    /**
669
     * Returns the number of items in the session.
670
     * @return int the number of session variables
671
     */
672
    public function getCount()
673
    {
674
        $this->open();
675
        return count($_SESSION);
676
    }
677
678
    /**
679
     * Returns the number of items in the session.
680
     * This method is required by [[\Countable]] interface.
681
     * @return int number of items in the session.
682
     */
683
    #[\ReturnTypeWillChange]
684
    public function count()
685
    {
686
        return $this->getCount();
687
    }
688
689
    /**
690
     * Returns the session variable value with the session variable name.
691
     * If the session variable does not exist, the `$defaultValue` will be returned.
692
     * @param string $key the session variable name
693
     * @param mixed $defaultValue the default value to be returned when the session variable does not exist.
694
     * @return mixed the session variable value, or $defaultValue if the session variable does not exist.
695
     */
696 77
    public function get($key, $defaultValue = null)
697
    {
698 77
        $this->open();
699 77
        return isset($_SESSION[$key]) ? $_SESSION[$key] : $defaultValue;
700
    }
701
702
    /**
703
     * Adds a session variable.
704
     * If the specified name already exists, the old value will be overwritten.
705
     * @param string $key session variable name
706
     * @param mixed $value session variable value
707
     */
708 60
    public function set($key, $value)
709
    {
710 60
        $this->open();
711 60
        $_SESSION[$key] = $value;
712
    }
713
714
    /**
715
     * Removes a session variable.
716
     * @param string $key the name of the session variable to be removed
717
     * @return mixed the removed value, null if no such session variable.
718
     */
719 45
    public function remove($key)
720
    {
721 45
        $this->open();
722 45
        if (isset($_SESSION[$key])) {
723 37
            $value = $_SESSION[$key];
724 37
            unset($_SESSION[$key]);
725
726 37
            return $value;
727
        }
728
729 45
        return null;
730
    }
731
732
    /**
733
     * Removes all session variables.
734
     */
735 22
    public function removeAll()
736
    {
737 22
        $this->open();
738 22
        foreach (array_keys($_SESSION) as $key) {
739 22
            unset($_SESSION[$key]);
740
        }
741
    }
742
743
    /**
744
     * @param mixed $key session variable name
745
     * @return bool whether there is the named session variable
746
     */
747
    public function has($key)
748
    {
749
        $this->open();
750
        return isset($_SESSION[$key]);
751
    }
752
753
    /**
754
     * Updates the counters for flash messages and removes outdated flash messages.
755
     * This method should only be called once in [[init()]].
756
     */
757 77
    protected function updateFlashCounters()
758
    {
759 77
        $counters = $this->get($this->flashParam, []);
760 77
        if (is_array($counters)) {
761 77
            foreach ($counters as $key => $count) {
762
                if ($count > 0) {
763
                    unset($counters[$key], $_SESSION[$key]);
764
                } elseif ($count == 0) {
765
                    $counters[$key]++;
766
                }
767
            }
768 77
            $_SESSION[$this->flashParam] = $counters;
769
        } else {
770
            // fix the unexpected problem that flashParam doesn't return an array
771
            unset($_SESSION[$this->flashParam]);
772
        }
773
    }
774
775
    /**
776
     * Returns a flash message.
777
     * @param string $key the key identifying the flash message
778
     * @param mixed $defaultValue value to be returned if the flash message does not exist.
779
     * @param bool $delete whether to delete this flash message right after this method is called.
780
     * If false, the flash message will be automatically deleted in the next request.
781
     * @return mixed the flash message or an array of messages if addFlash was used
782
     * @see setFlash()
783
     * @see addFlash()
784
     * @see hasFlash()
785
     * @see getAllFlashes()
786
     * @see removeFlash()
787
     */
788
    public function getFlash($key, $defaultValue = null, $delete = false)
789
    {
790
        $counters = $this->get($this->flashParam, []);
791
        if (isset($counters[$key])) {
792
            $value = $this->get($key, $defaultValue);
793
            if ($delete) {
794
                $this->removeFlash($key);
795
            } elseif ($counters[$key] < 0) {
796
                // mark for deletion in the next request
797
                $counters[$key] = 1;
798
                $_SESSION[$this->flashParam] = $counters;
799
            }
800
801
            return $value;
802
        }
803
804
        return $defaultValue;
805
    }
806
807
    /**
808
     * Returns all flash messages.
809
     *
810
     * You may use this method to display all the flash messages in a view file:
811
     *
812
     * ```php
813
     * <?php
814
     * foreach (Yii::$app->session->getAllFlashes() as $key => $message) {
815
     *     echo '<div class="alert alert-' . $key . '">' . $message . '</div>';
816
     * } ?>
817
     * ```
818
     *
819
     * With the above code you can use the [bootstrap alert][] classes such as `success`, `info`, `danger`
820
     * as the flash message key to influence the color of the div.
821
     *
822
     * Note that if you use [[addFlash()]], `$message` will be an array, and you will have to adjust the above code.
823
     *
824
     * [bootstrap alert]: https://getbootstrap.com/docs/3.4/components/#alerts
825
     *
826
     * @param bool $delete whether to delete the flash messages right after this method is called.
827
     * If false, the flash messages will be automatically deleted in the next request.
828
     * @return array flash messages (key => message or key => [message1, message2]).
829
     * @see setFlash()
830
     * @see addFlash()
831
     * @see getFlash()
832
     * @see hasFlash()
833
     * @see removeFlash()
834
     */
835
    public function getAllFlashes($delete = false)
836
    {
837
        $counters = $this->get($this->flashParam, []);
838
        $flashes = [];
839
        foreach (array_keys($counters) as $key) {
840
            if (array_key_exists($key, $_SESSION)) {
841
                $flashes[$key] = $_SESSION[$key];
842
                if ($delete) {
843
                    unset($counters[$key], $_SESSION[$key]);
844
                } elseif ($counters[$key] < 0) {
845
                    // mark for deletion in the next request
846
                    $counters[$key] = 1;
847
                }
848
            } else {
849
                unset($counters[$key]);
850
            }
851
        }
852
853
        $_SESSION[$this->flashParam] = $counters;
854
855
        return $flashes;
856
    }
857
858
    /**
859
     * Sets a flash message.
860
     * A flash message will be automatically deleted after it is accessed in a request and the deletion will happen
861
     * in the next request.
862
     * If there is already an existing flash message with the same key, it will be overwritten by the new one.
863
     * @param string $key the key identifying the flash message. Note that flash messages
864
     * and normal session variables share the same name space. If you have a normal
865
     * session variable using the same name, its value will be overwritten by this method.
866
     * @param mixed $value flash message
867
     * @param bool $removeAfterAccess whether the flash message should be automatically removed only if
868
     * it is accessed. If false, the flash message will be automatically removed after the next request,
869
     * regardless if it is accessed or not. If true (default value), the flash message will remain until after
870
     * it is accessed.
871
     * @see getFlash()
872
     * @see addFlash()
873
     * @see removeFlash()
874
     */
875
    public function setFlash($key, $value = true, $removeAfterAccess = true)
876
    {
877
        $counters = $this->get($this->flashParam, []);
878
        $counters[$key] = $removeAfterAccess ? -1 : 0;
879
        $_SESSION[$key] = $value;
880
        $_SESSION[$this->flashParam] = $counters;
881
    }
882
883
    /**
884
     * Adds a flash message.
885
     * If there are existing flash messages with the same key, the new one will be appended to the existing message array.
886
     * @param string $key the key identifying the flash message.
887
     * @param mixed $value flash message
888
     * @param bool $removeAfterAccess whether the flash message should be automatically removed only if
889
     * it is accessed. If false, the flash message will be automatically removed after the next request,
890
     * regardless if it is accessed or not. If true (default value), the flash message will remain until after
891
     * it is accessed.
892
     * @see getFlash()
893
     * @see setFlash()
894
     * @see removeFlash()
895
     */
896
    public function addFlash($key, $value = true, $removeAfterAccess = true)
897
    {
898
        $counters = $this->get($this->flashParam, []);
899
        $counters[$key] = $removeAfterAccess ? -1 : 0;
900
        $_SESSION[$this->flashParam] = $counters;
901
        if (empty($_SESSION[$key])) {
902
            $_SESSION[$key] = [$value];
903
        } elseif (is_array($_SESSION[$key])) {
904
            $_SESSION[$key][] = $value;
905
        } else {
906
            $_SESSION[$key] = [$_SESSION[$key], $value];
907
        }
908
    }
909
910
    /**
911
     * Removes a flash message.
912
     * @param string $key the key identifying the flash message. Note that flash messages
913
     * and normal session variables share the same name space.  If you have a normal
914
     * session variable using the same name, it will be removed by this method.
915
     * @return mixed the removed flash message. Null if the flash message does not exist.
916
     * @see getFlash()
917
     * @see setFlash()
918
     * @see addFlash()
919
     * @see removeAllFlashes()
920
     */
921
    public function removeFlash($key)
922
    {
923
        $counters = $this->get($this->flashParam, []);
924
        $value = isset($_SESSION[$key], $counters[$key]) ? $_SESSION[$key] : null;
925
        unset($counters[$key], $_SESSION[$key]);
926
        $_SESSION[$this->flashParam] = $counters;
927
928
        return $value;
929
    }
930
931
    /**
932
     * Removes all flash messages.
933
     * Note that flash messages and normal session variables share the same name space.
934
     * If you have a normal session variable using the same name, it will be removed
935
     * by this method.
936
     * @see getFlash()
937
     * @see setFlash()
938
     * @see addFlash()
939
     * @see removeFlash()
940
     */
941
    public function removeAllFlashes()
942
    {
943
        $counters = $this->get($this->flashParam, []);
944
        foreach (array_keys($counters) as $key) {
945
            unset($_SESSION[$key]);
946
        }
947
        unset($_SESSION[$this->flashParam]);
948
    }
949
950
    /**
951
     * Returns a value indicating whether there are flash messages associated with the specified key.
952
     * @param string $key key identifying the flash message type
953
     * @return bool whether any flash messages exist under specified key
954
     */
955
    public function hasFlash($key)
956
    {
957
        return $this->getFlash($key) !== null;
958
    }
959
960
    /**
961
     * This method is required by the interface [[\ArrayAccess]].
962
     * @param int|string $offset the offset to check on
963
     * @return bool
964
     */
965 3
    #[\ReturnTypeWillChange]
966
    public function offsetExists($offset)
967
    {
968 3
        $this->open();
969
970 3
        return isset($_SESSION[$offset]);
971
    }
972
973
    /**
974
     * This method is required by the interface [[\ArrayAccess]].
975
     * @param int|string $offset the offset to retrieve element.
976
     * @return mixed the element at the offset, null if no element is found at the offset
977
     */
978 3
    #[\ReturnTypeWillChange]
979
    public function offsetGet($offset)
980
    {
981 3
        $this->open();
982
983 3
        return isset($_SESSION[$offset]) ? $_SESSION[$offset] : null;
984
    }
985
986
    /**
987
     * This method is required by the interface [[\ArrayAccess]].
988
     * @param int|string $offset the offset to set element
989
     * @param mixed $item the element value
990
     */
991
    #[\ReturnTypeWillChange]
992
    public function offsetSet($offset, $item)
993
    {
994
        $this->open();
995
        $_SESSION[$offset] = $item;
996
    }
997
998
    /**
999
     * This method is required by the interface [[\ArrayAccess]].
1000
     * @param int|string $offset the offset to unset element
1001
     */
1002
    #[\ReturnTypeWillChange]
1003
    public function offsetUnset($offset)
1004
    {
1005
        $this->open();
1006
        unset($_SESSION[$offset]);
1007
    }
1008
1009
    /**
1010
     * If session is started it's not possible to edit session ini settings. In PHP7.2+ it throws exception.
1011
     * This function saves session data to temporary variable and stop session.
1012
     * @since 2.0.14
1013
     */
1014 19
    protected function freeze()
1015
    {
1016 19
        if ($this->getIsActive()) {
1017 4
            if (isset($_SESSION)) {
1018 4
                $this->_frozenSessionData = $_SESSION;
1019
            }
1020 4
            $this->close();
1021 4
            Yii::info('Session frozen', __METHOD__);
1022
        }
1023
    }
1024
1025
    /**
1026
     * Starts session and restores data from temporary variable
1027
     * @since 2.0.14
1028
     */
1029 19
    protected function unfreeze()
1030
    {
1031 19
        if (null !== $this->_frozenSessionData) {
1032 4
            YII_DEBUG ? session_start() : @session_start();
1033
1034 4
            if ($this->getIsActive()) {
1035 4
                Yii::info('Session unfrozen', __METHOD__);
1036
            } else {
1037
                $error = error_get_last();
1038
                $message = isset($error['message']) ? $error['message'] : 'Failed to unfreeze session.';
1039
                Yii::error($message, __METHOD__);
1040
            }
1041
1042 4
            $_SESSION = $this->_frozenSessionData;
1043 4
            $this->_frozenSessionData = null;
1044
        }
1045
    }
1046
1047
    /**
1048
     * Set cache limiter
1049
     *
1050
     * @param string $cacheLimiter
1051
     * @since 2.0.14
1052
     */
1053 1
    public function setCacheLimiter($cacheLimiter)
1054
    {
1055 1
        $this->freeze();
1056 1
        session_cache_limiter($cacheLimiter);
1057 1
        $this->unfreeze();
1058
    }
1059
1060
    /**
1061
     * Returns current cache limiter
1062
     *
1063
     * @return string current cache limiter
1064
     * @since 2.0.14
1065
     */
1066
    public function getCacheLimiter()
1067
    {
1068
        return session_cache_limiter();
1069
    }
1070
}
1071