validate user inputs
continuous-integration/drone/push Build is passing Details

pull/8/head
Override-6 1 year ago
parent 29685562bb
commit f8e8e642d3
Signed by untrusted user who does not match committer: maxime.batista
GPG Key ID: 8002CC4B4DD9ECA5

@ -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<HTMLInputElement>(null);
return (
<input className="title_input"
ref={ref}
style={style}
type="text"
value={value}
onChange={event => setValue(event.target.value)}

@ -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<CSSProperties>({});
return (
<div id="main">
<div id="topbar">
<div>LEFT</div>
<TitleInput default_value={name} on_validated={name => update_tactic_name(id, name)}/>
<TitleInput style={style} default_value={name} on_validated={new_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)
}
})
}}/>
<div>RIGHT</div>
</div>
</div>
)
}
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!")
})
}

@ -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());

@ -1,43 +1,77 @@
<?php
namespace App\Api;
use App\Gateway\TacticInfoGateway;
use App\Model\TacticModel;
use App\HttpCodes;
/**
* API endpoint related to tactics
*/
class TacticEndpoint {
private TacticInfoGateway $tactics;
private TacticModel $model;
/**
* @param TacticInfoGateway $tactics
* @param TacticModel $model
*/
public function __construct(TacticInfoGateway $tactics) {
$this->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);
}

@ -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;
}

@ -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",
[

@ -0,0 +1,14 @@
<?php
namespace App;
/**
* Utility class to define constants of used http codes
*/
class HttpCodes {
public const OK = 200;
public const BAD_REQUEST = 400;
public const NOT_FOUND = 404;
public const PRECONDITION_FAILED = 412;
}

@ -4,9 +4,14 @@ namespace App\Model;
use App\Data\TacticInfo;
use App\Gateway\TacticInfoGateway;
use App\Validation\ValidationFail;
use App\Validation\Validation;
use App\Validation\Validators;
class TacticModel {
const TACTIC_DEFAULT_NAME = "Nouvelle tactique";
public const TACTIC_DEFAULT_NAME = "Nouvelle tactique";
private TacticInfoGateway $tactics;
@ -17,12 +22,40 @@ class TacticModel {
$this->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);
}
}
}

@ -0,0 +1,38 @@
<?php
namespace App\Validation;
/**
* An error that concerns a field, with a bound message name
*/
class FieldValidationFail extends ValidationFail {
private string $fieldName;
/**
* @param string $fieldName
* @param string $message
*/
public function __construct(string $fieldName, string $message) {
parent::__construct("field", $message);
$this->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()];
}
}

@ -0,0 +1,28 @@
<?php
namespace App\Validation;
/**
* A simple validator that takes a predicate and an error factory
*/
class SimpleFunctionValidator implements Validator {
private $predicate;
private $error_factory;
/**
* @param callable $predicate a function predicate with signature: `(string) => 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;
}
}

@ -0,0 +1,30 @@
<?php
namespace App\Validation;
/**
* Utility class for validation
*/
class Validation {
/**
* Validate a value from validators, appending failures in the given errors array.
* @param mixed $val the value to validate
* @param string $val_name the name of the value
* @param array $errors array to push when a validator fails
* @param Validator ...$validators given validators
* @return bool true if any of the given validators did fail
*/
public static function validate($val, string $val_name, array &$errors, Validator...$validators): bool {
$had_errors = false;
foreach ($validators as $validator) {
$error = $validator->validate($val_name, $val);
if ($error != null) {
$errors[] = $error;
$had_errors = true;
}
}
return $had_errors;
}
}

@ -0,0 +1,34 @@
<?php
namespace App\Validation;
class ValidationFail implements \JsonSerializable {
private string $kind;
private string $message;
/**
* @param string $message
* @param string $kind
*/
protected function __construct(string $kind, string $message) {
$this->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);
}
}

@ -0,0 +1,15 @@
<?php
namespace App\Validation;
interface Validator {
/**
* validates a variable string
* @param string $name the name of the tested value
* @param mixed $val the value to validate
* @return ValidationFail|null the error if the validator did fail, or null if it succeeded
*/
public function validate(string $name, $val): ?ValidationFail;
}

@ -0,0 +1,34 @@
<?php
namespace App\Validation;
/**
* A collection of standard validators
*/
class Validators {
/**
* @return Validator a validator that validates strings that does not contain invalid chars such as `<` and `>`
*/
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)
);
}
}
Loading…
Cancel
Save