1<?php
2
3namespace GuzzleHttp\Psr7;
4
5use InvalidArgumentException;
6use Psr\Http\Message\ServerRequestInterface;
7use Psr\Http\Message\UriInterface;
8use Psr\Http\Message\StreamInterface;
9use Psr\Http\Message\UploadedFileInterface;
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 null|array|object
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|null|resource|StreamInterface $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     * @return array|UploadedFileInterface
115     */
116    private static function createUploadedFileFromSpec(array $value)
117    {
118        if (is_array($value['tmp_name'])) {
119            return self::normalizeNestedFileSpec($value);
120        }
121
122        return new UploadedFile(
123            $value['tmp_name'],
124            (int) $value['size'],
125            (int) $value['error'],
126            $value['name'],
127            $value['type']
128        );
129    }
130
131    /**
132     * Normalize an array of file specifications.
133     *
134     * Loops through all nested files and returns a normalized array of
135     * UploadedFileInterface instances.
136     *
137     * @param array $files
138     * @return UploadedFileInterface[]
139     */
140    private static function normalizeNestedFileSpec(array $files = [])
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],
148                'error'    => $files['error'][$key],
149                'name'     => $files['name'][$key],
150                'type'     => $files['type'][$key],
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     * @return ServerRequestInterface
167     */
168    public static function fromGlobals()
169    {
170        $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';
171        $headers = getallheaders();
172        $uri = self::getUriFromGlobals();
173        $body = new CachingStream(new LazyOpenStream('php://input', 'r+'));
174        $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']) : '1.1';
175
176        $serverRequest = new ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER);
177
178        return $serverRequest
179            ->withCookieParams($_COOKIE)
180            ->withQueryParams($_GET)
181            ->withParsedBody($_POST)
182            ->withUploadedFiles(self::normalizeFiles($_FILES));
183    }
184
185    private static function extractHostAndPortFromAuthority($authority)
186    {
187        $uri = 'http://'.$authority;
188        $parts = parse_url($uri);
189        if (false === $parts) {
190            return [null, null];
191        }
192
193        $host = isset($parts['host']) ? $parts['host'] : null;
194        $port = isset($parts['port']) ? $parts['port'] : null;
195
196        return [$host, $port];
197    }
198
199    /**
200     * Get a Uri populated with values from $_SERVER.
201     *
202     * @return UriInterface
203     */
204    public static function getUriFromGlobals()
205    {
206        $uri = new Uri('');
207
208        $uri = $uri->withScheme(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http');
209
210        $hasPort = false;
211        if (isset($_SERVER['HTTP_HOST'])) {
212            list($host, $port) = self::extractHostAndPortFromAuthority($_SERVER['HTTP_HOST']);
213            if ($host !== null) {
214                $uri = $uri->withHost($host);
215            }
216
217            if ($port !== null) {
218                $hasPort = true;
219                $uri = $uri->withPort($port);
220            }
221        } elseif (isset($_SERVER['SERVER_NAME'])) {
222            $uri = $uri->withHost($_SERVER['SERVER_NAME']);
223        } elseif (isset($_SERVER['SERVER_ADDR'])) {
224            $uri = $uri->withHost($_SERVER['SERVER_ADDR']);
225        }
226
227        if (!$hasPort && isset($_SERVER['SERVER_PORT'])) {
228            $uri = $uri->withPort($_SERVER['SERVER_PORT']);
229        }
230
231        $hasQuery = false;
232        if (isset($_SERVER['REQUEST_URI'])) {
233            $requestUriParts = explode('?', $_SERVER['REQUEST_URI'], 2);
234            $uri = $uri->withPath($requestUriParts[0]);
235            if (isset($requestUriParts[1])) {
236                $hasQuery = true;
237                $uri = $uri->withQuery($requestUriParts[1]);
238            }
239        }
240
241        if (!$hasQuery && isset($_SERVER['QUERY_STRING'])) {
242            $uri = $uri->withQuery($_SERVER['QUERY_STRING']);
243        }
244
245        return $uri;
246    }
247
248
249    /**
250     * {@inheritdoc}
251     */
252    public function getServerParams()
253    {
254        return $this->serverParams;
255    }
256
257    /**
258     * {@inheritdoc}
259     */
260    public function getUploadedFiles()
261    {
262        return $this->uploadedFiles;
263    }
264
265    /**
266     * {@inheritdoc}
267     */
268    public function withUploadedFiles(array $uploadedFiles)
269    {
270        $new = clone $this;
271        $new->uploadedFiles = $uploadedFiles;
272
273        return $new;
274    }
275
276    /**
277     * {@inheritdoc}
278     */
279    public function getCookieParams()
280    {
281        return $this->cookieParams;
282    }
283
284    /**
285     * {@inheritdoc}
286     */
287    public function withCookieParams(array $cookies)
288    {
289        $new = clone $this;
290        $new->cookieParams = $cookies;
291
292        return $new;
293    }
294
295    /**
296     * {@inheritdoc}
297     */
298    public function getQueryParams()
299    {
300        return $this->queryParams;
301    }
302
303    /**
304     * {@inheritdoc}
305     */
306    public function withQueryParams(array $query)
307    {
308        $new = clone $this;
309        $new->queryParams = $query;
310
311        return $new;
312    }
313
314    /**
315     * {@inheritdoc}
316     */
317    public function getParsedBody()
318    {
319        return $this->parsedBody;
320    }
321
322    /**
323     * {@inheritdoc}
324     */
325    public function withParsedBody($data)
326    {
327        $new = clone $this;
328        $new->parsedBody = $data;
329
330        return $new;
331    }
332
333    /**
334     * {@inheritdoc}
335     */
336    public function getAttributes()
337    {
338        return $this->attributes;
339    }
340
341    /**
342     * {@inheritdoc}
343     */
344    public function getAttribute($attribute, $default = null)
345    {
346        if (false === array_key_exists($attribute, $this->attributes)) {
347            return $default;
348        }
349
350        return $this->attributes[$attribute];
351    }
352
353    /**
354     * {@inheritdoc}
355     */
356    public function withAttribute($attribute, $value)
357    {
358        $new = clone $this;
359        $new->attributes[$attribute] = $value;
360
361        return $new;
362    }
363
364    /**
365     * {@inheritdoc}
366     */
367    public function withoutAttribute($attribute)
368    {
369        if (false === array_key_exists($attribute, $this->attributes)) {
370            return $this;
371        }
372
373        $new = clone $this;
374        unset($new->attributes[$attribute]);
375
376        return $new;
377    }
378}
379