1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13:
14: namespace Cake\Http\Cookie;
15:
16: use ArrayIterator;
17: use Countable;
18: use DateTimeImmutable;
19: use DateTimeZone;
20: use Exception;
21: use InvalidArgumentException;
22: use IteratorAggregate;
23: use Psr\Http\Message\RequestInterface;
24: use Psr\Http\Message\ResponseInterface;
25: use Psr\Http\Message\ServerRequestInterface;
26:
27: 28: 29: 30: 31: 32:
33: class CookieCollection implements IteratorAggregate, Countable
34: {
35: 36: 37: 38: 39:
40: protected $cookies = [];
41:
42: 43: 44: 45: 46:
47: public function __construct(array $cookies = [])
48: {
49: $this->checkCookies($cookies);
50: foreach ($cookies as $cookie) {
51: $this->cookies[$cookie->getId()] = $cookie;
52: }
53: }
54:
55: 56: 57: 58: 59: 60:
61: public static function createFromHeader(array $header)
62: {
63: $cookies = static::parseSetCookieHeader($header);
64:
65: return new static($cookies);
66: }
67:
68: 69: 70: 71: 72: 73:
74: public static function createFromServerRequest(ServerRequestInterface $request)
75: {
76: $data = $request->getCookieParams();
77: $cookies = [];
78: foreach ($data as $name => $value) {
79: $cookies[] = new Cookie($name, $value);
80: }
81:
82: return new static($cookies);
83: }
84:
85: 86: 87: 88: 89:
90: public function count()
91: {
92: return count($this->cookies);
93: }
94:
95: 96: 97: 98: 99: 100: 101: 102: 103: 104:
105: public function add(CookieInterface $cookie)
106: {
107: $new = clone $this;
108: $new->cookies[$cookie->getId()] = $cookie;
109:
110: return $new;
111: }
112:
113: 114: 115: 116: 117: 118:
119: public function get($name)
120: {
121: $key = mb_strtolower($name);
122: foreach ($this->cookies as $cookie) {
123: if (mb_strtolower($cookie->getName()) === $key) {
124: return $cookie;
125: }
126: }
127:
128: return null;
129: }
130:
131: 132: 133: 134: 135: 136:
137: public function has($name)
138: {
139: $key = mb_strtolower($name);
140: foreach ($this->cookies as $cookie) {
141: if (mb_strtolower($cookie->getName()) === $key) {
142: return true;
143: }
144: }
145:
146: return false;
147: }
148:
149: 150: 151: 152: 153: 154: 155: 156:
157: public function remove($name)
158: {
159: $new = clone $this;
160: $key = mb_strtolower($name);
161: foreach ($new->cookies as $i => $cookie) {
162: if (mb_strtolower($cookie->getName()) === $key) {
163: unset($new->cookies[$i]);
164: }
165: }
166:
167: return $new;
168: }
169:
170: 171: 172: 173: 174: 175: 176:
177: protected function checkCookies(array $cookies)
178: {
179: foreach ($cookies as $index => $cookie) {
180: if (!$cookie instanceof CookieInterface) {
181: throw new InvalidArgumentException(
182: sprintf(
183: 'Expected `%s[]` as $cookies but instead got `%s` at index %d',
184: static::class,
185: getTypeName($cookie),
186: $index
187: )
188: );
189: }
190: }
191: }
192:
193: 194: 195: 196: 197:
198: public function getIterator()
199: {
200: return new ArrayIterator($this->cookies);
201: }
202:
203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214:
215: public function addToRequest(RequestInterface $request, array $extraCookies = [])
216: {
217: $uri = $request->getUri();
218: $cookies = $this->findMatchingCookies(
219: $uri->getScheme(),
220: $uri->getHost(),
221: $uri->getPath() ?: '/'
222: );
223: $cookies = array_merge($cookies, $extraCookies);
224: $cookiePairs = [];
225: foreach ($cookies as $key => $value) {
226: $cookie = sprintf("%s=%s", rawurlencode($key), rawurlencode($value));
227: $size = strlen($cookie);
228: if ($size > 4096) {
229: triggerWarning(sprintf(
230: 'The cookie `%s` exceeds the recommended maximum cookie length of 4096 bytes.',
231: $key
232: ));
233: }
234: $cookiePairs[] = $cookie;
235: }
236:
237: if (empty($cookiePairs)) {
238: return $request;
239: }
240:
241: return $request->withHeader('Cookie', implode('; ', $cookiePairs));
242: }
243:
244: 245: 246: 247: 248: 249: 250: 251:
252: protected function findMatchingCookies($scheme, $host, $path)
253: {
254: $out = [];
255: $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
256: foreach ($this->cookies as $cookie) {
257: if ($scheme === 'http' && $cookie->isSecure()) {
258: continue;
259: }
260: if (strpos($path, $cookie->getPath()) !== 0) {
261: continue;
262: }
263: $domain = $cookie->getDomain();
264: $leadingDot = substr($domain, 0, 1) === '.';
265: if ($leadingDot) {
266: $domain = ltrim($domain, '.');
267: }
268:
269: if ($cookie->isExpired($now)) {
270: continue;
271: }
272:
273: $pattern = '/' . preg_quote($domain, '/') . '$/';
274: if (!preg_match($pattern, $host)) {
275: continue;
276: }
277:
278: $out[$cookie->getName()] = $cookie->getValue();
279: }
280:
281: return $out;
282: }
283:
284: 285: 286: 287: 288: 289: 290:
291: public function addFromResponse(ResponseInterface $response, RequestInterface $request)
292: {
293: $uri = $request->getUri();
294: $host = $uri->getHost();
295: $path = $uri->getPath() ?: '/';
296:
297: $cookies = static::parseSetCookieHeader($response->getHeader('Set-Cookie'));
298: $cookies = $this->setRequestDefaults($cookies, $host, $path);
299: $new = clone $this;
300: foreach ($cookies as $cookie) {
301: $new->cookies[$cookie->getId()] = $cookie;
302: }
303: $new->removeExpiredCookies($host, $path);
304:
305: return $new;
306: }
307:
308: 309: 310: 311: 312: 313: 314: 315:
316: protected function setRequestDefaults(array $cookies, $host, $path)
317: {
318: $out = [];
319: foreach ($cookies as $name => $cookie) {
320: if (!$cookie->getDomain()) {
321: $cookie = $cookie->withDomain($host);
322: }
323: if (!$cookie->getPath()) {
324: $cookie = $cookie->withPath($path);
325: }
326: $out[] = $cookie;
327: }
328:
329: return $out;
330: }
331:
332: 333: 334: 335: 336: 337:
338: protected static function parseSetCookieHeader($values)
339: {
340: $cookies = [];
341: foreach ($values as $value) {
342: $value = rtrim($value, ';');
343: $parts = preg_split('/\;[ \t]*/', $value);
344:
345: $name = false;
346: $cookie = [
347: 'value' => '',
348: 'path' => '',
349: 'domain' => '',
350: 'secure' => false,
351: 'httponly' => false,
352: 'expires' => null,
353: 'max-age' => null
354: ];
355: foreach ($parts as $i => $part) {
356: if (strpos($part, '=') !== false) {
357: list($key, $value) = explode('=', $part, 2);
358: } else {
359: $key = $part;
360: $value = true;
361: }
362: if ($i === 0) {
363: $name = $key;
364: $cookie['value'] = urldecode($value);
365: continue;
366: }
367: $key = strtolower($key);
368: if (array_key_exists($key, $cookie) && !strlen($cookie[$key])) {
369: $cookie[$key] = $value;
370: }
371: }
372: try {
373: $expires = null;
374: if ($cookie['max-age'] !== null) {
375: $expires = new DateTimeImmutable('@' . (time() + $cookie['max-age']));
376: } elseif ($cookie['expires']) {
377: $expires = new DateTimeImmutable('@' . strtotime($cookie['expires']));
378: }
379: } catch (Exception $e) {
380: $expires = null;
381: }
382:
383: try {
384: $cookies[] = new Cookie(
385: $name,
386: $cookie['value'],
387: $expires,
388: $cookie['path'],
389: $cookie['domain'],
390: $cookie['secure'],
391: $cookie['httponly']
392: );
393: } catch (Exception $e) {
394:
395: }
396: }
397:
398: return $cookies;
399: }
400:
401: 402: 403: 404: 405: 406: 407:
408: protected function removeExpiredCookies($host, $path)
409: {
410: $time = new DateTimeImmutable('now', new DateTimeZone('UTC'));
411: $hostPattern = '/' . preg_quote($host, '/') . '$/';
412:
413: foreach ($this->cookies as $i => $cookie) {
414: $expired = $cookie->isExpired($time);
415: $pathMatches = strpos($path, $cookie->getPath()) === 0;
416: $hostMatches = preg_match($hostPattern, $cookie->getDomain());
417: if ($pathMatches && $hostMatches && $expired) {
418: unset($this->cookies[$i]);
419: }
420: }
421: }
422: }
423: