CookieJar::clear()   B
last analyzed

Complexity

Conditions 7
Paths 4

Size

Total Lines 27
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 21
c 1
b 1
f 0
dl 0
loc 27
rs 8.6506
cc 7
nc 4
nop 3
1
<?php
2
3
namespace GuzzleHttp\Cookie;
4
5
use Psr\Http\Message\RequestInterface;
6
use Psr\Http\Message\ResponseInterface;
7
8
/**
9
 * Cookie jar that stores cookies as an array
10
 */
11
class CookieJar implements CookieJarInterface
12
{
13
    /**
14
     * @var SetCookie[] Loaded cookie data
15
     */
16
    private $cookies = [];
17
18
    /**
19
     * @var bool
20
     */
21
    private $strictMode;
22
23
    /**
24
     * @param bool  $strictMode  Set to true to throw exceptions when invalid
25
     *                           cookies are added to the cookie jar.
26
     * @param array $cookieArray Array of SetCookie objects or a hash of
27
     *                           arrays that can be used with the SetCookie
28
     *                           constructor
29
     */
30
    public function __construct(bool $strictMode = false, array $cookieArray = [])
31
    {
32
        $this->strictMode = $strictMode;
33
34
        foreach ($cookieArray as $cookie) {
35
            if (!($cookie instanceof SetCookie)) {
36
                $cookie = new SetCookie($cookie);
37
            }
38
            $this->setCookie($cookie);
39
        }
40
    }
41
42
    /**
43
     * Create a new Cookie jar from an associative array and domain.
44
     *
45
     * @param array  $cookies Cookies to create the jar from
46
     * @param string $domain  Domain to set the cookies to
47
     */
48
    public static function fromArray(array $cookies, string $domain): self
49
    {
50
        $cookieJar = new self();
51
        foreach ($cookies as $name => $value) {
52
            $cookieJar->setCookie(new SetCookie([
53
                'Domain'  => $domain,
54
                'Name'    => $name,
55
                'Value'   => $value,
56
                'Discard' => true
57
            ]));
58
        }
59
60
        return $cookieJar;
61
    }
62
63
    /**
64
     * Evaluate if this cookie should be persisted to storage
65
     * that survives between requests.
66
     *
67
     * @param SetCookie $cookie              Being evaluated.
68
     * @param bool      $allowSessionCookies If we should persist session cookies
69
     */
70
    public static function shouldPersist(SetCookie $cookie, bool $allowSessionCookies = false): bool
71
    {
72
        if ($cookie->getExpires() || $allowSessionCookies) {
73
            if (!$cookie->getDiscard()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cookie->getDiscard() of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
74
                return true;
75
            }
76
        }
77
78
        return false;
79
    }
80
81
    /**
82
     * Finds and returns the cookie based on the name
83
     *
84
     * @param string $name cookie name to search for
85
     *
86
     * @return SetCookie|null cookie that was found or null if not found
87
     */
88
    public function getCookieByName(string $name): ?SetCookie
89
    {
90
        foreach ($this->cookies as $cookie) {
91
            if ($cookie->getName() !== null && \strcasecmp($cookie->getName(), $name) === 0) {
92
                return $cookie;
93
            }
94
        }
95
96
        return null;
97
    }
98
99
    /**
100
     * @inheritDoc
101
     */
102
    public function toArray(): array
103
    {
104
        return \array_map(static function (SetCookie $cookie): array {
105
            return $cookie->toArray();
106
        }, $this->getIterator()->getArrayCopy());
107
    }
108
109
    /**
110
     * @inheritDoc
111
     */
112
    public function clear(?string $domain = null, ?string $path = null, ?string $name = null): void
113
    {
114
        if (!$domain) {
115
            $this->cookies = [];
116
            return;
117
        } elseif (!$path) {
118
            $this->cookies = \array_filter(
119
                $this->cookies,
120
                static function (SetCookie $cookie) use ($domain): bool {
121
                    return !$cookie->matchesDomain($domain);
122
                }
123
            );
124
        } elseif (!$name) {
125
            $this->cookies = \array_filter(
126
                $this->cookies,
127
                static function (SetCookie $cookie) use ($path, $domain): bool {
128
                    return !($cookie->matchesPath($path) &&
129
                        $cookie->matchesDomain($domain));
130
                }
131
            );
132
        } else {
133
            $this->cookies = \array_filter(
134
                $this->cookies,
135
                static function (SetCookie $cookie) use ($path, $domain, $name) {
136
                    return !($cookie->getName() == $name &&
137
                        $cookie->matchesPath($path) &&
138
                        $cookie->matchesDomain($domain));
139
                }
140
            );
141
        }
142
    }
143
144
    /**
145
     * @inheritDoc
146
     */
147
    public function clearSessionCookies(): void
148
    {
149
        $this->cookies = \array_filter(
150
            $this->cookies,
151
            static function (SetCookie $cookie): bool {
152
                return !$cookie->getDiscard() && $cookie->getExpires();
0 ignored issues
show
Bug Best Practice introduced by
The expression $cookie->getDiscard() of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
153
            }
154
        );
155
    }
156
157
    /**
158
     * @inheritDoc
159
     */
160
    public function setCookie(SetCookie $cookie): bool
161
    {
162
        // If the name string is empty (but not 0), ignore the set-cookie
163
        // string entirely.
164
        $name = $cookie->getName();
165
        if (!$name && $name !== '0') {
166
            return false;
167
        }
168
169
        // Only allow cookies with set and valid domain, name, value
170
        $result = $cookie->validate();
171
        if ($result !== true) {
172
            if ($this->strictMode) {
173
                throw new \RuntimeException('Invalid cookie: ' . $result);
174
            }
175
            $this->removeCookieIfEmpty($cookie);
176
            return false;
177
        }
178
179
        // Resolve conflicts with previously set cookies
180
        foreach ($this->cookies as $i => $c) {
181
182
            // Two cookies are identical, when their path, and domain are
183
            // identical.
184
            if ($c->getPath() != $cookie->getPath() ||
185
                $c->getDomain() != $cookie->getDomain() ||
186
                $c->getName() != $cookie->getName()
187
            ) {
188
                continue;
189
            }
190
191
            // The previously set cookie is a discard cookie and this one is
192
            // not so allow the new cookie to be set
193
            if (!$cookie->getDiscard() && $c->getDiscard()) {
194
                unset($this->cookies[$i]);
195
                continue;
196
            }
197
198
            // If the new cookie's expiration is further into the future, then
199
            // replace the old cookie
200
            if ($cookie->getExpires() > $c->getExpires()) {
201
                unset($this->cookies[$i]);
202
                continue;
203
            }
204
205
            // If the value has changed, we better change it
206
            if ($cookie->getValue() !== $c->getValue()) {
207
                unset($this->cookies[$i]);
208
                continue;
209
            }
210
211
            // The cookie exists, so no need to continue
212
            return false;
213
        }
214
215
        $this->cookies[] = $cookie;
216
217
        return true;
218
    }
219
220
    public function count(): int
221
    {
222
        return \count($this->cookies);
223
    }
224
225
    /**
226
     * @return \ArrayIterator<int, SetCookie>
227
     */
228
    public function getIterator(): \ArrayIterator
229
    {
230
        return new \ArrayIterator(\array_values($this->cookies));
231
    }
232
233
    public function extractCookies(RequestInterface $request, ResponseInterface $response): void
234
    {
235
        if ($cookieHeader = $response->getHeader('Set-Cookie')) {
236
            foreach ($cookieHeader as $cookie) {
237
                $sc = SetCookie::fromString($cookie);
238
                if (!$sc->getDomain()) {
239
                    $sc->setDomain($request->getUri()->getHost());
240
                }
241
                if (0 !== \strpos($sc->getPath(), '/')) {
242
                    $sc->setPath($this->getCookiePathFromRequest($request));
243
                }
244
                if (!$sc->matchesDomain($request->getUri()->getHost())) {
245
                    continue;
246
                }
247
                // Note: At this point `$sc->getDomain()` being a public suffix should
248
                // be rejected, but we don't want to pull in the full PSL dependency.
249
                $this->setCookie($sc);
250
            }
251
        }
252
    }
253
254
    /**
255
     * Computes cookie path following RFC 6265 section 5.1.4
256
     *
257
     * @link https://tools.ietf.org/html/rfc6265#section-5.1.4
258
     */
259
    private function getCookiePathFromRequest(RequestInterface $request): string
260
    {
261
        $uriPath = $request->getUri()->getPath();
262
        if ('' === $uriPath) {
263
            return '/';
264
        }
265
        if (0 !== \strpos($uriPath, '/')) {
266
            return '/';
267
        }
268
        if ('/' === $uriPath) {
269
            return '/';
270
        }
271
        $lastSlashPos = \strrpos($uriPath, '/');
272
        if (0 === $lastSlashPos || false === $lastSlashPos) {
273
            return '/';
274
        }
275
276
        return \substr($uriPath, 0, $lastSlashPos);
277
    }
278
279
    public function withCookieHeader(RequestInterface $request): RequestInterface
280
    {
281
        $values = [];
282
        $uri = $request->getUri();
283
        $scheme = $uri->getScheme();
284
        $host = $uri->getHost();
285
        $path = $uri->getPath() ?: '/';
286
287
        foreach ($this->cookies as $cookie) {
288
            if ($cookie->matchesPath($path) &&
289
                $cookie->matchesDomain($host) &&
290
                !$cookie->isExpired() &&
291
                (!$cookie->getSecure() || $scheme === 'https')
292
            ) {
293
                $values[] = $cookie->getName() . '='
294
                    . $cookie->getValue();
295
            }
296
        }
297
298
        return $values
299
            ? $request->withHeader('Cookie', \implode('; ', $values))
300
            : $request;
301
    }
302
303
    /**
304
     * If a cookie already exists and the server asks to set it again with a
305
     * null value, the cookie must be deleted.
306
     */
307
    private function removeCookieIfEmpty(SetCookie $cookie): void
308
    {
309
        $cookieValue = $cookie->getValue();
310
        if ($cookieValue === null || $cookieValue === '') {
311
            $this->clear(
312
                $cookie->getDomain(),
313
                $cookie->getPath(),
314
                $cookie->getName()
315
            );
316
        }
317
    }
318
}
319