1<?php 2 3declare(strict_types=1); 4 5namespace GuzzleHttp\Psr7; 6 7use InvalidArgumentException; 8use Psr\Http\Message\ServerRequestInterface; 9use Psr\Http\Message\StreamInterface; 10use Psr\Http\Message\UploadedFileInterface; 11use Psr\Http\Message\UriInterface; 12 13/** 14 * Server-side HTTP request 15 * 16 * Extends the Request definition to add methods for accessing incoming data, 17 * specifically server parameters, cookies, matched path parameters, query 18 * string arguments, body parameters, and upload file information. 19 * 20 * "Attributes" are discovered via decomposing the request (and usually 21 * specifically the URI path), and typically will be injected by the application. 22 * 23 * Requests are considered immutable; all methods that might change state are 24 * implemented such that they retain the internal state of the current 25 * message and return a new instance that contains the changed state. 26 */ 27class ServerRequest extends Request implements ServerRequestInterface 28{ 29 /** 30 * @var array 31 */ 32 private $attributes = []; 33 34 /** 35 * @var array 36 */ 37 private $cookieParams = []; 38 39 /** 40 * @var array|object|null 41 */ 42 private $parsedBody; 43 44 /** 45 * @var array 46 */ 47 private $queryParams = []; 48 49 /** 50 * @var array 51 */ 52 private $serverParams; 53 54 /** 55 * @var array 56 */ 57 private $uploadedFiles = []; 58 59 /** 60 * @param string $method HTTP method 61 * @param string|UriInterface $uri URI 62 * @param (string|string[])[] $headers Request headers 63 * @param string|resource|StreamInterface|null $body Request body 64 * @param string $version Protocol version 65 * @param array $serverParams Typically the $_SERVER superglobal 66 */ 67 public function __construct( 68 string $method, 69 $uri, 70 array $headers = [], 71 $body = null, 72 string $version = '1.1', 73 array $serverParams = [] 74 ) { 75 $this->serverParams = $serverParams; 76 77 parent::__construct($method, $uri, $headers, $body, $version); 78 } 79 80 /** 81 * Return an UploadedFile instance array. 82 * 83 * @param array $files An array which respect $_FILES structure 84 * 85 * @throws InvalidArgumentException for unrecognized values 86 */ 87 public static function normalizeFiles(array $files): array 88 { 89 $normalized = []; 90 91 foreach ($files as $key => $value) { 92 if ($value instanceof UploadedFileInterface) { 93 $normalized[$key] = $value; 94 } elseif (is_array($value) && isset($value['tmp_name'])) { 95 $normalized[$key] = self::createUploadedFileFromSpec($value); 96 } elseif (is_array($value)) { 97 $normalized[$key] = self::normalizeFiles($value); 98 continue; 99 } else { 100 throw new InvalidArgumentException('Invalid value in files specification'); 101 } 102 } 103 104 return $normalized; 105 } 106 107 /** 108 * Create and return an UploadedFile instance from a $_FILES specification. 109 * 110 * If the specification represents an array of values, this method will 111 * delegate to normalizeNestedFileSpec() and return that return value. 112 * 113 * @param array $value $_FILES struct 114 * 115 * @return UploadedFileInterface|UploadedFileInterface[] 116 */ 117 private static function createUploadedFileFromSpec(array $value) 118 { 119 if (is_array($value['tmp_name'])) { 120 return self::normalizeNestedFileSpec($value); 121 } 122 123 return new UploadedFile( 124 $value['tmp_name'], 125 (int) $value['size'], 126 (int) $value['error'], 127 $value['name'], 128 $value['type'] 129 ); 130 } 131 132 /** 133 * Normalize an array of file specifications. 134 * 135 * Loops through all nested files and returns a normalized array of 136 * UploadedFileInterface instances. 137 * 138 * @return UploadedFileInterface[] 139 */ 140 private static function normalizeNestedFileSpec(array $files = []): array 141 { 142 $normalizedFiles = []; 143 144 foreach (array_keys($files['tmp_name']) as $key) { 145 $spec = [ 146 'tmp_name' => $files['tmp_name'][$key], 147 'size' => $files['size'][$key] ?? null, 148 'error' => $files['error'][$key] ?? null, 149 'name' => $files['name'][$key] ?? null, 150 'type' => $files['type'][$key] ?? null, 151 ]; 152 $normalizedFiles[$key] = self::createUploadedFileFromSpec($spec); 153 } 154 155 return $normalizedFiles; 156 } 157 158 /** 159 * Return a ServerRequest populated with superglobals: 160 * $_GET 161 * $_POST 162 * $_COOKIE 163 * $_FILES 164 * $_SERVER 165 */ 166 public static function fromGlobals(): ServerRequestInterface 167 { 168 $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; 169 $headers = getallheaders(); 170 $uri = self::getUriFromGlobals(); 171 $body = new CachingStream(new LazyOpenStream('php://input', 'r+')); 172 $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']) : '1.1'; 173 174 $serverRequest = new ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER); 175 176 return $serverRequest 177 ->withCookieParams($_COOKIE) 178 ->withQueryParams($_GET) 179 ->withParsedBody($_POST) 180 ->withUploadedFiles(self::normalizeFiles($_FILES)); 181 } 182 183 private static function extractHostAndPortFromAuthority(string $authority): array 184 { 185 $uri = 'http://'.$authority; 186 $parts = parse_url($uri); 187 if (false === $parts) { 188 return [null, null]; 189 } 190 191 $host = $parts['host'] ?? null; 192 $port = $parts['port'] ?? null; 193 194 return [$host, $port]; 195 } 196 197 /** 198 * Get a Uri populated with values from $_SERVER. 199 */ 200 public static function getUriFromGlobals(): UriInterface 201 { 202 $uri = new Uri(''); 203 204 $uri = $uri->withScheme(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http'); 205 206 $hasPort = false; 207 if (isset($_SERVER['HTTP_HOST'])) { 208 [$host, $port] = self::extractHostAndPortFromAuthority($_SERVER['HTTP_HOST']); 209 if ($host !== null) { 210 $uri = $uri->withHost($host); 211 } 212 213 if ($port !== null) { 214 $hasPort = true; 215 $uri = $uri->withPort($port); 216 } 217 } elseif (isset($_SERVER['SERVER_NAME'])) { 218 $uri = $uri->withHost($_SERVER['SERVER_NAME']); 219 } elseif (isset($_SERVER['SERVER_ADDR'])) { 220 $uri = $uri->withHost($_SERVER['SERVER_ADDR']); 221 } 222 223 if (!$hasPort && isset($_SERVER['SERVER_PORT'])) { 224 $uri = $uri->withPort($_SERVER['SERVER_PORT']); 225 } 226 227 $hasQuery = false; 228 if (isset($_SERVER['REQUEST_URI'])) { 229 $requestUriParts = explode('?', $_SERVER['REQUEST_URI'], 2); 230 $uri = $uri->withPath($requestUriParts[0]); 231 if (isset($requestUriParts[1])) { 232 $hasQuery = true; 233 $uri = $uri->withQuery($requestUriParts[1]); 234 } 235 } 236 237 if (!$hasQuery && isset($_SERVER['QUERY_STRING'])) { 238 $uri = $uri->withQuery($_SERVER['QUERY_STRING']); 239 } 240 241 return $uri; 242 } 243 244 public function getServerParams(): array 245 { 246 return $this->serverParams; 247 } 248 249 public function getUploadedFiles(): array 250 { 251 return $this->uploadedFiles; 252 } 253 254 public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface 255 { 256 $new = clone $this; 257 $new->uploadedFiles = $uploadedFiles; 258 259 return $new; 260 } 261 262 public function getCookieParams(): array 263 { 264 return $this->cookieParams; 265 } 266 267 public function withCookieParams(array $cookies): ServerRequestInterface 268 { 269 $new = clone $this; 270 $new->cookieParams = $cookies; 271 272 return $new; 273 } 274 275 public function getQueryParams(): array 276 { 277 return $this->queryParams; 278 } 279 280 public function withQueryParams(array $query): ServerRequestInterface 281 { 282 $new = clone $this; 283 $new->queryParams = $query; 284 285 return $new; 286 } 287 288 /** 289 * @return array|object|null 290 */ 291 public function getParsedBody() 292 { 293 return $this->parsedBody; 294 } 295 296 public function withParsedBody($data): ServerRequestInterface 297 { 298 $new = clone $this; 299 $new->parsedBody = $data; 300 301 return $new; 302 } 303 304 public function getAttributes(): array 305 { 306 return $this->attributes; 307 } 308 309 /** 310 * @return mixed 311 */ 312 public function getAttribute($attribute, $default = null) 313 { 314 if (false === array_key_exists($attribute, $this->attributes)) { 315 return $default; 316 } 317 318 return $this->attributes[$attribute]; 319 } 320 321 public function withAttribute($attribute, $value): ServerRequestInterface 322 { 323 $new = clone $this; 324 $new->attributes[$attribute] = $value; 325 326 return $new; 327 } 328 329 public function withoutAttribute($attribute): ServerRequestInterface 330 { 331 if (false === array_key_exists($attribute, $this->attributes)) { 332 return $this; 333 } 334 335 $new = clone $this; 336 unset($new->attributes[$attribute]); 337 338 return $new; 339 } 340} 341