1<?php
2namespace GuzzleHttp\Cookie;
3
4use Psr\Http\Message\RequestInterface;
5use Psr\Http\Message\ResponseInterface;
6
7/**
8 * Cookie jar that stores cookies as an array
9 */
10class CookieJar implements CookieJarInterface
11{
12    /** @var SetCookie[] Loaded cookie data */
13    private $cookies = [];
14
15    /** @var bool */
16    private $strictMode;
17
18    /**
19     * @param bool $strictMode   Set to true to throw exceptions when invalid
20     *                           cookies are added to the cookie jar.
21     * @param array $cookieArray Array of SetCookie objects or a hash of
22     *                           arrays that can be used with the SetCookie
23     *                           constructor
24     */
25    public function __construct($strictMode = false, $cookieArray = [])
26    {
27        $this->strictMode = $strictMode;
28
29        foreach ($cookieArray as $cookie) {
30            if (!($cookie instanceof SetCookie)) {
31                $cookie = new SetCookie($cookie);
32            }
33            $this->setCookie($cookie);
34        }
35    }
36
37    /**
38     * Create a new Cookie jar from an associative array and domain.
39     *
40     * @param array  $cookies Cookies to create the jar from
41     * @param string $domain  Domain to set the cookies to
42     *
43     * @return self
44     */
45    public static function fromArray(array $cookies, $domain)
46    {
47        $cookieJar = new self();
48        foreach ($cookies as $name => $value) {
49            $cookieJar->setCookie(new SetCookie([
50                'Domain'  => $domain,
51                'Name'    => $name,
52                'Value'   => $value,
53                'Discard' => true
54            ]));
55        }
56
57        return $cookieJar;
58    }
59
60    /**
61     * @deprecated
62     */
63    public static function getCookieValue($value)
64    {
65        return $value;
66    }
67
68    /**
69     * Evaluate if this cookie should be persisted to storage
70     * that survives between requests.
71     *
72     * @param SetCookie $cookie Being evaluated.
73     * @param bool $allowSessionCookies If we should persist session cookies
74     * @return bool
75     */
76    public static function shouldPersist(
77        SetCookie $cookie,
78        $allowSessionCookies = false
79    ) {
80        if ($cookie->getExpires() || $allowSessionCookies) {
81            if (!$cookie->getDiscard()) {
82                return true;
83            }
84        }
85
86        return false;
87    }
88
89    /**
90     * Finds and returns the cookie based on the name
91     *
92     * @param string $name cookie name to search for
93     * @return SetCookie|null cookie that was found or null if not found
94     */
95    public function getCookieByName($name)
96    {
97        // don't allow a non string name
98        if ($name === null || !is_scalar($name)) {
99            return null;
100        }
101        foreach ($this->cookies as $cookie) {
102            if ($cookie->getName() !== null && strcasecmp($cookie->getName(), $name) === 0) {
103                return $cookie;
104            }
105        }
106
107        return null;
108    }
109
110    public function toArray()
111    {
112        return array_map(function (SetCookie $cookie) {
113            return $cookie->toArray();
114        }, $this->getIterator()->getArrayCopy());
115    }
116
117    public function clear($domain = null, $path = null, $name = null)
118    {
119        if (!$domain) {
120            $this->cookies = [];
121            return;
122        } elseif (!$path) {
123            $this->cookies = array_filter(
124                $this->cookies,
125                function (SetCookie $cookie) use ($domain) {
126                    return !$cookie->matchesDomain($domain);
127                }
128            );
129        } elseif (!$name) {
130            $this->cookies = array_filter(
131                $this->cookies,
132                function (SetCookie $cookie) use ($path, $domain) {
133                    return !($cookie->matchesPath($path) &&
134                        $cookie->matchesDomain($domain));
135                }
136            );
137        } else {
138            $this->cookies = array_filter(
139                $this->cookies,
140                function (SetCookie $cookie) use ($path, $domain, $name) {
141                    return !($cookie->getName() == $name &&
142                        $cookie->matchesPath($path) &&
143                        $cookie->matchesDomain($domain));
144                }
145            );
146        }
147    }
148
149    public function clearSessionCookies()
150    {
151        $this->cookies = array_filter(
152            $this->cookies,
153            function (SetCookie $cookie) {
154                return !$cookie->getDiscard() && $cookie->getExpires();
155            }
156        );
157    }
158
159    public function setCookie(SetCookie $cookie)
160    {
161        // If the name string is empty (but not 0), ignore the set-cookie
162        // string entirely.
163        $name = $cookie->getName();
164        if (!$name && $name !== '0') {
165            return false;
166        }
167
168        // Only allow cookies with set and valid domain, name, value
169        $result = $cookie->validate();
170        if ($result !== true) {
171            if ($this->strictMode) {
172                throw new \RuntimeException('Invalid cookie: ' . $result);
173            } else {
174                $this->removeCookieIfEmpty($cookie);
175                return false;
176            }
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()
221    {
222        return count($this->cookies);
223    }
224
225    public function getIterator()
226    {
227        return new \ArrayIterator(array_values($this->cookies));
228    }
229
230    public function extractCookies(
231        RequestInterface $request,
232        ResponseInterface $response
233    ) {
234        if ($cookieHeader = $response->getHeader('Set-Cookie')) {
235            foreach ($cookieHeader as $cookie) {
236                $sc = SetCookie::fromString($cookie);
237                if (!$sc->getDomain()) {
238                    $sc->setDomain($request->getUri()->getHost());
239                }
240                if (0 !== strpos($sc->getPath(), '/')) {
241                    $sc->setPath($this->getCookiePathFromRequest($request));
242                }
243                $this->setCookie($sc);
244            }
245        }
246    }
247
248    /**
249     * Computes cookie path following RFC 6265 section 5.1.4
250     *
251     * @link https://tools.ietf.org/html/rfc6265#section-5.1.4
252     *
253     * @param RequestInterface $request
254     * @return string
255     */
256    private function getCookiePathFromRequest(RequestInterface $request)
257    {
258        $uriPath = $request->getUri()->getPath();
259        if (''  === $uriPath) {
260            return '/';
261        }
262        if (0 !== strpos($uriPath, '/')) {
263            return '/';
264        }
265        if ('/' === $uriPath) {
266            return '/';
267        }
268        if (0 === $lastSlashPos = strrpos($uriPath, '/')) {
269            return '/';
270        }
271
272        return substr($uriPath, 0, $lastSlashPos);
273    }
274
275    public function withCookieHeader(RequestInterface $request)
276    {
277        $values = [];
278        $uri = $request->getUri();
279        $scheme = $uri->getScheme();
280        $host = $uri->getHost();
281        $path = $uri->getPath() ?: '/';
282
283        foreach ($this->cookies as $cookie) {
284            if ($cookie->matchesPath($path) &&
285                $cookie->matchesDomain($host) &&
286                !$cookie->isExpired() &&
287                (!$cookie->getSecure() || $scheme === 'https')
288            ) {
289                $values[] = $cookie->getName() . '='
290                    . $cookie->getValue();
291            }
292        }
293
294        return $values
295            ? $request->withHeader('Cookie', implode('; ', $values))
296            : $request;
297    }
298
299    /**
300     * If a cookie already exists and the server asks to set it again with a
301     * null value, the cookie must be deleted.
302     *
303     * @param SetCookie $cookie
304     */
305    private function removeCookieIfEmpty(SetCookie $cookie)
306    {
307        $cookieValue = $cookie->getValue();
308        if ($cookieValue === null || $cookieValue === '') {
309            $this->clear(
310                $cookie->getDomain(),
311                $cookie->getPath(),
312                $cookie->getName()
313            );
314        }
315    }
316}
317