Skip to content

Commit e6c6225

Browse files
authored
default types validation (#648)
default types validation + updated readme with instructions
1 parent aacb9ea commit e6c6225

File tree

7 files changed

+755
-126
lines changed

7 files changed

+755
-126
lines changed

README.md

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,7 @@ You can enable the following middleware using the "middlewares" config parameter
597597
- "basicAuth": Support for "Basic Authentication"
598598
- "reconnect": Reconnect to the database with different parameters
599599
- "authorization": Restrict access to certain tables or columns
600-
- "validation": Return input validation errors for custom rules
600+
- "validation": Return input validation errors for custom rules and default type rules
601601
- "ipAddress": Fill a protected field with the IP address on create
602602
- "sanitation": Apply input sanitation on create and update
603603
- "multiTenancy": Restricts tenants access in a multi-tenant scenario
@@ -652,6 +652,7 @@ You can tune the middleware behavior using middleware specific configuration par
652652
- "authorization.columnHandler": Handler to implement column authorization rules ("")
653653
- "authorization.recordHandler": Handler to implement record authorization filter rules ("")
654654
- "validation.handler": Handler to implement validation rules for input values ("")
655+
- "validation.types": List of types for which the default validation must take place ("all")
655656
- "ipAddress.tables": Tables to search for columns to override with IP address ("")
656657
- "ipAddress.columns": Columns to protect and override with the IP address on create ("")
657658
- "sanitation.handler": Handler to implement sanitation rules for input values ("")
@@ -888,8 +889,12 @@ The above example will strip all HTML tags from strings in the input.
888889

889890
### Validating input
890891

891-
By default all input is accepted. If you want to validate the input, you may add the 'validation' middleware and define a
892-
'validation.handler' function that returns a boolean indicating whether or not the value is valid.
892+
By default all input is accepted unless the validation middleware is specified. The default types validations are then applied.
893+
894+
#### Validation handler
895+
896+
If you want to validate the input in a custom way, you may add the 'validation' middleware and define a 'validation.handler'
897+
function that returns a boolean indicating whether or not the value is valid.
893898

894899
'validation.handler' => function ($operation, $tableName, $column, $value, $context) {
895900
return ($column['name'] == 'post_id' && !is_numeric($value)) ? 'must be numeric' : true;
@@ -915,6 +920,43 @@ Then the server will return a '422' HTTP status code and nice error message:
915920

916921
You can parse this output to make form fields show up with a red border and their appropriate error message.
917922

923+
#### Validation types
924+
925+
The default types validations return the following error messages:
926+
| error message | applies to types |
927+
| ---- | ---- |
928+
| cannot be null | any non-nullable column |
929+
| must be numeric | integer bigint |
930+
| exceeds range | integer bigint |
931+
| too long | varchar varbinary |
932+
| not a float | decimal float double |
933+
| not a valid boolean | boolean |
934+
| invalid date format use yyyy-mm-dd | date timestamp |
935+
| not a valid date | date timestamp |
936+
| invalid time format use hh:mm:ss | time timestamp |
937+
| non-numeric time value | time timestamp |
938+
| not a valid time | time timestamp |
939+
| invalid timestamp format use yyyy-mm-dd hh:mm:ss | timestamp |
940+
941+
If you want the types validation to apply to all the types, you must activate the `validation` middleware.
942+
By default, all types are enabled. Which is equivalent to the two configuration possibilities:
943+
944+
'validation.types' => 'all',
945+
946+
or
947+
948+
'validation.types'=> 'integer,bigint,varchar,decimal,float,double,boolean,date,time,timestamp,clob,blob,varbinary,geometry',
949+
950+
Types with no declared error message can be checked whether null when the column is non-nullable.
951+
952+
In case you want to use a validation handler but don't want any types validation, use either:
953+
954+
'validation.types' => '',
955+
956+
or
957+
958+
'validation.types'=> 'none',
959+
918960
### Multi-tenancy support
919961

920962
Two forms of multi-tenancy are supported:

api.php

Lines changed: 210 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -8218,74 +8218,223 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
82188218
use Psr\Http\Message\ResponseInterface;
82198219
use Psr\Http\Message\ServerRequestInterface;
82208220
use Psr\Http\Server\RequestHandlerInterface;
8221-
use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
82228221
use Tqdev\PhpCrudApi\Column\ReflectionService;
8222+
use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
82238223
use Tqdev\PhpCrudApi\Controller\Responder;
82248224
use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
82258225
use Tqdev\PhpCrudApi\Middleware\Router\Router;
82268226
use Tqdev\PhpCrudApi\Record\ErrorCode;
82278227
use Tqdev\PhpCrudApi\RequestUtils;
82288228

8229-
class ValidationMiddleware extends Middleware
8230-
{
8231-
private $reflection;
8232-
8233-
public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
8234-
{
8235-
parent::__construct($router, $responder, $properties);
8236-
$this->reflection = $reflection;
8237-
}
8238-
8239-
private function callHandler($handler, $record, string $operation, ReflectedTable $table) /*: ResponseInterface?*/
8240-
{
8241-
$context = (array) $record;
8242-
$details = array();
8243-
$tableName = $table->getName();
8244-
foreach ($context as $columnName => $value) {
8245-
if ($table->hasColumn($columnName)) {
8246-
$column = $table->getColumn($columnName);
8247-
$valid = call_user_func($handler, $operation, $tableName, $column->serialize(), $value, $context);
8248-
if ($valid !== true && $valid !== '') {
8249-
$details[$columnName] = $valid;
8250-
}
8251-
}
8252-
}
8253-
if (count($details) > 0) {
8254-
return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED, $tableName, $details);
8255-
}
8256-
return null;
8257-
}
8258-
8259-
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
8260-
{
8261-
$operation = RequestUtils::getOperation($request);
8262-
if (in_array($operation, ['create', 'update', 'increment'])) {
8263-
$tableName = RequestUtils::getPathSegment($request, 2);
8264-
if ($this->reflection->hasTable($tableName)) {
8265-
$record = $request->getParsedBody();
8266-
if ($record !== null) {
8267-
$handler = $this->getProperty('handler', '');
8268-
if ($handler !== '') {
8269-
$table = $this->reflection->getTable($tableName);
8270-
if (is_array($record)) {
8271-
foreach ($record as $r) {
8272-
$response = $this->callHandler($handler, $r, $operation, $table);
8273-
if ($response !== null) {
8274-
return $response;
8275-
}
8276-
}
8277-
} else {
8278-
$response = $this->callHandler($handler, $record, $operation, $table);
8279-
if ($response !== null) {
8280-
return $response;
8281-
}
8282-
}
8283-
}
8284-
}
8285-
}
8286-
}
8287-
return $next->handle($request);
8288-
}
8229+
class ValidationMiddleware extends Middleware {
8230+
private $reflection;
8231+
private $typesToValidate;
8232+
8233+
public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection) {
8234+
parent::__construct($router, $responder, $properties);
8235+
$this->reflection = $reflection;
8236+
$typesStr = $this->getProperty('types', 'all');
8237+
if (is_null($typesStr)) {
8238+
$typesStr = 'all';
8239+
}
8240+
if (strlen($typesStr) == 0) {
8241+
$typesStr = 'none';
8242+
}
8243+
$this->typesToValidate = explode(',', $typesStr);
8244+
if (is_null($this->typesToValidate) || count($this->typesToValidate) == 0) {
8245+
$this->typesToValidate = ['all'];
8246+
}
8247+
}
8248+
8249+
private function callHandler($handler, $record, string $operation, ReflectedTable $table) /*: ResponseInterface?*/ {
8250+
$context = (array) $record;
8251+
$details = array();
8252+
$tableName = $table->getName();
8253+
foreach ($context as $columnName => $value) {
8254+
if ($table->hasColumn($columnName)) {
8255+
$column = $table->getColumn($columnName);
8256+
$valid = call_user_func($handler, $operation, $tableName, $column->serialize(), $value, $context);
8257+
if ($valid || $valid == '') {
8258+
$valid = $this->validateType($column->serialize(), $value);
8259+
}
8260+
if ($valid !== true && $valid !== '') {
8261+
$details[$columnName] = $valid;
8262+
}
8263+
}
8264+
}
8265+
if (count($details) > 0) {
8266+
return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED, $tableName, $details);
8267+
}
8268+
return null;
8269+
}
8270+
8271+
private function validateType($column, $value) {
8272+
if ($this->typesToValidate[0] == 'none') {
8273+
return (true);
8274+
}
8275+
if ($this->typesToValidate[0] != 'all') {
8276+
if (!in_array($column['type'], $this->typesToValidate)) {
8277+
return (true);
8278+
}
8279+
}
8280+
if (is_null($value)) {
8281+
return ($column["nullable"] ? true : "cannot be null");
8282+
}
8283+
switch ($column['type']) {
8284+
case 'integer':
8285+
if (!is_numeric($value)) {
8286+
return ('must be numeric');
8287+
}
8288+
8289+
if (strlen($value) > 20) {
8290+
return ('exceeds range');
8291+
}
8292+
8293+
break;
8294+
case 'bigint':
8295+
if (!is_numeric($value)) {
8296+
return ('must be numeric');
8297+
}
8298+
8299+
if (strlen($value) > 20) {
8300+
return ('exceeds range');
8301+
}
8302+
8303+
break;
8304+
case 'varchar':
8305+
if (strlen($value) > $column['length']) {
8306+
return ('too long');
8307+
}
8308+
8309+
break;
8310+
case 'decimal':
8311+
if (!is_float($value) && !is_numeric($value)) {
8312+
return ('not a float');
8313+
}
8314+
8315+
break;
8316+
case 'float':
8317+
if (!is_float($value) && !is_numeric($value)) {
8318+
return ('not a float');
8319+
}
8320+
8321+
break;
8322+
case 'double':
8323+
if (!is_float($value) && !is_numeric($value)) {
8324+
return ('not a float');
8325+
}
8326+
8327+
break;
8328+
case 'boolean':
8329+
if ($value != 0 && $value != 1) {
8330+
return ('not a valid boolean');
8331+
}
8332+
8333+
break;
8334+
case 'date':
8335+
$date_array = explode('-', $value);
8336+
if (count($date_array) != 3) {
8337+
return ('invalid date format use yyyy-mm-dd');
8338+
}
8339+
8340+
if (!@checkdate($date_array[1], $date_array[2], $date_array[0])) {
8341+
return ('not a valid date');
8342+
}
8343+
8344+
break;
8345+
case 'time':
8346+
$time_array = explode(':', $value);
8347+
if (count($time_array) != 3) {
8348+
return ('invalid time format use hh:mm:ss');
8349+
}
8350+
8351+
foreach ($time_array as $t) {
8352+
if (!is_numeric($t)) {
8353+
return ('non-numeric time value');
8354+
}
8355+
}
8356+
8357+
if ($time_array[1] < 0 || $time_array[2] < 0 || $time_array[0] < -838 || $time_array[1] > 59 || $time_array[2] > 59 || $time_array[0] > 838) {
8358+
return ('not a valid time');
8359+
}
8360+
8361+
break;
8362+
case 'timestamp':
8363+
$split_timestamp = explode(' ', $value);
8364+
if (count($split_timestamp) != 2) {
8365+
return ('invalid timestamp format use yyyy-mm-dd hh:mm:ss');
8366+
}
8367+
8368+
$date_array = explode('-', $split_timestamp[0]);
8369+
if (count($date_array) != 3) {
8370+
return ('invalid date format use yyyy-mm-dd');
8371+
}
8372+
8373+
if (!@checkdate($date_array[1], $date_array[2], $date_array[0])) {
8374+
return ('not a valid date');
8375+
}
8376+
8377+
$time_array = explode(':', $split_timestamp[1]);
8378+
if (count($time_array) != 3) {
8379+
return ('invalid time format use hh:mm:ss');
8380+
}
8381+
8382+
foreach ($time_array as $t) {
8383+
if (!is_numeric($t)) {
8384+
return ('non-numeric time value');
8385+
}
8386+
}
8387+
8388+
if ($time_array[1] < 0 || $time_array[2] < 0 || $time_array[0] < 0 || $time_array[1] > 59 || $time_array[2] > 59 || $time_array[0] > 23) {
8389+
return ('not a valid time');
8390+
}
8391+
8392+
break;
8393+
case 'clob':
8394+
break;
8395+
case 'blob':
8396+
break;
8397+
case 'varbinary':
8398+
if (((strlen($value) * 3 / 4) - substr_count(substr($value, -2), '=')) > $column['length']) {
8399+
return ('too long');
8400+
}
8401+
8402+
break;
8403+
case 'geometry':
8404+
break;
8405+
}
8406+
return (true);
8407+
}
8408+
8409+
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface{
8410+
$operation = RequestUtils::getOperation($request);
8411+
if (in_array($operation, ['create', 'update', 'increment'])) {
8412+
$tableName = RequestUtils::getPathSegment($request, 2);
8413+
if ($this->reflection->hasTable($tableName)) {
8414+
$record = $request->getParsedBody();
8415+
if ($record !== null) {
8416+
$handler = $this->getProperty('handler', '');
8417+
if ($handler !== '') {
8418+
$table = $this->reflection->getTable($tableName);
8419+
if (is_array($record)) {
8420+
foreach ($record as $r) {
8421+
$response = $this->callHandler($handler, $r, $operation, $table);
8422+
if ($response !== null) {
8423+
return $response;
8424+
}
8425+
}
8426+
} else {
8427+
$response = $this->callHandler($handler, $record, $operation, $table);
8428+
if ($response !== null) {
8429+
return $response;
8430+
}
8431+
}
8432+
}
8433+
}
8434+
}
8435+
}
8436+
return $next->handle($request);
8437+
}
82898438
}
82908439
}
82918440

