1: <?php
2: /**
3: * Cookie Component
4: *
5: * PHP versions 4 and 5
6: *
7: * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
8: * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
9: *
10: * Licensed under The MIT License
11: * Redistributions of files must retain the above copyright notice.
12: *
13: * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
14: * @link http://cakephp.org CakePHP(tm) Project
15: * @package cake
16: * @subpackage cake.cake.libs.controller.components
17: * @since CakePHP(tm) v 1.2.0.4213
18: * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
19: */
20:
21: /**
22: * Load Security class
23: */
24: App::import('Core', 'Security');
25:
26: /**
27: * Cookie Component.
28: *
29: * Cookie handling for the controller.
30: *
31: * @package cake
32: * @subpackage cake.cake.libs.controller.components
33: * @link http://book.cakephp.org/1.3/en/The-Manual/Core-Components/Cookies.html
34: *
35: */
36: class CookieComponent extends Object {
37:
38: /**
39: * The name of the cookie.
40: *
41: * Overridden with the controller beforeFilter();
42: * $this->Cookie->name = 'CookieName';
43: *
44: * @var string
45: * @access public
46: */
47: var $name = 'CakeCookie';
48:
49: /**
50: * The time a cookie will remain valid.
51: *
52: * Can be either integer Unix timestamp or a date string.
53: *
54: * Overridden with the controller beforeFilter();
55: * $this->Cookie->time = '5 Days';
56: *
57: * @var mixed
58: * @access public
59: */
60: var $time = null;
61:
62: /**
63: * Cookie path.
64: *
65: * Overridden with the controller beforeFilter();
66: * $this->Cookie->path = '/';
67: *
68: * The path on the server in which the cookie will be available on.
69: * If var $cookiePath is set to '/foo/', the cookie will only be available
70: * within the /foo/ directory and all sub-directories such as /foo/bar/ of domain.
71: * The default value is the entire domain.
72: *
73: * @var string
74: * @access public
75: */
76: var $path = '/';
77:
78: /**
79: * Domain path.
80: *
81: * The domain that the cookie is available.
82: *
83: * Overridden with the controller beforeFilter();
84: * $this->Cookie->domain = '.example.com';
85: *
86: * To make the cookie available on all subdomains of example.com.
87: * Set $this->Cookie->domain = '.example.com'; in your controller beforeFilter
88: *
89: * @var string
90: * @access public
91: */
92: var $domain = '';
93:
94: /**
95: * Secure HTTPS only cookie.
96: *
97: * Overridden with the controller beforeFilter();
98: * $this->Cookie->secure = true;
99: *
100: * Indicates that the cookie should only be transmitted over a secure HTTPS connection.
101: * When set to true, the cookie will only be set if a secure connection exists.
102: *
103: * @var boolean
104: * @access public
105: */
106: var $secure = false;
107:
108: /**
109: * Encryption key.
110: *
111: * Overridden with the controller beforeFilter();
112: * $this->Cookie->key = 'SomeRandomString';
113: *
114: * @var string
115: * @access protected
116: */
117: var $key = null;
118:
119: /**
120: * Values stored in the cookie.
121: *
122: * Accessed in the controller using $this->Cookie->read('Name.key');
123: *
124: * @see CookieComponent::read();
125: * @var string
126: * @access private
127: */
128: var $__values = array();
129:
130: /**
131: * Type of encryption to use.
132: *
133: * Currently only one method is available
134: * Defaults to Security::cipher();
135: *
136: * @var string
137: * @access private
138: * @todo add additional encryption methods
139: */
140: var $__type = 'cipher';
141:
142: /**
143: * Used to reset cookie time if $expire is passed to CookieComponent::write()
144: *
145: * @var string
146: * @access private
147: */
148: var $__reset = null;
149:
150: /**
151: * Expire time of the cookie
152: *
153: * This is controlled by CookieComponent::time;
154: *
155: * @var string
156: * @access private
157: */
158: var $__expires = 0;
159:
160: /**
161: * Main execution method.
162: *
163: * @param object $controller A reference to the instantiating controller object
164: * @access public
165: */
166: function initialize(&$controller, $settings) {
167: $this->key = Configure::read('Security.salt');
168: $this->_set($settings);
169: if (isset($this->time)) {
170: $this->__expire($this->time);
171: }
172: }
173:
174: /**
175: * Start CookieComponent for use in the controller
176: *
177: * @access public
178: */
179: function startup() {
180: $this->__expire($this->time);
181:
182: if (isset($_COOKIE[$this->name])) {
183: $this->__values = $this->__decrypt($_COOKIE[$this->name]);
184: }
185: }
186:
187: /**
188: * Write a value to the $_COOKIE[$key];
189: *
190: * Optional [Name.], required key, optional $value, optional $encrypt, optional $expires
191: * $this->Cookie->write('[Name.]key, $value);
192: *
193: * By default all values are encrypted.
194: * You must pass $encrypt false to store values in clear test
195: *
196: * You must use this method before any output is sent to the browser.
197: * Failure to do so will result in header already sent errors.
198: *
199: * @param mixed $key Key for the value
200: * @param mixed $value Value
201: * @param boolean $encrypt Set to true to encrypt value, false otherwise
202: * @param string $expires Can be either Unix timestamp, or date string
203: * @access public
204: */
205: function write($key, $value = null, $encrypt = true, $expires = null) {
206: if (is_null($encrypt)) {
207: $encrypt = true;
208: }
209: $this->__encrypted = $encrypt;
210: $this->__expire($expires);
211:
212: if (!is_array($key)) {
213: $key = array($key => $value);
214: }
215:
216: foreach ($key as $name => $value) {
217: if (strpos($name, '.') === false) {
218: $this->__values[$name] = $value;
219: $this->__write("[$name]", $value);
220:
221: } else {
222: $names = explode('.', $name, 2);
223: if (!isset($this->__values[$names[0]])) {
224: $this->__values[$names[0]] = array();
225: }
226: $this->__values[$names[0]] = Set::insert($this->__values[$names[0]], $names[1], $value);
227: $this->__write('[' . implode('][', $names) . ']', $value);
228: }
229: }
230: $this->__encrypted = true;
231: }
232:
233: /**
234: * Read the value of the $_COOKIE[$key];
235: *
236: * Optional [Name.], required key
237: * $this->Cookie->read(Name.key);
238: *
239: * @param mixed $key Key of the value to be obtained. If none specified, obtain map key => values
240: * @return string or null, value for specified key
241: * @access public
242: */
243: function read($key = null) {
244: if (empty($this->__values) && isset($_COOKIE[$this->name])) {
245: $this->__values = $this->__decrypt($_COOKIE[$this->name]);
246: }
247:
248: if (is_null($key)) {
249: return $this->__values;
250: }
251:
252: if (strpos($key, '.') !== false) {
253: $names = explode('.', $key, 2);
254: $key = $names[0];
255: }
256: if (!isset($this->__values[$key])) {
257: return null;
258: }
259:
260: if (!empty($names[1])) {
261: return Set::extract($this->__values[$key], $names[1]);
262: }
263: return $this->__values[$key];
264: }
265:
266: /**
267: * Delete a cookie value
268: *
269: * Optional [Name.], required key
270: * $this->Cookie->read('Name.key);
271: *
272: * You must use this method before any output is sent to the browser.
273: * Failure to do so will result in header already sent errors.
274: *
275: * @param string $key Key of the value to be deleted
276: * @return void
277: * @access public
278: */
279: function delete($key) {
280: if (empty($this->__values)) {
281: $this->read();
282: }
283: if (strpos($key, '.') === false) {
284: if (isset($this->__values[$key]) && is_array($this->__values[$key])) {
285: foreach ($this->__values[$key] as $idx => $val) {
286: $this->__delete("[$key][$idx]");
287: }
288: }
289: $this->__delete("[$key]");
290: unset($this->__values[$key]);
291: return;
292: }
293: $names = explode('.', $key, 2);
294: if (isset($this->__values[$names[0]])) {
295: $this->__values[$names[0]] = Set::remove($this->__values[$names[0]], $names[1]);
296: }
297: $this->__delete('[' . implode('][', $names) . ']');
298: }
299:
300: /**
301: * Destroy current cookie
302: *
303: * You must use this method before any output is sent to the browser.
304: * Failure to do so will result in header already sent errors.
305: *
306: * @return void
307: * @access public
308: */
309: function destroy() {
310: if (isset($_COOKIE[$this->name])) {
311: $this->__values = $this->__decrypt($_COOKIE[$this->name]);
312: }
313:
314: foreach ($this->__values as $name => $value) {
315: if (is_array($value)) {
316: foreach ($value as $key => $val) {
317: unset($this->__values[$name][$key]);
318: $this->__delete("[$name][$key]");
319: }
320: }
321: unset($this->__values[$name]);
322: $this->__delete("[$name]");
323: }
324: }
325:
326: /**
327: * Will allow overriding default encryption method.
328: *
329: * @param string $type Encryption method
330: * @access public
331: * @todo NOT IMPLEMENTED
332: */
333: function type($type = 'cipher') {
334: $this->__type = 'cipher';
335: }
336:
337: /**
338: * Set the expire time for a session variable.
339: *
340: * Creates a new expire time for a session variable.
341: * $expire can be either integer Unix timestamp or a date string.
342: *
343: * Used by write()
344: * CookieComponent::write(string, string, boolean, 8400);
345: * CookieComponent::write(string, string, boolean, '5 Days');
346: *
347: * @param mixed $expires Can be either Unix timestamp, or date string
348: * @return int Unix timestamp
349: * @access private
350: */
351: function __expire($expires = null) {
352: $now = time();
353: if (is_null($expires)) {
354: return $this->__expires;
355: }
356: $this->__reset = $this->__expires;
357:
358: if ($expires == 0) {
359: return $this->__expires = 0;
360: }
361:
362: if (is_integer($expires) || is_numeric($expires)) {
363: return $this->__expires = $now + intval($expires);
364: }
365: return $this->__expires = strtotime($expires, $now);
366: }
367:
368: /**
369: * Set cookie
370: *
371: * @param string $name Name for cookie
372: * @param string $value Value for cookie
373: * @access private
374: */
375: function __write($name, $value) {
376: setcookie($this->name . $name, $this->__encrypt($value), $this->__expires, $this->path, $this->domain, $this->secure);
377:
378: if (!is_null($this->__reset)) {
379: $this->__expires = $this->__reset;
380: $this->__reset = null;
381: }
382: }
383:
384: /**
385: * Sets a cookie expire time to remove cookie value
386: *
387: * @param string $name Name of cookie
388: * @access private
389: */
390: function __delete($name) {
391: setcookie($this->name . $name, '', time() - 42000, $this->path, $this->domain, $this->secure);
392: }
393:
394: /**
395: * Encrypts $value using var $type method in Security class
396: *
397: * @param string $value Value to encrypt
398: * @return string encrypted string
399: * @access private
400: */
401: function __encrypt($value) {
402: if (is_array($value)) {
403: $value = $this->__implode($value);
404: }
405:
406: if ($this->__encrypted === true) {
407: $type = $this->__type;
408: $value = "Q2FrZQ==." .base64_encode(Security::$type($value, $this->key));
409: }
410: return $value;
411: }
412:
413: /**
414: * Decrypts $value using var $type method in Security class
415: *
416: * @param array $values Values to decrypt
417: * @return string decrypted string
418: * @access private
419: */
420: function __decrypt($values) {
421: $decrypted = array();
422: $type = $this->__type;
423:
424: foreach ((array)$values as $name => $value) {
425: if (is_array($value)) {
426: foreach ($value as $key => $val) {
427: $pos = strpos($val, 'Q2FrZQ==.');
428:
429: if ($pos !== false) {
430: $val = substr($val, 8);
431: $decrypted[$name][$key] = $this->__explode(Security::$type(base64_decode($val), $this->key));
432: } else {
433: $decrypted[$name][$key] = $this->__explode($val);
434: }
435: }
436: } else {
437: $pos = strpos($value, 'Q2FrZQ==.');
438:
439: if ($pos !== false) {
440: $value = substr($value, 8);
441: $decrypted[$name] = $this->__explode(Security::$type(base64_decode($value), $this->key));
442: } else {
443: $decrypted[$name] = $this->__explode($value);
444: }
445: }
446: }
447: return $decrypted;
448: }
449:
450: /**
451: * Implode method to keep keys are multidimensional arrays
452: *
453: * @param array $array Map of key and values
454: * @return string String in the form key1|value1,key2|value2
455: * @access private
456: */
457: function __implode($array) {
458: $string = '';
459: foreach ($array as $key => $value) {
460: $string .= ',' . $key . '|' . $value;
461: }
462: return substr($string, 1);
463: }
464:
465: /**
466: * Explode method to return array from string set in CookieComponent::__implode()
467: *
468: * @param string $string String in the form key1|value1,key2|value2
469: * @return mixed If array, map of key and values. If string, value.
470: * @access private
471: */
472: function __explode($string) {
473: $first = substr($string, 0, 1);
474: if ($first !== false && ($first === '{' || $first === '[') && function_exists('json_decode')) {
475: $ret = json_decode($string, true);
476: return ($ret != null) ? $ret : $string;
477: }
478: $array = array();
479: foreach (explode(',', $string) as $pair) {
480: $key = explode('|', $pair);
481: if (!isset($key[1])) {
482: return $key[0];
483: }
484: $array[$key[0]] = $key[1];
485: }
486: return $array;
487: }
488: }
489: