formML = $formML; // Extract the validation rules $extractor =& new FormRuleExtractor($formML); $this->rules =& $extractor->rules; $this->errormsgs =& $extractor->errormsgs; } function validate() { if (!$_POST) { return false; } $this->errors = array(); foreach ($this->rules as $name => $rules) { // Check if compulsory field has not been filled if ($rules['compulsory'] && (!isset($_POST[$name]) || trim($_POST[$name]) == '')) { $this->errors[$name] = $this->getErrorMsg($name, 'compulsory'); continue; } if (!isset($_POST[$name])) { // Field not set, and it's not compulsory continue; } $value = $_POST[$name]; // If a regular expression is specified, check using that if (isset($rules['regexp']) && !preg_match($rules['regexp'], $value)) { $this->errors[$name] = $this->getErrorMsg($name, 'regexp'); continue; } // If there is a mustmatch rule, check using that if (isset($rules['mustmatch']) && $value != $_POST[$rules['mustmatch']]) { $this->errors[$name] = $this->getErrorMsg($name, 'mustmatch'); continue; } // If there is a calback rule, run that function if (isset($rules['callback'])) { $callback = $rules['callback']; if (substr($callback, 0, 5) == 'this:') { // It's actually a method on this class $method = substr($callback, 5); if (!$this->$method($value)) { $this->errors[$name] = $this->getErrorMsg($name, 'callback'); continue; } } else { // It's just a normal function if (!$callback($value)) { $this->errors[$name] = $this->getErrorMsg($name, 'callback'); continue; } } } } // All rules should now have been processed return count($this->errors) == 0; } function display() { echo $this->form(); } function form() { // Returns the XHTML for the form, after processing $this->output = ''; $this->_parser = xml_parser_create(); // Set XML parser to take the case of tags in to account xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, false); // Set XML parser callback functions xml_set_object($this->_parser, $this); xml_set_element_handler($this->_parser, 'tag_open', 'tag_close'); xml_set_character_data_handler($this->_parser, 'cdata'); if (!xml_parse($this->_parser, $this->formML)) { die(sprintf('XML error: %s at line %d', xml_error_string(xml_get_error_code($this->_parser)), xml_get_current_line_number($this->_parser))); } xml_parser_free($this->_parser); return $this->output; } function tag_open($parser, $tag, $attr) { if (!$this->_copy) { return; } if ($this->_saveForTemplate) { if ($tag == 'message') { $this->_errorTemplate .= '%%%MESSAGE_HERE%%%'; } else { $this->_errorTemplate .= '<'.$tag.$this->_makeAttr($attr); if (in_array($tag, array('br', 'img'))) { $this->_errorTemplate .= ' />'; } else { $this->_errorTemplate .= '>'; } } return; } $killattrs = array('compulsory', 'validate', 'regexp', 'callback', 'mustmatch', 'errormsg'); // Kill unrequired attributes foreach ($killattrs as $a) { unset($attr[$a]); } switch ($tag) { case 'error': if (!$_POST || !isset($this->errors[$attr['for']])) { $this->_copy = false; } return; case 'errormsg': $this->_copy = false; return; case 'errorlist': if (!$_POST) { // Stop copying $this->_copy = false; return; } else { // We are going to be redisplaying, so keep copying return; } case 'erroritem': $this->_saveForTemplate = true; return; case 'img': case 'br': // Empty tags $this->output .= '<'.$tag.$this->_makeAttr($attr).' />'; return; case 'input': // Add the value attribute, if redisplaying if (isset($_POST[$attr['name']]) && $attr['type'] != 'password') { $attr['value'] = htmlentities(stripslashes($_POST[$attr['name']])); } // Add en error class if an error occured if (isset($this->errors[$attr['name']])) { $attr['class'] = isset($attr['class']) ? $attr['class'].' '.$this->errorClass : $this->errorClass; } $this->output .= '<'.$tag.$this->_makeAttr($attr).' />'; return; case 'textarea': $this->_inTextArea = $attr['name']; } // Add tag to the output $this->output .= '<'.$tag.$this->_makeAttr($attr).'>'; } function cdata($parser, $data) { if ($this->_saveForTemplate) { $this->_errorTemplate .= $data; return; } if ($this->_copy) { $this->output.= $data; } } function tag_close($parser, $tag) { if ($this->_saveForTemplate && $tag != 'erroritem' && !in_array($tag, array('br', 'img', 'message'))) { $this->_errorTemplate .= ''; return; } switch ($tag) { case 'error': case 'errormsg': case 'errorlist': $this->_copy = true; break; case 'erroritem': // Stop saving this in the template $this->_saveForTemplate = false; // Now output all error messages using that template foreach ($this->errors as $name => $error) { $this->output .= str_replace('%%%MESSAGE_HERE%%%', $error, $this->_errorTemplate); } return; case 'textarea': if (isset($_POST[$this->_inTextArea])) { $this->output .= htmlentities(stripslashes($_POST[$this->_inTextArea])); } $this->output .= ''; $this->_inTextArea = false; break; case 'img': case 'br': case 'input': case 'message': // Empty tags break; default: if ($this->_copy) { $this->output.= ''; } } } function _makeAttr($attr) { $html = ' '; foreach ($attr as $name => $value) { $html .= $name.'="'.$value.'" '; } return substr($html, 0, -1); // Remove trailing space } function getErrorMsg($field, $test) { if (isset($this->errormsgs["$field:$test"])) { return $this->errormsgs["$field:$test"]; } // No error message has been specified - generate one based on the $test switch ($test) { case 'regexp': return "Field '$field' contained invalid data"; case 'compulsory': return "Field '$field' must be filled in "; case 'mustmatch': return "'$field' must match '{$this->rules[$field]['mustmatch']}'"; case 'callback': default: return "Field '$field' was not valid"; } } function checkEmail($email) { // Used as callback or validate="email" shortcut return preg_match( '#^([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)$#', $email ); // Regexp from http://www.regexplib.com/REDetails.aspx?regexp_id=26 } } class FormRuleExtractor { // Extracts the validation rules from the formML. // Implementation note: the 'validate' attribute is a convenience, it is converted in // to either a regular expression rule or a callback var $rules = array(); var $errormsgs; // Following variables used during XML parsing var $_parser; var $_errorMsgName; var $_errorMsgValue; var $_collectErrorMsg = false; function FormRuleExtractor($formML) { $this->_parser = xml_parser_create(); // Set XML parser to take the case of tags in to account xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, false); // Set XML parser callback functions xml_set_object($this->_parser, $this); xml_set_element_handler($this->_parser, 'tag_open', 'tag_close'); xml_set_character_data_handler($this->_parser, 'cdata'); if (!xml_parse($this->_parser, $formML)) { die(sprintf('XML error: %s at line %d', xml_error_string(xml_get_error_code($this->_parser)), xml_get_current_line_number($this->_parser))); } xml_parser_free($this->_parser); } function tag_open($parser, $tag, $attr) { // First, the stuff to deal with the errormsg tag and contents if ($tag == 'errormsg') { if (!isset($attr['field']) || !isset($attr['test'])) { // Die noisily die('FormML error: errormsg tag needs field and test attributes'); } $this->_errorMsgName = $attr['field'].':'.$attr['test']; $this->_errorMsgValue = ''; $this->_collectErrorMsg = true; return; } if ($this->_collectErrorMsg) { $this->_errorMsgValue .= '<'.$tag.$this->_makeAttr($attr); if (in_array($tag, array('br', 'img'))) { $this->_errorMsgValue .= ' />'; } else { $this->_errorMsgValue .= '>'; } } // Now the stuff to deal with everything else if (!in_array($tag, array('input', 'select', 'textarea'))) { return; } // Skip submit, image and reset fields if (isset($attr['type']) && in_array($attr['type'], array('submit', 'reset', 'image'))) { return; } $rules = array(); if (isset($attr['type'])) { $rules['type'] = $attr['type']; } else { $rules['type'] = $tag; } $name = $attr['name']; // compulsory="yes" if (isset($attr['compulsory']) && $attr['compulsory'] == 'yes') { $rules['compulsory'] = true; } // validate="something" if (isset($attr['validate'])) { switch ($attr['validate']) { case 'alpha': $rules['regexp'] = '|^[a-zA-Z]*$|'; break; case 'alphanumeric': $rules['regexp'] = '|^[a-zA-Z0-9]*$|'; break; case 'numeric': $rules['regexp'] = '|^[a-zA-Z]*$|'; break; case 'email': $rules['callback'] = 'this:checkEmail'; break; } } // callback="someFunction" if (isset($attr['callback'])) { $rules['callback'] = $attr['callback']; } // regexp="someregexp" if (isset($attr['regexp'])) { $rules['regexp'] = $attr['regexp']; } // mustmatch="something" if (isset($attr['mustmatch'])) { $rules['mustmatch'] = $attr['mustmatch']; } // Save the rules to $this->rules $this->rules[$name] = $rules; } function cdata($parser, $data) { if ($this->_collectErrorMsg) { $this->_errorMsgValue .= $data; } } function tag_close($parser, $tag) { if ($tag == 'errormsg') { $this->errormsgs[$this->_errorMsgName] = $this->_errorMsgValue; $this->_collectErrorMsg = false; } if ($this->_collectErrorMsg && !in_array($tag, array('br', 'img'))) { $this->_errorMsgValue .= ''; } } function _makeAttr($attr) { $html = ' '; foreach ($attr as $name => $value) { $html .= $name.'="'.$value.'" '; } return substr($html, 0, -1); // Remove trailing space } } ?>