1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://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. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 1.2.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\Controller\Component;
16:
17: use Cake\Controller\Component;
18: use Cake\Http\Cookie\Cookie;
19: use Cake\Http\ServerRequestFactory;
20: use Cake\I18n\Time;
21: use Cake\Utility\CookieCryptTrait;
22: use Cake\Utility\Hash;
23: use Cake\Utility\Security;
24:
25: /**
26: * Cookie Component.
27: *
28: * Provides enhanced cookie handling features for use in the controller layer.
29: * In addition to the basic features offered be Cake\Http\Response, this class lets you:
30: *
31: * - Create and read encrypted cookies.
32: * - Store non-scalar data.
33: * - Use hash compatible syntax to read/write/delete values.
34: *
35: * @link https://book.cakephp.org/3.0/en/controllers/components/cookie.html
36: * @deprecated 3.5.0 Use Cake\Http\Middleware\EncryptedCookieMiddleware and Cake\Http\Cookie\Cookie methods instead.
37: */
38: class CookieComponent extends Component
39: {
40: use CookieCryptTrait;
41:
42: /**
43: * Default config
44: *
45: * - `expires` - How long the cookies should last for. Defaults to 1 month.
46: * - `path` - The path on the server in which the cookie will be available on.
47: * If path is set to '/foo/', the cookie will only be available within the
48: * /foo/ directory and all sub-directories such as /foo/bar/ of domain.
49: * The default value is base path of app. For e.g. if your app is running
50: * under a subfolder "cakeapp" of document root the path would be "/cakeapp/"
51: * else it would be "/".
52: * - `domain` - The domain that the cookie is available. To make the cookie
53: * available on all subdomains of example.com set domain to '.example.com'.
54: * - `secure` - Indicates that the cookie should only be transmitted over a
55: * secure HTTPS connection. When set to true, the cookie will only be set if
56: * a secure connection exists.
57: * - `key` - Encryption key used when encrypted cookies are enabled. Defaults to Security.salt.
58: * - `httpOnly` - Set to true to make HTTP only cookies. Cookies that are HTTP only
59: * are not accessible in JavaScript. Default false.
60: * - `encryption` - Type of encryption to use. Defaults to 'aes'.
61: *
62: * @var array
63: */
64: protected $_defaultConfig = [
65: 'path' => null,
66: 'domain' => '',
67: 'secure' => false,
68: 'key' => null,
69: 'httpOnly' => false,
70: 'encryption' => 'aes',
71: 'expires' => '+1 month',
72: ];
73:
74: /**
75: * Config specific to a given top level key name.
76: *
77: * The values in this array are merged with the general config
78: * to generate the configuration for a given top level cookie name.
79: *
80: * @var array
81: */
82: protected $_keyConfig = [];
83:
84: /**
85: * Values stored in the cookie.
86: *
87: * Accessed in the controller using $this->Cookie->read('Name.key');
88: *
89: * @var array
90: */
91: protected $_values = [];
92:
93: /**
94: * A map of keys that have been loaded.
95: *
96: * Since CookieComponent lazily reads cookie data,
97: * we need to track which cookies have been read to account for
98: * read, delete, read patterns.
99: *
100: * @var array
101: */
102: protected $_loaded = [];
103:
104: /**
105: * A reference to the Controller's Cake\Http\Response object.
106: * Currently unused.
107: *
108: * @var \Cake\Http\Response|null
109: * @deprecated 3.4.0 Will be removed in 4.0.0
110: */
111: protected $_response;
112:
113: /**
114: * Initialize config data and properties.
115: *
116: * @param array $config The config data.
117: * @return void
118: */
119: public function initialize(array $config)
120: {
121: if (!$this->_config['key']) {
122: $this->setConfig('key', Security::getSalt());
123: }
124:
125: $controller = $this->_registry->getController();
126:
127: if ($controller === null) {
128: $this->request = ServerRequestFactory::fromGlobals();
129: }
130:
131: if (empty($this->_config['path'])) {
132: $this->setConfig('path', $this->getController()->getRequest()->getAttribute('webroot'));
133: }
134: }
135:
136: /**
137: * Set the configuration for a specific top level key.
138: *
139: * ### Examples:
140: *
141: * Set a single config option for a key:
142: *
143: * ```
144: * $this->Cookie->configKey('User', 'expires', '+3 months');
145: * ```
146: *
147: * Set multiple options:
148: *
149: * ```
150: * $this->Cookie->configKey('User', [
151: * 'expires', '+3 months',
152: * 'httpOnly' => true,
153: * ]);
154: * ```
155: *
156: * @param string $keyname The top level keyname to configure.
157: * @param null|string|array $option Either the option name to set, or an array of options to set,
158: * or null to read config options for a given key.
159: * @param string|null $value Either the value to set, or empty when $option is an array.
160: * @return array|null
161: */
162: public function configKey($keyname, $option = null, $value = null)
163: {
164: if ($option === null) {
165: $default = $this->_config;
166: $local = isset($this->_keyConfig[$keyname]) ? $this->_keyConfig[$keyname] : [];
167:
168: return $local + $default;
169: }
170: if (!is_array($option)) {
171: $option = [$option => $value];
172: }
173: $this->_keyConfig[$keyname] = $option;
174:
175: return null;
176: }
177:
178: /**
179: * Events supported by this component.
180: *
181: * @return array
182: */
183: public function implementedEvents()
184: {
185: return [];
186: }
187:
188: /**
189: * Write a value to the response cookies.
190: *
191: * You must use this method before any output is sent to the browser.
192: * Failure to do so will result in header already sent errors.
193: *
194: * @param string|array $key Key for the value
195: * @param mixed $value Value
196: * @return void
197: */
198: public function write($key, $value = null)
199: {
200: if (!is_array($key)) {
201: $key = [$key => $value];
202: }
203:
204: $keys = [];
205: foreach ($key as $name => $value) {
206: $this->_load($name);
207:
208: $this->_values = Hash::insert($this->_values, $name, $value);
209: $parts = explode('.', $name);
210: $keys[] = $parts[0];
211: }
212:
213: foreach ($keys as $name) {
214: $this->_write($name, $this->_values[$name]);
215: }
216: }
217:
218: /**
219: * Read the value of key path from request cookies.
220: *
221: * This method will also allow you to read cookies that have been written in this
222: * request, but not yet sent to the client.
223: *
224: * @param string|null $key Key of the value to be obtained.
225: * @return string or null, value for specified key
226: */
227: public function read($key = null)
228: {
229: $this->_load($key);
230:
231: return Hash::get($this->_values, $key);
232: }
233:
234: /**
235: * Load the cookie data from the request and response objects.
236: *
237: * Based on the configuration data, cookies will be decrypted. When cookies
238: * contain array data, that data will be expanded.
239: *
240: * @param string|array $key The key to load.
241: * @return void
242: */
243: protected function _load($key)
244: {
245: $parts = explode('.', $key);
246: $first = array_shift($parts);
247: if (isset($this->_loaded[$first])) {
248: return;
249: }
250: $cookie = $this->getController()->getRequest()->getCookie($first);
251: if ($cookie === null) {
252: return;
253: }
254: $config = $this->configKey($first);
255: $this->_loaded[$first] = true;
256: $this->_values[$first] = $this->_decrypt($cookie, $config['encryption'], $config['key']);
257: }
258:
259: /**
260: * Returns true if given key is set in the cookie.
261: *
262: * @param string|null $key Key to check for
263: * @return bool True if the key exists
264: */
265: public function check($key = null)
266: {
267: if (empty($key)) {
268: return false;
269: }
270:
271: return $this->read($key) !== null;
272: }
273:
274: /**
275: * Delete a cookie value
276: *
277: * You must use this method before any output is sent to the browser.
278: * Failure to do so will result in header already sent errors.
279: *
280: * Deleting a top level key will delete all keys nested within that key.
281: * For example deleting the `User` key, will also delete `User.email`.
282: *
283: * @param string $key Key of the value to be deleted
284: * @return void
285: */
286: public function delete($key)
287: {
288: $this->_load($key);
289:
290: $this->_values = Hash::remove($this->_values, $key);
291: $parts = explode('.', $key);
292: $top = $parts[0];
293:
294: if (isset($this->_values[$top])) {
295: $this->_write($top, $this->_values[$top]);
296: } else {
297: $this->_delete($top);
298: }
299: }
300:
301: /**
302: * Set cookie
303: *
304: * @param string $name Name for cookie
305: * @param string $value Value for cookie
306: * @return void
307: */
308: protected function _write($name, $value)
309: {
310: $config = $this->configKey($name);
311: $expires = new Time($config['expires']);
312:
313: $controller = $this->getController();
314:
315: $cookie = new Cookie(
316: $name,
317: $this->_encrypt($value, $config['encryption'], $config['key']),
318: $expires,
319: $config['path'],
320: $config['domain'],
321: (bool)$config['secure'],
322: (bool)$config['httpOnly']
323: );
324:
325: $controller->response = $controller->response->withCookie($cookie);
326: }
327:
328: /**
329: * Sets a cookie expire time to remove cookie value.
330: *
331: * This is only done once all values in a cookie key have been
332: * removed with delete.
333: *
334: * @param string $name Name of cookie
335: * @return void
336: */
337: protected function _delete($name)
338: {
339: $config = $this->configKey($name);
340: $expires = new Time('now');
341: $controller = $this->getController();
342:
343: $cookie = new Cookie(
344: $name,
345: '',
346: $expires,
347: $config['path'],
348: $config['domain'],
349: (bool)$config['secure'],
350: (bool)$config['httpOnly']
351: );
352:
353: $controller->response = $controller->response->withExpiredCookie($cookie);
354: }
355:
356: /**
357: * Returns the encryption key to be used.
358: *
359: * @return string
360: */
361: protected function _getCookieEncryptionKey()
362: {
363: return $this->_config['key'];
364: }
365: }
366: