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