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