mirror of
https://github.com/Lastorder-DC/rhymix.git
synced 2026-01-04 01:01:41 +09:00
741 lines
15 KiB
PHP
741 lines
15 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Validator class
|
|
*
|
|
* @author NAVER (developers@xpressengine.com)
|
|
*/
|
|
class Validator
|
|
{
|
|
|
|
/**
|
|
* cache directory
|
|
* @var string
|
|
*/
|
|
var $_cache_dir = '';
|
|
|
|
/**
|
|
* last error
|
|
* @var array
|
|
*/
|
|
var $_last_error;
|
|
|
|
/**
|
|
* xml ruleset object
|
|
* @var Xml_Node_ object
|
|
*/
|
|
var $_xml_ruleset = NULL;
|
|
|
|
/**
|
|
* rule list
|
|
* @var array
|
|
*/
|
|
var $_rules = [];
|
|
|
|
/**
|
|
* filter list
|
|
* @var array
|
|
*/
|
|
var $_filters = [];
|
|
|
|
/**
|
|
* custom message list
|
|
* @var array
|
|
*/
|
|
var $_message = [];
|
|
|
|
/**
|
|
* custom field name list
|
|
* @var array
|
|
*/
|
|
var $_fieldNames = [];
|
|
|
|
/**
|
|
* validator version
|
|
* @var string
|
|
*/
|
|
var $_version = '1.0';
|
|
|
|
/**
|
|
* ruleset xml file path
|
|
* @var string
|
|
*/
|
|
var $_xml_path = '';
|
|
|
|
/**
|
|
* @constructor
|
|
* @param string $xml_path
|
|
* @return void
|
|
*/
|
|
function __construct($xml_path = '')
|
|
{
|
|
if ($xml_path)
|
|
{
|
|
$this->load($xml_path);
|
|
}
|
|
|
|
// predefined rules
|
|
$this->addRule(array(
|
|
'email' => '/^[\w-]+((?:\.|\+|\~)[\w-]+)*@[\w-]+(\.[\w-]+)+$/',
|
|
'userid' => '/^[a-z]+[\w-]*[a-z0-9_]+$/i',
|
|
'url' => '/^https?:\/\//i',
|
|
'alpha' => '/^[a-z]*$/i',
|
|
'alpha_number' => '/^[a-z][a-z0-9_]*$/i',
|
|
'number' => '/^(?:[1-9]\\d*|0)$/',
|
|
'float' => '/^\d+(\.\d+)?$/'
|
|
));
|
|
|
|
$this->_cache_dir = RX_BASEDIR . 'files/cache';
|
|
}
|
|
|
|
/**
|
|
* @destructor
|
|
* @return void
|
|
*/
|
|
function __destruct()
|
|
{
|
|
$this->_rules = NULL;
|
|
$this->_filters = NULL;
|
|
}
|
|
|
|
/**
|
|
* Load a xml file
|
|
* @param string $xml_path A file name to be loaded
|
|
* @return boolean
|
|
*/
|
|
function load($xml_path)
|
|
{
|
|
if (!file_exists($xml_path) || !is_readable($xml_path))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
$output = Rhymix\Framework\Parsers\RulesetParser::loadXML($xml_path);
|
|
if (!$output)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ($output->rules)
|
|
{
|
|
$this->addRule($output->rules);
|
|
}
|
|
$this->_filters = $output->filters;
|
|
$this->_message = $output->messages;
|
|
$this->_fieldNames = $output->fieldsNames;
|
|
$this->_xml_path = $xml_path;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Set root cache directory
|
|
* @param string $cache_dir Root cache directory
|
|
* @return void
|
|
*/
|
|
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 array $fields Target fields. The keys of the array represents field's name, its values represents field's value.
|
|
* @return boolean TRUE if it is valid, FALSE otherwise.
|
|
*/
|
|
function validate($fields_ = null)
|
|
{
|
|
if(is_array($fields_))
|
|
{
|
|
$fields = $fields_;
|
|
}
|
|
else
|
|
{
|
|
$args = array_keys($this->_filters);
|
|
$fields = (array) Context::getRequestVars();
|
|
}
|
|
|
|
if(!is_array($fields))
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
$filter_default = array(
|
|
'required' => 'false',
|
|
'default' => '',
|
|
'modifiers' => array(),
|
|
'length' => 0,
|
|
'equalto' => 0,
|
|
'rule' => 0,
|
|
'if' => array()
|
|
);
|
|
|
|
$fields = array_map(array($this, 'arrayTrim'), $fields);
|
|
$field_names = array_keys($fields);
|
|
|
|
$filters = array();
|
|
|
|
// get field names matching patterns
|
|
foreach($this->_filters as $key => $filter)
|
|
{
|
|
$names = array();
|
|
if($key[0] == '^')
|
|
{
|
|
$names = preg_grep('/^' . preg_quote(substr($key, 1)) . '/', $field_names);
|
|
}
|
|
elseif(substr($key, -2) == '[]')
|
|
{
|
|
$filters[substr($key, 0, -2)] = $filter;
|
|
unset($filters[$key]);
|
|
}
|
|
else
|
|
{
|
|
$filters[$key] = $filter;
|
|
}
|
|
|
|
if(!count($names))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach($names as $name)
|
|
{
|
|
$filters[$name] = $filter;
|
|
}
|
|
unset($filters[$key]);
|
|
}
|
|
|
|
foreach($filters as $key => $filter)
|
|
{
|
|
$fname = preg_replace('/\[\]$/', '', $key);
|
|
$filter = array_merge($filter_default, $filter);
|
|
|
|
if(preg_match("/(^[a-z_]*)[\[](?:\'|\")?([a-z_]*)(?:\'|\")?[\]]$/i", $key, $matches))
|
|
{
|
|
$exists = array_key_exists($matches[1], $fields);
|
|
$value = $exists ? $fields[$matches[1]][$matches[2]] : NULL;
|
|
}
|
|
else
|
|
{
|
|
$exists = array_key_exists($key, $fields);
|
|
$value = $exists ? $fields[$fname] : NULL;
|
|
}
|
|
|
|
if(is_array($value))
|
|
{
|
|
if(!isset($value['tmp_name']))
|
|
{
|
|
$value = implode('', $value);
|
|
}
|
|
else
|
|
{
|
|
$value = $value['name'];
|
|
}
|
|
}
|
|
|
|
// conditional statement
|
|
foreach($filter['if'] as $cond)
|
|
{
|
|
if(!isset($cond['test']) || !isset($cond['attr']))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$expr = '!!(' . preg_replace('/\\$(\w+)/', '$value[\'$1\']', $cond['test']) . ')';
|
|
if(self::_execExpression($fields, $expr))
|
|
{
|
|
$filter[$cond['attr']] = $cond['value'];
|
|
}
|
|
}
|
|
|
|
// attr : default
|
|
if(!$value && strlen($default = trim($filter['default'])))
|
|
{
|
|
$value = $default;
|
|
if(is_null($fields_))
|
|
{
|
|
Context::set($fname, $value);
|
|
}
|
|
else
|
|
{
|
|
$fields_[$fname] = $value;
|
|
}
|
|
}
|
|
$value_len = strlen($value);
|
|
|
|
// attr : modifier
|
|
if(is_string($modifiers = $filter['modifiers']))
|
|
{
|
|
$modifiers = explode(',', trim($modifiers));
|
|
}
|
|
|
|
// attr : required
|
|
if(isset($filter['required']) && $filter['required'] === 'true' && !$value_len)
|
|
{
|
|
return $this->error($key, 'isnull');
|
|
}
|
|
|
|
// if the field wasn't passed, ignore this value
|
|
if(!$exists && !$value_len)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// 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 = mb_strlen($value, 'UTF-8');
|
|
}
|
|
|
|
if(($min && $min > ($is_min_b ? $strbytes : $strlength)) || ($max && $max < ($is_max_b ? $strbytes : $strlength)))
|
|
{
|
|
return $this->error($key, 'outofrange');
|
|
}
|
|
}
|
|
|
|
// equalto
|
|
if($equalto = $filter['equalto'] ?? '')
|
|
{
|
|
if(!array_key_exists($equalto, $fields) || trim($fields[$equalto]) !== $value)
|
|
{
|
|
return $this->error($key, 'equalto');
|
|
}
|
|
}
|
|
|
|
// rules
|
|
if($rules = $filter['rule'] ?? '')
|
|
{
|
|
$rules = explode(',', $rules);
|
|
foreach($rules as $rule)
|
|
{
|
|
$result = $this->applyRule($rule, $value);
|
|
// apply the 'not' modifier
|
|
if(in_array('not', $modifiers))
|
|
{
|
|
$result = !$result;
|
|
}
|
|
if(!$result)
|
|
{
|
|
return $this->error($key, 'invalid_' . $rule);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* apply trim recursive
|
|
* @param string|array $array
|
|
* @return string|array
|
|
*/
|
|
function arrayTrim($array)
|
|
{
|
|
if(!is_array($array) && !is_object($array))
|
|
{
|
|
return trim($array);
|
|
}
|
|
|
|
foreach($array as $key => $value)
|
|
{
|
|
$array[$key] = $this->arrayTrim($value);
|
|
}
|
|
|
|
return $array;
|
|
}
|
|
|
|
/**
|
|
* Log an error
|
|
* @param $msg error message
|
|
* @return boolean always false
|
|
*/
|
|
function error($field, $msg)
|
|
{
|
|
if(isset($this->_message[$msg]))
|
|
{
|
|
$msg = $this->_message[$msg];
|
|
}
|
|
else
|
|
{
|
|
$lang_filter = lang('filter');
|
|
$msg = isset($lang_filter->{$msg}) ? $lang_filter->{$msg} : $lang_filter->invalid;
|
|
}
|
|
|
|
if(isset($this->_fieldNames[$field]))
|
|
{
|
|
$fieldName = $this->_fieldNames[$field];
|
|
}
|
|
else
|
|
{
|
|
$fieldName = lang($field);
|
|
}
|
|
|
|
$msg = sprintf($msg, $fieldName);
|
|
|
|
$this->_last_error = array('field' => $field, 'msg' => $msg);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* Returns the last error infomation including a field name and an error message.
|
|
* @return array The last error infomation
|
|
*/
|
|
function getLastError()
|
|
{
|
|
return $this->_last_error;
|
|
}
|
|
|
|
/**
|
|
* Add a new rule
|
|
* @param string $name rule name
|
|
* @param mixed $rule
|
|
* @return void
|
|
*/
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove a rule
|
|
* @param string $name rule name
|
|
* @return void
|
|
*/
|
|
function removeRule($name)
|
|
{
|
|
unset($this->_rules[$name]);
|
|
}
|
|
|
|
/**
|
|
* add filter to filter list
|
|
* @param string $name rule name
|
|
* @param string $filter filter
|
|
* @return void
|
|
*/
|
|
function addFilter($name, $filter = '')
|
|
{
|
|
if(is_array($name))
|
|
{
|
|
$args = $name;
|
|
}
|
|
else
|
|
{
|
|
$args = array($name => $filter);
|
|
}
|
|
|
|
foreach($args as $name => $filter)
|
|
{
|
|
if(!$filter)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if(isset($filter['if']))
|
|
{
|
|
if(is_array($filter['if']) && count($filter['if']))
|
|
{
|
|
$key = key($filter['if']);
|
|
if(!is_int($key))
|
|
{
|
|
$filter['if'] = array($filter['if']);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
unset($filter['if']);
|
|
}
|
|
}
|
|
|
|
$this->_filters[$name] = $filter;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* remove filter from filter list
|
|
* @param string $name rule name
|
|
* @return void
|
|
*/
|
|
function removeFilter($name)
|
|
{
|
|
unset($this->_filters[$name]);
|
|
}
|
|
|
|
/**
|
|
* Find whether the field is valid with the rule
|
|
* @param string $name rule name
|
|
* @param string $value a value to be validated
|
|
* @return boolean TRUE if the field is valid, FALSE otherwise.
|
|
*/
|
|
function applyRule($name, $value)
|
|
{
|
|
$rule = $this->_rules[$name];
|
|
|
|
if(is_array($value) && isset($value['tmp_name']))
|
|
{
|
|
$value = $value['name'];
|
|
}
|
|
|
|
switch($rule['type'])
|
|
{
|
|
case 'regex':
|
|
return (preg_match($rule['test'], $value) > 0);
|
|
case 'enum':
|
|
return in_array($value, $rule['test']);
|
|
case 'expr':
|
|
if(isset($rule['func_test']) && is_callable($rule['func_test']))
|
|
{
|
|
return $rule['func_test']($value);
|
|
}
|
|
else
|
|
{
|
|
$expr = '(' . preg_replace('/\$\$/', '$value', html_entity_decode($rule['test'])) . ')';
|
|
return self::_execExpression($value, $expr);
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* Returns compiled javascript file path. The path begins from XE root directory.
|
|
* @return string Compiled JavaScript file path
|
|
*/
|
|
function getJsPath()
|
|
{
|
|
if(!$this->_cache_dir)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
$dir = $this->_cache_dir . '/ruleset';
|
|
if(!is_dir($dir) && !mkdir($dir))
|
|
{
|
|
return FALSE;
|
|
}
|
|
if(!$this->_xml_path)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
// current language
|
|
$lang_type = class_exists('Context', false) ? Context::getLangType() : 'en';
|
|
|
|
// check the file
|
|
$filepath = $dir . '/' . md5($this->_version . ' ' . $this->_xml_path) . ".{$lang_type}.js";
|
|
if(is_readable($filepath) && filemtime($filepath) > filemtime($this->_xml_path))
|
|
{
|
|
return $filepath;
|
|
}
|
|
|
|
$content = $this->_compile2js();
|
|
if($content === FALSE)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
Rhymix\Framework\Storage::write($filepath, $content);
|
|
|
|
return $filepath;
|
|
}
|
|
|
|
/**
|
|
* Compile a ruleset to a javascript file
|
|
* @return string
|
|
*/
|
|
function _compile2js()
|
|
{
|
|
global $lang;
|
|
|
|
$ruleset = basename($this->_xml_path, '.xml');
|
|
$content = array();
|
|
|
|
if(preg_match('@(^|/)files/ruleset/\w+\.xml$@i', $this->_xml_path))
|
|
{
|
|
$ruleset = '@' . $ruleset;
|
|
}
|
|
|
|
list($ruleset) = explode('.', $ruleset);
|
|
|
|
// current language
|
|
$lang_type = class_exists('Context', false) ? Context::getLangType() : 'en';
|
|
|
|
// custom rulesets
|
|
$addrules = array();
|
|
foreach($this->_rules as $name => $rule)
|
|
{
|
|
$name = strtolower($name);
|
|
|
|
if(in_array($name, array('email', 'userid', 'url', 'alpha', 'alpha_number', 'number', 'float')))
|
|
{
|
|
continue;
|
|
}
|
|
switch($rule['type'])
|
|
{
|
|
case 'regex':
|
|
$addrules[] = "v.cast('ADD_RULE', ['{$name}', {$rule['test']}]);";
|
|
break;
|
|
case 'enum':
|
|
$enums = '"' . implode('","', $rule['test']) . '"';
|
|
$addrules[] = "v.cast('ADD_RULE', ['{$name}', function($$){ return ($.inArray($$,[{$enums}]) > -1); }]);";
|
|
break;
|
|
case 'expr':
|
|
$addrules[] = "v.cast('ADD_RULE', ['{$name}', function($$){ return ({$rule['test']}); }]);";
|
|
break;
|
|
}
|
|
|
|
// if have a message, add message
|
|
if(isset($rule['message']))
|
|
{
|
|
$text = preg_replace('@\r?\n@', '\\n', addslashes($rule['message']));
|
|
$addrules[] = "v.cast('ADD_MESSAGE',['invalid_{$name}','{$text}']);";
|
|
}
|
|
}
|
|
$addrules = implode('', $addrules);
|
|
|
|
// filters
|
|
$content = array();
|
|
$messages = array();
|
|
foreach($this->_filters as $name => $filter)
|
|
{
|
|
$field = array();
|
|
|
|
// form filed name
|
|
if(isset($filter['title']))
|
|
{
|
|
$field_lang = addslashes($filter['title']);
|
|
$messages[] = "v.cast('ADD_MESSAGE',['{$name}','{$field_lang}']);";
|
|
}
|
|
elseif(isset($lang->{$name}))
|
|
{
|
|
$field_lang = addslashes($lang->{$name});
|
|
$messages[] = "v.cast('ADD_MESSAGE',['{$name}','{$field_lang}']);";
|
|
}
|
|
|
|
if(isset($filter['required']) && $filter['required'] == 'true')
|
|
{
|
|
$field[] = 'required:true';
|
|
}
|
|
if(isset($filter['rule']) && $filter['rule'])
|
|
{
|
|
$field[] = "rule:'" . strtolower($filter['rule']) . "'";
|
|
}
|
|
if(isset($filter['default']) && $filter['default'])
|
|
{
|
|
$field[] = "default:'{$filter['default']}'";
|
|
}
|
|
if(isset($filter['modifier']) && $filter['modifier'])
|
|
{
|
|
$field[] = "modifier:'{$filter['modifier']}'";
|
|
}
|
|
if(isset($filter['length']) && $filter['length'])
|
|
{
|
|
list($min, $max) = explode(':', $filter['length']);
|
|
if($min)
|
|
{
|
|
$field[] = "minlength:'{$min}'";
|
|
}
|
|
if($max)
|
|
{
|
|
$field[] = "maxlength:'{$max}'";
|
|
}
|
|
}
|
|
if(isset($filter['if']) && $filter['if'])
|
|
{
|
|
$ifs = array();
|
|
if(!isset($filter['if'][0]))
|
|
{
|
|
$filter['if'] = array($filter['if']);
|
|
}
|
|
foreach($filter['if'] as $if)
|
|
{
|
|
$ifs[] = "{test:'" . addslashes($if['test']) . "', attr:'{$if['attr']}', value:'" . addslashes($if['value']) . "'}";
|
|
}
|
|
$field[] = "'if':[" . implode(',', $ifs) . "]";
|
|
}
|
|
if(count($field))
|
|
{
|
|
$field = '{' . implode(',', $field) . '}';
|
|
$content[] = "'{$name}':{$field}";
|
|
}
|
|
}
|
|
|
|
if(!$content)
|
|
{
|
|
return '/* Error : empty ruleset */';
|
|
}
|
|
|
|
// error messages
|
|
foreach($lang->filter as $key => $text)
|
|
{
|
|
if($text)
|
|
{
|
|
$text = preg_replace('@\r?\n@', '\\n', addslashes($text));
|
|
$messages[] = "v.cast('ADD_MESSAGE',['{$key}','{$text}']);";
|
|
}
|
|
}
|
|
|
|
$content = implode(',', $content);
|
|
$messages = implode("\n", $messages);
|
|
|
|
return "(function($,v){\nv=xe.getApp('validator')[0];if(!v)return;\n{$addrules}\nv.cast('ADD_FILTER',['{$ruleset}', {{$content}}]);\n{$messages}\n})(jQuery);";
|
|
}
|
|
|
|
/**
|
|
* Polyfill for create_function()
|
|
*
|
|
* @param mixed $value
|
|
* @param string $expression
|
|
* @return mixed
|
|
*/
|
|
protected static function _execExpression($value, $expression)
|
|
{
|
|
$hash_key = sha1($expression);
|
|
$filename = RX_BASEDIR . 'files/cache/validator/' . $hash_key . '.php';
|
|
if (!Rhymix\Framework\Storage::exists($filename))
|
|
{
|
|
$buff = '<?php if(!defined(\'RX_VERSION\')) return;' . "\n" . 'return ' . $expression . ';' . "\n";
|
|
Rhymix\Framework\Storage::write($filename, $buff);
|
|
}
|
|
return (include $filename);
|
|
}
|
|
|
|
}
|