-
-
Notifications
You must be signed in to change notification settings - Fork 188
/
Cookie.php
456 lines (409 loc) · 14.6 KB
/
Cookie.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
<?php
namespace Neos\Flow\Http;
/*
* This file is part of the Neos.Flow package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/
use Neos\Flow\Annotations as Flow;
/**
* Represents a HTTP Cookie as of RFC 6265
*
* @phpstan-consistent-constructor
* @api
* @see http://tools.ietf.org/html/rfc6265
* @Flow\Proxy(false)
*/
class Cookie
{
/**
* A token as per RFC 2616, Section 2.2
*/
const PATTERN_TOKEN = '/^([\x21\x23-\x27\x2A-\x2E0-9A-Z\x5E-\x60a-z\x7C\x7E]+)$/';
/**
* The max age pattern as per RFC 6265, Section 5.2.2
*/
const PATTERN_MAX_AGE = '/^\-?\d+$/';
/**
* A simplified pattern for a basically valid domain (<subdomain>) as per RFC 6265, 4.1.1 / RFC 1034, 3.5 + RFC 1123, 2.1
*/
const PATTERN_DOMAIN = '/^([a-z0-9]+[a-z0-9.-]*[a-z0-9])$|([0-9\.]+[0-9])$/i';
/**
* A path as per RFC 6265, 4.1.1
*/
const PATTERN_PATH = '/^([\x20-\x3A\x3C-\x7E])+$/';
const SAMESITE_NONE = 'none';
const SAMESITE_LAX = 'lax';
const SAMESITE_STRICT = 'strict';
/**
* Cookie Name, a token (RFC 6265, 4.1.1)
* @var string
*/
protected $name;
/**
* @var string
*/
protected $value;
/**
* Unix timestamp of the expiration date / time or 0 for "session" expiration (RFC 6265, 4.1.2.1)
* @var integer
*/
protected $expiresTimestamp;
/**
* Number of seconds until the cookie expires (RFC 6265, 4.1.2.2)
* @var integer
*/
protected $maximumAge;
/**
* Hosts to which this cookie will be sent (RFC 6265, 4.1.2.3)
* @var string
*/
protected $domain;
/**
* @var string
*/
protected $path;
/**
* @var boolean
*/
protected $secure;
/**
* @var boolean
*/
protected $httpOnly;
/**
* Possible values: none, lax, or strict (RFC 6265bis-05, 8.8)
*
* sameSite=strict
* Cookie will only be sent in a first-party context, cookie is ignored on the initial request to your site.
* This is a good setting when you have cookies relating to functionality, such as changing a password
* sameSite=lax
* Cookie will only be sent in a top-level navigations.
* This is a good setting when you need the cookie on the initial request, such as session login via cookie
* sameSite=none
* Cookie will be sent in a third-party context.
* This is a good setting when you need the cookie in cors ajax request, such as providing an api with session login via cookie
*
* @var string
*/
protected $sameSite;
/**
* Constructs a new Cookie object
*
* @param string $name The cookie name as a valid token (RFC 2616)
* @param mixed $value The value to store in the cookie. Must be possible to cast into a string.
* @param integer|\DateTime $expires Date and time after which this cookie expires.
* @param integer $maximumAge Number of seconds until the cookie expires.
* @param string $domain The host to which the user agent will send this cookie
* @param string $path The path describing the scope of this cookie
* @param boolean $secure If this cookie should only be sent through a "secure" channel by the user agent
* @param boolean $httpOnly If this cookie should only be used through the HTTP protocol
* @param string $sameSite If this cookie should restricted to a first-party or top-level navigation or third-party context
* @api
* @throws \InvalidArgumentException
*/
public function __construct($name, $value = null, $expires = 0, $maximumAge = null, $domain = null, $path = '/', $secure = false, $httpOnly = true, $sameSite = null)
{
if (preg_match(self::PATTERN_TOKEN, $name) !== 1) {
throw new \InvalidArgumentException('The parameter "name" passed to the Cookie constructor must be a valid token as per RFC 2616, Section 2.2.', 1345101977);
}
if ($expires instanceof \Datetime) {
$expires = $expires->getTimestamp();
}
if (!is_int($expires)) {
throw new \InvalidArgumentException('The parameter "expires" passed to the Cookie constructor must be a unix timestamp or a DateTime object.', 1345108785);
}
if ($maximumAge !== null && !is_int($maximumAge)) {
throw new \InvalidArgumentException('The parameter "maximumAge" passed to the Cookie constructor must be an integer value.', 1345108786);
}
if ($domain !== null && preg_match(self::PATTERN_DOMAIN, $domain) !== 1) {
throw new \InvalidArgumentException('The parameter "domain" passed to the Cookie constructor must be a valid domain as per RFC 6265, Section 4.1.2.3.', 1345116246);
}
if ($path !== null && preg_match(self::PATTERN_PATH, $path) !== 1) {
throw new \InvalidArgumentException('The parameter "path" passed to the Cookie constructor must be a valid path as per RFC 6265, Section 4.1.1.', 1345123078);
}
$sameSite = $sameSite ?? self::SAMESITE_LAX;
if (!\in_array($sameSite, [self::SAMESITE_LAX, self::SAMESITE_STRICT, self::SAMESITE_NONE], true)) {
throw new \InvalidArgumentException('The parameter "sameSite" passed to the Cookie constructor must be a valid samesite value. Possible values are "none", "strict" and "lax"', 1584955500);
}
$this->name = $name;
$this->value = $value;
$this->expiresTimestamp = $expires;
$this->maximumAge = $maximumAge;
$this->domain = $domain;
$this->path = $path;
$this->secure = ($secure == true) || $sameSite === self::SAMESITE_NONE;
$this->httpOnly = ($httpOnly == true);
$this->sameSite = $sameSite;
}
/**
* Creates a cookie (an instance of this class) by a provided
* raw header string like "foo=507d9f20317a5; path=/; domain=.example.org"
* This is is an implementation of the algorithm explained in RFC 6265, Section 5.2
* A basic statement of this algorithm is to "ignore the set-cookie-string entirely"
* in case a required condition is not met. In these cases this function will return NULL
* rather than the created cookie.
*
* @param string $header The Set-Cookie string without the actual "Set-Cookie:" part
* @return Cookie
* @see http://tools.ietf.org/html/rfc6265
*/
public static function createFromRawSetCookieHeader($header)
{
$nameValueAndUnparsedAttributes = explode(';', $header, 2);
$expectedNameValuePair = $nameValueAndUnparsedAttributes[0];
$unparsedAttributes = isset($nameValueAndUnparsedAttributes[1]) ? $nameValueAndUnparsedAttributes[1] : '';
if (strpos($expectedNameValuePair, '=') === false) {
return null;
}
$cookieNameAndValue = explode('=', $expectedNameValuePair, 2);
$cookieName = trim($cookieNameAndValue[0]);
$cookieValue = isset($cookieNameAndValue[1]) ? trim($cookieNameAndValue[1]) : '';
if ($cookieName === '') {
return null;
}
$expiresAttribute = 0;
$maxAgeAttribute = null;
$domainAttribute = null;
$pathAttribute = null;
$secureAttribute = false;
$httpOnlyAttribute = true;
$sameSite = self::SAMESITE_LAX;
if ($unparsedAttributes !== '') {
foreach (explode(';', $unparsedAttributes) as $cookieAttributeValueString) {
$attributeNameAndValue = explode('=', $cookieAttributeValueString, 2);
$attributeName = trim($attributeNameAndValue[0]);
$attributeValue = isset($attributeNameAndValue[1]) ? trim($attributeNameAndValue[1]) : '';
switch (strtoupper($attributeName)) {
case 'EXPIRES':
try {
$expiresAttribute = new \DateTime($attributeValue);
} catch (\Exception $exception) {
// as of RFC 6265 Section 5.2.1, a non parsable Expires date should result into
// ignoring, but since the Cookie constructor relies on it, we'll
// assume a Session cookie with an expiry date of 0.
$expiresAttribute = 0;
}
break;
case 'MAX-AGE':
if (preg_match(self::PATTERN_MAX_AGE, $attributeValue) === 1) {
$maxAgeAttribute = (int)$attributeValue;
}
break;
case 'DOMAIN':
if ($attributeValue !== '') {
$domainAttribute = strtolower(ltrim($attributeValue, '.'));
}
break;
case 'PATH':
if ($attributeValue === '' || substr($attributeValue, 0, 1) !== '/') {
$pathAttribute = '/';
} else {
$pathAttribute = $attributeValue;
}
break;
case 'SECURE':
$secureAttribute = true;
break;
case 'HTTPONLY':
$httpOnlyAttribute = true;
break;
case 'SAMESITE':
if (\in_array(strtolower($attributeValue), [self::SAMESITE_LAX, self::SAMESITE_STRICT, self::SAMESITE_NONE], true)) {
$sameSite = strtolower($attributeValue);
}
if (strtolower($attributeValue) === self::SAMESITE_NONE) {
$secureAttribute = true;
}
break;
}
}
}
$cookie = new static(
$cookieName,
$cookieValue,
$expiresAttribute,
$maxAgeAttribute,
$domainAttribute,
$pathAttribute,
$secureAttribute,
$httpOnlyAttribute,
$sameSite
);
return $cookie;
}
/**
* Returns the name of this cookie
*
* @return string The cookie name
* @api
*/
public function getName()
{
return $this->name;
}
/**
* Returns the value of this cookie
*
* @return mixed
* @api
*/
public function getValue()
{
return $this->value;
}
/**
* Sets the value of this cookie
*
* @param mixed $value The new value
* @return void
* @api
*/
public function setValue($value)
{
$this->value = $value;
}
/**
* Returns the date and time of the Expires attribute, if any.
*
* Note that this date / time is returned as a unix timestamp, no matter what
* the format was originally set through the constructor of this Cookie.
*
* The special case "no expiration time" is returned in form of a zero value.
*
* @return integer A unix timestamp or 0
* @api
*/
public function getExpires()
{
return $this->expiresTimestamp;
}
/**
* Returns the number of seconds until the cookie expires, if defined.
*
* This information is rendered as the Max-Age attribute (RFC 6265, 4.1.2.2).
* Note that not all browsers support this attribute.
*
* @return integer The maximum age in seconds, or NULL if none has been defined.
* @api
*/
public function getMaximumAge()
{
return $this->maximumAge;
}
/**
* Returns the domain this cookie is valid for.
*
* @return string The domain name
* @api
*/
public function getDomain()
{
return $this->domain;
}
/**
* Returns the path this cookie is valid for.
*
* @return string The path
* @api
*/
public function getPath()
{
return $this->path;
}
/**
* Tells if the cookie was flagged to be sent over "secure" channels only.
*
* This security measure only has a limited effect. Please read RFC 6265 Section 8.6
* for more details.
*
* @return boolean State of the "Secure" attribute
* @api
*/
public function isSecure()
{
return $this->secure;
}
/**
* Tells if this cookie should only be used through the HTTP protocol.
*
* @return boolean State of the "HttpOnly" attribute
* @api
*/
public function isHttpOnly()
{
return $this->httpOnly;
}
/**
* Returns the SameSite of this cookie
*
* @return string|null
* @api
*/
public function getSameSite()
{
return $this->sameSite;
}
/**
* Marks this cookie for removal.
*
* On executing this method, the expiry time of this cookie is set to a point
* in time in the past triggers the removal of the cookie in the user agent.
*
* @return void
*/
public function expire()
{
$this->expiresTimestamp = 202046400;
$this->maximumAge = 0;
}
/**
* Tells if this cookie is expired and will be removed in the user agent when it
* received the response containing this cookie.
*
* @return boolean True if this cookie is expired
*/
public function isExpired()
{
return ($this->expiresTimestamp !== 0 && $this->expiresTimestamp < time());
}
/**
* Renders the field value suitable for a HTTP "Set-Cookie" header.
*
* @return string
*/
public function __toString()
{
if ($this->value === false) {
$value = 0;
} else {
$value = urlencode((string)$this->value);
}
$cookiePair = sprintf('%s=%s', $this->name, $value);
$attributes = '';
if ($this->expiresTimestamp !== 0) {
$attributes .= '; Expires=' . gmdate('D, d-M-Y H:i:s T', $this->expiresTimestamp);
}
if ($this->maximumAge !== null && $this->maximumAge > 0) {
$attributes .= '; Max-Age=' . $this->maximumAge;
}
if ($this->domain !== null) {
$attributes .= '; Domain=' . $this->domain;
}
$attributes .= '; Path=' . $this->path;
if ($this->secure) {
$attributes .= '; Secure';
}
if ($this->httpOnly) {
$attributes .= '; HttpOnly';
}
if ($this->sameSite) {
$attributes .= '; SameSite=' . $this->sameSite;
}
return $cookiePair . $attributes;
}
}