Completed
Push — master ( 4b777b...4845a8 )
by David
04:04 queued 02:38
created

src/Cookie.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Http\Message;
4
5
/**
6
 * Cookie Value Object.
7
 *
8
 * @author Márk Sági-Kazár <[email protected]>
9
 *
10
 * @see http://tools.ietf.org/search/rfc6265
11
 */
12
final class Cookie
13
{
14
    /**
15
     * @var string
16
     */
17
    private $name;
18
19
    /**
20
     * @var string|null
21
     */
22
    private $value;
23
24
    /**
25
     * @var int|null
26
     */
27
    private $maxAge;
28
29
    /**
30
     * @var string|null
31
     */
32
    private $domain;
33
34
    /**
35
     * @var string
36
     */
37
    private $path;
38
39
    /**
40
     * @var bool
41
     */
42
    private $secure;
43
44
    /**
45
     * @var bool
46
     */
47
    private $httpOnly;
48
49
    /**
50
     * Expires attribute is HTTP 1.0 only and should be avoided.
51
     *
52
     * @var \DateTime|null
53
     */
54
    private $expires;
55
56
    /**
57
     * @param string         $name
58
     * @param string|null    $value
59
     * @param int            $maxAge
60
     * @param string|null    $domain
61
     * @param string|null    $path
62
     * @param bool           $secure
63
     * @param bool           $httpOnly
64
     * @param \DateTime|null $expires  Expires attribute is HTTP 1.0 only and should be avoided.
65
     *
66
     * @throws \InvalidArgumentException If name, value or max age is not valid.
67
     */
68 61
    public function __construct(
69
        $name,
70
        $value = null,
71
        $maxAge = null,
72
        $domain = null,
73
        $path = null,
74
        $secure = false,
75
        $httpOnly = false,
76
        \DateTime $expires = null
77
    ) {
78 61
        $this->validateName($name);
79 57
        $this->validateValue($value);
80 54
        $this->validateMaxAge($maxAge);
81
82 54
        $this->name = $name;
83 54
        $this->value = $value;
84 54
        $this->maxAge = $maxAge;
85 54
        $this->expires = $expires;
86 54
        $this->domain = $this->normalizeDomain($domain);
87 54
        $this->path = $this->normalizePath($path);
88 54
        $this->secure = (bool) $secure;
89 54
        $this->httpOnly = (bool) $httpOnly;
90 54
    }
91
92
    /**
93
     * Creates a new cookie without any attribute validation.
94
     *
95
     * @param string         $name
96
     * @param string|null    $value
97
     * @param int            $maxAge
98
     * @param string|null    $domain
99
     * @param string|null    $path
100
     * @param bool           $secure
101
     * @param bool           $httpOnly
102
     * @param \DateTime|null $expires  Expires attribute is HTTP 1.0 only and should be avoided.
103
     */
104 3
    public static function createWithoutValidation(
105
        $name,
106
        $value = null,
107
        $maxAge = null,
108
        $domain = null,
109
        $path = null,
110
        $secure = false,
111
        $httpOnly = false,
112
        \DateTime $expires = null
113
    ) {
114 3
        $cookie = new self('name', null, null, $domain, $path, $secure, $httpOnly, $expires);
115 3
        $cookie->name = $name;
116 3
        $cookie->value = $value;
117 3
        $cookie->maxAge = $maxAge;
118
119 3
        return $cookie;
120
    }
121
122
    /**
123
     * Returns the name.
124
     *
125
     * @return string
126
     */
127 2
    public function getName()
128
    {
129 2
        return $this->name;
130
    }
131
132
    /**
133
     * Returns the value.
134
     *
135
     * @return string|null
136
     */
137 5
    public function getValue()
138
    {
139 5
        return $this->value;
140
    }
141
142
    /**
143
     * Checks if there is a value.
144
     *
145
     * @return bool
146
     */
147 13
    public function hasValue()
148
    {
149 13
        return isset($this->value);
150
    }
151
152
    /**
153
     * Sets the value.
154
     *
155
     * @param string|null $value
156
     *
157
     * @return Cookie
158
     */
159 8
    public function withValue($value)
160
    {
161 8
        $this->validateValue($value);
162
163 5
        $new = clone $this;
164 5
        $new->value = $value;
165
166 5
        return $new;
167
    }
168
169
    /**
170
     * Returns the max age.
171
     *
172
     * @return int|null
173
     */
174 3
    public function getMaxAge()
175
    {
176 3
        return $this->maxAge;
177
    }
178
179
    /**
180
     * Checks if there is a max age.
181
     *
182
     * @return bool
183
     */
184 1
    public function hasMaxAge()
185
    {
186 1
        return isset($this->maxAge);
187
    }
188
189
    /**
190
     * Sets the max age.
191
     *
192
     * @param int|null $maxAge
193
     *
194
     * @return Cookie
195
     */
196 2
    public function withMaxAge($maxAge)
197
    {
198 2
        $this->validateMaxAge($maxAge);
199
200 1
        $new = clone $this;
201 1
        $new->maxAge = $maxAge;
202
203 1
        return $new;
204
    }
205
206
    /**
207
     * Returns the expiration time.
208
     *
209
     * @return \DateTime|null
210
     */
211 3
    public function getExpires()
212
    {
213 3
        return $this->expires;
214
    }
215
216
    /**
217
     * Checks if there is an expiration time.
218
     *
219
     * @return bool
220
     */
221 2
    public function hasExpires()
222
    {
223 2
        return isset($this->expires);
224
    }
225
226
    /**
227
     * Sets the expires.
228
     *
229
     * @param \DateTime|null $expires
230
     *
231
     * @return Cookie
232
     */
233 1
    public function withExpires(\DateTime $expires = null)
234
    {
235 1
        $new = clone $this;
236 1
        $new->expires = $expires;
237
238 1
        return $new;
239
    }
240
241
    /**
242
     * Checks if the cookie is expired.
243
     *
244
     * @return bool
245
     */
246 2
    public function isExpired()
247
    {
248 2
        return isset($this->expires) and $this->expires < new \DateTime();
249
    }
250
251
    /**
252
     * Returns the domain.
253
     *
254
     * @return string|null
255
     */
256 3
    public function getDomain()
257
    {
258 3
        return $this->domain;
259
    }
260
261
    /**
262
     * Checks if there is a domain.
263
     *
264
     * @return bool
265
     */
266 4
    public function hasDomain()
267
    {
268 4
        return isset($this->domain);
269
    }
270
271
    /**
272
     * Sets the domain.
273
     *
274
     * @param string|null $domain
275
     *
276
     * @return Cookie
277
     */
278 1
    public function withDomain($domain)
279
    {
280 1
        $new = clone $this;
281 1
        $new->domain = $this->normalizeDomain($domain);
282
283 1
        return $new;
284
    }
285
286
    /**
287
     * Checks whether this cookie is meant for this domain.
288
     *
289
     * @see http://tools.ietf.org/html/rfc6265#section-5.1.3
290
     *
291
     * @param string $domain
292
     *
293
     * @return bool
294
     */
295 2
    public function matchDomain($domain)
296
    {
297
        // Domain is not set or exact match
298 2
        if (!$this->hasDomain() || 0 === strcasecmp($domain, $this->domain)) {
299 2
            return true;
300
        }
301
302
        // Domain is not an IP address
303 1
        if (filter_var($domain, FILTER_VALIDATE_IP)) {
304 1
            return false;
305
        }
306
307 1
        return (bool) preg_match(sprintf('/\b%s$/i', preg_quote($this->domain)), $domain);
308
    }
309
310
    /**
311
     * Returns the path.
312
     *
313
     * @return string
314
     */
315 2
    public function getPath()
316
    {
317 2
        return $this->path;
318
    }
319
320
    /**
321
     * Sets the path.
322
     *
323
     * @param string|null $path
324
     *
325
     * @return Cookie
326
     */
327 1
    public function withPath($path)
328
    {
329 1
        $new = clone $this;
330 1
        $new->path = $this->normalizePath($path);
331
332 1
        return $new;
333
    }
334
335
    /**
336
     * Checks whether this cookie is meant for this path.
337
     *
338
     * @see http://tools.ietf.org/html/rfc6265#section-5.1.4
339
     *
340
     * @param string $path
341
     *
342
     * @return bool
343
     */
344 3
    public function matchPath($path)
345
    {
346 3
        return $this->path === $path || (0 === strpos($path, rtrim($this->path, '/').'/'));
347
    }
348
349
    /**
350
     * Checks whether this cookie may only be sent over HTTPS.
351
     *
352
     * @return bool
353
     */
354 2
    public function isSecure()
355
    {
356 2
        return $this->secure;
357
    }
358
359
    /**
360
     * Sets whether this cookie should only be sent over HTTPS.
361
     *
362
     * @param bool $secure
363
     *
364
     * @return Cookie
365
     */
366 1
    public function withSecure($secure)
367
    {
368 1
        $new = clone $this;
369 1
        $new->secure = (bool) $secure;
370
371 1
        return $new;
372
    }
373
374
    /**
375
     * Check whether this cookie may not be accessed through Javascript.
376
     *
377
     * @return bool
378
     */
379 2
    public function isHttpOnly()
380
    {
381 2
        return $this->httpOnly;
382
    }
383
384
    /**
385
     * Sets whether this cookie may not be accessed through Javascript.
386
     *
387
     * @param bool $httpOnly
388
     *
389
     * @return Cookie
390
     */
391 1
    public function withHttpOnly($httpOnly)
392
    {
393 1
        $new = clone $this;
394 1
        $new->httpOnly = (bool) $httpOnly;
395
396 1
        return $new;
397
    }
398
399
    /**
400
     * Checks if this cookie represents the same cookie as $cookie.
401
     *
402
     * This does not compare the values, only name, domain and path.
403
     *
404
     * @param Cookie $cookie
405
     *
406
     * @return bool
407
     */
408 10
    public function match(self $cookie)
409
    {
410 10
        return $this->name === $cookie->name && $this->domain === $cookie->domain and $this->path === $cookie->path;
411
    }
412
413
    /**
414
     * Validates cookie attributes.
415
     *
416
     * @return bool
417
     */
418 4
    public function isValid()
419
    {
420
        try {
421 4
            $this->validateName($this->name);
422 3
            $this->validateValue($this->value);
423 2
            $this->validateMaxAge($this->maxAge);
424 4
        } catch (\InvalidArgumentException $e) {
425 3
            return false;
426
        }
427
428 1
        return true;
429
    }
430
431
    /**
432
     * Validates the name attribute.
433
     *
434
     * @see http://tools.ietf.org/search/rfc2616#section-2.2
435
     *
436
     * @param string $name
437
     *
438
     * @throws \InvalidArgumentException If the name is empty or contains invalid characters.
439
     */
440 61
    private function validateName($name)
441
    {
442 61
        if (strlen($name) < 1) {
443 1
            throw new \InvalidArgumentException('The name cannot be empty');
444
        }
445
446
        // Name attribute is a token as per spec in RFC 2616
447 60
        if (preg_match('/[\x00-\x20\x22\x28-\x29\x2C\x2F\x3A-\x40\x5B-\x5D\x7B\x7D\x7F]/', $name)) {
448 4
            throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name));
449
        }
