rhymix/tools/dbxml_validator/validate.php
2013-11-30 19:21:47 +09:00

2215 lines
49 KiB
PHP

#!/bin/env php
<?php
/* Copyright (C) NAVER <http://www.navercorp.com> */
/**
vi:set ts=4:
@file
Script to validate a query or a SQL statement written in the
XpressEngine XML Query Language or the XML Schema language.
XpressEngine is an open source framework for creating your web sites.
http://xpressengine.org/
@Author: Arnia Software
@Date: 9 apr 2012
The validation is based on, and is meant to model, the behavior exposed
by the php classes in classes/xml/xmlquery/ and class/db/queryparts/
in the XE installation directory.
Usage:
validate.php query-file.xml query-file.xml ...
or
validate.php schema-definition.xsd query-file.xml ...
*/
error_reporting(E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR);
ini_set('display_errors', 'stderr');
/**
@brief callback to turn a php error into a php exception
So now any error interrupts or terminates script execution
@developer Arnia Software
@param $errno - php error number
@param $errstr - php error string
@param $errfile - file name
@param $errline - line no
@return none
*/
function exception_error_handler($errno, $errstr, $errfile, $errline)
{
// exit on error
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
// set_error_handler("exception_error_handler");
// Error reporting classes/functions
/**
@brief Exception class for user error messages.
@developer Arnia Software
*/
class ErrorMessage extends Exception
{
}
/**
@brief Error message class to signal and carry
the command-line usage description (string) for the script
@developer Arnia Software
*/
class SyntaxError extends ErrorMessage
{
}
/**
@brief Error in an XML query
@developer Arnia Software
*/
class XmlSchemaError extends ErrorMessage
{
public $xml_file;
public $xml_line_no;
public $xml_message;
/**
@brief Composes a message in the format:
<pre>
file_name (line_no):
message
</pre>
@developer Arnia Software
@return
@param $file
@param $line_no
@param $message
@access
*/
public function __construct($file, $line_no, $message)
{
parent::__construct("{$file}({$line_no}):\n\t$message");
$this->xml_file = $file;
$this->xml_line_no = $line_no;
$this->xml_message = $message;
}
}
/**
@brief Clean up libxml errors list when going out of scope (on the destructor)
@developer Arnia Software
*/
class LibXmlClearErrors
{
/**
@brief Clear libXML errors
@developer
@return
@access
*/
public function __destruct()
{
libxml_clear_errors();
}
}
/**
@brief
@developer
@return
@param $filename
@param $throw_error
*/
function libXmlDisplayError($filename = NULL, $throw_error = FALSE)
{
// set up clean-up call to libxml_clear_errors()
$libXmlClearErrors = new LibXmlClearErrors();
$libXmlErrors = libxml_get_errors();
if(count($libXmlErrors))
{
if(!$filename)
{
$filename = $libXmlErrors[0]->file;
}
$msg = '';
foreach($libXmlErrors as $libXmlError)
{
$msg .= "{$libXmlError->file}({$libXmlError->line}):\n\t {$libXmlError->message}";
}
if($throw_error)
{
throw new ErrorMessage($msg);
}
else
{
fwrite(STDERR, $msg . "\n");
}
}
else
{
if($throw_error)
{
throw new ErrorMessage('Schema validation failed.');
}
}
}
/**
Checks an XML node for duplicate descendants of a give tag.
Throws XmlSchemaError if duplicates found.
@brief
@developer
@return
@param $xml_file
@param $node
@param $child_tag
*/
function checkDuplicateDescendants($xml_file, $node, $child_tag)
{
$children = $node->getElementsByTagName($child_tag);
if($children->length > 1)
{
throw
new XmlSchemaError(
$xml_file,
$children->item(1)->getLineNo(),
"Duplicate <{$child_tag}> elements."
);
}
}
/**
Checks the XML child nodes for unique/key values of one (or more)
attribute(s)
@param $xml_file
Name of file with the XML node to be checked. Used in the error
messages.
@param $node
The XML node with the children to be checked.
@param $child_tags
Array with tag names for the children elements
@param $attr_tags
Array with names of attributes to be checked. If multiple attributes
are given, than the first one that is present on a child is included
in the check.
@param $key
True if child elements are required to expose at least one of the
attribute. False if only the child nodes with some of the
attributes present are to be checked.
@brief
@developer
@return
*/
function checkUniqueKey($xml_file, $node, $child_tags, $attr_tags, $key)
{
$key_values = array();
foreach($node->childNodes as $child_node)
{
if($child_node->nodeType == XML_ELEMENT_NODE
&&
in_array($child_node->tagName, $child_tags))
{
$key_value = NULL;
foreach($attr_tags as $attr_tag)
{
if($child_node->hasAttribute($attr_tag))
{
$key_value = $child_node->getAttribute($attr_tag);
if(array_key_exists($key_value, $key_values))
{
throw
new XmlSchemaError(
$xml_file,
$child_node->getLineNo(),
"Duplicate {$attr_tag} found in <{$node->tagName}>."
);
}
$key_values[$key_value] = TRUE;
break;
}
}
if(!$key_value && $key)
{
throw
new XmlSchemaError(
$xml_file,
$child_node->getLineNo(),
"<{$child_node->tagName}>: at least one of the following attributes is expected: "
.
implode(', ', $attr_tags) . '.'
);
}
}
}
}
/**
Checks a SQL table-expression in the FROM clause. The table
should be either:
- a named table. If the table is the right table of a join,
then the join conditions should be given as content.
- a sub-query. No table name should be given, table alias
should be present and query attribute should be present
and have the value "true". Content should include at
least a select list or a table specification
@brief
@developer
@return
@param $xml_file
@param $table_element
*/
function checkTableExpression($xml_file, $table_element)
{
$table_name = NULL;
$join_type = NULL;
if($table_element->hasAttribute('name'))
{
$table_name = $table_element->getAttribute('name');
}
if($table_element->hasAttribute('type'))
{
$join_type = $table_element->getAttribute('type');
}
if($table_element->getAttribute('query') == 'true')
{
if($table_name !== NULL)
{
throw
new XmlSchemaError(
$xml_file,
$table_element->getLineNo(),
'Subqueries should only use aliases, not names'
);
}
if($join_type !== NULL)
{
throw
new XmlSchemaError(
$xml_file,
$table_element->getLineNo(),
'Currently subqueries may not be used as '
.
'the right side table in a join'
);
}
// table alias is already checked by the unique key constraint on
// the (alias or name) key on the tables element
// check contents for a select list or a table-specification
$has_query_clauses = FALSE;
foreach($table_element->childNodes as $query_clause)
{
if($query_clause->nodeType == XML_ELEMENT_NODE
&&
(
$query_clause->tagName == 'columns'
||
$query_clause->tagName == 'tables'))
{
$has_query_clauses = TRUE;
}
}
if(!$has_query_clauses)
{
throw
new XmlSchemaError(
$xml_file,
$table_element->getLineNo(),
'Subquery tables should have at least a select list or a table specification.'
.
"\nANSI SQL-99 declares the table specification as required."
);
}
}
else
{
// base table or view
if($join_type !== NULL)
{
$has_conditions_element = FALSE;
foreach($table_element->childNodes as $child_node)
{
if($child_node->nodeType == XML_ELEMENT_NODE)
{
if($child_node->tagName == 'conditions')
{
if($has_conditions_element)
{
throw
new XmlSchemaError(
$xml_file,
$child_node->getLineNo(),
'Duplicate <conditions> elements.'
);
}
else
{
$has_conditions_element = TRUE;
}
}
else
{
throw
new XmlSchemaError(
$xml_file,
$child_node->getLineNo(),
'<conditions> element must be the only content for a joined <table>.'
);
}
}
}
if(!$has_conditions_element)
{
throw
new XmlSchemaError(
$xml_file,
$table_element->getLineNo(),
'Expected <conditions> element as content.'
);
}
}
else
{
foreach($table_element->childNodes as $child_node)
{
if($child_node->nodeType == XML_ELEMENT_NODE)
{
throw
new XmlSchemaError(
$xml_file,
$child_node->getLineNo(),
'<table> element can only have content if it is a sub-query or is joined.'
);
}
}
}
}
}
/**
All table names and aliases should be distinct throughout
the <tables> element.
Subquery tables should be valid queries.
@brief
@developer
@return
@param $xml_file
@param $tables_element
*/
function checkTablesClause($xml_file, $tables_element)
{
checkUniqueKey
(
$xml_file,
$tables_element,
array('table'), // child elements to be checked
array('alias', 'name'), // attributes to be checked
TRUE // attributes are required
);
foreach($tables_element->childNodes as $table)
{
if($table->nodeType == XML_ELEMENT_NODE
&&
$table->tagName == 'table')
{
checkTableExpression($xml_file, $table);
if($table->getAttribute('query') == 'true')
{
validate_select_query($xml_file, $table); // recursive call
}
}
}
}
/**
Table columns in a select-list should be unique. This is not a
requirement by the SQL language, and it can be commented out
below, but it is still common sense.
Some of the "columns" here are actually small expressions, but
they can still be included literally in the unique constraint.
@brief
@developer
@return
@param $xml_file
@param $columns_element
*/
function checkSelectListClause($xml_file, $columns_element)
{
checkUniqueKey
(
$xml_file,
$columns_element,
array('column', 'query'), // child elements
array('alias', 'name'), // attributes
FALSE // ignore if no attributes present
);
}
/**
Check that attributes for variable contents validation
(filter, notnull, minlength, maxlength) are present only if
the var attribute is present.
@brief
@developer
@return
@param $xml_file
@param $container_element
@param $child_tag
*/
function checkVarContentsValidation
(
$xml_file,
$container_element,
$child_tag
)
{
static
$key_attr = 'var';
static
$var_attrs =
array
(
'filter', 'notnull', 'minlength',
'maxlength'
);
foreach($container_element->childNodes as $child_node)
{
if($child_node->nodeType == XML_ELEMENT_NODE
&&
$child_node->tagName == $child_tag)
{
if(!$child_node->hasAttribute($key_attr))
{
foreach($var_attrs as $var_attr)
{
if($child_node->hasAttribute($var_attr))
{
throw
new XmlSchemaError(
$xml_file,
$child_node->getLineNo(),
"<{$child_node->tagName}>: Attribute '{$var_attr}' "
.
"should only be used with the '{$key_attr}' attribute."
);
}
}
}
}
}
}
/**
Checks that a subquery condition does not have a var or default attribute.
@brief
@developer
@return
@param $xml_file
@param $condition
*/
function checkConditionElement($xml_file, $condition)
{
$child_query_node = FALSE;
$has_var_attribute = $condition->hasAttribute('var') || $condition->hasAttribute('default');
$query_line_no = -1;
foreach($condition->childNodes as $query_node)
{
if($query_node->nodeType == XML_ELEMENT_NODE
&&
$query_node->tagName == 'query')
{
validate_select_query($xml_file, $query_node);
$child_query_node = TRUE;
$query_line_no = $query_node->getLineNo();
}
}
if($child_query_node && $has_var_attribute)
{
throw
new XmlSchemaError(
$xml_file,
$query_line_no,
"<query> element found when <condition> has a 'var' or 'default' attribute."
);
}
if(!($child_query_node || $has_var_attribute))
{
throw
new XmlSchemaError(
$xml_file,
$condition->getLineNo(),
"<condition>: either a <query> child, 'var' attribute or 'default' attribute expected."
);
}
}
/**
Checks that conditions have the pipe attribute, and that variable-contents-validation
attributes are only present if var attribute is present.
Also recurses into condition groups and expression subqueries
@brief
@developer
@return
@param $xml_file
@param $conditions
*/
function checkConditionsGroup($xml_file, $conditions)
{
$first_child = TRUE;
foreach($conditions->childNodes as $child_node)
{
if($child_node->nodeType == XML_ELEMENT_NODE)
{
// check for 'pipe' attribute
if($first_child)
{
$first_child = FALSE;
}
else
{
if(!$child_node->hasAttribute('pipe'))
{
throw
new XmlSchemaError(
$xml_file,
$child_node->getLineNo(),
'Attribute pipe expected for all but the first element'
.
" in <{$conditions->tagName}> content."
);
}
}
// recurse in condition groups/queries
if($child_node->tagName == 'group')
{
checkConditionsGroup($xml_file, $child_node);
}
else
{
if($child_node->tagName == 'query')
{
validate_select_query($xml_file, $child_node);
}
else
{
if($child_node->tagName == 'condition')
{
checkConditionElement($xml_file, $child_node);
}
}
}
}
}
// check variable contents validation attributes
checkVarContentsValidation($xml_file, $conditions, 'condition');
}
/**
Ensure at most one <list_count>, <page_count> and
<page> elements are present. There can be any number of
<index> elements listed.
@brief
@developer
@return
@param $xml_file
@param $navigation_element
*/
function checkNavigationClauses($xml_file, $navigation_element)
{
foreach(array('list_count', 'page_count', 'page')
as
$navigation_el)
{
checkDuplicateDescendants
(
$xml_file,
$navigation_element,
$navigation_el
);
}
}
/**
Additional checks to validate a query XML, that can not
be properly expressed in the schema definition (.xsd) file
for the query.
Most likely the conditions explicitly coded and checked for
here can also be expressed as XPath queries.
@brief
@developer
@return
@param $xml_file
@param $query_element
*/
function validate_select_query($xml_file, $query_element)
{
foreach($query_element->childNodes as $select_clause)
{
if($select_clause->nodeType == XML_ELEMENT_NODE)
{
switch($select_clause->tagName)
{
case 'columns':
checkSelectListClause($xml_file, $select_clause);
break;
case 'tables':
checkTablesClause($xml_file, $select_clause);
break;
case 'conditions':
checkConditionsGroup($xml_file, $select_clause);
break;
case 'navigation':
checkNavigationClauses($xml_file, $select_clause);
break;
}
}
}
}
/**
@brief
@developer
@return
@param $xml_file
@param $query_element
*/
function validate_update_query($xml_file, $query_element)
{
foreach($query_element->childNodes as $update_clause)
{
if($update_clause->nodeType == XML_ELEMENT_NODE)
{
switch($update_clause->tagName)
{
case 'tables':
checkTablesClause($xml_file, $update_clause);
break;
case 'conditions':
checkConditionsGroup($xml_file, $update_clause);
break;
}
}
}
}
/**
@brief
@developer
@param $xml_file
@param $query_element
@return
*/
function validate_delete_query($xml_file, $query_element)
{
foreach($query_element->childNodes as $delete_clause)
{
if($delete_clause->nodeType == XML_ELEMENT_NODE)
{
switch($delete_clause->tagName)
{
case 'conditions':
checkConditionsGroup($xml_file, $delete_clause);
break;
}
}
}
}
/**
@brief
@developer
@return
@param $xml_file
@param $query_element
*/
function validate_insert_select_query($xml_file, $query_element)
{
foreach($query_element->childNodes as $statement_clause)
{
if($statement_clause->nodeType == XML_ELEMENT_NODE)
{
switch($statement_clause->tagName)
{
case 'query':
validate_select_query($xml_file, $statement_clause);
break;
}
}
}
}
$validate_query_type =
array
(
// 'insert' =>
// there is currently nothing special to check
// for a plain insert, all the needed checks
// are already expressed in the .xsd
'insert-select' => 'validate_insert_select_query',
'update' => 'validate_update_query',
'select' => 'validate_select_query',
'delete' => 'validate_delete_query'
);
/**
@brief
@developer
@return
@param $xml_file
@param $query_element
*/
function validate_xml_query($xml_file, $query_element)
{
global $validate_query_type;
$action = $query_element->getAttribute('action');
if(array_key_exists($action, $validate_query_type))
{
$validate_query_type[$action]($xml_file, $query_element);
}
}
if(strpos(PHP_SAPI, 'cli') !== FALSE
||
strpos(PHP_SAPI, 'cgi') !== FALSE)
{
/**
Saves working directory and restores it upon destruction.
Only use with single-threaded php SAPIs like CLI.
@brief
@developer
*/
class RestoreWorkDir
{
protected $dirname;
/**
@brief
@developer
@return
@access
*/
public function __destruct()
{
try
{
$success = chdir($this->dirname);
}
catch (Exception $e)
{
print "Failed to restore working dir {$this->dirname}.";
}
if(!$success)
{
print "Failed to restore working dir {$this->dirname}.";
}
}
/**
@brief
@developer
@return
@access
*/
public function __construct()
{
$this->dirname = getcwd();
if(!$this->dirname)
{
throw new ErrorMessage("Failed to get current directory.");
}
}
}
}
/**
Checks that the query_id is the same as the given file name.
For portability with case-sensitive file systems, the
actual casing of the file name from the file system is used,
and the subsequent string comparatin is case-sensitive.
Assumes the file is known to exist (has already been opened).
@brief
@developer
@return
@param $xml_file
@param $query_id
*/
function validate_query_id($xml_file, $query_id)
{
$xml_path_info = pathinfo($xml_file);
$filename_len = strlen($xml_path_info['basename']);
$lowercase_name = strtolower($xml_path_info['basename']);
$uppercase_name = strtoupper($xml_path_info['basename']);
if(strlen($lowercase_name) != $filename_len
||
strlen($uppercase_name) != $filename_len)
{
// multi-byte encodings may result in a different number of characters
// in the two strings
throw new ErrorMessage("Unsupported file name encoding.");
}
// transform the given file name into a case-insensitive glob() pattern
$varing_case_filename = '';
for($i = 0; $i < $filename_len; $i++)
{
if($lowercase_name[$i] != $uppercase_name[$i])
{
$varing_case_filename .= "[{$lowercase_name[$i]}{$uppercase_name[$i]}]";
}
else
{
$varing_case_filename .= $lowercase_name[$i];
}
}
$glob_pattern = $xml_path_info['dirname'];
$restoreWorkDir = new RestoreWorkDir();
if($glob_pattern)
{
// change current dir to the xml file directory to keep
// glob pattern shorter (maximum 260 chars).
$success = chdir($glob_pattern);
if(!$success)
{
throw new ErrorMessage("Failed to change work dir to {$glob_pattern}.");
}
}
$glob_pattern = $varing_case_filename;
// use glob() to get the file name from the file system
// realpath() would have the same effect, but it is not documented as such
$matched_files = glob($glob_pattern, GLOB_NOSORT | GLOB_NOESCAPE | GLOB_ERR);
unset($RestoreWorkDir); // restore work dir after call to glob()
if($matched_files === FALSE || !is_array($matched_files))
{
throw new ErrorMessage("Directory listing for $xml_file failed.");
}
switch(count($matched_files))
{
case 0:
throw new ErrorMessage("Directory listing for $xml_file failed.");
case 1:
return (pathinfo($matched_files[0], PATHINFO_FILENAME) == $query_id);
default:
// more than one files with the same name and different case
// case-sensitive file system
foreach($mached_files as $matched_file)
{
if(pathinfo($matched_file, PATHINFO_BASENAME) == $xml_path_info['basename'])
{
return ($xml_path_info['filename'] == $query_id);
}
}
throw new ErrorMessage("Directory listing for $xml_file failed.");
}
throw new ErrorMessage("Internal application error."); // unreachable
}
/**
Validate a table definition in the XML Schema Language.
Check that the size attributes is only given for FLOAT and [VAR]CHAR
types, and that it is always present for VARCHAR.
Check that auto_increment is only given for (big)number types.
Check for CUBRID-only/mysql+MSsql-only attributes 'auto_increment'
and 'tinytext'.
@brief
@developer
@return
@param $xml_file
@param $table_element
*/
function validate_schema_doc($xml_file, $table_element)
{
foreach($table_element->childNodes as $col_node)
{
if($col_node->nodeType == XML_ELEMENT_NODE
&&
$col_node->tagName == 'column')
{
$col_type = $col_node->getAttribute('type');
$col_size = NULL;
// check auto-increment column
if($col_node->hasAttribute('auto_increment'))
{
fwrite
(
fopen('php://stdout', 'wt'),
$xml_file . '(' . $col_node->getLineNo() . ")\n\t"
.
"<column>: attribute 'auto_increment' is currently supported only by SQL Server and mysql backends.\n"
);
static
$autoinc_types = array('number', 'bignumber');
if(!in_array($col_type, $autoinc_types))
{
throw
new XmlSchemaError(
$xml_file,
$col_node->getLineNo(),
"<column>: attribute 'auto_increment' only expected for one of the following types: "
.
implode(', ', $autoinc_types) . '.'
);
}
}
// check tinytext
if($col_type == 'tinytext')
{
fwrite
(
fopen('php://stdout', 'wt'),
$xml_file . '(' . $col_node->getLineNo() . ")\n\t"
.
"<column>: type \"tinytext\" is supported only by CUBRID.\n"
);
}
// check size attribute
if($col_node->hasAttribute('size'))
{
$col_size = $col_node->getAttribute('size');
}
if($col_type == 'varchar' && $col_size === NULL)
{
throw
new XmlSchemaError(
$xml_file,
$col_node->getLineNo(),
"<column>: 'size' attribute expected for \"varchar\" type."
);
}
static
$varsize_types = array('char', 'varchar', 'float');
if($col_size !== NULL && !in_array($col_type, $varsize_types))
{
throw
new XmlSchemaError(
$xml_file,
$col_node->getLineNo(),
"<column>: 'size' attribute only expected for the following types: "
.
implode(', ', $varsize_types) . "."
);
}
}
}
}
/**
Class to accumulate the highest return code when multiple files are being
processed, list return codes, and save/restore the code as needed.
Use specific error codes depending on the validation stage and results,
so the unit tests can tell what validation step has failed.
@brief
@developer
*/
class ReturnCode
{
protected $save;
protected $exit_code;
const RETCODE_VALIDATOR_INTERNAL = 60;
const RETCODE_GENERIC_XML_SYNTAX = 50;
const RETCODE_QUERY_ELEMENT = 40;
const RETCODE_XSD_VALIDATION = 30;
const RETCODE_BUILTIN_CHECKS = 20;
const RETCODE_DB_SCHEMA_MATCH = 10; // no schema match is currently implemented.
const RETCODE_SUCCESS = 0;
/**
@brief
@developer
@return
@access
@param $val
*/
public function code($val = -1)
{
if($val == -1)
{
return $this->exit_code;
}
else
{
if($this->exit_code < $val)
{
$this->exit_code = $val;
}
}
}
/**
@brief
@developer
@return
@access
@param $val
*/
public function push($val)
{
$this->save = $this->exit_code;
$this->code($val);
}
/**
@brief
@developer
@access
@return
*/
public function pop()
{
$this->exit_code = $this->save;
$this->save = self::RETCODE_VALIDATOR_INTERNAL;
}
/**
@brief
@developer
@return
@access
@param $val
*/
public function __construct($val = 0)
{
$this->save = self::RETCODE_VALIDATOR_INTERNAL;
$this->exit_code = $val;
}
}
/**
@brief
@developer
*/
class UnlinkFile
{
public $file_name;
/**
@brief
@developer
@access
@return
*/
public function __destruct()
{
if($this->file_name)
{
unlink($this->file_name);
$this->file_name = NULL;
}
}
/**
@brief
@developer
@access
@return
@param $file_name
*/
public function __construct($file_name)
{
$this->file_name = $file_name;
}
}
// main program entry point
try
{
// Explicitly set time zone, to silence some php warning about it
date_default_timezone_set('Europe/Bucharest');
define('CMD_NAME', basename($argv[0]));
$cmdname = CMD_NAME;
// php manual says resources should not normally be declared constant
if(!defined('STDERR'))
{
define('STDERR', fopen('php://stderr', 'wt'));
}
if(!defined('__DIR__'))
{
define('__DIR__', dirname(__FILE__));
}
$retcode = new ReturnCode(ReturnCode::RETCODE_SUCCESS);
$auto_schema = NULL;
$schema_language = NULL;
$skip_query_id = NULL;
$xe_path = NULL;
$validate_only = NULL;
$query_args = NULL;
$query_args_file = NULL;
while($argc >= 2 && $argv[1][0] == '-')
{
$option = $argv[1];
unset($argv[1]);
$argv = array_values($argv);
$argc = count($argv);
switch($option)
{
case '-s':
case '--schema':
case '--schema-language':
if($query_args !== NULL)
{
throw new SyntaxError("Both --args-string and --schema-language options given.");
}
if($query_args_file !== NULL)
{
throw new SyntaxError("Both --args-file and --schema-language options given.");
}
$schema_language = TRUE;
break;
case '--auto-schema':
$auto_schema = TRUE;
case '--skip-query-id':
$skip_query_id = TRUE;
break;
case '--validate-only':
$validate_only = TRUE;
break;
case '--xe-path':
case '--xe':
if($argc < 2)
{
throw
new SyntaxError("Option '{$option}' requires an argument., see `{$cmdname} --help`");
}
$xe_path = $argv[1];
unset($argv[1]);
$argv = array_values($argv);
$argc = count($argv);
break;
case '--arguments-string':
case '--args-string':
case '--arguments':
case '--args':
if($schema_language !== NULL)
{
throw new SyntaxError("Both --schema-language and --args-string options given.");
}
if($query_args_file !== NULL)
{
throw new SyntaxError("Both --args-string and --args-file options given.");
}
if($argc < 2)
{
throw
new SyntaxError("Option '{$option}' requires an argument., see `{$cmdname} --help`");
}
$query_args = $argv[1];
unset($argv[1]);
$argv = array_values($argv);
$argc = count($argv);
break;
case '--arguments-file':
case '--args-file':
if($schema_language !== NULL)
{
throw new SyntaxError("Both --schema-language and --args-file options given.");
}
if($query_args !== NULL)
{
throw new SyntaxError("Both --args-string and --args-file options given.");
}
if($argc < 2)
{
throw
new SyntaxError("Option '{$option}' requires an argument., see `{$cmdname} --help`");
}
$query_args_file = $argv[1];
unset($argv[1]);
$argv = array_values($argv);
$argc = count($argv);
break;
case '--help':
case '--usage':
case '/?':
case '-?':
case '-h':
case '--':
// break out of both the switch
// and while statements
break 2;
default:
throw
new SyntaxError("Unknown option $option, see {$cmdname} --help.");
}
}
if($argc < 2 ||
(
$argc == 2
&&
in_array($argv[1], array('--help', '--usage', '/?', '-?', '-h'))
))
{
throw
new SyntaxError(
"Validates an XML document against a given schema definition (XSD), using the standard php library.\n" .
"Syntax:\n" .
" {$cmdname} schema.xsd document.xml...\n" .
" {$cmdname} [ --schema-language ] [--skip-query-id] ... [--] document.xml...\n" .
"Where:\n" .
" --schema-language\n" .
" --schema\n" .
" -s\n" .
" If given, the document(s) are validated against XE XML Schema Language,\n" .
" otherwise document(s) are validated against XE XML Query Language.\n" .
"\n" .
" --skip-query-id\n" .
" Do not check the query id, which should normally match the file name.\n" .
"\n" .
" --xe-path\n" .
" --xe\n" .
" Path to XE installation. Used to load the database-specific parsers to generate\n" .
" SQL from the XML language files.\n" .
"\n" .
" --validate-only\n" .
" Only check XML schemas, no SQL generated with the database-specific parsers.\n" .
"\n" .
" --args-string \" 'name' => 'val..', 'name' => 'val...' \"\n" .
" --args-file args/file/name.php\n" .
" Variables and values for the query, if it has any (only for XML Query Language).\n" .
" Use a comma-separated 'var-name' => 'var_value...' pairs, in php syntax for an\n" .
" array constructor. The validator script will directly eval()/include() this content.\n" .
" The file named with --args-file should include an array() constructor around the\n" .
" name-value list, and should immediately return it, without a named array variable.\n" .
" E.g.:\n" .
" return \n" .
" array\n" .
" (\n" .
" 'name' => 'val',\n" .
" 'name' => 'val',\n" .
" ...\n" .
" );\n" .
"\n" .
" schema.xsd if given, is the file name for the schema definition to validate the\n" .
" document against\n" .
"\n" .
" document.xml is the file name for the XML document to be validated against the schema.\n" .
" Multiple .xml files can be given.\n"
);
}
$query_user_args = array();
// check $xe_path, $query_args
if(!$validate_only)
{
if($xe_path == NULL)
{
// assume validator.php is in directory .../xe/tools/dbxml_validator/ in an XE installation
$xe_path = dirname(dirname(realpath(__DIR__)));
}
if(!file_exists($xe_path . '/index.php'))
{
throw
new ErrorMessage("File index.php not found in {$xe_path}.");
}
if(!defined('_XE_PATH_'))
{
define('_XE_PATH_', $xe_path . '/');
}
/**
Replaces the Context class in XE.
@brief
@developer
*/
class Context
{
protected static $db_info = NULL;
/**
@brief
@developer
@return
@access
*/
public static function isInstalled()
{
return TRUE;
}
/**
@brief
@developer
@return
@access
*/
public static function getLangType()
{
return 'en';
}
/**
@brief
@developer
@return
@access
*/
public static function getLang()
{
return 'en';
}
/**
@brief
@developer
@return
@access
*/
public static function getDBType()
{
if(self::$db_info)
{
return self::$db_info->master_db['db_type'];
}
else
{
return NULL;
}
}
/**
@brief
@developer
@return
@access
@param $db_info
*/
public static function setDBInfo($db_info)
{
self::$db_info = $db_info;
}
/**
@brief
@developer
@return
@access
*/
public static function getDBInfo()
{
return self::$db_info;
}
/**
@brief
@developer
@return
@access
@param $str
*/
public static function convertEncodingStr($str)
{
return $str;
}
/**
@brief
@developer
@return
@access
*/
public static function setNoDBInfo()
{
$db_info = (object)NULL;
$db_info->master_db =
array
(
'db_type' => NULL,
'db_hostname' => NULL,
'db_port' => NULL,
'db_userid' => NULL,
'db_password' => NULL,
'db_database' => NULL,
'db_table_prefix' => NULL,
'is_connected' => TRUE // that will skip connection attempts
);
$db_info->slave_db = array($db_info->master_db);
$db_info->use_prepared_statements = TRUE;
self::setDBInfo($db_info);
}
/**
@brief
@developer
@access
@return
*/
public static function setMysqlDBInfo()
{
$db_info = (object)NULL;
$db_info->master_db =
array
(
'db_type' => 'mysql',
'db_hostname' => NULL,
'db_port' => NULL,
'db_userid' => NULL,
'db_password' => NULL,
'db_database' => NULL,
'db_table_prefix' => NULL,
'resource' => TRUE,
'is_connected' => TRUE // that will skip connection attempts
);
$db_info->slave_db = array($db_info->master_db);
$db_info->use_prepared_statements = TRUE;
self::setDBInfo($db_info);
if(array_key_exists('__DB__', $GLOBALS)
&&
array_key_exists($db_info->master_db['db_type'], $GLOBALS['__DB__']))
{
}
else
{
$GLOBALS['__DB__'][$db_info->master_db['db_type']] =
new DBMysqlConnectWrapper();
}
$oDB = new DB();
$oDB->getParser(TRUE);
}
/**
@brief
@developer
@return
@access
*/
public static function setMysqliDBInfo()
{
$db_info = (object)NULL;
$db_info->master_db =
array
(
'db_type' => 'mysqli',
'db_hostname' => NULL,
'db_port' => NULL,
'db_userid' => NULL,
'db_password' => NULL,
'db_database' => NULL,
'db_table_prefix' => NULL,
'resource' => TRUE,
'is_connected' => TRUE // that will skip connection attempts
);
$db_info->slave_db = array($db_info->master_db);
$db_info->use_prepared_statements = TRUE;
self::setDBInfo($db_info);
if(array_key_exists('__DB__', $GLOBALS)
&&
array_key_exists($db_info->master_db['db_type'], $GLOBALS['__DB__']))
{
}
else
{
$GLOBALS['__DB__'][$db_info->master_db['db_type']] =
new DBMysqliConnectWrapper();
}
$oDB = new DB();
$oDB->getParser(TRUE);
}
/**
@brief
@developer
@return
@access
*/
public static function setCubridDBInfo()
{
$db_info = (object)NULL;
$db_info->master_db =
array
(
'db_type' => 'cubrid',
'db_hostname' => NULL,
'db_port' => NULL,
'db_userid' => NULL,
'db_password' => NULL,
'db_database' => NULL,
'db_table_prefix' => NULL,
'resource' => TRUE,
'is_connected' => TRUE // that will skip connection attempts
);
$db_info->slave_db = array($db_info->master_db);
$db_info->use_prepared_statements = TRUE;
self::setDBInfo($db_info);
if(array_key_exists('__DB__', $GLOBALS)
&&
array_key_exists($db_info->master_db['db_type'], $GLOBALS['__DB__']))
{
}
else
{
$GLOBALS['__DB__'][$db_info->master_db['db_type']] =
new DBCubridConnectWrapper();
}
$oDB = new DB();
$oDB->getParser(TRUE);
}
/**
@brief
@developer
@return
@access
*/
public static function setMssqlDBInfo()
{
$db_info = (object)NULL;
$db_info->master_db =
array
(
'db_type' => 'mssql',
'db_hostname' => NULL,
'db_port' => NULL,
'db_userid' => NULL,
'db_password' => NULL,
'db_database' => NULL,
'db_table_prefix' => NULL,
'resource' => TRUE,
'is_connected' => TRUE // that will skip connection attempts
);
$db_info->slave_db = array($db_info->master_db);
$db_info->use_prepared_statements = TRUE;
self::setDBInfo($db_info);
if(array_key_exists('__DB__', $GLOBALS)
&&
array_key_exists($db_info->master_db['db_type'], $GLOBALS['__DB__']))
{
}
else
{
$GLOBALS['__DB__'][$db_info->master_db['db_type']] =
new DBMssqlConnectWrapper();
}
$oDB = new DB();
$oDB->getParser(TRUE);
}
}
/**
@brief
@developer
*/
class Any_prop_obj_base
{
/**
@brief
@developer
@return
@param $property
@access
*/
public function __get($property)
{
return NULL;
}
}
/**
@brief
@developer
*/
class LangArgFilterErrorMessage
{
/**
@brief
@developer
@return
@param $property
@access
*/
public function __get($property)
{
return 'Argument filter error';
}
}
global $lang;
$lang = new Any_prop_obj_base(); // to return NULL on non-existent properties
$lang->filter = New LangArgFilterErrorMessage();
if(!defined('__XE__'))
{
define('__XE__', TRUE);
}
if(!defined('__ZBXE__'))
{
define('__ZBXE__', TRUE);
}
if(!defined('__DEBUG__'))
{
define('__DEBUG__', 0);
}
if(!defined('__DEBUG_QUERY__'))
{
define('__DEBUG_QUERY__', 0);
}
include(_XE_PATH_ . 'classes/object/Object.class.php');
include(_XE_PATH_ . 'classes/handler/Handler.class.php');
include(_XE_PATH_ . 'classes/file/FileHandler.class.php');
include(_XE_PATH_ . 'classes/page/PageHandler.class.php');
Context::setNoDBInfo();
require_once(_XE_PATH_ . 'classes/db/DB.class.php');
require_once(_XE_PATH_ . 'classes/db/DBMysql.class.php');
require_once(_XE_PATH_ . 'classes/db/DBMysqli.class.php');
require_once(_XE_PATH_ . 'classes/db/DBMysql_innodb.class.php');
require_once(_XE_PATH_ . 'classes/db/DBCubrid.class.php');
require_once(_XE_PATH_ . 'classes/db/DBMssql.class.php');
require_once(_XE_PATH_ . 'classes/xml/XmlParser.class.php');
require_once(_XE_PATH_ . 'classes/xml/XmlQueryParser.class.php');
require_once(__DIR__ . '/connect_wrapper.php');
// check $query_args, $query_args_file
if($query_args_file)
{
try
{
$query_user_args = require($query_args_file);
}
catch (Exception $exc)
{
fwrite(STDERR, "Error in arguments file.\n");
throw $exc;
}
}
else
{
if($query_args)
{
try
{
eval('$query_user_args = array(' . $query_args . ');');
}
catch (Exception $exc)
{
fwrite(STDERR, "Error in arguments string.\n");
throw $exc;
}
}
}
}
libxml_use_internal_errors(TRUE);
$schema_file = NULL;
$schemas_set =
array
(
'delete' => __DIR__ . '/xml_delete.xsd',
'update' => __DIR__ . '/xml_update.xsd',
'select' => __DIR__ . '/xml_select.xsd',
'insert' => __DIR__ . '/xml_insert.xsd',
'insert-select' => __DIR__ . '/xml_insert_select.xsd'
);
$table_schema = __DIR__ . '/xml_create_table.xsd';
$domDocument = new DOMDocument();
$i = 1;
if(pathinfo($argv[1], PATHINFO_EXTENSION) == 'xsd')
{
$schema_file = $argv[$i++];
}
for(; $i < count($argv); $i++)
{
try
{
$document_schema = $schema_file;
$success = FALSE;
$use_schema_language = $schema_language;
$retcode->push(ReturnCode::RETCODE_GENERIC_XML_SYNTAX);
if($domDocument->load($argv[$i]))
{
$retcode->pop();
$queryElement = $domDocument->documentElement;
if (!$schema_language && $auto_schema)
{
if ($queryElement->tagName == 'table')
{
$use_schema_language = TRUE;
}
}
if(!$schema_file && !$use_schema_language
&&
(
$queryElement->tagName != 'query'
||
!array_key_exists($queryElement->getAttribute('action'), $schemas_set)
))
{
$retcode->code(ReturnCode::RETCODE_QUERY_ELEMENT);
throw
new ErrorMessage(
"{$argv[$i]}:" .
" Root element should be <query> and should have an action attribute of:" .
" insert, insert-select, select, update or delete." .
" Otherwise an explicit schema, to validate the document with, should be" .
" specified as first argument on the command line."
);
}
if(!$schema_file && !$use_schema_language && !$skip_query_id
&&
!validate_query_id($argv[$i], $queryElement->getAttribute('id')))
{
$retcode->code(ReturnCode::RETCODE_QUERY_ELEMENT);
$query_id = $queryElement->getAttribute('id');
throw
new ErrorMessage(
"{$argv[$i]}(" . $queryElement->getLineNo() . "):\n\tQuery 'id' attribute value \"{$query_id}\" should match file name."
);
}
if($use_schema_language)
{
$document_schema = $table_schema;
}
else
{
if(!$document_schema)
{
$document_schema = $schemas_set[$queryElement->getAttribute('action')];
}
}
$retcode->push(ReturnCode::RETCODE_XSD_VALIDATION);
if($domDocument->schemaValidate($document_schema))
{
$retcode->pop();
if($use_schema_language)
{
validate_schema_doc($argv[$i], $domDocument->documentElement);
}
else
{
validate_xml_query($argv[$i], $domDocument->documentElement);
}
$success = TRUE;
}
if(!$validate_only)
{
// Generate SQL with the db provider back-ends
if(function_exists('sys_get_temp_dir'))
{
$tmpdir = sys_get_temp_dir();
}
else
{
$tmpdir = getenv('TEMP');
if(!$tmpdir)
{
$tmpdir = getenv('TMP');
}
if(!$tmpdir)
{
$tmpdir = '/tmp';
}
}
global $_SERVER;
if(!is_array($_SERVER))
{
$_SERVER = array();
}
if(!array_key_exists('REMOTE_ADDR', $_SERVER))
{
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
}
$set_db_info_methods =
array
(
'mysql' => 'setMysqlDBInfo',
'mysqli' => 'setMysqliDBInfo',
'cubrid' => 'setCubridDBInfo',
'mssql' => 'setMssqlDBInfo'
);
foreach($set_db_info_methods as $db_type => $set_info_method)
{
Context::$set_info_method(); // calls setMysqlDBInfo()/setCubridDBInfo()/...
if($use_schema_language)
{
$GLOBALS['__DB__'][$db_type]->queries = '';
$GLOBALS['__DB__'][$db_type]->createTableByXmlFile($argv[$i]);
print "\n";
print pathinfo($argv[$i], PATHINFO_FILENAME);
print " {$db_type} query:\n";
print $GLOBALS['__DB__'][$db_type]->queries;
print "\n";
}
else
{
$unlink_tmpfile =
new UnlinkFile(tempnam($tmpdir, 'xe_'));
// copied from classes/db/DB.class.php
$oParser = new XmlQueryParser();
$args_array =
$oParser->parse_xml_query
(
pathinfo($argv[$i], PATHINFO_FILENAME), // query id
$argv[$i], // xml file
$unlink_tmpfile->file_name // cache file
);
$args_array = $args_array->queryTag->getArguments();
$GLOBALS['__DB__'][$db_type]->queries = '';
$k = 1;
foreach($args_array as $arg)
{
if(// why would there be a query arg without a var name ?
isset($arg->variable_name)
&&
!array_key_exists($arg->variable_name, $query_user_args))
{
if(isset($arg->argument_validator))
{
if(FALSE // some default values are to be parsed by php, some are not...
&&
isset($arg->argument_validator->default_value)
&&
isset($arg->argument_validator->default_value->value))
{
$query_user_args[$arg->variable_name] =
eval('return ' . $arg->argument_validator->default_value->toString() . ';');
}
else
{
if($arg->argument_validator->filter)
{
switch($arg->argument_validator->filter)
{
case 'email':
case 'email_address':
$query_user_args[$arg->variable_name] =
'user@mail.com';
break;
case 'homepage':
$query_user_args[$arg->variable_name] =
'http://user.domain.srv/page_path';
break;
case 'userid':
case 'user_id':
$query_user_args[$arg->variable_name] =
'user_login_name';
break;
case 'number':
case 'numbers':
$query_user_args[$arg->variable_name] =
10982431;
break;
case 'alpha':
$query_user_args[$arg->variable_name] =
'textStringLine';
break;
case 'alpha_number':
$query_user_args[$arg->variable_name] =
'textString1234Line2';
break;
}
}
}
}
if(!array_key_exists($arg->variable_name, $query_user_args))
{
$query_user_args[$arg->variable_name] = sprintf('%06d', $k);
}
if(isset($arg->argument_validator))
{
if(isset($arg->argument_validator->min_length))
{
$query_user_args[$arg->variable_name] =
str_pad
(
$query_user_args[$arg->variable_name],
$arg->argument_validator->min_length,
isset($arg->argument_validator->filter) &&
(
$arg->argument_validator->filter == 'number'
||
$arg->argument_validator->filter == 'numbers'
)
? '0' : 'M'
);
}
if(isset($arg->argument_validator->max_length))
{
$query_user_args[$arg->variable_name] =
substr
(
$query_user_args[$arg->variable_name],
0,
$arg->argument_validator->max_length
);
}
}
}
$k++;
}
$resultset =
$GLOBALS['__DB__'][$db_type]->_executeQuery
(
$unlink_tmpfile->file_name, // cache_file
(object)$query_user_args, // source_args
basename($argv[$i]), // query_id
array() // arg_columns
);
if(is_a($resultset, 'Object') && !$resultset->toBool())
{
throw new XmlSchemaError($argv[$i], -1, 'mysql SQL query generation failed');
}
else
{
print "\n";
print pathinfo($argv[$i], PATHINFO_FILENAME);
print " {$db_type} query:\n";
print $GLOBALS['__DB__'][$db_type]->queries;
print "\n";
}
}
}
}
}
if(!$success)
{
libXmlDisplayError($argv[$i], TRUE);
}
}
catch (XmlSchemaError $exc)
{
$retcode->code(ReturnCode::RETCODE_BUILTIN_CHECKS);
fwrite(STDERR, $exc->getMessage() . "\n");
}
catch (ErrorMessage $exc)
{
if($retcode->code() == ReturnCode::RETCODE_SUCCESS)
{
$retcode->code(ReturnCode::RETCODE_VALIDATOR_INTERNAL);
}
fwrite(STDERR, $exc->getMessage() . "\n");
libXmlDisplayError($argv[$i]);
}
catch (ErrorException $exc)
{
if($retcode->code() == ReturnCode::RETCODE_SUCCESS)
{
$retcode->code(ReturnCode::RETCODE_VALIDATOR_INTERNAL);
}
fwrite(STDERR, "{$exc->getFile()}({$exc->getLine()}):\n\t{$exc->getMessage()}.\n");
fwrite(STDERR, $exc->getTraceAsString());
libXmlDisplayError($argv[$i]);
}
catch (Exception $exc)
{
$retcode->code(ReturnCode::RETCODE_VALIDATOR_INTERNAL);
fwrite(STDERR, $exc->getMessage() . "\n");
fwrite(STDERR, $exc->getTraceAsString());
libXmlDisplayError($argv[$i]);
}
}
exit($retcode->code());
}
catch (SyntaxError $syntax)
{
fwrite(STDERR, $syntax->getMessage() . "\n");
exit(254); // wrong command line
// 255 is reserved by php (for parse errors, etc.)
}
catch (ErrorMessage $exc)
{
fwrite(STDERR, $exc->getMessage() . "\n");
libXmlDisplayError();
exit(ReturnCode::RETCODE_VALIDATOR_INTERNAL); // internal validator error
}
catch (Exception $exc)
{
fwrite(STDERR, $exc->getFile() . '(' . $exc->getLine() . ")\n\t" . $exc->getMessage() . "\n");
fwrite(STDERR, $exc->getTraceAsString());
libXmlDisplayError();
exit(ReturnCode::RETCODE_VALIDATOR_INTERNAL); // internal validator error
}
/* End of file validate.php */
/* Location: tools/dbxml_validator/validate.php */