From f8e8e642d3726dd3312e799c523c8083e0dbbc35 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Sat, 11 Nov 2023 03:46:27 +0100 Subject: [PATCH] validate user inputs --- front/components/TitleInput.tsx | 12 +++-- front/views/Editor.tsx | 40 +++++++++------ public/api/index.php | 5 +- src/Api/TacticEndpoint.php | 58 +++++++++++++++++----- src/Controller/EditorController.php | 5 +- src/Gateway/TacticInfoGateway.php | 2 +- src/HttpCodes.php | 14 ++++++ src/Model/TacticModel.php | 37 +++++++++++++- src/Validation/FieldValidationFail.php | 38 ++++++++++++++ src/Validation/SimpleFunctionValidator.php | 28 +++++++++++ src/Validation/Validation.php | 30 +++++++++++ src/Validation/ValidationFail.php | 34 +++++++++++++ src/Validation/Validator.php | 15 ++++++ src/Validation/Validators.php | 34 +++++++++++++ 14 files changed, 315 insertions(+), 37 deletions(-) create mode 100644 src/HttpCodes.php create mode 100644 src/Validation/FieldValidationFail.php create mode 100644 src/Validation/SimpleFunctionValidator.php create mode 100644 src/Validation/Validation.php create mode 100644 src/Validation/ValidationFail.php create mode 100644 src/Validation/Validator.php create mode 100644 src/Validation/Validators.php diff --git a/front/components/TitleInput.tsx b/front/components/TitleInput.tsx index 655071d..eb162d1 100644 --- a/front/components/TitleInput.tsx +++ b/front/components/TitleInput.tsx @@ -1,16 +1,20 @@ -import React, {useRef, useState} from "react"; +import React, {CSSProperties, useRef, useState} from "react"; import "../style/title_input.css"; -export default function TitleInput({default_value, on_validated}: { +export interface TitleInputOptions { + style: CSSProperties, default_value: string, on_validated: (a: string) => void -}) { +} + +export default function TitleInput({style, default_value, on_validated}: TitleInputOptions) { const [value, setValue] = useState(default_value); const ref = useRef(null); - + return ( setValue(event.target.value)} diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 3cbbf9b..84d24e6 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -1,29 +1,41 @@ -import React from "react"; +import React, {CSSProperties, useState} from "react"; import "../style/editor.css"; import TitleInput from "../components/TitleInput"; import {API} from "../Constants"; +const ERROR_STYLE: CSSProperties = { + borderColor: "red" +} + export default function Editor({id, name}: { id: number, name: string }) { + + const [style, setStyle] = useState({}); + return (
LEFT
- update_tactic_name(id, name)}/> + { + fetch(`${API}/tactic/${id}/edit/name`, { + method: "POST", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: new_name, + }) + }).then(response => { + if (response.ok) { + setStyle({}) + } else { + setStyle(ERROR_STYLE) + } + }) + }}/>
RIGHT
) } -function update_tactic_name(id: number, new_name: string) { - //FIXME avoid absolute path as they would not work on staging server - fetch(`${API}/tactic/${id}/edit/name`, { - method: "POST", - body: JSON.stringify({ - name: new_name - }) - }).then(response => { - if (!response.ok) - alert("could not update tactic name!") - }) -} \ No newline at end of file diff --git a/public/api/index.php b/public/api/index.php index 52ecf2f..2032833 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -5,16 +5,17 @@ require "../../vendor/autoload.php"; require "../../sql/database.php"; require "../utils.php"; -use App\Api\TacticEndpoint; use App\Connexion; +use App\Api\TacticEndpoint; use App\Gateway\TacticInfoGateway; +use App\Model\TacticModel; $con = new Connexion(get_database()); $router = new AltoRouter(); $router->setBasePath(get_public_path() . "/api"); -$tacticEndpoint = new TacticEndpoint(new TacticInfoGateway($con)); +$tacticEndpoint = new TacticEndpoint(new TacticModel(new TacticInfoGateway($con))); $router->map("POST", "/tactic/[i:id]/edit/name", fn(int $id) => $tacticEndpoint->updateName($id)); $router->map("GET", "/tactic/[i:id]", fn(int $id) => $tacticEndpoint->getTacticInfo($id)); $router->map("POST", "/tactic/new", fn() => $tacticEndpoint->newTactic()); diff --git a/src/Api/TacticEndpoint.php b/src/Api/TacticEndpoint.php index 1c35072..9c1ee8c 100644 --- a/src/Api/TacticEndpoint.php +++ b/src/Api/TacticEndpoint.php @@ -1,43 +1,77 @@ tactics = $tactics; + public function __construct(TacticModel $model) { + $this->model = $model; } - public function updateName(int $tactic_id) { + public function updateName(int $tactic_id): void { $request_body = file_get_contents('php://input'); $data = json_decode($request_body); - $new_name = $data->name; + if (!isset($data->name)) { + http_response_code(HttpCodes::BAD_REQUEST); + echo "missing 'name'"; + return; + } - $this->tactics->update($tactic_id, $new_name); + $fails = []; + + $this->model->updateName($fails, $tactic_id, $data->name); + if (!empty($fails)) { + http_response_code(HttpCodes::PRECONDITION_FAILED); + echo json_encode($fails); + } } - public function newTactic() { + public function newTactic(): void { $request_body = file_get_contents('php://input'); $data = json_decode($request_body); $initial_name = $data->name; - $id = $this->tactics->insert($initial_name)->getId(); + if (!isset($data->name)) { + http_response_code(HttpCodes::BAD_REQUEST); + echo "missing 'name'"; + return; + } + + $fails = []; + $tactic = $this->model->makeNew($fails, $initial_name); + + if (!empty($fails)) { + http_response_code(HttpCodes::PRECONDITION_FAILED); + echo json_encode($fails); + return; + } + + $id = $tactic->getId(); echo "{id: $id}"; } - public function getTacticInfo(int $id) { - $tactic_info = $this->tactics->get($id); + public function getTacticInfo(int $id): void { + $tactic_info = $this->model->get($id); + + if ($tactic_info == null) { + http_response_code(HttpCodes::NOT_FOUND); + return; + } + echo json_encode($tactic_info); } diff --git a/src/Controller/EditorController.php b/src/Controller/EditorController.php index e815ed1..de3056a 100644 --- a/src/Controller/EditorController.php +++ b/src/Controller/EditorController.php @@ -4,6 +4,7 @@ namespace App\Controller; use App\Data\TacticInfo; use App\Gateway\TacticInfoGateway; +use App\HttpCodes; use App\Model\TacticModel; class EditorController { @@ -23,7 +24,7 @@ class EditorController { } public function makeNew() { - $tactic = $this->model->makeNew(); + $tactic = $this->model->makeNewDefault(); $this->openEditor($tactic); } @@ -31,8 +32,8 @@ class EditorController { $tactic = $this->model->get($id); if ($tactic == null) { - echo "la tactique " . $id . " n'existe pas"; http_response_code(404); + echo "la tactique " . $id . " n'existe pas"; return; } diff --git a/src/Gateway/TacticInfoGateway.php b/src/Gateway/TacticInfoGateway.php index 690d5bd..1f95d55 100644 --- a/src/Gateway/TacticInfoGateway.php +++ b/src/Gateway/TacticInfoGateway.php @@ -40,7 +40,7 @@ class TacticInfoGateway { return new TacticInfo(intval($row["id"]), $name, strtotime($row["creation_date"])); } - public function update(int $id, string $name) { + public function updateName(int $id, string $name) { $this->con->exec( "UPDATE TacticInfo SET name = :name WHERE id = :id", [ diff --git a/src/HttpCodes.php b/src/HttpCodes.php new file mode 100644 index 0000000..6bcfa5f --- /dev/null +++ b/src/HttpCodes.php @@ -0,0 +1,14 @@ +tactics = $tactics; } - public function makeNew(): TacticInfo { + public function makeNew(array &$fails, string $name): ?TacticInfo { + $failure = Validation::validate($name, "name", $fails, Validators::nonEmpty(), Validators::noInvalidChars()); + if ($failure) { + return null; + } + return $this->tactics->insert($name); + } + + public function makeNewDefault(): ?TacticInfo { return $this->tactics->insert(self::TACTIC_DEFAULT_NAME); } + /** + * Tries to retrieve information about a tactic + * @param int $id tactic identifier + * @return TacticInfo|null or null if the identifier did not match a tactic + */ public function get(int $id): ?TacticInfo { return $this->tactics->get($id); } + /** + * Update the name of a tactic + * @param int $id the tactic identifier + * @param string $name the new name to set + */ + public function updateName(array &$fails, int $id, string $name): void { + $failure = Validation::validate($name, "name", $fails, Validators::nonEmpty(), Validators::noInvalidChars()); + + if ($this->tactics->get($id) == null) { + $fails[] = ValidationFail::notFound("$id is an unknown tactic identifier"); + } else if (!$failure) { + $this->tactics->updateName($id, $name); + } + } + } \ No newline at end of file diff --git a/src/Validation/FieldValidationFail.php b/src/Validation/FieldValidationFail.php new file mode 100644 index 0000000..cae1496 --- /dev/null +++ b/src/Validation/FieldValidationFail.php @@ -0,0 +1,38 @@ +fieldName = $fieldName; + } + + + public function getFieldName(): string { + return $this->fieldName; + } + + + public static function invalidChars(string $fieldName): FieldValidationFail { + return new FieldValidationFail($fieldName, "field contains illegal chars"); + } + + public static function empty(string $fieldName): FieldValidationFail { + return new FieldValidationFail($fieldName, "field is empty"); + } + + public function jsonSerialize() { + return ["field" => $this->fieldName, "message" => $this->getMessage()]; + } +} \ No newline at end of file diff --git a/src/Validation/SimpleFunctionValidator.php b/src/Validation/SimpleFunctionValidator.php new file mode 100644 index 0000000..4282916 --- /dev/null +++ b/src/Validation/SimpleFunctionValidator.php @@ -0,0 +1,28 @@ + bool`, to validate the given string + * @param callable $error_factory a factory function with signature `(string) => Error)` to emit error when the predicate fails + */ + public function __construct(callable $predicate, callable $error_factory) { + $this->predicate = $predicate; + $this->error_factory = $error_factory; + } + + public function validate(string $name, $val): ?ValidationFail { + if (!call_user_func_array($this->predicate, [$val])) { + return call_user_func_array($this->error_factory, [$name]); + } + return null; + } +} \ No newline at end of file diff --git a/src/Validation/Validation.php b/src/Validation/Validation.php new file mode 100644 index 0000000..da5dadd --- /dev/null +++ b/src/Validation/Validation.php @@ -0,0 +1,30 @@ +validate($val_name, $val); + if ($error != null) { + $errors[] = $error; + $had_errors = true; + } + } + return $had_errors; + } + +} \ No newline at end of file diff --git a/src/Validation/ValidationFail.php b/src/Validation/ValidationFail.php new file mode 100644 index 0000000..9daded8 --- /dev/null +++ b/src/Validation/ValidationFail.php @@ -0,0 +1,34 @@ +message = $message; + $this->kind = $kind; + } + + public function getMessage(): string { + return $this->message; + } + + public function getKind(): string { + return $this->kind; + } + + public function jsonSerialize() { + return ["error" => $this->kind, "message" => $this->message]; + } + + public static function notFound(string $message): ValidationFail { + return new ValidationFail("not found", $message); + } +} \ No newline at end of file diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php new file mode 100644 index 0000000..b7c77ce --- /dev/null +++ b/src/Validation/Validator.php @@ -0,0 +1,15 @@ +` + */ + public static function noInvalidChars(): Validator { + return new SimpleFunctionValidator( + fn($str) => !filter_var($str, FILTER_VALIDATE_REGEXP, ['options' => ["regexp" => "/[<>]/"]]), + fn(string $name) => FieldValidationFail::invalidChars($name) + ); + } + + /** + * @return Validator a validator that validates non-empty strings + */ + public static function nonEmpty(): Validator { + return new SimpleFunctionValidator( + fn($str) => !empty($str), + fn(string $name) => FieldValidationFail::empty($name) + ); + } + + +} + +