Session.php 18 KB

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