1
|
|
|
<?php namespace CMPayments\SchemaValidator; |
2
|
|
|
|
3
|
|
|
use CMPayments\Cache\Cache; |
4
|
|
|
use CMPayments\Json\Json; |
5
|
|
|
use CMPayments\SchemaValidator\Exceptions\ValidateException; |
6
|
|
|
use CMPayments\SchemaValidator\Exceptions\ValidateSchemaException; |
7
|
|
|
|
8
|
|
|
/** |
9
|
|
|
* Class SchemaValidator |
10
|
|
|
* |
11
|
|
|
* @package CMPayments\Validator |
12
|
|
|
* @Author Boy Wijnmaalen <[email protected]> |
13
|
|
|
* @Author Rob Theeuwes <[email protected]> |
14
|
|
|
*/ |
15
|
|
|
class SchemaValidator extends BaseValidator implements ValidatorInterface |
16
|
|
|
{ |
17
|
|
|
/** |
18
|
|
|
* @var array |
19
|
|
|
*/ |
20
|
|
|
private $cacheReferencedSchemas = []; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @var null|object |
24
|
|
|
*/ |
25
|
|
|
private $rootSchema = null; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @var Cache|null |
29
|
|
|
*/ |
30
|
|
|
protected $cache; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* @var array |
34
|
|
|
*/ |
35
|
|
|
private $minMaxProperties = [ |
36
|
|
|
'minimum' => ['minimum', 'maximum'], |
37
|
|
|
'minItems' => ['minItems', 'maxItems'], |
38
|
|
|
'minLength' => ['minLength', 'maxLength'] |
39
|
|
|
]; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* SchemaValidator constructor. |
43
|
|
|
* |
44
|
|
|
* @param $data |
45
|
|
|
* @param $schema |
46
|
|
|
* @param Cache|null $cache |
47
|
|
|
* |
48
|
|
|
* @throws ValidateSchemaException |
49
|
|
|
*/ |
50
|
|
|
public function __construct($data, $schema, Cache $cache = null) |
51
|
|
|
{ |
52
|
|
|
// if $cache is empty, create a new instance of Cache |
53
|
|
|
if (is_null($cache)) { |
54
|
|
|
|
55
|
|
|
$cache = new Cache(); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
$this->cache = $cache; |
59
|
|
|
|
60
|
|
|
// check if $schema is an object to begin with |
61
|
|
|
if (!is_object($schema) || (is_callable($schema) && ($schema instanceof \Closure))) { |
62
|
|
|
|
63
|
|
|
throw new ValidateSchemaException(ValidateSchemaException::ERROR_INPUT_IS_NOT_A_OBJECT, ['Schema', $this->getPreposition(gettype($schema)), gettype($schema), '']); |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
// PHP 5.4 |
67
|
|
|
$filename = $cache->getFilename(); |
68
|
|
|
if (empty($filename)) { |
69
|
|
|
|
70
|
|
|
$cache->setFilename(md5(json_encode($schema)) . '.php'); |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
// if cache file exists, require it, if not, validate the schema |
74
|
|
|
if (file_exists(($filename = $cache->getAbsoluteFilePath()))) { |
75
|
|
|
|
76
|
|
|
$this->rootSchema = require $filename; |
77
|
|
|
} else { |
78
|
|
|
|
79
|
|
|
// decode the valid JSON |
80
|
|
|
$this->rootSchema = $schema; |
81
|
|
|
|
82
|
|
|
// validate Schema |
83
|
|
|
$this->rootSchema = $this->validateSchema($this->rootSchema); |
84
|
|
|
|
85
|
|
|
$cache->putContent($this->rootSchema, $filename); |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
// decode the valid JSON |
89
|
|
|
$this->validateData($this->rootSchema, $data); |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Validate the Data |
94
|
|
|
* |
95
|
|
|
* @param \stdClass $schema |
96
|
|
|
* @param $data |
97
|
|
|
* @param null|string $path |
98
|
|
|
* |
99
|
|
|
* @return boolean|null |
100
|
|
|
*/ |
101
|
|
|
public function validateData($schema, $data, $path = null) |
102
|
|
|
{ |
103
|
|
|
// check if the required property is set |
104
|
|
|
if (isset($schema->required)) { |
105
|
|
|
|
106
|
|
|
if (count($missing = array_diff_key(array_flip($schema->required), (array)$data)) > 0) { |
107
|
|
|
|
108
|
|
|
$count = count($missing); |
109
|
|
|
|
110
|
|
|
// add error |
111
|
|
|
$this->addError( |
112
|
|
|
ValidateException::ERROR_USER_CHECK_IF_ALL_REQUIRED_PROPERTIES_ARE_SET, |
113
|
|
|
[$this->conjugationObject($count, 'property', 'properties'), implode('\', \'', array_flip($missing)), $this->conjugationToBe($count), (($path) ?: '/')] |
114
|
|
|
); |
115
|
|
|
} |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
// BaseValidator::_ARRAY |
119
|
|
|
if (is_array($data) && ($schema->type === BaseValidator::_ARRAY)) { |
120
|
|
|
|
121
|
|
|
// check if the expected $schema->type matches gettype($data) |
122
|
|
|
$this->validateType($schema, $data, $path); |
123
|
|
|
|
124
|
|
|
// when variable path is null it means that the base element is an array validate it anyway (even when there are no items present in the array) |
125
|
|
|
if (is_null($path)) { |
126
|
|
|
|
127
|
|
|
$this->validate($schema, null, $data, null); |
128
|
|
|
} else { |
129
|
|
|
|
130
|
|
|
foreach ($data as $property => $value) { |
131
|
|
|
|
132
|
|
|
$this->validate($schema->items, $property, $value, $path); |
133
|
|
|
} |
134
|
|
|
} |
135
|
|
|
// BaseValidator::OBJECT |
136
|
|
|
} elseif (is_object($data)) { |
137
|
|
|
|
138
|
|
|
// check if the expected $schema->type matches gettype($data) |
139
|
|
|
$this->validateType($schema, $data, (($path) ?: '/')); |
140
|
|
|
|
141
|
|
|
foreach ($data as $property => $value) { |
142
|
|
|
|
143
|
|
|
if (isset($schema->properties->$property)) { |
144
|
|
|
|
145
|
|
|
$this->validate($schema->properties->$property, $property, $value, $path); |
146
|
|
|
// $schema->properties->$property is not set but check if it allowed based on $schema->additionalProperties |
147
|
|
|
} elseif ( |
148
|
|
|
(isset($schema->additionalProperties) && !$schema->additionalProperties) |
149
|
|
|
|| (!isset($schema->additionalProperties) && (isset($this->rootSchema->additionalProperties) && !$this->rootSchema->additionalProperties)) |
150
|
|
|
) { |
151
|
|
|
// $schema->additionalProperties says NO, log that a fields is missing |
152
|
|
|
$this->addError(ValidateException::ERROR_USER_DATA_PROPERTY_IS_NOT_AN_ALLOWED_PROPERTY, [$path . '/' . $property]); |
153
|
|
|
} |
154
|
|
|
} |
155
|
|
|
// Everything else |
156
|
|
|
} else { |
157
|
|
|
|
158
|
|
|
$this->validate($schema, null, $data, $path); |
159
|
|
|
} |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* Validate a single Data value |
164
|
|
|
* |
165
|
|
|
* @param $schema |
166
|
|
|
* @param $data |
167
|
|
|
* @param $property |
168
|
|
|
* @param null|string $path |
169
|
|
|
* |
170
|
|
|
* @return bool |
171
|
|
|
*/ |
172
|
|
|
public function validate($schema, $property, $data, $path) |
173
|
|
|
{ |
174
|
|
|
// check if the expected $schema->type matches gettype($data) |
175
|
|
|
$type = $this->validateType($schema, $data, ((substr($path, -1) !== '/') ? $path . '/' . $property : $path . $property)); |
176
|
|
|
|
177
|
|
|
// append /$property to $path |
178
|
|
|
$path .= (substr($path, 0, 1) !== '/') ? '/' . $property : $property; |
179
|
|
|
|
180
|
|
|
// if $type is an object |
181
|
|
|
if ($type === BaseValidator::OBJECT) { |
182
|
|
|
|
183
|
|
|
$this->validateData($schema, $data, $path); |
184
|
|
|
} elseif ( |
185
|
|
|
($type !== BaseValidator::BOOLEAN) |
186
|
|
|
&& ($schema->type === $type) |
187
|
|
|
) { // everything else except boolean |
188
|
|
|
|
189
|
|
|
$method = 'validate' . ucfirst($type); |
190
|
|
|
$this->$method($data, $schema, $path); |
191
|
|
|
|
192
|
|
|
// check for format property on schema |
193
|
|
|
$this->validateFormat($data, $schema, $path); |
194
|
|
|
|
195
|
|
|
// check for enum property on schema |
196
|
|
|
$this->validateEnum($data, $schema, $path); |
197
|
|
|
|
198
|
|
|
// check for pattern (regex) property on schema |
199
|
|
|
$this->validateRegex($data, $schema, $path); |
200
|
|
|
|
201
|
|
|
// @TODO; check for $schema->oneOf { format: "" }, { format: "" } |
|
|
|
|
202
|
|
|
//$this->validateOneOf($data, $schema, $path); |
|
|
|
|
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
return false; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
/** |
209
|
|
|
* Validates a schema |
210
|
|
|
* |
211
|
|
|
* @param $schema |
212
|
|
|
* @param null $path |
213
|
|
|
* |
214
|
|
|
* @return mixed |
215
|
|
|
* @throws ValidateSchemaException |
216
|
|
|
*/ |
217
|
|
|
public function validateSchema($schema, $path = null) |
218
|
|
|
{ |
219
|
|
|
$path = ($path) ?: 'root'; |
220
|
|
|
|
221
|
|
|
// check if there is a reference to another schema, update the current $parameters either with a online reference or a local reference |
222
|
|
|
if (isset($schema->{'$ref'}) && !empty($schema->{'$ref'})) { |
223
|
|
|
|
224
|
|
|
$schema = $this->getReference($schema); |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
// PHP 5.4 |
228
|
|
|
$schemaInArray = (array)$schema; |
229
|
|
|
|
230
|
|
|
// check if the given schema is not empty |
231
|
|
|
if (empty($schemaInArray)) { |
232
|
|
|
|
233
|
|
|
throw new ValidateSchemaException(ValidateSchemaException::ERROR_SCHEMA_CANNOT_BE_EMPTY_IN_PATH, [$path]); |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
// validate mandatory $schema properties |
237
|
|
|
$this->validateSchemaMandatoryProperties($schema, $path); |
238
|
|
|
|
239
|
|
|
// validate optional $schema properties |
240
|
|
|
$this->validateSchemaOptionalProperties($schema, $path); |
241
|
|
|
|
242
|
|
|
// validate $schema->properties |
243
|
|
|
if (isset($schema->properties)) { |
244
|
|
|
|
245
|
|
|
// PHP 5.4 |
246
|
|
|
$schemaPropertiesInArray = (array)$schema->properties; |
247
|
|
|
|
248
|
|
|
// check if the given schema is not empty |
249
|
|
|
if (empty($schemaPropertiesInArray)) { |
250
|
|
|
|
251
|
|
|
throw new ValidateSchemaException(ValidateSchemaException::ERROR_SCHEMA_CANNOT_BE_EMPTY_IN_PATH, [$path . '/properties']); |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
foreach ($schema->properties as $property => $childSchema) { |
255
|
|
|
|
256
|
|
|
$subPath = $path . '.properties'; |
257
|
|
|
|
258
|
|
|
// when an object key is empty it becomes '_empty_' by json_decode(), catch it since this is not valid |
259
|
|
|
if ($property === '_empty_') { |
260
|
|
|
|
261
|
|
|
throw new ValidateSchemaException(ValidateSchemaException::ERROR_EMPTY_KEY_NOT_ALLOWED_IN_OBJECT, [$subPath]); |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
// check if $childSchema is an object to begin with |
265
|
|
|
if (!is_object($childSchema)) { |
266
|
|
|
|
267
|
|
|
throw new ValidateSchemaException( |
268
|
|
|
ValidateSchemaException::ERROR_INPUT_IS_NOT_A_OBJECT, |
269
|
|
|
[ |
270
|
|
|
'Schema', |
271
|
|
|
$this->getPreposition(gettype($childSchema)), |
272
|
|
|
gettype($childSchema), |
273
|
|
|
(' in ' . $subPath) |
274
|
|
|
]); |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
// do recursion |
278
|
|
|
$schema->properties->$property = $this->validateSchema($childSchema, ($subPath . '.' . $property)); |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
// check if the optional property 'required' is set on $schema |
282
|
|
|
if (isset($schema->required)) { |
283
|
|
|
|
284
|
|
|
$this->validateSchemaRequiredAgainstProperties($schema, $path); |
285
|
|
|
} |
286
|
|
|
// when dealing with an array we want to do recursion |
287
|
|
|
} elseif ($schema->type === BaseValidator::_ARRAY) { |
288
|
|
|
|
289
|
|
|
// do recursion |
290
|
|
|
$schema->items = $this->validateSchema($schema->items, ($path . '.items')); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
return $schema; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* Validate mandatory $schema->$property properties |
298
|
|
|
* |
299
|
|
|
* @param $schema |
300
|
|
|
* @param string $path |
301
|
|
|
* |
302
|
|
|
* @return mixed |
303
|
|
|
* @throws ValidateException |
304
|
|
|
*/ |
305
|
|
|
private function validateSchemaMandatoryProperties($schema, $path) |
306
|
|
|
{ |
307
|
|
|
$input = [sprintf('type|is_string|%s', BaseValidator::STRING)]; |
308
|
|
|
|
309
|
|
|
// it is now possible that property type can both a string or an array |
310
|
|
View Code Duplication |
if (isset($schema->type) && is_array($schema->type)) { |
|
|
|
|
311
|
|
|
|
312
|
|
|
$input = [sprintf('type|is_array|%s', BaseValidator::_ARRAY)]; |
313
|
|
|
} |
314
|
|
|
|
315
|
|
View Code Duplication |
if (isset($schema->type) && $schema->type === BaseValidator::_ARRAY) { |
|
|
|
|
316
|
|
|
|
317
|
|
|
// field|must_be|type_in_error_msg |
318
|
|
|
$input = array_merge($input, [ |
319
|
|
|
sprintf('items|is_object|%s', BaseValidator::OBJECT) |
320
|
|
|
]); |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
return $this->validateSchemaProperties($input, $schema, $path, true); |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
/** |
327
|
|
|
* Validate optional $schema->$property properties |
328
|
|
|
* |
329
|
|
|
* @param $schema |
330
|
|
|
* @param string $path |
331
|
|
|
* |
332
|
|
|
* @return mixed |
333
|
|
|
* @throws ValidateException |
334
|
|
|
*/ |
335
|
|
|
private function validateSchemaOptionalProperties($schema, $path) |
336
|
|
|
{ |
337
|
|
|
$input = [ |
338
|
|
|
sprintf('format|is_string|%s', BaseValidator::STRING), |
339
|
|
|
sprintf('enum|is_array|%s', BaseValidator::_ARRAY), |
340
|
|
|
sprintf('caseSensitive|is_bool|%s', BaseValidator::BOOLEAN) |
341
|
|
|
]; |
342
|
|
|
|
343
|
|
View Code Duplication |
if (isset($schema->type) && ($schema->type === BaseValidator::_ARRAY)) { |
|
|
|
|
344
|
|
|
|
345
|
|
|
// field|must_be|type_in_error_msg |
346
|
|
|
$input = array_merge($input, [ |
347
|
|
|
sprintf('minItems|is_int|%s', BaseValidator::INTEGER), |
348
|
|
|
sprintf('maxItems|is_int|%s', BaseValidator::INTEGER), |
349
|
|
|
sprintf('uniqueItems|is_bool|%s', BaseValidator::BOOLEAN) |
350
|
|
|
]); |
351
|
|
|
} |
352
|
|
|
|
353
|
|
View Code Duplication |
if (isset($schema->type) && ($schema->type === BaseValidator::STRING)) { |
|
|
|
|
354
|
|
|
|
355
|
|
|
$input = array_merge($input, [ |
356
|
|
|
sprintf('minLength|is_int|%s', BaseValidator::INTEGER), |
357
|
|
|
sprintf('maxLength|is_int|%s', BaseValidator::INTEGER), |
358
|
|
|
sprintf('format|is_string|%s', BaseValidator::STRING) |
359
|
|
|
]); |
360
|
|
|
} |
361
|
|
|
|
362
|
|
View Code Duplication |
if (isset($schema->type) && ($schema->type === BaseValidator::INTEGER)) { |
|
|
|
|
363
|
|
|
|
364
|
|
|
$input = array_merge($input, [ |
365
|
|
|
sprintf('minimum|is_int|%s', BaseValidator::INTEGER), |
366
|
|
|
sprintf('maximum|is_int|%s', BaseValidator::INTEGER) |
367
|
|
|
]); |
368
|
|
|
} |
369
|
|
|
|
370
|
|
View Code Duplication |
if (isset($schema->type) && ($schema->type === BaseValidator::NUMBER)) { |
|
|
|
|
371
|
|
|
|
372
|
|
|
$input = array_merge($input, [ |
373
|
|
|
sprintf('minimum|is_numeric|%s', BaseValidator::NUMBER), |
374
|
|
|
sprintf('maximum|is_numeric|%s', BaseValidator::NUMBER) |
375
|
|
|
]); |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
return $this->validateSchemaProperties($input, $schema, $path); |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
/** |
382
|
|
|
* Validate $schema->$property |
383
|
|
|
* |
384
|
|
|
* @param array $input |
385
|
|
|
* @param $schema |
386
|
|
|
* @param $path |
387
|
|
|
* @param bool|false $mandatory |
388
|
|
|
* |
389
|
|
|
* @return mixed |
390
|
|
|
* @throws ValidateSchemaException |
391
|
|
|
*/ |
392
|
|
|
private function validateSchemaProperties($input, $schema, $path, $mandatory = false) |
393
|
|
|
{ |
394
|
|
|
foreach ($input as $properties) { |
395
|
|
|
|
396
|
|
|
list($property, $method, $expectedType) = explode('|', $properties); |
397
|
|
|
|
398
|
|
|
// when $mandatory is true, check if a certain $property isset |
399
|
|
|
if ($mandatory && !isset($schema->$property)) { |
400
|
|
|
|
401
|
|
|
throw new ValidateSchemaException(ValidateSchemaException::ERROR_SCHEMA_PROPERTY_NOT_DEFINED, [$property, $path]); |
402
|
|
|
} |
403
|
|
|
|
404
|
|
|
// check if $property is of a certain type |
405
|
|
|
if (isset($schema->$property)) { |
406
|
|
|
|
407
|
|
|
if (!$method($schema->$property)) { |
408
|
|
|
|
409
|
|
|
$actualType = gettype($schema->$property); |
410
|
|
|
|
411
|
|
|
throw new ValidateSchemaException( |
412
|
|
|
ValidateSchemaException::ERROR_SCHEMA_PROPERTY_TYPE_NOT_VALID, |
413
|
|
|
[$path, $property, $this->getPreposition($expectedType), $expectedType, $this->getPreposition($actualType), $actualType] |
414
|
|
|
); |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
// check if a $property' value match a list of predefined values |
418
|
|
|
$this->validateSchemaPropertyValue($schema, $property, $path); |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
if (in_array($property, array_keys($this->minMaxProperties))) { |
422
|
|
|
|
423
|
|
|
$this->validateSchemaMinMaxProperties($schema, $this->minMaxProperties[$property][0], $this->minMaxProperties[$property][1], $path); |
424
|
|
|
} |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
return $schema; |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
/** |
431
|
|
|
* Validate Schema property against a predefined list of values |
432
|
|
|
* |
433
|
|
|
* @param $schema |
434
|
|
|
* @param $property |
435
|
|
|
* @param $path |
436
|
|
|
* |
437
|
|
|
* @throws ValidateSchemaException |
438
|
|
|
*/ |
439
|
|
|
private function validateSchemaPropertyValue($schema, $property, $path) |
440
|
|
|
{ |
441
|
|
|
// return if not applicable |
442
|
|
|
if (!in_array($property, [BaseValidator::TYPE, BaseValidator::FORMAT])) { |
443
|
|
|
|
444
|
|
|
return; |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
// set the correct $expected |
448
|
|
|
switch (1) { |
449
|
|
|
case ($property === BaseValidator::TYPE): |
450
|
|
|
$expected = $this->getValidTypes(); |
451
|
|
|
break; |
452
|
|
|
case ($property === BaseValidator::FORMAT): |
453
|
|
|
$expected = $this->getValidFormats(); |
454
|
|
|
break; |
455
|
|
|
default: |
456
|
|
|
$expected = []; |
457
|
|
|
break; |
458
|
|
|
} |
459
|
|
|
|
460
|
|
|
// we are dealing with type property that is an array |
461
|
|
|
if (($property === BaseValidator::TYPE) && is_array($schema->$property)) { |
462
|
|
|
|
463
|
|
|
// check if all the given values are strings |
464
|
|
|
// http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.21 |
465
|
|
|
if (count($schema->$property) != count(array_filter($schema->$property, 'is_string')) |
466
|
|
|
|| (count($schema->$property) != count(array_filter($schema->$property))) // we do not allow empty strings as well |
467
|
|
|
) { |
468
|
|
|
|
469
|
|
|
throw new ValidateSchemaException(ValidateSchemaException::ERROR_SCHEMA_PROPERTY_TYPE_IS_ARRAY_BUT_VALUES_AR_NOT_ALL_STRINGS, [$path, BaseValidator::TYPE, BaseValidator::STRING]); |
470
|
|
|
} |
471
|
|
|
|
472
|
|
|
// check if all the given values are unique |
473
|
|
|
// http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.21 |
474
|
|
|
if (count($schema->type) != count(array_unique($schema->type))) { |
475
|
|
|
|
476
|
|
|
throw new ValidateSchemaException(ValidateSchemaException::ERROR_SCHEMA_PROPERTY_TYPE_IS_ARRAY_BUT_VALUES_ARE_NOT_UNIQUE, [$path, BaseValidator::TYPE]); |
477
|
|
|
} |
478
|
|
|
} |
479
|
|
|
|
480
|
|
|
$notAllowed = []; |
481
|
|
|
foreach ((array)$schema->$property as $value) { |
482
|
|
|
|
483
|
|
|
if (!in_array($value, $expected)) { |
484
|
|
|
|
485
|
|
|
$notAllowed[] = $value; |
486
|
|
|
} |
487
|
|
|
} |
488
|
|
|
|
489
|
|
|
// check if $notAllowed is not empty |
490
|
|
|
if (!empty($notAllowed)) { |
491
|
|
|
|
492
|
|
|
$count = count($expected); |
493
|
|
|
|
494
|
|
|
throw new ValidateSchemaException( |
495
|
|
|
ValidateSchemaException::ERROR_SCHEMA_PROPERTY_VALUE_IS_NOT_VALID, |
496
|
|
|
[implode('\', \'', $notAllowed), $path, $property, $this->conjugationObject($count, '', 'any of '), $this->conjugationObject($count), implode('\', \'', $expected)] |
497
|
|
|
); |
498
|
|
|
} |
499
|
|
|
} |
500
|
|
|
|
501
|
|
|
/** |
502
|
|
|
* Validate Schema a min and max properties (if set) |
503
|
|
|
* |
504
|
|
|
* @param $schema |
505
|
|
|
* @param $minProperty |
506
|
|
|
* @param $maxProperty |
507
|
|
|
* @param $path |
508
|
|
|
* |
509
|
|
|
* @throws ValidateSchemaException |
510
|
|
|
*/ |
511
|
|
|
private function validateSchemaMinMaxProperties($schema, $minProperty, $maxProperty, $path) |
512
|
|
|
{ |
513
|
|
|
// both $schema->$maxProperty cannot be zero |
514
|
|
|
if (isset($schema->$maxProperty) && ($schema->$maxProperty === 0)) { |
515
|
|
|
|
516
|
|
|
throw new ValidateSchemaException(ValidateSchemaException::ERROR_SCHEMA_MAX_PROPERTY_CANNOT_NOT_BE_ZERO, [$path, $maxProperty]); |
517
|
|
|
} |
518
|
|
|
|
519
|
|
|
if (isset($schema->$minProperty) && isset($schema->$maxProperty) && ($schema->$minProperty > $schema->$maxProperty)) { |
520
|
|
|
|
521
|
|
|
throw new ValidateSchemaException( |
522
|
|
|
ValidateSchemaException::ERROR_SCHEMA_PROPERTY_MIN_NOT_BIGGER_THAN_MAX, |
523
|
|
|
[$path, $minProperty, $schema->$minProperty, $path, $maxProperty, $schema->$maxProperty] |
524
|
|
|
); |
525
|
|
|
} |
526
|
|
|
} |
527
|
|
|
|
528
|
|
|
/** |
529
|
|
|
* Walk through all $schema->required items and check if there is a $schema->properties item defined for it |
530
|
|
|
* |
531
|
|
|
* @param $schema |
532
|
|
|
* @param string $path |
533
|
|
|
* |
534
|
|
|
* @throws ValidateSchemaException |
535
|
|
|
*/ |
536
|
|
|
private function validateSchemaRequiredAgainstProperties($schema, $path) |
537
|
|
|
{ |
538
|
|
|
$requiredPath = $path . '.required'; |
539
|
|
|
|
540
|
|
|
// $schema->required must be an array |
541
|
|
|
if (!is_array($schema->required)) { |
542
|
|
|
|
543
|
|
|
$type = gettype($schema->required); |
544
|
|
|
$preposition = $this->getPreposition($type); |
545
|
|
|
|
546
|
|
|
throw new ValidateSchemaException(ValidateSchemaException::ERROR_SCHEMA_PROPERTY_REQUIRED_MUST_BE_AN_ARRAY, [$requiredPath, $preposition, $type]); |
547
|
|
|
} |
548
|
|
|
|
549
|
|
|
// check if the $schema->required property contains fields that have not been defined in $schema->properties |
550
|
|
|
if (!empty($schema->required) && count($missing = array_diff_key(array_flip($schema->required), (array)$schema->properties)) > 0) { |
551
|
|
|
|
552
|
|
|
$count = count($missing); |
553
|
|
|
$verb = $this->conjugationToBe($count); |
554
|
|
|
|
555
|
|
|
throw new ValidateSchemaException( |
556
|
|
|
ValidateSchemaException::ERROR_SCHEMA_REQUIRED_AND_PROPERTIES_MUST_MATCH, |
557
|
|
|
[$this->conjugationObject($count), implode('\', \'', array_flip($missing)), $verb, $requiredPath, $verb, $path] |
558
|
|
|
); |
559
|
|
|
} |
560
|
|
|
} |
561
|
|
|
|
562
|
|
|
/** |
563
|
|
|
* Retrieve and validate a reference |
564
|
|
|
* |
565
|
|
|
* @param $schema |
566
|
|
|
* |
567
|
|
|
* @return mixed |
568
|
|
|
* @throws ValidateSchemaException |
569
|
|
|
*/ |
570
|
|
|
private function getReference($schema) |
571
|
|
|
{ |
572
|
|
|
// return any previously requested definitions |
573
|
|
|
if (isset($this->cacheReferencedSchemas[$schema->{'$ref'}])) { |
574
|
|
|
|
575
|
|
|
return $this->cacheReferencedSchemas[$schema->{'$ref'}]; |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
$referencedSchema = null; |
579
|
|
|
|
580
|
|
|
// fetch local reference |
581
|
|
|
if (strpos($schema->{'$ref'}, '#/definitions/') !== false) { |
582
|
|
|
|
583
|
|
|
$referencedSchema = $this->getLocalReference($schema); |
584
|
|
|
// fetch remote reference |
585
|
|
|
} elseif (strpos($schema->{'$ref'}, 'http') !== false) { |
586
|
|
|
|
587
|
|
|
$referencedSchema = $this->getRemoteReference($schema); |
588
|
|
|
} |
589
|
|
|
|
590
|
|
|
// not a local reference nor a remote reference |
591
|
|
|
if (is_null($referencedSchema)) { |
592
|
|
|
|
593
|
|
|
throw new ValidateSchemaException(ValidateSchemaException::ERROR_INVALID_REFERENCE, $schema->{'$ref'}); |
594
|
|
|
} |
595
|
|
|
|
596
|
|
|
// cache the result |
597
|
|
|
$this->cacheReferencedSchemas[$schema->{'$ref'}] = $referencedSchema; |
598
|
|
|
|
599
|
|
|
// unset the reference for the $schema for cleanup purposes |
600
|
|
|
unset($schema->{'$ref'}); |
601
|
|
|
|
602
|
|
|
// augment the current $schema with the fetched referenced schema properties (and override if necessary) |
603
|
|
|
foreach (get_object_vars($referencedSchema) as $property => $value) { |
604
|
|
|
|
605
|
|
|
$schema->$property = $value; |
606
|
|
|
} |
607
|
|
|
|
608
|
|
|
return $schema; |
609
|
|
|
} |
610
|
|
|
|
611
|
|
|
/** |
612
|
|
|
* Matches and returns a local reference |
613
|
|
|
* |
614
|
|
|
* @param $schema |
615
|
|
|
* |
616
|
|
|
* @return mixed |
617
|
|
|
* @throws ValidateSchemaException |
618
|
|
|
*/ |
619
|
|
|
private function getLocalReference($schema) |
620
|
|
|
{ |
621
|
|
|
// check if there is at least one local reference defined to match it to |
622
|
|
|
if (!isset($this->rootSchema->definitions) || (count($definitions = get_object_vars($this->rootSchema->definitions)) === 0)) { |
623
|
|
|
|
624
|
|
|
throw new ValidateSchemaException(ValidateSchemaException::ERROR_NO_LOCAL_DEFINITIONS_HAVE_BEEN_DEFINED); |
625
|
|
|
} |
626
|
|
|
|
627
|
|
|
// check if the referenced schema is locally defined |
628
|
|
|
$definitionKeys = array_keys($definitions); |
629
|
|
|
$reference = substr($schema->{'$ref'}, strlen('#/definitions/')); |
630
|
|
|
|
631
|
|
|
if (!in_array($reference, $definitionKeys)) { |
632
|
|
|
|
633
|
|
|
throw new ValidateSchemaException(ValidateSchemaException::ERROR_CHECK_IF_LOCAL_DEFINITIONS_EXISTS, [$schema->{'$ref'}, implode('\', ', $definitionKeys)]); |
634
|
|
|
} |
635
|
|
|
|
636
|
|
|
return $this->rootSchema->definitions->$reference; |
637
|
|
|
} |
638
|
|
|
|
639
|
|
|
/** |
640
|
|
|
* Matches, validates and returns a remote reference |
641
|
|
|
* |
642
|
|
|
* @param $schema |
643
|
|
|
* |
644
|
|
|
* @return mixed |
645
|
|
|
* @throws ValidateSchemaException |
646
|
|
|
*/ |
647
|
|
|
private function getRemoteReference($schema) |
648
|
|
|
{ |
649
|
|
|
// check if the curl_init exists |
650
|
|
|
if (!function_exists('curl_init')) { |
651
|
|
|
|
652
|
|
|
throw new ValidateSchemaException(ValidateSchemaException::ERROR_CURL_NOT_INSTALLED); |
653
|
|
|
} |
654
|
|
|
|
655
|
|
|
$ch = curl_init(); |
656
|
|
|
curl_setopt_array($ch, [ |
657
|
|
|
CURLOPT_URL => $schema->{'$ref'}, |
658
|
|
|
CURLOPT_RETURNTRANSFER => true, |
659
|
|
|
CURLOPT_HTTPHEADER => ['Accept: application/schema+json'], |
660
|
|
|
CURLOPT_CONNECTTIMEOUT => 10, //timeout in seconds |
661
|
|
|
CURLOPT_TIMEOUT => 10 //timeout in seconds |
662
|
|
|
]); |
663
|
|
|
|
664
|
|
|
$response = curl_exec($ch); |
665
|
|
|
$info = curl_getinfo($ch); |
666
|
|
|
|
667
|
|
|
curl_close($ch); |
668
|
|
|
|
669
|
|
|
if (isset($info['http_code']) && (($info['http_code'] < 200) || ($info['http_code'] > 207))) { |
670
|
|
|
|
671
|
|
|
throw new ValidateSchemaException(ValidateSchemaException::ERROR_REMOTE_REFERENCE_DOES_NOT_EXIST, $schema->{'$ref'}); |
672
|
|
|
} |
673
|
|
|
|
674
|
|
|
// if the response is empty no valid schema (or actually no data at all) was found |
675
|
|
|
if (empty($response)) { |
676
|
|
|
|
677
|
|
|
throw new ValidateSchemaException(ValidateSchemaException::ERROR_NO_DATA_WAS_FOUND_IN_REMOTE_SCHEMA, $schema->{'$ref'}); |
678
|
|
|
} |
679
|
|
|
|
680
|
|
|
$json = new Json($response); |
681
|
|
|
|
682
|
|
|
if ($json->validate()) { |
683
|
|
|
|
684
|
|
|
// if the validate method returned true it means valid JSON was found, return the decoded JSON schema |
685
|
|
|
return $json->getDecodedJSON(); |
686
|
|
|
} else { |
687
|
|
|
|
688
|
|
|
// if the validate method returned false it means the JSON Linter can not make chocolate from $response |
689
|
|
|
throw new ValidateSchemaException(ValidateSchemaException::ERROR_NO_VALID_JSON_WAS_FOUND_IN_REMOTE_SCHEMA, $schema->{'$ref'}); |
690
|
|
|
} |
691
|
|
|
} |
692
|
|
|
|
693
|
|
|
/** |
694
|
|
|
* Validates the JSON SCHEMA data type against $data |
695
|
|
|
* |
696
|
|
|
* @param $schema |
697
|
|
|
* @param $data |
698
|
|
|
* @param null|string $path |
699
|
|
|
* |
700
|
|
|
* @return string |
701
|
|
|
* @throws ValidateException |
702
|
|
|
*/ |
703
|
|
|
private function validateType($schema, $data, $path) |
704
|
|
|
{ |
705
|
|
|
// gettype() on a closure returns 'object' which is not what we want |
706
|
|
|
if (is_callable($data) && ($data instanceof \Closure)) { |
707
|
|
|
|
708
|
|
|
$type = BaseValidator::CLOSURE; |
709
|
|
|
} else { |
710
|
|
|
|
711
|
|
|
// override because 'double' (float), 'integer' are covered by 'number' according to http://json-schema.org/latest/json-schema-validation.html#anchor79 |
712
|
|
|
// strtolower; important when dealing with 'null' values |
713
|
|
|
if (in_array(($type = strtolower(gettype($data))), [BaseValidator::DOUBLE, BaseValidator::INTEGER])) { |
714
|
|
|
|
715
|
|
|
$type = BaseValidator::NUMBER; |
716
|
|
|
} |
717
|
|
|
} |
718
|
|
|
|
719
|
|
|
// check if given type matches the expected type, if not add verbose error |
720
|
|
|
if (is_array($schema->type)) { |
721
|
|
|
|
722
|
|
|
$isValid = false; |
723
|
|
|
|
724
|
|
|
foreach ($schema->type as $t) { |
725
|
|
|
|
726
|
|
|
if ($type === strtolower($t)) { |
727
|
|
|
|
728
|
|
|
$isValid = true; |
729
|
|
|
break; |
730
|
|
|
} |
731
|
|
|
} |
732
|
|
|
} |
733
|
|
|
|
734
|
|
|
if ((isset($isValid) && !$isValid) || (is_string($schema->type) && ($type !== strtolower($schema->type)))) { |
|
|
|
|
735
|
|
|
|
736
|
|
|
$msg = ValidateException::ERROR_USER_DATA_VALUE_DOES_NOT_MATCH_CORRECT_TYPE_1; |
737
|
|
|
$schemaType = (is_array($schema->type) ? implode('\' OR \'', $schema->type) : $schema->type); |
738
|
|
|
$params = [$path, $schemaType, $type]; |
739
|
|
|
|
740
|
|
|
if (!in_array($type, [BaseValidator::OBJECT, BaseValidator::CLOSURE, BaseValidator::_ARRAY, BaseValidator::_NULL])) { |
741
|
|
|
|
742
|
|
|
$msg = ValidateException::ERROR_USER_DATA_VALUE_DOES_NOT_MATCH_CORRECT_TYPE_2; |
743
|
|
|
|
744
|
|
|
if (in_array($type, [BaseValidator::STRING])) { |
745
|
|
|
|
746
|
|
|
$data = str_replace("\n", '', $data); |
747
|
|
|
$data = preg_replace("/\r|\n/", '', $data); |
748
|
|
|
$data = (strlen($data) < 25) ? $data : substr($data, 0, 25) . ' [...]'; |
749
|
|
|
} |
750
|
|
|
|
751
|
|
|
// boolean handling |
752
|
|
|
if (in_array($type, [BaseValidator::BOOLEAN])) { |
753
|
|
|
|
754
|
|
|
$data = ($data) ? true : false; |
755
|
|
|
} |
756
|
|
|
|
757
|
|
|
$params[] = $data; |
758
|
|
|
} |
759
|
|
|
|
760
|
|
|
// add error |
761
|
|
|
$this->addError($msg, $params); |
762
|
|
|
} |
763
|
|
|
|
764
|
|
|
return $type; |
765
|
|
|
} |
766
|
|
|
} |
767
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.