XpressEngine Core  1.11.2
 All Classes Namespaces Files Functions Variables Pages
Validator.class.php
Go to the documentation of this file.
1 <?php
2 /* Copyright (C) NAVER <http://www.navercorp.com> */
3 
10 class Validator
11 {
12 
17  var $_cache_dir = '';
18 
24 
29  var $_xml_ruleset = NULL;
30 
35  var $_rules;
36 
41  var $_filters;
42 
48 
54 
60 
65  var $_version = '1.0';
66 
71  var $_xml_path = '';
72 
78  function __construct($xml_path = '')
79  {
80  $this->_rules = array();
81  $this->_filters = array();
82  $this->_xml_ruleset = NULL;
83 
84  if($xml_path)
85  $this->load($xml_path);
86 
87  // predefined rules
88  $this->addRule(array(
89  'email' => '/^[\w-]+((?:\.|\+|\~)[\w-]+)*@[\w-]+(\.[\w-]+)+$/',
90  'userid' => '/^[a-z]+[\w-]*[a-z0-9_]+$/i',
91  'url' => '/^(https?|ftp|mms):\/\/[0-9a-z-]+(\.[_0-9a-z-]+)+(:\d+)?/',
92  'alpha' => '/^[a-z]*$/i',
93  'alpha_number' => '/^[a-z][a-z0-9_]*$/i',
94  'number' => '/^(?:[1-9]\\d*|0)$/',
95  'float' => '/^\d+(\.\d+)?$/'
96  ));
97 
98  $this->_has_mb_func = is_callable('mb_strlen');
99  $this->setCacheDir(_XE_PATH_ . 'files/cache');
100  }
101 
106  function __destruct()
107  {
108  $this->_rules = NULL;
109  $this->_filters = NULL;
110  }
111 
117  function load($xml_path)
118  {
119  $this->_xml_ruleset = NULL;
120 
121  $xml_path = realpath($xml_path);
122  if(!is_readable($xml_path))
123  {
124  return FALSE;
125  }
126 
127  $parser = new XmlParser();
128  $xml = $parser->loadXmlFile($xml_path);
129  if(!isset($xml->ruleset) || !isset($xml->ruleset->fields) || !isset($xml->ruleset->fields->field))
130  {
131  return FALSE;
132  }
133 
134  // custom rules
135  if(isset($xml->ruleset->customrules) && isset($xml->ruleset->customrules->rule))
136  {
137  $customrules = $xml->ruleset->customrules->rule;
138  if(!is_array($customrules))
139  {
140  $customrules = array($customrules);
141  }
142 
143  $rules = array();
144  $messages = array();
145  foreach($customrules as $rule)
146  {
147  if(!isset($rule->attrs) || !isset($rule->attrs->name))
148  {
149  continue;
150  }
151 
152  $message = $rule->message ? $rule->message->body : NULL;
153  $rule = (array) $rule->attrs;
154  $rule['message'] = $message;
155  $name = $rule['name'];
156  unset($rule['name']);
157 
158  $rules[$name] = $rule;
159  if(isset($message))
160  {
161  $messages['invalid_' . $name] = $message;
162  }
163  }
164  if(count($rules))
165  {
166  $this->addRule($rules);
167  }
168  }
169 
170  // filters
171  $fields = $xml->ruleset->fields->field;
172  if(!is_array($fields))
173  {
174  $fields = array($fields);
175  }
176 
177  $filters = array();
178  $fieldsNames = array();
179  foreach($fields as $field)
180  {
181  $name = '';
182  $filter = array();
183 
184  if(!isset($field->attrs) || !isset($field->attrs->name))
185  {
186  continue;
187  }
188 
189  $title = $field->title ? $field->title->body : NULL;
190  $filter = (array) $field->attrs;
191  $filter['title'] = $title;
192 
193  $name = $filter['name'];
194  if(isset($title))
195  {
196  $fieldsNames[$name] = $title;
197  }
198 
199  unset($filter['name']);
200 
201  // conditional statement
202  if(isset($field->if))
203  {
204  $if = $field->if;
205  if(!is_array($if))
206  {
207  $if = array($if);
208  }
209  foreach($if as $idx => $cond)
210  {
211  $if[$idx] = (array) $cond->attrs;
212  }
213  $filter['if'] = $if;
214  }
215 
216  $filters[$name] = $filter;
217  }
218 
219  $this->_xml_ruleset = $xml->ruleset;
220  $this->_filters = $filters;
221  $this->_message = $messages;
222  $this->_fieldNames = $fieldsNames;
223  $this->_xml_path = $xml_path;
224 
225  return TRUE;
226  }
227 
233  function setCacheDir($cache_dir)
234  {
235  if(is_dir($cache_dir))
236  {
237  $this->_cache_dir = preg_replace('@/$@', '', $cache_dir);
238  }
239  }
240 
246  function validate($fields_ = null)
247  {
248  if(is_array($fields_))
249  {
250  $fields = $fields_;
251  }
252  else
253  {
254  $args = array_keys($this->_filters);
255  $fields = (array) Context::getRequestVars();
256  }
257 
258  if(!is_array($fields))
259  {
260  return TRUE;
261  }
262 
263  $filter_default = array(
264  'required' => 'false',
265  'default' => '',
266  'modifiers' => array(),
267  'length' => 0,
268  'equalto' => 0,
269  'rule' => 0,
270  'if' => array()
271  );
272 
273  $fields = array_map(array($this, 'arrayTrim'), $fields);
274  $field_names = array_keys($fields);
275 
276  $filters = array();
277 
278  // get field names matching patterns
279  foreach($this->_filters as $key => $filter)
280  {
281  $names = array();
282  if($key{0} == '^')
283  {
284  $names = preg_grep('/^' . preg_quote(substr($key, 1)) . '/', $field_names);
285  }
286  elseif(substr($key, -2) == '[]')
287  {
288  $filters[substr($key, 0, -2)] = $filter;
289  unset($filters[$key]);
290  }
291  else
292  {
293  $filters[$key] = $filter;
294  }
295 
296  if(!count($names))
297  {
298  continue;
299  }
300 
301  foreach($names as $name)
302  {
303  $filters[$name] = $filter;
304  }
305  unset($filters[$key]);
306  }
307 
308  foreach($filters as $key => $filter)
309  {
310  $fname = preg_replace('/\[\]$/', '', $key);
311  $filter = array_merge($filter_default, $filter);
312 
313  if(preg_match("/(^[a-z_]*)[\[](?:\'|\")?([a-z_]*)(?:\'|\")?[\]]$/i", $key, $matches))
314  {
315  $exists = array_key_exists($matches[1], $fields);
316  $value = $exists ? $fields[$matches[1]][$matches[2]] : NULL;
317  }
318  else
319  {
320  $exists = array_key_exists($key, $fields);
321  $value = $exists ? $fields[$fname] : NULL;
322  }
323 
324  if(is_array($value))
325  {
326  if(!isset($value[tmp_name]))
327  {
328  $value = implode('', $value);
329  }
330  else
331  {
332  $value = $value['name'];
333  }
334  }
335 
336  // conditional statement
337  foreach($filter['if'] as $cond)
338  {
339  if(!isset($cond['test']) || !isset($cond['attr']))
340  {
341  continue;
342  }
343 
344  $func_body = preg_replace('/\\$(\w+)/', '$c[\'$1\']', $cond['test']);
345  $func = create_function('$c', "return !!({$func_body});");
346 
347  if($func($fields))
348  {
349  $filter[$cond['attr']] = $cond['value'];
350  }
351  }
352 
353  // attr : default
354  if(!$value && strlen($default = trim($filter['default'])))
355  {
356  $value = $default;
357  if(is_null($fields_))
358  {
359  Context::set($fname, $value);
360  }
361  else
362  {
363  $fields_[$fname] = $value;
364  }
365  }
366  $value_len = strlen($value);
367 
368  // attr : modifier
369  if(is_string($modifiers = $filter['modifiers']))
370  {
371  $modifiers = explode(',', trim($modifiers));
372  }
373 
374  // attr : required
375  if($filter['required'] === 'true' && !$value_len)
376  {
377  return $this->error($key, 'isnull');
378  }
379 
380  // if the field wasn't passed, ignore this value
381  if(!$exists && !$value_len)
382  {
383  continue;
384  }
385 
386  // attr : length
387  if($length = $filter['length'])
388  {
389  list($min, $max) = explode(':', trim($length));
390  $is_min_b = (substr($min, -1) === 'b');
391  $is_max_b = (substr($max, -1) === 'b');
392  list($min, $max) = array((int) $min, (int) $max);
393 
394  $strbytes = strlen($value);
395  if(!$is_min_b || !$is_max_b)
396  {
397  $strlength = $this->_has_mb_func ? mb_strlen($value, 'utf-8') : $this->mbStrLen($value);
398  }
399 
400  if(($min && $min > ($is_min_b ? $strbytes : $strlength)) || ($max && $max < ($is_max_b ? $strbytes : $strlength)))
401  {
402  return $this->error($key, 'outofrange');
403  }
404  }
405 
406  // equalto
407  if($equalto = $filter['equalto'])
408  {
409  if(!array_key_exists($equalto, $fields) || trim($fields[$equalto]) !== $value)
410  {
411  return $this->error($key, 'equalto');
412  }
413  }
414 
415  // rules
416  if($rules = $filter['rule'])
417  {
418  $rules = explode(',', $rules);
419  foreach($rules as $rule)
420  {
421  $result = $this->applyRule($rule, $value);
422  // apply the 'not' modifier
423  if(in_array('not', $modifiers))
424  {
425  $result = !$result;
426  }
427  if(!$result)
428  {
429  return $this->error($key, 'invalid_' . $rule);
430  }
431  }
432  }
433  }
434 
435  return TRUE;
436  }
437 
443  function arrayTrim($array)
444  {
445  if(!is_array($array))
446  {
447  return trim($array);
448  }
449 
450  foreach($array as $key => $value)
451  {
452  $array[$key] = $this->arrayTrim($value);
453  }
454 
455  return $array;
456  }
457 
463  function error($field, $msg)
464  {
465  if(isset($this->_message[$msg]))
466  {
467  $msg = $this->_message[$msg];
468  }
469  else
470  {
471  $lang_filter = Context::getLang('filter');
472  $msg = isset($lang_filter->{$msg}) ? $lang_filter->{$msg} : $lang_filter->invalid;
473  }
474 
475  if(isset($this->_fieldNames[$field]))
476  {
477  $fieldName = $this->_fieldNames[$field];
478  }
479  else
480  {
481  $fieldName = Context::getLang($field);
482  }
483 
484  $msg = sprintf($msg, $fieldName);
485 
486  $this->_last_error = array('field' => $field, 'msg' => $msg);
487 
488  return FALSE;
489  }
490 
495  function getLastError()
496  {
497  return $this->_last_error;
498  }
499 
506  function addRule($name, $rule = '')
507  {
508  if(is_array($name))
509  {
510  $args = $name;
511  }
512  else
513  {
514  $args = array($name => $rule);
515  }
516 
517  foreach($args as $name => $rule)
518  {
519  if(!$rule)
520  {
521  continue;
522  }
523  if(is_string($rule))
524  {
525  $rule = array('type' => 'regex', 'test' => $rule);
526  }
527 
528  if($rule['type'] == 'enum')
529  {
530  $delim = isset($rule['delim']) ? $rule['delim'] : ',';
531  $rule['test'] = explode($delim, $rule['test']);
532  }
533 
534  $this->_rules[$name] = $rule;
535  }
536  }
537 
543  function removeRule($name)
544  {
545  unset($this->_rules[$name]);
546  }
547 
554  function addFilter($name, $filter = '')
555  {
556  if(is_array($name))
557  {
558  $args = $name;
559  }
560  else
561  {
562  $args = array($name => $filter);
563  }
564 
565  foreach($args as $name => $filter)
566  {
567  if(!$filter)
568  {
569  continue;
570  }
571 
572  if(isset($filter['if']))
573  {
574  if(is_array($filter['if']) && count($filter['if']))
575  {
576  $key = key($filter['if']);
577  if(!is_int($key))
578  {
579  $filter['if'] = array($filter['if']);
580  }
581  }
582  else
583  {
584  unset($filter['if']);
585  }
586  }
587 
588  $this->_filters[$name] = $filter;
589  }
590  }
591 
597  function removeFilter($name)
598  {
599  unset($this->_filters[$name]);
600  }
601 
608  function applyRule($name, $value)
609  {
610  $rule = $this->_rules[$name];
611 
612  if(is_array($value) && isset($value['tmp_name']))
613  {
614  $value = $value['name'];
615  }
616 
617  switch($rule['type'])
618  {
619  case 'regex':
620  return (preg_match($rule['test'], $value) > 0);
621  case 'enum':
622  return in_array($value, $rule['test']);
623  case 'expr':
624  if(!$rule['func_test'])
625  {
626  $rule['func_test'] = create_function('$a', 'return (' . preg_replace('/\$\$/', '$a', html_entity_decode($rule['test'])) . ');');
627  }
628  return $rule['func_test']($value);
629  }
630 
631  return TRUE;
632  }
633 
639  function mbStrLen($str)
640  {
641  $arr = count_chars($str);
642  for($i = 0x80; $i < 0xc0; $i++)
643  {
644  unset($arr[$i]);
645  }
646  return array_sum($arr);
647  }
648 
653  function getJsPath()
654  {
655  if(!$this->_cache_dir)
656  {
657  return FALSE;
658  }
659 
660  $dir = $this->_cache_dir . '/ruleset';
661  if(!is_dir($dir) && !mkdir($dir))
662  {
663  return FALSE;
664  }
665  if(!$this->_xml_path)
666  {
667  return FALSE;
668  }
669 
670  // current language
671  $lang_type = class_exists('Context', false) ? Context::getLangType() : 'en';
672 
673  // check the file
674  $filepath = $dir . '/' . md5($this->_version . ' ' . $this->_xml_path) . ".{$lang_type}.js";
675  if(is_readable($filepath) && filemtime($filepath) > filemtime($this->_xml_path))
676  {
677  return $filepath;
678  }
679 
680  $content = $this->_compile2js();
681  if($content === FALSE)
682  {
683  return FALSE;
684  }
685 
686  FileHandler::writeFile($filepath, $content);
687 
688  return $filepath;
689  }
690 
695  function _compile2js()
696  {
697  global $lang;
698 
699  $ruleset = basename($this->_xml_path, '.xml');
700  $content = array();
701 
702  if(preg_match('@(^|/)files/ruleset/\w+\.xml$@i', $this->_xml_path))
703  {
704  $ruleset = '@' . $ruleset;
705  }
706 
707  list($ruleset) = explode('.', $ruleset);
708 
709  // current language
710  $lang_type = class_exists('Context', false) ? Context::getLangType() : 'en';
711 
712  // custom rulesets
713  $addrules = array();
714  foreach($this->_rules as $name => $rule)
715  {
716  $name = strtolower($name);
717 
718  if(in_array($name, array('email', 'userid', 'url', 'alpha', 'alpha_number', 'number', 'float')))
719  {
720  continue;
721  }
722 
723  switch($rule['type'])
724  {
725  case 'regex':
726  $addrules[] = "v.cast('ADD_RULE', ['{$name}', {$rule['test']}]);";
727  break;
728  case 'enum':
729  $enums = '"' . implode('","', $rule['test']) . '"';
730  $addrules[] = "v.cast('ADD_RULE', ['{$name}', function($$){ return ($.inArray($$,[{$enums}]) > -1); }]);";
731  break;
732  case 'expr':
733  $addrules[] = "v.cast('ADD_RULE', ['{$name}', function($$){ return ({$rule['test']}); }]);";
734  break;
735  }
736 
737  // if have a message, add message
738  if(isset($rule['message']))
739  {
740  $text = preg_replace('@\r?\n@', '\\n', addslashes($rule['message']));
741  $addrules[] = "v.cast('ADD_MESSAGE',['invalid_{$name}','{$text}']);";
742  }
743  }
744  $addrules = implode('', $addrules);
745 
746  // filters
747  $content = array();
748  $messages = array();
749  foreach($this->_filters as $name => $filter)
750  {
751  $field = array();
752 
753  // form filed name
754  if(isset($filter['title']))
755  {
756  $field_lang = addslashes($filter['title']);
757  $messages[] = "v.cast('ADD_MESSAGE',['{$name}','{$field_lang}']);";
758  }
759  elseif(isset($lang->{$name}))
760  {
761  $field_lang = addslashes($lang->{$name});
762  $messages[] = "v.cast('ADD_MESSAGE',['{$name}','{$field_lang}']);";
763  }
764 
765  if($filter['required'] == 'true')
766  {
767  $field[] = 'required:true';
768  }
769  if($filter['rule'])
770  {
771  $field[] = "rule:'" . strtolower($filter['rule']) . "'";
772  }
773  if($filter['default'])
774  {
775  $field[] = "default:'{$filter['default']}'";
776  }
777  if($filter['modifier'])
778  {
779  $field[] = "modifier:'{$filter['modifier']}'";
780  }
781  if($filter['length'])
782  {
783  list($min, $max) = explode(':', $filter['length']);
784  if($min)
785  {
786  $field[] = "minlength:'{$min}'";
787  }
788  if($max)
789  {
790  $field[] = "maxlength:'{$max}'";
791  }
792  }
793  if($filter['if'])
794  {
795  $ifs = array();
796  if(!isset($filter['if'][0]))
797  {
798  $filter['if'] = array($filter['if']);
799  }
800  foreach($filter['if'] as $if)
801  {
802  $ifs[] = "{test:'" . addslashes($if['test']) . "', attr:'{$if['attr']}', value:'" . addslashes($if['value']) . "'}";
803  }
804  $field[] = "'if':[" . implode(',', $ifs) . "]";
805  }
806  if(count($field))
807  {
808  $field = '{' . implode(',', $field) . '}';
809  $content[] = "'{$name}':{$field}";
810  }
811  }
812 
813  if(!$content)
814  {
815  return '/* Error : empty ruleset */';
816  }
817 
818  // error messages
819  foreach($lang->filter as $key => $text)
820  {
821  if($text)
822  {
823  $text = preg_replace('@\r?\n@', '\\n', addslashes($text));
824  $messages[] = "v.cast('ADD_MESSAGE',['{$key}','{$text}']);";
825  }
826  }
827 
828  $content = implode(',', $content);
829  $messages = implode("\n", $messages);
830 
831  return "(function($,v){\nv=xe.getApp('validator')[0];if(!v)return;\n{$addrules}\nv.cast('ADD_FILTER',['{$ruleset}', {{$content}}]);\n{$messages}\n})(jQuery);";
832  }
833 
834 }
835 /* End of file Validator.class.php */
836 /* Location: ./classes/validator/Validator.class.php */
error($field, $msg)
setCacheDir($cache_dir)
set($key, $val, $set_to_get_vars=0)
writeFile($filename, $buff, $mode="w")
load($xml_path)
applyRule($name, $value)
__construct($xml_path= '')
$args
Definition: ko.install.php:185
getLang($code)
const _XE_PATH_
Definition: config.inc.php:49
removeFilter($name)
removeRule($name)
validate($fields_=null)
addFilter($name, $filter= '')
arrayTrim($array)
addRule($name, $rule= '')
if(isset($_REQUEST['encode'])) if(isset($_REQUEST['decode'])) $lang
Definition: example.php:23