1: <?php
2:
3: namespace Guzzle\Plugin\Cookie;
4:
5: use Guzzle\Common\ToArrayInterface;
6:
7: /**
8: * Set-Cookie object
9: */
10: class Cookie implements ToArrayInterface
11: {
12: /**
13: * @var array Cookie data
14: */
15: protected $data;
16:
17: /**
18: * @var string ASCII codes not valid for for use in a cookie name
19: *
20: * Cookie names are defined as 'token', according to RFC 2616, Section 2.2
21: * A valid token may contain any CHAR except CTLs (ASCII 0 - 31 or 127)
22: * or any of the following separators
23: */
24: protected static $invalidCharString;
25:
26: /**
27: * Gets an array of invalid cookie characters
28: *
29: * @return array
30: */
31: protected static function getInvalidCharacters()
32: {
33: if (!self::$invalidCharString) {
34: self::$invalidCharString = implode('', array_map('chr', array_merge(
35: range(0, 32),
36: array(34, 40, 41, 44, 47),
37: array(58, 59, 60, 61, 62, 63, 64, 91, 92, 93, 123, 125, 127)
38: )));
39: }
40:
41: return self::$invalidCharString;
42: }
43:
44: /**
45: * @param array $data Array of cookie data provided by a Cookie parser
46: */
47: public function __construct(array $data = array())
48: {
49: static $defaults = array(
50: 'name' => '',
51: 'value' => '',
52: 'domain' => '',
53: 'path' => '/',
54: 'expires' => null,
55: 'max_age' => 0,
56: 'comment' => null,
57: 'comment_url' => null,
58: 'port' => array(),
59: 'version' => null,
60: 'secure' => false,
61: 'discard' => false,
62: 'http_only' => false
63: );
64:
65: $this->data = array_merge($defaults, $data);
66: // Extract the expires value and turn it into a UNIX timestamp if needed
67: if (!$this->getExpires() && $this->getMaxAge()) {
68: // Calculate the expires date
69: $this->setExpires(time() + (int) $this->getMaxAge());
70: } elseif ($this->getExpires() && !is_numeric($this->getExpires())) {
71: $this->setExpires(strtotime($this->getExpires()));
72: }
73: }
74:
75: /**
76: * Get the cookie as an array
77: *
78: * @return array
79: */
80: public function toArray()
81: {
82: return $this->data;
83: }
84:
85: /**
86: * Get the cookie name
87: *
88: * @return string
89: */
90: public function getName()
91: {
92: return $this->data['name'];
93: }
94:
95: /**
96: * Set the cookie name
97: *
98: * @param string $name Cookie name
99: *
100: * @return Cookie
101: */
102: public function setName($name)
103: {
104: return $this->setData('name', $name);
105: }
106:
107: /**
108: * Get the cookie value
109: *
110: * @return string
111: */
112: public function getValue()
113: {
114: return $this->data['value'];
115: }
116:
117: /**
118: * Set the cookie value
119: *
120: * @param string $value Cookie value
121: *
122: * @return Cookie
123: */
124: public function setValue($value)
125: {
126: return $this->setData('value', $value);
127: }
128:
129: /**
130: * Get the domain
131: *
132: * @return string|null
133: */
134: public function getDomain()
135: {
136: return $this->data['domain'];
137: }
138:
139: /**
140: * Set the domain of the cookie
141: *
142: * @param string $domain
143: *
144: * @return Cookie
145: */
146: public function setDomain($domain)
147: {
148: return $this->setData('domain', $domain);
149: }
150:
151: /**
152: * Get the path
153: *
154: * @return string
155: */
156: public function getPath()
157: {
158: return $this->data['path'];
159: }
160:
161: /**
162: * Set the path of the cookie
163: *
164: * @param string $path Path of the cookie
165: *
166: * @return Cookie
167: */
168: public function setPath($path)
169: {
170: return $this->setData('path', $path);
171: }
172:
173: /**
174: * Maximum lifetime of the cookie in seconds
175: *
176: * @return int|null
177: */
178: public function getMaxAge()
179: {
180: return $this->data['max_age'];
181: }
182:
183: /**
184: * Set the max-age of the cookie
185: *
186: * @param int $maxAge Max age of the cookie in seconds
187: *
188: * @return Cookie
189: */
190: public function setMaxAge($maxAge)
191: {
192: return $this->setData('max_age', $maxAge);
193: }
194:
195: /**
196: * The UNIX timestamp when the cookie expires
197: *
198: * @return mixed
199: */
200: public function getExpires()
201: {
202: return $this->data['expires'];
203: }
204:
205: /**
206: * Set the unix timestamp for which the cookie will expire
207: *
208: * @param int $timestamp Unix timestamp
209: *
210: * @return Cookie
211: */
212: public function setExpires($timestamp)
213: {
214: return $this->setData('expires', $timestamp);
215: }
216:
217: /**
218: * Version of the cookie specification. RFC 2965 is 1
219: *
220: * @return mixed
221: */
222: public function getVersion()
223: {
224: return $this->data['version'];
225: }
226:
227: /**
228: * Set the cookie version
229: *
230: * @param string|int $version Version to set
231: *
232: * @return Cookie
233: */
234: public function setVersion($version)
235: {
236: return $this->setData('version', $version);
237: }
238:
239: /**
240: * Get whether or not this is a secure cookie
241: *
242: * @return null|bool
243: */
244: public function getSecure()
245: {
246: return $this->data['secure'];
247: }
248:
249: /**
250: * Set whether or not the cookie is secure
251: *
252: * @param bool $secure Set to true or false if secure
253: *
254: * @return Cookie
255: */
256: public function setSecure($secure)
257: {
258: return $this->setData('secure', (bool) $secure);
259: }
260:
261: /**
262: * Get whether or not this is a session cookie
263: *
264: * @return null|bool
265: */
266: public function getDiscard()
267: {
268: return $this->data['discard'];
269: }
270:
271: /**
272: * Set whether or not this is a session cookie
273: *
274: * @param bool $discard Set to true or false if this is a session cookie
275: *
276: * @return Cookie
277: */
278: public function setDiscard($discard)
279: {
280: return $this->setData('discard', $discard);
281: }
282:
283: /**
284: * Get the comment
285: *
286: * @return string|null
287: */
288: public function getComment()
289: {
290: return $this->data['comment'];
291: }
292:
293: /**
294: * Set the comment of the cookie
295: *
296: * @param string $comment Cookie comment
297: *
298: * @return Cookie
299: */
300: public function setComment($comment)
301: {
302: return $this->setData('comment', $comment);
303: }
304:
305: /**
306: * Get the comment URL of the cookie
307: *
308: * @return string|null
309: */
310: public function getCommentUrl()
311: {
312: return $this->data['comment_url'];
313: }
314:
315: /**
316: * Set the comment URL of the cookie
317: *
318: * @param string $commentUrl Cookie comment URL for more information
319: *
320: * @return Cookie
321: */
322: public function setCommentUrl($commentUrl)
323: {
324: return $this->setData('comment_url', $commentUrl);
325: }
326:
327: /**
328: * Get an array of acceptable ports this cookie can be used with
329: *
330: * @return array
331: */
332: public function getPorts()
333: {
334: return $this->data['port'];
335: }
336:
337: /**
338: * Set a list of acceptable ports this cookie can be used with
339: *
340: * @param array $ports Array of acceptable ports
341: *
342: * @return Cookie
343: */
344: public function setPorts(array $ports)
345: {
346: return $this->setData('port', $ports);
347: }
348:
349: /**
350: * Get whether or not this is an HTTP only cookie
351: *
352: * @return bool
353: */
354: public function getHttpOnly()
355: {
356: return $this->data['http_only'];
357: }
358:
359: /**
360: * Set whether or not this is an HTTP only cookie
361: *
362: * @param bool $httpOnly Set to true or false if this is HTTP only
363: *
364: * @return Cookie
365: */
366: public function setHttpOnly($httpOnly)
367: {
368: return $this->setData('http_only', $httpOnly);
369: }
370:
371: /**
372: * Get an array of extra cookie data
373: *
374: * @return array
375: */
376: public function getAttributes()
377: {
378: return $this->data['data'];
379: }
380:
381: /**
382: * Get a specific data point from the extra cookie data
383: *
384: * @param string $name Name of the data point to retrieve
385: *
386: * @return null|string
387: */
388: public function getAttribute($name)
389: {
390: return array_key_exists($name, $this->data['data']) ? $this->data['data'][$name] : null;
391: }
392:
393: /**
394: * Set a cookie data attribute
395: *
396: * @param string $name Name of the attribute to set
397: * @param string $value Value to set
398: *
399: * @return Cookie
400: */
401: public function setAttribute($name, $value)
402: {
403: $this->data['data'][$name] = $value;
404:
405: return $this;
406: }
407:
408: /**
409: * Check if the cookie matches a path value
410: *
411: * @param string $path Path to check against
412: *
413: * @return bool
414: */
415: public function matchesPath($path)
416: {
417: return !$this->getPath() || 0 === stripos($path, $this->getPath());
418: }
419:
420: /**
421: * Check if the cookie matches a domain value
422: *
423: * @param string $domain Domain to check against
424: *
425: * @return bool
426: */
427: public function matchesDomain($domain)
428: {
429: $cookieDomain = $this->getDomain();
430:
431: // Domain not set or exact match.
432: if (!$cookieDomain || !strcasecmp($domain, $cookieDomain)) {
433: return true;
434: }
435:
436: // . prefix match.
437: if (strpos($cookieDomain, '.') === 0) {
438: $realDomain = substr($cookieDomain, 1);
439:
440: // Root domains don't match except for .local.
441: if (!substr_count($realDomain, '.') && strcasecmp($realDomain, 'local')) {
442: return false;
443: }
444:
445: if (substr($domain, -strlen($realDomain)) === $realDomain) {
446: // Match exact or 1 deep subdomain.
447: return !strcasecmp($domain, $realDomain) ||
448: substr_count(substr($domain, 0, -strlen($realDomain)), '.') === 1;
449: }
450: }
451:
452: return false;
453: }
454:
455: /**
456: * Check if the cookie is compatible with a specific port
457: *
458: * @param int $port Port to check
459: *
460: * @return bool
461: */
462: public function matchesPort($port)
463: {
464: return count($this->getPorts()) == 0 || in_array($port, $this->getPorts());
465: }
466:
467: /**
468: * Check if the cookie is expired
469: *
470: * @return bool
471: */
472: public function isExpired()
473: {
474: return $this->getExpires() && time() > $this->getExpires();
475: }
476:
477: /**
478: * Check if the cookie is valid according to RFC 6265
479: *
480: * @return bool|string Returns true if valid or an error message if invalid
481: */
482: public function validate()
483: {
484: // Names must not be empty, but can be 0
485: $name = $this->getName();
486: if (empty($name) && !is_numeric($name)) {
487: return 'The cookie name must not be empty';
488: }
489:
490: // Check if any of the invalid characters are present in the cookie name
491: if (strpbrk($name, self::getInvalidCharacters()) !== false) {
492: return 'The cookie name must not contain invalid characters: ' . $name;
493: }
494:
495: // Value must not be empty, but can be 0
496: $value = $this->getValue();
497: if (empty($value) && !is_numeric($value)) {
498: return 'The cookie value must not be empty';
499: }
500:
501: // Domains must not be empty, but can be 0
502: // A "0" is not a valid internet domain, but may be used as server name in a private network
503: $domain = $this->getDomain();
504: if (empty($domain) && !is_numeric($domain)) {
505: return 'The cookie domain must not be empty';
506: }
507:
508: return true;
509: }
510:
511: /**
512: * Set a value and return the cookie object
513: *
514: * @param string $key Key to set
515: * @param string $value Value to set
516: *
517: * @return Cookie
518: */
519: private function setData($key, $value)
520: {
521: $this->data[$key] = $value;
522:
523: return $this;
524: }
525: }
526: