Completed
Push — master ( dfd01d...9e28b5 )
by Márk
14s
created

src/Cookie/CookieJar.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
namespace GuzzleHttp\Cookie;
3
4
use Psr\Http\Message\RequestInterface;
5
use Psr\Http\Message\ResponseInterface;
6
7
/**
8
 * Cookie jar that stores cookies as an array
9
 */
10
class 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 null name
98
        if($name === null) {
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
108
    public function toArray()
109
    {
110
        return array_map(function (SetCookie $cookie) {
111
            return $cookie->toArray();
112
        }, $this->getIterator()->getArrayCopy());
0 ignored issues
show
It seems like you code against a concrete implementation and not the interface Traversable as the method getArrayCopy() does only exist in the following implementations of said interface: ArrayIterator, ArrayObject, DoctrineTest\Instantiato...tAsset\ArrayObjectAsset, DoctrineTest\Instantiato...lizableArrayObjectAsset, DoctrineTest\Instantiato...ceptionArrayObjectAsset, DoctrineTest\Instantiato...sset\WakeUpNoticesAsset, Issue523, RecursiveArrayIterator.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

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