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_file($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 information including a field name and an error message. * @return array The last error information */ 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 = '