mirror of
https://github.com/Lastorder-DC/rhymix.git
synced 2026-01-09 03:32:00 +09:00
issue 18 : Done for writing the ruleset compiler
git-svn-id: http://xe-core.googlecode.com/svn/branches/1.5.0@8454 201d5d3c-b55e-5fd7-737f-ddc643e51545
This commit is contained in:
parent
2770b28ee9
commit
23453c0aac
4 changed files with 410 additions and 18 deletions
|
|
@ -4,33 +4,184 @@
|
|||
*/
|
||||
class Validator
|
||||
{
|
||||
var $_cache_dir = '';
|
||||
var $_last_error;
|
||||
var $_xml_ruleset = null;
|
||||
var $_rules;
|
||||
var $_filters;
|
||||
var $_has_mb_func;
|
||||
var $_version = '1.0';
|
||||
var $_xml_path = '';
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
function Validator($xml_path){
|
||||
function Validator($xml_path=''){
|
||||
if($xml_path) $this->load($xml_path);
|
||||
|
||||
$this->_rules = array();
|
||||
$this->_filters = array();
|
||||
$this->_xml_ruleset = null;
|
||||
|
||||
// predefined rules
|
||||
$this->addRule(array(
|
||||
'email' => '/^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/',
|
||||
'userid' => '/^[a-z]+[\w-]*[a-z0-9_]+$/i',
|
||||
'url' => '/^(https?|ftp|mms):\/\/[0-9a-z-]+(\.[_0-9a-z-]+)+(:[0-9]+)/',
|
||||
'alpha' => '/^[a-z]*$/i',
|
||||
'alpha_number' => '/^[a-z][a-z0-9_]*$/i',
|
||||
'number' => '/^[1-9][0-9]*$/'
|
||||
));
|
||||
|
||||
$this->_has_mb_func = is_callable('mb_strlen');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a xml file
|
||||
* @param[in] string $xml_path A file name to be loaded
|
||||
*/
|
||||
function load($xml_path) {
|
||||
$this->_xml_ruleset = null;
|
||||
|
||||
$xml_path = realpath($xml_path);
|
||||
if(!is_readable($xml_path)) return false;
|
||||
|
||||
$parser = new XmlParser();
|
||||
$xml = $parser->loadXmlFile($xml_path);
|
||||
if(!isset($xml->ruleset) || !isset($xml->ruleset->fields) || !isset($xml->ruleset->fields->field)) return false;
|
||||
|
||||
// custom rules
|
||||
if(isset($xml->ruleset->customrules) && isset($xml->ruleset->customrules->rule)) {
|
||||
$customrules = $xml->ruleset->customrules->rule;
|
||||
if(!is_array($customrules)) $customrules = array($customrules);
|
||||
|
||||
$rules = array();
|
||||
foreach($customrules as $rule) {
|
||||
if(!isset($rule->attrs) || !isset($rule->attrs->name)) continue;
|
||||
|
||||
$rule = (array)$rule->attrs;
|
||||
$name = $rule['name'];
|
||||
unset($rule['name']);
|
||||
|
||||
$rules[$name] = $rule;
|
||||
}
|
||||
if(count($rules)) $this->addRule($rules);
|
||||
}
|
||||
|
||||
// filters
|
||||
$fields = $xml->ruleset->fields->field;
|
||||
if(!is_array($fields)) $fields = array($fields);
|
||||
|
||||
$filters = array();
|
||||
foreach($fields as $field) {
|
||||
$name = '';
|
||||
$filter = array();
|
||||
|
||||
if(!isset($field->attrs) || !isset($field->attrs->name)) continue;
|
||||
$filter = (array)$field->attrs;
|
||||
|
||||
$name = $filter['name'];
|
||||
unset($filter['name']);
|
||||
|
||||
$filters[$name] = $filter;
|
||||
}
|
||||
|
||||
$this->_xml_ruleset = $xml->ruleset;
|
||||
$this->_filters = $filters;
|
||||
$this->_xml_path = $xml_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set root cache directory
|
||||
* @param[in] string $cache_dir Root cache directory
|
||||
*/
|
||||
function setCacheDir($cache_dir){
|
||||
}
|
||||
|
||||
/**
|
||||
* Set target fields to be checked.
|
||||
* The keys of array represents filed's name, its values represents field's vale.
|
||||
* @param[in] array $fields Target fields
|
||||
*/
|
||||
function setFields($fields){
|
||||
function setCacheDir($_cache_dir){
|
||||
if(is_dir($cache_dir)) {
|
||||
$this->$_cache_dir = preg_replace('@/$@', '', $cache_dir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the fields. If the fields aren't passed, validation will be execute on the Context variables.
|
||||
* @param[in] (optional) array $fields
|
||||
* @param[in] (optional) array $fields Target fields. The keys of the array represents field's name, its values represents field's value.
|
||||
* @return bool True if it is valid, FALSE otherwise.
|
||||
*/
|
||||
function validate($fields){
|
||||
function validate($fields_=null){
|
||||
if(is_array($fields_)) {
|
||||
$fields = $fields_;
|
||||
} else {
|
||||
$args = array_keys($this->_filters);
|
||||
$fields = (array)call_user_func_array(array('Context','gets'), $args);
|
||||
}
|
||||
|
||||
if(!is_array($fields)) return true;
|
||||
|
||||
$filter_default = array(
|
||||
'default' => 0,
|
||||
'modifiers' => array(),
|
||||
'length' => 0,
|
||||
'equalto' => 0,
|
||||
'rule' => 0
|
||||
);
|
||||
|
||||
foreach($this->_filters as $key=>$filter) {
|
||||
$value = isset($fields[$key])?trim($fields[$key]):'';
|
||||
$filter = array_merge($filter_default, $filter);
|
||||
|
||||
// attr : default
|
||||
if(!strlen($value) && ($default=trim($filter['default']))) {
|
||||
$value = $default;
|
||||
if(is_null($fields_)) Context::set($key, $value);
|
||||
else $fields_[$key] = $value;
|
||||
}
|
||||
|
||||
// attr : modifier
|
||||
if(is_string($modifiers=$filter['modifiers'])) $modifiers = explode(',', trim($modifiers));
|
||||
|
||||
// attr : required
|
||||
if(!$value && $filter['required'] === 'true') return $this->error($key, '');
|
||||
|
||||
// attr : length
|
||||
if($length=$filter['length']){
|
||||
list($min, $max) = explode(':', trim($length));
|
||||
$is_min_b = (substr($min, -1) === 'b');
|
||||
$is_max_b = (substr($max, -1) === 'b');
|
||||
list($min, $max) = array((int)$min, (int)$max);
|
||||
|
||||
$strbytes = strlen($value);
|
||||
if(!$is_min_b || !$is_max_b){
|
||||
$strlength = $this->_has_mb_func?mb_strlen($value,'utf-8'):$this->mbStrLen($value);
|
||||
}
|
||||
|
||||
if($min > ($is_min_b?$strbytes:$strlength) || $max < ($is_max_b?$strbytes:$strlength)) return $this->error($key, 'length');
|
||||
}
|
||||
|
||||
// equalto
|
||||
if($equalto=$filter['equalto']){
|
||||
if(!array_key_exists($equalto, $fields) || trim($fields[$equalto]) !== $value) return $this->error($key, 'equalto');
|
||||
}
|
||||
|
||||
// rule
|
||||
if($rule=$filter['rule']){
|
||||
$result = $this->applyRule($rule, $value);
|
||||
// apply the 'not' modifier
|
||||
if(in_array('not', $modifiers)) $result = !$result;
|
||||
if(!$result) return $this->error($key, 'rule error');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error
|
||||
* @param[in] $msg error message
|
||||
* @return always false
|
||||
*/
|
||||
function error($field, $msg){
|
||||
$this->_last_error = array('field'=>$field, 'msg'=>$msg);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -38,14 +189,29 @@ class Validator
|
|||
* @return array The last error infomation
|
||||
*/
|
||||
function getLastError(){
|
||||
return $this->_last_error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new rule
|
||||
* @param[in] string $name rule name
|
||||
* @param[in] string $rule
|
||||
* @param[in] mixed $rule
|
||||
*/
|
||||
function addRule($name, $rule){
|
||||
function addRule($name, $rule=''){
|
||||
if(is_array($name)) $args = $name;
|
||||
else $args = array($name=>$rule);
|
||||
|
||||
foreach($args as $name=>$rule){
|
||||
if(!$rule) continue;
|
||||
if(is_string($rule)) $rule = array('type'=>'regex', 'test'=>$rule);
|
||||
|
||||
if($rule['type'] == 'enum') {
|
||||
$delim = isset($rule['delim'])?$rule['delim']:',';
|
||||
$rule['test'] = explode($delim, $rule['test']);
|
||||
}
|
||||
|
||||
$this->_rules[$name] = $rule;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -53,15 +219,55 @@ class Validator
|
|||
* @param[in] string $name rule name
|
||||
*/
|
||||
function removeRule($name){
|
||||
unset($this->_rules[$name]);
|
||||
}
|
||||
|
||||
function addFilter($name, $filter='') {
|
||||
if(is_array($name)) $args = $name;
|
||||
else $args = array($name=>$filter);
|
||||
|
||||
foreach($args as $name=>$filter) {
|
||||
if($filter) $this->_filters[$name] = $filter;
|
||||
}
|
||||
}
|
||||
|
||||
function removeFilter($name) {
|
||||
unset($this->_filters[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find whether the field is valid with the rule
|
||||
* @param[in] string $name rule name
|
||||
* @param[in] string $field field name
|
||||
* @param[in] string $value a value to be validated
|
||||
* @return bool TRUE if the field is valid, FALSE otherwise.
|
||||
*/
|
||||
function applyRule($name, $field){
|
||||
function applyRule($name, $value){
|
||||
$rule = $this->_rules[$name];
|
||||
|
||||
switch($rule['type']) {
|
||||
case 'regex':
|
||||
return (preg_match($rule['test'], $value) > 0);
|
||||
case 'enum':
|
||||
return in_array($value, $rule['test']);
|
||||
case 'expr':
|
||||
if(!$rule['func_test']) {
|
||||
$rule['func_test'] = create_function('$a', 'return ('.preg_replace('/$$/', '$a', $rule['test']).');');
|
||||
}
|
||||
return $rule['func_test']($value);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return
|
||||
*/
|
||||
function mbStrLen($str){
|
||||
$arr = count_chars($str);
|
||||
for($i=0x80; $i < 0xc0; $i++) {
|
||||
unset($arr[$i]);
|
||||
}
|
||||
return array_sum($arr);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -69,6 +275,84 @@ class Validator
|
|||
* @return string Compiled JavaScript file path
|
||||
*/
|
||||
function getJsPath(){
|
||||
if(!$this->_cache_dir) return false;
|
||||
|
||||
$dir = $this->_cache_dir.'/rulesets';
|
||||
if(!is_dir($dir) && !mkdir($dir)) return false;
|
||||
if(!$this->_xml_path) return false;
|
||||
|
||||
$filepath = $dir.'/'.md5($this->_version.' '.$this->_xml_path).'.js';
|
||||
if(is_readable($filepath) && filemtime($filepath) > filemtime($this->_xml_path)) return $filepath;
|
||||
|
||||
$content = $this->_compile2js();
|
||||
if($content === false) return false;
|
||||
|
||||
if(is_callable('file_put_contents')) {
|
||||
@file_put_contents($filepath, $content);
|
||||
} else {
|
||||
$fp = @fopen($filepath, 'w');
|
||||
if(is_resource($fp)) {
|
||||
fwrite($fp, $content);
|
||||
fclose($fp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a ruleset to a javascript file
|
||||
* @private
|
||||
*/
|
||||
function _compile2js() {
|
||||
$content = array();
|
||||
|
||||
// custom rulesets
|
||||
foreach($this->_rules as $name=>$rule) {
|
||||
if(strpos('email,userid,url,alpha,alpha_number,number,', $name.',') !== false) continue;
|
||||
switch($rule['type']) {
|
||||
case 'regex':
|
||||
$content[] = "v.addRule('{$name}', {$rule['test']});";
|
||||
break;
|
||||
case 'enum':
|
||||
$enums = '"'.implode('","', $rule['test']).'"';
|
||||
$content[] = "v.addRule('{$name}', function($$){ return ($.inArray($$,[{$enums}]) > -1); });";
|
||||
break;
|
||||
case 'expr':
|
||||
$content[] = "v.addRule('{$name}', function($$){ return ({$rule['test']}); });";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// filters
|
||||
foreach($this->_filters as $name=>$filter) {
|
||||
$field = array();
|
||||
|
||||
if($filter['required'] == 'true') $field[] = 'required:true';
|
||||
if($filter['rule']) $field[] = "rule:'{$filter['rule']}'";
|
||||
if($filter['default']) $field[] = "default:'{$filter['default']}'";
|
||||
if($filter['modifier']) $field[] = "modifier:'{$filter['modifier']}'";
|
||||
if($filter['length']) {
|
||||
list($min, $max) = explode(':', $filter['length']);
|
||||
if($min) $field[] = "minlength:'{$min}'";
|
||||
if($max) $field[] = "maxlength:'{$max}'";
|
||||
}
|
||||
if(count($field)) {
|
||||
$field = '{'.implode(',', $field).'}';
|
||||
$content[] = "v.addFilter('{$name}', {$field});";
|
||||
}
|
||||
}
|
||||
|
||||
if(count($content)) {
|
||||
array_unshift($content,
|
||||
'(function($){',
|
||||
'var v = xe.getApp("validator")[0];',
|
||||
'if(!v) return false;'
|
||||
);
|
||||
$content[] = '})(jQuery);'; // array_push
|
||||
|
||||
return implode("\n", $content);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@
|
|||
}
|
||||
// uncheck the language if no specific language is set.
|
||||
} else {
|
||||
unset($this->lang);
|
||||
$this->lang = '';
|
||||
}
|
||||
|
||||
$this->oParser = xml_parser_create('UTF-8');
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
if($this->lang&&$cur_obj->attrs->{'xml:lang'}&&$cur_obj->attrs->{'xml:lang'}!=$this->lang) return;
|
||||
if($this->lang&&$parent_obj->{$node_name}->attrs->{'xml:lang'}&&$parent_obj->{$node_name}->attrs->{'xml:lang'}!=$this->lang) return;
|
||||
|
||||
if($parent_obj->{$node_name}) {
|
||||
if(isset($parent_obj->{$node_name})) {
|
||||
$tmp_obj = $parent_obj->{$node_name};
|
||||
if(is_array($tmp_obj)) {
|
||||
array_push($parent_obj->{$node_name}, $cur_obj);
|
||||
|
|
|
|||
101
tests/classes/validator/ValidatorTest.php
Normal file
101
tests/classes/validator/ValidatorTest.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
define('__DEBUG__', 1);
|
||||
require dirname(__FILE__).'/../../../classes/xml/XmlParser.class.php';
|
||||
require dirname(__FILE__).'/../../../classes/handler/Handler.class.php';
|
||||
require dirname(__FILE__).'/../../../classes/file/FileHandler.class.php';
|
||||
require dirname(__FILE__).'/../../../classes/validator/Validator.class.php';
|
||||
|
||||
error_reporting(E_ALL & ~E_NOTICE);
|
||||
|
||||
class ValidatorTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function _testRequired() {
|
||||
$vd = new Validator();
|
||||
$vd->addFilter('userid', array('required'=>'true'));
|
||||
|
||||
// given data
|
||||
$this->assertFalse( $vd->validate(array('no-userid'=>'hello')) );
|
||||
$this->assertTrue( $vd->validate(array('userid'=>'myuserid')) );
|
||||
$this->assertFalse( $vd->validate(array('userid'=>'')) );
|
||||
|
||||
// context data
|
||||
$this->assertFalse( $vd->validate() );
|
||||
Context::set('userid', '');
|
||||
$this->assertFalse( $vd->validate() );
|
||||
Context::set('userid', 'myuserid');
|
||||
$this->assertTrue( $vd->validate() );
|
||||
$vd->removeFilter('userid');
|
||||
$this->assertTrue( $vd->validate() );
|
||||
}
|
||||
|
||||
public function testDefault() {
|
||||
global $mock_vars;
|
||||
|
||||
$vd = new Validator();
|
||||
$vd->addFilter('userid', array('default'=>'ididid'));
|
||||
|
||||
// given data
|
||||
$arr = array('no-userid'=>'');
|
||||
$vd->validate($arr);
|
||||
$this->assertEquals( $arr, array('no-userid'=>'') );
|
||||
|
||||
$arr = array('userid'=>'');
|
||||
$vd->validate(&$arr); // pass-by-reference
|
||||
$this->assertEquals( $arr, array('userid'=>'ididid') );
|
||||
|
||||
$arr = array('userid'=>'ownid');
|
||||
$vd->validate(&$arr);
|
||||
$this->assertEquals( $arr, array('userid'=>'ownid') );
|
||||
|
||||
// context data
|
||||
$mock_vars = array(); // empty context variables
|
||||
$vd->validate();
|
||||
$this->assertEquals( 'ididid', Context::get('userid') );
|
||||
|
||||
$vd->load(dirname(__FILE__).'/login.xml');
|
||||
|
||||
Context::set('userid', '');
|
||||
$vd->validate();
|
||||
$this->assertEquals( 'idididid', Context::get('userid') );
|
||||
}
|
||||
|
||||
public function testCustomRule() {
|
||||
}
|
||||
|
||||
public function testJSCompile() {
|
||||
}
|
||||
}
|
||||
|
||||
$mock_vars = array();
|
||||
|
||||
class Context
|
||||
{
|
||||
public function gets() {
|
||||
global $mock_vars;
|
||||
|
||||
$args = func_get_args();
|
||||
$output = new stdClass;
|
||||
|
||||
foreach($args as $name) {
|
||||
if(array_key_exists($name, $mock_vars)) $output->{$name} = $mock_vars[$name];
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function get($name) {
|
||||
global $mock_vars;
|
||||
return array_key_exists($name, $mock_vars)?$mock_vars[$name]:'';
|
||||
}
|
||||
|
||||
public function set($name, $value) {
|
||||
global $mock_vars;
|
||||
|
||||
$mock_vars[$name] = $value;
|
||||
}
|
||||
|
||||
public function getLangType() {
|
||||
return 'en';
|
||||
}
|
||||
}
|
||||
7
tests/classes/validator/login.xml
Normal file
7
tests/classes/validator/login.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ruleset>
|
||||
<fields>
|
||||
<field name="userid" required="true" length="5:10" rule="userid" default="idididid" />
|
||||
<field name="password" required="true" />
|
||||
</fields>
|
||||
</ruleset>
|
||||
Loading…
Add table
Add a link
Reference in a new issue