docker/ubuntu16/run.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ echo "done"
4141

4242
echo -n "[3/4] Starting SQLServer 2017 ... "
4343
# run sqlserver server
44-
nohup /opt/mssql/bin/sqlservr --accept-eula > /root/mysql.log 2>&1 &
44+
nohup /opt/mssql/bin/sqlservr --accept-eula > /root/mssql.log 2>&1 &
4545
# create database and user on postgres
4646
/opt/mssql-tools/bin/sqlcmd -l 30 -S localhost -U SA -P sapwd123! >/dev/null << 'EOF'
4747
CREATE DATABASE [php-crud-api]

docker/ubuntu18/Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ FROM ubuntu:18.04
22

33
ARG DEBIAN_FRONTEND=noninteractive
44

5+
ENV TZ=Etc/UTC
6+
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
7+
58
# install: php / mysql / postgres / sqlite / tools
69
RUN apt-get update && apt-get -y install \
710
php-cli php-xml \

docker/ubuntu20/Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ FROM ubuntu:20.04
22

33
ARG DEBIAN_FRONTEND=noninteractive
44

5+
ENV TZ=Etc/UTC
6+
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
7+
58
# install: php / mysql / postgres / sqlite / tools
69
RUN apt-get update && apt-get -y install \
710
php-cli php-xml \

0 commit comments

Comments
 (0)