Session.php 15 KB

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