Completed
Pull Request — main (#46)
by Franz
01:39
created

SetCookie::getSecure()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Dflydev\FigCookies;
6
7
use DateTime;
8
use DateTimeInterface;
9
use Dflydev\FigCookies\Modifier\SameSite;
10
use InvalidArgumentException;
11
12
use function array_shift;
13
use function count;
14
use function explode;
15
use function gmdate;
16
use function implode;
17
use function is_int;
18
use function is_numeric;
19
use function is_string;
20
use function sprintf;
21
use function strtolower;
22
use function strtotime;
23
use function urlencode;
24
25
class SetCookie
26
{
27
    /** @var string */
28
    private $name;
29
    /** @var string|null */
30
    private $value;
31
    /** @var int */
32
    private $expires = 0;
33
    /** @var int */
34
    private $maxAge = 0;
35
    /** @var string|null */
36
    private $path;
37
    /** @var string|null */
38
    private $domain;
39
    /** @var bool */
40
    private $secure = false;
41
    /** @var bool */
42
    private $httpOnly = false;
43
    /** @var SameSite|null */
44
    private $sameSite;
45
46 35
    private function __construct(string $name, ?string $value = null)
47
    {
48 35
        $this->name  = $name;
49 35
        $this->value = $value;
50 35
    }
51
52 19
    public function getName(): string
53
    {
54 19
        return $this->name;
55
    }
56
57 3
    public function getValue(): ?string
58
    {
59 3
        return $this->value;
60
    }
61
62 2
    public function getExpires(): int
63
    {
64 2
        return $this->expires;
65
    }
66
67
    public function getMaxAge(): int
68
    {
69
        return $this->maxAge;
70
    }
71
72
    public function getPath(): ?string
73
    {
74
        return $this->path;
75
    }
76
77
    public function getDomain(): ?string
78
    {
79
        return $this->domain;
80
    }
81
82
    public function getSecure(): bool
83
    {
84
        return $this->secure;
85
    }
86
87
    public function getHttpOnly(): bool
88
    {
89
        return $this->httpOnly;
90
    }
91
92 1
    public function getSameSite(): ?SameSite
93
    {
94 1
        return $this->sameSite;
95
    }
96
97 31
    public function withValue(?string $value = null): self
98
    {
99 31
        $clone = clone $this;
100
101 31
        $clone->value = $value;
102
103 31
        return $clone;
104
    }
105
106
    /** @param int|DateTimeInterface|string|null $expires */
107 16
    private function resolveExpires($expires = null): int
108
    {
109 16
        if ($expires === null) {
110
            return 0;
111
        }
112
113 16
        if ($expires instanceof DateTimeInterface) {
114 2
            return (int) $expires->getTimestamp();
115
        }
116
117 14
        if (is_numeric($expires)) {
118
            return (int) $expires;
119
        }
120
121 14
        $time = strtotime($expires);
122
123 14
        if (! is_int($time)) {
124 1
            throw new InvalidArgumentException(sprintf('Invalid expires "%s" provided', $expires));
125
        }
126
127 13
        return $time;
128
    }
129
130
    /** @param int|string|DateTimeInterface|null $expires */
131 16
    public function withExpires($expires = null): self
132
    {
133 16
        $expires = $this->resolveExpires($expires);
134
135 15
        $clone = clone $this;
136
137 15
        $clone->expires = $expires;
138
139 15
        return $clone;
140
    }
141
142 1
    public function rememberForever(): self
143
    {
144 1
        return $this->withExpires(new DateTime('+5 years'));
145
    }
146
147 1
    public function expire(): self
148
    {
149 1
        return $this->withExpires(new DateTime('-5 years'));
150
    }
151
152 6
    public function withMaxAge(?int $maxAge = null): self
153
    {
154 6
        $clone = clone $this;
155
156 6
        $clone->maxAge = (int) $maxAge;
157
158 6
        return $clone;
159
    }
160
161 14
    public function withPath(?string $path = null): self
162
    {
163 14
        $clone = clone $this;
164
165 14
        $clone->path = $path;
166
167 14
        return $clone;
168
    }
169
170 9
    public function withDomain(?string $domain = null): self
171
    {
172 9
        $clone = clone $this;
173
174 9
        $clone->domain = $domain;
175
176 9
        return $clone;
177
    }
178
179 12
    public function withSecure(bool $secure = true): self
180
    {
181 12
        $clone = clone $this;
182
183 12
        $clone->secure = $secure;
184
185 12
        return $clone;
186
    }
187
188 14
    public function withHttpOnly(bool $httpOnly = true): self
189
    {
190 14
        $clone = clone $this;
191
192 14
        $clone->httpOnly = $httpOnly;
193
194 14
        return $clone;
195
    }
196
197 3
    public function withSameSite(SameSite $sameSite): self
198
    {
199 3
        $clone = clone $this;
200
201 3
        $clone->sameSite = $sameSite;
202
203 3
        return $clone;
204
    }
205
206 1
    public function withoutSameSite(): self
207
    {
208 1
        $clone = clone $this;
209
210 1
        $clone->sameSite = null;
211
212 1
        return $clone;
213
    }
214
215 20
    public function __toString(): string
216
    {
217
        $cookieStringParts = [
218 20
            urlencode($this->name) . '=' . urlencode((string) $this->value),
219
        ];
220
221 20
        $cookieStringParts = $this->appendFormattedDomainPartIfSet($cookieStringParts);
222 20
        $cookieStringParts = $this->appendFormattedPathPartIfSet($cookieStringParts);
223 20
        $cookieStringParts = $this->appendFormattedExpiresPartIfSet($cookieStringParts);
224 20
        $cookieStringParts = $this->appendFormattedMaxAgePartIfSet($cookieStringParts);
225 20
        $cookieStringParts = $this->appendFormattedSecurePartIfSet($cookieStringParts);
226 20
        $cookieStringParts = $this->appendFormattedHttpOnlyPartIfSet($cookieStringParts);
227 20
        $cookieStringParts = $this->appendFormattedSameSitePartIfSet($cookieStringParts);
228
229 20
        return implode('; ', $cookieStringParts);
230
    }
231
232 10
    public static function create(string $name, ?string $value = null): self
233
    {
234 10
        return new static($name, $value);
235
    }
236
237 1
    public static function createRememberedForever(string $name, ?string $value = null): self
238
    {
239 1
        return static::create($name, $value)->rememberForever();
240
    }
241
242 1
    public static function createExpired(string $name): self
243
    {
244 1
        return static::create($name)->expire();
245
    }
246
247 32
    public static function fromSetCookieString(string $string): self
248
    {
249 32
        $rawAttributes = StringUtil::splitOnAttributeDelimiter($string);
250
251 32
        $rawAttribute = array_shift($rawAttributes);
252
253 32
        if (! is_string($rawAttribute)) {
254 1
            throw new InvalidArgumentException(sprintf(
255
                'The provided cookie string "%s" must have at least one attribute',
256 1
                $string
257
            ));
258
        }
259
260 31
        [$cookieName, $cookieValue] = StringUtil::splitCookiePair($rawAttribute);
0 ignored issues
show
Bug introduced by
The variable $cookieName does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $cookieValue does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
261
262 31
        $setCookie = new static($cookieName);
263
264 31
        if ($cookieValue !== null) {
265 31
            $setCookie = $setCookie->withValue($cookieValue);
266
        }
267
268 31
        while ($rawAttribute = array_shift($rawAttributes)) {
269 14
            $rawAttributePair = explode('=', $rawAttribute, 2);
270
271 14
            $attributeKey   = $rawAttributePair[0];
272 14
            $attributeValue = count($rawAttributePair) > 1 ? $rawAttributePair[1] : null;
273
274 14
            $attributeKey = strtolower($attributeKey);
275
276
            switch ($attributeKey) {
277 14
                case 'expires':
278 13
                    $setCookie = $setCookie->withExpires($attributeValue);
279 13
                    break;
280 14
                case 'max-age':
281 6
                    $setCookie = $setCookie->withMaxAge((int) $attributeValue);
282 6
                    break;
283 14
                case 'domain':
284 9
                    $setCookie = $setCookie->withDomain($attributeValue);
285 9
                    break;
286 14
                case 'path':
287 14
                    $setCookie = $setCookie->withPath($attributeValue);
288 14
                    break;
289 14
                case 'secure':
290 12
                    $setCookie = $setCookie->withSecure(true);
291 12
                    break;
292 14
                case 'httponly':
293 14
                    $setCookie = $setCookie->withHttpOnly(true);
294 14
                    break;
295 2
                case 'samesite':
296 2
                    $setCookie = $setCookie->withSameSite(SameSite::fromString((string) $attributeValue));
297 2
                    break;
298
            }
299
        }
300
301 31
        return $setCookie;
302
    }
303
304
    /**
305
     * @param string[] $cookieStringParts
306
     *
307
     * @return string[]
308
     */
309 20
    private function appendFormattedDomainPartIfSet(array $cookieStringParts): array
310
    {
311 20
        if ($this->domain) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->domain of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
312 9
            $cookieStringParts[] = sprintf('Domain=%s', $this->domain);
313
        }
314
315 20
        return $cookieStringParts;
316
    }
317
318
    /**
319
     * @param string[] $cookieStringParts
320
     *
321
     * @return string[]
322
     */
323 20
    private function appendFormattedPathPartIfSet(array $cookieStringParts): array
324
    {
325 20
        if ($this->path) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->path of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
326 10
            $cookieStringParts[] = sprintf('Path=%s', $this->path);
327
        }
328
329 20
        return $cookieStringParts;
330
    }
331
332
    /**
333
     * @param string[] $cookieStringParts
334
     *
335
     * @return string[]
336
     */
337 20
    private function appendFormattedExpiresPartIfSet(array $cookieStringParts): array
338
    {
339 20
        if ($this->expires) {
340 9
            $cookieStringParts[] = sprintf('Expires=%s', gmdate('D, d M Y H:i:s T', $this->expires));
341
        }
342
343 20
        return $cookieStringParts;
344
    }
345
346
    /**
347
     * @param string[] $cookieStringParts
348
     *
349
     * @return string[]
350
     */
351 20
    private function appendFormattedMaxAgePartIfSet(array $cookieStringParts): array
352
    {
353 20
        if ($this->maxAge) {
354 6
            $cookieStringParts[] = sprintf('Max-Age=%s', $this->maxAge);
355
        }
356
357 20
        return $cookieStringParts;
358
    }
359
360
    /**
361
     * @param string[] $cookieStringParts
362
     *
363
     * @return string[]
364
     */
365 20
    private function appendFormattedSecurePartIfSet(array $cookieStringParts): array
366
    {
367 20
        if ($this->secure) {
368 8
            $cookieStringParts[] = 'Secure';
369
        }
370
371 20
        return $cookieStringParts;
372
    }
373
374
    /**
375
     * @param string[] $cookieStringParts
376
     *
377
     * @return string[]
378
     */
379 20
    private function appendFormattedHttpOnlyPartIfSet(array $cookieStringParts): array
380
    {
381 20
        if ($this->httpOnly) {
382 10
            $cookieStringParts[] = 'HttpOnly';
383
        }
384
385 20
        return $cookieStringParts;
386
    }
387
388
    /**
389
     * @param string[] $cookieStringParts
390
     *
391
     * @return string[]
392
     */
393 20
    private function appendFormattedSameSitePartIfSet(array $cookieStringParts): array
394
    {
395 20
        if ($this->sameSite === null) {
396 18
            return $cookieStringParts;
397
        }
398
399 3
        $cookieStringParts[] = $this->sameSite->asString();
400
401 3
        return $cookieStringParts;
402
    }
403
}
404