Test for cookie strings and allow taking a $_SERVER variable in HttpRequest::fromRequest.

This commit is contained in:
flash 2025-03-08 20:12:57 +00:00
parent 346b8a52e2
commit d5b5efab46
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
4 changed files with 129 additions and 82 deletions

View file

@ -1 +1 @@
0.2503.80055 0.2503.82012

View file

@ -31,7 +31,7 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
* @param string $method HTTP request method. * @param string $method HTTP request method.
* @param HttpUri $uri HTTP request URI. * @param HttpUri $uri HTTP request URI.
* @param array<string, list<?string>> $params HTTP request query parameters. * @param array<string, list<?string>> $params HTTP request query parameters.
* @param array<string, string[]> $cookies HTTP request cookies. * @param array<string, string> $cookies HTTP request cookies.
* @param array<string, array<object|string>>|object|null $parsedBody Parsed body contents. * @param array<string, array<object|string>>|object|null $parsedBody Parsed body contents.
* @param ?array<string, HttpUploadedFile[]> $uploadedFiles Parsed files. * @param ?array<string, HttpUploadedFile[]> $uploadedFiles Parsed files.
*/ */
@ -110,7 +110,7 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
* @param ?string $method Value you'd otherwise pass to withMethod, null to leave unmodified. * @param ?string $method Value you'd otherwise pass to withMethod, null to leave unmodified.
* @param ?UriInterface $uri Value you'd otherwise pass to withUri, null to leave unmodified. * @param ?UriInterface $uri Value you'd otherwise pass to withUri, null to leave unmodified.
* @param ?array<string, list<?string>> $params Value you'd otherwise pass to withQueryParams, null to leave unmodified. * @param ?array<string, list<?string>> $params Value you'd otherwise pass to withQueryParams, null to leave unmodified.
* @param ?array<string, string[]> $cookies Value you'd otherwise pass to withCookiesParams, null to leave unmodified. * @param ?array<string, string> $cookies Value you'd otherwise pass to withCookiesParams, null to leave unmodified.
* @param null|array<string, string[]|object[]>|object|false $parsedBody Value you'd otherwise pass to withParsedBody, false to leave unmodified. * @param null|array<string, string[]|object[]>|object|false $parsedBody Value you'd otherwise pass to withParsedBody, false to leave unmodified.
* @param array<string, HttpUploadedFile[]>|null|false $uploadedFiles Value you'd otherwise pass to withUploadedFiles, false to leave unmodified. * @param array<string, HttpUploadedFile[]>|null|false $uploadedFiles Value you'd otherwise pass to withUploadedFiles, false to leave unmodified.
* @throws InvalidArgumentException If any of the arguments are not acceptable. * @throws InvalidArgumentException If any of the arguments are not acceptable.
@ -210,14 +210,14 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
} }
/** /**
* @return array<string, string[]> * @return array<string, string>
*/ */
public function getCookieParams(): array { public function getCookieParams(): array {
return $this->cookies; return $this->cookies;
} }
/** /**
* @param array<string, string[]> $cookies Array of key/value pairs representing cookies. * @param array<string, string> $cookies Array of key/value pairs representing cookies.
*/ */
public function withCookieParams(array $cookies): HttpRequest { public function withCookieParams(array $cookies): HttpRequest {
return $this->with(cookies: $cookies); return $this->with(cookies: $cookies);
@ -234,19 +234,18 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
} }
/** /**
* Retrieves an HTTP request cookie, or null if it is not present. * Retrieves an HTTP request cookie, or a default value if it is not present.
* *
* @todo UPDATE FOR THE NEW SYNTAX THINGY WITH THE LIST<STRING>!!!!!!!!!!!
* @param string $name Name of the request cookie. * @param string $name Name of the request cookie.
* @param int $filter A PHP filter extension filter constant. * @param int $filter A PHP filter extension filter constant.
* @param array<string, mixed>|int $options Options for the PHP filter. * @param array<string, mixed>|int $options Options for the PHP filter.
* @return mixed Value of the cookie, null if not present. * @param mixed $default Default value.
* @return mixed
*/ */
public function getCookie(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed { public function getCookie(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0, mixed $default = null): mixed {
if(!isset($this->cookies[$name])) return isset($this->cookies[$name])
return null; ? (filter_var($this->cookies[$name], $filter, $options) ?? $default)
: $default;
return filter_var($this->cookies[$name], $filter, $options);
} }
/** /**
@ -263,16 +262,6 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
return $this->with(params: $query); return $this->with(params: $query);
} }
/**
* Checks if a query field is present.
*
* @param string $name Name of the query field.
* @return bool true if the field is present, false if not.
*/
public function hasParam(string $name): bool {
return isset($this->params[$name]);
}
/** /**
* Retrieves all HTTP request query fields as a query string. * Retrieves all HTTP request query fields as a query string.
* *
@ -285,24 +274,68 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
: http_build_query($this->params, '', '&', $spacesAsPlus ? PHP_QUERY_RFC1738 : PHP_QUERY_RFC3986); : http_build_query($this->params, '', '&', $spacesAsPlus ? PHP_QUERY_RFC1738 : PHP_QUERY_RFC3986);
} }
/**
* Checks if a query field is present.
*
* @param string $name Name of the query field.
* @return bool true if the field is present, false if not.
*/
public function hasParam(string $name): bool {
return isset($this->params[$name]);
}
/** /**
* Retrieves an HTTP request query field, or null if it is not present. * Retrieves an HTTP request query field, or null if it is not present.
* *
* @todo UPDATE FOR THE NEW SYNTAX THINGY WITH THE LIST<?STRING>!!!!!!!!!!!
* @param string $name Name of the request query field. * @param string $name Name of the request query field.
* @param int $filter A PHP filter extension filter constant. * @param int $filter A PHP filter extension filter constant.
* @param mixed[]|int $options Options for the PHP filter. * @param mixed[]|int $options Options for the PHP filter.
* @param ?scalar $default Default value to fall back on. * @param mixed $default Default value to fall back on.
* @return ?scalar Value of the query field, null if not present. * @return mixed Value of the query field, null if not present.
*/ */
public function getParam(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0, mixed $default = null): mixed { public function getParam(
if(!isset($this->params[$name])) string $name,
int $filter = FILTER_DEFAULT,
array|int $options = 0,
mixed $default = null,
): mixed {
return $this->getParamAt($name, 0, $filter, $options, $default);
}
/**
* Retrieves an HTTP request query field, or null if it is not present.
*
* @param string $name Name of the request query field.
* @param int $index Index of the parameter value.
* @param int $filter A PHP filter extension filter constant.
* @param mixed[]|int $options Options for the PHP filter.
* @param mixed $default Default value to fall back on.
* @return mixed Value of the query field, null if not present.
*/
public function getParamAt(
string $name,
int $index,
int $filter = FILTER_DEFAULT,
array|int $options = 0,
mixed $default = null,
): mixed {
if(!isset($this->params[$name]) && isset($this->params[$name][$index]))
return $default; return $default;
$value = filter_var($this->params[$name], $filter, $options); $value = filter_var($this->params[$name][$index], $filter, $options);
return is_scalar($value) ? $value : $default; return is_scalar($value) ? $value : $default;
} }
/**
* Retrieves how many parameters of the same name appear in the HTTP request query field.
*
* @param string $name Name of the request query field.
* @return int
*/
public function getParamCount(string $name): int {
return isset($this->params[$name]) ? count($this->params[$name]) : 0;
}
/** /**
* @return array<string, HttpUploadedFile[]> An array tree of UploadedFileInterface instances; an empty array MUST be returned if no data is present. * @return array<string, HttpUploadedFile[]> An array tree of UploadedFileInterface instances; an empty array MUST be returned if no data is present.
*/ */
@ -364,7 +397,7 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
* Parses a Cookie header string. * Parses a Cookie header string.
* *
* @param string $cookies Cookie header string. * @param string $cookies Cookie header string.
* @return array<string, string[]> * @return array<string, string>
*/ */
public static function parseCookieString(string $cookies): array { public static function parseCookieString(string $cookies): array {
$params = []; $params = [];
@ -375,11 +408,8 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
$parts = explode('=', ltrim($paramPart, ' '), 2); $parts = explode('=', ltrim($paramPart, ' '), 2);
if(count($parts) > 1) { if(count($parts) > 1) {
$name = urldecode($parts[0]); $name = urldecode($parts[0]);
$value = urldecode($parts[1]); if(!array_key_exists($name, $params))
if(array_key_exists($name, $params)) $params[$name] = urldecode($parts[1]);
$params[$name][] = $value;
else
$params[$name] = [$value];
} }
} }
} }
@ -390,20 +420,23 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
/** /**
* Creates an HttpRequest instance from the current request. * Creates an HttpRequest instance from the current request.
* *
* @param ?array<string, mixed> $server Value of the $_SERVER variable, if null the current super-global $_SERVER is used.
* @return HttpRequest An instance representing the current request. * @return HttpRequest An instance representing the current request.
*/ */
public static function fromRequest(): HttpRequest { public static function fromRequest(?array $server = null): HttpRequest {
/** @var array<string, mixed> $server */
$server ??= $_SERVER;
$build = new HttpRequestBuilder; $build = new HttpRequestBuilder;
$build->remoteAddress = (string)filter_input(INPUT_SERVER, 'REMOTE_ADDR'); $build->remoteAddress = (string)filter_var($server['REMOTE_ADDR'] ?? '::');
$build->secure = filter_has_var(INPUT_SERVER, 'HTTPS'); $build->secure = !empty($server['HTTPS']);
$build->protocolVersion = (string)filter_input(INPUT_SERVER, 'SERVER_PROTOCOL'); $build->protocolVersion = (string)filter_var($server['SERVER_PROTOCOL'] ?? '1.1');
$build->method = (string)filter_input(INPUT_SERVER, 'REQUEST_METHOD'); $build->method = (string)filter_var($server['REQUEST_METHOD'] ?? 'GET');
if(filter_has_var(INPUT_SERVER, 'COUNTRY_CODE')) if(!empty($server['COUNTRY_CODE']))
$build->countryCode = (string)filter_input(INPUT_SERVER, 'COUNTRY_CODE'); $build->countryCode = (string)filter_var($server['COUNTRY_CODE']);
// this currently doesn't "properly" support the scenario where a full url is specified in the http request // this currently doesn't "properly" support the scenario where a full url is specified in the http request
$path = (string)filter_input(INPUT_SERVER, 'REQUEST_URI'); $path = (string)filter_var($server['REQUEST_URI'] ?? '/');
$pathQueryOffset = strpos($path, '?'); $pathQueryOffset = strpos($path, '?');
if($pathQueryOffset !== false) if($pathQueryOffset !== false)
$path = substr($path, 0, $pathQueryOffset); $path = substr($path, 0, $pathQueryOffset);
@ -417,12 +450,12 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
$path = '/' . $path; $path = '/' . $path;
$build->path = $path; $build->path = $path;
$build->params = HttpUri::parseQueryString((string)filter_input(INPUT_SERVER, 'QUERY_STRING')); $build->params = HttpUri::parseQueryString((string)filter_var($server['QUERY_STRING'] ?? ''));
$contentType = null; $contentType = null;
$contentLength = 0; $contentLength = 0;
$headers = self::getRawRequestHeaders(); $headers = self::getRawRequestHeaders($server);
foreach($headers as $name => $value) { foreach($headers as $name => $value) {
if($name === 'content-type') if($name === 'content-type')
try { try {
@ -438,13 +471,16 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
$build->setHeader($name, $value); $build->setHeader($name, $value);
} }
$build->body = null; $build->body = HttpStream::createStreamFromFile('php://input', 'rb');
return $build->toRequest(); return $build->toRequest();
} }
/** @return array<string, string> */ /**
private static function getRawRequestHeaders(): array { * @param ?array<string, mixed> $server
* @return array<string, string>
*/
private static function getRawRequestHeaders(?array $server = null): array {
if(function_exists('getallheaders')) { if(function_exists('getallheaders')) {
$raw = getallheaders(); $raw = getallheaders();
$headers = []; $headers = [];
@ -455,9 +491,10 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
return $headers; return $headers;
} }
$server ??= $_SERVER;
$headers = []; $headers = [];
foreach($_SERVER as $key => $value) { foreach($server as $key => $value) {
if(!is_string($key) || !is_scalar($value)) if(!is_string($key) || !is_scalar($value))
continue; continue;
@ -472,16 +509,16 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
} }
if(!isset($headers['authorization'])) { if(!isset($headers['authorization'])) {
if(filter_has_var(INPUT_SERVER, 'REDIRECT_HTTP_AUTHORIZATION')) { if(!empty($server['REDIRECT_HTTP_AUTHORIZATION'])) {
$headers['authorization'] = (string)filter_input(INPUT_SERVER, 'REDIRECT_HTTP_AUTHORIZATION'); $headers['authorization'] = (string)filter_var($server['REDIRECT_HTTP_AUTHORIZATION']);
} elseif(filter_has_var(INPUT_SERVER, 'PHP_AUTH_USER')) { } elseif(!empty($server['PHP_AUTH_USER'])) {
$headers['authorization'] = sprintf('Basic %s', base64_encode(sprintf( $headers['authorization'] = sprintf('Basic %s', base64_encode(sprintf(
'%s:%s', '%s:%s',
(string)filter_input(INPUT_SERVER, 'PHP_AUTH_USER'), (string)filter_var($server['PHP_AUTH_USER']),
(string)filter_input(INPUT_SERVER, 'PHP_AUTH_PW') (string)filter_var($server['PHP_AUTH_PW'])
))); )));
} elseif(filter_has_var(INPUT_SERVER, 'PHP_AUTH_DIGEST')) { } elseif(!empty($server['PHP_AUTH_DIGEST'])) {
$headers['authorization'] = (string)filter_input(INPUT_SERVER, 'PHP_AUTH_DIGEST'); $headers['authorization'] = (string)filter_var($server['PHP_AUTH_DIGEST']);
} }
} }

View file

@ -51,7 +51,7 @@ final class HttpRequestBuilder extends HttpMessageBuilder {
/** /**
* HTTP request cookies. * HTTP request cookies.
* *
* @var array<string, string[]> * @var array<string, string>
*/ */
public array $cookies = []; public array $cookies = [];
@ -104,30 +104,7 @@ final class HttpRequestBuilder extends HttpMessageBuilder {
* @param string $value Value of the cookie. * @param string $value Value of the cookie.
*/ */
public function setCookie(string $name, string $value): void { public function setCookie(string $name, string $value): void {
$this->cookies[$name] = [$value]; $this->cookies[$name] = $value;
}
/**
* Adds a value for a HTTP request cookie.
*
* @param string $name Name of the cookie.
* @param string $value Value of the cookie.
*/
public function addCookie(string $name, string $value): void {
if(array_key_exists($name, $this->cookies))
$this->cookies[$name][] = $value;
else
$this->cookies[$name] = [$value];
}
/**
* Sets values for a HTTP request cookie.
*
* @param string $name Name of the cookie.
* @param string[] $values Values of the cookie.
*/
public function setCookieValues(string $name, array $values): void {
$this->cookies[$name] = $values;
} }
/** /**

View file

@ -5,12 +5,13 @@
declare(strict_types=1); declare(strict_types=1);
use Index\Http\HttpUri; use Index\Http\{HttpRequest,HttpUri};
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\{CoversClass,DataProvider}; use PHPUnit\Framework\Attributes\{CoversClass,DataProvider};
use Psr\Http\Message\UriInterface; use Psr\Http\Message\UriInterface;
// based on https://github.com/bakame-php/psr7-uri-interface-tests/blob/5a556fdfe668a6c6a14772efeba6134c0b7dae34/tests/AbstractUriTestCase.php // based on https://github.com/bakame-php/psr7-uri-interface-tests/blob/5a556fdfe668a6c6a14772efeba6134c0b7dae34/tests/AbstractUriTestCase.php
#[CoversClass(HttpRequest::class)]
#[CoversClass(HttpUri::class)] #[CoversClass(HttpUri::class)]
final class HttpUriTest extends TestCase { final class HttpUriTest extends TestCase {
private const string URI = 'http://username:pwd@secure.example.com:443/meow/soap.php?soup=beans#mewow'; private const string URI = 'http://username:pwd@secure.example.com:443/meow/soap.php?soup=beans#mewow';
@ -502,4 +503,36 @@ final class HttpUriTest extends TestCase {
public function testBuildQueryString(array $queryParams, string $expected): void { public function testBuildQueryString(array $queryParams, string $expected): void {
$this->assertEquals(HttpUri::buildQueryString($queryParams), $expected); $this->assertEquals(HttpUri::buildQueryString($queryParams), $expected);
} }
/** @return array<array{0: string, 1: array<string, string>}> */
public static function cookieStringProvider(): array {
return [
[
'',
[]
],
[
'soup=meow',
['soup' => 'meow']
],
[
'soup=meow;the=the1; the2=the3; the4=the5;',
['soup' => 'meow', 'the' => 'the1', 'the2' => 'the3', 'the4' => 'the5']
],
[
'test%5B%5D=%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82',
['test[]' => 'あああああああ']
],
[
' =empty',
['' => 'empty']
],
];
}
/** @param array<string, string> $expected */
#[DataProvider('cookieStringProvider')]
public function testParseCookieString(string $cookieString, array $expected): void {
$this->assertEquals(HttpRequest::parseCookieString($cookieString), $expected);
}
} }