CookieComponent.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  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 CakePHP(tm) v 1.2.0.4213
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Controller\Component;
  16. use Cake\Controller\Component;
  17. use Cake\Controller\ComponentRegistry;
  18. use Cake\Controller\Controller;
  19. use Cake\Core\Configure;
  20. use Cake\Error;
  21. use Cake\Event\Event;
  22. use Cake\Network\Request;
  23. use Cake\Network\Response;
  24. use Cake\Utility\Hash;
  25. use Cake\Utility\Security;
  26. /**
  27. * Cookie Component.
  28. *
  29. * Cookie handling for the controller.
  30. *
  31. * @link http://book.cakephp.org/2.0/en/core-libraries/components/cookie.html
  32. *
  33. */
  34. class CookieComponent extends Component {
  35. /**
  36. * The name of the cookie.
  37. *
  38. * Overridden with the controller beforeFilter();
  39. * $this->Cookie->name = 'CookieName';
  40. *
  41. * @var string
  42. */
  43. public $name = 'CakeCookie';
  44. /**
  45. * The time a cookie will remain valid.
  46. *
  47. * Can be either integer Unix timestamp or a date string.
  48. *
  49. * Overridden with the controller beforeFilter();
  50. * $this->Cookie->time = '5 Days';
  51. *
  52. * @var mixed
  53. */
  54. public $time = null;
  55. /**
  56. * Cookie path.
  57. *
  58. * Overridden with the controller beforeFilter();
  59. * $this->Cookie->path = '/';
  60. *
  61. * The path on the server in which the cookie will be available on.
  62. * If public $cookiePath is set to '/foo/', the cookie will only be available
  63. * within the /foo/ directory and all sub-directories such as /foo/bar/ of domain.
  64. * The default value is the entire domain.
  65. *
  66. * @var string
  67. */
  68. public $path = '/';
  69. /**
  70. * Domain path.
  71. *
  72. * The domain that the cookie is available.
  73. *
  74. * Overridden with the controller beforeFilter();
  75. * $this->Cookie->domain = '.example.com';
  76. *
  77. * To make the cookie available on all subdomains of example.com.
  78. * Set $this->Cookie->domain = '.example.com'; in your controller beforeFilter
  79. *
  80. * @var string
  81. */
  82. public $domain = '';
  83. /**
  84. * Secure HTTPS only cookie.
  85. *
  86. * Overridden with the controller beforeFilter();
  87. * $this->Cookie->secure = true;
  88. *
  89. * Indicates that the cookie should only be transmitted over a secure HTTPS connection.
  90. * When set to true, the cookie will only be set if a secure connection exists.
  91. *
  92. * @var boolean
  93. */
  94. public $secure = false;
  95. /**
  96. * Encryption key.
  97. *
  98. * Overridden with the controller beforeFilter();
  99. * $this->Cookie->key = 'SomeRandomString';
  100. *
  101. * @var string
  102. */
  103. public $key = null;
  104. /**
  105. * HTTP only cookie
  106. *
  107. * Set to true to make HTTP only cookies. Cookies that are HTTP only
  108. * are not accessible in JavaScript.
  109. *
  110. * @var boolean
  111. */
  112. public $httpOnly = false;
  113. /**
  114. * Values stored in the cookie.
  115. *
  116. * Accessed in the controller using $this->Cookie->read('Name.key');
  117. *
  118. * @see CookieComponent::read();
  119. * @var string
  120. */
  121. protected $_values = array();
  122. /**
  123. * Type of encryption to use.
  124. *
  125. * Defaults to Security::encrypt(); or AES encryption.
  126. *
  127. * @var string
  128. */
  129. protected $_type = 'aes';
  130. /**
  131. * Used to reset cookie time if $expire is passed to CookieComponent::write()
  132. *
  133. * @var string
  134. */
  135. protected $_reset = null;
  136. /**
  137. * Expire time of the cookie
  138. *
  139. * This is controlled by CookieComponent::time;
  140. *
  141. * @var string
  142. */
  143. protected $_expires = 0;
  144. /**
  145. * A reference to the Controller's Cake\Network\Response object
  146. *
  147. * @var Cake\Network\Response
  148. */
  149. protected $_response = null;
  150. /**
  151. * The request from the controller.
  152. *
  153. * @var Cake\Network\Request
  154. */
  155. protected $_request;
  156. /**
  157. * Constructor
  158. *
  159. * @param ComponentRegistry $collection A ComponentRegistry for this component
  160. * @param array $settings Array of settings.
  161. */
  162. public function __construct(ComponentRegistry $collection, $settings = array()) {
  163. $this->key = Configure::read('Security.salt');
  164. parent::__construct($collection, $settings);
  165. if (isset($this->time)) {
  166. $this->_expire($this->time);
  167. }
  168. $controller = $collection->getController();
  169. if ($controller && isset($controller->request)) {
  170. $this->_request = $controller->request;
  171. } else {
  172. $this->_request = Request::createFromGlobals();
  173. }
  174. if ($controller && isset($controller->response)) {
  175. $this->_response = $controller->response;
  176. } else {
  177. $this->_response = new Response();
  178. }
  179. }
  180. /**
  181. * Start CookieComponent for use in the controller
  182. *
  183. * @param Event $event An Event instance
  184. * @return void
  185. */
  186. public function startup(Event $event) {
  187. $this->_expire($this->time);
  188. $this->_values[$this->name] = array();
  189. }
  190. /**
  191. * Write a value to the $_COOKIE[$key];
  192. *
  193. * Optional [Name.], required key, optional $value, optional $encrypt, optional $expires
  194. * $this->Cookie->write('[Name.]key, $value);
  195. *
  196. * By default all values are encrypted.
  197. * You must pass $encrypt false to store values in clear test
  198. *
  199. * You must use this method before any output is sent to the browser.
  200. * Failure to do so will result in header already sent errors.
  201. *
  202. * @param string|array $key Key for the value
  203. * @param mixed $value Value
  204. * @param boolean $encrypt Set to true to encrypt value, false otherwise
  205. * @param integer|string $expires Can be either the number of seconds until a cookie
  206. * expires, or a strtotime compatible time offset.
  207. * @return void
  208. * @link http://book.cakephp.org/2.0/en/core-libraries/components/cookie.html#CookieComponent::write
  209. */
  210. public function write($key, $value = null, $encrypt = true, $expires = null) {
  211. if (empty($this->_values[$this->name])) {
  212. $this->read();
  213. }
  214. if ($encrypt === null) {
  215. $encrypt = true;
  216. }
  217. $this->_encrypted = $encrypt;
  218. $this->_expire($expires);
  219. if (!is_array($key)) {
  220. $key = array($key => $value);
  221. }
  222. foreach ($key as $name => $value) {
  223. if (strpos($name, '.') === false) {
  224. $this->_values[$this->name][$name] = $value;
  225. $this->_write("[$name]", $value);
  226. } else {
  227. $names = explode('.', $name, 2);
  228. if (!isset($this->_values[$this->name][$names[0]])) {
  229. $this->_values[$this->name][$names[0]] = array();
  230. }
  231. $this->_values[$this->name][$names[0]] = Hash::insert($this->_values[$this->name][$names[0]], $names[1], $value);
  232. $this->_write('[' . implode('][', $names) . ']', $value);
  233. }
  234. }
  235. $this->_encrypted = true;
  236. }
  237. /**
  238. * Read the value of the $_COOKIE[$key];
  239. *
  240. * Optional [Name.], required key
  241. * $this->Cookie->read(Name.key);
  242. *
  243. * @param string $key Key of the value to be obtained. If none specified, obtain map key => values
  244. * @return string or null, value for specified key
  245. * @link http://book.cakephp.org/2.0/en/core-libraries/components/cookie.html#CookieComponent::read
  246. */
  247. public function read($key = null) {
  248. $values = $this->_request->cookie($this->name);
  249. if (empty($this->_values[$this->name]) && $values) {
  250. $this->_values[$this->name] = $this->_decrypt($values);
  251. }
  252. if (empty($this->_values[$this->name])) {
  253. $this->_values[$this->name] = array();
  254. }
  255. if ($key === null) {
  256. return $this->_values[$this->name];
  257. }
  258. if (strpos($key, '.') !== false) {
  259. $names = explode('.', $key, 2);
  260. $key = $names[0];
  261. }
  262. if (!isset($this->_values[$this->name][$key])) {
  263. return null;
  264. }
  265. if (!empty($names[1])) {
  266. return Hash::get($this->_values[$this->name][$key], $names[1]);
  267. }
  268. return $this->_values[$this->name][$key];
  269. }
  270. /**
  271. * Returns true if given variable is set in cookie.
  272. *
  273. * @param string $var Variable name to check for
  274. * @return boolean True if variable is there
  275. */
  276. public function check($key = null) {
  277. if (empty($key)) {
  278. return false;
  279. }
  280. return $this->read($key) !== null;
  281. }
  282. /**
  283. * Delete a cookie value
  284. *
  285. * Optional [Name.], required key
  286. * $this->Cookie->read('Name.key);
  287. *
  288. * You must use this method before any output is sent to the browser.
  289. * Failure to do so will result in header already sent errors.
  290. *
  291. * @param string $key Key of the value to be deleted
  292. * @return void
  293. * @link http://book.cakephp.org/2.0/en/core-libraries/components/cookie.html#CookieComponent::delete
  294. */
  295. public function delete($key) {
  296. if (empty($this->_values[$this->name])) {
  297. $this->read();
  298. }
  299. if (strpos($key, '.') === false) {
  300. if (isset($this->_values[$this->name][$key]) && is_array($this->_values[$this->name][$key])) {
  301. foreach ($this->_values[$this->name][$key] as $idx => $val) {
  302. $this->_delete("[$key][$idx]");
  303. }
  304. }
  305. $this->_delete("[$key]");
  306. unset($this->_values[$this->name][$key]);
  307. return;
  308. }
  309. $names = explode('.', $key, 2);
  310. if (isset($this->_values[$this->name][$names[0]])) {
  311. $this->_values[$this->name][$names[0]] = Hash::remove($this->_values[$this->name][$names[0]], $names[1]);
  312. }
  313. $this->_delete('[' . implode('][', $names) . ']');
  314. }
  315. /**
  316. * Destroy current cookie
  317. *
  318. * You must use this method before any output is sent to the browser.
  319. * Failure to do so will result in header already sent errors.
  320. *
  321. * @return void
  322. * @link http://book.cakephp.org/2.0/en/core-libraries/components/cookie.html#CookieComponent::destroy
  323. */
  324. public function destroy() {
  325. if (empty($this->_values[$this->name])) {
  326. $this->read();
  327. }
  328. foreach ($this->_values[$this->name] as $name => $value) {
  329. if (is_array($value)) {
  330. foreach ($value as $key => $val) {
  331. unset($this->_values[$this->name][$name][$key]);
  332. $this->_delete("[$name][$key]");
  333. }
  334. }
  335. unset($this->_values[$this->name][$name]);
  336. $this->_delete("[$name]");
  337. }
  338. }
  339. /**
  340. * Will allow overriding default encryption method. Use this method
  341. * in ex: AppController::beforeFilter() before you have read or
  342. * written any cookies.
  343. *
  344. * @param string $type Encryption method
  345. * @return void
  346. * @throws Cake\Error\Exception When an unknown type is used.
  347. */
  348. public function type($type = 'aes') {
  349. $availableTypes = [
  350. 'rijndael',
  351. 'aes'
  352. ];
  353. if (!in_array($type, $availableTypes)) {
  354. throw new Error\Exception(__d('cake_dev', 'You must use rijndael, or aes for cookie encryption type'));
  355. }
  356. $this->_type = $type;
  357. }
  358. /**
  359. * Set the expire time for a session variable.
  360. *
  361. * Creates a new expire time for a session variable.
  362. * $expire can be either integer Unix timestamp or a date string.
  363. *
  364. * Used by write()
  365. * CookieComponent::write(string, string, boolean, 8400);
  366. * CookieComponent::write(string, string, boolean, '5 Days');
  367. *
  368. * @param integer|string $expires Can be either Unix timestamp, or date string
  369. * @return integer Unix timestamp
  370. */
  371. protected function _expire($expires = null) {
  372. if ($expires === null) {
  373. return $this->_expires;
  374. }
  375. $this->_reset = $this->_expires;
  376. if (!$expires) {
  377. return $this->_expires = 0;
  378. }
  379. $now = new \DateTime();
  380. if (is_int($expires) || is_numeric($expires)) {
  381. return $this->_expires = $now->format('U') + intval($expires);
  382. }
  383. $now->modify($expires);
  384. return $this->_expires = $now->format('U');
  385. }
  386. /**
  387. * Set cookie
  388. *
  389. * @param string $name Name for cookie
  390. * @param string $value Value for cookie
  391. * @return void
  392. */
  393. protected function _write($name, $value) {
  394. $this->_response->cookie(array(
  395. 'name' => $this->name . $name,
  396. 'value' => $this->_encrypt($value),
  397. 'expire' => $this->_expires,
  398. 'path' => $this->path,
  399. 'domain' => $this->domain,
  400. 'secure' => $this->secure,
  401. 'httpOnly' => $this->httpOnly
  402. ));
  403. if (!empty($this->_reset)) {
  404. $this->_expires = $this->_reset;
  405. $this->_reset = null;
  406. }
  407. }
  408. /**
  409. * Sets a cookie expire time to remove cookie value
  410. *
  411. * @param string $name Name of cookie
  412. * @return void
  413. */
  414. protected function _delete($name) {
  415. $this->_response->cookie(array(
  416. 'name' => $this->name . $name,
  417. 'value' => '',
  418. 'expire' => time() - 42000,
  419. 'path' => $this->path,
  420. 'domain' => $this->domain,
  421. 'secure' => $this->secure,
  422. 'httpOnly' => $this->httpOnly
  423. ));
  424. }
  425. /**
  426. * Encrypts $value using public $type method in Security class
  427. *
  428. * @param string $value Value to encrypt
  429. * @return string Encoded values
  430. */
  431. protected function _encrypt($value) {
  432. if (is_array($value)) {
  433. $value = $this->_implode($value);
  434. }
  435. if (!$this->_encrypted) {
  436. return $value;
  437. }
  438. $prefix = "Q2FrZQ==.";
  439. if ($this->_type === 'rijndael') {
  440. $cipher = Security::rijndael($value, $this->key, 'encrypt');
  441. }
  442. if ($this->_type === 'aes') {
  443. $cipher = Security::encrypt($value, $this->key);
  444. }
  445. return $prefix . base64_encode($cipher);
  446. }
  447. /**
  448. * Decrypts $value using public $type method in Security class
  449. *
  450. * @param array $values Values to decrypt
  451. * @return string decrypted string
  452. */
  453. protected function _decrypt($values) {
  454. $decrypted = array();
  455. $type = $this->_type;
  456. foreach ((array)$values as $name => $value) {
  457. if (is_array($value)) {
  458. foreach ($value as $key => $val) {
  459. $decrypted[$name][$key] = $this->_decode($val);
  460. }
  461. } else {
  462. $decrypted[$name] = $this->_decode($value);
  463. }
  464. }
  465. return $decrypted;
  466. }
  467. /**
  468. * Decodes and decrypts a single value.
  469. *
  470. * @param string $value The value to decode & decrypt.
  471. * @return string Decoded value.
  472. */
  473. protected function _decode($value) {
  474. $prefix = 'Q2FrZQ==.';
  475. $pos = strpos($value, $prefix);
  476. if ($pos === false) {
  477. return $this->_explode($value);
  478. }
  479. $value = base64_decode(substr($value, strlen($prefix)));
  480. if ($this->_type === 'rijndael') {
  481. $plain = Security::rijndael($value, $this->key, 'decrypt');
  482. }
  483. if ($this->_type === 'aes') {
  484. $plain = Security::decrypt($value, $this->key);
  485. }
  486. return $this->_explode($plain);
  487. }
  488. /**
  489. * Implode method to keep keys are multidimensional arrays
  490. *
  491. * @param array $array Map of key and values
  492. * @return string A json encoded string.
  493. */
  494. protected function _implode(array $array) {
  495. return json_encode($array);
  496. }
  497. /**
  498. * Explode method to return array from string set in CookieComponent::_implode()
  499. * Maintains reading backwards compatibility with 1.x CookieComponent::_implode().
  500. *
  501. * @param string $string A string containing JSON encoded data, or a bare string.
  502. * @return array Map of key and values
  503. */
  504. protected function _explode($string) {
  505. $first = substr($string, 0, 1);
  506. if ($first === '{' || $first === '[') {
  507. $ret = json_decode($string, true);
  508. return ($ret) ? $ret : $string;
  509. }
  510. $array = array();
  511. foreach (explode(',', $string) as $pair) {
  512. $key = explode('|', $pair);
  513. if (!isset($key[1])) {
  514. return $key[0];
  515. }
  516. $array[$key[0]] = $key[1];
  517. }
  518. return $array;
  519. }
  520. }