Completed
Pull Request — master (#4)
by
unknown
03:58
created

validateSchemaRequiredAgainstProperties()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 3
nop 2
dl 0
loc 25
rs 8.5806
c 0
b 0
f 0
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: "" }
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
202
            //$this->validateOneOf($data, $schema, $path);
0 ignored issues
show
Unused Code Comprehensibility introduced by
77% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
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 = [
308
            sprintf('type|is_string:is_array_of_strings|%s', BaseValidator::STRING),
309
        ];
310
311
        if (isset($schema->type) && $schema->type === BaseValidator::_ARRAY) {
312
313
            // field|must_be|type_in_error_msg
314
            $input = array_merge($input, [
315
                sprintf('items|is_object|%s', BaseValidator::OBJECT)
316
            ]);
317
        }
318
319
        return $this->validateSchemaProperties($input, $schema, $path, true);
320
    }
321
322
    /**
323
     * Validate optional $schema->$property properties
324
     *
325
     * @param $schema
326
     * @param string $path
327
     *
328
     * @return mixed
329
     * @throws ValidateException
330
     */
331
    private function validateSchemaOptionalProperties($schema, $path)
332
    {
333
        $input = [
334
            sprintf('format|is_string|%s', BaseValidator::STRING),
335
            sprintf('enum|is_array|%s', BaseValidator::_ARRAY),
336
            sprintf('caseSensitive|is_bool|%s', BaseValidator::BOOLEAN)
337
        ];
338
339 View Code Duplication
        if (isset($schema->type) && ($schema->type === BaseValidator::_ARRAY)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
340
341
            // field|must_be|type_in_error_msg
342
            $input = array_merge($input, [
343
                sprintf('minItems|is_int|%s', BaseValidator::INTEGER),
344
                sprintf('maxItems|is_int|%s', BaseValidator::INTEGER),
345
                sprintf('uniqueItems|is_bool|%s', BaseValidator::BOOLEAN)
346
            ]);
347
        }
348
349 View Code Duplication
        if (isset($schema->type) && ($schema->type === BaseValidator::STRING)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
350
351
            $input = array_merge($input, [
352
                sprintf('minLength|is_int|%s', BaseValidator::INTEGER),
353
                sprintf('maxLength|is_int|%s', BaseValidator::INTEGER),
354
                sprintf('format|is_string|%s', BaseValidator::STRING)
355
            ]);
356
        }
357
358 View Code Duplication
        if (isset($schema->type) && ($schema->type === BaseValidator::INTEGER)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
359
360
            $input = array_merge($input, [
361
                sprintf('minimum|is_int|%s', BaseValidator::INTEGER),
362
                sprintf('maximum|is_int|%s', BaseValidator::INTEGER)
363
            ]);
364
        }
365
366 View Code Duplication
        if (isset($schema->type) && ($schema->type === BaseValidator::NUMBER)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
367
368
            $input = array_merge($input, [
369
                sprintf('minimum|is_numeric|%s', BaseValidator::NUMBER),
370
                sprintf('maximum|is_numeric|%s', BaseValidator::NUMBER)
371
            ]);
372
        }
373
374
        return $this->validateSchemaProperties($input, $schema, $path);
375
    }
376
377
    /**
378
     * Validate $schema->$property
379
     *
380
     * @param            string[] $input
381
     * @param            $schema
382
     * @param            $path
383
     * @param bool|false $mandatory
384
     *
385
     * @return mixed
386
     * @throws ValidateSchemaException
387
     */
388
    private function validateSchemaProperties($input, $schema, $path, $mandatory = false)
389
    {
390
        foreach ($input as $properties) {
391
392
            list($property, $method, $expectedType) = explode('|', $properties);
393
394
            // when $mandatory is true, check if a certain $property isset
395
            if ($mandatory && !isset($schema->$property)) {
396
397
                throw new ValidateSchemaException(ValidateSchemaException::ERROR_SCHEMA_PROPERTY_NOT_DEFINED, [$property, $path]);
398
            }
399
400
            // check if $property is of a certain type
401
            if (isset($schema->$property)) {
402
403
                $methods = explode(':', $method);
404
                $isValid = false;
405
406
                foreach ($methods as $checkMethod) {
407
                    if ($this->checkPropertyWithFunction($schema->$property, $checkMethod)) {
408
                        $isValid = true;
409
                        break;
410
                    }
411
                }
412
                if (!$isValid) {
413
414
                    $actualType = gettype($schema->$property);
415
416
                    throw new ValidateSchemaException(
417
                        ValidateSchemaException::ERROR_SCHEMA_PROPERTY_TYPE_NOT_VALID,
418
                        [$path, $property, $this->getPreposition($expectedType), $expectedType, $this->getPreposition($actualType), $actualType]
419
                    );
420
                }
421
422
                if (is_array($schema->$property) && count($schema->$property) != count(array_unique($schema->$property))) {
423
424
                    throw new ValidateSchemaException(
425
                        ValidateSchemaException::ERROR_SCHEMA_PROPERTY_TYPES_NOT_UNIQUE,
426
                        [$path, str_replace('"', "'", json_encode($schema->$property)), str_replace('"', "'", json_encode(array_unique($schema->$property)))]
427
                    );
428
429
                }
430
431
                // check if a $property' value must match a list of predefined values
432
                $this->validateSchemaPropertyValue($schema, $property, $path);
433
            }
434
435
            if (in_array($property, array_keys($this->minMaxProperties))) {
436
437
                $this->validateSchemaMinMaxProperties($schema, $this->minMaxProperties[$property][0], $this->minMaxProperties[$property][1], $path);
438
            }
439
        }
440
441
        return $schema;
442
    }
