PageRenderTime 43ms CodeModel.GetById 23ms app.highlight 14ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Http/Session.php

http://github.com/cakephp/cakephp
PHP | 651 lines | 342 code | 78 blank | 231 comment | 75 complexity | 072ce635b75f0fec77ed4e864566ddb7 MD5 | raw file
  1<?php
  2declare(strict_types=1);
  3
  4/**
  5 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  6 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  7 *
  8 * Licensed under The MIT License
  9 * For full copyright and license information, please see the LICENSE.txt
 10 * Redistributions of files must retain the above copyright notice.
 11 *
 12 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 13 * @link          https://cakephp.org CakePHP(tm) Project
 14 * @since         0.10.0
 15 * @license       https://opensource.org/licenses/mit-license.php MIT License
 16 */
 17namespace Cake\Http;
 18
 19use Cake\Core\App;
 20use Cake\Utility\Hash;
 21use InvalidArgumentException;
 22use RuntimeException;
 23use SessionHandlerInterface;
 24
 25/**
 26 * This class is a wrapper for the native PHP session functions. It provides
 27 * several defaults for the most common session configuration
 28 * via external handlers and helps with using session in cli without any warnings.
 29 *
 30 * Sessions can be created from the defaults using `Session::create()` or you can get
 31 * an instance of a new session by just instantiating this class and passing the complete
 32 * options you want to use.
 33 *
 34 * When specific options are omitted, this class will take its defaults from the configuration
 35 * values from the `session.*` directives in php.ini. This class will also alter such
 36 * directives when configuration values are provided.
 37 */
 38class Session
 39{
 40    /**
 41     * The Session handler instance used as an engine for persisting the session data.
 42     *
 43     * @var \SessionHandlerInterface
 44     */
 45    protected $_engine;
 46
 47    /**
 48     * Indicates whether the sessions has already started
 49     *
 50     * @var bool
 51     */
 52    protected $_started;
 53
 54    /**
 55     * The time in seconds the session will be valid for
 56     *
 57     * @var int
 58     */
 59    protected $_lifetime;
 60
 61    /**
 62     * Whether this session is running under a CLI environment
 63     *
 64     * @var bool
 65     */
 66    protected $_isCLI = false;
 67
 68    /**
 69     * Returns a new instance of a session after building a configuration bundle for it.
 70     * This function allows an options array which will be used for configuring the session
 71     * and the handler to be used. The most important key in the configuration array is
 72     * `defaults`, which indicates the set of configurations to inherit from, the possible
 73     * defaults are:
 74     *
 75     * - php: just use session as configured in php.ini
 76     * - cache: Use the CakePHP caching system as an storage for the session, you will need
 77     *   to pass the `config` key with the name of an already configured Cache engine.
 78     * - database: Use the CakePHP ORM to persist and manage sessions. By default this requires
 79     *   a table in your database named `sessions` or a `model` key in the configuration
 80     *   to indicate which Table object to use.
 81     * - cake: Use files for storing the sessions, but let CakePHP manage them and decide
 82     *   where to store them.
 83     *
 84     * The full list of options follows:
 85     *
 86     * - defaults: either 'php', 'database', 'cache' or 'cake' as explained above.
 87     * - handler: An array containing the handler configuration
 88     * - ini: A list of php.ini directives to set before the session starts.
 89     * - timeout: The time in minutes the session should stay active
 90     *
 91     * @param array $sessionConfig Session config.
 92     * @return static
 93     * @see \Cake\Http\Session::__construct()
 94     */
 95    public static function create(array $sessionConfig = [])
 96    {
 97        if (isset($sessionConfig['defaults'])) {
 98            $defaults = static::_defaultConfig($sessionConfig['defaults']);
 99            if ($defaults) {
100                $sessionConfig = Hash::merge($defaults, $sessionConfig);
101            }
102        }
103
104        if (
105            !isset($sessionConfig['ini']['session.cookie_secure'])
106            && env('HTTPS')
107            && ini_get('session.cookie_secure') != 1
108        ) {
109            $sessionConfig['ini']['session.cookie_secure'] = 1;
110        }
111
112        if (
113            !isset($sessionConfig['ini']['session.name'])
114            && isset($sessionConfig['cookie'])
115        ) {
116            $sessionConfig['ini']['session.name'] = $sessionConfig['cookie'];
117        }
118
119        if (!isset($sessionConfig['ini']['session.use_strict_mode']) && ini_get('session.use_strict_mode') != 1) {
120            $sessionConfig['ini']['session.use_strict_mode'] = 1;
121        }
122
123        if (!isset($sessionConfig['ini']['session.cookie_httponly']) && ini_get('session.cookie_httponly') != 1) {
124            $sessionConfig['ini']['session.cookie_httponly'] = 1;
125        }
126
127        return new static($sessionConfig);
128    }
129
130    /**
131     * Get one of the prebaked default session configurations.
132     *
133     * @param string $name Config name.
134     * @return array|false
135     */
136    protected static function _defaultConfig(string $name)
137    {
138        $tmp = defined('TMP') ? TMP : sys_get_temp_dir() . DIRECTORY_SEPARATOR;
139        $defaults = [
140            'php' => [
141                'ini' => [
142                    'session.use_trans_sid' => 0,
143                ],
144            ],
145            'cake' => [
146                'ini' => [
147                    'session.use_trans_sid' => 0,
148                    'session.serialize_handler' => 'php',
149                    'session.use_cookies' => 1,
150                    'session.save_path' => $tmp . 'sessions',
151                    'session.save_handler' => 'files',
152                ],
153            ],
154            'cache' => [
155                'ini' => [
156                    'session.use_trans_sid' => 0,
157                    'session.use_cookies' => 1,
158                ],
159                'handler' => [
160                    'engine' => 'CacheSession',
161                    'config' => 'default',
162                ],
163            ],
164            'database' => [
165                'ini' => [
166                    'session.use_trans_sid' => 0,
167                    'session.use_cookies' => 1,
168                    'session.serialize_handler' => 'php',
169                ],
170                'handler' => [
171                    'engine' => 'DatabaseSession',
172                ],
173            ],
174        ];
175
176        if (isset($defaults[$name])) {
177            if (
178                PHP_VERSION_ID >= 70300
179                && ($name !== 'php' || empty(ini_get('session.cookie_samesite')))
180            ) {
181                $defaults['php']['ini']['session.cookie_samesite'] = 'Lax';
182            }
183
184            return $defaults[$name];
185        }
186
187        return false;
188    }
189
190    /**
191     * Constructor.
192     *
193     * ### Configuration:
194     *
195     * - timeout: The time in minutes the session should be valid for.
196     * - cookiePath: The url path for which session cookie is set. Maps to the
197     *   `session.cookie_path` php.ini config. Defaults to base path of app.
198     * - ini: A list of php.ini directives to change before the session start.
199     * - handler: An array containing at least the `engine` key. To be used as the session
200     *   engine for persisting data. The rest of the keys in the array will be passed as
201     *   the configuration array for the engine. You can set the `engine` key to an already
202     *   instantiated session handler object.
203     *
204     * @param array $config The Configuration to apply to this session object
205     */
206    public function __construct(array $config = [])
207    {
208        $config += [
209            'timeout' => null,
210            'cookie' => null,
211            'ini' => [],
212            'handler' => [],
213        ];
214
215        if ($config['timeout']) {
216            $config['ini']['session.gc_maxlifetime'] = 60 * $config['timeout'];
217        }
218
219        if ($config['cookie']) {
220            $config['ini']['session.name'] = $config['cookie'];
221        }
222
223        if (!isset($config['ini']['session.cookie_path'])) {
224            $cookiePath = empty($config['cookiePath']) ? '/' : $config['cookiePath'];
225            $config['ini']['session.cookie_path'] = $cookiePath;
226        }
227
228        $this->options($config['ini']);
229
230        if (!empty($config['handler'])) {
231            $class = $config['handler']['engine'];
232            unset($config['handler']['engine']);
233            $this->engine($class, $config['handler']);
234        }
235
236        $this->_lifetime = (int)ini_get('session.gc_maxlifetime');
237        $this->_isCLI = (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg');
238        session_register_shutdown();
239    }
240
241    /**
242     * Sets the session handler instance to use for this session.
243     * If a string is passed for the first argument, it will be treated as the
244     * class name and the second argument will be passed as the first argument
245     * in the constructor.
246     *
247     * If an instance of a SessionHandlerInterface is provided as the first argument,
248     * the handler will be set to it.
249     *
250     * If no arguments are passed it will return the currently configured handler instance
251     * or null if none exists.
252     *
253     * @param string|\SessionHandlerInterface|null $class The session handler to use
254     * @param array $options the options to pass to the SessionHandler constructor
255     * @return \SessionHandlerInterface|null
256     * @throws \InvalidArgumentException
257     */
258    public function engine($class = null, array $options = []): ?SessionHandlerInterface
259    {
260        if ($class === null) {
261            return $this->_engine;
262        }
263        if ($class instanceof SessionHandlerInterface) {
264            return $this->setEngine($class);
265        }
266        $className = App::className($class, 'Http/Session');
267
268        if (!$className) {
269            throw new InvalidArgumentException(
270                sprintf('The class "%s" does not exist and cannot be used as a session engine', $class)
271            );
272        }
273
274        $handler = new $className($options);
275        if (!($handler instanceof SessionHandlerInterface)) {
276            throw new InvalidArgumentException(
277                'The chosen SessionHandler does not implement SessionHandlerInterface, it cannot be used as an engine.'
278            );
279        }
280
281        return $this->setEngine($handler);
282    }
283
284    /**
285     * Set the engine property and update the session handler in PHP.
286     *
287     * @param \SessionHandlerInterface $handler The handler to set
288     * @return \SessionHandlerInterface
289     */
290    protected function setEngine(SessionHandlerInterface $handler): SessionHandlerInterface
291    {
292        if (!headers_sent() && session_status() !== \PHP_SESSION_ACTIVE) {
293            session_set_save_handler($handler, false);
294        }
295
296        return $this->_engine = $handler;
297    }
298
299    /**
300     * Calls ini_set for each of the keys in `$options` and set them
301     * to the respective value in the passed array.
302     *
303     * ### Example:
304     *
305     * ```
306     * $session->options(['session.use_cookies' => 1]);
307     * ```
308     *
309     * @param array $options Ini options to set.
310     * @return void
311     * @throws \RuntimeException if any directive could not be set
312     */
313    public function options(array $options): void
314    {
315        if (session_status() === \PHP_SESSION_ACTIVE || headers_sent()) {
316            return;
317        }
318
319        foreach ($options as $setting => $value) {
320            if (ini_set($setting, (string)$value) === false) {
321                throw new RuntimeException(
322                    sprintf('Unable to configure the session, setting %s failed.', $setting)
323                );
324            }
325        }
326    }
327
328    /**
329     * Starts the Session.
330     *
331     * @return bool True if session was started
332     * @throws \RuntimeException if the session was already started
333     */
334    public function start(): bool
335    {
336        if ($this->_started) {
337            return true;
338        }
339
340        if ($this->_isCLI) {
341            $_SESSION = [];
342            $this->id('cli');
343
344            return $this->_started = true;
345        }
346
347        if (session_status() === \PHP_SESSION_ACTIVE) {
348            throw new RuntimeException('Session was already started');
349        }
350
351        if (ini_get('session.use_cookies') && headers_sent()) {
352            return false;
353        }
354
355        if (!session_start()) {
356            throw new RuntimeException('Could not start the session');
357        }
358
359        $this->_started = true;
360
361        if ($this->_timedOut()) {
362            $this->destroy();
363
364            return $this->start();
365        }
366
367        return $this->_started;
368    }
369
370    /**
371     * Write data and close the session
372     *
373     * @return true
374     */
375    public function close(): bool
376    {
377        if (!$this->_started) {
378            return true;
379        }
380
381        if ($this->_isCLI) {
382            $this->_started = false;
383
384            return true;
385        }
386
387        if (!session_write_close()) {
388            throw new RuntimeException('Could not close the session');
389        }
390
391        $this->_started = false;
392
393        return true;
394    }
395
396    /**
397     * Determine if Session has already been started.
398     *
399     * @return bool True if session has been started.
400     */
401    public function started(): bool
402    {
403        return $this->_started || session_status() === \PHP_SESSION_ACTIVE;
404    }
405
406    /**
407     * Returns true if given variable name is set in session.
408     *
409     * @param string|null $name Variable name to check for
410     * @return bool True if variable is there
411     */
412    public function check(?string $name = null): bool
413    {
414        if ($this->_hasSession() && !$this->started()) {
415            $this->start();
416        }
417
418        if (!isset($_SESSION)) {
419            return false;
420        }
421
422        if ($name === null) {
423            return (bool)$_SESSION;
424        }
425
426        return Hash::get($_SESSION, $name) !== null;
427    }
428
429    /**
430     * Returns given session variable, or all of them, if no parameters given.
431     *
432     * @param string|null $name The name of the session variable (or a path as sent to Hash.extract)
433     * @return string|array|null The value of the session variable, null if session not available,
434     *   session not started, or provided name not found in the session.
435     */
436    public function read(?string $name = null)
437    {
438        if ($this->_hasSession() && !$this->started()) {
439            $this->start();
440        }
441
442        if (!isset($_SESSION)) {
443            return null;
444        }
445
446        if ($name === null) {
447            return $_SESSION ?: [];
448        }
449
450        return Hash::get($_SESSION, $name);
451    }
452
453    /**
454     * Reads and deletes a variable from session.
455     *
456     * @param string $name The key to read and remove (or a path as sent to Hash.extract).
457     * @return mixed The value of the session variable, null if session not available,
458     *   session not started, or provided name not found in the session.
459     */
460    public function consume(string $name)
461    {
462        if (empty($name)) {
463            return null;
464        }
465        $value = $this->read($name);
466        if ($value !== null) {
467            $this->_overwrite($_SESSION, Hash::remove($_SESSION, $name));
468        }
469
470        return $value;
471    }
472
473    /**
474     * Writes value to given session variable name.
475     *
476     * @param string|array $name Name of variable
477     * @param mixed $value Value to write
478     * @return void
479     */
480    public function write($name, $value = null): void
481    {
482        if (!$this->started()) {
483            $this->start();
484        }
485
486        if (!is_array($name)) {
487            $name = [$name => $value];
488        }
489
490        $data = $_SESSION ?? [];
491        foreach ($name as $key => $val) {
492            $data = Hash::insert($data, $key, $val);
493        }
494
495        $this->_overwrite($_SESSION, $data);
496    }
497
498    /**
499     * Returns the session id.
500     * Calling this method will not auto start the session. You might have to manually
501     * assert a started session.
502     *
503     * Passing an id into it, you can also replace the session id if the session
504     * has not already been started.
505     * Note that depending on the session handler, not all characters are allowed
506     * within the session id. For example, the file session handler only allows
507     * characters in the range a-z A-Z 0-9 , (comma) and - (minus).
508     *
509     * @param string|null $id Id to replace the current session id
510     * @return string Session id
511     */
512    public function id(?string $id = null): string
513    {
514        if ($id !== null && !headers_sent()) {
515            session_id($id);
516        }
517
518        return session_id();
519    }
520
521    /**
522     * Removes a variable from session.
523     *
524     * @param string $name Session variable to remove
525     * @return void
526     */
527    public function delete(string $name): void
528    {
529        if ($this->check($name)) {
530            $this->_overwrite($_SESSION, Hash::remove($_SESSION, $name));
531        }
532    }
533
534    /**
535     * Used to write new data to _SESSION, since PHP doesn't like us setting the _SESSION var itself.
536     *
537     * @param array $old Set of old variables => values
538     * @param array $new New set of variable => value
539     * @return void
540     */
541    protected function _overwrite(array &$old, array $new): void
542    {
543        if (!empty($old)) {
544            foreach ($old as $key => $var) {
545                if (!isset($new[$key])) {
546                    unset($old[$key]);
547                }
548            }
549        }
550        foreach ($new as $key => $var) {
551            $old[$key] = $var;
552        }
553    }
554
555    /**
556     * Helper method to destroy invalid sessions.
557     *
558     * @return void
559     */
560    public function destroy(): void
561    {
562        if ($this->_hasSession() && !$this->started()) {
563            $this->start();
564        }
565
566        if (!$this->_isCLI && session_status() === \PHP_SESSION_ACTIVE) {
567            session_destroy();
568        }
569
570        $_SESSION = [];
571        $this->_started = false;
572    }
573
574    /**
575     * Clears the session.
576     *
577     * Optionally it also clears the session id and renews the session.
578     *
579     * @param bool $renew If session should be renewed, as well. Defaults to false.
580     * @return void
581     */
582    public function clear(bool $renew = false): void
583    {
584        $_SESSION = [];
585        if ($renew) {
586            $this->renew();
587        }
588    }
589
590    /**
591     * Returns whether a session exists
592     *
593     * @return bool
594     */
595    protected function _hasSession(): bool
596    {
597        return !ini_get('session.use_cookies')
598            || isset($_COOKIE[session_name()])
599            || $this->_isCLI
600            || (ini_get('session.use_trans_sid') && isset($_GET[session_name()]));
601    }
602
603    /**
604     * Restarts this session.
605     *
606     * @return void
607     */
608    public function renew(): void
609    {
610        if (!$this->_hasSession() || $this->_isCLI) {
611            return;
612        }
613
614        $this->start();
615        $params = session_get_cookie_params();
616        setcookie(
617            session_name(),
618            '',
619            time() - 42000,
620            $params['path'],
621            $params['domain'],
622            $params['secure'],
623            $params['httponly']
624        );
625
626        if (session_id() !== '') {
627            session_regenerate_id(true);
628        }
629    }
630
631    /**
632     * Returns true if the session is no longer valid because the last time it was
633     * accessed was after the configured timeout.
634     *
635     * @return bool
636     */
637    protected function _timedOut(): bool
638    {
639        $time = $this->read('Config.time');
640        $result = false;
641
642        $checkTime = $time !== null && $this->_lifetime > 0;
643        if ($checkTime && (time() - (int)$time > $this->_lifetime)) {
644            $result = true;
645        }
646
647        $this->write('Config.time', time());
648
649        return $result;
650    }
651}