* @license http://www.apache.org/licenses/LICENSE-2.0 * @link http://phpsx.org */ class SchemaTraverser { /** @var array */ private $pathStack; /** @var bool */ private $assertConstraints; /** * @param bool $assertConstraints */ public function __construct(bool $assertConstraints = true) { $this->assertConstraints = $assertConstraints; } /** * Traverses through the data and validates it according to the provided * schema. Calls also the visitor methods for each type * * @return mixed * @throws ValidationException * @throws TraverserException */ public function traverse($data, SchemaInterface $schema, VisitorInterface $visitor = null) { $this->pathStack = []; if ($visitor === null) { $visitor = new NullVisitor(); } return $this->recTraverse($data, $schema->getType(), $schema->getDefinitions(), $visitor); } /** * @throws TypeNotFoundException * @throws ValidationException * @throws TraverserException */ protected function recTraverse( $data, TypeInterface $type, DefinitionsInterface $definitions, VisitorInterface $visitor, array $context = [], ) { if ($type instanceof StructType) { if ($this->assertConstraints) { $this->assertStructConstraints($data, $type); } $result = $this->traverseStruct($data, $type, $definitions, $visitor, $context); } elseif ($type instanceof MapType) { if ($this->assertConstraints) { $this->assertMapConstraints($data, $type); } $result = $this->traverseMap($data, $type, $definitions, $visitor, $context); } elseif ($type instanceof ArrayType) { if ($this->assertConstraints) { $this->assertArrayConstraints($data, $type); } $result = $this->traverseArray($data, $type, $definitions, $visitor, $context); } elseif ($type instanceof StringType) { if ($this->assertConstraints) { $this->assertStringConstraints($data, $type); $this->assertScalarConstraints($data, $type); } $result = $this->traverseString($data, $type, $visitor); } elseif ($type instanceof IntegerType) { if ($this->assertConstraints) { $this->assertNumberConstraints($data, $type); $this->assertScalarConstraints($data, $type); } $result = $visitor->visitInteger($data, $type, $this->getCurrentPath()); } elseif ($type instanceof NumberType) { if ($this->assertConstraints) { $this->assertNumberConstraints($data, $type); $this->assertScalarConstraints($data, $type); } $result = $visitor->visitNumber($data, $type, $this->getCurrentPath()); } elseif ($type instanceof BooleanType) { if ($this->assertConstraints) { $this->assertBooleanConstraints($data, $type); } $result = $visitor->visitBoolean($data, $type, $this->getCurrentPath()); } elseif ($type instanceof IntersectionType) { $result = $this->traverseIntersection($data, $type, $definitions, $visitor, $context); } elseif ($type instanceof UnionType) { $result = $this->traverseUnion($data, $type, $definitions, $visitor, $context); } elseif ($type instanceof ReferenceType) { $subType = $definitions->getType($type->getRef()); // in case a reference has a concrete class we inherit this class to the sub type $class = $type->getAttribute(TypeAbstract::ATTR_CLASS); if ($subType instanceof TypeAbstract && !empty($class)) { $subType->setAttribute(TypeAbstract::ATTR_CLASS, $class); } $result = $this->recTraverse($data, $subType, $definitions, $visitor, $type->getTemplate() ?: []); } elseif ($type instanceof GenericType) { if (!isset($context[$type->getGeneric()])) { throw new TraverserException('Could not resolve generic type from context'); } $subType = $definitions->getType($context[$type->getGeneric()]); $result = $this->recTraverse($data, $subType, $definitions, $visitor, $context); } elseif ($type instanceof AnyType) { $result = $data; } else { $result = null; } return $result; } protected function traverseStruct( \stdClass $data, StructType $type, DefinitionsInterface $definitions, VisitorInterface $visitor, array $context, ) { $result = new \stdClass(); $properties = []; $extends = $type->getExtends(); while (!empty($extends)) { $parent = $definitions->getType($extends); if (!$parent instanceof StructType) { break; } $properties = array_merge($properties, $parent->getProperties() ?? []); $extends = $parent->getExtends(); } $properties = array_merge($properties, $type->getProperties() ?? []); if (!empty($properties)) { $data = (array) $data; foreach ($properties as $key => $subType) { array_push($this->pathStack, $key); if (array_key_exists($key, $data)) { $result->{$key} = $this->recTraverse($data[$key], $subType, $definitions, $visitor, $context); } array_pop($this->pathStack); } } return $visitor->visitStruct($result, $type, $this->getCurrentPath()); } protected function traverseMap( \stdClass $data, MapType $type, DefinitionsInterface $definitions, VisitorInterface $visitor, array $context, ) { $data = (array) $data; $result = new \stdClass(); $additionalProperties = $type->getAdditionalProperties(); if (is_bool($additionalProperties)) { if ($additionalProperties === true) { $result = (object) $data; } } elseif ($additionalProperties instanceof TypeInterface) { foreach ($data as $key => $value) { array_push($this->pathStack, $key); $result->{$key} = $this->recTraverse($data[$key], $additionalProperties, $definitions, $visitor, $context); array_pop($this->pathStack); } } return $visitor->visitMap($result, $type, $this->getCurrentPath()); } protected function traverseArray( array $data, ArrayType $type, DefinitionsInterface $definitions, VisitorInterface $visitor, array $context, ) { $result = []; $items = $type->getItems(); if ($items instanceof TypeInterface) { foreach ($data as $index => $value) { array_push($this->pathStack, $index); $result[] = $this->recTraverse($value, $items, $definitions, $visitor, $context); array_pop($this->pathStack); } } return $visitor->visitArray($result, $type, $this->getCurrentPath()); } protected function traverseString($data, StringType $type, VisitorInterface $visitor) { $format = $type->getFormat(); if ($format === TypeAbstract::FORMAT_BINARY) { return $visitor->visitBinary($data, $type, $this->getCurrentPath()); } elseif ($format === TypeAbstract::FORMAT_DATETIME) { return $visitor->visitDateTime($data, $type, $this->getCurrentPath()); } elseif ($format === TypeAbstract::FORMAT_DATE) { return $visitor->visitDate($data, $type, $this->getCurrentPath()); } elseif ($format === TypeAbstract::FORMAT_DURATION) { return $visitor->visitDuration($data, $type, $this->getCurrentPath()); } elseif ($format === TypeAbstract::FORMAT_TIME) { return $visitor->visitTime($data, $type, $this->getCurrentPath()); } elseif ($format === TypeAbstract::FORMAT_URI) { return $visitor->visitUri($data, $type, $this->getCurrentPath()); } else { return $visitor->visitString($data, $type, $this->getCurrentPath()); } } protected function traverseIntersection( $data, IntersectionType $type, DefinitionsInterface $definitions, VisitorInterface $visitor, array $context, ) { $items = $type->getAllOf(); $count = count($items); $match = 0; $newType = new StructType(); foreach ($items as $index => $item) { $assertConstraints = $this->assertConstraints; try { $this->assertConstraints = true; if ($item instanceof ReferenceType) { $item = $definitions->getType($item->getRef()); } if ($item instanceof StructType) { foreach ($item->getProperties() as $name => $subType) { $newType->addProperty($name, $subType); } } else { throw new ValidationException($this->getCurrentPath() . ' must only contain struct types', 'allOf', $this->pathStack); } $match++; } catch (ValidationException $e) { } finally { $this->assertConstraints = $assertConstraints; } } if ($this->assertConstraints && $count !== $match) { throw new ValidationException($this->getCurrentPath() . ' must match all required schemas (matched only ' . $match . ' out of ' . $count . ')', 'allOf', $this->pathStack); } return $this->recTraverse($data, $newType, $definitions, $visitor, $context); } protected function traverseUnion( $data, UnionType $type, DefinitionsInterface $definitions, VisitorInterface $visitor, array $context, ) { $propertyName = $type->getPropertyName(); if (!empty($propertyName)) { return $this->traverseDiscriminatedUnion($data, $type, $definitions, $visitor, $context); } $items = $type->getOneOf(); $match = 0; $result = null; foreach ($items as $index => $item) { $assertConstraints = $this->assertConstraints; try { $this->assertConstraints = true; $result = $this->recTraverse($data, $item, $definitions, $visitor, $context); $match++; } catch (ValidationException $e) { } finally { $this->assertConstraints = $assertConstraints; } } if ($this->assertConstraints && $match !== 1) { throw new ValidationException($this->getCurrentPath() . ' must match one required schema', 'oneOf', $this->pathStack); } return $result; } private function traverseDiscriminatedUnion( $data, UnionType $type, DefinitionsInterface $definitions, VisitorInterface $visitor, array $context, ) { if (!$data instanceof \stdClass) { throw new ValidationException($this->getCurrentPath() . ' discriminated union provided value must be an object', 'oneOf', $this->pathStack); } $key = $type->getPropertyName(); if (!isset($data->{$key})) { throw new ValidationException($this->getCurrentPath() . ' discriminated union object must have the property "' . $key . '"', 'oneOf', $this->pathStack); } $mapping = $type->getMapping(); if (!empty($mapping)) { if (!isset($mapping[$data->{$key}])) { throw new ValidationException($this->getCurrentPath() . ' discriminated union provided type "' . $data->{$key} . '" not available, use one of ' . implode(', ', array_keys($mapping)), 'oneOf', $this->pathStack); } $ref = $mapping[$data->{$key}]; } else { $ref = $data->{$key}; } $items = $type->getOneOf(); foreach ($items as $item) { if (!$item instanceof ReferenceType) { // must be a reference type continue; } if ($item->getRef() === $ref) { return $this->recTraverse($data, $item, $definitions, $visitor, $context); } } throw new ValidationException($this->getCurrentPath() . ' discriminated union could not match fitting type', 'oneOf', $this->pathStack); } /** * @param mixed $data * @param ScalarType $type * @throws ValidationException */ protected function assertScalarConstraints($data, ScalarType $type) { if (!is_scalar($data)) { throw new ValidationException($this->getCurrentPath() . ' must be of type scalar', 'type', $this->pathStack); } $format = $type->getFormat(); if ($format !== null && is_string($data)) { if ($format === TypeAbstract::FORMAT_BINARY) { if (!preg_match('~^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$~', $data)) { throw new ValidationException($this->getCurrentPath() . ' must be a valid Base64 encoded string [RFC4648]', 'format', $this->pathStack); } } elseif ($format === TypeAbstract::FORMAT_DATETIME) { if (!preg_match('/^' . DateTime::getPattern() . '$/', $data)) { throw new ValidationException($this->getCurrentPath() . ' must be a valid date-time format [RFC3339]', 'format', $this->pathStack); } } elseif ($format === TypeAbstract::FORMAT_DATE) { if (!preg_match('/^' . Date::getPattern() . '$/', $data)) { throw new ValidationException($this->getCurrentPath() . ' must be a valid full-date format [RFC3339]', 'format', $this->pathStack); } } elseif ($format === TypeAbstract::FORMAT_DURATION) { if (!preg_match('/^' . Duration::getPattern() . '$/', $data)) { throw new ValidationException($this->getCurrentPath() . ' must be a valid duration format [ISO8601]', 'format', $this->pathStack); } } elseif ($format === TypeAbstract::FORMAT_TIME) { if (!preg_match('/^' . Time::getPattern() . '$/', $data)) { throw new ValidationException($this->getCurrentPath() . ' must be a valid full-time format [RFC3339]', 'format', $this->pathStack); } } elseif ($format === 'email') { if (!filter_var($data, FILTER_VALIDATE_EMAIL)) { throw new ValidationException($this->getCurrentPath() . ' must contain a valid email address', 'format', $this->pathStack); } } elseif ($format === 'ipv4') { if (!filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { throw new ValidationException($this->getCurrentPath() . ' must contain a valid IPv4 address', 'format', $this->pathStack); } } elseif ($format === 'ipv6') { if (!filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { throw new ValidationException($this->getCurrentPath() . ' must contain a valid IPv6 address', 'format', $this->pathStack); } } } $enum = $type->getEnum(); if ($enum !== null) { if (!in_array($data, $enum, true)) { throw new ValidationException($this->getCurrentPath() . ' is not in enumeration ' . json_encode($enum), 'enum', $this->pathStack); } } $const = $type->getConst(); if ($const !== null) { if ($const !== $data) { throw new ValidationException($this->getCurrentPath() . ' must contain the constant value ' . json_encode($const), 'const', $this->pathStack); } } } /** * @param mixed $data * @param StructType $type * @throws ValidationException */ protected function assertStructConstraints($data, StructType $type) { if (!$data instanceof \stdClass) { throw new ValidationException($this->getCurrentPath() . ' must be of type object', 'type', $this->pathStack); } $keys = array_keys(get_object_vars($data)); $required = $type->getRequired(); if ($required !== null) { $diff = array_diff($required, $keys); if (count($diff) > 0) { throw new ValidationException($this->getCurrentPath() . ' the following properties are required: ' . implode(', ', $diff), 'required', $this->pathStack); } } } /** * @param mixed $data * @param MapType $type * @throws ValidationException */ protected function assertMapConstraints($data, MapType $type) { if (!$data instanceof \stdClass) { throw new ValidationException($this->getCurrentPath() . ' must be of type object', 'type', $this->pathStack); } $keys = array_keys(get_object_vars($data)); $minProperties = $type->getMinProperties(); if ($minProperties !== null) { if (count($keys) < $minProperties) { throw new ValidationException($this->getCurrentPath() . ' must contain more or equal than ' . $minProperties . ' properties', 'minProperties', $this->pathStack); } } $maxProperties = $type->getMaxProperties(); if ($maxProperties !== null) { if (count($keys) > $maxProperties) { throw new ValidationException($this->getCurrentPath() . ' must contain less or equal than ' . $maxProperties . ' properties', 'maxProperties', $this->pathStack); } } } /** * @param mixed $data * @param ArrayType $type * @throws ValidationException */ protected function assertArrayConstraints($data, ArrayType $type) { if (!is_array($data)) { throw new ValidationException($this->getCurrentPath() . ' must be of type array', 'type', $this->pathStack); } $minItems = $type->getMinItems(); if ($minItems !== null) { if (count($data) < $minItems) { throw new ValidationException($this->getCurrentPath() . ' must contain more or equal than ' . $minItems . ' items', 'minItems', $this->pathStack); } } $maxItems = $type->getMaxItems(); if ($maxItems !== null) { if (count($data) > $maxItems) { throw new ValidationException($this->getCurrentPath() . ' must contain less or equal than ' . $maxItems . ' items', 'maxItems', $this->pathStack); } } } /** * @param mixed $data * @param NumberType $property * @throws ValidationException */ protected function assertNumberConstraints($data, NumberType $property) { if ($property instanceof IntegerType) { if (!is_int($data)) { throw new ValidationException($this->getCurrentPath() . ' must be of type integer', 'type', $this->pathStack); } } else { if (!is_float($data) && !is_int($data)) { throw new ValidationException($this->getCurrentPath() . ' must be of type float', 'type', $this->pathStack); } } $maximum = $property->getMaximum(); if ($maximum !== null) { if ($property->getExclusiveMaximum()) { if ($data >= $maximum) { throw new ValidationException($this->getCurrentPath() . ' must be lower than ' . $maximum, 'maximum', $this->pathStack); } } else { if ($data > $maximum) { throw new ValidationException($this->getCurrentPath() . ' must be lower or equal than ' . $maximum, 'maximum', $this->pathStack); } } } $minimum = $property->getMinimum(); if ($minimum !== null) { if ($property->getExclusiveMinimum()) { if ($data <= $minimum) { throw new ValidationException($this->getCurrentPath() . ' must be greater than ' . $minimum, 'minimum', $this->pathStack); } } else { if ($data < $minimum) { throw new ValidationException($this->getCurrentPath() . ' must be greater or equal than ' . $minimum, 'minimum', $this->pathStack); } } } $multipleOf = $property->getMultipleOf(); if ($multipleOf !== null) { $result = $data / $multipleOf; $base = (int) $result; // its important to make a loose comparison if ($data > 0 && $result - $base != 0) { throw new ValidationException($this->getCurrentPath() . ' must be a multiple of ' . $multipleOf, 'multipleOf', $this->pathStack); } } } /** * @param mixed $data * @param BooleanType $type * @throws ValidationException */ protected function assertBooleanConstraints($data, BooleanType $type) { if (!is_bool($data)) { throw new ValidationException($this->getCurrentPath() . ' must be of type boolean', 'type', $this->pathStack); } } /** * @param mixed $data * @param StringType $property * @throws ValidationException */ protected function assertStringConstraints($data, StringType $property) { if (!is_string($data)) { throw new ValidationException($this->getCurrentPath() . ' must be of type string', 'type', $this->pathStack); } $minLength = $property->getMinLength(); if ($minLength !== null) { if (strlen($data) < $minLength) { throw new ValidationException($this->getCurrentPath() . ' must contain more or equal than ' . $minLength . ' characters', 'minLength', $this->pathStack); } } $maxLength = $property->getMaxLength(); if ($maxLength !== null) { if (strlen($data) > $maxLength) { throw new ValidationException($this->getCurrentPath() . ' must contain less or equal than ' . $maxLength . ' characters', 'maxLength', $this->pathStack); } } $pattern = $property->getPattern(); if ($pattern !== null) { $result = preg_match('/' . $pattern . '/', $data); if (!$result) { throw new ValidationException($this->getCurrentPath() . ' does not match pattern [' . $pattern . ']', 'pattern', $this->pathStack); } } } private function getCurrentPath() { return '/' . implode('/', $this->pathStack); } } __halt_compiler();----SIGNATURE:----an43bok0GDVB4Jodo8XbJjX/29Kwer+GfSapFGn99mBI07v+ebGZJtfZByj9m/hQY3xahs3w5lJAM7mSRD5X0zEPlsoHynde974ZL9q3l5msFQ4vYUscyi0vIJ68dXb+F+Wk7ts4wxI/VdTn00XFc3V47KvGv4d/b+fUb3WqH35e/CxjLbhKSMlExKP05vrAa9V0bDx2lek5Oc1GGm0T5092HqOUllQZsrjixAn+0vFf2ddAM+nDGQDPvXXpeZdrGFOwC5WVQNMaurCT0C8ZOWDjk8YIjYbmTmv8gvPzmK63n/JLLPswg5ErcS+woKfn6ILnSMdpnWDeofLQOHReiU+9Dge553U7GGCyOG/gbIh/S1FfnYldwbwpSsIe0yF+OuG+GFAxEeviVG90jKEXGVjOkYpU9kG8+Agjg0E6Nu9Bl5YC7Uds24zf6rCe5LezybbDV0NcaPdydSI496L3giLb5ojB2BOPB/MOhUPmDLpcXGvuECuTGmjFDLeZJPs/NMz6jQGgcoCwvn8uvhpRU2LBoLa8Tp2oBff3DCIgsYlm6T6GXPvXUL3gtf7PUkCIhJGCT1yDp4mGaUg3i6VtmyR93dtI8jMuSLZYIKqLRXZXi3STnR18DSIzntCFfVPBgH5LOC7h233J2EuNpCWrXNozbopBtmGDCl5nYYbVBpY=----ATTACHMENT:----ODM3MjMwMDEwOTE0MDIyMyAzNTU0Mjg5ODQ5MTE0NjQxIDgwODc1NzQ1NDU4NzA0NDY=