443
444
    /**
445
     * Validate Schema property against a predefined list of values
446
     *
447
     * @param $schema
448
     * @param $property
449
     * @param $path
450
     *
451
     * @throws ValidateSchemaException
452
     */
453
    private function validateSchemaPropertyValue($schema, $property, $path)
454
    {
455
        // return if not applicable
456
        if (!in_array($property, [BaseValidator::TYPE, BaseValidator::FORMAT])) {
457
458
            return;
459
        }
460
461
        // set the correct $expected
462
        switch (1) {
463
            case ($property === BaseValidator::TYPE):
464
                $expected = $this->getValidTypes();
465
                break;
466
            case ($property === BaseValidator::FORMAT):
467
                $expected = $this->getValidFormats();
468
                break;
469
            default:
470
                $expected = [];
471
                break;
472
        }
473
474
        // check if $expected contains the $property
475
        $values = (array) $schema->$property;
476
        foreach ($values as $value) {
477
            if (!in_array($value, $expected)) {
478
479
                $count = count($expected);
480
481
                throw new ValidateSchemaException(
482
                    ValidateSchemaException::ERROR_SCHEMA_PROPERTY_VALUE_IS_NOT_VALID,
483
                    [$value, $path, $property, $this->conjugationObject($count, '', 'any of '), $this->conjugationObject($count), implode('\', \'', $expected)]
484
                );
485
            }
486
        }
487
    }
488
489
    /**
490
     * Validate Schema a min and max properties (if set)
491
     *
492
     * @param $schema
493
     * @param $minProperty
494
     * @param $maxProperty
495
     * @param $path
496
     *
497
     * @throws ValidateSchemaException
498
     */
499
    private function validateSchemaMinMaxProperties($schema, $minProperty, $maxProperty, $path)
500
    {
501
        // both $schema->$maxProperty cannot be zero
502
        if (isset($schema->$maxProperty) && ($schema->$maxProperty === 0)) {
503
504
            throw new ValidateSchemaException(ValidateSchemaException::ERROR_SCHEMA_MAX_PROPERTY_CANNOT_NOT_BE_ZERO, [$path, $maxProperty]);
505
        }
506
507
        if (isset($schema->$minProperty) && isset($schema->$maxProperty) && ($schema->$minProperty > $schema->$maxProperty)) {
508
509
            throw new ValidateSchemaException(
510
                ValidateSchemaException::ERROR_SCHEMA_PROPERTY_MIN_NOT_BIGGER_THAN_MAX,
511
                [$path, $minProperty, $schema->$minProperty, $path, $maxProperty, $schema->$maxProperty]
512
            );
513
        }
514
    }
515
516
    /**
517
     * Walk through all $schema->required items and check if there is a $schema->properties item defined for it
518
     *
519
     * @param $schema
520
     * @param string $path
521
     *
522
     * @throws ValidateSchemaException
523
     */
524
    private function validateSchemaRequiredAgainstProperties($schema, $path)
525
    {
526
        $requiredPath = $path . '.required';
527
528
        // $schema->required must be an array
529
        if (!is_array($schema->required)) {
530
531
            $type        = gettype($schema->required);
532
            $preposition = $this->getPreposition($type);
533
534
            throw new ValidateSchemaException(ValidateSchemaException::ERROR_SCHEMA_PROPERTY_REQUIRED_MUST_BE_AN_ARRAY, [$requiredPath, $preposition, $type]);
535
        }
536
537
        // check if the $schema->required property contains fields that have not been defined in $schema->properties
538
        if (!empty($schema->required) && count($missing = array_diff_key(array_flip($schema->required), (array)$schema->properties)) > 0) {
539
540
            $count = count($missing);
541
            $verb  = $this->conjugationToBe($count);
542
543
            throw new ValidateSchemaException(
544
                ValidateSchemaException::ERROR_SCHEMA_REQUIRED_AND_PROPERTIES_MUST_MATCH,
545
                [$this->conjugationObject($count), implode('\', \'', array_flip($missing)), $verb, $requiredPath, $verb, $path]
546
            );
547
        }
548
    }
