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