SetCookie   F
last analyzed

Complexity

Total Complexity 60

Size/Duplication

Total Lines 403
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 0

Importance

Changes 0
Metric Value
dl 0
loc 403
rs 3.6
c 0
b 0
f 0
wmc 60
lcom 2
cbo 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
B fromString() 0 36 8
A __construct() 0 17 6
B __toString() 0 15 8
A toArray() 0 4 1
A getName() 0 4 1
A setName() 0 4 1
A getValue() 0 4 1
A setValue() 0 4 1
A getDomain() 0 4 1
A setDomain() 0 4 1
A getPath() 0 4 1
A setPath() 0 4 1
A getMaxAge() 0 4 1
A setMaxAge() 0 4 1
A getExpires() 0 4 1
A setExpires() 0 6 2
A getSecure() 0 4 1
A setSecure() 0 4 1
A getDiscard() 0 4 1
A setDiscard() 0 4 1
A getHttpOnly() 0 4 1
A setHttpOnly() 0 4 1
A matchesPath() 0 22 5
A matchesDomain() 0 24 5
A isExpired() 0 4 2
B validate() 0 33 6

How to fix   Complexity   

Complex Class

Complex classes like SetCookie often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SetCookie, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace GuzzleHttp\Cookie;
4
5
/**
6
 * Set-Cookie object
7
 */