450 57
    }
451
452
    /**
453
     * Validates a value.
454
     *
455
     * @see http://tools.ietf.org/html/rfc6265#section-4.1.1
456
     *
457
     * @param string|null $value
458
     *
459
     * @throws \InvalidArgumentException If the value contains invalid characters.
460
     */
461 57
    private function validateValue($value)
462
    {
463 57
        if (isset($value)) {
464 52
            if (preg_match('/[^\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]/', $value)) {
465 7
                throw new \InvalidArgumentException(sprintf('The cookie value "%s" contains invalid characters.', $value));
466
            }
467 48
        }
468 54
    }
469
470
    /**
471
     * Validates a Max-Age attribute.
472
     *
473
     * @param int|null $maxAge
474
     *
475
     * @throws \InvalidArgumentException If the Max-Age is not an empty or integer value.
476
     */
477 54
    private function validateMaxAge($maxAge)
478
    {
479 54
        if (isset($maxAge)) {
480 8
            if (!is_int($maxAge)) {
481 2
                throw new \InvalidArgumentException('Max-Age must be integer');
482
            }
483 6
        }
484 54
    }
485
486
    /**
487
     * Remove the leading '.' and lowercase the domain as per spec in RFC 6265.
488
     *
489
     * @see http://tools.ietf.org/html/rfc6265#section-4.1.2.3
490
     * @see http://tools.ietf.org/html/rfc6265#section-5.1.3
491
     * @see http://tools.ietf.org/html/rfc6265#section-5.2.3
492
     *
493
     * @param string|null $domain
494
     *
495
     * @return string
496
     */
497 54
    private function normalizeDomain($domain)
498
    {
499 54
        if (isset($domain)) {
500 6
            $domain = ltrim(strtolower($domain), '.');
501 6
        }
502
503 54
        return $domain;
504
    }
505
506
    /**
507
     * Processes path as per spec in RFC 6265.
508
     *
509
     * @see http://tools.ietf.org/html/rfc6265#section-5.1.4
510
     * @see http://tools.ietf.org/html/rfc6265#section-5.2.4
511
     *
512
     * @param string|null $path
513
     *
514
     * @return string
515
     */
516 54
    private function normalizePath($path)
517
    {
518 54
        $path = rtrim($path, '/');
519
520 54
        if (empty($path) or '/' !== substr($path, 0, 1)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
521 53
            $path = '/';
522 53
        }
523
524 54
        return $path;
525
    }
526
}
527