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 HttpUri $uri HTTP request URI.
* @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, 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 ?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, 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 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.
@ -210,14 +210,14 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
}
/**
* @return array<string, string[]>
* @return array<string, string>
*/
public function getCookieParams(): array {
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 {
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 int $filter A PHP filter extension filter constant.
* @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 {
if(!isset($this->cookies[$name]))
return null;
return filter_var($this->cookies[$name], $filter, $options);
public function getCookie(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0, mixed $default = null): mixed {
return isset($this->cookies[$name])
? (filter_var($this->cookies[$name], $filter, $options) ?? $default)
: $default;
}
/**
@ -263,16 +262,6 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
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.
*
@ -285,24 +274,68 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
: 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.
*
* @todo UPDATE FOR THE NEW SYNTAX THINGY WITH THE LIST<?STRING>!!!!!!!!!!!
* @param string $name Name of the request query field.
* @param int $filter A PHP filter extension filter constant.
* @param mixed[]|int $options Options for the PHP filter.
* @param ?scalar $default Default value to fall back on.
* @return ?scalar Value of the query field, null if not present.
* @param mixed $default Default value to fall back on.
* @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 {
if(!isset($this->params[$name]))
public function getParam(
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;
$value = filter_var($this->params[$name], $filter, $options);
$value = filter_var($this->params[$name][$index], $filter, $options);
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.
*/
@ -364,7 +397,7 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
* Parses a Cookie header string.
*
* @param string $cookies Cookie header string.
* @return array<string, string[]>
* @return array<string, string>
*/
public static function parseCookieString(string $cookies): array {
$params = [];
@ -375,11 +408,8 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
$parts = explode('=', ltrim($paramPart, ' '), 2);
if(count($parts) > 1) {
$name = urldecode($parts[0]);
$value = urldecode($parts[1]);
if(array_key_exists($name, $params))
$params[$name][] = $value;
else
$params[$name] = [$value];
if(!array_key_exists($name, $params))
$params[$name] = urldecode($parts[1]);
}
}
}
@ -390,20 +420,23 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
/**
* 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.
*/
public static function fromRequest(): HttpRequest {
public static function fromRequest(?array $server = null): HttpRequest {
/** @var array<string, mixed> $server */
$server ??= $_SERVER;
$build = new HttpRequestBuilder;
$build->remoteAddress = (string)filter_input(INPUT_SERVER, 'REMOTE_ADDR');
$build->secure = filter_has_var(INPUT_SERVER, 'HTTPS');
$build->protocolVersion = (string)filter_input(INPUT_SERVER, 'SERVER_PROTOCOL');
$build->method = (string)filter_input(INPUT_SERVER, 'REQUEST_METHOD');
$build->remoteAddress = (string)filter_var($server['REMOTE_ADDR'] ?? '::');
$build->secure = !empty($server['HTTPS']);
$build->protocolVersion = (string)filter_var($server['SERVER_PROTOCOL'] ?? '1.1');
$build->method = (string)filter_var($server['REQUEST_METHOD'] ?? 'GET');
if(filter_has_var(INPUT_SERVER, 'COUNTRY_CODE'))
$build->countryCode = (string)filter_input(INPUT_SERVER, 'COUNTRY_CODE');
if(!empty($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
$path = (string)filter_input(INPUT_SERVER, 'REQUEST_URI');
$path = (string)filter_var($server['REQUEST_URI'] ?? '/');
$pathQueryOffset = strpos($path, '?');
if($pathQueryOffset !== false)
$path = substr($path, 0, $pathQueryOffset);
@ -417,12 +450,12 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
$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;
$contentLength = 0;
$headers = self::getRawRequestHeaders();
$headers = self::getRawRequestHeaders($server);
foreach($headers as $name => $value) {
if($name === 'content-type')
try {
@ -438,13 +471,16 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
$build->setHeader($name, $value);
}
$build->body = null;
$build->body = HttpStream::createStreamFromFile('php://input', 'rb');
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')) {
$raw = getallheaders();
$headers = [];
@ -455,9 +491,10 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
return $headers;
}
$server ??= $_SERVER;
$headers = [];
foreach($_SERVER as $key => $value) {
foreach($server as $key => $value) {
if(!is_string($key) || !is_scalar($value))
continue;
@ -472,16 +509,16 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
}
if(!isset($headers['authorization'])) {
if(filter_has_var(INPUT_SERVER, 'REDIRECT_HTTP_AUTHORIZATION')) {
$headers['authorization'] = (string)filter_input(INPUT_SERVER, 'REDIRECT_HTTP_AUTHORIZATION');
} elseif(filter_has_var(INPUT_SERVER, 'PHP_AUTH_USER')) {
if(!empty($server['REDIRECT_HTTP_AUTHORIZATION'])) {
$headers['authorization'] = (string)filter_var($server['REDIRECT_HTTP_AUTHORIZATION']);
} elseif(!empty($server['PHP_AUTH_USER'])) {
$headers['authorization'] = sprintf('Basic %s', base64_encode(sprintf(
'%s:%s',
(string)filter_input(INPUT_SERVER, 'PHP_AUTH_USER'),
(string)filter_input(INPUT_SERVER, 'PHP_AUTH_PW')
(string)filter_var($server['PHP_AUTH_USER']),
(string)filter_var($server['PHP_AUTH_PW'])
)));
} elseif(filter_has_var(INPUT_SERVER, 'PHP_AUTH_DIGEST')) {
$headers['authorization'] = (string)filter_input(INPUT_SERVER, 'PHP_AUTH_DIGEST');
} elseif(!empty($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.
*
* @var array<string, string[]>
* @var array<string, string>
*/
public array $cookies = [];
@ -104,30 +104,7 @@ final class HttpRequestBuilder extends HttpMessageBuilder {
* @param string $value Value of the cookie.
*/
public function setCookie(string $name, string $value): void {
$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;
$this->cookies[$name] = $value;
}
/**

View file

@ -5,12 +5,13 @@
declare(strict_types=1);
use Index\Http\HttpUri;
use Index\Http\{HttpRequest,HttpUri};
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\{CoversClass,DataProvider};
use Psr\Http\Message\UriInterface;
// based on https://github.com/bakame-php/psr7-uri-interface-tests/blob/5a556fdfe668a6c6a14772efeba6134c0b7dae34/tests/AbstractUriTestCase.php
#[CoversClass(HttpRequest::class)]
#[CoversClass(HttpUri::class)]
final class HttpUriTest extends TestCase {
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 {
$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);
}
}