Compare commits

..

No commits in common. "master" and "1.3.0" have entirely different histories.

3 changed files with 24 additions and 163 deletions

View file

@ -1,14 +1,11 @@
> [!IMPORTANT] # Request validation plugin for the [Reflect API Framework](https://github.com/victorwesterlund/reflect)
> This plugin has since [Reflect version 3.8.5](https://codeberg.org/reflect/reflect/releases/tag/2.8.5) been deprecated. Reflect now has built-in request validation which is enabled by default. The built-in validator is based on this plugin.
# Request validation plugin for the [Reflect API Framework](https://codeberg.org/reflect/reflect)
This request pre-processor adds request validation for an API written in the Reflect API Framework. This request pre-processor adds request validation for an API written in the Reflect API Framework.
Write safer Reflect endpoints by enforcing request data structure validation before the request reaches your endpoint's logic. This plugin validates GET and POST data (even JSON) and returns an array with scoped `Error`s that can be further acted upon if desired. Write safer Reflect endpoints by enforcing request data structure validation before the request reaches your endpoint's logic. This plugin validates GET and POST data (even JSON) and returns an array with scoped `Error`s that can be further acted upon if desired.
## Example *Example:*
``` ```
POST Request: /my-endpoint?key1=lorem-ipsum&key2=dolor GET Request: /my-endpoint?key1=lorem-ipsum&key2=dolor
POST Body: {"key3":15, "key4":["hello", "world"]} POST Body: {"key3":15, "key4":["hello", "world"]}
``` ```
```php ```php
@ -19,7 +16,7 @@ use \ReflectRules\Type;
use \ReflectRules\Rules; use \ReflectRules\Rules;
use \ReflectRules\Ruleset; use \ReflectRules\Ruleset;
class POST_MyEndpoint implements Endpoint { class GET_MyEndpoint implements Endpoint {
private Ruleset $rules; private Ruleset $rules;
public function __construct() { public function __construct() {
@ -74,8 +71,6 @@ Ruleset->get_errors();
Use `Ruleset->is_valid(): bool` to quickly check if any errors are set. Use `Ruleset->is_valid(): bool` to quickly check if any errors are set.
You can also use `Ruleset->validate_or_exit(): true|Response` to automatically return a `Reflect\Response` with all errors to current STDOUT if validation fails.
# Installation # Installation
Install with composer Install with composer
@ -111,20 +106,10 @@ Error
--| --|
[`Error::VALUE_MIN_ERROR`](#min) [`Error::VALUE_MIN_ERROR`](#min)
[`Error::VALUE_MAX_ERROR`](#max) [`Error::VALUE_MAX_ERROR`](#max)
[`Error::UNKNOWN_PROPERTY_NAME`](#strict-mode)
[`Error::INVALID_PROPERTY_TYPE`](#type) [`Error::INVALID_PROPERTY_TYPE`](#type)
[`Error::INVALID_PROPERTY_VALUE`](#typeenum) [`Error::INVALID_PROPERTY_VALUE`](#typeenum)
[`Error::MISSING_REQUIRED_PROPERTY`](#required) [`Error::MISSING_REQUIRED_PROPERTY`](#required)
# Strict mode
Enable strict mode by initializing a Ruleset with the "strict" argument set to `true`.
```php
new Ruleset(strict: true);
```
Strict mode will not allow undefined properties to be set in all configured scopes. If a property exists in `Scope` that hasn't been defined with a `Rules()` instance, a `Errors::UNKNOWN_PROPERTY_NAME` error will be set.
# Available rules # Available rules
The following methods can be chained onto a `Rules` instance to enforce certain constraints on a particular property The following methods can be chained onto a `Rules` instance to enforce certain constraints on a particular property
@ -156,11 +141,11 @@ Type|Description
--|-- --|--
`Type::NUMERIC`|Value must be a number or a numeric string `Type::NUMERIC`|Value must be a number or a numeric string
`Type::STRING`|Value must be a string `Type::STRING`|Value must be a string
`Type::BOOLEAN`|Value must be a boolean ([**considered bool for GET rules**](#boolean-coercion-from-string-for-search-parameters)) `Type::BOOLEAN`|Value must be a boolean or ([**considered bool for GET rules**](#boolean-coercion-from-string-for-search-parameters))
`Type::ARRAY`|Value must be a JSON array or ([**CSV for GET rules**](#csv-for-search-parameters)) `Type::ARRAY`|Value must be a JSON array
`Type::OBJECT`|Value must be a JSON object `Type::OBJECT`|Value must be a JSON object
`Type::ENUM`|Value must be exactly one of pre-defined values ([**more information**](#typeenum)) `Type::ENUM`|Value must be exactly one of pre-defined values ([**more information**](#typeenum))
`Type::NULL`|Value must be null ([**considered null for GET rules**](#null-coercion-from-string-for-search-parameters)) `Type::NULL`|Value must be null or ([**considered null for GET rules**](#null-coercion-from-string-for-search-parameters))
Will set a `Error::INVALID_PROPERTY_TYPE` error on the current scope and property if failed, except Type::ENUM that will set a `Error::INVALID_PROPERTY_VALUE` with an array of the valid vaules. Will set a `Error::INVALID_PROPERTY_TYPE` error on the current scope and property if failed, except Type::ENUM that will set a `Error::INVALID_PROPERTY_VALUE` with an array of the valid vaules.
@ -197,19 +182,6 @@ Any other value will cause the `type()` rule to fail.
> [!IMPORTANT] > [!IMPORTANT]
> This coercion is only applies for `Ruleset->GET()`. `Ruleset->POST()` will enforce real `true` and `type` values since it's JSON > This coercion is only applies for `Ruleset->GET()`. `Ruleset->POST()` will enforce real `true` and `type` values since it's JSON
#### CSV array for search parameters
A CSV string is expected when `Type::ARRAY` is set for a GET rule.
*Example:*
```
https://example.com?typeArray=key1,key2,key3
```
Any other value will cause the `type()` rule to fail.
> [!IMPORTANT]
> This coercion is only applies for `Ruleset->GET()`. `Ruleset->POST()` will enforce a JSON array
#### Null coercion from string for search parameters #### Null coercion from string for search parameters
Search parameters are read as strings, a null value is therefor coerced from an empty string `""`. Search parameters are read as strings, a null value is therefor coerced from an empty string `""`.

View file

@ -16,15 +16,8 @@
} }
class Rules { class Rules {
private const CSV_DELIMITER = ",";
private string $property; private string $property;
/*
# Rule properties
These properties store rules for an instance of a property
*/
public bool $required = false; public bool $required = false;
// Matched Type against $types array // Matched Type against $types array
@ -140,40 +133,6 @@
return !is_bool($value) && in_array($value, $this->enum); return !is_bool($value) && in_array($value, $this->enum);
} }
private function eval_object(mixed $value, Scope $scope): bool {
// Arrays in POST parameters should already be decoded
if ($scope === Scope::POST) {
return is_array($value);
}
// Decode stringified JSON
$json = json_decode($value);
// Failed to decode JSON
if ($json === null) {
return false;
}
// Mutate property on superglobal with decoded JSON
$GLOBALS[Scope::GET->value][$this->property] = $json;
return true;
}
private function eval_array(string|array $value, Scope $scope): bool {
// Arrays in POST parameters should already be decoded
if ($scope === Scope::POST) {
return is_array($value);
}
// Mutate property on superglobal with decoded CSV if not already an array
if (!is_array($_GET[$this->property])) {
$GLOBALS[Scope::GET->value][$this->property] = explode(self::CSV_DELIMITER, $_GET[$this->property]);
}
return true;
}
/* /*
## Public eval methods ## Public eval methods
These are the entry-point eval methods that in turn can call other These are the entry-point eval methods that in turn can call other
@ -203,8 +162,8 @@
Type::NUMBER => $match = is_numeric($value), Type::NUMBER => $match = is_numeric($value),
Type::STRING => $match = is_string($value), Type::STRING => $match = is_string($value),
Type::BOOLEAN => $match = $this->eval_type_boolean($value, $scope), Type::BOOLEAN => $match = $this->eval_type_boolean($value, $scope),
Type::ARRAY => $match = $this->eval_array($value, $scope), Type::ARRAY,
Type::OBJECT => $match = $this->eval_object($value, $scope), Type::OBJECT => $match = is_array($value),
Type::ENUM => $match = $this->eval_type_enum($value), Type::ENUM => $match = $this->eval_type_enum($value),
Type::NULL => $match = is_null($value) Type::NULL => $match = is_null($value)
}; };
@ -226,7 +185,7 @@
Type::NUMBER => $this->eval_type($value, $scope) && $value >= $this->min, Type::NUMBER => $this->eval_type($value, $scope) && $value >= $this->min,
Type::STRING => $this->eval_type($value, $scope) && strlen($value) >= $this->min, Type::STRING => $this->eval_type($value, $scope) && strlen($value) >= $this->min,
Type::ARRAY, Type::ARRAY,
Type::OBJECT => $this->eval_type($value, $scope) && count($GLOBALS[$scope->value][$this->property]) >= $this->min, Type::OBJECT => $this->eval_type($value, $scope) && count($value) >= $this->min,
default => true default => true
}; };
} }
@ -236,7 +195,7 @@
Type::NUMBER => $this->eval_type($value, $scope) && $value <= $this->max, Type::NUMBER => $this->eval_type($value, $scope) && $value <= $this->max,
Type::STRING => $this->eval_type($value, $scope) && strlen($value) <= $this->max, Type::STRING => $this->eval_type($value, $scope) && strlen($value) <= $this->max,
Type::ARRAY, Type::ARRAY,
Type::OBJECT => $this->eval_type($value, $scope) && count($GLOBALS[$scope->value][$this->property]) <= $this->max, Type::OBJECT => $this->eval_type($value, $scope) && count($value) <= $this->max,
default => true default => true
}; };
} }

View file

@ -4,52 +4,35 @@
use \ReflectRules\Rules; use \ReflectRules\Rules;
use \Reflect\Response;
require_once "Rules.php"; require_once "Rules.php";
// Available superglobal scopes // Available superglobal scopes
enum Scope: string { enum Scope: string {
case GET = "_GET"; case GET = "_GET";
case POST = "_POST"; case POST = "_POST";
static function get_array(): array {
return [
Scope::GET->name => [],
Scope::POST->name => []
];
}
} }
enum Error { enum Error {
case VALUE_MIN_ERROR; case VALUE_MIN_ERROR;
case VALUE_MAX_ERROR; case VALUE_MAX_ERROR;
case UNKNOWN_PROPERTY_NAME;
case INVALID_PROPERTY_TYPE; case INVALID_PROPERTY_TYPE;
case INVALID_PROPERTY_VALUE; case INVALID_PROPERTY_VALUE;
case MISSING_REQUIRED_PROPERTY; case MISSING_REQUIRED_PROPERTY;
} }
class Ruleset { class Ruleset {
private bool $is_valid = true; // Array of arrays with failed constraints
private ?bool $strict; private array $errors = [];
private array $errors;
private array $rules_names; public function __construct() {}
public function __construct(bool $strict = false) {
/*
Strict mode can only be enabled or disabled as a bool argument.
'null' is used internally on this property as a re-evaluate flag.
*/
$this->strict = $strict;
$this->errors = Scope::get_array();
$this->rules_names = Scope::get_array();
}
// Append an error to the array of errors // Append an error to the array of errors
private function add_error(Error $error, Scope $scope, string $property, mixed $expected): void { private function add_error(Error $error, Scope $scope, string $property, mixed $expected): void {
// Create sub array if this is the first error in this scope
if (!array_key_exists($scope->name, $this->errors)) {
$this->errors[$scope->name] = [];
}
// Create sub array if this is the first error for this property // Create sub array if this is the first error for this property
if (!array_key_exists($property, $this->errors[$scope->name])) { if (!array_key_exists($property, $this->errors[$scope->name])) {
$this->errors[$scope->name][$property] = []; $this->errors[$scope->name][$property] = [];
@ -57,21 +40,6 @@
// Set expected value value for property in scope // Set expected value value for property in scope
$this->errors[$scope->name][$property][$error->name] = $expected; $this->errors[$scope->name][$property][$error->name] = $expected;
// Unset valid flag
$this->is_valid = false;
}
// Evaluate an array of Rules property names against scope keys
private function eval_strict(Scope $scope): void {
$name_diffs = array_diff(array_keys($GLOBALS[$scope->value]), $this->rules_names[$scope->name]);
// Set errors for each undefined property
foreach ($name_diffs as $name_diff) {
$this->add_error(Error::UNKNOWN_PROPERTY_NAME, $scope, $name_diff, null);
}
// Unset strict mode property now that we have evaled it up to this point
$this->strict = null;
} }
// Evaluate Rules against a given value // Evaluate Rules against a given value
@ -122,62 +90,24 @@
// ---- // ----
// Perform request processing on GET properties (search parameters) // Perform request processing on GET properties (search parameters)
public function GET(array $rules): self { public function GET(array $rules): void {
// (Re)enable strict mode if property is null
if ($this->strict === null) {
$this->strict = true;
}
foreach ($rules as $rule) { foreach ($rules as $rule) {
$this->rules_names[Scope::GET->name][] = $rule->get_property_name();
$this->eval_rules($rule, Scope::GET); $this->eval_rules($rule, Scope::GET);
} }
return $this;
} }
// Perform request processing on POST properties (request body) // Perform request processing on POST properties (request body)
public function POST(array $rules): self { public function POST(array $rules): void {
// (Re)enable strict mode if property is null
if ($this->strict === null) {
$this->strict = true;
}
foreach ($rules as $rule) { foreach ($rules as $rule) {
$this->rules_names[Scope::POST->name][] = $rule->get_property_name();
$this->eval_rules($rule, Scope::POST); $this->eval_rules($rule, Scope::POST);
} }
return $this;
}
// ----
// Return array of all set Errors
public function get_errors(): array {
// Strict mode is enabled
if ($this->strict === true) {
$this->eval_strict(Scope::GET);
$this->eval_strict(Scope::POST);
}
return $this->errors;
} }
public function is_valid(): bool { public function is_valid(): bool {
// Strict mode is enabled return empty($this->errors);
if ($this->strict === true) {
$this->eval_strict(Scope::GET);
$this->eval_strict(Scope::POST);
}
return $this->is_valid;
} }
// Return Reflect\Response with errors and code 422 Unprocessable Content if validation failed public function get_errors(): array {
public function validate_or_exit(): true|Response { return $this->errors;
return $this->is_valid() ? true : new Response($this->errors, 422);
} }
} }