| Current File : /home/jvzmxxx/wiki1/extensions/EventLogging/includes/JsonSchema.php |
<?php
/**
* JSON Schema Validation Library
*
* Copyright (c) 2005-2012, Rob Lanphier
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* * Neither my name nor the names of my contributors may be used to
* endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @author Rob Lanphier <robla@wikimedia.org>
* @copyright © 2011-2012 Rob Lanphier
* @licence http://jsonwidget.org/LICENSE BSD 3-clause
*/
/*
* Note, this is a standalone component. Please don't mix MediaWiki-specific
* code or library calls into this file.
*/
class JsonSchemaException extends Exception {
/**
* Arguments for the message
*
* @var array
*/
public $args;
/**
* @var string 'validate-fail' or 'validate-fail-null'
*/
public $subtype;
public function __construct( $code /* ... */ ) {
parent::__construct( $code );
$this->code = $code;
$this->args = func_get_args();
array_shift( $this->args );
}
}
class JsonUtil {
/**
* Converts the string into something safe for an HTML id.
* performs the easiest transformation to safe id, but is lossy
*/
public static function stringToId( $var ) {
if ( is_int( $var ) ) {
return (string)$var;
} elseif ( is_string( $var ) ) {
return preg_replace( '/[^a-z0-9\-_:\.]/i', '', $var );
} else {
throw new JsonSchemaException( 'jsonschema-idconvert', JsonUtil::encodeForMsg( $var ) );
}
}
/**
* Converts data to JSON format with pretty-formatting, but limited to a single line and escaped
* to be suitable for wikitext message parameters.
*/
public static function encodeForMsg( $data ) {
if ( class_exists( 'FormatJson' ) && function_exists( 'wfEscapeWikiText' ) ) {
$json = FormatJson::encode( $data, "\t", FormatJson::ALL_OK );
// Literal newlines can't appear in JSON string values, so this neatly folds the formatting
$json = preg_replace( "/\n\t+/", ' ', $json );
return wfEscapeWikiText( $json );
} else {
return json_encode( $data );
}
}
/**
* Given a type (e.g. 'object', 'integer', 'string'), return the default/empty
* value for that type.
*/
public static function getNewValueForType( $thistype ) {
switch ( $thistype ) {
case 'object':
$newvalue = [];
break;
case 'array':
$newvalue = [];
break;
case 'number':
case 'integer':
$newvalue = 0;
break;
case 'string':
$newvalue = "";
break;
case 'boolean':
$newvalue = false;
break;
default:
$newvalue = null;
break;
}
return $newvalue;
}
/**
* Return a JSON-schema type for arbitrary data $foo
*/
public static function getType( $foo ) {
if ( is_null( $foo ) ) {
return null;
}
switch ( gettype( $foo ) ) {
case "array":
$retval = "array";
foreach ( array_keys( $foo ) as $key ) {
if ( !is_int( $key ) ) {
$retval = "object";
}
}
return $retval;
break;
case "integer":
case "double":
return "number";
break;
case "boolean":
return "boolean";
break;
case "string":
return "string";
break;
default:
return null;
break;
}
}
/**
* Generate a schema from a data example ($parent)
*/
public static function getSchemaArray( $parent ) {
$schema = [];
$schema['type'] = JsonUtil::getType( $parent );
switch ( $schema['type'] ) {
case 'object':
$schema['properties'] = [];
foreach ( $parent as $name ) {
$schema['properties'][$name] = JsonUtil::getSchemaArray( $parent[$name] );
}
break;
case 'array':
$schema['items'] = [];
$schema['items'][0] = JsonUtil::getSchemaArray( $parent[0] );
break;
}
return $schema;
}
}
/*
* Internal terminology:
* Node: "node" in the graph theory sense, but specifically, a node in the
* raw PHP data representation of the structure
* Ref: a node in the object tree. Refs contain nodes and metadata about the
* nodes, as well as pointers to parent refs
*/
/**
* Structure for representing a generic tree which each node is aware of its
* context (can refer to its parent). Used for schema refs.
*/
class TreeRef {
public $node;
public $parent;
public $nodeindex;
public $nodename;
public function __construct( $node, $parent, $nodeindex, $nodename ) {
$this->node = $node;
$this->parent = $parent;
$this->nodeindex = $nodeindex;
$this->nodename = $nodename;
}
}
/**
* Structure for representing a data tree, where each node (ref) is aware of its
* context and associated schema.
*/
class JsonTreeRef {
public function __construct( $node, $parent = null, $nodeindex = null,
$nodename = null, $schemaref = null ) {
$this->node = $node;
$this->parent = $parent;
$this->nodeindex = $nodeindex;
$this->nodename = $nodename;
$this->schemaref = $schemaref;
$this->fullindex = $this->getFullIndex();
$this->datapath = [];
if ( !is_null( $schemaref ) ) {
$this->attachSchema();
}
}
/**
* Associate the relevant node of the JSON schema to this node in the JSON
*/
public function attachSchema( $schema = null ) {
if ( !is_null( $schema ) ) {
$this->schemaindex = new JsonSchemaIndex( $schema );
$this->nodename =
isset( $schema['title'] ) ? $schema['title'] : "Root node";
$this->schemaref = $this->schemaindex->newRef( $schema, null, null, $this->nodename );
} elseif ( !is_null( $this->parent ) ) {
$this->schemaindex = $this->parent->schemaindex;
}
}
/**
* Return the title for this ref, typically defined in the schema as the
* user-friendly string for this node.
*/
public function getTitle() {
if ( isset( $this->nodename ) ) {
return $this->nodename;
} elseif ( isset( $this->node['title'] ) ) {
return $this->node['title'];
} else {
return $this->nodeindex;
}
}
/**
* Rename a user key. Useful for interactive editing/modification, but not
* so helpful for static interpretation.
*/
public function renamePropname( $newindex ) {
$oldindex = $this->nodeindex;
$this->parent->node[$newindex] = $this->node;
$this->nodeindex = $newindex;
$this->nodename = $newindex;
$this->fullindex = $this->getFullIndex();
unset( $this->parent->node[$oldindex] );
}
/**
* Return the type of this node as specified in the schema. If "any",
* infer it from the data.
*/
public function getType() {
if ( array_key_exists( 'type', $this->schemaref->node ) ) {
$nodetype = $this->schemaref->node['type'];
} else {
$nodetype = 'any';
}
if ( $nodetype == 'any' ) {
if ( $this->node === null ) {
return null;
} else {
return JsonUtil::getType( $this->node );
}
} else {
return $nodetype;
}
}
/**
* Return a unique identifier that may be used to find a node. This
* is only as robust as stringToId is (i.e. not that robust), but is
* good enough for many cases.
*/
public function getFullIndex() {
if ( is_null( $this->parent ) ) {
return "json_root";
} else {
return $this->parent->getFullIndex() . "." . JsonUtil::stringToId( $this->nodeindex );
}
}
/**
* Get a path to the element in the array. if $foo['a'][1] would load the
* node, then the return value of this would be array('a',1)
*/
public function getDataPath() {
if ( !is_object( $this->parent ) ) {
return [];
} else {
$retval = $this->parent->getDataPath();
$retval[] = $this->nodeindex;
return $retval;
}
}
/**
* Return path in something that looks like an array path. For example,
* for this data: [{'0a':1,'0b':{'0ba':2,'0bb':3}},{'1a':4}]
* the leaf node with a value of 4 would have a data path of '[1]["1a"]',
* while the leaf node with a value of 2 would have a data path of
* '[0]["0b"]["oba"]'
*/
public function getDataPathAsString() {
$retval = "";
foreach ( $this->getDataPath() as $item ) {
$retval .= '[' . json_encode( $item ) . ']';
}
return $retval;
}
/**
* Return data path in user-friendly terms. This will use the same
* terminology as used in the user interface (1-indexed arrays)
*/
public function getDataPathTitles() {
if ( !is_object( $this->parent ) ) {
return $this->getTitle();
} else {
return $this->parent->getDataPathTitles() . ' -> '
. $this->getTitle();
}
}
/**
* Return the child ref for $this ref associated with a given $key
*/
public function getMappingChildRef( $key ) {
$snode = $this->schemaref->node;
$schemadata = [];
$nodename = $key;
if ( array_key_exists( 'properties', $snode ) &&
array_key_exists( $key, $snode['properties'] ) ) {
$schemadata = $snode['properties'][$key];
$nodename = isset( $schemadata['title'] ) ? $schemadata['title'] : $key;
} elseif ( array_key_exists( 'additionalProperties', $snode ) ) {
// additionalProperties can *either* be a boolean or can be
// defined as a schema (an object)
if ( gettype( $snode['additionalProperties'] ) == "boolean" ) {
if ( !$snode['additionalProperties'] ) {
throw new JsonSchemaException( 'jsonschema-invalidkey',
$key, $this->getDataPathTitles() );
}
} else {
$schemadata = $snode['additionalProperties'];
$nodename = $key;
}
}
$value = $this->node[$key];
$schemai = $this->schemaindex->newRef( $schemadata, $this->schemaref, $key, $key );
$jsoni = new JsonTreeRef( $value, $this, $key, $nodename, $schemai );
return $jsoni;
}
/**
* Return the child ref for $this ref associated with a given index $i
*/
public function getSequenceChildRef( $i ) {
// TODO: make this conform to draft-03 by also allowing single object
if ( array_key_exists( 'items', $this->schemaref->node ) ) {
$schemanode = $this->schemaref->node['items'][0];
} else {
$schemanode = [];
}
$itemname = isset( $schemanode['title'] ) ? $schemanode['title'] : "Item";
$nodename = $itemname . " #" . ( (string)$i + 1 );
$schemai = $this->schemaindex->newRef( $schemanode, $this->schemaref, 0, $i );
$jsoni = new JsonTreeRef( $this->node[$i], $this, $i, $nodename, $schemai );
return $jsoni;
}
/**
* Validate the JSON node in this ref against the attached schema ref.
* Return true on success, and throw a JsonSchemaException on failure.
*/
public function validate() {
if ( array_key_exists( 'enum', $this->schemaref->node ) &&
!in_array( $this->node, $this->schemaref->node['enum'] ) ) {
$e = new JsonSchemaException( 'jsonschema-invalid-notinenum',
JsonUtil::encodeForMsg( $this->node ), $this->getDataPathTitles() );
$e->subtype = "validate-fail";
throw( $e );
}
$datatype = JsonUtil::getType( $this->node );
$schematype = $this->getType();
if ( $datatype == 'array' && $schematype == 'object' ) {
// PHP datatypes are kinda loose, so we'll fudge
$datatype = 'object';
}
if ( $datatype == 'number' && $schematype == 'integer' &&
$this->node == (int)$this->node ) {
// Alright, it'll work as an int
$datatype = 'integer';
}
if ( $datatype != $schematype ) {
if ( is_null( $datatype ) && !is_object( $this->parent ) ) {
$e = new JsonSchemaException( 'jsonschema-invalidempty' );
$e->subtype = "validate-fail-null";
throw( $e );
} else {
$datatype = is_null( $datatype ) ? "null" : $datatype;
$e = new JsonSchemaException( 'jsonschema-invalidnode',
$schematype, $datatype, $this->getDataPathTitles() );
$e->subtype = "validate-fail";
throw( $e );
}
}
switch ( $schematype ) {
case 'object':
$this->validateObjectChildren();
break;
case 'array':
$this->validateArrayChildren();
break;
}
return true;
}
/**
*/
private function validateObjectChildren() {
if ( array_key_exists( 'properties', $this->schemaref->node ) ) {
foreach ( $this->schemaref->node['properties'] as $skey => $svalue ) {
$keyRequired = array_key_exists( 'required', $svalue ) ? $svalue['required'] : false;
if ( $keyRequired && !array_key_exists( $skey, $this->node ) ) {
$e = new JsonSchemaException( 'jsonschema-invalid-missingfield', $skey );
$e->subtype = "validate-fail-missingfield";
throw( $e );
}
}
}
foreach ( $this->node as $key => $value ) {
$jsoni = $this->getMappingChildRef( $key );
$jsoni->validate();
}
return true;
}
/*
*/
private function validateArrayChildren() {
$length = count( $this->node );
for ( $i = 0; $i < $length; $i++ ) {
$jsoni = $this->getSequenceChildRef( $i );
$jsoni->validate();
}
}
}
/**
* The JsonSchemaIndex object holds all schema refs with an "id", and is used
* to resolve an idref to a schema ref. This also holds the root of the schema
* tree. This also serves as sort of a class factory for schema refs.
*/
class JsonSchemaIndex {
public $root;
public $idtable;
/**
* The whole tree is indexed on instantiation of this class.
*/
public function __construct( $schema ) {
$this->root = $schema;
$this->idtable = [];
if ( is_null( $this->root ) ) {
return null;
}
$this->indexSubtree( $this->root );
}
/**
* Recursively find all of the ids in this schema, and store them in the
* index.
*/
public function indexSubtree( $schemanode ) {
if ( !array_key_exists( 'type', $schemanode ) ) {
$schemanode['type'] = 'any';
}
$nodetype = $schemanode['type'];
switch ( $nodetype ) {
case 'object':
foreach ( $schemanode['properties'] as $value ) {
$this->indexSubtree( $value );
}
break;
case 'array':
foreach ( $schemanode['items'] as $value ) {
$this->indexSubtree( $value );
}
break;
}
if ( isset( $schemanode['id'] ) ) {
$this->idtable[$schemanode['id']] = $schemanode;
}
}
/**
* Generate a new schema ref, or return an existing one from the index if
* the node is an idref.
*/
public function newRef( $node, $parent, $nodeindex, $nodename ) {
if ( array_key_exists( '$ref', $node ) ) {
if ( strspn( $node['$ref'], '#' ) != 1 ) {
throw new JsonSchemaException( 'jsonschema-badidref', $node['$ref'] );
}
$idref = $node['$ref'];
try {
$node = $this->idtable[$idref];
}
catch ( Exception $e ) {
throw new JsonSchemaException( 'jsonschema-badidref', $node['$ref'] );
}
}
return new TreeRef( $node, $parent, $nodeindex, $nodename );
}
}