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