Compare commits

...

7 commits

Author SHA1 Message Date
vlw
01cc1eea02 doc: add deprecation note to README (#19)
This plugin has been deprecated as of Reflect version 3.8.5

Reviewed-on: https://codeberg.org/reflect/rules-plugin/pulls/19
Co-authored-by: vlw <victor@vlw.se>
Co-committed-by: vlw <victor@vlw.se>
2025-04-04 06:00:01 +00:00
aa7d969350 feat: add return Ruleset instance to Ruleset->GET() and Ruleset->POST() (#17)
Quick PR that adds return values to `Ruleset->GET()` and `Ruleset->POST()` to allow for method chaining with for example `Ruleset->validate_or_exit()`. This can come handy for streamlining simple rulesets.

# Example
```php
$ruleset->GET(new Rules("something")->type(Type::STRING))->validate_or_exit();
```

Reviewed-on: https://codeberg.org/reflect/reflect-rules-plugin/pulls/17
2024-11-28 17:05:16 +00:00
vlw
df150f0d86 feat: add method to validate and return Reflect\Response if Ruleset is invalid to the Ruleset class (#16)
Closes #13

Reviewed-on: https://codeberg.org/reflect/reflect-rules-plugin/pulls/16
Co-authored-by: vlw <victor@vlw.se>
Co-committed-by: vlw <victor@vlw.se>
2024-11-20 10:39:33 +00:00
vlw
4133b25e93 docs(chore): change link to Reflect in README from GitHub to Codeberg (#15)
Reviewed-on: https://codeberg.org/reflect/reflect-rules-plugin/pulls/15
Co-authored-by: vlw <victor@vlw.se>
Co-committed-by: vlw <victor@vlw.se>
2024-11-20 10:39:03 +00:00
vlw
24352ae45b doc(fix): use a POST request as the example in the README instread of GET (#14)
The example sends a request body to a GET request, and [GET requests can not have a request body](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET). So this PR changes the example to a POST request instead.

Reviewed-on: https://codeberg.org/reflect/reflect-rules-plugin/pulls/14
Co-authored-by: vlw <victor@vlw.se>
Co-committed-by: vlw <victor@vlw.se>
2024-11-20 10:38:06 +00:00
9c837fd194
feat: add strict mode (#11)
* feat: add strict mode

* feat(doc): add strict mode to README
2024-01-17 12:07:44 +01:00
81a2b2129c
fix: typehint for eval_array with POST data (#12) 2024-01-17 12:03:11 +01:00
3 changed files with 103 additions and 18 deletions

View file

@ -1,11 +1,14 @@
# Request validation plugin for the [Reflect API Framework](https://github.com/victorwesterlund/reflect)
> [!IMPORTANT]
> 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.
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
```
GET Request: /my-endpoint?key1=lorem-ipsum&key2=dolor
POST Request: /my-endpoint?key1=lorem-ipsum&key2=dolor
POST Body: {"key3":15, "key4":["hello", "world"]}
```
```php
@ -16,7 +19,7 @@ use \ReflectRules\Type;
use \ReflectRules\Rules;
use \ReflectRules\Ruleset;
class GET_MyEndpoint implements Endpoint {
class POST_MyEndpoint implements Endpoint {
private Ruleset $rules;
public function __construct() {
@ -71,6 +74,8 @@ Ruleset->get_errors();
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
Install with composer
@ -106,10 +111,20 @@ Error
--|
[`Error::VALUE_MIN_ERROR`](#min)
[`Error::VALUE_MAX_ERROR`](#max)
[`Error::UNKNOWN_PROPERTY_NAME`](#strict-mode)
[`Error::INVALID_PROPERTY_TYPE`](#type)
[`Error::INVALID_PROPERTY_VALUE`](#typeenum)
[`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
The following methods can be chained onto a `Rules` instance to enforce certain constraints on a particular property

View file

@ -160,7 +160,7 @@
return true;
}
private function eval_array(string $value, Scope $scope): bool {
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);

View file

@ -4,35 +4,52 @@
use \ReflectRules\Rules;
use \Reflect\Response;
require_once "Rules.php";
// Available superglobal scopes
enum Scope: string {
case GET = "_GET";
case POST = "_POST";
static function get_array(): array {
return [
Scope::GET->name => [],
Scope::POST->name => []
];
}
}
enum Error {
case VALUE_MIN_ERROR;
case VALUE_MAX_ERROR;
case UNKNOWN_PROPERTY_NAME;
case INVALID_PROPERTY_TYPE;
case INVALID_PROPERTY_VALUE;
case MISSING_REQUIRED_PROPERTY;
}
class Ruleset {
// Array of arrays with failed constraints
private array $errors = [];
private bool $is_valid = true;
private ?bool $strict;
private array $errors;
public function __construct() {}
private array $rules_names;
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
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
if (!array_key_exists($property, $this->errors[$scope->name])) {
$this->errors[$scope->name][$property] = [];
@ -40,6 +57,21 @@
// Set expected value value for property in scope
$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
@ -90,24 +122,62 @@
// ----
// Perform request processing on GET properties (search parameters)
public function GET(array $rules): void {
public function GET(array $rules): self {
// (Re)enable strict mode if property is null
if ($this->strict === null) {
$this->strict = true;
}
foreach ($rules as $rule) {
$this->rules_names[Scope::GET->name][] = $rule->get_property_name();
$this->eval_rules($rule, Scope::GET);
}
return $this;
}
// Perform request processing on POST properties (request body)
public function POST(array $rules): void {
public function POST(array $rules): self {
// (Re)enable strict mode if property is null
if ($this->strict === null) {
$this->strict = true;
}
foreach ($rules as $rule) {
$this->rules_names[Scope::POST->name][] = $rule->get_property_name();
$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 {
return empty($this->errors);
// Strict mode is enabled
if ($this->strict === true) {
$this->eval_strict(Scope::GET);
$this->eval_strict(Scope::POST);
}
return $this->is_valid;
}
public function get_errors(): array {
return $this->errors;
// Return Reflect\Response with errors and code 422 Unprocessable Content if validation failed
public function validate_or_exit(): true|Response {
return $this->is_valid() ? true : new Response($this->errors, 422);
}
}