Browse Source

Add type sanitation

Maurits van der Schee 4 years ago
parent
commit
78e1314864

+ 54
- 52
README.md View File

@@ -652,10 +652,13 @@ You can tune the middleware behavior using middleware specific configuration par
652 652
 - "authorization.columnHandler": Handler to implement column authorization rules ("")
653 653
 - "authorization.recordHandler": Handler to implement record authorization filter rules ("")
654 654
 - "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")
655
+- "validation.types": Types to enable type validation for, empty means 'none' ("all")
656
+- "validation.tables": Tables to enable type validation for, empty means 'none' ("all")
656 657
 - "ipAddress.tables": Tables to search for columns to override with IP address ("")
657 658
 - "ipAddress.columns": Columns to protect and override with the IP address on create ("")
658 659
 - "sanitation.handler": Handler to implement sanitation rules for input values ("")
660
+- "sanitation.types": Types to enable type sanitation for, empty means 'none' ("all")
661
+- "sanitation.tables": Tables to enable type sanitation for, empty means 'none' ("all")
659 662
 - "multiTenancy.handler": Handler to implement simple multi-tenancy rules ("")
660 663
 - "pageLimits.pages": The maximum page number that a list operation allows ("100")
661 664
 - "pageLimits.records": The maximum number of records returned by a list operation ("1000")
@@ -887,14 +890,28 @@ the 'sanitation' middleware and define a 'sanitation.handler' function that retu
887 890
 
888 891
 The above example will strip all HTML tags from strings in the input.
889 892
 
890
-### Validating input
893
+### Type sanitation
894
+
895
+If you enable the 'sanitation' middleware, then you (automtically) also enable type sanitation. When this is enabled you may:
896
+
897
+- send leading and trailing whitespace (it will be ignored).
898
+- send a float to an integer field (it will be rounded).
899
+- send a base64url encoded (it will be converted to regular base64 encoding).
900
+- send a time/date/timestamp in any strtotime accepted format (it will be converted).
891 901
 
892
-By default all input is accepted unless the validation middleware is specified. The default types validations are then applied.
902
+You may use the config settings "`sanitation.types`" and "`sanitation.tables`"' to define for which types and
903
+in which tables you want to apply type sanitation (defaults to 'all'). Example:
893 904
 
894
-#### Validation handler
905
+    'sanitation.types' => 'date,timestamp',
906
+    'sanitation.tables' => 'posts,comments',
895 907
 
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.
908
+Here we enable the type sanitation for date and timestamp fields in the posts and comments tables.
909
+
910
+### Validating input
911
+
912
+By default all input is accepted and sent to the database. If you want to validate the input in a custom way, 
913
+you may add the 'validation' middleware and define a 'validation.handler' function that returns a boolean 
914
+indicating whether or not the value is valid.
898 915
 
899 916
     'validation.handler' => function ($operation, $tableName, $column, $value, $context) {
900 917
         return ($column['name'] == 'post_id' && !is_numeric($value)) ? 'must be numeric' : true;
@@ -920,9 +937,10 @@ Then the server will return a '422' HTTP status code and nice error message:
920 937
 
921 938
 You can parse this output to make form fields show up with a red border and their appropriate error message.
922 939
 
923
-#### Validation types
940
+### Type validations
924 941
 
925
-The default types validations return the following error messages:
942
+If you enable the 'validation' middleware, then you (automtically) also enable type validation. 
943
+This includes the following error messages:
926 944
 
927 945
 | error message       | reason                      | applies to types                            |
928 946
 | ------------------- | --------------------------- | ------------------------------------------- |
@@ -940,18 +958,13 @@ The default types validations return the following error messages:
940 958
 | invalid timestamp   | use yyyy-mm-dd hh:mm:ss     | timestamp                                   |
941 959
 | invalid base64      | illegal characters          | varbinary, blob                             |
942 960
 
943
-If you want the types validation to apply to all the types, you must activate the "`validation`" middleware.
944
-By default, all types are enabled. Which is equivalent to the two configuration possibilities:
945
-
946
-    'validation.types' => 'all',
947
-    
948
-or
949
-
950
-    'validation.types'=> 'integer,bigint,varchar,decimal,float,double,boolean,date,time,timestamp,clob,blob,varbinary,geometry',
961
+You may use the config settings "`validation.types`" and "`validation.tables`"' to define for which types and
962
+in which tables you want to apply type validation (defaults to 'all'). Example:
951 963
 
952
-In case you want to use a validation handler but don't want any types validation, use:
964
+    'validation.types' => 'date,timestamp',
965
+    'validation.tables' => 'posts,comments',
953 966
 
954
-    'validation.types' => '',
967
+Here we enable the type validation for date and timestamp fields in the posts and comments tables.
955 968
 
956 969
 NB: Types that are enabled will be checked for null values when the column is non-nullable.
957 970
 
@@ -1058,41 +1071,30 @@ in case that you use a non-default "cacheType" the hostname (optionally with por
1058 1071
 
1059 1072
 ## Types
1060 1073
 
1061
-These are the supported types with their default length/precision/scale:
1062
-
1063
-character types
1064
-- varchar(255)
1065
-- clob
1066
-
1067
-boolean types:
1068
-- boolean
1069
-
1070
-integer types:
1071
-- integer
1072
-- bigint
1073
-
1074
-floating point types:
1075
-- float
1076
-- double
1077
-
1078
-decimal types:
1079
-- decimal(19,4)
1080
-
1081
-date/time types:
1082
-- date
1083
-- time
1084
-- timestamp
1085
-
1086
-binary types:
1087
-- varbinary(255)
1088
-- blob
1089
-
1090
-other types:
1091
-- geometry /* non-jdbc type, extension with limited support */
1074
+These are the supported types with their length, category, JSON type and format:
1075
+
1076
+| type       | length | category  | JSON type | format              |
1077
+| ---------- | ------ | --------- | --------- | ------------------- |
1078
+| varchar    | 255    | character | string    |                     |
1079
+| clob       |        | character | string    |                     |
1080
+| boolean    |        | boolean   | boolean   |                     |
1081
+| integer    |        | integer   | number    |                     |
1082
+| bigint     |        | integer   | number    |                     |
1083
+| float      |        | float     | number    |                     |
1084
+| double     |        | float     | number    |                     |
1085
+| decimal    | 19,4   | decimal   | string    |                     |
1086
+| date       |        | date/time | string    | yyyy-mm-dd          | 
1087
+| time       |        | date/time | srting    | hh:mm:ss            |
1088
+| timestamp  |        | date/time | string    | yyyy-mm-dd hh:mm:ss |
1089
+| varbinary  | 255    | binary    | string    | base64 encoded      |
1090
+| blob       |        | binary    | string    | base64 encoded      |
1091
+| geometry   |        | other     | string    | well-known text     |
1092
+
1093
+Note that geometry is a non-jdbc type and thus has limited support.
1092 1094
 
1093 1095
 ## Data types in JavaScript
1094 1096
 
1095
-Javascript and Javascript object notation are not very well suited for reading database records. Decimal, date/time, binary and geometry types are represented as strings in JSON (binary is base64 encoded, geometries are in WKT format). Below are two more serious issues described.
1097
+Javascript and Javascript object notation (JSON) are not very well suited for reading database records. Decimal, date/time, binary and geometry types must be represented as strings in JSON (binary is base64 encoded, geometries are in WKT format). Below are two more serious issues described.
1096 1098
 
1097 1099
 ### 64 bit integers
1098 1100
 
@@ -1147,7 +1149,7 @@ I am testing mainly on Ubuntu and I have the following test setups:
1147 1149
   - (Docker) Debian 9 with PHP 7.0, MariaDB 10.1, PostgreSQL 9.6 (PostGIS 2.3) and SQLite 3.16
1148 1150
   - (Docker) Ubuntu 18.04 with PHP 7.2, MySQL 5.7, PostgreSQL 10.4 (PostGIS 2.4) and SQLite 3.22
1149 1151
   - (Docker) Debian 10 with PHP 7.3, MariaDB 10.3, PostgreSQL 11.4 (PostGIS 2.5) and SQLite 3.27
1150
-  - (Docker) Ubuntu 20.04 with PHP 7.3, MySQL 8.0, PostgreSQL 12.2 (PostGIS 3.0) and SQLite 3.31
1152
+  - (Docker) Ubuntu 20.04 with PHP 7.4, MySQL 8.0, PostgreSQL 12.2 (PostGIS 3.0) and SQLite 3.31
1151 1153
   - (Docker) CentOS 8 with PHP 7.4, MariaDB 10.4, PostgreSQL 12.2 (PostGIS 3.0) and SQLite 3.26
1152 1154
 
1153 1155
 This covers not all environments (yet), so please notify me of failing tests and report your environment. 
@@ -1262,7 +1264,7 @@ To run the docker tests run "build_all.sh" and "run_all.sh" from the docker dire
1262 1264
     sqlsrv: skipped, driver not loaded
1263 1265
     sqlite: 105 tests ran in 1063 ms, 12 skipped, 0 failed
1264 1266
     ================================================
1265
-    Ubuntu 20.04 (PHP 7.3)
1267
+    Ubuntu 20.04 (PHP 7.4)
1266 1268
     ================================================
1267 1269
     [1/4] Starting MySQL 8.0 ........ done
1268 1270
     [2/4] Starting PostgreSQL 12.2 .. done

+ 118
- 40
api.php View File

@@ -8155,6 +8155,7 @@ namespace Tqdev\PhpCrudApi\Middleware {
8155 8155
     use Psr\Http\Message\ServerRequestInterface;
8156 8156
     use Psr\Http\Server\RequestHandlerInterface;
8157 8157
     use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
8158
+    use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
8158 8159
     use Tqdev\PhpCrudApi\Column\ReflectionService;
8159 8160
     use Tqdev\PhpCrudApi\Controller\Responder;
8160 8161
     use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
@@ -8179,11 +8180,97 @@ namespace Tqdev\PhpCrudApi\Middleware {
8179 8180
                 if ($table->hasColumn($columnName)) {
8180 8181
                     $column = $table->getColumn($columnName);
8181 8182
                     $value = call_user_func($handler, $operation, $tableName, $column->serialize(), $value);
8183
+                    $value = $this->sanitizeType($table, $column, $value);
8182 8184
                 }
8183 8185
             }
8184 8186
             return (object) $context;
8185 8187
         }
8186 8188
 
8189
+        private function sanitizeType(ReflectedTable $table, ReflectedColumn $column, $value)
8190
+        {
8191
+            $tables = $this->getArrayProperty('tables', 'all');
8192
+            $types = $this->getArrayProperty('types', 'all');
8193
+            if (
8194
+                (in_array('all', $tables) || in_array($table->getName(), $tables)) &&
8195
+                (in_array('all', $types) || in_array($column->getType(), $types))
8196
+            ) {
8197
+                if (is_null($value)) {
8198
+                    return $value;
8199
+                }
8200
+                if (is_string($value)) {
8201
+                    $newValue = null;
8202
+                    switch ($column->getType()) {
8203
+                        case 'integer':
8204
+                        case 'bigint':
8205
+                            $newValue = filter_var(trim($value), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
8206
+                            break;
8207
+                        case 'decimal':
8208
+                            $newValue = filter_var(trim($value), FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
8209
+                            if (is_float($newValue)) {
8210
+                                $newValue = number_format($newValue, $column->getScale(), '.', '');
8211
+                            }
8212
+                            break;
8213
+                        case 'float':
8214
+                        case 'double':
8215
+                            $newValue = filter_var(trim($value), FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
8216
+                            break;
8217
+                        case 'boolean':
8218
+                            $newValue = filter_var(trim($value), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
8219
+                            break;
8220
+                        case 'date':
8221
+                            $time = strtotime(trim($value));
8222
+                            if ($time !== false) {
8223
+                                $newValue = date('Y-m-d', $time);
8224
+                            }
8225
+                            break;
8226
+                        case 'time':
8227
+                            $time = strtotime(trim($value));
8228
+                            if ($time !== false) {
8229
+                                $newValue = date('H:i:s', $time);
8230
+                            }
8231
+                            break;
8232
+                        case 'timestamp':
8233
+                            $time = strtotime(trim($value));
8234
+                            if ($time !== false) {
8235
+                                $newValue = date('Y-m-d H:i:s', $time);
8236
+                            }
8237
+                            break;
8238
+                        case 'blob':
8239
+                        case 'varbinary':
8240
+                            // allow base64url format
8241
+                            $newValue = strtr(trim($value), '-_', '+/');
8242
+                            break;
8243
+                        case 'clob':
8244
+                        case 'varchar':
8245
+                            $newValue = $value;
8246
+                            break;
8247
+                        case 'geometry':
8248
+                            $newValue = trim($value);
8249
+                            break;
8250
+                    }
8251
+                    if (!is_null($newValue)) {
8252
+                        $value = $newValue;
8253
+                    }
8254
+                } else {
8255
+                    switch ($column->getType()) {
8256
+                        case 'integer':
8257
+                        case 'bigint':
8258
+                            if (is_float($value)) {
8259
+                                $value = (int) round($value);
8260
+                            }
8261
+                            break;
8262
+                        case 'decimal':
8263
+                            if (is_float($value) || is_int($value)) {
8264
+                                $value = number_format((float) $value, $column->getScale(), '.', '');
8265
+                            }
8266
+                            break;
8267
+                    }
8268
+                }
8269
+                // post process
8270
+            }
8271
+            return $value;
8272
+        }
8273
+
8187 8274
         public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
8188 8275
         {
8189 8276
             $operation = RequestUtils::getOperation($request);
@@ -8247,7 +8334,7 @@ namespace Tqdev\PhpCrudApi\Middleware {
8247 8334
     				$column = $table->getColumn($columnName);
8248 8335
     				$valid = call_user_func($handler, $operation, $tableName, $column->serialize(), $value, $context);
8249 8336
     				if ($valid === true || $valid === '') {
8250
-    					$valid = $this->validateType($column, $value);
8337
+    					$valid = $this->validateType($table, $column, $value);
8251 8338
     				}
8252 8339
     				if ($valid !== true && $valid !== '') {
8253 8340
     					$details[$columnName] = $valid;
@@ -8260,10 +8347,14 @@ namespace Tqdev\PhpCrudApi\Middleware {
8260 8347
     		return null;
8261 8348
     	}
8262 8349
 
8263
-    	private function validateType(ReflectedColumn $column, $value)
8350
+    	private function validateType(ReflectedTable $table, ReflectedColumn $column, $value)
8264 8351
     	{
8352
+    		$tables = $this->getArrayProperty('tables', 'all');
8265 8353
     		$types = $this->getArrayProperty('types', 'all');
8266
-    		if (in_array('all', $types) || in_array($column->getType(), $types)) {
8354
+    		if (
8355
+    			(in_array('all', $tables) || in_array($table->getName(), $tables)) &&
8356
+    			(in_array('all', $types) || in_array($column->getType(), $types))
8357
+    		) {
8267 8358
     			if (is_null($value)) {
8268 8359
     				return ($column->getNullable() ? true : "cannot be null");
8269 8360
     			}
@@ -8290,15 +8381,24 @@ namespace Tqdev\PhpCrudApi\Middleware {
8290 8381
     							return 'invalid integer';
8291 8382
     						}
8292 8383
     						break;
8293
-    					case 'varchar':
8294
-    						if (mb_strlen($value, 'UTF-8') > $column->getLength()) {
8295
-    							return 'string too long';
8296
-    						}
8297
-    						break;
8298 8384
     					case 'decimal':
8299
-    						if (!is_numeric($value)) {
8385
+    						if (strpos($value, '.') !== false) {
8386
+    							list($whole, $decimals) = explode('.', $value, 2);
8387
+    						} else {
8388
+    							list($whole, $decimals) = array($value, '');
8389
+    						}
8390
+    						if (strlen($whole) > 0 && !ctype_digit($whole)) {
8391
+    							return 'invalid decimal';
8392
+    						}
8393
+    						if (strlen($decimals) > 0 && !ctype_digit($decimals)) {
8300 8394
     							return 'invalid decimal';
8301 8395
     						}
8396
+    						if (strlen($whole) > $column->getPrecision() - $column->getScale()) {
8397
+    							return 'decimal too large';
8398
+    						}
8399
+    						if (strlen($decimals) > $column->getScale()) {
8400
+    							return 'decimal too precise';
8401
+    						}
8302 8402
     						break;
8303 8403
     					case 'float':
8304 8404
     					case 'double':
@@ -8310,9 +8410,7 @@ namespace Tqdev\PhpCrudApi\Middleware {
8310 8410
     						}
8311 8411
     						break;
8312 8412
     					case 'boolean':
8313
-    						if (
8314
-    							filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) === null
8315
-    						) {
8413
+    						if (!in_array(strtolower($value), array('true', 'false'))) {
8316 8414
     							return 'invalid boolean';
8317 8415
     						}
8318 8416
     						break;
@@ -8332,13 +8430,19 @@ namespace Tqdev\PhpCrudApi\Middleware {
8332 8430
     						}
8333 8431
     						break;
8334 8432
     					case 'clob':
8335
-    						// no checks needed
8433
+    					case 'varchar':
8434
+    						if ($column->hasLength() && mb_strlen($value, 'UTF-8') > $column->getLength()) {
8435
+    							return 'string too long';
8436
+    						}
8336 8437
     						break;
8337 8438
     					case 'blob':
8338 8439
     					case 'varbinary':
8339 8440
     						if (base64_decode($value, true) === false) {
8340 8441
     							return 'invalid base64';
8341 8442
     						}
8443
+    						if ($column->hasLength() && strlen(base64_decode($value)) > $column->getLength()) {
8444
+    							return 'string too long';
8445
+    						}
8342 8446
     						break;
8343 8447
     					case 'geometry':
8344 8448
     						// no checks yet
@@ -8352,7 +8456,6 @@ namespace Tqdev\PhpCrudApi\Middleware {
8352 8456
     							return 'invalid integer';
8353 8457
     						}
8354 8458
     						break;
8355
-    					case 'decimal':
8356 8459
     					case 'float':
8357 8460
     					case 'double':
8358 8461
     						if (!is_float($value) && !is_int($value)) {
@@ -8360,7 +8463,7 @@ namespace Tqdev\PhpCrudApi\Middleware {
8360 8463
     						}
8361 8464
     						break;
8362 8465
     					case 'boolean':
8363
-    						if (!(is_int($value) && ($value === 1 || $value === 0)) && !is_bool($value)) {
8466
+    						if (!is_bool($value) && ($value !== 0) && ($value !== 1)) {
8364 8467
     							return 'invalid boolean';
8365 8468
     						}
8366 8469
     						break;
@@ -8376,31 +8479,6 @@ namespace Tqdev\PhpCrudApi\Middleware {
8376 8479
     						return 'invalid integer';
8377 8480
     					}
8378 8481
     					break;
8379
-    				case 'decimal':
8380
-    					$value = "$value";
8381
-    					if (strpos($value, '.') !== false) {
8382
-    						list($whole, $decimals) = explode('.', $value, 2);
8383
-    					} else {
8384
-    						list($whole, $decimals) = array($value, '');
8385
-    					}
8386
-    					if (strlen($whole) > 0 && !ctype_digit($whole)) {
8387
-    						return 'invalid decimal';
8388
-    					}
8389
-    					if (strlen($decimals) > 0 && !ctype_digit($decimals)) {
8390
-    						return 'invalid decimal';
8391
-    					}
8392
-    					if (strlen($whole) > $column->getPrecision() - $column->getScale()) {
8393
-    						return 'decimal too large';
8394
-    					}
8395
-    					if (strlen($decimals) > $column->getScale()) {
8396
-    						return 'decimal too precise';
8397
-    					}
8398
-    					break;
8399
-    				case 'varbinary':
8400
-    					if (strlen(base64_decode($value)) > $column->getLength()) {
8401
-    						return 'string too long';
8402
-    					}
8403
-    					break;
8404 8482
     			}
8405 8483
     		}
8406 8484
     		return (true);

+ 1
- 1
docker/ubuntu20/run.sh View File

@@ -1,6 +1,6 @@
1 1
 #!/bin/bash
2 2
 echo "================================================"
3
-echo " Ubuntu 20.04 (PHP 7.3)"
3
+echo " Ubuntu 20.04 (PHP 7.4)"
4 4
 echo "================================================"
5 5
 
6 6
 echo -n "[1/4] Starting MySQL 8.0 ........ "

+ 87
- 0
src/Tqdev/PhpCrudApi/Middleware/SanitationMiddleware.php View File

@@ -6,6 +6,7 @@ use Psr\Http\Message\ResponseInterface;
6 6
 use Psr\Http\Message\ServerRequestInterface;
7 7
 use Psr\Http\Server\RequestHandlerInterface;
8 8
 use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
9
+use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
9 10
 use Tqdev\PhpCrudApi\Column\ReflectionService;
10 11
 use Tqdev\PhpCrudApi\Controller\Responder;
11 12
 use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
@@ -30,11 +31,97 @@ class SanitationMiddleware extends Middleware
30 31
             if ($table->hasColumn($columnName)) {
31 32
                 $column = $table->getColumn($columnName);
32 33
                 $value = call_user_func($handler, $operation, $tableName, $column->serialize(), $value);
34
+                $value = $this->sanitizeType($table, $column, $value);
33 35
             }
34 36
         }
35 37
         return (object) $context;
36 38
     }
37 39
 
40
+    private function sanitizeType(ReflectedTable $table, ReflectedColumn $column, $value)
41
+    {
42
+        $tables = $this->getArrayProperty('tables', 'all');
43
+        $types = $this->getArrayProperty('types', 'all');
44
+        if (
45
+            (in_array('all', $tables) || in_array($table->getName(), $tables)) &&
46
+            (in_array('all', $types) || in_array($column->getType(), $types))
47
+        ) {
48
+            if (is_null($value)) {
49
+                return $value;
50
+            }
51
+            if (is_string($value)) {
52
+                $newValue = null;
53
+                switch ($column->getType()) {
54
+                    case 'integer':
55
+                    case 'bigint':
56
+                        $newValue = filter_var(trim($value), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
57
+                        break;
58
+                    case 'decimal':
59
+                        $newValue = filter_var(trim($value), FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
60
+                        if (is_float($newValue)) {
61
+                            $newValue = number_format($newValue, $column->getScale(), '.', '');
62
+                        }
63
+                        break;
64
+                    case 'float':
65
+                    case 'double':
66
+                        $newValue = filter_var(trim($value), FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
67
+                        break;
68
+                    case 'boolean':
69
+                        $newValue = filter_var(trim($value), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
70
+                        break;
71
+                    case 'date':
72
+                        $time = strtotime(trim($value));
73
+                        if ($time !== false) {
74
+                            $newValue = date('Y-m-d', $time);
75
+                        }
76
+                        break;
77
+                    case 'time':
78
+                        $time = strtotime(trim($value));
79
+                        if ($time !== false) {
80
+                            $newValue = date('H:i:s', $time);
81
+                        }
82
+                        break;
83
+                    case 'timestamp':
84
+                        $time = strtotime(trim($value));
85
+                        if ($time !== false) {
86
+                            $newValue = date('Y-m-d H:i:s', $time);
87
+                        }
88
+                        break;
89
+                    case 'blob':
90
+                    case 'varbinary':
91
+                        // allow base64url format
92
+                        $newValue = strtr(trim($value), '-_', '+/');
93
+                        break;
94
+                    case 'clob':
95
+                    case 'varchar':
96
+                        $newValue = $value;
97
+                        break;
98
+                    case 'geometry':
99
+                        $newValue = trim($value);
100
+                        break;
101
+                }
102
+                if (!is_null($newValue)) {
103
+                    $value = $newValue;
104
+                }
105
+            } else {
106
+                switch ($column->getType()) {
107
+                    case 'integer':
108
+                    case 'bigint':
109
+                        if (is_float($value)) {
110
+                            $value = (int) round($value);
111
+                        }
112
+                        break;
113
+                    case 'decimal':
114
+                        if (is_float($value) || is_int($value)) {
115
+                            $value = number_format((float) $value, $column->getScale(), '.', '');
116
+                        }
117
+                        break;
118
+                }
119
+            }
120
+            // post process
121
+        }
122
+        return $value;
123
+    }
124
+
38 125
     public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
39 126
     {
40 127
         $operation = RequestUtils::getOperation($request);

+ 31
- 40
src/Tqdev/PhpCrudApi/Middleware/ValidationMiddleware.php View File

@@ -34,7 +34,7 @@ class ValidationMiddleware extends Middleware
34 34
 				$column = $table->getColumn($columnName);
35 35
 				$valid = call_user_func($handler, $operation, $tableName, $column->serialize(), $value, $context);
36 36
 				if ($valid === true || $valid === '') {
37
-					$valid = $this->validateType($column, $value);
37
+					$valid = $this->validateType($table, $column, $value);
38 38
 				}
39 39
 				if ($valid !== true && $valid !== '') {
40 40
 					$details[$columnName] = $valid;
@@ -47,10 +47,14 @@ class ValidationMiddleware extends Middleware
47 47
 		return null;
48 48
 	}
49 49
 
50
-	private function validateType(ReflectedColumn $column, $value)
50
+	private function validateType(ReflectedTable $table, ReflectedColumn $column, $value)
51 51
 	{
52
+		$tables = $this->getArrayProperty('tables', 'all');
52 53
 		$types = $this->getArrayProperty('types', 'all');
53
-		if (in_array('all', $types) || in_array($column->getType(), $types)) {
54
+		if (
55
+			(in_array('all', $tables) || in_array($table->getName(), $tables)) &&
56
+			(in_array('all', $types) || in_array($column->getType(), $types))
57
+		) {
54 58
 			if (is_null($value)) {
55 59
 				return ($column->getNullable() ? true : "cannot be null");
56 60
 			}
@@ -77,15 +81,24 @@ class ValidationMiddleware extends Middleware
77 81
 							return 'invalid integer';
78 82
 						}
79 83
 						break;
80
-					case 'varchar':
81
-						if (mb_strlen($value, 'UTF-8') > $column->getLength()) {
82
-							return 'string too long';
83
-						}
84
-						break;
85 84
 					case 'decimal':
86
-						if (!is_numeric($value)) {
85
+						if (strpos($value, '.') !== false) {
86
+							list($whole, $decimals) = explode('.', $value, 2);
87
+						} else {
88
+							list($whole, $decimals) = array($value, '');
89
+						}
90
+						if (strlen($whole) > 0 && !ctype_digit($whole)) {
87 91
 							return 'invalid decimal';
88 92
 						}
93
+						if (strlen($decimals) > 0 && !ctype_digit($decimals)) {
94
+							return 'invalid decimal';
95
+						}
96
+						if (strlen($whole) > $column->getPrecision() - $column->getScale()) {
97
+							return 'decimal too large';
98
+						}
99
+						if (strlen($decimals) > $column->getScale()) {
100
+							return 'decimal too precise';
101
+						}
89 102
 						break;
90 103
 					case 'float':
91 104
 					case 'double':
@@ -97,9 +110,7 @@ class ValidationMiddleware extends Middleware
97 110
 						}
98 111
 						break;
99 112
 					case 'boolean':
100
-						if (
101
-							filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) === null
102
-						) {
113
+						if (!in_array(strtolower($value), array('true', 'false'))) {
103 114
 							return 'invalid boolean';
104 115
 						}
105 116
 						break;
@@ -119,13 +130,19 @@ class ValidationMiddleware extends Middleware
119 130
 						}
120 131
 						break;
121 132
 					case 'clob':
122
-						// no checks needed
133
+					case 'varchar':
134
+						if ($column->hasLength() && mb_strlen($value, 'UTF-8') > $column->getLength()) {
135
+							return 'string too long';
136
+						}
123 137
 						break;
124 138
 					case 'blob':
125 139
 					case 'varbinary':
126 140
 						if (base64_decode($value, true) === false) {
127 141
 							return 'invalid base64';
128 142
 						}
143
+						if ($column->hasLength() && strlen(base64_decode($value)) > $column->getLength()) {
144
+							return 'string too long';
145
+						}
129 146
 						break;
130 147
 					case 'geometry':
131 148
 						// no checks yet
@@ -139,7 +156,6 @@ class ValidationMiddleware extends Middleware
139 156
 							return 'invalid integer';
140 157
 						}
141 158
 						break;
142
-					case 'decimal':
143 159
 					case 'float':
144 160
 					case 'double':
145 161
 						if (!is_float($value) && !is_int($value)) {
@@ -147,7 +163,7 @@ class ValidationMiddleware extends Middleware
147 163
 						}
148 164
 						break;
149 165
 					case 'boolean':
150
-						if (!(is_int($value) && ($value === 1 || $value === 0)) && !is_bool($value)) {
166
+						if (!is_bool($value) && ($value !== 0) && ($value !== 1)) {
151 167
 							return 'invalid boolean';
152 168
 						}
153 169
 						break;
@@ -163,31 +179,6 @@ class ValidationMiddleware extends Middleware
163 179
 						return 'invalid integer';
164 180
 					}
165 181
 					break;
166
-				case 'decimal':
167
-					$value = "$value";
168
-					if (strpos($value, '.') !== false) {
169
-						list($whole, $decimals) = explode('.', $value, 2);
170
-					} else {
171
-						list($whole, $decimals) = array($value, '');
172
-					}
173
-					if (strlen($whole) > 0 && !ctype_digit($whole)) {
174
-						return 'invalid decimal';
175
-					}
176
-					if (strlen($decimals) > 0 && !ctype_digit($decimals)) {
177
-						return 'invalid decimal';
178
-					}
179
-					if (strlen($whole) > $column->getPrecision() - $column->getScale()) {
180
-						return 'decimal too large';
181
-					}
182
-					if (strlen($decimals) > $column->getScale()) {
183
-						return 'decimal too precise';
184
-					}
185
-					break;
186
-				case 'varbinary':
187
-					if (strlen(base64_decode($value)) > $column->getLength()) {
188
-						return 'string too long';
189
-					}
190
-					break;
191 182
 			}
192 183
 		}
193 184
 		return (true);

+ 2
- 1
tests/config/base.php View File

@@ -4,7 +4,7 @@ $settings = [
4 4
     'username' => 'incorrect_username',
5 5
     'password' => 'incorrect_password',
6 6
     'controllers' => 'records,columns,cache,openapi,geojson',
7
-    'middlewares' => 'cors,reconnect,dbAuth,jwtAuth,basicAuth,authorization,validation,ipAddress,sanitation,multiTenancy,pageLimits,joinLimits,customization',
7
+    'middlewares' => 'cors,reconnect,dbAuth,jwtAuth,basicAuth,authorization,sanitation,validation,ipAddress,multiTenancy,pageLimits,joinLimits,customization',
8 8
     'dbAuth.mode' => 'optional',
9 9
     'dbAuth.returnedColumns' => 'id,username,password',
10 10
     'jwtAuth.mode' => 'optional',
@@ -35,6 +35,7 @@ $settings = [
35 35
     'sanitation.handler' => function ($operation, $tableName, $column, $value) {
36 36
         return is_string($value) ? strip_tags($value) : $value;
37 37
     },
38
+    'sanitation.tables' => 'forgiving',
38 39
     'validation.handler' => function ($operation, $tableName, $column, $value, $context) {
39 40
         return ($column['name'] == 'post_id' && !is_numeric($value)) ? 'must be numeric' : true;
40 41
     },

+ 22
- 22
tests/functional/003_columns/015_update_types_table.log View File

@@ -12,7 +12,7 @@ true
12 12
 POST /records/types
13 13
 Content-Type: application/json
14 14
 
15
-{"integer":2,"bigint":3,"varchar":"abc","decimal":1.23,"float":1,"double":23.45,"boolean":true,"date":"1970-01-01","time":"00:00:01","timestamp":"2001-02-03 04:05:06","clob":"a","blob":"YQ==","geometry":"POINT(1 2)"}
15
+{"integer":2,"bigint":3,"varchar":"abc","decimal":"1.23","float":1,"double":23.45,"boolean":true,"date":"1970-01-01","time":"00:00:01","timestamp":"2001-02-03 04:05:06","clob":"a","blob":"YQ==","geometry":"POINT(1 2)"}
16 16
 ===
17 17
 200
18 18
 Content-Type: application/json
@@ -62,7 +62,7 @@ Content-Length: 101
62 62
 ===
63 63
 PUT /records/types/1
64 64
 
65
-{"integer":"12345678901"}
65
+{"integer":"2.3"}
66 66
 ===
67 67
 422
68 68
 Content-Type: application/json
@@ -72,57 +72,57 @@ Content-Length: 101
72 72
 ===
73 73
 PUT /records/types/1
74 74
 
75
-{"bigint":"12345678901234567890"}
75
+{"integer":2.3}
76 76
 ===
77 77
 422
78 78
 Content-Type: application/json
79
-Content-Length: 100
79
+Content-Length: 101
80 80
 
81
-{"code":1013,"message":"Input validation failed for 'types'","details":{"bigint":"invalid integer"}}
81
+{"code":1013,"message":"Input validation failed for 'types'","details":{"integer":"invalid integer"}}
82 82
 ===
83 83
 PUT /records/types/1
84 84
 
85
-{"varchar":"12345678901"}
85
+{"integer":"12345678901"}
86 86
 ===
87 87
 422
88 88
 Content-Type: application/json
89 89
 Content-Length: 101
90 90
 
91
-{"code":1013,"message":"Input validation failed for 'types'","details":{"varchar":"string too long"}}
91
+{"code":1013,"message":"Input validation failed for 'types'","details":{"integer":"invalid integer"}}
92 92
 ===
93 93
 PUT /records/types/1
94 94
 
95
-{"decimal":"12.23.34"}
95
+{"bigint":"12345678901234567890"}
96 96
 ===
97 97
 422
98 98
 Content-Type: application/json
99
-Content-Length: 101
99
+Content-Length: 100
100 100
 
101
-{"code":1013,"message":"Input validation failed for 'types'","details":{"decimal":"invalid decimal"}}
101
+{"code":1013,"message":"Input validation failed for 'types'","details":{"bigint":"invalid integer"}}
102 102
 ===
103 103
 PUT /records/types/1
104 104
 
105
-{"decimal":1131313145345}
105
+{"varchar":"12345678901"}
106 106
 ===
107 107
 422
108 108
 Content-Type: application/json
109
-Content-Length: 103
109
+Content-Length: 101
110 110
 
111
-{"code":1013,"message":"Input validation failed for 'types'","details":{"decimal":"decimal too large"}}
111
+{"code":1013,"message":"Input validation failed for 'types'","details":{"varchar":"string too long"}}
112 112
 ===
113 113
 PUT /records/types/1
114 114
 
115
-{"decimal":"1234567.123"}
115
+{"decimal":"12.23.34"}
116 116
 ===
117 117
 422
118 118
 Content-Type: application/json
119
-Content-Length: 103
119
+Content-Length: 101
120 120
 
121
-{"code":1013,"message":"Input validation failed for 'types'","details":{"decimal":"decimal too large"}}
121
+{"code":1013,"message":"Input validation failed for 'types'","details":{"decimal":"invalid decimal"}}
122 122
 ===
123 123
 PUT /records/types/1
124 124
 
125
-{"decimal":11313131e+5}
125
+{"decimal":"1131313145345"}
126 126
 ===
127 127
 422
128 128
 Content-Type: application/json
@@ -132,17 +132,17 @@ Content-Length: 103
132 132
 ===
133 133
 PUT /records/types/1
134 134
 
135
-{"decimal":"123456.12345"}
135
+{"decimal":"1234567.123"}
136 136
 ===
137 137
 422
138 138
 Content-Type: application/json
139
-Content-Length: 105
139
+Content-Length: 103
140 140
 
141
-{"code":1013,"message":"Input validation failed for 'types'","details":{"decimal":"decimal too precise"}}
141
+{"code":1013,"message":"Input validation failed for 'types'","details":{"decimal":"decimal too large"}}
142 142
 ===
143 143
 PUT /records/types/1
144 144
 
145
-{"decimal":113131.3145345}
145
+{"decimal":"123456.12345"}
146 146
 ===
147 147
 422
148 148
 Content-Type: application/json
@@ -152,7 +152,7 @@ Content-Length: 105
152 152
 ===
153 153
 PUT /records/types/1
154 154
 
155
-{"decimal":11313131E-5}
155
+{"decimal":"113131.3145345"}
156 156
 ===
157 157
 422
158 158
 Content-Type: application/json

+ 383
- 0
tests/functional/003_columns/016_update_forgiving_table.log View File

@@ -0,0 +1,383 @@
1
+===
2
+POST /columns
3
+
4
+{"name":"forgiving","type":"table","columns":[{"name":"id","type":"integer","pk":true},{"name":"integer","type":"integer"},{"name":"bigint","type":"bigint"},{"name":"varchar","type":"varchar","length":10},{"name":"decimal","type":"decimal","precision":10,"scale":4},{"name":"float","type":"float"},{"name":"double","type":"double"},{"name":"boolean","type":"boolean"},{"name":"date","type":"date"},{"name":"time","type":"time"},{"name":"timestamp","type":"timestamp"},{"name":"clob","type":"clob"},{"name":"blob","type":"blob"},{"name":"geometry","type":"geometry"}]}
5
+===
6
+200
7
+Content-Type: application/json
8
+Content-Length: 4
9
+
10
+true
11
+===
12
+POST /records/forgiving
13
+Content-Type: application/json
14
+
15
+{"integer":2,"bigint":3,"varchar":"abc","decimal":1.23,"float":1,"double":23.45,"boolean":true,"date":"1970-01-01","time":"00:00:01","timestamp":"2001-02-03 04:05:06","clob":"a","blob":"YQ==","geometry":"POINT(1 2)"}
16
+===
17
+200
18
+Content-Type: application/json
19
+Content-Length: 1
20
+
21
+1
22
+===
23
+PUT /records/forgiving/1
24
+
25
+{"boolean":"true"}
26
+===
27
+200
28
+Content-Type: application/json
29
+Content-Length: 1
30
+
31
+1
32
+===
33
+GET /records/forgiving/1?include=boolean
34
+===
35
+200
36
+Content-Type: application/json
37
+Content-Length: 16
38
+
39
+{"boolean":true}
40
+===
41
+PUT /records/forgiving/1
42
+
43
+{"boolean":"yes"}
44
+===
45
+200
46
+Content-Type: application/json
47
+Content-Length: 1
48
+
49
+1
50
+===
51
+GET /records/forgiving/1?include=boolean
52
+===
53
+200
54
+Content-Type: application/json
55
+Content-Length: 16
56
+
57
+{"boolean":true}
58
+===
59
+PUT /records/forgiving/1
60
+
61
+{"boolean":"1"}
62
+===
63
+200
64
+Content-Type: application/json
65
+Content-Length: 1
66
+
67
+1
68
+===
69
+GET /records/forgiving/1?include=boolean
70
+===
71
+200
72
+Content-Type: application/json
73
+Content-Length: 16
74
+
75
+{"boolean":true}
76
+===
77
+PUT /records/forgiving/1
78
+
79
+{"boolean":1}
80
+===
81
+200
82
+Content-Type: application/json
83
+Content-Length: 1
84
+
85
+1
86
+===
87
+GET /records/forgiving/1?include=boolean
88
+===
89
+200
90
+Content-Type: application/json
91
+Content-Length: 16
92
+
93
+{"boolean":true}
94
+===
95
+PUT /records/forgiving/1
96
+
97
+{"integer":" 2\n"}
98
+===
99
+200
100
+Content-Type: application/json
101
+Content-Length: 1
102
+
103
+1
104
+===
105
+GET /records/forgiving/1?include=integer
106
+===
107
+200
108
+Content-Type: application/json
109
+Content-Length: 13
110
+
111
+{"integer":2}
112
+===
113
+PUT /records/forgiving/1
114
+
115
+integer=2%20
116
+===
117
+200
118
+Content-Type: application/json
119
+Content-Length: 1
120
+
121
+1
122
+===
123
+GET /records/forgiving/1?include=integer
124
+===
125
+200
126
+Content-Type: application/json
127
+Content-Length: 13
128
+
129
+{"integer":2}
130
+===
131
+PUT /records/forgiving/1
132
+
133
+{"integer":1.99999}
134
+===
135
+200
136
+Content-Type: application/json
137
+Content-Length: 1
138
+
139
+1
140
+===
141
+GET /records/forgiving/1?include=integer
142
+===
143
+200
144
+Content-Type: application/json
145
+Content-Length: 13
146
+
147
+{"integer":2}
148
+===
149
+PUT /records/forgiving/1
150
+
151
+{"bigint":" 3\n"}
152
+===
153
+200
154
+Content-Type: application/json
155
+Content-Length: 1
156
+
157
+1
158
+===
159
+GET /records/forgiving/1?include=bigint
160
+===
161
+200
162
+Content-Type: application/json
163
+Content-Length: 12
164
+
165
+{"bigint":3}
166
+===
167
+PUT /records/forgiving/1
168
+
169
+{"bigint":2.99999}
170
+===
171
+200
172
+Content-Type: application/json
173
+Content-Length: 1
174
+
175
+1
176
+===
177
+GET /records/forgiving/1?include=bigint
178
+===
179
+200
180
+Content-Type: application/json
181
+Content-Length: 12
182
+
183
+{"bigint":3}
184
+===
185
+PUT /records/forgiving/1
186
+
187
+{"decimal":"1.23"}
188
+===
189
+200
190
+Content-Type: application/json
191
+Content-Length: 1
192
+
193
+1
194
+===
195
+GET /records/forgiving/1?include=decimal
196
+===
197
+200
198
+Content-Type: application/json
199
+Content-Length: 20
200
+
201
+{"decimal":"1.2300"}
202
+===
203
+PUT /records/forgiving/1
204
+
205
+{"decimal":"1.23004"}
206
+===
207
+200
208
+Content-Type: application/json
209
+Content-Length: 1
210
+
211
+1
212
+===
213
+GET /records/forgiving/1?include=decimal
214
+===
215
+200
216
+Content-Type: application/json
217
+Content-Length: 20
218
+
219
+{"decimal":"1.2300"}
220
+===
221
+PUT /records/forgiving/1
222
+
223
+{"decimal":"1.23006"}
224
+===
225
+200
226
+Content-Type: application/json
227
+Content-Length: 1
228
+
229
+1
230
+===
231
+GET /records/forgiving/1?include=decimal
232
+===
233
+200
234
+Content-Type: application/json
235
+Content-Length: 20
236
+
237
+{"decimal":"1.2301"}
238
+===
239
+PUT /records/forgiving/1
240
+
241
+float=1%20
242
+===
243
+200
244
+Content-Type: application/json
245
+Content-Length: 1
246
+
247
+1
248
+===
249
+GET /records/forgiving/1?include=float
250
+===
251
+200
252
+Content-Type: application/json
253
+Content-Length: 11
254
+
255
+{"float":1}
256
+===
257
+PUT /records/forgiving/1
258
+
259
+{"double":" 23.45e-1 "}
260
+===
261
+200
262
+Content-Type: application/json
263
+Content-Length: 1
264
+
265
+1
266
+===
267
+GET /records/forgiving/1?include=double
268
+===
269
+200
270
+Content-Type: application/json
271
+Content-Length: 16
272
+
273
+{"double":2.345}
274
+===
275
+PUT /records/forgiving/1
276
+
277
+{"date":"2020-12-05"}
278
+===
279
+200
280
+Content-Type: application/json
281
+Content-Length: 1
282
+
283
+1
284
+===
285
+GET /records/forgiving/1?include=date
286
+===
287
+200
288
+Content-Type: application/json
289
+Content-Length: 21
290
+
291
+{"date":"2020-12-05"}
292
+===
293
+PUT /records/forgiving/1
294
+
295
+{"date":"December 20th, 2020"}
296
+===
297
+200
298
+Content-Type: application/json
299
+Content-Length: 1
300
+
301
+1
302
+===
303
+GET /records/forgiving/1?include=date
304
+===
305
+200
306
+Content-Type: application/json
307
+Content-Length: 21
308
+
309
+{"date":"2020-12-20"}
310
+===
311
+PUT /records/forgiving/1
312
+
313
+{"time":"13:15"}
314
+===
315
+200
316
+Content-Type: application/json
317
+Content-Length: 1
318
+
319
+1
320
+===
321
+GET /records/forgiving/1?include=time
322
+===
323
+200
324
+Content-Type: application/json
325
+Content-Length: 19
326
+
327
+{"time":"13:15:00"}
328
+===
329
+PUT /records/forgiving/1
330
+
331
+{"timestamp":"2012-1-1 23:46"}
332
+===
333
+200
334
+Content-Type: application/json
335
+Content-Length: 1
336
+
337
+1
338
+===
339
+GET /records/forgiving/1?include=timestamp
340
+===
341
+200
342
+Content-Type: application/json
343
+Content-Length: 35
344
+
345
+{"timestamp":"2012-01-01 23:46:00"}
346
+===
347
+PUT /records/forgiving/1
348
+
349
+{"clob":"𠜎𠜱𠝹𠱓𠱸𠲖𠳏𠳕𠴕𠵼𠵿𠸎𠸏𠹷𠺝𠺢𠻗"}
350
+===
351
+200
352
+Content-Type: application/json
353
+Content-Length: 1
354
+
355
+1
356
+===
357
+PUT /records/forgiving/1
358
+
359
+{"blob":"!"}
360
+===
361
+422
362
+Content-Type: application/json
363
+Content-Length: 101
364
+
365
+{"code":1013,"message":"Input validation failed for 'forgiving'","details":{"blob":"invalid base64"}}
366
+===
367
+PUT /records/forgiving/1
368
+
369
+{"blob":"T8O5IGVzdCBsZSBjYWbDqSBsZSBwbHVzIHByb2NoZT8"}
370
+===
371
+200
372
+Content-Type: application/json
373
+Content-Length: 1
374
+
375
+1
376
+===
377
+DELETE /columns/forgiving
378
+===
379
+200
380
+Content-Type: application/json
381
+Content-Length: 4
382
+
383
+true

Loading…
Cancel
Save