1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Bridge\PsrHttpMessage\Factory;
13
14use Psr\Http\Message\ResponseInterface;
15use Psr\Http\Message\ServerRequestInterface;
16use Psr\Http\Message\StreamInterface;
17use Psr\Http\Message\UploadedFileInterface;
18use Psr\Http\Message\UriInterface;
19use Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface;
20use Symfony\Component\HttpFoundation\Cookie;
21use Symfony\Component\HttpFoundation\Request;
22use Symfony\Component\HttpFoundation\Response;
23use Symfony\Component\HttpFoundation\StreamedResponse;
24
25/**
26 * {@inheritdoc}
27 *
28 * @author Kévin Dunglas <dunglas@gmail.com>
29 */
30class HttpFoundationFactory implements HttpFoundationFactoryInterface
31{
32    /**
33     * @var int The maximum output buffering size for each iteration when sending the response
34     */
35    private $responseBufferMaxLength;
36
37    public function __construct(int $responseBufferMaxLength = 16372)
38    {
39        $this->responseBufferMaxLength = $responseBufferMaxLength;
40    }
41
42    /**
43     * {@inheritdoc}
44     */
45    public function createRequest(ServerRequestInterface $psrRequest, bool $streamed = false)
46    {
47        $server = [];
48        $uri = $psrRequest->getUri();
49
50        if ($uri instanceof UriInterface) {
51            $server['SERVER_NAME'] = $uri->getHost();
52            $server['SERVER_PORT'] = $uri->getPort() ?: ('https' === $uri->getScheme() ? 443 : 80);
53            $server['REQUEST_URI'] = $uri->getPath();
54            $server['QUERY_STRING'] = $uri->getQuery();
55
56            if ('' !== $server['QUERY_STRING']) {
57                $server['REQUEST_URI'] .= '?'.$server['QUERY_STRING'];
58            }
59
60            if ('https' === $uri->getScheme()) {
61                $server['HTTPS'] = 'on';
62            }
63        }
64
65        $server['REQUEST_METHOD'] = $psrRequest->getMethod();
66
67        $server = array_replace($psrRequest->getServerParams(), $server);
68
69        $parsedBody = $psrRequest->getParsedBody();
70        $parsedBody = \is_array($parsedBody) ? $parsedBody : [];
71
72        $request = new Request(
73            $psrRequest->getQueryParams(),
74            $parsedBody,
75            $psrRequest->getAttributes(),
76            $psrRequest->getCookieParams(),
77            $this->getFiles($psrRequest->getUploadedFiles()),
78            $server,
79            $streamed ? $psrRequest->getBody()->detach() : $psrRequest->getBody()->__toString()
80        );
81        $request->headers->add($psrRequest->getHeaders());
82
83        return $request;
84    }
85
86    /**
87     * Converts to the input array to $_FILES structure.
88     */
89    private function getFiles(array $uploadedFiles): array
90    {
91        $files = [];
92
93        foreach ($uploadedFiles as $key => $value) {
94            if ($value instanceof UploadedFileInterface) {
95                $files[$key] = $this->createUploadedFile($value);
96            } else {
97                $files[$key] = $this->getFiles($value);
98            }
99        }
100
101        return $files;
102    }
103
104    /**
105     * Creates Symfony UploadedFile instance from PSR-7 ones.
106     */
107    private function createUploadedFile(UploadedFileInterface $psrUploadedFile): UploadedFile
108    {
109        return new UploadedFile($psrUploadedFile, function () { return $this->getTemporaryPath(); });
110    }
111
112    /**
113     * Gets a temporary file path.
114     *
115     * @return string
116     */
117    protected function getTemporaryPath()
118    {
119        return tempnam(sys_get_temp_dir(), uniqid('symfony', true));
120    }
121
122    /**
123     * {@inheritdoc}
124     */
125    public function createResponse(ResponseInterface $psrResponse, bool $streamed = false)
126    {
127        $cookies = $psrResponse->getHeader('Set-Cookie');
128        $psrResponse = $psrResponse->withoutHeader('Set-Cookie');
129
130        if ($streamed) {
131            $response = new StreamedResponse(
132                $this->createStreamedResponseCallback($psrResponse->getBody()),
133                $psrResponse->getStatusCode(),
134                $psrResponse->getHeaders()
135            );
136        } else {
137            $response = new Response(
138                $psrResponse->getBody()->__toString(),
139                $psrResponse->getStatusCode(),
140                $psrResponse->getHeaders()
141            );
142        }
143
144        $response->setProtocolVersion($psrResponse->getProtocolVersion());
145
146        foreach ($cookies as $cookie) {
147            $response->headers->setCookie($this->createCookie($cookie));
148        }
149
150        return $response;
151    }
152
153    /**
154     * Creates a Cookie instance from a cookie string.
155     *
156     * Some snippets have been taken from the Guzzle project: https://github.com/guzzle/guzzle/blob/5.3/src/Cookie/SetCookie.php#L34
157     *
158     * @throws \InvalidArgumentException
159     */
160    private function createCookie(string $cookie): Cookie
161    {
162        foreach (explode(';', $cookie) as $part) {
163            $part = trim($part);
164
165            $data = explode('=', $part, 2);
166            $name = $data[0];
167            $value = isset($data[1]) ? trim($data[1], " \n\r\t\0\x0B\"") : null;
168
169            if (!isset($cookieName)) {
170                $cookieName = $name;
171                $cookieValue = $value;
172
173                continue;
174            }
175
176            if ('expires' === strtolower($name) && null !== $value) {
177                $cookieExpire = new \DateTime($value);
178
179                continue;
180            }
181
182            if ('path' === strtolower($name) && null !== $value) {
183                $cookiePath = $value;
184
185                continue;
186            }
187
188            if ('domain' === strtolower($name) && null !== $value) {
189                $cookieDomain = $value;
190
191                continue;
192            }
193
194            if ('secure' === strtolower($name)) {
195                $cookieSecure = true;
196
197                continue;
198            }
199
200            if ('httponly' === strtolower($name)) {
201                $cookieHttpOnly = true;
202
203                continue;
204            }
205
206            if ('samesite' === strtolower($name) && null !== $value) {
207                $samesite = $value;
208
209                continue;
210            }
211        }
212
213        if (!isset($cookieName)) {
214            throw new \InvalidArgumentException('The value of the Set-Cookie header is malformed.');
215        }
216
217        return new Cookie(
218            $cookieName,
219            $cookieValue,
220            isset($cookieExpire) ? $cookieExpire : 0,
221            isset($cookiePath) ? $cookiePath : '/',
222            isset($cookieDomain) ? $cookieDomain : null,
223            isset($cookieSecure),
224            isset($cookieHttpOnly),
225            true,
226            isset($samesite) ? $samesite : null
227        );
228    }
229
230    private function createStreamedResponseCallback(StreamInterface $body): callable
231    {
232        return function () use ($body) {
233            if ($body->isSeekable()) {
234                $body->rewind();
235            }
236
237            if (!$body->isReadable()) {
238                echo $body;
239
240                return;
241            }
242
243            while (!$body->eof()) {
244                echo $body->read($this->responseBufferMaxLength);
245            }
246        };
247    }
248}
249