Implemented default processors for form handling.
This commit is contained in:
parent
cc351a2f05
commit
ee48bb5bd6
12 changed files with 486 additions and 219 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.2503.142359
|
||||
0.2503.150208
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// Dependencies.php
|
||||
// Created: 2025-01-18
|
||||
// Updated: 2025-03-07
|
||||
// Updated: 2025-03-15
|
||||
|
||||
namespace Index;
|
||||
|
||||
|
@ -50,7 +50,7 @@ class Dependencies {
|
|||
try {
|
||||
$typeInfo = $paramInfo->getType();
|
||||
$allowsNull = $typeInfo === null || $typeInfo->allowsNull();
|
||||
$value = null;
|
||||
$value = $paramInfo->isDefaultValueAvailable() ? $paramInfo->getDefaultValue() : null;
|
||||
|
||||
// maybe support intersection and union someday
|
||||
if($typeInfo instanceof ReflectionNamedType && !$typeInfo->isBuiltin()) {
|
||||
|
|
|
@ -1,66 +1,16 @@
|
|||
<?php
|
||||
// FormContent.php
|
||||
// Created: 2025-03-12
|
||||
// Updated: 2025-03-14
|
||||
// Updated: 2025-03-15
|
||||
|
||||
namespace Index\Http\Content;
|
||||
|
||||
use Iterator;
|
||||
use Index\Http\HttpParameters;
|
||||
|
||||
/**
|
||||
* Provides a common interface for application/x-www-form-urlencoded and multipart/form-data.
|
||||
*
|
||||
* @template TValue of string|\Stringable
|
||||
* @extends Iterator<string, ?list<TValue>>
|
||||
* @extends Iterator<string, ?list<string|\Stringable>>
|
||||
*/
|
||||
interface FormContent extends Content, Iterator {
|
||||
/**
|
||||
* Checks if a form field is present.
|
||||
*
|
||||
* @param string $name Name of the form field.
|
||||
* @return bool
|
||||
*/
|
||||
public function hasParam(string $name): bool;
|
||||
|
||||
/**
|
||||
* Retrieves an HTTP request form field, or null if it is not present.
|
||||
*
|
||||
* @param string $name Name of the request form field.
|
||||
* @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 form field, null if not present.
|
||||
*/
|
||||
public function getParam(
|
||||
string $name,
|
||||
int $filter = FILTER_DEFAULT,
|
||||
array|int $options = 0,
|
||||
mixed $default = null,
|
||||
): mixed;
|
||||
|
||||
/**
|
||||
* Retrieves an HTTP request form field, or null if it is not present.
|
||||
*
|
||||
* @param string $name Name of the request form 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 form field, null if not present.
|
||||
*/
|
||||
public function getParamAt(
|
||||
string $name,
|
||||
int $index,
|
||||
int $filter = FILTER_DEFAULT,
|
||||
array|int $options = 0,
|
||||
mixed $default = null,
|
||||
): mixed;
|
||||
|
||||
/**
|
||||
* 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<0, max> Amount of values.
|
||||
*/
|
||||
public function getParamCount(string $name): int;
|
||||
}
|
||||
interface FormContent extends Content, HttpParameters, Iterator {}
|
||||
|
|
|
@ -1,21 +1,25 @@
|
|||
<?php
|
||||
// MultipartFormContent.php
|
||||
// Created: 2025-03-12
|
||||
// Updated: 2025-03-14
|
||||
// Updated: 2025-03-15
|
||||
|
||||
namespace Index\Http\Content;
|
||||
|
||||
use Iterator;
|
||||
use RuntimeException;
|
||||
use Index\Http\HttpParametersCommon;
|
||||
use Index\Http\Content\Multipart\{FileMultipartFormData,MultipartFormData,ValueMultipartFormData};
|
||||
use Index\Http\Streams\{ScopedStream,Stream,StreamBuffer};
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* Implements multipart/form-data.
|
||||
*
|
||||
* @implements FormContent<MultipartFormData>
|
||||
|
||||
* @implements Iterator<string, ?list<MultipartFormData>>
|
||||
*/
|
||||
class MultipartFormContent implements FormContent {
|
||||
class MultipartFormContent implements FormContent, Iterator {
|
||||
use HttpParametersCommon;
|
||||
|
||||
/** @var string[] */
|
||||
private array $keys;
|
||||
|
||||
|
@ -59,19 +63,6 @@ class MultipartFormContent implements FormContent {
|
|||
++$this->position;
|
||||
}
|
||||
|
||||
public function hasParam(string $name): bool {
|
||||
return 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 value of the form field, including additional data.
|
||||
*
|
||||
|
@ -82,20 +73,6 @@ class MultipartFormContent implements FormContent {
|
|||
return $this->getParamDataAt($name, 0);
|
||||
}
|
||||
|
||||
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][$index], $filter, $options);
|
||||
return is_scalar($value) ? $value : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves value of the form field, including additional data.
|
||||
*
|
||||
|
@ -108,10 +85,6 @@ class MultipartFormContent implements FormContent {
|
|||
? $this->params[$name][$index] : null;
|
||||
}
|
||||
|
||||
public function getParamCount(string $name): int {
|
||||
return isset($this->params[$name]) ? count($this->params[$name]) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses multipart form data in a stream.
|
||||
*
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
<?php
|
||||
// UrlEncodedFormContent.php
|
||||
// Created: 2025-03-12
|
||||
// Updated: 2025-03-14
|
||||
// Updated: 2025-03-15
|
||||
|
||||
namespace Index\Http\Content;
|
||||
|
||||
use Index\Http\HttpUri;
|
||||
use Index\Http\{HttpParametersCommon,HttpUri};
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* Implements application/x-www-form-urlencoded.
|
||||
*
|
||||
* @implements FormContent<string>
|
||||
*/
|
||||
class UrlEncodedFormContent implements FormContent {
|
||||
use HttpParametersCommon;
|
||||
|
||||
/** @var string[] */
|
||||
private array $keys;
|
||||
|
||||
|
@ -55,37 +55,6 @@ class UrlEncodedFormContent implements FormContent {
|
|||
++$this->position;
|
||||
}
|
||||
|
||||
public function hasParam(string $name): bool {
|
||||
return 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);
|
||||
}
|
||||
|
||||
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][$index], $filter, $options);
|
||||
return is_scalar($value) ? $value : $default;
|
||||
}
|
||||
|
||||
public function getParamCount(string $name): int {
|
||||
return isset($this->params[$name]) ? count($this->params[$name]) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses URL encoded form params in a stream.
|
||||
*
|
||||
|
|
87
src/Http/HttpParameters.php
Normal file
87
src/Http/HttpParameters.php
Normal file
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
// HttpParameters.php
|
||||
// Created: 2025-03-15
|
||||
// Updated: 2025-03-15
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
/**
|
||||
* Common definition for HTTP parameters.
|
||||
*/
|
||||
interface HttpParameters {
|
||||
/**
|
||||
* Checks if a parameter is present.
|
||||
*
|
||||
* @param string $name Name of the parameter.
|
||||
* @return bool
|
||||
*/
|
||||
public function hasParam(string $name): bool;
|
||||
|
||||
/**
|
||||
* 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<0, max> Amount of values.
|
||||
*/
|
||||
public function getParamCount(string $name): int;
|
||||
|
||||
/**
|
||||
* Retrieves a parameter, or a default value if it is not present.
|
||||
*
|
||||
* @param string $name Name of the parameter.
|
||||
* @param ?string $default Default value to fall back on.
|
||||
* @return ?string Value of the parameter, $default if not present.
|
||||
*/
|
||||
public function getParam(
|
||||
string $name,
|
||||
?string $default = null,
|
||||
): ?string;
|
||||
|
||||
/**
|
||||
* Retrieves a parameter, or a default value if it is not present.
|
||||
*
|
||||
* @param string $name Name of the parameter.
|
||||
* @param int $index Index of the parameter value.
|
||||
* @param ?string $default Default value to fall back on.
|
||||
* @return ?string Value of the parameter, null if not present.
|
||||
*/
|
||||
public function getParamAt(
|
||||
string $name,
|
||||
int $index,
|
||||
?string $default = null,
|
||||
): ?string;
|
||||
|
||||
/**
|
||||
* Retrieves a filtered parameter, or a default value if it is not present.
|
||||
*
|
||||
* @param string $name Name of the parameter.
|
||||
* @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 parameter, default value if not present.
|
||||
*/
|
||||
public function getFilteredParam(
|
||||
string $name,
|
||||
int $filter = FILTER_DEFAULT,
|
||||
array|int $options = 0,
|
||||
mixed $default = null,
|
||||
): mixed;
|
||||
|
||||
/**
|
||||
* Retrieves a filtered parameter, or a default value if it is not present.
|
||||
*
|
||||
* @param string $name Name of the parameter.
|
||||
* @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 parameter, default value if not present.
|
||||
*/
|
||||
public function getFilteredParamAt(
|
||||
string $name,
|
||||
int $index,
|
||||
int $filter = FILTER_DEFAULT,
|
||||
array|int $options = 0,
|
||||
mixed $default = null,
|
||||
): mixed;
|
||||
}
|
102
src/Http/HttpParametersCommon.php
Normal file
102
src/Http/HttpParametersCommon.php
Normal file
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
// HttpParametersCommon.php
|
||||
// Created: 2025-03-15
|
||||
// Updated: 2025-03-15
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
/**
|
||||
* Common implementation for HTTP parameters.
|
||||
*/
|
||||
trait HttpParametersCommon {
|
||||
/**
|
||||
* Checks if a parameter is present.
|
||||
*
|
||||
* @param string $name Name of the parameter.
|
||||
* @return bool
|
||||
*/
|
||||
public function hasParam(string $name): bool {
|
||||
return isset($this->params[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<0, max> Amount of values.
|
||||
*/
|
||||
public function getParamCount(string $name): int {
|
||||
return isset($this->params[$name]) ? count($this->params[$name]) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a parameter, or a default value if it is not present.
|
||||
*
|
||||
* @param string $name Name of the parameter.
|
||||
* @param ?string $default Default value to fall back on.
|
||||
* @return ?string Value of the parameter, $default if not present.
|
||||
*/
|
||||
public function getParam(
|
||||
string $name,
|
||||
?string $default = null,
|
||||
): ?string {
|
||||
return $this->getParamAt($name, 0, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a parameter, or a default value if it is not present.
|
||||
*
|
||||
* @param string $name Name of the parameter.
|
||||
* @param int $index Index of the parameter value.
|
||||
* @param ?string $default Default value to fall back on.
|
||||
* @return ?string Value of the parameter, null if not present.
|
||||
*/
|
||||
public function getParamAt(
|
||||
string $name,
|
||||
int $index,
|
||||
?string $default = null,
|
||||
): ?string {
|
||||
return isset($this->params[$name]) && isset($this->params[$name][$index])
|
||||
? $this->params[$name][$index] : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a filtered parameter, or a default value if it is not present.
|
||||
*
|
||||
* @param string $name Name of the parameter.
|
||||
* @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 parameter, default value if not present.
|
||||
*/
|
||||
public function getFilteredParam(
|
||||
string $name,
|
||||
int $filter = FILTER_DEFAULT,
|
||||
array|int $options = 0,
|
||||
mixed $default = null,
|
||||
): mixed {
|
||||
return $this->getFilteredParamAt($name, 0, $filter, $options, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a filtered parameter, or a default value if it is not present.
|
||||
*
|
||||
* @param string $name Name of the parameter.
|
||||
* @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 parameter, default value if not present.
|
||||
*/
|
||||
public function getFilteredParamAt(
|
||||
string $name,
|
||||
int $index,
|
||||
int $filter = FILTER_DEFAULT,
|
||||
array|int $options = 0,
|
||||
mixed $default = null,
|
||||
): mixed {
|
||||
return isset($this->params[$name]) && isset($this->params[$name][$index])
|
||||
? filter_var($this->params[$name][$index], $filter, $options)
|
||||
: $default;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpRequest.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2025-03-12
|
||||
// Updated: 2025-03-15
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -9,14 +9,16 @@ use RuntimeException;
|
|||
use InvalidArgumentException;
|
||||
use Index\MediaType;
|
||||
use Index\Http\Content\{MultipartFormContent,UrlEncodedFormContent};
|
||||
use Index\Http\Streams\Stream;
|
||||
use Index\Http\Streams\{NullStream,Stream};
|
||||
use Index\Json\JsonHttpContent;
|
||||
use Psr\Http\Message\{ServerRequestInterface,StreamInterface,UploadedFileInterface,UriInterface};
|
||||
|
||||
/**
|
||||
* Represents a HTTP request message.
|
||||
*/
|
||||
class HttpRequest extends HttpMessage implements ServerRequestInterface {
|
||||
class HttpRequest extends HttpMessage implements ServerRequestInterface, HttpParameters {
|
||||
use HttpParametersCommon;
|
||||
|
||||
/** Name of attribute containing the country code value. */
|
||||
public const string ATTR_COUNTRY_CODE = 'countryCode';
|
||||
|
||||
|
@ -277,68 +279,6 @@ 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.
|
||||
*
|
||||
* @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 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 {
|
||||
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][$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<0, max> Amount of values.
|
||||
*/
|
||||
public function getParamCount(string $name): int {
|
||||
return isset($this->params[$name]) ? count($this->params[$name]) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, UploadedFileInterface[]> An array tree of UploadedFileInterface instances; an empty array MUST be returned if no data is present.
|
||||
*/
|
||||
|
@ -390,6 +330,13 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
|
|||
return $this->with(parsedBody: $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts a PSR-7 ServerRequestInterface to an Index HttpRequest.
|
||||
* If $request was already HttpRequest, it will be returned verbatim.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return HttpRequest
|
||||
*/
|
||||
public static function castRequest(ServerRequestInterface $request): HttpRequest {
|
||||
if($request instanceof HttpRequest)
|
||||
return $request;
|
||||
|
@ -438,6 +385,58 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a HttpRequest with some given parameters, exists for testing.
|
||||
*
|
||||
* @param string $method Request method.
|
||||
* @param string $path Request target.
|
||||
* @param array<string, list<string>> $headers Request headers.
|
||||
* @return HttpRequest
|
||||
*/
|
||||
public static function createRequestWithoutBody(
|
||||
string $method,
|
||||
string $path,
|
||||
array $headers = [],
|
||||
): HttpRequest {
|
||||
return new HttpRequest(
|
||||
'1.1',
|
||||
$headers,
|
||||
NullStream::instance(),
|
||||
[],
|
||||
$method,
|
||||
HttpUri::createUri($path),
|
||||
[],
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a HttpRequest with some given parameters, exists for testing.
|
||||
*
|
||||
* @param string $method Request method.
|
||||
* @param string $path Request target.
|
||||
* @param array<string, list<string>> $headers Request headers.
|
||||
* @param StreamInterface $body Request body.
|
||||
* @return HttpRequest
|
||||
*/
|
||||
public static function createRequestWithBody(
|
||||
string $method,
|
||||
string $path,
|
||||
array $headers,
|
||||
StreamInterface $body,
|
||||
): HttpRequest {
|
||||
return new HttpRequest(
|
||||
'1.1',
|
||||
$headers,
|
||||
$body,
|
||||
[],
|
||||
$method,
|
||||
HttpUri::createUri($path),
|
||||
[],
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a Cookie header string.
|
||||
*
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// Router.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2025-03-12
|
||||
// Updated: 2025-03-15
|
||||
|
||||
namespace Index\Http\Routing;
|
||||
|
||||
|
@ -47,9 +47,15 @@ class Router implements RequestHandlerInterface {
|
|||
|
||||
/**
|
||||
* @param ErrorHandler|'html'|'plain' $errorHandler Error handling to use for error responses with an empty body. 'html' for the default HTML implementation, 'plain' for the plaintext implementation.
|
||||
* @param bool $registerDefaultProcessors Whether the default processors should be registered.
|
||||
*/
|
||||
public function __construct(ErrorHandler|string $errorHandler = 'html') {
|
||||
public function __construct(
|
||||
ErrorHandler|string $errorHandler = 'html',
|
||||
bool $registerDefaultProcessors = true
|
||||
) {
|
||||
$this->setErrorHandler($errorHandler);
|
||||
if($registerDefaultProcessors)
|
||||
$this->register(new RouterProcessors);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
68
src/Http/Routing/RouterProcessors.php
Normal file
68
src/Http/Routing/RouterProcessors.php
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
// RouterProcessors.php
|
||||
// Created: 2025-03-15
|
||||
// Updated: 2025-03-15
|
||||
|
||||
namespace Index\Http\Routing;
|
||||
|
||||
use RuntimeException;
|
||||
use Index\MediaType;
|
||||
use Index\Http\Content\{MultipartFormContent,UrlEncodedFormContent};
|
||||
use Index\Http\Routing\Processors\{Postprocessor,Preprocessor};
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* Implements some standard route handlers, like parsing form data and outputting json.
|
||||
*/
|
||||
class RouterProcessors implements RouteHandler {
|
||||
use RouteHandlerCommon;
|
||||
|
||||
private function handlePreInputUrlencoded(HandlerContext $context, RequestInterface $request): bool {
|
||||
$contentType = MediaType::parse($request->getHeaderLine('Content-Type'));
|
||||
if(!$contentType->equals('application/x-www-form-urlencoded'))
|
||||
return false;
|
||||
|
||||
$context->deps->register(UrlEncodedFormContent::parseStream($request->getBody()));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#[Preprocessor('input:urlencoded')]
|
||||
public function preInputUrlencoded(HandlerContext $context, RequestInterface $request, bool $required = true): void {
|
||||
if(!$this->handlePreInputUrlencoded($context, $request) && $required) {
|
||||
$context->response->statusCode = 400;
|
||||
$context->response->reasonPhrase = '';
|
||||
$context->halt();
|
||||
}
|
||||
}
|
||||
|
||||
private function handlePreInputMultipart(HandlerContext $context, RequestInterface $request): bool {
|
||||
$contentType = MediaType::parse($request->getHeaderLine('Content-Type'));
|
||||
if(!$contentType->equals('multipart/form-data'))
|
||||
return false;
|
||||
|
||||
$boundary = $contentType->boundary;
|
||||
if(empty($boundary))
|
||||
return false;
|
||||
|
||||
$context->deps->register(MultipartFormContent::parseStream($request->getBody(), $boundary));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#[Preprocessor('input:multipart')]
|
||||
public function preInputMultipart(HandlerContext $context, RequestInterface $request, bool $required = true): void {
|
||||
try {
|
||||
$result = $this->handlePreInputMultipart($context, $request);
|
||||
} catch(RuntimeException $ex) {
|
||||
$result = false;
|
||||
$required = true;
|
||||
}
|
||||
|
||||
if(!$result && $required) {
|
||||
$context->response->statusCode = 400;
|
||||
$context->response->reasonPhrase = '';
|
||||
$context->halt();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpFormContentTest.php
|
||||
// Created: 2025-03-12
|
||||
// Updated: 2025-03-14
|
||||
// Updated: 2025-03-15
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
@ -63,7 +63,7 @@ final class HttpFormContentTest extends TestCase {
|
|||
$this->assertFalse($form->hasParam('never_has_this'));
|
||||
$this->assertEquals(0, $form->getParamCount('never_has_this_either'));
|
||||
$this->assertNull($form->getParam('this_is_not_there_either'));
|
||||
$this->assertEquals(0401, $form->getParamAt('abusing_octal_notation_for_teto_reference', 3510, default: 0401));
|
||||
$this->assertEquals(0401, $form->getFilteredParamAt('abusing_octal_notation_for_teto_reference', 3510, default: 0401));
|
||||
|
||||
foreach($expected as $key => $values) {
|
||||
$this->assertTrue($form->hasParam($key));
|
||||
|
@ -90,7 +90,7 @@ final class HttpFormContentTest extends TestCase {
|
|||
$this->assertEquals(0, $form->getParamCount('never_has_this_either'));
|
||||
$this->assertNull($form->getParam('this_is_not_there_either'));
|
||||
$this->assertNull($form->getParamData('this_is_not_there_either'));
|
||||
$this->assertEquals(0401, $form->getParamAt('abusing_octal_notation_for_teto_reference', 3510, default: 0401));
|
||||
$this->assertEquals(0401, $form->getFilteredParamAt('abusing_octal_notation_for_teto_reference', 3510, default: 0401));
|
||||
$this->assertEquals(null, $form->getParamDataAt('abusing_octal_notation_for_teto_reference', 3510));
|
||||
|
||||
$this->assertTrue($form->hasParam('meow'));
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
<?php
|
||||
// RouterTest.php
|
||||
// Created: 2022-01-20
|
||||
// Updated: 2025-03-12
|
||||
// Updated: 2025-03-15
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Index\Http\{HttpHeaders,HttpResponseBuilder,HttpRequest,HttpUri};
|
||||
use Index\Http\Content\{FormContent,MultipartFormContent,UrlEncodedFormContent};
|
||||
use Index\Http\Routing\{HandlerContext,RouteHandler,RouteHandlerCommon,Router};
|
||||
use Index\Http\Routing\Filters\{FilterInfo,PrefixFilter};
|
||||
use Index\Http\Routing\Processors\{After,Before,Postprocessor,Preprocessor,ProcessorInfo};
|
||||
use Index\Http\Routing\Routes\{ExactRoute,PatternRoute,RouteInfo};
|
||||
use Index\Http\Streams\{NullStream,Stream};
|
||||
use Index\Http\Streams\Stream;
|
||||
|
||||
/**
|
||||
* This test isn't super representative of the current functionality
|
||||
|
@ -37,8 +38,8 @@ final class RouterTest extends TestCase {
|
|||
$router1->route(RouteInfo::exact('PUT', '/', fn() => 'put'));
|
||||
$router1->route(RouteInfo::exact('CUSTOM', '/', fn() => 'wacky'));
|
||||
|
||||
$this->assertEquals('get', (string)$router1->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/'), [], []))->getBody());
|
||||
$this->assertEquals('wacky', (string)$router1->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'CUSTOM', HttpUri::createUri('/'), [], []))->getBody());
|
||||
$this->assertEquals('get', (string)$router1->handle(HttpRequest::createRequestWithoutBody('GET', '/'))->getBody());
|
||||
$this->assertEquals('wacky', (string)$router1->handle(HttpRequest::createRequestWithoutBody('CUSTOM', '/'))->getBody());
|
||||
|
||||
$router1->filter(FilterInfo::prefix('/', function() { /* this one intentionally does nothing */ }));
|
||||
|
||||
|
@ -52,7 +53,7 @@ final class RouterTest extends TestCase {
|
|||
$router1->route(RouteInfo::pattern('GET', '#^/user/([A-Za-z0-9]+)$#uD', fn(string $user) => $user));
|
||||
$router1->route(RouteInfo::pattern('GET', '#^/user/([A-Za-z0-9]+)/below$#uD', fn(string $user) => 'below ' . $user));
|
||||
|
||||
$this->assertEquals('warioware below static', (string)$router1->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/user/static/below'), [], []))->getBody());
|
||||
$this->assertEquals('warioware below static', (string)$router1->handle(HttpRequest::createRequestWithoutBody('GET', '/user/static/below'))->getBody());
|
||||
|
||||
$router2 = new Router;
|
||||
$router2->filter(FilterInfo::prefix('/', fn() => 'meow'));
|
||||
|
@ -60,7 +61,7 @@ final class RouterTest extends TestCase {
|
|||
$router2->route(RouteInfo::exact('GET', '/contact', fn() => 'contact page'));
|
||||
$router2->route(RouteInfo::exact('GET', '/25252', fn() => 'numeric test'));
|
||||
|
||||
$this->assertEquals('meow', (string)$router2->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/rules'), [], []))->getBody());
|
||||
$this->assertEquals('meow', (string)$router2->handle(HttpRequest::createRequestWithoutBody('GET', '/rules'))->getBody());
|
||||
}
|
||||
|
||||
public function testAttribute(): void {
|
||||
|
@ -116,13 +117,13 @@ final class RouterTest extends TestCase {
|
|||
|
||||
$router->register($handler);
|
||||
|
||||
$this->assertEquals('index', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/'), [], []))->getBody());
|
||||
$this->assertEquals('avatar', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'POST', HttpUri::createUri('/avatar'), [], []))->getBody());
|
||||
$this->assertEquals('static', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'PUT', HttpUri::createUri('/static'), [], []))->getBody());
|
||||
$this->assertEquals('meow', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/meow'), [], []))->getBody());
|
||||
$this->assertEquals('meow', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'POST', HttpUri::createUri('/meow'), [], []))->getBody());
|
||||
$this->assertEquals('profile of Cool134', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/profile/Cool134'), [], []))->getBody());
|
||||
$this->assertEquals('still the profile of Cool134', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/profile-but-raw/Cool134'), [], []))->getBody());
|
||||
$this->assertEquals('index', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/'))->getBody());
|
||||
$this->assertEquals('avatar', (string)$router->handle(HttpRequest::createRequestWithoutBody('POST', '/avatar'))->getBody());
|
||||
$this->assertEquals('static', (string)$router->handle(HttpRequest::createRequestWithoutBody('PUT', '/static'))->getBody());
|
||||
$this->assertEquals('meow', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/meow'))->getBody());
|
||||
$this->assertEquals('meow', (string)$router->handle(HttpRequest::createRequestWithoutBody('POST', '/meow'))->getBody());
|
||||
$this->assertEquals('profile of Cool134', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/profile/Cool134'))->getBody());
|
||||
$this->assertEquals('still the profile of Cool134', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/profile-but-raw/Cool134'))->getBody());
|
||||
}
|
||||
|
||||
public function testEEPROMSituation(): void {
|
||||
|
@ -131,8 +132,8 @@ final class RouterTest extends TestCase {
|
|||
$router->route(RouteInfo::pattern('DELETE', '#^/uploads/([A-Za-z0-9\-_]+)$#uD', fn(string $id) => "Delete {$id}"));
|
||||
|
||||
// make sure both GET and DELETE are able to execute with a different pattern
|
||||
$this->assertEquals('Get AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/uploads/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'), [], []))->getBody());
|
||||
$this->assertEquals('Delete BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'DELETE', HttpUri::createUri('/uploads/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'), [], []))->getBody());
|
||||
$this->assertEquals('Get AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/uploads/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'))->getBody());
|
||||
$this->assertEquals('Delete BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', (string)$router->handle(HttpRequest::createRequestWithoutBody('DELETE', '/uploads/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'))->getBody());
|
||||
}
|
||||
|
||||
public function testFilterInterceptionOnRoot(): void {
|
||||
|
@ -141,9 +142,9 @@ final class RouterTest extends TestCase {
|
|||
$router->route(RouteInfo::exact('GET', '/', fn() => 'unexpected'));
|
||||
$router->route(RouteInfo::exact('GET', '/test', fn() => 'also unexpected'));
|
||||
|
||||
$this->assertEquals('expected', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/'), [], []))->getBody());
|
||||
$this->assertEquals('expected', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/test'), [], []))->getBody());
|
||||
$this->assertEquals('expected', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/error'), [], []))->getBody());
|
||||
$this->assertEquals('expected', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/'))->getBody());
|
||||
$this->assertEquals('expected', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/test'))->getBody());
|
||||
$this->assertEquals('expected', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/error'))->getBody());
|
||||
}
|
||||
|
||||
public function testDefaultOptionsImplementation(): void {
|
||||
|
@ -152,12 +153,124 @@ final class RouterTest extends TestCase {
|
|||
$router->route(RouteInfo::exact('POST', '/test', fn() => 'post'));
|
||||
$router->route(RouteInfo::exact('PATCH', '/test', fn() => 'patch'));
|
||||
|
||||
$response = $router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'OPTIONS', HttpUri::createUri('/test'), [], []));
|
||||
$response = $router->handle(HttpRequest::createRequestWithoutBody('OPTIONS', '/test'));
|
||||
$this->assertEquals(204, $response->getStatusCode());
|
||||
$this->assertEquals('No Content', $response->getReasonPhrase());
|
||||
$this->assertEquals('GET, HEAD, OPTIONS, PATCH, POST', $response->getHeaderLine('Allow'));
|
||||
}
|
||||
|
||||
public function testDefaultProcessorsNoRegister(): void {
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage(sprintf('preprocessor "%s" was not found', 'input:urlencoded:required'));
|
||||
|
||||
$router = new Router(registerDefaultProcessors: false);
|
||||
$router->route(RouteInfo::exact(
|
||||
'GET', '/soap',
|
||||
#[Before('input:urlencoded:required')]
|
||||
function() {
|
||||
return 'test';
|
||||
},
|
||||
));
|
||||
$router->handle(HttpRequest::createRequestWithoutBody('GET', '/soap'));
|
||||
}
|
||||
|
||||
public function testDefaultProcessors(): void {
|
||||
$router = new Router;
|
||||
$router->register(new class implements RouteHandler {
|
||||
use RouteHandlerCommon;
|
||||
|
||||
#[Before('input:urlencoded', required: false)]
|
||||
#[Before('input:multipart', required: false)]
|
||||
#[ExactRoute('POST', '/optional-form')]
|
||||
public function formOptional(?FormContent $content = null): string {
|
||||
if($content instanceof MultipartFormContent)
|
||||
return 'multipart:' . (string)$content->getParam('test');
|
||||
if($content instanceof UrlEncodedFormContent)
|
||||
return 'urlencoded:' . (string)$content->getParam('test');
|
||||
return 'none';
|
||||
}
|
||||
|
||||
#[Before('input:multipart')]
|
||||
#[ExactRoute('POST', '/required-multipart')]
|
||||
public function multipartRequired(MultipartFormContent $content): string {
|
||||
return (string)$content->getParam('test');
|
||||
}
|
||||
|
||||
#[Before('input:urlencoded')]
|
||||
#[ExactRoute('POST', '/required-urlencoded')]
|
||||
public function urlencodedRequired(UrlEncodedFormContent $content): string {
|
||||
return (string)$content->getParam('test');
|
||||
}
|
||||
});
|
||||
|
||||
$response = $router->handle(HttpRequest::createRequestWithoutBody('POST', '/optional-form'));
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals('none', (string)$response->getBody());
|
||||
|
||||
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/optional-form', [], Stream::createStream('test=mewow')));
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals('none', (string)$response->getBody());
|
||||
|
||||
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/optional-form', ['Content-Type' => ['application/x-www-form-urlencoded']], Stream::createStream('test=mewow')));
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals('urlencoded:mewow', (string)$response->getBody());
|
||||
|
||||
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/optional-form', ['Content-Type' => ['multipart/form-data; boundary="--soap12345"']], Stream::createStream(implode("\r\n", [
|
||||
'----soap12345',
|
||||
'Content-Disposition: form-data; name="test"',
|
||||
'',
|
||||
'wowof',
|
||||
'----soap12345--',
|
||||
'',
|
||||
]))));
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals('multipart:wowof', (string)$response->getBody());
|
||||
|
||||
$response = $router->handle(HttpRequest::createRequestWithoutBody('POST', '/required-urlencoded'));
|
||||
$this->assertEquals(400, $response->getStatusCode());
|
||||
|
||||
$response = $router->handle(HttpRequest::createRequestWithoutBody('POST', '/required-multipart'));
|
||||
$this->assertEquals(400, $response->getStatusCode());
|
||||
|
||||
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-urlencoded', ['Content-Type' => ['application/x-www-form-urlencoded']], Stream::createStream('test=meow')));
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals('meow', (string)$response->getBody());
|
||||
|
||||
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-multipart', ['Content-Type' => ['multipart/form-data; boundary="--soap56789"']], Stream::createStream(implode("\r\n", [
|
||||
'----soap56789',
|
||||
'Content-Disposition: form-data; name="test"',
|
||||
'',
|
||||
'woof',
|
||||
'----soap56789--',
|
||||
'',
|
||||
]))));
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals('woof', (string)$response->getBody());
|
||||
|
||||
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-urlencoded', ['Content-Type' => ['multipart/form-data; boundary="--soap56789"']], Stream::createStream('test=meow')));
|
||||
$this->assertEquals(400, $response->getStatusCode());
|
||||
|
||||
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-multipart', ['Content-Type' => ['application/x-www-form-urlencoded']], Stream::createStream(implode("\r\n", [
|
||||
'----soap56789',
|
||||
'Content-Disposition: form-data; name="test"',
|
||||
'',
|
||||
'woof',
|
||||
'----soap56789--',
|
||||
'',
|
||||
]))));
|
||||
$this->assertEquals(400, $response->getStatusCode());
|
||||
|
||||
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-multipart', ['Content-Type' => ['multipart/form-data']], Stream::createStream(implode("\r\n", [
|
||||
'----aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
'Content-Disposition: form-data; name="test"',
|
||||
'',
|
||||
'the',
|
||||
'----aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--',
|
||||
'',
|
||||
]))));
|
||||
$this->assertEquals(400, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testProcessors(): void {
|
||||
$router = new Router;
|
||||
|
||||
|
@ -216,15 +329,15 @@ final class RouterTest extends TestCase {
|
|||
}
|
||||
));
|
||||
|
||||
$response = $router->handle(new HttpRequest('1.1', [], Stream::createStream(base64_encode('mewow')), [], 'POST', HttpUri::createUri('/test'), [], []));
|
||||
$response = $router->handle(HttpRequest::createRequestWithBody('POST', '/test', [], Stream::createStream(base64_encode('mewow'))));
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals("{\n \"data\": \"mewow\"\n}", (string)$response->getBody());
|
||||
|
||||
$response = $router->handle(new HttpRequest('1.1', [], Stream::createStream(base64_encode('soap')), [], 'PUT', HttpUri::createUri('/alternate'), [], []));
|
||||
$response = $router->handle(HttpRequest::createRequestWithBody('PUT', '/alternate', [], Stream::createStream(base64_encode('soap'))));
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals("{\n \"path\": \"/alternate\",\n \"data\": \"soap\"\n}", (string)$response->getBody());
|
||||
|
||||
$response = $router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/filtered'), [], []));
|
||||
$response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/filtered'));
|
||||
$this->assertEquals(403, $response->getStatusCode());
|
||||
$this->assertEquals('it can work like a filter too', (string)$response->getBody());
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue