diff --git a/composer.json b/composer.json index f4278091..d3d76d98 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,15 @@ "psr/http-message-implementation": "1.0" }, "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/get_header_from_array.php", + "src/functions/marshal_host_and_port.php", + "src/functions/marshal_protocol_version.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/normalize_uploaded_file_specification.php", + "src/functions/parse_cookie_header.php" + ], "psr-4": { "Zend\\Diactoros\\": "src/" } diff --git a/composer.lock b/composer.lock index 102472cc..7bfcbe8e 100644 --- a/composer.lock +++ b/composer.lock @@ -1,7 +1,7 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], "content-hash": "02a4a4ab06235930ffa531039a00af20", diff --git a/src/ServerRequestFactory.php b/src/ServerRequestFactory.php index 57cdc167..6e10a45b 100644 --- a/src/ServerRequestFactory.php +++ b/src/ServerRequestFactory.php @@ -78,11 +78,11 @@ public static function fromGlobals( array $files = null ) { $server = static::normalizeServer($server ?: $_SERVER); - $files = static::normalizeFiles($files ?: $_FILES); + $files = normalizeUploadedFiles($files ?: $_FILES); $headers = static::marshalHeaders($server); if (null === $cookies && array_key_exists('cookie', $headers)) { - $cookies = self::parseCookieHeader($headers['cookie']); + $cookies = parseCookieHeader($headers['cookie']); } return new ServerRequest( @@ -95,7 +95,7 @@ public static function fromGlobals( $cookies ?: $_COOKIE, $query ?: $_GET, $body ?: $_POST, - static::marshalProtocolVersion($server) + marshalProtocolVersion($server) ); } @@ -127,6 +127,7 @@ public static function get($key, array $values, $default = null) * * If not, the $default is returned. * + * @deprecated since 1.8.0; use Zend\Diactoros\getHeaderFromArray() instead. * @param string $header * @param array $headers * @param mixed $default @@ -134,14 +135,7 @@ public static function get($key, array $values, $default = null) */ public static function getHeader($header, array $headers, $default = null) { - $header = strtolower($header); - $headers = array_change_key_case($headers, CASE_LOWER); - if (array_key_exists($header, $headers)) { - $value = is_array($headers[$header]) ? implode(', ', $headers[$header]) : $headers[$header]; - return $value; - } - - return $default; + return getHeaderFromArray($header, $headers, $default); } /** @@ -182,32 +176,14 @@ public static function normalizeServer(array $server) * Transforms each value into an UploadedFileInterface instance, and ensures * that nested arrays are normalized. * + * @deprecated since 1.8.0; use \Zend\Diactoros\normalizeUploadedFiles instead. * @param array $files * @return array * @throws InvalidArgumentException for unrecognized values */ public static function normalizeFiles(array $files) { - $normalized = []; - foreach ($files as $key => $value) { - if ($value instanceof UploadedFileInterface) { - $normalized[$key] = $value; - continue; - } - - if (is_array($value) && isset($value['tmp_name'])) { - $normalized[$key] = self::createUploadedFileFromSpec($value); - continue; - } - - if (is_array($value)) { - $normalized[$key] = self::normalizeFiles($value); - continue; - } - - throw new InvalidArgumentException('Invalid value in files specification'); - } - return $normalized; + return normalizeUploadedFiles($files); } /** @@ -263,7 +239,7 @@ public static function marshalUriFromServer(array $server, array $headers) $scheme = 'http'; $https = self::get('HTTPS', $server); if (($https && 'off' !== $https) - || self::getHeader('x-forwarded-proto', $headers, false) === 'https' + || getHeaderFromArray('x-forwarded-proto', $headers, false) === 'https' ) { $scheme = 'https'; } @@ -271,9 +247,7 @@ public static function marshalUriFromServer(array $server, array $headers) // Set the host $accumulator = (object) ['host' => '', 'port' => null]; - self::marshalHostAndPortFromHeaders($accumulator, $server, $headers); - $host = $accumulator->host; - $port = $accumulator->port; + list($host, $port) = marshalHostAndPort($headers, $server); if (! empty($host)) { $uri = $uri->withHost($host); if (! empty($port)) { @@ -306,33 +280,16 @@ public static function marshalUriFromServer(array $server, array $headers) /** * Marshal the host and port from HTTP headers and/or the PHP environment * + * @deprecated since 1.8.0; use Zend\Diactoros\marshalHostAndPort instead. * @param stdClass $accumulator * @param array $server * @param array $headers */ public static function marshalHostAndPortFromHeaders(stdClass $accumulator, array $server, array $headers) { - if (self::getHeader('host', $headers, false)) { - self::marshalHostAndPortFromHeader($accumulator, self::getHeader('host', $headers)); - return; - } - - if (! isset($server['SERVER_NAME'])) { - return; - } - - $accumulator->host = $server['SERVER_NAME']; - if (isset($server['SERVER_PORT'])) { - $accumulator->port = (int) $server['SERVER_PORT']; - } - - if (! isset($server['SERVER_ADDR']) || ! preg_match('/^\[[0-9a-fA-F\:]+\]$/', $accumulator->host)) { - return; - } - - // Misinterpreted IPv6-Address - // Reported for Safari on Windows - self::marshalIpv6HostAndPort($accumulator, $server); + list($host, $port) = marshalHostAndPort($headers, $server); + $accumulator->host = $host; + $accumulator->port = $port; } /** @@ -397,145 +354,4 @@ public static function stripQueryString($path) } return $path; } - - /** - * Marshal the host and port from the request header - * - * @param stdClass $accumulator - * @param string|array $host - * @return void - */ - private static function marshalHostAndPortFromHeader(stdClass $accumulator, $host) - { - if (is_array($host)) { - $host = implode(', ', $host); - } - - $accumulator->host = $host; - $accumulator->port = null; - - // works for regname, IPv4 & IPv6 - if (preg_match('|\:(\d+)$|', $accumulator->host, $matches)) { - $accumulator->host = substr($accumulator->host, 0, -1 * (strlen($matches[1]) + 1)); - $accumulator->port = (int) $matches[1]; - } - } - - /** - * Marshal host/port from misinterpreted IPv6 address - * - * @param stdClass $accumulator - * @param array $server - */ - private static function marshalIpv6HostAndPort(stdClass $accumulator, array $server) - { - $accumulator->host = '[' . $server['SERVER_ADDR'] . ']'; - $accumulator->port = $accumulator->port ?: 80; - if ($accumulator->port . ']' === substr($accumulator->host, strrpos($accumulator->host, ':') + 1)) { - // The last digit of the IPv6-Address has been taken as port - // Unset the port so the default port can be used - $accumulator->port = null; - } - } - - /** - * Create and return an UploadedFile instance from a $_FILES specification. - * - * If the specification represents an array of values, this method will - * delegate to normalizeNestedFileSpec() and return that return value. - * - * @param array $value $_FILES struct - * @return array|UploadedFileInterface - */ - private static function createUploadedFileFromSpec(array $value) - { - if (is_array($value['tmp_name'])) { - return self::normalizeNestedFileSpec($value); - } - - return new UploadedFile( - $value['tmp_name'], - $value['size'], - $value['error'], - $value['name'], - $value['type'] - ); - } - - /** - * Normalize an array of file specifications. - * - * Loops through all nested files and returns a normalized array of - * UploadedFileInterface instances. - * - * @param array $files - * @return UploadedFileInterface[] - */ - private static function normalizeNestedFileSpec(array $files = []) - { - $normalizedFiles = []; - foreach (array_keys($files['tmp_name']) as $key) { - $spec = [ - 'tmp_name' => $files['tmp_name'][$key], - 'size' => $files['size'][$key], - 'error' => $files['error'][$key], - 'name' => $files['name'][$key], - 'type' => $files['type'][$key], - ]; - $normalizedFiles[$key] = self::createUploadedFileFromSpec($spec); - } - return $normalizedFiles; - } - - /** - * Return HTTP protocol version (X.Y) - * - * @param array $server - * @return string - */ - private static function marshalProtocolVersion(array $server) - { - if (! isset($server['SERVER_PROTOCOL'])) { - return '1.1'; - } - - if (! preg_match('#^(HTTP/)?(?P[1-9]\d*(?:\.\d)?)$#', $server['SERVER_PROTOCOL'], $matches)) { - throw new UnexpectedValueException(sprintf( - 'Unrecognized protocol version (%s)', - $server['SERVER_PROTOCOL'] - )); - } - - return $matches['version']; - } - - /** - * Parse a cookie header according to RFC 6265. - * - * PHP will replace special characters in cookie names, which results in other cookies not being available due to - * overwriting. Thus, the server request should take the cookies from the request header instead. - * - * @param $cookieHeader - * @return array - */ - private static function parseCookieHeader($cookieHeader) - { - preg_match_all('( - (?:^\\n?[ \t]*|;[ ]) - (?P[!#$%&\'*+-.0-9A-Z^_`a-z|~]+) - = - (?P"?) - (?P[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*) - (?P=DQUOTE) - (?=\\n?[ \t]*$|;[ ]) - )x', $cookieHeader, $matches, PREG_SET_ORDER); - - $cookies = []; - - foreach ($matches as $match) { - $cookies[$match['name']] = urldecode($match['value']); - } - - return $cookies; - } } diff --git a/src/functions/create_uploaded_file.php b/src/functions/create_uploaded_file.php new file mode 100644 index 00000000..03ae13eb --- /dev/null +++ b/src/functions/create_uploaded_file.php @@ -0,0 +1,40 @@ +[1-9]\d*(?:\.\d)?)$#', $server['SERVER_PROTOCOL'], $matches)) { + throw new UnexpectedValueException(sprintf( + 'Unrecognized protocol version (%s)', + $server['SERVER_PROTOCOL'] + )); + } + + return $matches['version']; +} diff --git a/src/functions/normalize_uploaded_file_specification.php b/src/functions/normalize_uploaded_file_specification.php new file mode 100644 index 00000000..a2ee02f3 --- /dev/null +++ b/src/functions/normalize_uploaded_file_specification.php @@ -0,0 +1,51 @@ + $files['tmp_name'][$key], + 'size' => $files['size'][$key], + 'error' => $files['error'][$key], + 'name' => $files['name'][$key] ?? null, + 'type' => $files['type'][$key] ?? null, + ]; + $normalized[$key] = createUploadedFile($spec); + } + return $normalized; +} diff --git a/src/functions/normalize_uploaded_files.php b/src/functions/normalize_uploaded_files.php new file mode 100644 index 00000000..4f762670 --- /dev/null +++ b/src/functions/normalize_uploaded_files.php @@ -0,0 +1,50 @@ + $value) { + if ($value instanceof UploadedFileInterface) { + $normalized[$key] = $value; + continue; + } + + if (is_array($value) && isset($value['tmp_name']) && is_array($value['tmp_name'])) { + $normalized[$key] = normalizeUploadedFileSpecification($value); + continue; + } + + if (is_array($value) && isset($value['tmp_name'])) { + $normalized[$key] = createUploadedFile($value); + continue; + } + + if (is_array($value)) { + $normalized[$key] = normalizeUploadedFiles($value); + continue; + } + + throw new InvalidArgumentException('Invalid value in files specification'); + } + return $normalized; +} diff --git a/src/functions/parse_cookie_header.php b/src/functions/parse_cookie_header.php new file mode 100644 index 00000000..fa8de908 --- /dev/null +++ b/src/functions/parse_cookie_header.php @@ -0,0 +1,38 @@ +[!#$%&\'*+-.0-9A-Z^_`a-z|~]+) + = + (?P"?) + (?P[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*) + (?P=DQUOTE) + (?=\\n?[ \t]*$|;[ ]) + )x', $cookieHeader, $matches, PREG_SET_ORDER); + + $cookies = []; + + foreach ($matches as $match) { + $cookies[$match['name']] = urldecode($match['value']); + } + + return $cookies; +} diff --git a/test/ServerRequestFactoryTest.php b/test/ServerRequestFactoryTest.php index 63f202e1..8da94f68 100644 --- a/test/ServerRequestFactoryTest.php +++ b/test/ServerRequestFactoryTest.php @@ -16,6 +16,10 @@ use Zend\Diactoros\UploadedFile; use Zend\Diactoros\Uri; +use function Zend\Diactoros\marshalHostAndPort; +use function Zend\Diactoros\marshalProtocolVersion; +use function Zend\Diactoros\normalizeUploadedFiles; + class ServerRequestFactoryTest extends TestCase { public function testGetWillReturnValueIfPresentInArray() @@ -175,10 +179,10 @@ public function testMarshalHostAndPortUsesHostHeaderWhenPresent() $request = $request->withMethod('GET'); $request = $request->withHeader('Host', 'example.com'); - $accumulator = (object) ['host' => '', 'port' => null]; - ServerRequestFactory::marshalHostAndPortFromHeaders($accumulator, [], $request->getHeaders()); - $this->assertSame('example.com', $accumulator->host); - $this->assertNull($accumulator->port); + list($host, $port) = marshalHostAndPort($request->getHeaders(), []); + + $this->assertSame('example.com', $host); + $this->assertNull($port); } public function testMarshalHostAndPortWillDetectPortInHostHeaderWhenPresent() @@ -188,10 +192,10 @@ public function testMarshalHostAndPortWillDetectPortInHostHeaderWhenPresent() $request = $request->withMethod('GET'); $request = $request->withHeader('Host', 'example.com:8000'); - $accumulator = (object) ['host' => '', 'port' => null]; - ServerRequestFactory::marshalHostAndPortFromHeaders($accumulator, [], $request->getHeaders()); - $this->assertSame('example.com', $accumulator->host); - $this->assertSame(8000, $accumulator->port); + list($host, $port) = marshalHostAndPort($request->getHeaders(), []); + + $this->assertSame('example.com', $host); + $this->assertSame(8000, $port); } public function testMarshalHostAndPortReturnsEmptyValuesIfNoHostHeaderAndNoServerName() @@ -199,10 +203,10 @@ public function testMarshalHostAndPortReturnsEmptyValuesIfNoHostHeaderAndNoServe $request = new ServerRequest(); $request = $request->withUri(new Uri()); - $accumulator = (object) ['host' => '', 'port' => null]; - ServerRequestFactory::marshalHostAndPortFromHeaders($accumulator, [], $request->getHeaders()); - $this->assertSame('', $accumulator->host); - $this->assertNull($accumulator->port); + list($host, $port) = marshalHostAndPort($request->getHeaders(), []); + + $this->assertSame('', $host); + $this->assertNull($port); } public function testMarshalHostAndPortReturnsServerNameForHostWhenPresent() @@ -213,10 +217,11 @@ public function testMarshalHostAndPortReturnsServerNameForHostWhenPresent() $server = [ 'SERVER_NAME' => 'example.com', ]; - $accumulator = (object) ['host' => '', 'port' => null]; - ServerRequestFactory::marshalHostAndPortFromHeaders($accumulator, $server, $request->getHeaders()); - $this->assertSame('example.com', $accumulator->host); - $this->assertNull($accumulator->port); + + list($host, $port) = marshalHostAndPort($request->getHeaders(), $server); + + $this->assertSame('example.com', $host); + $this->assertNull($port); } public function testMarshalHostAndPortReturnsServerPortForPortWhenPresentWithServerName() @@ -228,10 +233,11 @@ public function testMarshalHostAndPortReturnsServerPortForPortWhenPresentWithSer 'SERVER_NAME' => 'example.com', 'SERVER_PORT' => 8000, ]; - $accumulator = (object) ['host' => '', 'port' => null]; - ServerRequestFactory::marshalHostAndPortFromHeaders($accumulator, $server, $request->getHeaders()); - $this->assertSame('example.com', $accumulator->host); - $this->assertSame(8000, $accumulator->port); + + list($host, $port) = marshalHostAndPort($request->getHeaders(), $server); + + $this->assertSame('example.com', $host); + $this->assertSame(8000, $port); } public function testMarshalHostAndPortReturnsServerNameForHostIfServerAddrPresentButHostIsNotIpv6Address() @@ -243,9 +249,10 @@ public function testMarshalHostAndPortReturnsServerNameForHostIfServerAddrPresen 'SERVER_ADDR' => '127.0.0.1', 'SERVER_NAME' => 'example.com', ]; - $accumulator = (object) ['host' => '', 'port' => null]; - ServerRequestFactory::marshalHostAndPortFromHeaders($accumulator, $server, $request->getHeaders()); - $this->assertSame('example.com', $accumulator->host); + + list($host, $port) = marshalHostAndPort($request->getHeaders(), $server); + + $this->assertSame('example.com', $host); } public function testMarshalHostAndPortReturnsServerAddrForHostIfPresentAndHostIsIpv6Address() @@ -258,10 +265,11 @@ public function testMarshalHostAndPortReturnsServerAddrForHostIfPresentAndHostIs 'SERVER_NAME' => '[FE80::0202:B3FF:FE1E:8329]', 'SERVER_PORT' => 8000, ]; - $accumulator = (object) ['host' => '', 'port' => null]; - ServerRequestFactory::marshalHostAndPortFromHeaders($accumulator, $server, $request->getHeaders()); - $this->assertSame('[FE80::0202:B3FF:FE1E:8329]', $accumulator->host); - $this->assertSame(8000, $accumulator->port); + + list($host, $port) = marshalHostAndPort($request->getHeaders(), $server); + + $this->assertSame('[FE80::0202:B3FF:FE1E:8329]', $host); + $this->assertSame(8000, $port); } public function testMarshalHostAndPortWillDetectPortInIpv6StyleHost() @@ -273,10 +281,11 @@ public function testMarshalHostAndPortWillDetectPortInIpv6StyleHost() 'SERVER_ADDR' => 'FE80::0202:B3FF:FE1E:8329', 'SERVER_NAME' => '[FE80::0202:B3FF:FE1E:8329:80]', ]; - $accumulator = (object) ['host' => '', 'port' => null]; - ServerRequestFactory::marshalHostAndPortFromHeaders($accumulator, $server, $request->getHeaders()); - $this->assertSame('[FE80::0202:B3FF:FE1E:8329]', $accumulator->host); - $this->assertSame(80, $accumulator->port); + + list($host, $port) = marshalHostAndPort($request->getHeaders(), $server); + + $this->assertSame('[FE80::0202:B3FF:FE1E:8329]', $host); + $this->assertSame(80, $port); } public function testMarshalUriDetectsHttpsSchemeFromServerValue() @@ -538,26 +547,20 @@ public function testNormalizeFilesReturnsOnlyActualFilesWhenOriginalFilesContain 'type' => ['file' => 'text/plain'], ]]; - $normalizedFiles = ServerRequestFactory::normalizeFiles($files); + $normalizedFiles = normalizeUploadedFiles($files); $this->assertCount(1, $normalizedFiles['fooFiles']); } public function testMarshalProtocolVersionRisesExceptionIfVersionIsNotRecognized() { - $method = new ReflectionMethod(ServerRequestFactory::class, 'marshalProtocolVersion'); - $method->setAccessible(true); - $this->expectException(UnexpectedValueException::class); - - $method->invoke(null, ['SERVER_PROTOCOL' => 'dadsa/1.0']); + marshalProtocolVersion(['SERVER_PROTOCOL' => 'dadsa/1.0']); } public function testMarshalProtocolReturnsDefaultValueIfHeaderIsNotPresent() { - $method = new ReflectionMethod(ServerRequestFactory::class, 'marshalProtocolVersion'); - $method->setAccessible(true); - $version = $method->invoke(null, []); + $version = marshalProtocolVersion([]); $this->assertSame('1.1', $version); } @@ -566,9 +569,7 @@ public function testMarshalProtocolReturnsDefaultValueIfHeaderIsNotPresent() */ public function testMarshalProtocolVersionReturnsHttpVersions($protocol, $expected) { - $method = new ReflectionMethod(ServerRequestFactory::class, 'marshalProtocolVersion'); - $method->setAccessible(true); - $version = $method->invoke(null, ['SERVER_PROTOCOL' => $protocol]); + $version = marshalProtocolVersion(['SERVER_PROTOCOL' => $protocol]); $this->assertSame($expected, $version); }