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: * The Session handler instance used as an engine for persisting the session data.
40: *
41: * @var \SessionHandlerInterface
42: */
43: protected $_engine;
44:
45: /**
46: * Indicates whether the sessions has already started
47: *
48: * @var bool
49: */
50: protected $_started;
51:
52: /**
53: * The time in seconds the session will be valid for
54: *
55: * @var int
56: */
57: protected $_lifetime;
58:
59: /**
60: * Whether this session is running under a CLI environment
61: *
62: * @var bool
63: */
64: protected $_isCLI = false;
65:
66: /**
67: * Returns a new instance of a session after building a configuration bundle for it.
68: * This function allows an options array which will be used for configuring the session
69: * and the handler to be used. The most important key in the configuration array is
70: * `defaults`, which indicates the set of configurations to inherit from, the possible
71: * defaults are:
72: *
73: * - php: just use session as configured in php.ini
74: * - cache: Use the CakePHP caching system as an storage for the session, you will need
75: * to pass the `config` key with the name of an already configured Cache engine.
76: * - database: Use the CakePHP ORM to persist and manage sessions. By default this requires
77: * a table in your database named `sessions` or a `model` key in the configuration
78: * to indicate which Table object to use.
79: * - cake: Use files for storing the sessions, but let CakePHP manage them and decide
80: * where to store them.
81: *
82: * The full list of options follows:
83: *
84: * - defaults: either 'php', 'database', 'cache' or 'cake' as explained above.
85: * - handler: An array containing the handler configuration
86: * - ini: A list of php.ini directives to set before the session starts.
87: * - timeout: The time in minutes the session should stay active
88: *
89: * @param array $sessionConfig Session config.
90: * @return static
91: * @see \Cake\Http\Session::__construct()
92: */
93: public static function create($sessionConfig = [])
94: {
95: if (isset($sessionConfig['defaults'])) {
96: $defaults = static::_defaultConfig($sessionConfig['defaults']);
97: if ($defaults) {
98: $sessionConfig = Hash::merge($defaults, $sessionConfig);
99: }
100: }
101:
102: if (!isset($sessionConfig['ini']['session.cookie_secure']) && env('HTTPS') && ini_get('session.cookie_secure') != 1) {
103: $sessionConfig['ini']['session.cookie_secure'] = 1;
104: }
105:
106: if (!isset($sessionConfig['ini']['session.name'])) {
107: $sessionConfig['ini']['session.name'] = $sessionConfig['cookie'];
108: }
109:
110: if (!empty($sessionConfig['handler'])) {
111: $sessionConfig['ini']['session.save_handler'] = 'user';
112: }
113:
114: // In PHP7.2.0+ session.save_handler can't be set to user by the user.
115: // https://github.com/php/php-src/commit/a93a51c3bf4ea1638ce0adc4a899cb93531b9f0d
116: if (version_compare(PHP_VERSION, '7.2.0', '>=')) {
117: unset($sessionConfig['ini']['session.save_handler']);
118: }
119:
120: if (!isset($sessionConfig['ini']['session.use_strict_mode']) && ini_get('session.use_strict_mode') != 1) {
121: $sessionConfig['ini']['session.use_strict_mode'] = 1;
122: }
123:
124: if (!isset($sessionConfig['ini']['session.cookie_httponly']) && ini_get('session.cookie_httponly') != 1) {
125: $sessionConfig['ini']['session.cookie_httponly'] = 1;
126: }
127:
128: return new static($sessionConfig);
129: }
130:
131: /**
132: * Get one of the prebaked default session configurations.
133: *
134: * @param string $name Config name.
135: * @return bool|array
136: */
137: protected static function _defaultConfig($name)
138: {
139: $defaults = [
140: 'php' => [
141: 'cookie' => 'CAKEPHP',
142: 'ini' => [
143: 'session.use_trans_sid' => 0,
144: ]
145: ],
146: 'cake' => [
147: 'cookie' => 'CAKEPHP',
148: 'ini' => [
149: 'session.use_trans_sid' => 0,
150: 'session.serialize_handler' => 'php',
151: 'session.use_cookies' => 1,
152: 'session.save_path' => TMP . 'sessions',
153: 'session.save_handler' => 'files'
154: ]
155: ],
156: 'cache' => [
157: 'cookie' => 'CAKEPHP',
158: 'ini' => [
159: 'session.use_trans_sid' => 0,
160: 'session.use_cookies' => 1,
161: 'session.save_handler' => 'user',
162: ],
163: 'handler' => [
164: 'engine' => 'CacheSession',
165: 'config' => 'default'
166: ]
167: ],
168: 'database' => [
169: 'cookie' => 'CAKEPHP',
170: 'ini' => [
171: 'session.use_trans_sid' => 0,
172: 'session.use_cookies' => 1,
173: 'session.save_handler' => 'user',
174: 'session.serialize_handler' => 'php',
175: ],
176: 'handler' => [
177: 'engine' => 'DatabaseSession'
178: ]
179: ]
180: ];
181:
182: if (isset($defaults[$name])) {
183: return $defaults[$name];
184: }
185:
186: return false;
187: }
188:
189: /**
190: * Constructor.
191: *
192: * ### Configuration:
193: *
194: * - timeout: The time in minutes the session should be valid for.
195: * - cookiePath: The url path for which session cookie is set. Maps to the
196: * `session.cookie_path` php.ini config. Defaults to base path of app.
197: * - ini: A list of php.ini directives to change before the session start.
198: * - handler: An array containing at least the `class` key. To be used as the session
199: * engine for persisting data. The rest of the keys in the array will be passed as
200: * the configuration array for the engine. You can set the `class` key to an already
201: * instantiated session handler object.
202: *
203: * @param array $config The Configuration to apply to this session object
204: */
205: public function __construct(array $config = [])
206: {
207: if (isset($config['timeout'])) {
208: $config['ini']['session.gc_maxlifetime'] = 60 * $config['timeout'];
209: }
210:
211: if (!empty($config['cookie'])) {
212: $config['ini']['session.name'] = $config['cookie'];
213: }
214:
215: if (!isset($config['ini']['session.cookie_path'])) {
216: $cookiePath = empty($config['cookiePath']) ? '/' : $config['cookiePath'];
217: $config['ini']['session.cookie_path'] = $cookiePath;
218: }
219:
220: if (!empty($config['ini']) && is_array($config['ini'])) {
221: $this->options($config['ini']);
222: }
223:
224: if (!empty($config['handler']['engine'])) {
225: $class = $config['handler']['engine'];
226: unset($config['handler']['engine']);
227: $this->engine($class, $config['handler']);
228: }
229:
230: $this->_lifetime = (int)ini_get('session.gc_maxlifetime');
231: $this->_isCLI = (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg');
232: session_register_shutdown();
233: }
234:
235: /**
236: * Sets the session handler instance to use for this session.
237: * If a string is passed for the first argument, it will be treated as the
238: * class name and the second argument will be passed as the first argument
239: * in the constructor.
240: *
241: * If an instance of a SessionHandlerInterface is provided as the first argument,
242: * the handler will be set to it.
243: *
244: * If no arguments are passed it will return the currently configured handler instance
245: * or null if none exists.
246: *
247: * @param string|\SessionHandlerInterface|null $class The session handler to use
248: * @param array $options the options to pass to the SessionHandler constructor
249: * @return \SessionHandlerInterface|null
250: * @throws \InvalidArgumentException
251: */
252: public function engine($class = null, array $options = [])
253: {
254: if ($class === null) {
255: return $this->_engine;
256: }
257: if ($class instanceof SessionHandlerInterface) {
258: return $this->setEngine($class);
259: }
260: $className = App::className($class, 'Http/Session');
261:
262: if (!$className) {
263: $className = App::className($class, 'Network/Session');
264: if ($className) {
265: deprecationWarning('Session adapters should be moved to the Http/Session namespace.');
266: }
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)
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)
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()
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($file, $line)) {
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 bool True if session was started
374: */
375: public function close()
376: {
377: if (!$this->_started) {
378: return true;
379: }
380:
381: if (!session_write_close()) {
382: throw new RuntimeException('Could not close the session');
383: }
384:
385: $this->_started = false;
386:
387: return true;
388: }
389:
390: /**
391: * Determine if Session has already been started.
392: *
393: * @return bool True if session has been started.
394: */
395: public function started()
396: {
397: return $this->_started || session_status() === \PHP_SESSION_ACTIVE;
398: }
399:
400: /**
401: * Returns true if given variable name is set in session.
402: *
403: * @param string|null $name Variable name to check for
404: * @return bool True if variable is there
405: */
406: public function check($name = null)
407: {
408: if ($this->_hasSession() && !$this->started()) {
409: $this->start();
410: }
411:
412: if (!isset($_SESSION)) {
413: return false;
414: }
415:
416: return Hash::get($_SESSION, $name) !== null;
417: }
418:
419: /**
420: * Returns given session variable, or all of them, if no parameters given.
421: *
422: * @param string|null $name The name of the session variable (or a path as sent to Hash.extract)
423: * @return string|array|null The value of the session variable, null if session not available,
424: * session not started, or provided name not found in the session.
425: */
426: public function read($name = null)
427: {
428: if ($this->_hasSession() && !$this->started()) {
429: $this->start();
430: }
431:
432: if (!isset($_SESSION)) {
433: return null;
434: }
435:
436: if ($name === null) {
437: return isset($_SESSION) ? $_SESSION : [];
438: }
439:
440: return Hash::get($_SESSION, $name);
441: }
442:
443: /**
444: * Reads and deletes a variable from session.
445: *
446: * @param string $name The key to read and remove (or a path as sent to Hash.extract).
447: * @return mixed The value of the session variable, null if session not available,
448: * session not started, or provided name not found in the session.
449: */
450: public function consume($name)
451: {
452: if (empty($name)) {
453: return null;
454: }
455: $value = $this->read($name);
456: if ($value !== null) {
457: $this->_overwrite($_SESSION, Hash::remove($_SESSION, $name));
458: }
459:
460: return $value;
461: }
462:
463: /**
464: * Writes value to given session variable name.
465: *
466: * @param string|array $name Name of variable
467: * @param mixed $value Value to write
468: * @return void
469: */
470: public function write($name, $value = null)
471: {
472: if (!$this->started()) {
473: $this->start();
474: }
475:
476: $write = $name;
477: if (!is_array($name)) {
478: $write = [$name => $value];
479: }
480:
481: $data = isset($_SESSION) ? $_SESSION : [];
482: foreach ($write as $key => $val) {
483: $data = Hash::insert($data, $key, $val);
484: }
485:
486: $this->_overwrite($_SESSION, $data);
487: }
488:
489: /**
490: * Returns the session id.
491: * Calling this method will not auto start the session. You might have to manually
492: * assert a started session.
493: *
494: * Passing an id into it, you can also replace the session id if the session
495: * has not already been started.
496: * Note that depending on the session handler, not all characters are allowed
497: * within the session id. For example, the file session handler only allows
498: * characters in the range a-z A-Z 0-9 , (comma) and - (minus).
499: *
500: * @param string|null $id Id to replace the current session id
501: * @return string Session id
502: */
503: public function id($id = null)
504: {
505: if ($id !== null && !headers_sent()) {
506: session_id($id);
507: }
508:
509: return session_id();
510: }
511:
512: /**
513: * Removes a variable from session.
514: *
515: * @param string $name Session variable to remove
516: * @return void
517: */
518: public function delete($name)
519: {
520: if ($this->check($name)) {
521: $this->_overwrite($_SESSION, Hash::remove($_SESSION, $name));
522: }
523: }
524:
525: /**
526: * Used to write new data to _SESSION, since PHP doesn't like us setting the _SESSION var itself.
527: *
528: * @param array $old Set of old variables => values
529: * @param array $new New set of variable => value
530: * @return void
531: */
532: protected function _overwrite(&$old, $new)
533: {
534: if (!empty($old)) {
535: foreach ($old as $key => $var) {
536: if (!isset($new[$key])) {
537: unset($old[$key]);
538: }
539: }
540: }
541: foreach ($new as $key => $var) {
542: $old[$key] = $var;
543: }
544: }
545:
546: /**
547: * Helper method to destroy invalid sessions.
548: *
549: * @return void
550: */
551: public function destroy()
552: {
553: if ($this->_hasSession() && !$this->started()) {
554: $this->start();
555: }
556:
557: if (!$this->_isCLI && session_status() === \PHP_SESSION_ACTIVE) {
558: session_destroy();
559: }
560:
561: $_SESSION = [];
562: $this->_started = false;
563: }
564:
565: /**
566: * Clears the session.
567: *
568: * Optionally it also clears the session id and renews the session.
569: *
570: * @param bool $renew If session should be renewed, as well. Defaults to false.
571: * @return void
572: */
573: public function clear($renew = false)
574: {
575: $_SESSION = [];
576: if ($renew) {
577: $this->renew();
578: }
579: }
580:
581: /**
582: * Returns whether a session exists
583: *
584: * @return bool
585: */
586: protected function _hasSession()
587: {
588: return !ini_get('session.use_cookies')
589: || isset($_COOKIE[session_name()])
590: || $this->_isCLI
591: || (ini_get('session.use_trans_sid') && isset($_GET[session_name()]));
592: }
593:
594: /**
595: * Restarts this session.
596: *
597: * @return void
598: */
599: public function renew()
600: {
601: if (!$this->_hasSession() || $this->_isCLI) {
602: return;
603: }
604:
605: $this->start();
606: $params = session_get_cookie_params();
607: setcookie(
608: session_name(),
609: '',
610: time() - 42000,
611: $params['path'],
612: $params['domain'],
613: $params['secure'],
614: $params['httponly']
615: );
616:
617: if (session_id()) {
618: session_regenerate_id(true);
619: }
620: }
621:
622: /**
623: * Returns true if the session is no longer valid because the last time it was
624: * accessed was after the configured timeout.
625: *
626: * @return bool
627: */
628: protected function _timedOut()
629: {
630: $time = $this->read('Config.time');
631: $result = false;
632:
633: $checkTime = $time !== null && $this->_lifetime > 0;
634: if ($checkTime && (time() - (int)$time > $this->_lifetime)) {
635: $result = true;
636: }
637:
638: $this->write('Config.time', time());
639:
640: return $result;
641: }
642: }
643: