PageRenderTime 30ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Http/Session.php

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