Completed
Branch 09branch (946dde)
by Anton
05:16
created

CookieManager::packCookies()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 10
nc 4
nop 2
dl 0
loc 19
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Spiral\Http\Cookies;
10
11
use Psr\Http\Message\ResponseInterface as Response;
12
use Psr\Http\Message\ServerRequestInterface as Request;
13
use Spiral\Core\Component;
14
use Spiral\Core\ContainerInterface;
15
use Spiral\Encrypter\EncrypterInterface;
16
use Spiral\Encrypter\Exceptions\DecryptException;
17
use Spiral\Http\Configs\HttpConfig;
18
use Spiral\Http\MiddlewareInterface;
19
20
/**
21
 * Middleware used to encrypt and decrypt cookies. Creates container scope for a cookie bucket.
22
 *
23
 * Attention, EncrypterInterface is requested from container on demand.
24
 */
25
class CookieManager extends Component implements MiddlewareInterface
26
{
27
    /**
28
     * @var EncrypterInterface
29
     */
30
    private $encrypter = null;
31
32
    /**
33
     * @var HttpConfig
34
     */
35
    private $httpConfig = null;
36
37
    /**
38
     * @invisible
39
     * @var ContainerInterface
40
     */
41
    protected $container = null;
42
43
    /**
44
     * @param HttpConfig         $httpConfig
45
     * @param ContainerInterface $container Lazy access to encrypter.
46
     */
47
    public function __construct(HttpConfig $httpConfig, ContainerInterface $container)
48
    {
49
        $this->httpConfig = $httpConfig;
50
        $this->container = $container;
51
    }
52
53
    /**
54
     * {@inheritdoc}
55
     */
56 View Code Duplication
    public function __invoke(Request $request, Response $response, callable $next = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
57
    {
58
        //Aggregates all user cookies
59
        $queue = new CookieQueue($this->httpConfig, $request);
60
61
        //Opening cookie scope
62
        $scope = $this->container->replace(CookieQueue::class, $queue);
0 ignored issues
show
Documentation introduced by
$queue is of type object<Spiral\Http\Cookies\CookieQueue>, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
63
        try {
64
            /**
65
             * Debug: middleware creates scope for [CookieQueue].
66
             */
67
            $response = $next(
68
                $this->unpackCookies($request)->withAttribute(CookieQueue::ATTRIBUTE, $queue),
69
                $response
70
            );
71
72
            //New cookies
73
            return $this->packCookies($response, $queue);
74
        } finally {
75
            $this->container->restore($scope);
76
        }
77
    }
78
79
    /**
80
     * Unpack incoming cookies and decrypt their content.
81
     *
82
     * @param Request $request
83
     *
84
     * @return Request
85
     */
86
    protected function unpackCookies(Request $request): Request
87
    {
88
        $cookies = $request->getCookieParams();
89
90
        foreach ($cookies as $name => $cookie) {
91
            if (!$this->isProtected($name)) {
92
                //Nothing to protect
93
                continue;
94
            }
95
96
            $cookies[$name] = $this->decodeCookie($cookie);
97
        }
98
99
        return $request->withCookieParams($cookies);
100
    }
101
102
    /**
103
     * Pack outcoming cookies with encrypted value.
104
     *
105
     * @param Response    $response
106
     * @param CookieQueue $queue
107
     *
108
     * @return Response
109
     *
110
     * @throws \Spiral\Encrypter\Exceptions\EncryptException
111
     */
112
    protected function packCookies(Response $response, CookieQueue $queue): Response
113
    {
114
        if (empty($queue->getScheduled())) {
115
            return $response;
116
        }
117
118
        $cookies = $response->getHeader('Set-Cookie');
119
120
        foreach ($queue->getScheduled() as $cookie) {
121
            if (!$this->isProtected($cookie->getName()) || empty($cookie->getValue())) {
122
                $cookies[] = $cookie->createHeader();
123
                continue;
124
            }
125
126
            $cookies[] = $this->encodeCookie($cookie)->createHeader();
127
        }
128
129
        return $response->withHeader('Set-Cookie', $cookies);
130
    }
131
132
    /**
133
     * Check if cookie has to be protected.
134
     *
135
     * @param string $cookie
136
     *
137
     * @return bool
138
     */
139
    protected function isProtected(string $cookie): bool
140
    {
141
        if (in_array($cookie, $this->httpConfig->excludedCookies())) {
142
            //Excluded
143
            return false;
144
        }
145
146
        return $this->httpConfig->cookieProtection() != HttpConfig::COOKIE_UNPROTECTED;
147
    }
148
149
    /**
150
     * @param string|array $cookie
151
     *
152
     * @return array|mixed|null
153
     */
154
    private function decodeCookie($cookie)
155
    {
156
        if ($this->httpConfig->cookieProtection() == HttpConfig::COOKIE_ENCRYPT) {
157
            try {
158
                if (is_array($cookie)) {
159
                    return array_map([$this, 'decodeCookie'], $cookie);
160
                }
161
162
                return $this->getEncrypter()->decrypt($cookie);
163
            } catch (DecryptException $exception) {
164
                return null;
165
            }
166
        }
167
168
        //HMAC
169
        $hmac = substr($cookie, -1 * HttpConfig::MAC_LENGTH);
170
        $value = substr($cookie, 0, strlen($cookie) - strlen($hmac));
171
172
        if ($this->hmacSign($value) != $hmac) {
173
            return null;
174
        }
175
176
        return $value;
177
    }
178
179
    /**
180
     * Get or create encrypter instance.
181
     *
182
     * @return EncrypterInterface
183
     */
184
    protected function getEncrypter()
185
    {
186
        if (empty($this->encrypter)) {
187
            //On demand creation (speed up app when no cookies were set)
188
            $this->encrypter = $this->container->get(EncrypterInterface::class);
189
        }
190
191
        return $this->encrypter;
192
    }
193
194
    /**
195
     * @param Cookie $cookie
196
     *
197
     * @return Cookie
198
     */
199
    private function encodeCookie(Cookie $cookie): Cookie
200
    {
201
        if ($this->httpConfig->cookieProtection() == HttpConfig::COOKIE_ENCRYPT) {
202
            return $cookie->withValue(
203
                $this->getEncrypter()->encrypt($cookie->getValue())
204
            );
205
        }
206
207
        //VALUE.HMAC
208
        return $cookie->withValue($cookie->getValue() . $this->hmacSign($cookie->getValue()));
209
    }
210
211
    /**
212
     * Sign string.
213
     *
214
     * @param string|null $value
215
     *
216
     * @return string
217
     */
218
    private function hmacSign($value): string
219
    {
220
        return hash_hmac(
221
            HttpConfig::HMAC_ALGORITHM,
222
            $value,
223
            $this->getEncrypter()->getKey()
224
        );
225
    }
226
}
227