#!/bin/env php */ /** 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:
		file_name (line_no):
			message
		
@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 elements.' ); } else { $has_conditions_element = TRUE; } } else { throw new XmlSchemaError( $xml_file, $child_node->getLineNo(), ' element must be the only content for a joined .' ); } } } if(!$has_conditions_element) { throw new XmlSchemaError( $xml_file, $table_element->getLineNo(), 'Expected 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(), '
element can only have content if it is a sub-query or is joined.' ); } } } } } /** All table names and aliases should be distinct throughout the 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, " element found when has a 'var' or 'default' attribute." ); } if(!($child_query_node || $has_var_attribute)) { throw new XmlSchemaError( $xml_file, $condition->getLineNo(), ": either a 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 , and elements are present. There can be any number of 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" . ": 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(), ": 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" . ": 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(), ": '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(), ": '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 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 */