Completed
Push — master ( b002ee...8b7e77 )
by Alexander
02:11
created

Cookie::validFor()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 6
rs 10
ccs 5
cts 5
cp 1
crap 1
1
<?php
2
namespace Yiisoft\Yii\Web;
3
4
use Psr\Http\Message\ResponseInterface;
5
6
/**
7
 * Cookie helps adding Set-Cookie header response in order to set cookie
8
 */
9
final class Cookie
10
{
11
    /**
12
     * SameSite policy Lax will prevent the cookie from being sent by the browser in all cross-site browsing context
13
     * during CSRF-prone request methods (e.g. POST, PUT, PATCH etc).
14
     * E.g. a POST request from https://otherdomain.com to https://yourdomain.com will not include the cookie, however a GET request will.
15
     * When a user follows a link from https://otherdomain.com to https://yourdomain.com it will include the cookie
16
     * @see $sameSite
17
     */
18
    public const SAME_SITE_LAX = 'Lax';
19
20
    /**
21
     * SameSite policy Strict will prevent the cookie from being sent by the browser in all cross-site browsing context
22
     * regardless of the request method and even when following a regular link.
23
     * E.g. a GET request from https://otherdomain.com to https://yourdomain.com or a user following a link from
24
     * https://otherdomain.com to https://yourdomain.com will not include the cookie.
25
     * @see $sameSite
26
     */
27
    public const SAME_SITE_STRICT = 'Strict';
28
29
    public const SAME_SITE_NONE = '';
30
31
    /**
32
     * @var string name of the cookie
33
     */
34
    private $name;
35
36
    /**
37
     * @var string value of the cookie
38
     */
39
    private $value;
40
41
    /**
42
     * @var string domain of the cookie
43
     */
44
    private $domain;
45
46
    /**
47
     * @var int the timestamp at which the cookie expires. This is the server timestamp.
48
     * Defaults to 0, meaning "until the browser is closed".
49
     */
50
    private $expire = 0;
51
52
    /**
53
     * @var string the path on the server in which the cookie will be available on. The default is '/'.
54
     */
55
    private $path = '/';
56
57
    /**
58
     * @var bool whether cookie should be sent via secure connection
59
     */
60
    private $secure = true;
61
62
    /**
63
     * @var bool whether the cookie should be accessible only through the HTTP protocol.
64
     * By setting this property to true, the cookie will not be accessible by scripting languages,
65
     * such as JavaScript, which can effectively help to reduce identity theft through XSS attacks.
66
     */
67
    private $httpOnly = true;
68
69
    /**
70
     * @var string SameSite prevents the browser from sending this cookie along with cross-site requests.
71
     * Please note that this feature is only supported since PHP 7.3.0
72
     * For better security, an exception will be thrown if `sameSite` is set while using an unsupported version of PHP.
73
     * To use this feature across different PHP versions check the version first. E.g.
74
     * ```php
75
     * $cookie->sameSite = PHP_VERSION_ID >= 70300 ? yii\web\Cookie::SAME_SITE_LAX : null,
76
     * ```
77
     * @see https://www.owasp.org/index.php/SameSite for more information about sameSite.
78
     */
79
    private $sameSite = self::SAME_SITE_LAX;
80
81 11
    public function __construct(string $name, string $value)
82
    {
83
        // @see https://tools.ietf.org/html/rfc6265#section-4
84
        // @see https://tools.ietf.org/html/rfc2616#section-2.2
85 11
        if (!preg_match('~^[a-z0-9._\-]+$~i', $name)) {
86 1
            throw new \InvalidArgumentException("The cookie name \"$name\" contains invalid characters.");
87
        }
88
89 10
        $this->name = $name;
90 10
        $this->value = $value;
91
    }
92
93 2
    public function domain(string $domain): self
94
    {
95 2
        $new = clone $this;
96 2
        $new->domain = $domain;
97 2
        return $new;
98
    }
99
100 1
    public function validFor(\DateInterval $dateInterval): self
101
    {
102 1
        $expireDateTime = (new \DateTimeImmutable())->add($dateInterval);
103 1
        $new = clone $this;
104 1
        $new->expire = (int)$expireDateTime->format('U');
105 1
        return $new;
106
    }
107
108 1
    public function expireAt(\DateTimeInterface $dateTime): self
109
    {
110 1
        $new = clone $this;
111 1
        $new->expire = (int)$dateTime->format('U');
112 1
        return $new;
113
    }
114
115 1
    public function expireWhenBrowserIsClosed(): self
116
    {
117 1
        $new = clone $this;
118 1
        $new->expire = 0;
119 1
        return $new;
120
    }
121
122 2
    public function path(string $path): self
123
    {
124 2
        $new = clone $this;
125 2
        $new->path = $path;
126 2
        return $new;
127
    }
128
129 2
    public function secure(bool $secure): self
130
    {
131 2
        $new = clone $this;
132 2
        $new->secure = $secure;
133 2
        return $new;
134
    }
135
136 2
    public function httpOnly(bool $httpOnly): self
137
    {
138 2
        $new = clone $this;
139 2
        $new->httpOnly = $httpOnly;
140 2
        return $new;
141
    }
142
143 3
    public function sameSite(string $sameSite): self
144
    {
145 3
        if (!in_array($sameSite, [self::SAME_SITE_LAX, self::SAME_SITE_STRICT, self::SAME_SITE_NONE], true)) {
146 1
            throw new \InvalidArgumentException('sameSite should be either Lax or Strict');
147
        }
148
149 2
        $new = clone $this;
150 2
        $new->sameSite = $sameSite;
151 2
        return $new;
152
    }
153
154 9
    public function addToResponse(ResponseInterface $response): ResponseInterface
155
    {
156 9
        $headerValue = $this->name . '=' . urlencode($this->value);
157
158 9
        if ($this->expire !== 0) {
159 2
            $headerValue .= '; Expires=' . gmdate('D, d-M-Y H:i:s T', $this->expire);
160
        }
161 9
        if (empty($this->path) === false) {
162 9
            $headerValue .= '; Path=' . $this->path;
163
        }
164 9
        if (empty($this->domain) === false) {
165 2
            $headerValue .= '; Domain=' . $this->domain;
166
        }
167 9
        if ($this->secure) {
168 8
            $headerValue .= '; Secure';
169
        }
170 9
        if ($this->httpOnly) {
171 8
            $headerValue .= '; HttpOnly';
172
        }
173 9
        if ($this->sameSite !== '') {
174 8
            $headerValue .= '; SameSite=' . $this->sameSite;
175
        }
176
177 9
        return $response->withAddedHeader('Set-Cookie', $headerValue);
178
    }
179
}
180