diff --git a/api/index.html b/api/index.html new file mode 100644 index 000000000..30e84fdd6 --- /dev/null +++ b/api/index.html @@ -0,0 +1 @@ + diff --git a/index.php b/index.php index 3ad48423d..8e98e09d2 100755 --- a/index.php +++ b/index.php @@ -58,8 +58,4 @@ $pageParams["accountType"] = ""; } -//$dataTypes = DataTypePluginHelper::getDataTypeList(Core::$dataTypePlugins); -//$schemaFiles = DataTypePluginHelper::getSchemaFiles($dataTypes); -//print_r($schemaFiles); - Templates::displayPage("resources/templates/index.tpl", $pageParams); diff --git a/library.php b/library.php index ffd873454..fc7a692df 100755 --- a/library.php +++ b/library.php @@ -40,7 +40,7 @@ require_once(__DIR__ . "/resources/classes/Utils.class.php"); // External libs -require_once(__DIR__ . "/resources/libs/json_validator/Validator.php"); +require_once(__DIR__ . "/resources/libs/jsv4.php"); require_once(__DIR__ . "/resources/libs/smarty/Smarty.class.php"); require_once(__DIR__ . "/resources/classes/SecureSmarty.class.php"); diff --git a/resources/classes/API.class.php b/resources/classes/API.class.php index dc45a3cd7..e5943f524 100644 --- a/resources/classes/API.class.php +++ b/resources/classes/API.class.php @@ -1,7 +1,5 @@ settings; + $result = Jsv4::validate($json, $schema); - //$result = $validator->validate($rows[$i]->settings); - return json_decode($schemaFiles[$dataType]); // $schemaFiles[$dataType]; + return json_decode($result); } } } diff --git a/resources/libs/json_validator/Validator.php b/resources/libs/json_validator/Validator.php deleted file mode 100644 index 903d29dba..000000000 --- a/resources/libs/json_validator/Validator.php +++ /dev/null @@ -1,765 +0,0 @@ - - * @version 0.1 - */ -class Validator -{ - protected $schemaDefinition; - - /** - * @var stdClass - */ - protected $schema; - - /** - * Initialize validation object. - * - * @param string $schema - * @param string $content "file", if the first param passed is a file location; "json" if it's raw JSON content - * @throws SchemaException - */ - public function __construct($schema, $content = "file") - { - if ($content === "file") { - if (!file_exists($schema)) { - throw new SchemaException(sprintf('Schema file not found: [%s]', $schema)); - } - $data = file_get_contents($schema); - } else if ($content === "json") { - $data = $schema; - } - - $this->schema = json_decode($data); - if ($this->schema === null) { - throw new SchemaException('Unable to parse JSON data - syntax error?'); - } - - // @TODO - validate schema itself - } - - /** - * Validate schema object - * - * @param mixed $entity - * @param string $entityName - * - * @return Validator - */ - public function validate($entity, $entityName = null) - { - $entityName = $entityName ?: 'root'; - - // Validate root type - $this->validateType($entity, $this->schema, $entityName); - - return $this; - } - - /** - * Check format restriction - * - * @param mixed $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - public function checkFormat($entity, $schema, $entityName) - { - if (!isset($schema->format)) { - return $this; - } - - $valid = true; - switch ($schema->format) { - case 'date-time': - if (!preg_match('#^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$#', $entity)) { - $valid = false; - } - break; - case 'date': - if (!preg_match('#^\d{4}-\d{2}-\d{2}$#', $entity)) { - $valid = false; - } - break; - case 'time': - if (!preg_match('#^\d{2}:\d{2}:\d{2}$#', $entity)) { - $valid = false; - } - break; - case 'utc-millisec': - if ($entity < 0) { - $valid = false; - } - break; - case 'color': - if (!in_array($entity, array('maroon', 'red', 'orange', - 'yellow', 'olive', 'green', 'purple', 'fuchsia', 'lime', - 'teal', 'aqua', 'blue', 'navy', 'black', 'gray', 'silver', 'white'))) { - if (!preg_match('#^\#[0-9A-F]{6}$#', $entity) && !preg_match('#^\#[0-9A-F]{3}$#', $entity)) { - $valid = false; - } - } - break; - case 'style': - if (!preg_match('#(\.*?)[ ]?:[ ]?(.*?)#', $entity)) { - $valid = false; - } - break; - case 'phone': - if (!preg_match('#^[0-9\-+ \(\)]*$#', $entity)) { - $valid = false; - } - break; - case 'uri': - if (!preg_match('#^[A-Za-z0-9:/;,\-_\?&\.%\+\|\#=]*$#', $entity)) { - $valid = false; - } - break; - } - - if (!$valid) { - throw new ValidationException(sprintf('Value for [%s] must match format [%s]', $entityName, $schema->format)); - } - - return $this; - } - - /** - * Validate object properties - * - * @param object $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function validateProperties($entity, $schema, $entityName) - { - $properties = get_object_vars($entity); - - if (!isset($schema->properties)) { - return $this; - //throw new SchemaException(sprintf('No properties defined for [%s]', $entityName)); - } - - // Check defined properties - foreach($schema->properties as $propertyName => $property) { - if (array_key_exists($propertyName, $properties)) { - // Check type - $path = $entityName . '.' . $propertyName; - $this->validateType($entity->{$propertyName}, $property, $path); - } else { - // Check required - if (isset($property->required) && $property->required) { - throw new ValidationException(sprintf('Missing required property [%s] for [%s]', $propertyName, $entityName)); - } - } - } - - // Check additional properties - if (isset($schema->additionalProperties) && !$schema->additionalProperties) { - $extra = array_diff(array_keys((array)$entity), array_keys((array)$schema->properties)); - if (count($extra)) { - throw new ValidationException(sprintf('Additional properties [%s] not allowed for property [%s]', implode(',', $extra), $entityName)); - } - } - - return $this; - } - - /** - * Validate entity type - * - * @param mixed $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function validateType($entity, $schema, $entityName) - { - if (isset($schema->type)) { - $types = $schema->type; - } else { - $types = 'any'; - //throw new ValidationException(sprintf('No type given for [%s]', $entityName)); - } - - if (!is_array($types)) { - $types = array($types); - } - - $valid = false; - - foreach ($types as $type) { - switch ($type) { - case 'object': - if (is_object($entity)) { - $this->checkTypeObject($entity, $schema, $entityName); - $valid = true; - } - break; - case 'string': - if (is_string($entity)) { - $this->checkTypeString($entity, $schema, $entityName); - $valid = true; - } - break; - case 'array': - if (is_array($entity)) { - $this->checkTypeArray($entity, $schema, $entityName); - $valid = true; - } - break; - case 'integer': - if (!is_string($entity) && is_int($entity)) { - $this->checkTypeInteger($entity, $schema, $entityName); - $valid = true; - } - break; - case 'number': - if (!is_string($entity) && is_numeric($entity)) { - $this->checkTypeNumber($entity, $schema, $entityName); - $valid = true; - } - break; - case 'boolean': - if (!is_string($entity) && is_bool($entity)) { - $this->checkTypeBoolean($entity, $schema, $entityName); - $valid = true; - } - break; - case 'null': - if (is_null($entity)) { - $this->checkTypeNull($entity, $schema, $entityName); - $valid = true; - } - break; - case 'any': - $this->checkTypeAny($entity, $schema, $entityName); - $valid = true; - break; - default: - // Do nothing - $valid = true; - break; - } - } - - if (!$valid) { - throw new ValidationException(sprintf('Property [%s] must be one of the following types: [%s]', $entityName, implode(', ', $types))); - } - - return $this; - } - - - /** - * Check object type - * - * @param mixed $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkTypeObject($entity, $schema, $entityName) - { - $this->validateProperties($entity, $schema, $entityName); - - return $this; - } - - /** - * Check number type - * - * @param mixed $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkTypeNumber($entity, $schema, $entityName) - { - $this->checkMinimum($entity, $schema, $entityName); - $this->checkMaximum($entity, $schema, $entityName); - $this->checkExclusiveMinimum($entity, $schema, $entityName); - $this->checkExclusiveMaximum($entity, $schema, $entityName); - $this->checkFormat($entity, $schema, $entityName); - $this->checkEnum($entity, $schema, $entityName); - $this->checkDisallow($entity, $schema, $entityName); - $this->checkDivisibleBy($entity, $schema, $entityName); - - return $this; - } - - /** - * Check integer type - * - * @param mixed $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkTypeInteger($entity, $schema, $entityName) - { - $this->checkMinimum($entity, $schema, $entityName); - $this->checkMaximum($entity, $schema, $entityName); - $this->checkExclusiveMinimum($entity, $schema, $entityName); - $this->checkExclusiveMaximum($entity, $schema, $entityName); - $this->checkFormat($entity, $schema, $entityName); - $this->checkEnum($entity, $schema, $entityName); - $this->checkDisallow($entity, $schema, $entityName); - $this->checkDivisibleBy($entity, $schema, $entityName); - - return $this; - } - - /** - * Check boolean type - * - * @param mixed $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkTypeBoolean($entity, $schema, $entityName) - { - return $this; - } - - /** - * Check string type - * - * @param mixed $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkTypeString($entity, $schema, $entityName) - { - $this->checkPattern($entity, $schema, $entityName); - $this->checkMinLength($entity, $schema, $entityName); - $this->checkMaxLength($entity, $schema, $entityName); - $this->checkFormat($entity, $schema, $entityName); - $this->checkEnum($entity, $schema, $entityName); - $this->checkDisallow($entity, $schema, $entityName); - - return $this; - } - - /** - * Check array type - * - * @param mixed $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkTypeArray($entity, $schema, $entityName) - { - $this->checkMinItems($entity, $schema, $entityName); - $this->checkMaxItems($entity, $schema, $entityName); - $this->checkUniqueItems($entity, $schema, $entityName); - $this->checkEnum($entity, $schema, $entityName); - $this->checkItems($entity, $schema, $entityName); - $this->checkDisallow($entity, $schema, $entityName); - - return $this; - } - - /** - * Check null type - * - * @param mixed $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkTypeNull($entity, $schema, $entityName) - { - return $this; - } - - /** - * Check any type - * - * @param mixed $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkTypeAny($entity, $schema, $entityName) - { - $this->checkDisallow($entity, $schema, $entityName); - - return $this; - } - - /** - * Check minimum value - * - * @param int|float $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkMinimum($entity, $schema, $entityName) - { - if (isset($schema->minimum)) { - if ($entity < $schema->minimum) { - throw new ValidationException(sprintf('Invalid value for [%s], minimum is [%s]', $entityName, $schema->minimum)); - } - } - - return $this; - } - - /** - * Check maximum value - * - * @param int|float $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkMaximum($entity, $schema, $entityName) - { - if (isset($schema->maximum)) { - if ($entity > $schema->maximum) { - throw new ValidationException(sprintf('Invalid value for [%s], maximum is [%s]', $entityName, $schema->maximum)); - } - } - - return $this; - } - - /** - * Check exlusive minimum requirement - * - * @param int|float $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkExclusiveMinimum($entity, $schema, $entityName) - { - if (isset($schema->minimum) && isset($schema->exclusiveMinimum) && $schema->exclusiveMinimum) { - if ($entity == $schema->minimum) { - throw new ValidationException(sprintf('Invalid value for [%s], must be greater than [%s]', $entityName, $schema->minimum)); - } - } - - return $this; - } - - /** - * Check exclusive maximum requirement - * - * @param int|float $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkExclusiveMaximum($entity, $schema, $entityName) - { - if (isset($schema->maximum) && isset($schema->exclusiveMaximum) && $schema->exclusiveMaximum) { - if ($entity == $schema->maximum) { - throw new ValidationException(sprintf('Invalid value for [%s], must be less than [%s]', $entityName, $schema->maximum)); - } - } - - return $this; - } - - /** - * Check value against regex pattern - * - * @param string $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkPattern($entity, $schema, $entityName) - { - if (isset($schema->pattern) && $schema->pattern) { - if (!preg_match($schema->pattern, $entity)) { - throw new ValidationException(sprintf('String does not match pattern for [%s]', $entityName)); - } - } - - return $this; - } - - /** - * Check string minimum length - * - * @param string $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkMinLength($entity, $schema, $entityName) - { - if (isset($schema->minLength) && $schema->minLength) { - if (strlen($entity) < $schema->minLength) { - throw new ValidationException(sprintf('String too short for [%s], minimum length is [%s]', $entityName, $schema->minLength)); - } - } - - return $this; - } - - /** - * Check string maximum length - * - * @param string $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkMaxLength($entity, $schema, $entityName) - { - if (isset($schema->maxLength) && $schema->maxLength) { - if (strlen($entity) > $schema->maxLength) { - throw new ValidationException(sprintf('String too long for [%s], maximum length is [%s]', $entityName, $schema->maxLength)); - } - } - - return $this; - } - - /** - * Check array minimum items - * - * @param array $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkMinItems($entity, $schema, $entityName) - { - if (isset($schema->minItems) && $schema->minItems) { - if (count($entity) < $schema->minItems) { - throw new ValidationException(sprintf('Not enough array items for [%s], minimum is [%s]', $entityName, $schema->minItems)); - } - } - - return $this; - } - - /** - * Check array maximum items - * - * @param array $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkMaxItems($entity, $schema, $entityName) - { - if (isset($schema->maxItems) && $schema->maxItems) { - if (count($entity) > $schema->maxItems) { - throw new ValidationException(sprintf('Too many array items for [%s], maximum is [%s]', $entityName, $schema->maxItems)); - } - } - - return $this; - } - - /** - * Check array unique items - * - * @param array $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkUniqueItems($entity, $schema, $entityName) - { - if (isset($schema->uniqueItems) && $schema->uniqueItems) { - if (count(array_unique($entity)) != count($entity)) { - throw new ValidationException(sprintf('All items in array [%s] must be unique', $entityName)); - } - } - - return $this; - } - - /** - * Check enum restriction - * - * @param array $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkEnum($entity, $schema, $entityName) - { - $valid = true; - if (isset($schema->enum) && $schema->enum) { - if (!is_array($schema->enum)) { - throw new SchemaException(sprintf('Enum property must be an array for [%s]', $entityName)); - } - if (is_array($entity)) { - foreach ($entity as $val) { - if (!in_array($val, $schema->enum)) { - $valid = false; - } - } - } else { - if (!in_array($entity, $schema->enum)) { - $valid = false; - } - } - } - - if (!$valid) { - throw new ValidationException(sprintf('Invalid value(s) for [%s], allowable values are [%s]', $entityName, implode(',', $schema->enum))); - } - - return $this; - } - - /** - * Check items restriction - * - * @param array $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkItems($entity, $schema, $entityName) - { - if (isset($schema->items) && $schema->items) { - // Item restriction is an array of schemas - if (is_array($schema->items)) { - foreach($entity as $index => $node) { - $nodeEntityName = $entityName . '[' . $index . ']'; - - // Check if the item passes any of the item validations - foreach($schema->items as $item) { - $nodeValid = true; - try { - $this->validateType($node, $item, $nodeEntityName); - // Pass - break; - } catch (ValidationException $e) { - $nodeValid = false; - } - } - - // If item did not pass any item validations - if (!$nodeValid) { - $allowedTypes = array_map(function($item){ - return $item->type == 'object' ? 'object (schema)' : $item->type; - }, $schema->items); - throw new ValidationException(sprintf('Invalid value for [%s], must be one of the following types: [%s]', - $nodeEntityName, implode(', ' , $allowedTypes))); - } - } - // Item restriction is a single schema - } else if (is_object($schema->items)) { - foreach($entity as $index => $node) { - $nodeEntityName = $entityName . '[' . $index . ']'; - $this->validateType($node, $schema->items, $nodeEntityName); - } - - } else { - throw new SchemaException(sprintf('Invalid items value for [%s]', $entityName)); - } - } - - return $this; - } - - /** - * Check disallowed entity type - * - * @param mixed $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkDisallow($entity, $schema, $entityName) - { - if (isset($schema->disallow) && $schema->disallow) { - $thisSchema = clone $schema; - $thisSchema->type = $schema->disallow; - unset($thisSchema->disallow); - - // We are expecting an exception - if one is not thrown, - // then we have a matching disallowed type - try { - $valid = false; - $this->validateType($entity, $thisSchema, $entityName); - } catch (ValidationException $e) { - $valid = true; - } - if (!$valid) { - $disallowedTypes = array_map(function($item){ - return is_object($item) ? 'object (schema)' : $item; - }, is_array($schema->disallow) ? $schema->disallow : array($schema->disallow)); - throw new ValidationException(sprintf('Invalid value for [%s], disallowed types are [%s]', - $entityName, implode(', ', $disallowedTypes))); - } - } - - return $this; - } - - /** - * Check divisibleby restriction - * - * @param int|float $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - protected function checkDivisibleBy($entity, $schema, $entityName) - { - if (isset($schema->divisibleBy) && $schema->divisibleBy) { - if (!is_numeric($schema->divisibleBy)) { - throw new SchemaException(sprintf('Invalid divisibleBy value for [%s], must be numeric', $entityName)); - } - - if ($entity % $schema->divisibleBy != 0) { - throw new ValidationException(sprintf('Invalid value for [%s], must be divisible by [%d]', $entityName, $schema->divisibleBy)); - } - } - - return $this; - } -} diff --git a/resources/libs/jsv4.php b/resources/libs/jsv4.php new file mode 100644 index 000000000..53eef1421 --- /dev/null +++ b/resources/libs/jsv4.php @@ -0,0 +1,553 @@ +valid; + } + + static public function coerce($data, $schema) { + if (is_object($data) || is_array($data)) { + $data = unserialize(serialize($data)); + } + $result = new Jsv4($data, $schema, FALSE, TRUE); + if ($result->valid) { + $result->value = $result->data; + } + return $result; + } + + static public function pointerJoin($parts) { + $result = ""; + foreach ($parts as $part) { + $part = str_replace("~", "~0", $part); + $part = str_replace("/", "~1", $part); + $result .= "/".$part; + } + return $result; + } + + static public function recursiveEqual($a, $b) { + if (is_object($a)) { + if (!is_object($b)) { + return FALSE; + } + foreach ($a as $key => $value) { + if (!isset($b->$key)) { + return FALSE; + } + if (!self::recursiveEqual($value, $b->$key)) { + return FALSE; + } + } + foreach ($b as $key => $value) { + if (!isset($a->$key)) { + return FALSE; + } + } + return TRUE; + } + if (is_array($a)) { + if (!is_array($b)) { + return FALSE; + } + foreach ($a as $key => $value) { + if (!isset($b[$key])) { + return FALSE; + } + if (!self::recursiveEqual($value, $b[$key])) { + return FALSE; + } + } + foreach ($b as $key => $value) { + if (!isset($a[$key])) { + return FALSE; + } + } + return TRUE; + } + return $a === $b; + } + + + private $data; + private $schema; + private $firstErrorOnly; + private $coerce; + public $valid; + public $errors; + + private function __construct(&$data, $schema, $firstErrorOnly=FALSE, $coerce=FALSE) { + $this->data =& $data; + $this->schema =& $schema; + $this->firstErrorOnly = $firstErrorOnly; + $this->coerce = $coerce; + $this->valid = TRUE; + $this->errors = array(); + + try { + $this->checkTypes(); + $this->checkEnum(); + $this->checkObject(); + $this->checkArray(); + $this->checkString(); + $this->checkNumber(); + $this->checkComposite(); + } catch (Jsv4Error $e) { + } + } + + private function fail($code, $dataPath, $schemaPath, $errorMessage, $subErrors=NULL) { + $this->valid = FALSE; + $error = new Jsv4Error($code, $dataPath, $schemaPath, $errorMessage, $subErrors); + $this->errors[] = $error; + if ($this->firstErrorOnly) { + throw $error; + } + } + + private function subResult(&$data, $schema, $allowCoercion=TRUE) { + return new Jsv4($data, $schema, $this->firstErrorOnly, $allowCoercion && $this->coerce); + } + + private function includeSubResult($subResult, $dataPrefix, $schemaPrefix) { + if (!$subResult->valid) { + $this->valid = FALSE; + foreach ($subResult->errors as $error) { + $this->errors[] = $error->prefix($dataPrefix, $schemaPrefix); + } + } + } + + private function checkTypes() { + if (isset($this->schema->type)) { + $types = $this->schema->type; + if (!is_array($types)) { + $types = array($types); + } + foreach ($types as $type) { + if ($type == "object" && is_object($this->data)) { + return; + } elseif ($type == "array" && is_array($this->data)) { + return; + } elseif ($type == "string" && is_string($this->data)) { + return; + } elseif ($type == "number" && !is_string($this->data) && is_numeric($this->data)) { + return; + } elseif ($type == "integer" && is_int($this->data)) { + return; + } elseif ($type == "boolean" && is_bool($this->data)) { + return; + } elseif ($type == "null" && $this->data === NULL) { + return; + } + } + + if ($this->coerce) { + foreach ($types as $type) { + if ($type == "number") { + if (is_numeric($this->data)) { + $this->data = (float)$this->data; + return; + } else if (is_bool($this->data)) { + $this->data = $this->data ? 1 : 0; + return; + } + } else if ($type == "integer") { + if ((int)$this->data == $this->data) { + $this->data = (int)$this->data; + return; + } + } else if ($type == "string") { + if (is_numeric($this->data)) { + $this->data = "".$this->data; + return; + } else if (is_bool($this->data)) { + $this->data = ($this->data) ? "true" : "false"; + return; + } else if (is_null($this->data)) { + $this->data = ""; + return; + } + } else if ($type == "boolean") { + if (is_numeric($this->data)) { + $this->data = ($this->data != "0"); + return; + } else if ($this->data == "yes" || $this->data == "true") { + $this->data = TRUE; + return; + } else if ($this->data == "no" || $this->data == "false") { + $this->data = FALSE; + return; + } else if ($this->data == NULL) { + $this->data = FALSE; + return; + } + } + } + } + + $type = gettype($this->data); + if ($type == "double") { + $type = ((int)$this->data == $this->data) ? "integer" : "number"; + } else if ($type == "NULL") { + $type = "null"; + } + $this->fail(JSV4_INVALID_TYPE, "", "/type", "Invalid type: $type"); + } + } + + private function checkEnum() { + if (isset($this->schema->enum)) { + foreach ($this->schema->enum as $option) { + if (self::recursiveEqual($this->data, $option)) { + return; + } + } + $this->fail(JSV4_ENUM_MISMATCH, "", "/enum", "Value must be one of the enum options"); + } + } + + private function checkObject() { + if (!is_object($this->data)) { + return; + } + if (isset($this->schema->required)) { + foreach ($this->schema->required as $index => $key) { + if (!array_key_exists($key, (array) $this->data)) { + if ($this->coerce && $this->createValueForProperty($key)) { + continue; + } + $this->fail(JSV4_OBJECT_REQUIRED, "", "/required/{$index}", "Missing required property: {$key}"); + } + } + } + $checkedProperties = array(); + if (isset($this->schema->properties)) { + foreach ($this->schema->properties as $key => $subSchema) { + $checkedProperties[$key] = TRUE; + if (array_key_exists($key, (array) $this->data)) { + $subResult = $this->subResult($this->data->$key, $subSchema); + $this->includeSubResult($subResult, self::pointerJoin(array($key)), self::pointerJoin(array("properties", $key))); + } + } + } + if (isset($this->schema->patternProperties)) { + foreach ($this->schema->patternProperties as $pattern => $subSchema) { + foreach ($this->data as $key => &$subValue) { + if (preg_match("/".str_replace("/", "\\/", $pattern)."/", $key)) { + $checkedProperties[$key] = TRUE; + $subResult = $this->subResult($this->data->$key, $subSchema); + $this->includeSubResult($subResult, self::pointerJoin(array($key)), self::pointerJoin(array("patternProperties", $pattern))); + } + } + } + } + if (isset($this->schema->additionalProperties)) { + $additionalProperties = $this->schema->additionalProperties; + foreach ($this->data as $key => &$subValue) { + if (isset($checkedProperties[$key])) { + continue; + } + if (!$additionalProperties) { + $this->fail(JSV4_OBJECT_ADDITIONAL_PROPERTIES, self::pointerJoin(array($key)), "/additionalProperties", "Additional properties not allowed"); + } else if (is_object($additionalProperties)) { + $subResult = $this->subResult($subValue, $additionalProperties); + $this->includeSubResult($subResult, self::pointerJoin(array($key)), "/additionalProperties"); + } + } + } + if (isset($this->schema->dependencies)) { + foreach ($this->schema->dependencies as $key => $dep) { + if (!isset($this->data->$key)) { + continue; + } + if (is_object($dep)) { + $subResult = $this->subResult($this->data, $dep); + $this->includeSubResult($subResult, "", self::pointerJoin(array("dependencies", $key))); + } else if (is_array($dep)) { + foreach ($dep as $index => $depKey) { + if (!isset($this->data->$depKey)) { + $this->fail(JSV4_OBJECT_DEPENDENCY_KEY, "", self::pointerJoin(array("dependencies", $key, $index)), "Property $key depends on $depKey"); + } + } + } else { + if (!isset($this->data->$dep)) { + $this->fail(JSV4_OBJECT_DEPENDENCY_KEY, "", self::pointerJoin(array("dependencies", $key)), "Property $key depends on $dep"); + } + } + } + } + if (isset($this->schema->minProperties)) { + if (count(get_object_vars($this->data)) < $this->schema->minProperties) { + $this->fail(JSV4_OBJECT_PROPERTIES_MINIMUM, "", "/minProperties", ($this->schema->minProperties == 1) ? "Object cannot be empty" : "Object must have at least {$this->schema->minProperties} defined properties"); + } + } + if (isset($this->schema->maxProperties)) { + if (count(get_object_vars($this->data)) > $this->schema->maxProperties) { + $this->fail(JSV4_OBJECT_PROPERTIES_MAXIMUM, "", "/minProperties", ($this->schema->maxProperties == 1) ? "Object must have at most one defined property" : "Object must have at most {$this->schema->maxProperties} defined properties"); + } + } + } + + private function checkArray() { + if (!is_array($this->data)) { + return; + } + if (isset($this->schema->items)) { + $items = $this->schema->items; + if (is_array($items)) { + foreach ($this->data as $index => &$subData) { + if (!is_numeric($index)) { + throw new Exception("Arrays must only be numerically-indexed"); + } + if (isset($items[$index])) { + $subResult = $this->subResult($subData, $items[$index]); + $this->includeSubResult($subResult, "/{$index}", "/items/{$index}"); + } else if (isset($this->schema->additionalItems)) { + $additionalItems = $this->schema->additionalItems; + if (!$additionalItems) { + $this->fail(JSV4_ARRAY_ADDITIONAL_ITEMS, "/{$index}", "/additionalItems", "Additional items (index ".count($items)." or more) are not allowed"); + } else if ($additionalItems !== TRUE) { + $subResult = $this->subResult($subData, $additionalItems); + $this->includeSubResult($subResult, "/{$index}", "/additionalItems"); + } + } + } + } else { + foreach ($this->data as $index => &$subData) { + if (!is_numeric($index)) { + throw new Exception("Arrays must only be numerically-indexed"); + } + $subResult = $this->subResult($subData, $items); + $this->includeSubResult($subResult, "/{$index}", "/items"); + } + } + } + if (isset($this->schema->minItems)) { + if (count($this->data) < $this->schema->minItems) { + $this->fail(JSV4_ARRAY_LENGTH_SHORT, "", "/minItems", "Array is too short (must have at least {$this->schema->minItems} items)"); + } + } + if (isset($this->schema->maxItems)) { + if (count($this->data) > $this->schema->maxItems) { + $this->fail(JSV4_ARRAY_LENGTH_LONG, "", "/maxItems", "Array is too long (must have at most {$this->schema->maxItems} items)"); + } + } + if (isset($this->schema->uniqueItems)) { + foreach ($this->data as $indexA => $itemA) { + foreach ($this->data as $indexB => $itemB) { + if ($indexA < $indexB) { + if (self::recursiveEqual($itemA, $itemB)) { + $this->fail(JSV4_ARRAY_UNIQUE, "", "/uniqueItems", "Array items must be unique (items $indexA and $indexB)"); + break 2; + } + } + } + } + } + } + + private function checkString() { + if (!is_string($this->data)) { + return; + } + if (isset($this->schema->minLength)) { + if (strlen($this->data) < $this->schema->minLength) { + $this->fail(JSV4_STRING_LENGTH_SHORT, "", "/minLength", "String must be at least {$this->schema->minLength} characters long"); + } + } + if (isset($this->schema->maxLength)) { + if (strlen($this->data) > $this->schema->maxLength) { + $this->fail(JSV4_STRING_LENGTH_LONG, "", "/maxLength", "String must be at most {$this->schema->maxLength} characters long"); + } + } + if (isset($this->schema->pattern)) { + $pattern = $this->schema->pattern; + $patternFlags = isset($this->schema->patternFlags) ? $this->schema->patternFlags : ''; + $result = preg_match("/".str_replace("/", "\\/", $pattern)."/".$patternFlags, $this->data); + if ($result === 0) { + $this->fail(JSV4_STRING_PATTERN, "", "/pattern", "String does not match pattern: $pattern"); + } + } + } + + private function checkNumber() { + if (is_string($this->data) || !is_numeric($this->data)) { + return; + } + if (isset($this->schema->multipleOf)) { + if (fmod($this->data/$this->schema->multipleOf, 1) != 0) { + $this->fail(JSV4_NUMBER_MULTIPLE_OF, "", "/multipleOf", "Number must be a multiple of {$this->schema->multipleOf}"); + } + } + if (isset($this->schema->minimum)) { + $minimum = $this->schema->minimum; + if (isset($this->schema->exclusiveMinimum) && $this->schema->exclusiveMinimum) { + if ($this->data <= $minimum) { + $this->fail(JSV4_NUMBER_MINIMUM_EXCLUSIVE, "", "", "Number must be > $minimum"); + } + } else { + if ($this->data < $minimum) { + $this->fail(JSV4_NUMBER_MINIMUM, "", "/minimum", "Number must be >= $minimum"); + } + } + } + if (isset($this->schema->maximum)) { + $maximum = $this->schema->maximum; + if (isset($this->schema->exclusiveMaximum) && $this->schema->exclusiveMaximum) { + if ($this->data >= $maximum) { + $this->fail(JSV4_NUMBER_MAXIMUM_EXCLUSIVE, "", "", "Number must be < $maximum"); + } + } else { + if ($this->data > $maximum) { + $this->fail(JSV4_NUMBER_MAXIMUM, "", "/maximum", "Number must be <= $maximum"); + } + } + } + } + + private function checkComposite() { + if (isset($this->schema->allOf)) { + foreach ($this->schema->allOf as $index => $subSchema) { + $subResult = $this->subResult($this->data, $subSchema, FALSE); + $this->includeSubResult($subResult, "", "/allOf/".(int)$index); + } + } + if (isset($this->schema->anyOf)) { + $failResults = array(); + foreach ($this->schema->anyOf as $index => $subSchema) { + $subResult = $this->subResult($this->data, $subSchema, FALSE); + if ($subResult->valid) { + return; + } + $failResults[] = $subResult; + } + $this->fail(JSV4_ANY_OF_MISSING, "", "/anyOf", "Value must satisfy at least one of the options", $failResults); + } + if (isset($this->schema->oneOf)) { + $failResults = array(); + $successIndex = NULL; + foreach ($this->schema->oneOf as $index => $subSchema) { + $subResult = $this->subResult($this->data, $subSchema, FALSE); + if ($subResult->valid) { + if ($successIndex === NULL) { + $successIndex = $index; + } else { + $this->fail(JSV4_ONE_OF_MULTIPLE, "", "/oneOf", "Value satisfies more than one of the options ($successIndex and $index)"); + } + continue; + } + $failResults[] = $subResult; + } + if ($successIndex === NULL) { + $this->fail(JSV4_ONE_OF_MISSING, "", "/oneOf", "Value must satisfy one of the options", $failResults); + } + } + if (isset($this->schema->not)) { + $subResult = $this->subResult($this->data, $this->schema->not, FALSE); + if ($subResult->valid) { + $this->fail(JSV4_NOT_PASSED, "", "/not", "Value satisfies prohibited schema"); + } + } + } + + private function createValueForProperty($key) { + $schema = NULL; + if (isset($this->schema->properties->$key)) { + $schema = $this->schema->properties->$key; + } else if (isset($this->schema->patternProperties)) { + foreach ($this->schema->patternProperties as $pattern => $subSchema) { + if (preg_match("/".str_replace("/", "\\/", $pattern)."/", $key)) { + $schema = $subSchema; + break; + } + } + } + if (!$schema && isset($this->schema->additionalProperties)) { + $schema = $this->schema->additionalProperties; + } + if ($schema) { + if (isset($schema->default)) { + $this->data->$key = unserialize(serialize($schema->default)); + return TRUE; + } + if (isset($schema->type)) { + $types = is_array($schema->type) ? $schema->type : array($schema->type); + if (in_array("null", $types)) { + $this->data->$key = NULL; + } elseif (in_array("boolean", $types)) { + $this->data->$key = TRUE; + } elseif (in_array("integer", $types) || in_array("number", $types)) { + $this->data->$key = 0; + } elseif (in_array("string", $types)) { + $this->data->$key = ""; + } elseif (in_array("object", $types)) { + $this->data->$key = new StdClass; + } elseif (in_array("array", $types)) { + $this->data->$key = array(); + } else { + return FALSE; + } + } + return TRUE; + } + return FALSE; + } +} + +class Jsv4Error extends Exception { + public $code; + public $dataPath; + public $schemaPath; + public $message; + + public function __construct($code, $dataPath, $schemaPath, $errorMessage, $subResults=NULL) { + parent::__construct($errorMessage); + $this->code = $code; + $this->dataPath = $dataPath; + $this->schemaPath = $schemaPath; + $this->message = $errorMessage; + if ($subResults) { + $this->subResults = $subResults; + } + } + + public function prefix($dataPrefix, $schemaPrefix) { + return new Jsv4Error($this->code, $dataPrefix.$this->dataPath, $schemaPrefix.$this->schemaPath, $this->message); + } +} + +?>