1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13:
14: namespace Cake\Http\Cookie;
15:
16: use Cake\Chronos\Chronos;
17: use Cake\Utility\Hash;
18: use DateTimeImmutable;
19: use DateTimeZone;
20: use InvalidArgumentException;
21:
22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47:
48: class Cookie implements CookieInterface
49: {
50: 51: 52: 53: 54:
55: protected $name = '';
56:
57: 58: 59: 60: 61:
62: protected $value = '';
63:
64: 65: 66: 67: 68:
69: protected $isExpanded = false;
70:
71: 72: 73: 74: 75:
76: protected $expiresAt;
77:
78: 79: 80: 81: 82:
83: protected $path = '/';
84:
85: 86: 87: 88: 89:
90: protected $domain = '';
91:
92: 93: 94: 95: 96:
97: protected $secure = false;
98:
99: 100: 101: 102: 103:
104: protected $httpOnly = false;
105:
106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121:
122: public function __construct(
123: $name,
124: $value = '',
125: $expiresAt = null,
126: $path = '/',
127: $domain = '',
128: $secure = false,
129: $httpOnly = false
130: ) {
131: $this->validateName($name);
132: $this->name = $name;
133:
134: $this->_setValue($value);
135:
136: $this->validateString($domain);
137: $this->domain = $domain;
138:
139: $this->validateBool($httpOnly);
140: $this->httpOnly = $httpOnly;
141:
142: $this->validateString($path);
143: $this->path = $path;
144:
145: $this->validateBool($secure);
146: $this->secure = $secure;
147: if ($expiresAt) {
148: $expiresAt = $expiresAt->setTimezone(new DateTimeZone('GMT'));
149: }
150: $this->expiresAt = $expiresAt;
151: }
152:
153: 154: 155: 156: 157:
158: public function toHeaderValue()
159: {
160: $value = $this->value;
161: if ($this->isExpanded) {
162: $value = $this->_flatten($this->value);
163: }
164: $headerValue[] = sprintf('%s=%s', $this->name, rawurlencode($value));
165:
166: if ($this->expiresAt) {
167: $headerValue[] = sprintf('expires=%s', $this->getFormattedExpires());
168: }
169: if ($this->path !== '') {
170: $headerValue[] = sprintf('path=%s', $this->path);
171: }
172: if ($this->domain !== '') {
173: $headerValue[] = sprintf('domain=%s', $this->domain);
174: }
175: if ($this->secure) {
176: $headerValue[] = 'secure';
177: }
178: if ($this->httpOnly) {
179: $headerValue[] = 'httponly';
180: }
181:
182: return implode('; ', $headerValue);
183: }
184:
185: 186: 187:
188: public function withName($name)
189: {
190: $this->validateName($name);
191: $new = clone $this;
192: $new->name = $name;
193:
194: return $new;
195: }
196:
197: 198: 199:
200: public function getId()
201: {
202: return "{$this->name};{$this->domain};{$this->path}";
203: }
204:
205: 206: 207:
208: public function getName()
209: {
210: return $this->name;
211: }
212:
213: 214: 215: 216: 217: 218: 219: 220:
221: protected function validateName($name)
222: {
223: if (preg_match("/[=,;\t\r\n\013\014]/", $name)) {
224: throw new InvalidArgumentException(
225: sprintf('The cookie name `%s` contains invalid characters.', $name)
226: );
227: }
228:
229: if (empty($name)) {
230: throw new InvalidArgumentException('The cookie name cannot be empty.');
231: }
232: }
233:
234: 235: 236:
237: public function getValue()
238: {
239: return $this->value;
240: }
241:
242: 243: 244:
245: public function getStringValue()
246: {
247: if ($this->isExpanded) {
248: return $this->_flatten($this->value);
249: }
250:
251: return $this->value;
252: }
253:
254: 255: 256:
257: public function withValue($value)
258: {
259: $new = clone $this;
260: $new->_setValue($value);
261:
262: return $new;
263: }
264:
265: 266: 267: 268: 269: 270:
271: protected function _setValue($value)
272: {
273: $this->isExpanded = is_array($value);
274: $this->value = $value;
275: }
276:
277: 278: 279:
280: public function withPath($path)
281: {
282: $this->validateString($path);
283: $new = clone $this;
284: $new->path = $path;
285:
286: return $new;
287: }
288:
289: 290: 291:
292: public function getPath()
293: {
294: return $this->path;
295: }
296:
297: 298: 299:
300: public function withDomain($domain)
301: {
302: $this->validateString($domain);
303: $new = clone $this;
304: $new->domain = $domain;
305:
306: return $new;
307: }
308:
309: 310: 311:
312: public function getDomain()
313: {
314: return $this->domain;
315: }
316:
317: 318: 319: 320: 321: 322: 323:
324: protected function validateString($value)
325: {
326: if (!is_string($value)) {
327: throw new InvalidArgumentException(sprintf(
328: 'The provided arg must be of type `string` but `%s` given',
329: gettype($value)
330: ));
331: }
332: }
333:
334: 335: 336:
337: public function isSecure()
338: {
339: return $this->secure;
340: }
341:
342: 343: 344:
345: public function withSecure($secure)
346: {
347: $this->validateBool($secure);
348: $new = clone $this;
349: $new->secure = $secure;
350:
351: return $new;
352: }
353:
354: 355: 356:
357: public function withHttpOnly($httpOnly)
358: {
359: $this->validateBool($httpOnly);
360: $new = clone $this;
361: $new->httpOnly = $httpOnly;
362:
363: return $new;
364: }
365:
366: 367: 368: 369: 370: 371: 372:
373: protected function validateBool($value)
374: {
375: if (!is_bool($value)) {
376: throw new InvalidArgumentException(sprintf(
377: 'The provided arg must be of type `bool` but `%s` given',
378: gettype($value)
379: ));
380: }
381: }
382:
383: 384: 385:
386: public function isHttpOnly()
387: {
388: return $this->httpOnly;
389: }
390:
391: 392: 393:
394: public function withExpiry($dateTime)
395: {
396: $new = clone $this;
397: $new->expiresAt = $dateTime->setTimezone(new DateTimeZone('GMT'));
398:
399: return $new;
400: }
401:
402: 403: 404:
405: public function getExpiry()
406: {
407: return $this->expiresAt;
408: }
409:
410: 411: 412:
413: public function getExpiresTimestamp()
414: {
415: if (!$this->expiresAt) {
416: return null;
417: }
418:
419: return $this->expiresAt->format('U');
420: }
421:
422: 423: 424:
425: public function getFormattedExpires()
426: {
427: if (!$this->expiresAt) {
428: return '';
429: }
430:
431: return $this->expiresAt->format(static::EXPIRES_FORMAT);
432: }
433:
434: 435: 436:
437: public function isExpired($time = null)
438: {
439: $time = $time ?: new DateTimeImmutable('now', new DateTimeZone('UTC'));
440: if (!$this->expiresAt) {
441: return false;
442: }
443:
444: return $this->expiresAt < $time;
445: }
446:
447: 448: 449:
450: public function withNeverExpire()
451: {
452: $new = clone $this;
453: $new->expiresAt = Chronos::createFromDate(2038, 1, 1);
454:
455: return $new;
456: }
457:
458: 459: 460:
461: public function withExpired()
462: {
463: $new = clone $this;
464: $new->expiresAt = Chronos::createFromTimestamp(1);
465:
466: return $new;
467: }
468:
469: 470: 471: 472: 473: 474: 475: 476: 477:
478: public function check($path)
479: {
480: if ($this->isExpanded === false) {
481: $this->value = $this->_expand($this->value);
482: }
483:
484: return Hash::check($this->value, $path);
485: }
486:
487: 488: 489: 490: 491: 492: 493:
494: public function withAddedValue($path, $value)
495: {
496: $new = clone $this;
497: if ($new->isExpanded === false) {
498: $new->value = $new->_expand($new->value);
499: }
500: $new->value = Hash::insert($new->value, $path, $value);
501:
502: return $new;
503: }
504:
505: 506: 507: 508: 509: 510:
511: public function withoutAddedValue($path)
512: {
513: $new = clone $this;
514: if ($new->isExpanded === false) {
515: $new->value = $new->_expand($new->value);
516: }
517: $new->value = Hash::remove($new->value, $path);
518:
519: return $new;
520: }
521:
522: 523: 524: 525: 526: 527: 528: 529: 530:
531: public function read($path = null)
532: {
533: if ($this->isExpanded === false) {
534: $this->value = $this->_expand($this->value);
535: }
536:
537: if ($path === null) {
538: return $this->value;
539: }
540:
541: return Hash::get($this->value, $path);
542: }
543:
544: 545: 546: 547: 548:
549: public function isExpanded()
550: {
551: return $this->isExpanded;
552: }
553:
554: 555: 556: 557: 558: 559:
560: protected function _flatten(array $array)
561: {
562: return json_encode($array);
563: }
564:
565: 566: 567: 568: 569: 570: 571:
572: protected function _expand($string)
573: {
574: $this->isExpanded = true;
575: $first = substr($string, 0, 1);
576: if ($first === '{' || $first === '[') {
577: $ret = json_decode($string, true);
578:
579: return ($ret !== null) ? $ret : $string;
580: }
581:
582: $array = [];
583: foreach (explode(',', $string) as $pair) {
584: $key = explode('|', $pair);
585: if (!isset($key[1])) {
586: return $key[0];
587: }
588: $array[$key[0]] = $key[1];
589: }
590:
591: return $array;
592: }
593: }
594: