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