549
550
    /**
551
     * Retrieve and validate a reference
552
     *
553
     * @param  $schema
554
     *
555
     * @return mixed
556
     * @throws ValidateSchemaException
557
     */
558
    private function getReference($schema)
559
    {
560
        // return any previously requested definitions
561
        if (isset($this->cacheReferencedSchemas[$schema->{'$ref'}])) {
562
563
            return $this->cacheReferencedSchemas[$schema->{'$ref'}];
564
        }
565
566
        $referencedSchema = null;
567
568
        // fetch local reference
569
        if (strpos($schema->{'$ref'}, '#/definitions/') !== false) {
570
571
            $referencedSchema = $this->getLocalReference($schema);
572
            // fetch remote reference
573
        } elseif (strpos($schema->{'$ref'}, 'http') !== false) {
574
575
            $referencedSchema = $this->getRemoteReference($schema);
576
        }
577
578
        // not a local reference nor a remote reference
579
        if (is_null($referencedSchema)) {
580
581
            throw new ValidateSchemaException(ValidateSchemaException::ERROR_INVALID_REFERENCE, $schema->{'$ref'});
582
        }
583
584
        // cache the result
585
        $this->cacheReferencedSchemas[$schema->{'$ref'}] = $referencedSchema;
586
587
        // unset the reference for the $schema for cleanup purposes
588
        unset($schema->{'$ref'});
589
590
        // augment the current $schema with the fetched referenced schema properties (and override if necessary)
591
        foreach (get_object_vars($referencedSchema) as $property => $value) {
592
593
            $schema->$property = $value;
594
        }
595
596
        return $schema;
597
    }
598
599
    /**
600
     * Matches and returns a local reference
601
     *
602
     * @param $schema
603
     *
604
     * @return mixed
605
     * @throws ValidateSchemaException
606
     */
607
    private function getLocalReference($schema)
608
    {
609
        // check if there is at least one local reference defined to match it to
610
        if (!isset($this->rootSchema->definitions) || (count($definitions = get_object_vars($this->rootSchema->definitions)) === 0)) {
611
612
            throw new ValidateSchemaException(ValidateSchemaException::ERROR_NO_LOCAL_DEFINITIONS_HAVE_BEEN_DEFINED);
613
        }
614
615
        // check if the referenced schema is locally defined
616
        $definitionKeys = array_keys($definitions);
617
        $reference      = substr($schema->{'$ref'}, strlen('#/definitions/'));
618
619
        if (!in_array($reference, $definitionKeys)) {
620
621
            throw new ValidateSchemaException(ValidateSchemaException::ERROR_CHECK_IF_LOCAL_DEFINITIONS_EXISTS, [$schema->{'$ref'}, implode('\', ', $definitionKeys)]);
622
        }
623
624
        return $this->rootSchema->definitions->$reference;
625
    }
626
627
    /**
628
     * Matches, validates and returns a remote reference
629
     *
630
     * @param $schema
631
     *
632
     * @return mixed
633
     * @throws ValidateSchemaException
634
     */
635
    private function getRemoteReference($schema)
636
    {
637
        // check if the curl_init exists
638
        if (!function_exists('curl_init')) {
639
640
            throw new ValidateSchemaException(ValidateSchemaException::ERROR_CURL_NOT_INSTALLED);
641
        }
642
643
        $ch = curl_init();
644
        curl_setopt_array($ch, [
645
            CURLOPT_URL            => $schema->{'$ref'},
646
            CURLOPT_RETURNTRANSFER => true,
647
            CURLOPT_HTTPHEADER     => ['Accept: application/schema+json'],
648
            CURLOPT_CONNECTTIMEOUT => 10, //timeout in seconds
649
            CURLOPT_TIMEOUT        => 10  //timeout in seconds
650
        ]);
651
652
        $response = curl_exec($ch);
653
        $info     = curl_getinfo($ch);
654
655
        curl_close($ch);
656
657
        if (isset($info['http_code']) && (($info['http_code'] < 200) || ($info['http_code'] > 207))) {
658
659
            throw new ValidateSchemaException(ValidateSchemaException::ERROR_REMOTE_REFERENCE_DOES_NOT_EXIST, $schema->{'$ref'});
660
        }
661
662
        // if the response is empty no valid schema (or actually no data at all) was found
663
        if (empty($response)) {
664
665
            throw new ValidateSchemaException(ValidateSchemaException::ERROR_NO_DATA_WAS_FOUND_IN_REMOTE_SCHEMA, $schema->{'$ref'});
666
        }
667
668
        $json = new Json($response);
669
670
        if ($json->validate()) {
671
672
            // if the validate method returned true it means valid JSON was found, return the decoded JSON schema
673
            return $json->getDecodedJSON();
674
        } else {
675
676
            // if the validate method returned false it means the JSON Linter can not make chocolate from $response
677
            throw new ValidateSchemaException(ValidateSchemaException::ERROR_NO_VALID_JSON_WAS_FOUND_IN_REMOTE_SCHEMA, $schema->{'$ref'});
678
        }
679
    }
680
681
    /**
682
     * Validates the JSON SCHEMA data type against $data
683
     *
684
     * @param $schema
685
     * @param $data
686
     * @param null|string $path
687
     *
688
     * @return string
689
     * @throws ValidateException
690
     */
691
    private function validateType($schema, $data, $path)
692
    {
693
        // gettype() on a closure returns 'object' which is not what we want
694
        if (is_callable($data) && ($data instanceof \Closure)) {
695
696
            $type = BaseValidator::CLOSURE;
697
        } else {
698
699
            // override because 'double' (float), 'integer' are covered by 'number' according to http://json-schema.org/latest/json-schema-validation.html#anchor79
700
            if (in_array(($type = gettype($data)), [BaseValidator::DOUBLE, BaseValidator::INTEGER])) {
701
702
                $type = BaseValidator::NUMBER;
703
            }
704
        }
705
706
        // check if given type matches the expected type, if not add verbose error
707
        $type = strtolower($type);
708
        $types = (array) $schema->type;
709
        if (!in_array($type, $types)) {
710
711
            $msg    = ValidateException::ERROR_USER_DATA_VALUE_DOES_NOT_MATCH_CORRECT_TYPE_1;
712
            $params = [$path, $this->getPreposition($schema->type), implode(' or ', $types), $this->getPreposition($type), $type];
713
714
            if (!in_array($type, [BaseValidator::OBJECT, BaseValidator::CLOSURE, BaseValidator::_ARRAY, BaseValidator::BOOLEAN])) {
715
716
                $msg = ValidateException::ERROR_USER_DATA_VALUE_DOES_NOT_MATCH_CORRECT_TYPE_2;
717
718
                if (in_array($type, [BaseValidator::STRING])) {
719
720
                    $data = str_replace("\n", '', $data);
721
                    $data = preg_replace("/\r|\n/", '', $data);
722
                    $data = (strlen($data) < 25) ? $data : substr($data, 0, 25) . ' [...]';
723
                }
724
725
                $params[] = $data;
726
            }
727
728
            // add error
729
            $this->addError($msg, $params);
730
        }
731
732
        return $type;
733
    }
734
735
    /**
736
     * Validate a property with a specific function.
737
     *
738
     * The function can be either an existing (global) function, or a function that matches an existing method
739
     * of the SchemaValidator class after prefixing with custom_validate_ and conversion from snake case to camel case.
740
     *
741
     * @param $propertyValue
742
     * @param $function
743
     * @return boolean
744
     * @throws ValidateSchemaException
745
     */
746
    private function checkPropertyWithFunction($propertyValue, $function)
747
    {
748
        //  Use a custom validation function, if it exists
749
        $customValidatorName = 'custom_validate_' . $function;
750
        $customValidatorFunction = str_replace('_', '', ucfirst($customValidatorName));
751
        if (method_exists($this, $customValidatorFunction)) {
752
            return $this->$customValidatorFunction($propertyValue);
753
        }
754
755
        //  Use global function
756
        if (function_exists($function)) {
757
            return $function($propertyValue);
758
        }
759
760
        throw new ValidateSchemaException(
761
            ValidateSchemaException::ERROR_SCHEMA_PROPERTY_VALIDATOR_DOES_NOT_EXIST,
762
            [$function]
763
        );
764
    }
765
766
    /**
767
     * Validate that a property value is an array and all elements in the array are strings.
768
     *
769
     * @param $data
770
     * @return bool
771
     */
772
    private function customValidateIsArrayOfStrings($data)
773
    {
774
        if (!is_array($data)) {
775
            return false;
776
        }
777
778
        foreach ($data as $item) {
779
            if (!is_string($item)) {
780
                return false;
781
            }
782
        }
783
784
        return true;
785
    }
786
}
787