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