annotate vendor/zendframework/zend-diactoros/src/ServerRequestFactory.php @ 0:c75dbcec494b

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