ServerRequest.php 13 KB

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