CookieComponent.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  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 1.2.0
  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\Core\Configure;
  19. use Cake\I18n\Time;
  20. use Cake\Network\Request;
  21. use Cake\Network\Response;
  22. use Cake\Utility\Hash;
  23. use Cake\Utility\Security;
  24. /**
  25. * Cookie Component.
  26. *
  27. * Provides enhanced cookie handling features for use in the controller layer.
  28. * In addition to the basic features offered be Cake\Network\Response, this class lets you:
  29. *
  30. * - Create and read encrypted cookies.
  31. * - Store non-scalar data.
  32. * - Use hash compatible syntax to read/write/delete values.
  33. *
  34. * @link http://book.cakephp.org/2.0/en/core-libraries/components/cookie.html
  35. */
  36. class CookieComponent extends Component {
  37. /**
  38. * Default config
  39. *
  40. * - `expires` - How long the cookies should last for. Defaults to 1 month.
  41. * - `path` - The path on the server in which the cookie will be available on.
  42. * If path is set to '/foo/', the cookie will only be available within the
  43. * /foo/ directory and all sub-directories such as /foo/bar/ of domain.
  44. * The default value is base path of app. For e.g. if your app is running
  45. * under a subfolder "cakeapp" of document root the path would be "/cakeapp"
  46. * else it would be "/".
  47. * - `domain` - The domain that the cookie is available. To make the cookie
  48. * available on all subdomains of example.com set domain to '.example.com'.
  49. * - `secure` - Indicates that the cookie should only be transmitted over a
  50. * secure HTTPS connection. When set to true, the cookie will only be set if
  51. * a secure connection exists.
  52. * - `key` - Encryption key used when encrypted cookies are enabled. Defaults to Security.salt.
  53. * - `httpOnly` - Set to true to make HTTP only cookies. Cookies that are HTTP only
  54. * are not accessible in JavaScript. Default false.
  55. * - `encryption` - Type of encryption to use. Defaults to 'aes'.
  56. *
  57. * @var array
  58. */
  59. protected $_defaultConfig = [
  60. 'path' => null,
  61. 'domain' => '',
  62. 'secure' => false,
  63. 'key' => null,
  64. 'httpOnly' => false,
  65. 'encryption' => 'aes',
  66. 'expires' => '+1 month',
  67. ];
  68. /**
  69. * Config specific to a given top level key name.
  70. *
  71. * The values in this array are merged with the general config
  72. * to generate the configuration for a given top level cookie name.
  73. *
  74. * @var array
  75. */
  76. protected $_keyConfig = [];
  77. /**
  78. * Values stored in the cookie.
  79. *
  80. * Accessed in the controller using $this->Cookie->read('Name.key');
  81. *
  82. * @var string
  83. */
  84. protected $_values = [];
  85. /**
  86. * A map of keys that have been loaded.
  87. *
  88. * Since CookieComponent lazily reads cookie data,
  89. * we need to track which cookies have been read to account for
  90. * read, delete, read patterns.
  91. *
  92. * @var array
  93. */
  94. protected $_loaded = [];
  95. /**
  96. * A reference to the Controller's Cake\Network\Response object
  97. *
  98. * @var \Cake\Network\Response
  99. */
  100. protected $_response = null;
  101. /**
  102. * The request from the controller.
  103. *
  104. * @var \Cake\Network\Request
  105. */
  106. protected $_request;
  107. /**
  108. * Valid cipher names for encrypted cookies.
  109. *
  110. * @var array
  111. */
  112. protected $_validCiphers = ['aes', 'rijndael'];
  113. /**
  114. * Constructor
  115. *
  116. * @param ComponentRegistry $collection A ComponentRegistry for this component
  117. * @param array $config Array of config.
  118. */
  119. public function __construct(ComponentRegistry $collection, array $config = array()) {
  120. parent::__construct($collection, $config);
  121. if (!$this->_config['key']) {
  122. $this->config('key', Configure::read('Security.salt'));
  123. }
  124. $controller = $collection->getController();
  125. if ($controller && isset($controller->request)) {
  126. $this->_request = $controller->request;
  127. } else {
  128. $this->_request = Request::createFromGlobals();
  129. }
  130. if (empty($this->_config['path'])) {
  131. $this->config('path', $this->_request->base ?: '/');
  132. }
  133. if ($controller && isset($controller->response)) {
  134. $this->_response = $controller->response;
  135. } else {
  136. $this->_response = new Response();
  137. }
  138. }
  139. /**
  140. * Set the configuration for a specific top level key.
  141. *
  142. * ### Examples:
  143. *
  144. * Set a single config option for a key:
  145. *
  146. * {{{
  147. * $this->Cookie->configKey('User', 'expires', '+3 months');
  148. * }}}
  149. *
  150. * Set multiple options:
  151. *
  152. * {{{
  153. * $this->Cookie->configKey('User', [
  154. * 'expires', '+3 months',
  155. * 'httpOnly' => true,
  156. * ]);
  157. * }}}
  158. *
  159. * @param string $keyname The top level keyname to configure.
  160. * @param null|string|array $option Either the option name to set, or an array of options to set,
  161. * or null to read config options for a given key.
  162. * @param string|null $value Either the value to set, or empty when $option is an array.
  163. * @return void
  164. */
  165. public function configKey($keyname, $option = null, $value = null) {
  166. if ($option === null) {
  167. $default = $this->_config;
  168. $local = isset($this->_keyConfig[$keyname]) ? $this->_keyConfig[$keyname] : [];
  169. return $local + $default;
  170. }
  171. if (!is_array($option)) {
  172. $option = [$option => $value];
  173. }
  174. $this->_keyConfig[$keyname] = $option;
  175. }
  176. /**
  177. * Events supported by this component.
  178. *
  179. * @return array
  180. */
  181. public function implementedEvents() {
  182. return [];
  183. }
  184. /**
  185. * Write a value to the response cookies.
  186. *
  187. * You must use this method before any output is sent to the browser.
  188. * Failure to do so will result in header already sent errors.
  189. *
  190. * @param string|array $key Key for the value
  191. * @param mixed $value Value
  192. * @return void
  193. * @link http://book.cakephp.org/2.0/en/core-libraries/components/cookie.html#CookieComponent::write
  194. */
  195. public function write($key, $value = null) {
  196. if (!is_array($key)) {
  197. $key = array($key => $value);
  198. }
  199. $keys = [];
  200. foreach ($key as $name => $value) {
  201. $this->_load($name);
  202. $this->_values = Hash::insert($this->_values, $name, $value);
  203. $parts = explode('.', $name);
  204. $keys[] = $parts[0];
  205. }
  206. foreach ($keys as $name) {
  207. $this->_write($name, $this->_values[$name]);
  208. }
  209. }
  210. /**
  211. * Read the value of key path from request cookies.
  212. *
  213. * This method will also allow you to read cookies that have been written in this
  214. * request, but not yet sent to the client.
  215. *
  216. * @param string $key Key of the value to be obtained.
  217. * @return string or null, value for specified key
  218. * @link http://book.cakephp.org/2.0/en/core-libraries/components/cookie.html#CookieComponent::read
  219. */
  220. public function read($key = null) {
  221. $this->_load($key);
  222. return Hash::get($this->_values, $key);
  223. }
  224. /**
  225. * Load the cookie data from the request and response objects.
  226. *
  227. * Based on the configuration data, cookies will be decrypted. When cookies
  228. * contain array data, that data will be expanded.
  229. *
  230. * @param string|array $key The key to load.
  231. * @return void
  232. */
  233. protected function _load($key) {
  234. $parts = explode('.', $key);
  235. $first = array_shift($parts);
  236. if (isset($this->_loaded[$first])) {
  237. return;
  238. }
  239. if (!isset($this->_request->cookies[$first])) {
  240. return;
  241. }
  242. $cookie = $this->_request->cookies[$first];
  243. $config = $this->configKey($first);
  244. $this->_loaded[$first] = true;
  245. $this->_values[$first] = $this->_decrypt($cookie, $config['encryption']);
  246. }
  247. /**
  248. * Returns true if given key is set in the cookie.
  249. *
  250. * @param string $key Key to check for
  251. * @return bool True if the key exists
  252. */
  253. public function check($key = null) {
  254. if (empty($key)) {
  255. return false;
  256. }
  257. return $this->read($key) !== null;
  258. }
  259. /**
  260. * Delete a cookie value
  261. *
  262. * You must use this method before any output is sent to the browser.
  263. * Failure to do so will result in header already sent errors.
  264. *
  265. * Deleting a top level key will delete all keys nested within that key.
  266. * For example deleting the `User` key, will also delete `User.email`.
  267. *
  268. * @param string $key Key of the value to be deleted
  269. * @return void
  270. * @link http://book.cakephp.org/2.0/en/core-libraries/components/cookie.html#CookieComponent::delete
  271. */
  272. public function delete($key) {
  273. $this->_load($key);
  274. $this->_values = Hash::remove($this->_values, $key);
  275. $parts = explode('.', $key);
  276. $top = $parts[0];
  277. if (isset($this->_values[$top])) {
  278. $this->_write($top, $this->_values[$top]);
  279. } else {
  280. $this->_delete($top);
  281. }
  282. }
  283. /**
  284. * Set cookie
  285. *
  286. * @param string $name Name for cookie
  287. * @param string $value Value for cookie
  288. * @return void
  289. */
  290. protected function _write($name, $value) {
  291. $config = $this->configKey($name);
  292. $expires = new Time($config['expires']);
  293. $this->_response->cookie(array(
  294. 'name' => $name,
  295. 'value' => $this->_encrypt($value, $config['encryption']),
  296. 'expire' => $expires->format('U'),
  297. 'path' => $config['path'],
  298. 'domain' => $config['domain'],
  299. 'secure' => $config['secure'],
  300. 'httpOnly' => $config['httpOnly']
  301. ));
  302. }
  303. /**
  304. * Sets a cookie expire time to remove cookie value.
  305. *
  306. * This is only done once all values in a cookie key have been
  307. * removed with delete.
  308. *
  309. * @param string $name Name of cookie
  310. * @return void
  311. */
  312. protected function _delete($name) {
  313. $config = $this->configKey($name);
  314. $expires = new Time('now');
  315. $this->_response->cookie(array(
  316. 'name' => $name,
  317. 'value' => '',
  318. 'expire' => $expires->format('U') - 42000,
  319. 'path' => $config['path'],
  320. 'domain' => $config['domain'],
  321. 'secure' => $config['secure'],
  322. 'httpOnly' => $config['httpOnly']
  323. ));
  324. }
  325. /**
  326. * Encrypts $value using public $type method in Security class
  327. *
  328. * @param string $value Value to encrypt
  329. * @param string|bool $encrypt Encryption mode to use. False
  330. * disabled encryption.
  331. * @return string Encoded values
  332. */
  333. protected function _encrypt($value, $encrypt) {
  334. if (is_array($value)) {
  335. $value = $this->_implode($value);
  336. }
  337. if (!$encrypt) {
  338. return $value;
  339. }
  340. $this->_checkCipher($encrypt);
  341. $prefix = "Q2FrZQ==.";
  342. if ($encrypt === 'rijndael') {
  343. $cipher = Security::rijndael($value, $this->_config['key'], 'encrypt');
  344. }
  345. if ($encrypt === 'aes') {
  346. $cipher = Security::encrypt($value, $this->_config['key']);
  347. }
  348. return $prefix . base64_encode($cipher);
  349. }
  350. /**
  351. * Helper method for validating encryption cipher names.
  352. *
  353. * @param string $encrypt The cipher name.
  354. * @return void
  355. * @throws \RuntimeException When an invalid cipher is provided.
  356. */
  357. protected function _checkCipher($encrypt) {
  358. if (!in_array($encrypt, $this->_validCiphers)) {
  359. $msg = sprintf(
  360. 'Invalid encryption cipher. Must be one of %s.',
  361. implode(', ', $this->_validCiphers)
  362. );
  363. throw new \RuntimeException($msg);
  364. }
  365. }
  366. /**
  367. * Decrypts $value using public $type method in Security class
  368. *
  369. * @param array $values Values to decrypt
  370. * @param string|bool $mode Encryption mode
  371. * @return string decrypted string
  372. */
  373. protected function _decrypt($values, $mode) {
  374. if (is_string($values)) {
  375. return $this->_decode($values, $mode);
  376. }
  377. $decrypted = array();
  378. foreach ($values as $name => $value) {
  379. $decrypted[$name] = $this->_decode($value, $mode);
  380. }
  381. return $decrypted;
  382. }
  383. /**
  384. * Decodes and decrypts a single value.
  385. *
  386. * @param string $value The value to decode & decrypt.
  387. * @param string|false $encrypt The encryption cipher to use.
  388. * @return string Decoded value.
  389. */
  390. protected function _decode($value, $encrypt) {
  391. if (!$encrypt) {
  392. return $this->_explode($value);
  393. }
  394. $this->_checkCipher($encrypt);
  395. $prefix = 'Q2FrZQ==.';
  396. $value = base64_decode(substr($value, strlen($prefix)));
  397. if ($encrypt === 'rijndael') {
  398. $value = Security::rijndael($value, $this->_config['key'], 'decrypt');
  399. }
  400. if ($encrypt === 'aes') {
  401. $value = Security::decrypt($value, $this->_config['key']);
  402. }
  403. return $this->_explode($value);
  404. }
  405. /**
  406. * Implode method to keep keys are multidimensional arrays
  407. *
  408. * @param array $array Map of key and values
  409. * @return string A json encoded string.
  410. */
  411. protected function _implode(array $array) {
  412. return json_encode($array);
  413. }
  414. /**
  415. * Explode method to return array from string set in CookieComponent::_implode()
  416. * Maintains reading backwards compatibility with 1.x CookieComponent::_implode().
  417. *
  418. * @param string $string A string containing JSON encoded data, or a bare string.
  419. * @return array Map of key and values
  420. */
  421. protected function _explode($string) {
  422. $first = substr($string, 0, 1);
  423. if ($first === '{' || $first === '[') {
  424. $ret = json_decode($string, true);
  425. return ($ret !== null) ? $ret : $string;
  426. }
  427. $array = array();
  428. foreach (explode(',', $string) as $pair) {
  429. $key = explode('|', $pair);
  430. if (!isset($key[1])) {
  431. return $key[0];
  432. }
  433. $array[$key[0]] = $key[1];
  434. }
  435. return $array;
  436. }
  437. }