secure-custom-fields
/
vendor
/
justinrainbow
/
json-schema
/
src
/
JsonSchema
/
Constraints
/
UndefinedConstraint.php
secure-custom-fields
/
vendor
/
justinrainbow
/
json-schema
/
src
/
JsonSchema
/
Constraints
Last commit date
TypeCheck
8 months ago
BaseConstraint.php
8 months ago
CollectionConstraint.php
8 months ago
Constraint.php
8 months ago
ConstraintInterface.php
8 months ago
EnumConstraint.php
8 months ago
Factory.php
8 months ago
FormatConstraint.php
8 months ago
NumberConstraint.php
8 months ago
ObjectConstraint.php
8 months ago
SchemaConstraint.php
8 months ago
StringConstraint.php
8 months ago
TypeConstraint.php
8 months ago
UndefinedConstraint.php
8 months ago
UndefinedConstraint.php
421 lines
| 1 | <?php |
| 2 | |
| 3 | /* |
| 4 | * This file is part of the JsonSchema package. |
| 5 | * |
| 6 | * For the full copyright and license information, please view the LICENSE |
| 7 | * file that was distributed with this source code. |
| 8 | */ |
| 9 | |
| 10 | namespace JsonSchema\Constraints; |
| 11 | |
| 12 | use JsonSchema\Constraints\TypeCheck\LooseTypeCheck; |
| 13 | use JsonSchema\Entity\JsonPointer; |
| 14 | use JsonSchema\Exception\ValidationException; |
| 15 | use JsonSchema\Uri\UriResolver; |
| 16 | |
| 17 | /** |
| 18 | * The UndefinedConstraint Constraints |
| 19 | * |
| 20 | * @author Robert Schönthal <seroscho@googlemail.com> |
| 21 | * @author Bruno Prieto Reis <bruno.p.reis@gmail.com> |
| 22 | */ |
| 23 | #[\AllowDynamicProperties] |
| 24 | class UndefinedConstraint extends Constraint |
| 25 | { |
| 26 | /** |
| 27 | * @var array List of properties to which a default value has been applied |
| 28 | */ |
| 29 | protected $appliedDefaults = array(); |
| 30 | |
| 31 | /** |
| 32 | * {@inheritdoc} |
| 33 | */ |
| 34 | public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null, $fromDefault = false) |
| 35 | { |
| 36 | if (is_null($schema) || !is_object($schema)) { |
| 37 | return; |
| 38 | } |
| 39 | |
| 40 | $path = $this->incrementPath($path ?: new JsonPointer(''), $i); |
| 41 | if ($fromDefault) { |
| 42 | $path->setFromDefault(); |
| 43 | } |
| 44 | |
| 45 | // check special properties |
| 46 | $this->validateCommonProperties($value, $schema, $path, $i); |
| 47 | |
| 48 | // check allOf, anyOf, and oneOf properties |
| 49 | $this->validateOfProperties($value, $schema, $path, ''); |
| 50 | |
| 51 | // check known types |
| 52 | $this->validateTypes($value, $schema, $path, $i); |
| 53 | } |
| 54 | |
| 55 | /** |
| 56 | * Validates the value against the types |
| 57 | * |
| 58 | * @param mixed $value |
| 59 | * @param mixed $schema |
| 60 | * @param JsonPointer $path |
| 61 | * @param string $i |
| 62 | */ |
| 63 | public function validateTypes(&$value, $schema, JsonPointer $path, $i = null) |
| 64 | { |
| 65 | // check array |
| 66 | if ($this->getTypeCheck()->isArray($value)) { |
| 67 | $this->checkArray($value, $schema, $path, $i); |
| 68 | } |
| 69 | |
| 70 | // check object |
| 71 | if (LooseTypeCheck::isObject($value)) { // object processing should always be run on assoc arrays, |
| 72 | // so use LooseTypeCheck here even if CHECK_MODE_TYPE_CAST |
| 73 | // is not set (i.e. don't use $this->getTypeCheck() here). |
| 74 | $this->checkObject( |
| 75 | $value, |
| 76 | $schema, |
| 77 | $path, |
| 78 | isset($schema->properties) ? $schema->properties : null, |
| 79 | isset($schema->additionalProperties) ? $schema->additionalProperties : null, |
| 80 | isset($schema->patternProperties) ? $schema->patternProperties : null, |
| 81 | $this->appliedDefaults |
| 82 | ); |
| 83 | } |
| 84 | |
| 85 | // check string |
| 86 | if (is_string($value)) { |
| 87 | $this->checkString($value, $schema, $path, $i); |
| 88 | } |
| 89 | |
| 90 | // check numeric |
| 91 | if (is_numeric($value)) { |
| 92 | $this->checkNumber($value, $schema, $path, $i); |
| 93 | } |
| 94 | |
| 95 | // check enum |
| 96 | if (isset($schema->enum)) { |
| 97 | $this->checkEnum($value, $schema, $path, $i); |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | /** |
| 102 | * Validates common properties |
| 103 | * |
| 104 | * @param mixed $value |
| 105 | * @param mixed $schema |
| 106 | * @param JsonPointer $path |
| 107 | * @param string $i |
| 108 | */ |
| 109 | protected function validateCommonProperties(&$value, $schema, JsonPointer $path, $i = '') |
| 110 | { |
| 111 | // if it extends another schema, it must pass that schema as well |
| 112 | if (isset($schema->extends)) { |
| 113 | if (is_string($schema->extends)) { |
| 114 | $schema->extends = $this->validateUri($schema, $schema->extends); |
| 115 | } |
| 116 | if (is_array($schema->extends)) { |
| 117 | foreach ($schema->extends as $extends) { |
| 118 | $this->checkUndefined($value, $extends, $path, $i); |
| 119 | } |
| 120 | } else { |
| 121 | $this->checkUndefined($value, $schema->extends, $path, $i); |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | // Apply default values from schema |
| 126 | if (!$path->fromDefault()) { |
| 127 | $this->applyDefaultValues($value, $schema, $path); |
| 128 | } |
| 129 | |
| 130 | // Verify required values |
| 131 | if ($this->getTypeCheck()->isObject($value)) { |
| 132 | if (!($value instanceof self) && isset($schema->required) && is_array($schema->required)) { |
| 133 | // Draft 4 - Required is an array of strings - e.g. "required": ["foo", ...] |
| 134 | foreach ($schema->required as $required) { |
| 135 | if (!$this->getTypeCheck()->propertyExists($value, $required)) { |
| 136 | $this->addError( |
| 137 | $this->incrementPath($path ?: new JsonPointer(''), $required), |
| 138 | 'The property ' . $required . ' is required', |
| 139 | 'required' |
| 140 | ); |
| 141 | } |
| 142 | } |
| 143 | } elseif (isset($schema->required) && !is_array($schema->required)) { |
| 144 | // Draft 3 - Required attribute - e.g. "foo": {"type": "string", "required": true} |
| 145 | if ($schema->required && $value instanceof self) { |
| 146 | $propertyPaths = $path->getPropertyPaths(); |
| 147 | $propertyName = end($propertyPaths); |
| 148 | $this->addError( |
| 149 | $path, |
| 150 | 'The property ' . $propertyName . ' is required', |
| 151 | 'required' |
| 152 | ); |
| 153 | } |
| 154 | } else { |
| 155 | // if the value is both undefined and not required, skip remaining checks |
| 156 | // in this method which assume an actual, defined instance when validating. |
| 157 | if ($value instanceof self) { |
| 158 | return; |
| 159 | } |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | // Verify type |
| 164 | if (!($value instanceof self)) { |
| 165 | $this->checkType($value, $schema, $path, $i); |
| 166 | } |
| 167 | |
| 168 | // Verify disallowed items |
| 169 | if (isset($schema->disallow)) { |
| 170 | $initErrors = $this->getErrors(); |
| 171 | |
| 172 | $typeSchema = new \stdClass(); |
| 173 | $typeSchema->type = $schema->disallow; |
| 174 | $this->checkType($value, $typeSchema, $path); |
| 175 | |
| 176 | // if no new errors were raised it must be a disallowed value |
| 177 | if (count($this->getErrors()) == count($initErrors)) { |
| 178 | $this->addError($path, 'Disallowed value was matched', 'disallow'); |
| 179 | } else { |
| 180 | $this->errors = $initErrors; |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | if (isset($schema->not)) { |
| 185 | $initErrors = $this->getErrors(); |
| 186 | $this->checkUndefined($value, $schema->not, $path, $i); |
| 187 | |
| 188 | // if no new errors were raised then the instance validated against the "not" schema |
| 189 | if (count($this->getErrors()) == count($initErrors)) { |
| 190 | $this->addError($path, 'Matched a schema which it should not', 'not'); |
| 191 | } else { |
| 192 | $this->errors = $initErrors; |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | // Verify that dependencies are met |
| 197 | if (isset($schema->dependencies) && $this->getTypeCheck()->isObject($value)) { |
| 198 | $this->validateDependencies($value, $schema->dependencies, $path); |
| 199 | } |
| 200 | } |
| 201 | |
| 202 | /** |
| 203 | * Check whether a default should be applied for this value |
| 204 | * |
| 205 | * @param mixed $schema |
| 206 | * @param mixed $parentSchema |
| 207 | * @param bool $requiredOnly |
| 208 | * |
| 209 | * @return bool |
| 210 | */ |
| 211 | private function shouldApplyDefaultValue($requiredOnly, $schema, $name = null, $parentSchema = null) |
| 212 | { |
| 213 | // required-only mode is off |
| 214 | if (!$requiredOnly) { |
| 215 | return true; |
| 216 | } |
| 217 | // draft-04 required is set |
| 218 | if ( |
| 219 | $name !== null |
| 220 | && isset($parentSchema->required) |
| 221 | && is_array($parentSchema->required) |
| 222 | && in_array($name, $parentSchema->required) |
| 223 | ) { |
| 224 | return true; |
| 225 | } |
| 226 | // draft-03 required is set |
| 227 | if (isset($schema->required) && !is_array($schema->required) && $schema->required) { |
| 228 | return true; |
| 229 | } |
| 230 | // default case |
| 231 | return false; |
| 232 | } |
| 233 | |
| 234 | /** |
| 235 | * Apply default values |
| 236 | * |
| 237 | * @param mixed $value |
| 238 | * @param mixed $schema |
| 239 | * @param JsonPointer $path |
| 240 | */ |
| 241 | protected function applyDefaultValues(&$value, $schema, $path) |
| 242 | { |
| 243 | // only apply defaults if feature is enabled |
| 244 | if (!$this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) { |
| 245 | return; |
| 246 | } |
| 247 | |
| 248 | // apply defaults if appropriate |
| 249 | $requiredOnly = $this->factory->getConfig(self::CHECK_MODE_ONLY_REQUIRED_DEFAULTS); |
| 250 | if (isset($schema->properties) && LooseTypeCheck::isObject($value)) { |
| 251 | // $value is an object or assoc array, and properties are defined - treat as an object |
| 252 | foreach ($schema->properties as $currentProperty => $propertyDefinition) { |
| 253 | $propertyDefinition = $this->factory->getSchemaStorage()->resolveRefSchema($propertyDefinition); |
| 254 | if ( |
| 255 | !LooseTypeCheck::propertyExists($value, $currentProperty) |
| 256 | && property_exists($propertyDefinition, 'default') |
| 257 | && $this->shouldApplyDefaultValue($requiredOnly, $propertyDefinition, $currentProperty, $schema) |
| 258 | ) { |
| 259 | // assign default value |
| 260 | if (is_object($propertyDefinition->default)) { |
| 261 | LooseTypeCheck::propertySet($value, $currentProperty, clone $propertyDefinition->default); |
| 262 | } else { |
| 263 | LooseTypeCheck::propertySet($value, $currentProperty, $propertyDefinition->default); |
| 264 | } |
| 265 | $this->appliedDefaults[] = $currentProperty; |
| 266 | } |
| 267 | } |
| 268 | } elseif (isset($schema->items) && LooseTypeCheck::isArray($value)) { |
| 269 | $items = array(); |
| 270 | if (LooseTypeCheck::isArray($schema->items)) { |
| 271 | $items = $schema->items; |
| 272 | } elseif (isset($schema->minItems) && count($value) < $schema->minItems) { |
| 273 | $items = array_fill(count($value), $schema->minItems - count($value), $schema->items); |
| 274 | } |
| 275 | // $value is an array, and items are defined - treat as plain array |
| 276 | foreach ($items as $currentItem => $itemDefinition) { |
| 277 | $itemDefinition = $this->factory->getSchemaStorage()->resolveRefSchema($itemDefinition); |
| 278 | if ( |
| 279 | !array_key_exists($currentItem, $value) |
| 280 | && property_exists($itemDefinition, 'default') |
| 281 | && $this->shouldApplyDefaultValue($requiredOnly, $itemDefinition)) { |
| 282 | if (is_object($itemDefinition->default)) { |
| 283 | $value[$currentItem] = clone $itemDefinition->default; |
| 284 | } else { |
| 285 | $value[$currentItem] = $itemDefinition->default; |
| 286 | } |
| 287 | } |
| 288 | $path->setFromDefault(); |
| 289 | } |
| 290 | } elseif ( |
| 291 | $value instanceof self |
| 292 | && property_exists($schema, 'default') |
| 293 | && $this->shouldApplyDefaultValue($requiredOnly, $schema)) { |
| 294 | // $value is a leaf, not a container - apply the default directly |
| 295 | $value = is_object($schema->default) ? clone $schema->default : $schema->default; |
| 296 | $path->setFromDefault(); |
| 297 | } |
| 298 | } |
| 299 | |
| 300 | /** |
| 301 | * Validate allOf, anyOf, and oneOf properties |
| 302 | * |
| 303 | * @param mixed $value |
| 304 | * @param mixed $schema |
| 305 | * @param JsonPointer $path |
| 306 | * @param string $i |
| 307 | */ |
| 308 | protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i = '') |
| 309 | { |
| 310 | // Verify type |
| 311 | if ($value instanceof self) { |
| 312 | return; |
| 313 | } |
| 314 | |
| 315 | if (isset($schema->allOf)) { |
| 316 | $isValid = true; |
| 317 | foreach ($schema->allOf as $allOf) { |
| 318 | $initErrors = $this->getErrors(); |
| 319 | $this->checkUndefined($value, $allOf, $path, $i); |
| 320 | $isValid = $isValid && (count($this->getErrors()) == count($initErrors)); |
| 321 | } |
| 322 | if (!$isValid) { |
| 323 | $this->addError($path, 'Failed to match all schemas', 'allOf'); |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | if (isset($schema->anyOf)) { |
| 328 | $isValid = false; |
| 329 | $startErrors = $this->getErrors(); |
| 330 | $caughtException = null; |
| 331 | foreach ($schema->anyOf as $anyOf) { |
| 332 | $initErrors = $this->getErrors(); |
| 333 | try { |
| 334 | $this->checkUndefined($value, $anyOf, $path, $i); |
| 335 | if ($isValid = (count($this->getErrors()) == count($initErrors))) { |
| 336 | break; |
| 337 | } |
| 338 | } catch (ValidationException $e) { |
| 339 | $isValid = false; |
| 340 | } |
| 341 | } |
| 342 | if (!$isValid) { |
| 343 | $this->addError($path, 'Failed to match at least one schema', 'anyOf'); |
| 344 | } else { |
| 345 | $this->errors = $startErrors; |
| 346 | } |
| 347 | } |
| 348 | |
| 349 | if (isset($schema->oneOf)) { |
| 350 | $allErrors = array(); |
| 351 | $matchedSchemas = 0; |
| 352 | $startErrors = $this->getErrors(); |
| 353 | foreach ($schema->oneOf as $oneOf) { |
| 354 | try { |
| 355 | $this->errors = array(); |
| 356 | $this->checkUndefined($value, $oneOf, $path, $i); |
| 357 | if (count($this->getErrors()) == 0) { |
| 358 | $matchedSchemas++; |
| 359 | } |
| 360 | $allErrors = array_merge($allErrors, array_values($this->getErrors())); |
| 361 | } catch (ValidationException $e) { |
| 362 | // deliberately do nothing here - validation failed, but we want to check |
| 363 | // other schema options in the OneOf field. |
| 364 | } |
| 365 | } |
| 366 | if ($matchedSchemas !== 1) { |
| 367 | $this->addErrors(array_merge($allErrors, $startErrors)); |
| 368 | $this->addError($path, 'Failed to match exactly one schema', 'oneOf'); |
| 369 | } else { |
| 370 | $this->errors = $startErrors; |
| 371 | } |
| 372 | } |
| 373 | } |
| 374 | |
| 375 | /** |
| 376 | * Validate dependencies |
| 377 | * |
| 378 | * @param mixed $value |
| 379 | * @param mixed $dependencies |
| 380 | * @param JsonPointer $path |
| 381 | * @param string $i |
| 382 | */ |
| 383 | protected function validateDependencies($value, $dependencies, JsonPointer $path, $i = '') |
| 384 | { |
| 385 | foreach ($dependencies as $key => $dependency) { |
| 386 | if ($this->getTypeCheck()->propertyExists($value, $key)) { |
| 387 | if (is_string($dependency)) { |
| 388 | // Draft 3 string is allowed - e.g. "dependencies": {"bar": "foo"} |
| 389 | if (!$this->getTypeCheck()->propertyExists($value, $dependency)) { |
| 390 | $this->addError($path, "$key depends on $dependency and $dependency is missing", 'dependencies'); |
| 391 | } |
| 392 | } elseif (is_array($dependency)) { |
| 393 | // Draft 4 must be an array - e.g. "dependencies": {"bar": ["foo"]} |
| 394 | foreach ($dependency as $d) { |
| 395 | if (!$this->getTypeCheck()->propertyExists($value, $d)) { |
| 396 | $this->addError($path, "$key depends on $d and $d is missing", 'dependencies'); |
| 397 | } |
| 398 | } |
| 399 | } elseif (is_object($dependency)) { |
| 400 | // Schema - e.g. "dependencies": {"bar": {"properties": {"foo": {...}}}} |
| 401 | $this->checkUndefined($value, $dependency, $path, $i); |
| 402 | } |
| 403 | } |
| 404 | } |
| 405 | } |
| 406 | |
| 407 | protected function validateUri($schema, $schemaUri = null) |
| 408 | { |
| 409 | $resolver = new UriResolver(); |
| 410 | $retriever = $this->factory->getUriRetriever(); |
| 411 | |
| 412 | $jsonSchema = null; |
| 413 | if ($resolver->isValid($schemaUri)) { |
| 414 | $schemaId = property_exists($schema, 'id') ? $schema->id : null; |
| 415 | $jsonSchema = $retriever->retrieve($schemaId, $schemaUri); |
| 416 | } |
| 417 | |
| 418 | return $jsonSchema; |
| 419 | } |
| 420 | } |
| 421 |