Passed
Pull Request — master (#27)
by Anatoly
04:06
created

SetCookieHeader::getFieldValue()   B

Complexity

Conditions 9
Paths 64

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 9

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 17
c 1
b 0
f 0
nc 64
nop 0
dl 0
loc 32
ccs 18
cts 18
cp 1
crap 9
rs 8.0555
1
<?php declare(strict_types=1);
2
3
/**
4
 * It's free open-source software released under the MIT License.
5
 *
6
 * @author Anatoly Nekhay <[email protected]>
7
 * @copyright Copyright (c) 2018, Anatoly Nekhay
8
 * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE
9
 * @link https://github.com/sunrise-php/http-message
10
 */
11
12
namespace Sunrise\Http\Message\Header;
13
14
/**
15
 * Import classes
16
 */
17
use DateTimeImmutable;
18
use DateTimeInterface;
19
use Sunrise\Http\Message\Exception\InvalidHeaderValueException;
20
use Sunrise\Http\Message\Header;
21
22
/**
23
 * Import functions
24
 */
25
use function max;
26
use function rawurlencode;
27
use function sprintf;
28
use function strpbrk;
29
use function time;
30
31
/**
32
 * @link https://tools.ietf.org/html/rfc6265#section-4.1
33
 * @link https://github.com/php/php-src/blob/master/ext/standard/head.c
34
 */
35
class SetCookieHeader extends Header
36
{
37
38
    /**
39
     * Cookies are not sent on normal cross-site subrequests, but
40
     * are sent when a user is navigating to the origin site.
41
     *
42
     * This is the default cookie value if SameSite has not been
43
     * explicitly specified in recent browser versions..
44
     *
45
     * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#lax
46
     *
47
     * @var string
48
     */
49
    public const SAME_SITE_LAX = 'Lax';
50
51
    /**
52
     * Cookies will only be sent in a first-party context and not
53
     * be sent along with requests initiated by third party websites.
54
     *
55
     * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#strict
56
     *
57
     * @var string
58
     */
59
    public const SAME_SITE_STRICT = 'Strict';
60
61
    /**
62
     * Cookies will be sent in all contexts, i.e. in responses to
63
     * both first-party and cross-site requests.
64
     *
65
     * If SameSite=None is set, the cookie Secure attribute must
66
     * also be set (or the cookie will be blocked).
67
     *
68
     * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#none
69
     *
70
     * @var string
71
     */
72
    public const SAME_SITE_NONE = 'None';
73
74
    /**
75
     * Cookie option keys
76
     *
77
     * @var string
78
     */
79
    public const OPTION_KEY_PATH = 'path';
80
    public const OPTION_KEY_DOMAIN = 'domain';
81
    public const OPTION_KEY_SECURE = 'secure';
82
    public const OPTION_KEY_HTTP_ONLY = 'httpOnly';
83
    public const OPTION_KEY_SAMESITE = 'sameSite';
84
85
    /**
86
     * Default cookie options
87
     *
88
     * @var array{
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{ at position 2 could not be parsed: the token is null at position 2.
Loading history...
89
     *        path?: ?string,
90
     *        domain?: ?string,
91
     *        secure?: ?bool,
92
     *        httpOnly?: ?bool,
93
     *        sameSite?: ?string
94
     *      }
95
     */
96
    protected static array $defaultOptions = [
97
        self::OPTION_KEY_PATH      => '/',
98
        self::OPTION_KEY_DOMAIN    => null,
99
        self::OPTION_KEY_SECURE    => null,
100
        self::OPTION_KEY_HTTP_ONLY => true,
101
        self::OPTION_KEY_SAMESITE  => self::SAME_SITE_LAX,
102
    ];
103
104
    /**
105
     * The cookie name
106
     *
107
     * @var string
108
     */
109
    private string $name;
110
111
    /**
112
     * The cookie value
113
     *
114
     * @var string
115
     */
116
    private string $value;
117
118
    /**
119
     * The cookie expiration date
120
     *
121
     * @var DateTimeInterface|null
122
     */
123
    private ?DateTimeInterface $expires;
124
125
    /**
126
     * The cookie options
127
     *
128
     * @var array{
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{ at position 2 could not be parsed: the token is null at position 2.
Loading history...
129
     *        path?: ?string,
130
     *        domain?: ?string,
131
     *        secure?: ?bool,
132
     *        httpOnly?: ?bool,
133
     *        sameSite?: ?string
134
     *      }
135
     */
136
    private array $options;
137
138
    /**
139
     * Constructor of the class
140
     *
141
     * @param string $name
142
     * @param string $value
143
     * @param DateTimeInterface|null $expires
144
     * @param array{path?: ?string, domain?: ?string, secure?: ?bool, httpOnly?: ?bool, sameSite?: ?string} $options
145
     *
146
     * @throws InvalidHeaderValueException
147
     *         If one of the parameters isn't valid.
148
     */
149 25
    public function __construct(string $name, string $value, ?DateTimeInterface $expires = null, array $options = [])
150
    {
151 25
        $this->validateCookieName($name);
152
153 23
        if (isset($options[self::OPTION_KEY_PATH])) {
154 3
            $this->validateCookieOption(self::OPTION_KEY_PATH, $options[self::OPTION_KEY_PATH]);
155
        }
156
157 21
        if (isset($options[self::OPTION_KEY_DOMAIN])) {
158 3
            $this->validateCookieOption(self::OPTION_KEY_DOMAIN, $options[self::OPTION_KEY_DOMAIN]);
159
        }
160
161 19
        if (isset($options[self::OPTION_KEY_SAMESITE])) {
162 3
            $this->validateCookieOption(self::OPTION_KEY_SAMESITE, $options[self::OPTION_KEY_SAMESITE]);
163
        }
164
165 17
        if ($value === '') {
166 1
            $value = 'deleted';
167 1
            $expires = new DateTimeImmutable('1 year ago');
168
        }
169
170 17
        $options += static::$defaultOptions;
171
172 17
        $this->name = $name;
173 17
        $this->value = $value;
174 17
        $this->expires = $expires;
175 17
        $this->options = $options;
176
    }
177
178
    /**
179
     * {@inheritdoc}
180
     */
181 3
    public function getFieldName(): string
182
    {
183 3
        return 'Set-Cookie';
184
    }
185
186
    /**
187
     * {@inheritdoc}
188
     */
189 15
    public function getFieldValue(): string
190
    {
191 15
        $name = rawurlencode($this->name);
192 15
        $value = rawurlencode($this->value);
193 15
        $result = sprintf('%s=%s', $name, $value);
194
195 15
        if (isset($this->expires)) {
196 5
            $result .= '; Expires=' . $this->formatDateTime($this->expires);
0 ignored issues
show
Bug introduced by
It seems like $this->expires can also be of type null; however, parameter $dateTime of Sunrise\Http\Message\Header::formatDateTime() does only seem to accept DateTimeInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

196
            $result .= '; Expires=' . $this->formatDateTime(/** @scrutinizer ignore-type */ $this->expires);
Loading history...
197 5
            $result .= '; Max-Age=' . max($this->expires->getTimestamp() - time(), 0);
0 ignored issues
show
Bug introduced by
The method getTimestamp() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

197
            $result .= '; Max-Age=' . max($this->expires->/** @scrutinizer ignore-call */ getTimestamp() - time(), 0);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
198
        }
199
200 15
        if (isset($this->options[self::OPTION_KEY_PATH])) {
201 15
            $result .= '; Path=' . $this->options[self::OPTION_KEY_PATH];
202
        }
203
204 15
        if (isset($this->options[self::OPTION_KEY_DOMAIN])) {
205 1
            $result .= '; Domain=' . $this->options[self::OPTION_KEY_DOMAIN];
206
        }
207
208 15
        if (isset($this->options[self::OPTION_KEY_SECURE]) && $this->options[self::OPTION_KEY_SECURE]) {
209 1
            $result .= '; Secure';
210
        }
211
212 15
        if (isset($this->options[self::OPTION_KEY_HTTP_ONLY]) && $this->options[self::OPTION_KEY_HTTP_ONLY]) {
213 14
            $result .= '; HttpOnly';
214
        }
215
216 15
        if (isset($this->options[self::OPTION_KEY_SAMESITE])) {
217 15
            $result .= '; SameSite=' . $this->options[self::OPTION_KEY_SAMESITE];
218
        }
219
220 15
        return $result;
221
    }
222
223
    /**
224
     * Validates the given cookie name
225
     *
226
     * @param string $name
227
     *
228
     * @return void
229
     *
230
     * @throws InvalidHeaderValueException
231
     *         If the cookie name isn't valid.
232
     */
233 25
    private function validateCookieName(string $name): void
234
    {
235 25
        if ('' === $name) {
236 1
            throw new InvalidHeaderValueException('Cookie name cannot be empty');
237
        }
238
239
        // https://github.com/php/php-src/blob/02a5335b710aa36cd0c3108bfb9c6f7a57d40000/ext/standard/head.c#L93
240 24
        if (strpbrk($name, "=,; \t\r\n\013\014") !== false) {
241 1
            throw new InvalidHeaderValueException(sprintf(
242 1
                'The cookie name "%s" contains prohibited characters',
243 1
                $name
244 1
            ));
245
        }
246
    }
247
248
    /**
249
     * Validates the given cookie option
250
     *
251
     * @param string $validKey
252
     * @param mixed $value
253
     *
254
     * @return void
255
     *
256
     * @throws InvalidHeaderValueException
257
     *         If the cookie option isn't valid.
258
     */
259 9
    private function validateCookieOption(string $validKey, $value): void
260
    {
261 9
        if (!is_string($value)) {
262 3
            throw new InvalidHeaderValueException(sprintf(
263 3
                'The cookie option "%s" must be a string',
264 3
                $validKey
265 3
            ));
266
        }
267
268
        // https://github.com/php/php-src/blob/02a5335b710aa36cd0c3108bfb9c6f7a57d40000/ext/standard/head.c#L103
269
        // https://github.com/php/php-src/blob/02a5335b710aa36cd0c3108bfb9c6f7a57d40000/ext/standard/head.c#L108
270 6
        if (strpbrk($value, ",; \t\r\n\013\014") !== false) {
271 3
            throw new InvalidHeaderValueException(sprintf(
272 3
                'The cookie option "%s" contains prohibited characters',
273 3
                $validKey
274 3
            ));
275
        }
276
    }
277
}
278