2d79d3778f38ff31bb3c7d06f349fe5ae2150473
[yaffs-website] / vendor / zendframework / zend-diactoros / src / ServerRequestFactory.php
1 <?php
2 /**
3  * @see       https://github.com/zendframework/zend-diactoros for the canonical source repository
4  * @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
5  * @license   https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
6  */
7
8 namespace Zend\Diactoros;
9
10 use InvalidArgumentException;
11 use Psr\Http\Message\UploadedFileInterface;
12 use stdClass;
13 use UnexpectedValueException;
14
15 /**
16  * Class for marshaling a request object from the current PHP environment.
17  *
18  * Logic largely refactored from the ZF2 Zend\Http\PhpEnvironment\Request class.
19  *
20  * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
21  * @license   http://framework.zend.com/license/new-bsd New BSD License
22  */
23 abstract class ServerRequestFactory
24 {
25     /**
26      * Function to use to get apache request headers; present only to simplify mocking.
27      *
28      * @var callable
29      */
30     private static $apacheRequestHeaders = 'apache_request_headers';
31
32     /**
33      * Create a request from the supplied superglobal values.
34      *
35      * If any argument is not supplied, the corresponding superglobal value will
36      * be used.
37      *
38      * The ServerRequest created is then passed to the fromServer() method in
39      * order to marshal the request URI and headers.
40      *
41      * @see fromServer()
42      * @param array $server $_SERVER superglobal
43      * @param array $query $_GET superglobal
44      * @param array $body $_POST superglobal
45      * @param array $cookies $_COOKIE superglobal
46      * @param array $files $_FILES superglobal
47      * @return ServerRequest
48      * @throws InvalidArgumentException for invalid file values
49      */
50     public static function fromGlobals(
51         array $server = null,
52         array $query = null,
53         array $body = null,
54         array $cookies = null,
55         array $files = null
56     ) {
57         $server  = static::normalizeServer($server ?: $_SERVER);
58         $files   = static::normalizeFiles($files ?: $_FILES);
59         $headers = static::marshalHeaders($server);
60
61         if (null === $cookies && array_key_exists('cookie', $headers)) {
62             $cookies = self::parseCookieHeader($headers['cookie']);
63         }
64
65         return new ServerRequest(
66             $server,
67             $files,
68             static::marshalUriFromServer($server, $headers),
69             static::get('REQUEST_METHOD', $server, 'GET'),
70             'php://input',
71             $headers,
72             $cookies ?: $_COOKIE,
73             $query ?: $_GET,
74             $body ?: $_POST,
75             static::marshalProtocolVersion($server)
76         );
77     }
78
79     /**
80      * Access a value in an array, returning a default value if not found
81      *
82      * Will also do a case-insensitive search if a case sensitive search fails.
83      *
84      * @param string $key
85      * @param array $values
86      * @param mixed $default
87      * @return mixed
88      */
89     public static function get($key, array $values, $default = null)
90     {
91         if (array_key_exists($key, $values)) {
92             return $values[$key];
93         }
94
95         return $default;
96     }
97
98     /**
99      * Search for a header value.
100      *
101      * Does a case-insensitive search for a matching header.
102      *
103      * If found, it is returned as a string, using comma concatenation.
104      *
105      * If not, the $default is returned.
106      *
107      * @param string $header
108      * @param array $headers
109      * @param mixed $default
110      * @return string
111      */
112     public static function getHeader($header, array $headers, $default = null)
113     {
114         $header  = strtolower($header);
115         $headers = array_change_key_case($headers, CASE_LOWER);
116         if (array_key_exists($header, $headers)) {
117             $value = is_array($headers[$header]) ? implode(', ', $headers[$header]) : $headers[$header];
118             return $value;
119         }
120
121         return $default;
122     }
123
124     /**
125      * Marshal the $_SERVER array
126      *
127      * Pre-processes and returns the $_SERVER superglobal.
128      *
129      * @param array $server
130      * @return array
131      */
132     public static function normalizeServer(array $server)
133     {
134         // This seems to be the only way to get the Authorization header on Apache
135         $apacheRequestHeaders = self::$apacheRequestHeaders;
136         if (isset($server['HTTP_AUTHORIZATION'])
137             || ! is_callable($apacheRequestHeaders)
138         ) {
139             return $server;
140         }
141
142         $apacheRequestHeaders = $apacheRequestHeaders();
143         if (isset($apacheRequestHeaders['Authorization'])) {
144             $server['HTTP_AUTHORIZATION'] = $apacheRequestHeaders['Authorization'];
145             return $server;
146         }
147
148         if (isset($apacheRequestHeaders['authorization'])) {
149             $server['HTTP_AUTHORIZATION'] = $apacheRequestHeaders['authorization'];
150             return $server;
151         }
152
153         return $server;
154     }
155
156     /**
157      * Normalize uploaded files
158      *
159      * Transforms each value into an UploadedFileInterface instance, and ensures
160      * that nested arrays are normalized.
161      *
162      * @param array $files
163      * @return array
164      * @throws InvalidArgumentException for unrecognized values
165      */
166     public static function normalizeFiles(array $files)
167     {
168         $normalized = [];
169         foreach ($files as $key => $value) {
170             if ($value instanceof UploadedFileInterface) {
171                 $normalized[$key] = $value;
172                 continue;
173             }
174
175             if (is_array($value) && isset($value['tmp_name'])) {
176                 $normalized[$key] = self::createUploadedFileFromSpec($value);
177                 continue;
178             }
179
180             if (is_array($value)) {
181                 $normalized[$key] = self::normalizeFiles($value);
182                 continue;
183             }
184
185             throw new InvalidArgumentException('Invalid value in files specification');
186         }
187         return $normalized;
188     }
189
190     /**
191      * Marshal headers from $_SERVER
192      *
193      * @param array $server
194      * @return array
195      */
196     public static function marshalHeaders(array $server)
197     {
198         $headers = [];
199         foreach ($server as $key => $value) {
200             // Apache prefixes environment variables with REDIRECT_
201             // if they are added by rewrite rules
202             if (strpos($key, 'REDIRECT_') === 0) {
203                 $key = substr($key, 9);
204
205                 // We will not overwrite existing variables with the
206                 // prefixed versions, though
207                 if (array_key_exists($key, $server)) {
208                     continue;
209                 }
210             }
211
212             if ($value && strpos($key, 'HTTP_') === 0) {
213                 $name = strtr(strtolower(substr($key, 5)), '_', '-');
214                 $headers[$name] = $value;
215                 continue;
216             }
217
218             if ($value && strpos($key, 'CONTENT_') === 0) {
219                 $name = 'content-' . strtolower(substr($key, 8));
220                 $headers[$name] = $value;
221                 continue;
222             }
223         }
224
225         return $headers;
226     }
227
228     /**
229      * Marshal the URI from the $_SERVER array and headers
230      *
231      * @param array $server
232      * @param array $headers
233      * @return Uri
234      */
235     public static function marshalUriFromServer(array $server, array $headers)
236     {
237         $uri = new Uri('');
238
239         // URI scheme
240         $scheme = 'http';
241         $https  = self::get('HTTPS', $server);
242         if (($https && 'off' !== $https)
243             || self::getHeader('x-forwarded-proto', $headers, false) === 'https'
244         ) {
245             $scheme = 'https';
246         }
247         $uri = $uri->withScheme($scheme);
248
249         // Set the host
250         $accumulator = (object) ['host' => '', 'port' => null];
251         self::marshalHostAndPortFromHeaders($accumulator, $server, $headers);
252         $host = $accumulator->host;
253         $port = $accumulator->port;
254         if (! empty($host)) {
255             $uri = $uri->withHost($host);
256             if (! empty($port)) {
257                 $uri = $uri->withPort($port);
258             }
259         }
260
261         // URI path
262         $path = self::marshalRequestUri($server);
263         $path = self::stripQueryString($path);
264
265         // URI query
266         $query = '';
267         if (isset($server['QUERY_STRING'])) {
268             $query = ltrim($server['QUERY_STRING'], '?');
269         }
270
271         // URI fragment
272         $fragment = '';
273         if (strpos($path, '#') !== false) {
274             list($path, $fragment) = explode('#', $path, 2);
275         }
276
277         return $uri
278             ->withPath($path)
279             ->withFragment($fragment)
280             ->withQuery($query);
281     }
282
283     /**
284      * Marshal the host and port from HTTP headers and/or the PHP environment
285      *
286      * @param stdClass $accumulator
287      * @param array $server
288      * @param array $headers
289      */
290     public static function marshalHostAndPortFromHeaders(stdClass $accumulator, array $server, array $headers)
291     {
292         if (self::getHeader('host', $headers, false)) {
293             self::marshalHostAndPortFromHeader($accumulator, self::getHeader('host', $headers));
294             return;
295         }
296
297         if (! isset($server['SERVER_NAME'])) {
298             return;
299         }
300
301         $accumulator->host = $server['SERVER_NAME'];
302         if (isset($server['SERVER_PORT'])) {
303             $accumulator->port = (int) $server['SERVER_PORT'];
304         }
305
306         if (! isset($server['SERVER_ADDR']) || ! preg_match('/^\[[0-9a-fA-F\:]+\]$/', $accumulator->host)) {
307             return;
308         }
309
310         // Misinterpreted IPv6-Address
311         // Reported for Safari on Windows
312         self::marshalIpv6HostAndPort($accumulator, $server);
313     }
314
315     /**
316      * Detect the base URI for the request
317      *
318      * Looks at a variety of criteria in order to attempt to autodetect a base
319      * URI, including rewrite URIs, proxy URIs, etc.
320      *
321      * From ZF2's Zend\Http\PhpEnvironment\Request class
322      * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
323      * @license   http://framework.zend.com/license/new-bsd New BSD License
324      *
325      * @param array $server
326      * @return string
327      */
328     public static function marshalRequestUri(array $server)
329     {
330         // IIS7 with URL Rewrite: make sure we get the unencoded url
331         // (double slash problem).
332         $iisUrlRewritten = self::get('IIS_WasUrlRewritten', $server);
333         $unencodedUrl    = self::get('UNENCODED_URL', $server, '');
334         if ('1' == $iisUrlRewritten && ! empty($unencodedUrl)) {
335             return $unencodedUrl;
336         }
337
338         $requestUri = self::get('REQUEST_URI', $server);
339
340         // Check this first so IIS will catch.
341         $httpXRewriteUrl = self::get('HTTP_X_REWRITE_URL', $server);
342         if ($httpXRewriteUrl !== null) {
343             $requestUri = $httpXRewriteUrl;
344         }
345
346         // Check for IIS 7.0 or later with ISAPI_Rewrite
347         $httpXOriginalUrl = self::get('HTTP_X_ORIGINAL_URL', $server);
348         if ($httpXOriginalUrl !== null) {
349             $requestUri = $httpXOriginalUrl;
350         }
351
352         if ($requestUri !== null) {
353             return preg_replace('#^[^/:]+://[^/]+#', '', $requestUri);
354         }
355
356         $origPathInfo = self::get('ORIG_PATH_INFO', $server);
357         if (empty($origPathInfo)) {
358             return '/';
359         }
360
361         return $origPathInfo;
362     }
363
364     /**
365      * Strip the query string from a path
366      *
367      * @param mixed $path
368      * @return string
369      */
370     public static function stripQueryString($path)
371     {
372         if (($qpos = strpos($path, '?')) !== false) {
373             return substr($path, 0, $qpos);
374         }
375         return $path;
376     }
377
378     /**
379      * Marshal the host and port from the request header
380      *
381      * @param stdClass $accumulator
382      * @param string|array $host
383      * @return void
384      */
385     private static function marshalHostAndPortFromHeader(stdClass $accumulator, $host)
386     {
387         if (is_array($host)) {
388             $host = implode(', ', $host);
389         }
390
391         $accumulator->host = $host;
392         $accumulator->port = null;
393
394         // works for regname, IPv4 & IPv6
395         if (preg_match('|\:(\d+)$|', $accumulator->host, $matches)) {
396             $accumulator->host = substr($accumulator->host, 0, -1 * (strlen($matches[1]) + 1));
397             $accumulator->port = (int) $matches[1];
398         }
399     }
400
401     /**
402      * Marshal host/port from misinterpreted IPv6 address
403      *
404      * @param stdClass $accumulator
405      * @param array $server
406      */
407     private static function marshalIpv6HostAndPort(stdClass $accumulator, array $server)
408     {
409         $accumulator->host = '[' . $server['SERVER_ADDR'] . ']';
410         $accumulator->port = $accumulator->port ?: 80;
411         if ($accumulator->port . ']' === substr($accumulator->host, strrpos($accumulator->host, ':') + 1)) {
412             // The last digit of the IPv6-Address has been taken as port
413             // Unset the port so the default port can be used
414             $accumulator->port = null;
415         }
416     }
417
418     /**
419      * Create and return an UploadedFile instance from a $_FILES specification.
420      *
421      * If the specification represents an array of values, this method will
422      * delegate to normalizeNestedFileSpec() and return that return value.
423      *
424      * @param array $value $_FILES struct
425      * @return array|UploadedFileInterface
426      */
427     private static function createUploadedFileFromSpec(array $value)
428     {
429         if (is_array($value['tmp_name'])) {
430             return self::normalizeNestedFileSpec($value);
431         }
432
433         return new UploadedFile(
434             $value['tmp_name'],
435             $value['size'],
436             $value['error'],
437             $value['name'],
438             $value['type']
439         );
440     }
441
442     /**
443      * Normalize an array of file specifications.
444      *
445      * Loops through all nested files and returns a normalized array of
446      * UploadedFileInterface instances.
447      *
448      * @param array $files
449      * @return UploadedFileInterface[]
450      */
451     private static function normalizeNestedFileSpec(array $files = [])
452     {
453         $normalizedFiles = [];
454         foreach (array_keys($files['tmp_name']) as $key) {
455             $spec = [
456                 'tmp_name' => $files['tmp_name'][$key],
457                 'size'     => $files['size'][$key],
458                 'error'    => $files['error'][$key],
459                 'name'     => $files['name'][$key],
460                 'type'     => $files['type'][$key],
461             ];
462             $normalizedFiles[$key] = self::createUploadedFileFromSpec($spec);
463         }
464         return $normalizedFiles;
465     }
466
467     /**
468      * Return HTTP protocol version (X.Y)
469      *
470      * @param array $server
471      * @return string
472      */
473     private static function marshalProtocolVersion(array $server)
474     {
475         if (! isset($server['SERVER_PROTOCOL'])) {
476             return '1.1';
477         }
478
479         if (! preg_match('#^(HTTP/)?(?P<version>[1-9]\d*(?:\.\d)?)$#', $server['SERVER_PROTOCOL'], $matches)) {
480             throw new UnexpectedValueException(sprintf(
481                 'Unrecognized protocol version (%s)',
482                 $server['SERVER_PROTOCOL']
483             ));
484         }
485
486         return $matches['version'];
487     }
488
489     /**
490      * Parse a cookie header according to RFC 6265.
491      *
492      * PHP will replace special characters in cookie names, which results in other cookies not being available due to
493      * overwriting. Thus, the server request should take the cookies from the request header instead.
494      *
495      * @param $cookieHeader
496      * @return array
497      */
498     private static function parseCookieHeader($cookieHeader)
499     {
500         preg_match_all('(
501             (?:^\\n?[ \t]*|;[ ])
502             (?P<name>[!#$%&\'*+-.0-9A-Z^_`a-z|~]+)
503             =
504             (?P<DQUOTE>"?)
505                 (?P<value>[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*)
506             (?P=DQUOTE)
507             (?=\\n?[ \t]*$|;[ ])
508         )x', $cookieHeader, $matches, PREG_SET_ORDER);
509
510         $cookies = [];
511
512         foreach ($matches as $match) {
513             $cookies[$match['name']] = urldecode($match['value']);
514         }
515
516         return $cookies;
517     }
518 }