| 1 | <?php |
|---|
| 2 | |
|---|
| 3 | require_once(IA_ROOT_DIR.'common/common.php'); |
|---|
| 4 | |
|---|
| 5 | // Get an array element. |
|---|
| 6 | // Path can be a string, int or array of string or ints. |
|---|
| 7 | // |
|---|
| 8 | // The $data array is navigated step by step; if it at any step on the way |
|---|
| 9 | // there is no non-null key then the default value is returned. |
|---|
| 10 | // |
|---|
| 11 | // Null values in $data are treated as non-existant, so |
|---|
| 12 | // array_get(array('ala' => null), 'ala', 'bala') will return 'bala'. |
|---|
| 13 | // This function uses isset rather than array_key_exists. |
|---|
| 14 | function array_get($data, $path, $default = null) |
|---|
| 15 | { |
|---|
| 16 | if (!is_array($path)) { |
|---|
| 17 | if (is_string($path) || is_int($path)) { |
|---|
| 18 | // Optimize for single step. |
|---|
| 19 | if (!is_array($data)) { |
|---|
| 20 | return $default; |
|---|
| 21 | } |
|---|
| 22 | if (isset($data[$path])) { |
|---|
| 23 | return $data[$path]; |
|---|
| 24 | } else { |
|---|
| 25 | return $default; |
|---|
| 26 | } |
|---|
| 27 | } else { |
|---|
| 28 | log_error("Invalid $path argument"); |
|---|
| 29 | } |
|---|
| 30 | } |
|---|
| 31 | while (count($path) > 0) { |
|---|
| 32 | if (!is_set($path[0])) { |
|---|
| 33 | log_error("Path must an array with integer keys"); |
|---|
| 34 | } |
|---|
| 35 | if (!is_string($path[0]) && !is_int($path[0])) { |
|---|
| 36 | log_error("Invalid array path step"); |
|---|
| 37 | } |
|---|
| 38 | if (!is_array($data)) { |
|---|
| 39 | return $default; |
|---|
| 40 | } |
|---|
| 41 | if (isset($data[$path[0]])) { |
|---|
| 42 | $data = $data[array_shift($path)]; |
|---|
| 43 | } else { |
|---|
| 44 | return $default; |
|---|
| 45 | } |
|---|
| 46 | } |
|---|
| 47 | if (isset($data)) { |
|---|
| 48 | return $data; |
|---|
| 49 | } else { |
|---|
| 50 | return $default; |
|---|
| 51 | } |
|---|
| 52 | } |
|---|
| 53 | |
|---|
| 54 | // Get the schema for an array schema. Sadly this is implemented using |
|---|
| 55 | // 'type' => 'any' and a callback. How ironic. |
|---|
| 56 | function array_schema_get_schema() |
|---|
| 57 | { |
|---|
| 58 | return array( |
|---|
| 59 | 'type' => 'any', |
|---|
| 60 | 'callback' => '_array_schema_schema_validate_callback', |
|---|
| 61 | ); |
|---|
| 62 | } |
|---|
| 63 | |
|---|
| 64 | // Callback for array schema schema validation. |
|---|
| 65 | // That is, this function validates an array schema. |
|---|
| 66 | // FIXME: Make a node type for typed_struct? |
|---|
| 67 | function _array_schema_schema_validate_callback($data, $schema) |
|---|
| 68 | { |
|---|
| 69 | static $fields = null; |
|---|
| 70 | if ($fields == null) { |
|---|
| 71 | $number_range = array( |
|---|
| 72 | 'type' => 'struct', |
|---|
| 73 | 'null' => 'true', |
|---|
| 74 | 'fields' => array( |
|---|
| 75 | 'min' => array('type' => 'number', 'null' => true), |
|---|
| 76 | 'max' => array('type' => 'number', 'null' => true), |
|---|
| 77 | 'min-ex' => array('type' => 'number', 'null' => true), |
|---|
| 78 | 'max-ex' => array('type' => 'number', 'null' => true), |
|---|
| 79 | ) |
|---|
| 80 | ); |
|---|
| 81 | $fields = array( |
|---|
| 82 | 'struct' => array('fields' => array_schema_get_schema()), |
|---|
| 83 | 'sequence' => array('values' => array_schema_get_schema()), |
|---|
| 84 | 'mapping' => array('values' => array_schema_get_schema()), |
|---|
| 85 | 'string' => array( |
|---|
| 86 | 'length' => array( |
|---|
| 87 | // String length range min/max values have to be ints. |
|---|
| 88 | 'type' => 'struct', |
|---|
| 89 | 'null' => 'true', |
|---|
| 90 | 'fields' => array( |
|---|
| 91 | 'min' => array('type' => 'int', 'null' => true), |
|---|
| 92 | 'max' => array('type' => 'int', 'null' => true), |
|---|
| 93 | 'min-ex' => array('type' => 'int', 'null' => true), |
|---|
| 94 | 'max-ex' => array('type' => 'int', 'null' => true), |
|---|
| 95 | ) |
|---|
| 96 | ), |
|---|
| 97 | 'pattern' => array( |
|---|
| 98 | 'type' => 'string', |
|---|
| 99 | 'null' => true, |
|---|
| 100 | ), |
|---|
| 101 | 'enum' => array( |
|---|
| 102 | 'type' => 'sequence', |
|---|
| 103 | 'null' => 'true', |
|---|
| 104 | 'values' => array('type' => 'string'), |
|---|
| 105 | ), |
|---|
| 106 | ), |
|---|
| 107 | 'int' => array('range' => $number_range), |
|---|
| 108 | 'float' => array('range' => $number_range), |
|---|
| 109 | 'number' => array('range' => $number_range), |
|---|
| 110 | 'date' => array( |
|---|
| 111 | 'range' => array( |
|---|
| 112 | 'type' => 'struct', |
|---|
| 113 | 'fields' => array( |
|---|
| 114 | 'min' => array('type' => 'date', 'null' => true), |
|---|
| 115 | 'max' => array('type' => 'date', 'null' => true), |
|---|
| 116 | 'min-ex' => array('type' => 'date', 'null' => true), |
|---|
| 117 | 'max-ex' => array('type' => 'date', 'null' => true), |
|---|
| 118 | ), |
|---|
| 119 | ), |
|---|
| 120 | ), |
|---|
| 121 | 'bool' => array(''), |
|---|
| 122 | 'any' => array('') |
|---|
| 123 | ); |
|---|
| 124 | } |
|---|
| 125 | |
|---|
| 126 | if (!is_array($data)) { |
|---|
| 127 | return (array)_local_error("Schema must be an array."); |
|---|
| 128 | } |
|---|
| 129 | $type = array_get($data, 'type', 'string'); |
|---|
| 130 | if (!is_string($type)) { |
|---|
| 131 | return (array)_local_error("Invalid schema node type $type."); |
|---|
| 132 | } |
|---|
| 133 | $type_schema_fields = array_get($fields, $type); |
|---|
| 134 | if (is_null($type_schema_fields)) { |
|---|
| 135 | return (array)_local_error("Unknown schema node type '$type'."); |
|---|
| 136 | } |
|---|
| 137 | return array_validate($data, array( |
|---|
| 138 | 'type' => 'struct', |
|---|
| 139 | 'fields' => $type_schema_fields, |
|---|
| 140 | )); |
|---|
| 141 | } |
|---|
| 142 | |
|---|
| 143 | // Validate a data hash against a schema. |
|---|
| 144 | // The schema is roughly inspire from kwalify, but there are significant |
|---|
| 145 | // differences. Please see the tests for samples. |
|---|
| 146 | // FIXME: This is incomplete |
|---|
| 147 | function array_validate($data, $schema) |
|---|
| 148 | { |
|---|
| 149 | // Default nullable is false, values are required by default. |
|---|
| 150 | // This is unlike SQL. |
|---|
| 151 | $null = array_get($schema, 'null', false); |
|---|
| 152 | if (is_null($data)) { |
|---|
| 153 | if (!$null) { |
|---|
| 154 | return array(_local_error("Required value missing.")); |
|---|
| 155 | } else { |
|---|
| 156 | return array(); |
|---|
| 157 | } |
|---|
| 158 | } |
|---|
| 159 | |
|---|
| 160 | // Create a static hash mapping types to validation functions. |
|---|
| 161 | // Blazing fast. |
|---|
| 162 | static $validation_funcs = array( |
|---|
| 163 | 'struct' => '_array_validate_struct', |
|---|
| 164 | 'sequence' => '_array_validate_sequence', |
|---|
| 165 | 'mapping' => '_array_validate_mapping', |
|---|
| 166 | 'string' => '_array_validate_string', |
|---|
| 167 | 'int' => '_array_validate_number', |
|---|
| 168 | 'float' => '_array_validate_number', |
|---|
| 169 | 'number' => '_array_validate_number', |
|---|
| 170 | 'date' => '_array_validate_date', |
|---|
| 171 | 'any' => '_array_validate_any', |
|---|
| 172 | ); |
|---|
| 173 | |
|---|
| 174 | // Default type is string. |
|---|
| 175 | $func = array_get($validation_funcs, $type = array_get($schema, 'type', 'string')); |
|---|
| 176 | if ($func !== null) { |
|---|
| 177 | $errors = $func($data, $schema); |
|---|
| 178 | } else { |
|---|
| 179 | // Invalid schema, crash. |
|---|
| 180 | log_error("Unknown type '$type'"); |
|---|
| 181 | } |
|---|
| 182 | |
|---|
| 183 | // Call the validation callback, but only if all other tests passed. |
|---|
| 184 | // This makes it a lot easier to write a validation callback function. |
|---|
| 185 | // Errors from the validaton function are merged with the declarative |
|---|
| 186 | // validation errors. |
|---|
| 187 | if ($errors == array()) { |
|---|
| 188 | $validation_function = array_get($schema, "callback", null); |
|---|
| 189 | if (is_callable($validation_function)) { |
|---|
| 190 | $errors = array_merge($errors, $validation_function($data, $schema)); |
|---|
| 191 | } |
|---|
| 192 | } |
|---|
| 193 | |
|---|
| 194 | return $errors; |
|---|
| 195 | } |
|---|
| 196 | |
|---|
| 197 | // Structs are php arrays with different constraints for each field. They |
|---|
| 198 | // are used among other things for database rows. |
|---|
| 199 | function _array_validate_struct($data, $schema) |
|---|
| 200 | { |
|---|
| 201 | if (!is_array($data)) { |
|---|
| 202 | return array(_local_error("Not a struct (is_array false)")); |
|---|
| 203 | } |
|---|
| 204 | |
|---|
| 205 | $errors = array(); |
|---|
| 206 | $struct_fields = $schema['fields']; |
|---|
| 207 | |
|---|
| 208 | // Check defined fields first. |
|---|
| 209 | foreach ($struct_fields as $field_name => $field_schema) { |
|---|
| 210 | // Don't differentiate between null values and missing keys. |
|---|
| 211 | // Such a distinction doesn't exist in some languages and it can be |
|---|
| 212 | // very confusing. |
|---|
| 213 | $field_value = array_get($data, $field_name, null); |
|---|
| 214 | |
|---|
| 215 | // Validate value and copy errors |
|---|
| 216 | // If $field_schema specifies null then it doesn't matter if |
|---|
| 217 | // $field_value doesn't exist, it will pass. |
|---|
| 218 | $field_errors = array_validate($field_value, $field_schema); |
|---|
| 219 | foreach ($field_errors as $field_error) { |
|---|
| 220 | array_unshift($field_error['path'], $field_name); |
|---|
| 221 | $errors[] = $field_error; |
|---|
| 222 | } |
|---|
| 223 | } |
|---|
| 224 | |
|---|
| 225 | // By default allow unknown values, but if the struct is marked as |
|---|
| 226 | // sealed then report an error for extra fields. |
|---|
| 227 | // |
|---|
| 228 | // This will be rather tricky to extend for inherited constraints. |
|---|
| 229 | if (array_get($schema, 'sealed', false)) { |
|---|
| 230 | foreach ($data as $k => $v) { |
|---|
| 231 | // Throw up on undefined keys, if marked as sealed. |
|---|
| 232 | if (!array_get($struct_fields, $k)) { |
|---|
| 233 | $errors[] = array( |
|---|
| 234 | 'path' => array($k), |
|---|
| 235 | 'message' => 'Field undefined', |
|---|
| 236 | ); |
|---|
| 237 | } |
|---|
| 238 | } |
|---|
| 239 | } |
|---|
| 240 | |
|---|
| 241 | return $errors; |
|---|
| 242 | } |
|---|
| 243 | |
|---|
| 244 | // Validate sequence types, returns $errors list. |
|---|
| 245 | // Sequences are simple C-style arrays, indexed with continuous integer values |
|---|
| 246 | // starting from 0. |
|---|
| 247 | function _array_validate_sequence($data, $schema) |
|---|
| 248 | { |
|---|
| 249 | // Check if it's at least an array. |
|---|
| 250 | if (!is_array($data)) { |
|---|
| 251 | return array(_local_error("Not a sequence (is_array false).")); |
|---|
| 252 | } |
|---|
| 253 | |
|---|
| 254 | $value_schema = $schema['values']; |
|---|
| 255 | $errors = array(); |
|---|
| 256 | |
|---|
| 257 | // Check every value. There is no easy way to tell sequences from |
|---|
| 258 | // maps in php, so we check for consecutive integer indexes by hand. |
|---|
| 259 | $index = 0; |
|---|
| 260 | foreach ($data as $k => $v) { |
|---|
| 261 | if ($k != $index) { |
|---|
| 262 | $errors[] = _local_error("Not a sequence, array keys are not all consecutive integers."); |
|---|
| 263 | } |
|---|
| 264 | ++$index; |
|---|
| 265 | $value_errors = array_validate($v, $value_schema); |
|---|
| 266 | foreach ($value_errors as $value_error) { |
|---|
| 267 | array_unshift($value_error['path'], $k); |
|---|
| 268 | $errors[] = $value_error; |
|---|
| 269 | } |
|---|
| 270 | } |
|---|
| 271 | |
|---|
| 272 | return $errors; |
|---|
| 273 | } |
|---|
| 274 | |
|---|
| 275 | // Validate mapping types. |
|---|
| 276 | // These are php arrays with constraints for values. |
|---|
| 277 | // FIXME: It's not possible to place constraints on keys. |
|---|
| 278 | // FIXME: How to properly report errors from key constraints? |
|---|
| 279 | function _array_validate_mapping($data, $schema) |
|---|
| 280 | { |
|---|
| 281 | // Check if it's at least an array. |
|---|
| 282 | if (!is_array($data)) { |
|---|
| 283 | return array(_local_error("Not a mapping (is_array false).")); |
|---|
| 284 | } |
|---|
| 285 | |
|---|
| 286 | $value_schema = $schema['values']; |
|---|
| 287 | |
|---|
| 288 | $errors = array(); |
|---|
| 289 | // Check every key/value pair. |
|---|
| 290 | foreach ($data as $k => $v) { |
|---|
| 291 | $value_errors = array_validate($v, $value_schema); |
|---|
| 292 | foreach ($value_errors as $value_error) { |
|---|
| 293 | array_unshift($value_error['path'], $k); |
|---|
| 294 | $errors[] = $value_error; |
|---|
| 295 | } |
|---|
| 296 | } |
|---|
| 297 | |
|---|
| 298 | return $errors; |
|---|
| 299 | } |
|---|
| 300 | |
|---|
| 301 | // Check string nodes. |
|---|
| 302 | // They can have length contraints similar to numeric ranges. |
|---|
| 303 | // They can be enums and thus restricted to a fixed set of values. |
|---|
| 304 | // They can also be forced to match a certain regex pattern. |
|---|
| 305 | function _array_validate_string($data, $schema) |
|---|
| 306 | { |
|---|
| 307 | if (!is_string($data)) { |
|---|
| 308 | return array(_local_error("Not a string.")); |
|---|
| 309 | } |
|---|
| 310 | |
|---|
| 311 | $errors = array(); |
|---|
| 312 | |
|---|
| 313 | // Check length |
|---|
| 314 | if (array_get($schema, 'length') !== null) { |
|---|
| 315 | $range = $schema['length']; |
|---|
| 316 | $len = strlen($data); |
|---|
| 317 | |
|---|
| 318 | // Max value, inclusive |
|---|
| 319 | if (!is_null($max = array_get($range, 'max')) && $len > $max) { |
|---|
| 320 | $errors[] = _local_error("Length out of range, $len > $max."); |
|---|
| 321 | } |
|---|
| 322 | // Min value, inclusive |
|---|
| 323 | if (!is_null($min = array_get($range, 'min')) && $len < $min) { |
|---|
| 324 | $errors[] = _local_error("Length out of range, $len < $min."); |
|---|
| 325 | } |
|---|
| 326 | // Max value, exclusive |
|---|
| 327 | if (!is_null($maxex = array_get($range, 'max-ex')) && $len >= $maxex) { |
|---|
| 328 | $errors[] = _local_error("Length out of range, $len >= $maxex."); |
|---|
| 329 | } |
|---|
| 330 | // Min value, exclusive |
|---|
| 331 | if (!is_null($minex = array_get($range, 'min-ex')) && $len <= $minex) { |
|---|
| 332 | $errors[] = _local_error("Length out of range, $len <= $minex."); |
|---|
| 333 | } |
|---|
| 334 | } |
|---|
| 335 | |
|---|
| 336 | // Check enum values. |
|---|
| 337 | // FIXME: flipping the array if too slow, better ideas? |
|---|
| 338 | if (array_get($schema, 'enum') !== null) { |
|---|
| 339 | if (!array_key_exists($data, array_flip($schema['enum']))) { |
|---|
| 340 | $errors[] = _local_error("Invalid enum value '$data'"); |
|---|
| 341 | } |
|---|
| 342 | } |
|---|
| 343 | |
|---|
| 344 | // Check string regular expression patterns. |
|---|
| 345 | if (array_get($schema, 'pattern') !== null) { |
|---|
| 346 | if (!preg_match($schema['pattern'], $data)) { |
|---|
| 347 | $errors[] = _local_error("Doesn't match pattern {$schema['pattern']}."); |
|---|
| 348 | } |
|---|
| 349 | } |
|---|
| 350 | |
|---|
| 351 | return $errors; |
|---|
| 352 | } |
|---|
| 353 | |
|---|
| 354 | // Validate numbers (ints or floats). |
|---|
| 355 | // There are 3 types actually: ints, floats or "numbers", which means either. |
|---|
| 356 | // $schema can have a 'range' field. |
|---|
| 357 | function _array_validate_number($data, $schema) |
|---|
| 358 | { |
|---|
| 359 | $type = array_get($schema, 'type', 'string'); |
|---|
| 360 | |
|---|
| 361 | // Check actual type. |
|---|
| 362 | if ($type == 'int') { |
|---|
| 363 | if (!is_int($data)) { |
|---|
| 364 | return array(_local_error("Not an integer.")); |
|---|
| 365 | } |
|---|
| 366 | } elseif ($type == 'float') { |
|---|
| 367 | if (!is_float($data)) { |
|---|
| 368 | return array(_local_error("Not a float.")); |
|---|
| 369 | } |
|---|
| 370 | } elseif ($type == 'number') { |
|---|
| 371 | if (!is_float($data) && !is_int($data)) { |
|---|
| 372 | return array(_local_error("Not a number, neither int not float.")); |
|---|
| 373 | } |
|---|
| 374 | } else { |
|---|
| 375 | log_error("Invalid type '$type' for _array_validate_number."); |
|---|
| 376 | } |
|---|
| 377 | |
|---|
| 378 | $errors = array(); |
|---|
| 379 | |
|---|
| 380 | // Check Ranges |
|---|
| 381 | if (!is_null($range = array_get($schema, 'range'))) { |
|---|
| 382 | // Max value, inclusive |
|---|
| 383 | if (!is_null($max = array_get($range, 'max')) && $data > $max) { |
|---|
| 384 | $errors[] = _local_error("Value out of range, $data > $max."); |
|---|
| 385 | } |
|---|
| 386 | // Min value, inclusive |
|---|
| 387 | if (!is_null($min = array_get($range, 'min')) && $data < $min) { |
|---|
| 388 | $errors[] = _local_error("Value out of range, $data < $min."); |
|---|
| 389 | } |
|---|
| 390 | // Max value, exclusive |
|---|
| 391 | if (!is_null($maxex = array_get($range, 'max-ex')) && $data >= $maxex) { |
|---|
| 392 | $errors[] = _local_error("Value out of range, $data >= $maxex."); |
|---|
| 393 | } |
|---|
| 394 | // Min value, exclusive |
|---|
| 395 | if (!is_null($minex = array_get($range, 'min-ex')) && $data <= $minex) { |
|---|
| 396 | $errors[] = _local_error("Value out of range, $data <= $minex."); |
|---|
| 397 | } |
|---|
| 398 | } |
|---|
| 399 | |
|---|
| 400 | return $errors; |
|---|
| 401 | } |
|---|
| 402 | |
|---|
| 403 | // Validate dates. Dates are represented as strings in mysql's format, |
|---|
| 404 | // which is very similar with RFC 3339, without the T. In short, it's |
|---|
| 405 | // YYYY-MM-DD HH:MM:SS. This is simple, readable and can even be ordered |
|---|
| 406 | // using strcmp. |
|---|
| 407 | // |
|---|
| 408 | // FIXME: Support ranges. This is important. |
|---|
| 409 | function _array_validate_date($data, $schema) |
|---|
| 410 | { |
|---|
| 411 | if (!is_db_date($data)) { |
|---|
| 412 | return array(_local_error("Not a datetime value. Valid values are YYYY-MM-DD HH:MM:SS with the time part optional")); |
|---|
| 413 | } |
|---|
| 414 | |
|---|
| 415 | // FIXME: Check date/time ranges. |
|---|
| 416 | if (array_get($schema, 'range') != null) { |
|---|
| 417 | log_error("Ranges not supported for dates yet"); |
|---|
| 418 | } |
|---|
| 419 | |
|---|
| 420 | return array(); |
|---|
| 421 | } |
|---|
| 422 | |
|---|
| 423 | // Validate booleans. There are no extra options for these. |
|---|
| 424 | function _array_validate_bool($data, $schema) |
|---|
| 425 | { |
|---|
| 426 | if (!is_bool($data)) { |
|---|
| 427 | return array(_local_error("Not a boolean.")); |
|---|
| 428 | } |
|---|
| 429 | |
|---|
| 430 | return array(); |
|---|
| 431 | } |
|---|
| 432 | |
|---|
| 433 | // Mock validation function for 'any' type. |
|---|
| 434 | // This can be used to only rely only the validation callback. |
|---|
| 435 | function _array_validate_any($data, $schema) |
|---|
| 436 | { |
|---|
| 437 | return array(); |
|---|
| 438 | } |
|---|
| 439 | |
|---|
| 440 | // Make a tiny local validation error. |
|---|
| 441 | function _local_error($message) |
|---|
| 442 | { |
|---|
| 443 | return array('path' => array(), 'message' => $message); |
|---|
| 444 | } |
|---|
| 445 | |
|---|
| 446 | ?> |
|---|