SchemaValidator   D
last analyzed

Complexity

Total Complexity 119

Size/Duplication

Total Lines 752
Duplicated Lines 5.59 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 3
Bugs 3 Features 0
Metric Value
c 3
b 3
f 0
dl 42
loc 752
rs 4.4444
wmc 119
lcom 1
cbo 4

14 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 0 41 7
C validateData() 0 60 17
B validate() 0 35 6
C validateSchema() 0 78 12
B validateSchemaMandatoryProperties() 11 20 5
D validateSchemaOptionalProperties() 31 45 9
C validateSchemaProperties() 0 37 7
C validateSchemaPropertyValue() 0 61 12
B validateSchemaMinMaxProperties() 0 16 6
B validateSchemaRequiredAgainstProperties() 0 25 4
B getReference() 0 40 6
A getLocalReference() 0 19 4
C getRemoteReference() 0 45 7
D validateType() 0 63 17

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SchemaValidator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SchemaValidator, and based on these observations, apply Extract Interface, too.

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 = [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)) {
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...
311
312
            $input = [sprintf('type|is_array|%s', BaseValidator::_ARRAY)];
313
        }
314
315 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...
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)) {
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...
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)) {
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...
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)) {
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...
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)) {
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...
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)))) {
0 ignored issues
show
Bug introduced by
The variable $isValid does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
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