8
class SetCookie
9
{
10
    /**
11
     * @var array
12
     */
13
    private static $defaults = [
14
        'Name'     => null,
15
        'Value'    => null,
16
        'Domain'   => null,
17
        'Path'     => '/',
18
        'Max-Age'  => null,
19
        'Expires'  => null,
20
        'Secure'   => false,
21
        'Discard'  => false,
22
        'HttpOnly' => false
23
    ];
24
25
    /**
26
     * @var array Cookie data
27
     */
28
    private $data;
29
30
    /**
31
     * Create a new SetCookie object from a string.
32
     *
33
     * @param string $cookie Set-Cookie header string
34
     */
35
    public static function fromString(string $cookie): self
36
    {
37
        // Create the default return array
38
        $data = self::$defaults;
39
        // Explode the cookie string using a series of semicolons
40
        $pieces = \array_filter(\array_map('trim', \explode(';', $cookie)));
41
        // The name of the cookie (first kvp) must exist and include an equal sign.
42
        if (!isset($pieces[0]) || \strpos($pieces[0], '=') === false) {
43
            return new self($data);
44
        }
45
46
        // Add the cookie pieces into the parsed data array
47
        foreach ($pieces as $part) {
48
            $cookieParts = \explode('=', $part, 2);
49
            $key = \trim($cookieParts[0]);
50
            $value = isset($cookieParts[1])
51
                ? \trim($cookieParts[1], " \n\r\t\0\x0B")
52
                : true;
53
54
            // Only check for non-cookies when cookies have been found
55
            if (!isset($data['Name'])) {
56
                $data['Name'] = $key;
57
                $data['Value'] = $value;
58
            } else {
59
                foreach (\array_keys(self::$defaults) as $search) {
60
                    if (!\strcasecmp($search, $key)) {
61
                        $data[$search] = $value;
62
                        continue 2;
63
                    }
64
                }
65
                $data[$key] = $value;
66
            }
67
        }
68
69
        return new self($data);
70
    }
71
72
    /**
73
     * @param array $data Array of cookie data provided by a Cookie parser
74
     */
75
    public function __construct(array $data = [])
76
    {
77
        /** @var array|null $replaced will be null in case of replace error */
78
        $replaced = \array_replace(self::$defaults, $data);
79
        if ($replaced === null) {
80
            throw new \InvalidArgumentException('Unable to replace the default values for the Cookie.');
81
        }
82
83
        $this->data = $replaced;
84
        // Extract the Expires value and turn it into a UNIX timestamp if needed
85
        if (!$this->getExpires() && $this->getMaxAge()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getMaxAge() of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
86
            // Calculate the Expires date
87
            $this->setExpires(\time() + $this->getMaxAge());
88
        } elseif (null !== ($expires = $this->getExpires()) && !\is_numeric($expires)) {
89
            $this->setExpires($expires);
90
        }
91
    }
92
93
    public function __toString()
94
    {
95
        $str = $this->data['Name'] . '=' . $this->data['Value'] . '; ';
96
        foreach ($this->data as $k => $v) {
97
            if ($k !== 'Name' && $k !== 'Value' && $v !== null && $v !== false) {
98
                if ($k === 'Expires') {
99
                    $str .= 'Expires=' . \gmdate('D, d M Y H:i:s \G\M\T', $v) . '; ';
100
                } else {
101
                    $str .= ($v === true ? $k : "{$k}={$v}") . '; ';
102
                }
103
            }
104
        }
105
106
        return \rtrim($str, '; ');
107
    }
108
109
    public function toArray(): array
110
    {
111
        return $this->data;
112
    }
113
114
    /**
115
     * Get the cookie name.
116
     *
117
     * @return string
118
     */
119
    public function getName()
120
    {
121
        return $this->data['Name'];
122
    }
123
124
    /**
125
     * Set the cookie name.
126
     *
127
     * @param string $name Cookie name
128
     */
129
    public function setName($name): void
130
    {
131
        $this->data['Name'] = $name;
132
    }
133
134
    /**
135
     * Get the cookie value.
136
     *
137
     * @return string|null
138
     */
139
    public function getValue()
140
    {
141
        return $this->data['Value'];
142
    }
143
144
    /**
145
     * Set the cookie value.
146
     *
147
     * @param string $value Cookie value
148
     */
149
    public function setValue($value): void
150
    {
151
        $this->data['Value'] = $value;
152
    }
153
154
    /**
155
     * Get the domain.
156
     *
157
     * @return string|null
158
     */
159
    public function getDomain()
160
    {
161
        return $this->data['Domain'];
162
    }
163
164
    /**
165
     * Set the domain of the cookie.
166
     *
167
     * @param string $domain
168
     */
169
    public function setDomain($domain): void
170
    {
171
        $this->data['Domain'] = $domain;
172
    }
173
174
    /**
175
     * Get the path.
176
     *
177
     * @return string
178
     */
179
    public function getPath()
180
    {
181
        return $this->data['Path'];
182
    }
183
184
    /**
185
     * Set the path of the cookie.
186
     *
187
     * @param string $path Path of the cookie
188
     */
189
    public function setPath($path): void
190
    {
191
        $this->data['Path'] = $path;
192
    }
193
194
    /**
195
     * Maximum lifetime of the cookie in seconds.
196
     *
197
     * @return int|null
198
     */
199
    public function getMaxAge()
200
    {
201
        return $this->data['Max-Age'];
202
    }
203
204
    /**
205
     * Set the max-age of the cookie.
206
     *
207
     * @param int $maxAge Max age of the cookie in seconds
208
     */
209
    public function setMaxAge($maxAge): void
210
    {
211
        $this->data['Max-Age'] = $maxAge;
212
    }
213
214
    /**
215
     * The UNIX timestamp when the cookie Expires.
216
     *
217
     * @return string|int|null
218
     */
219
    public function getExpires()
220
    {
221
        return $this->data['Expires'];
222
    }
223
224
    /**
225
     * Set the unix timestamp for which the cookie will expire.
226
     *
227
     * @param int|string $timestamp Unix timestamp or any English textual datetime description.
228
     */
229
    public function setExpires($timestamp): void
230
    {
231
        $this->data['Expires'] = \is_numeric($timestamp)
232
            ? (int) $timestamp
233
            : \strtotime($timestamp);
234
    }
235
236
    /**
237
     * Get whether or not this is a secure cookie.
238
     *
239
     * @return bool|null
240
     */
241
    public function getSecure()
242
    {
243
        return $this->data['Secure'];
244
    }
245
246
    /**
247
     * Set whether or not the cookie is secure.
248
     *
249
     * @param bool $secure Set to true or false if secure
250
     */
251
    public function setSecure($secure): void
252
    {
253
        $this->data['Secure'] = $secure;
254
    }
255
256
    /**
257
     * Get whether or not this is a session cookie.
258
     *
259
     * @return bool|null
260
     */
261
    public function getDiscard()
262
    {
263
        return $this->data['Discard'];
264
    }
265
266
    /**
267
     * Set whether or not this is a session cookie.
268
     *
269
     * @param bool $discard Set to true or false if this is a session cookie
270
     */
271
    public function setDiscard($discard): void
272
    {
273
        $this->data['Discard'] = $discard;
274
    }
275
276
    /**
277
     * Get whether or not this is an HTTP only cookie.
278
     *
279
     * @return bool
280
     */
281
    public function getHttpOnly()
282
    {
283
        return $this->data['HttpOnly'];
284
    }
285
286
    /**
287
     * Set whether or not this is an HTTP only cookie.
288
     *
289
     * @param bool $httpOnly Set to true or false if this is HTTP only
290
     */
291
    public function setHttpOnly($httpOnly): void
292
    {
293
        $this->data['HttpOnly'] = $httpOnly;
294
    }
295
296
    /**
297
     * Check if the cookie matches a path value.
298
     *
299
     * A request-path path-matches a given cookie-path if at least one of
300
     * the following conditions holds:
301
     *
302
     * - The cookie-path and the request-path are identical.
303
     * - The cookie-path is a prefix of the request-path, and the last
304
     *   character of the cookie-path is %x2F ("/").
305
     * - The cookie-path is a prefix of the request-path, and the first
306
     *   character of the request-path that is not included in the cookie-
307
     *   path is a %x2F ("/") character.
308
     *
309
     * @param string $requestPath Path to check against
310
     */
311
    public function matchesPath(string $requestPath): bool
312
    {
313
        $cookiePath = $this->getPath();
314
315
        // Match on exact matches or when path is the default empty "/"
316
        if ($cookiePath === '/' || $cookiePath == $requestPath) {
317
            return true;
318
        }
319
320
        // Ensure that the cookie-path is a prefix of the request path.
321
        if (0 !== \strpos($requestPath, $cookiePath)) {
322
            return false;
323
        }
324
325
        // Match if the last character of the cookie-path is "/"
326
        if (\substr($cookiePath, -1, 1) === '/') {
327
            return true;
328
        }
329
330
        // Match if the first character not included in cookie path is "/"
331
        return \substr($requestPath, \strlen($cookiePath), 1) === '/';
332
    }
333
334
    /**
335
     * Check if the cookie matches a domain value.
336
     *
337
     * @param string $domain Domain to check against
338
     */
339
    public function matchesDomain(string $domain): bool
340
    {
341
        $cookieDomain = $this->getDomain();
342
        if (null === $cookieDomain) {
343
            return true;
344
        }
345
346
        // Remove the leading '.' as per spec in RFC 6265.
347
        // https://tools.ietf.org/html/rfc6265#section-5.2.3
348
        $cookieDomain = \ltrim($cookieDomain, '.');
349
350
        // Domain not set or exact match.
351
        if (!$cookieDomain || !\strcasecmp($domain, $cookieDomain)) {
352
            return true;
353
        }
354
355
        // Matching the subdomain according to RFC 6265.
356
        // https://tools.ietf.org/html/rfc6265#section-5.1.3
357
        if (\filter_var($domain, \FILTER_VALIDATE_IP)) {
358
            return false;
359
        }
360
361
        return (bool) \preg_match('/\.' . \preg_quote($cookieDomain, '/') . '$/', $domain);
362
    }
363
364
    /**
365
     * Check if the cookie is expired.
366
     */
367
    public function isExpired(): bool
368
    {
369
        return $this->getExpires() !== null && \time() > $this->getExpires();
370
    }
371
372
    /**
373
     * Check if the cookie is valid according to RFC 6265.
374
     *
375
     * @return bool|string Returns true if valid or an error message if invalid
376
     */
377
    public function validate()
378
    {
379
        $name = $this->getName();
380
        if ($name === '') {
381
            return 'The cookie name must not be empty';
382
        }
383
384
        // Check if any of the invalid characters are present in the cookie name
385
        if (\preg_match(
386
            '/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5c\x7b\x7d\x7f]/',
387
            $name
388
        )) {
389
            return 'Cookie name must not contain invalid characters: ASCII '
390
                . 'Control characters (0-31;127), space, tab and the '
391
                . 'following characters: ()<>@,;:\"/?={}';
392
        }
393
394
        // Value must not be null. 0 and empty string are valid. Empty strings
395
        // are technically against RFC 6265, but known to happen in the wild.
396
        $value = $this->getValue();
397
        if ($value === null) {
398
            return 'The cookie value must not be empty';
399
        }
400
401
        // Domains must not be empty, but can be 0. "0" is not a valid internet
402
        // domain, but may be used as server name in a private network.
403
        $domain = $this->getDomain();
404
        if ($domain === null || $domain === '') {
405
            return 'The cookie domain must not be empty';
406
        }
407
408
        return true;
409
    }
410
}
411