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