Completed
Push — master ( 1739e8...065287 )
by Márk
02:10
created

CookieJar::getCookiePathFromRequest()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 5
nop 1
dl 0
loc 18
rs 8.8571
c 0
b 0
f 0
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()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cookie->getDiscard() of type null|boolean 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...
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
Bug introduced by
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $domain of type string|null is loosely compared to false; 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...
118
            $this->cookies = [];
119
            return;
120
        } elseif (!$path) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $path of type string|null is loosely compared to false; 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...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $name of type string|null is loosely compared to false; 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...
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();
0 ignored issues
show
Bug Best Practice introduced by
The expression $cookie->getDiscard() of type null|boolean 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
    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()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cookie->getDiscard() of type null|boolean 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...
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()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $sc->getDomain() of type string|null is loosely compared to false; 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...
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