From 6ee05c7916b36bd619ef5650f2b95b7940a4d1b3 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Mon, 13 Nov 2023 23:11:39 +0100 Subject: [PATCH] add more validations, handle invalid json payloads --- src/Controller/Api/APITacticController.php | 4 +- src/Controller/Control.php | 8 +++- src/Controller/EditorController.php | 1 + src/Controller/SampleFormController.php | 8 ++-- src/Validation/FunctionValidator.php | 19 +++++++++ src/Validation/ValidationFail.php | 3 +- src/Validation/Validators.php | 45 ++++++++++++++++------ 7 files changed, 68 insertions(+), 20 deletions(-) create mode 100644 src/Validation/FunctionValidator.php diff --git a/src/Controller/Api/APITacticController.php b/src/Controller/Api/APITacticController.php index 031c88e..f775ecf 100644 --- a/src/Controller/Api/APITacticController.php +++ b/src/Controller/Api/APITacticController.php @@ -25,7 +25,7 @@ class APITacticController { public function updateName(int $tactic_id): HttpResponse { return Control::runChecked([ - "name" => [Validators::userString(32)] + "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()] ], function (HttpRequest $request) use ($tactic_id) { $this->model->updateName($tactic_id, $request["name"]); return HttpResponse::fromCode(HttpCodes::OK); @@ -34,7 +34,7 @@ class APITacticController { public function newTactic(): HttpResponse { return Control::runChecked([ - "name" => [Validators::userString(32)] + "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()] ], function (HttpRequest $request) { $tactic = $this->model->makeNew($request["name"]); $id = $tactic->getId(); diff --git a/src/Controller/Control.php b/src/Controller/Control.php index 327a475..2aacb19 100644 --- a/src/Controller/Control.php +++ b/src/Controller/Control.php @@ -6,6 +6,7 @@ use App\Http\HttpCodes; use App\Http\HttpRequest; use App\Http\HttpResponse; use App\Http\JsonHttpResponse; +use App\Validation\ValidationFail; class Control { @@ -18,8 +19,11 @@ class Control { */ public static function runChecked(array $schema, callable $run): HttpResponse { $request_body = file_get_contents('php://input'); - $payload = get_object_vars(json_decode($request_body)); - + $payload_obj = json_decode($request_body); + if (!$payload_obj instanceof \stdClass) { + return new JsonHttpResponse([new ValidationFail("bad-payload", "request body is not a valid json object"), HttpCodes::BAD_REQUEST]); + } + $payload = get_object_vars($payload_obj); return self::runCheckedFrom($payload, $schema, $run); } diff --git a/src/Controller/EditorController.php b/src/Controller/EditorController.php index 96b8125..bf5dccc 100644 --- a/src/Controller/EditorController.php +++ b/src/Controller/EditorController.php @@ -4,6 +4,7 @@ namespace App\Controller; use App\Data\TacticInfo; use App\Http\HttpCodes; +use App\Http\HttpRequest; use App\Http\HttpResponse; use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; diff --git a/src/Controller/SampleFormController.php b/src/Controller/SampleFormController.php index b65b265..bbf1f59 100644 --- a/src/Controller/SampleFormController.php +++ b/src/Controller/SampleFormController.php @@ -32,12 +32,12 @@ class SampleFormController { private function submitForm(array $form, callable $response): HttpResponse { return Control::runCheckedFrom($form, [ - "name" => [Validators::userString(32)], - "description" => [Validators::userString(512)] + "name" => [Validators::lenBetween(0, 32), Validators::name()], + "description" => [Validators::lenBetween(0, 512)] ], function (HttpRequest $req) use ($response) { - $this->gateway->insert($req["name"], $req["description"]); + $description = htmlspecialchars($req["description"]); + $this->gateway->insert($req["name"], $description); $results = ["results" => $this->gateway->listResults()]; - return call_user_func_array($response, [$results]); }); } diff --git a/src/Validation/FunctionValidator.php b/src/Validation/FunctionValidator.php new file mode 100644 index 0000000..6874d63 --- /dev/null +++ b/src/Validation/FunctionValidator.php @@ -0,0 +1,19 @@ +validate_fn = $validate_fn; + } + + public function validate(string $name, $val): array { + return call_user_func_array($this->validate_fn, [$name, $val]); + } +} \ No newline at end of file diff --git a/src/Validation/ValidationFail.php b/src/Validation/ValidationFail.php index 9daded8..fa5139c 100644 --- a/src/Validation/ValidationFail.php +++ b/src/Validation/ValidationFail.php @@ -11,7 +11,7 @@ class ValidationFail implements \JsonSerializable { * @param string $message * @param string $kind */ - protected function __construct(string $kind, string $message) { + public function __construct(string $kind, string $message) { $this->message = $message; $this->kind = $kind; } @@ -31,4 +31,5 @@ class ValidationFail implements \JsonSerializable { public static function notFound(string $message): ValidationFail { return new ValidationFail("not found", $message); } + } \ No newline at end of file diff --git a/src/Validation/Validators.php b/src/Validation/Validators.php index 11d8fbd..c9172b1 100644 --- a/src/Validation/Validators.php +++ b/src/Validation/Validators.php @@ -8,24 +8,47 @@ namespace App\Validation; class Validators { /** - * @return Validator a validator that validates non-empty strings + * @return Validator a validator that validates a given regex */ - public static function nonEmpty(): Validator { + public static function regex(string $regex): Validator { return new SimpleFunctionValidator( - fn($str) => !empty($str), - fn(string $name) => [FieldValidationFail::empty($name)] + fn(string $str) => preg_match($regex, $str), + fn(string $name) => [new FieldValidationFail($name, "field does not validates pattern $regex")] ); } - public static function shorterThan(int $limit): Validator { - return new SimpleFunctionValidator( - fn(string $str) => strlen($str) <= $limit, - fn(string $name) => [new FieldValidationFail($name, "field is longer than $limit chars.")] - ); + /** + * @return Validator a validator that validates strings that only contains numbers, letters, accents letters, `-` and `_`. + */ + public static function name(): Validator { + return self::regex("/^[0-9a-zA-Zà-üÀ-Ü_-]*$/"); } - public static function userString(int $maxLen): Validator { - return self::nonEmpty()->then(self::shorterThan($maxLen)); + /** + * @return Validator a validator that validates strings that only contains numbers, letters, accents letters, `-`, `_` and spaces. + */ + public static function nameWithSpaces(): Validator { + return self::regex("/^[0-9a-zA-Zà-üÀ-Ü _-]*$/"); } + /** + * Validate string if its length is between given range + * @param int $min minimum accepted length, inclusive + * @param int $max maximum accepted length, exclusive + * @return Validator + */ + public static function lenBetween(int $min, int $max): Validator { + return new FunctionValidator( + function (string $fieldName, string $str) use ($min, $max) { + $len = strlen($str); + if ($len >= $max) { + return [new FieldValidationFail($fieldName, "field is longer than $max chars.")]; + } + if ($len < $min) { + return [new FieldValidationFail($fieldName, "field is shorted than $min chars.")]; + } + return []; + } + ); + } } \ No newline at end of file