Session.php 18 KB

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