$exception["key"], "index" => isset($exception["index"]) ? $exception["index"] : 0 ); } if(isset($exception["children"])) { $results = array_merge($results, $this->DoGetOffendingKeys($exception["children"])); } } return $results; } public function __construct($message, $exceptions) { $this->message = $message; $this->exceptions = $exceptions; } public function GetErrors() { /* We just need to return a flattened version of the exception list here. */ $results = array(); foreach($this->exceptions as $exception_list) { $results = array_merge($results, $exception_list); } return $results; } public function GetErrorMessages($custom_map = array()) { $flattened = $this->GetErrors(); $results = array(); foreach($flattened as $exception) { if(!empty($custom_map) && array_key_exists($exception["error_type"], $custom_map) && array_key_exists($exception["key"], $custom_map[$exception["error_type"]])) { /* A custom error message was defined for this particular key/type error combination. */ $results[] = $custom_map[$exception["error_type"]][$exception["key"]]; } else { /* Use default error message. */ $results[] = $exception["error_msg"]; } } return $results; } public function GetOffendingKeys() { $results = array(); foreach($this->exceptions as $exception_list) { $results = array_merge($results, $this->DoGetOffendingKeys($exception_list)); } return $results; } } class ImmediateAbort extends FormValidationException { } class CPHPFormValidatorPromiseBaseClass { public $previous = null; public $next = null; public function __construct($creator) { $this->previous = $creator; } public function StartResolve() { /* Back and forth! */ if($this->previous == $this->handler) { $this->ContinueResolve(array()); } else { $this->previous->StartResolve(); } } public function ContinueResolve($results) { $own_result = $this->Resolve($results); if(is_null($own_result) === false) { $results[] = $own_result; } if(is_null($this->next) === false) { $this->next->ContinueResolve($results); } else { $this->ValidationFinished($results); } } public function ValidationFinished($results) { if(count($results) > 0) { throw new FormValidationException("One or more validation steps failed.", $results); } } /* Operators */ public function Either($error_message) { $this->next = new CPHPFormValidatorOperatorEither($this, $error_message, array_slice(func_get_args(), 1)); $this->next->handler = $this->handler; return $this->next; } public function All($error_message) { $this->next = new CPHPFormValidatorOperatorAll($this, $error_message, array_slice(func_get_args(), 1)); $this->next->handler = $this->handler; return $this->next; } public function Switch_($varname, $error_message) { $this->next = new CPHPFormValidatorOperatorSwitch($this, $varname, $error_message, array_slice(func_get_args(), 2)); $this->next->handler = $this->handler; return $this->next; } public function Case_($value) { $this->next = new CPHPFormValidatorOperatorCase($this, $value, array_slice(func_get_args(), 1)); $this->next->handler = $this->handler; return $this->next; } /* Special instructions */ public function AbortIfErrors() { $this->next = new CPHPFormValidatorAbortIfErrors($this, $this->handler); $this->next->handler = $this->handler; return $this->next; } public function Done() { /* Trigger validation routine */ try { $this->StartResolve(); } catch (ImmediateAbort $e) { throw new FormValidationException("A critical validation step failed.", $e->exceptions); } } /* Validators */ public function RequireKey($key, $critical = false) { $this->next = new CPHPFormValidatorPromise($this, $this->handler, $key, array(), "required", "A value is required for this field.", $critical, function($key, $value, $args, $handler){ return isset($handler->formdata[$key]); }); $this->next->handler = $this->handler; return $this->next; } public function RequireNonEmpty($key, $critical = false) { $this->next = new CPHPFormValidatorPromise($this, $this->handler, $key, array(), "required", "The value for this field must not be empty.", $critical, function($key, $value, $args, $handler){ return trim($value) !== ""; }); $this->next->handler = $this->handler; return $this->next; } public function ValidateEmail($key, $critical = false) { $this->next = new CPHPFormValidatorPromise($this, $this->handler, $key, array(), "email", "The value is not a valid e-mail address.", $critical, function($key, $value, $args, $handler){ return filter_var($value, FILTER_VALIDATE_EMAIL) !== false; }); $this->next->handler = $this->handler; return $this->next; } public function ValidateNumeric($key, $critical = false) { $this->next = new CPHPFormValidatorPromise($this, $this->handler, $key, array(), "numeric", "The value is not numeric.", $critical, function($key, $value, $args, $handler){ return is_numeric($value) !== false; }); $this->next->handler = $this->handler; return $this->next; } public function ValidateUrl($key, $critical = false) { $this->next = new CPHPFormValidatorPromise($this, $this->handler, $key, array(), "url", "The value is not a valid URL.", $critical, function($key, $value, $args, $handler){ return filter_var($value, FILTER_VALIDATE_URL) !== false; }); $this->next->handler = $this->handler; return $this->next; } public function ValidateIp($key, $critical = false) { $this->next = new CPHPFormValidatorPromise($this, $this->handler, $key, array(), "ip", "The value is not a valid IP address.", $critical, function($key, $value, $args, $handler){ return filter_var($value, FILTER_VALIDATE_IP) !== false; }); $this->next->handler = $this->handler; return $this->next; } public function ValidateIpv4($key, $critical = false) { $this->next = new CPHPFormValidatorPromise($this, $this->handler, $key, array(), "ip4", "The value is not a valid IPv4 address.", $critical, function($key, $value, $args, $handler){ return filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false; }); $this->next->handler = $this->handler; return $this->next; } public function ValidateIpv6($key, $critical = false) { $this->next = new CPHPFormValidatorPromise($this, $this->handler, $key, array(), "ip6", "The value is not a valid IPv6 address.", $critical, function($key, $value, $args, $handler){ return filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false; }); $this->next->handler = $this->handler; return $this->next; } public function ValidatePublicIp($key, $critical = false) { $this->next = new CPHPFormValidatorPromise($this, $this->handler, $key, array(), "ip_public", "The value is not an IP in a publicly usable range.", $critical, function($key, $value, $args, $handler){ return filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false; }); $this->next->handler = $this->handler; return $this->next; } public function ValidatePrivateIp($key, $critical = false) { $this->next = new CPHPFormValidatorPromise($this, $this->handler, $key, array(), "ip_private", "The value is not an IP in a private range.", $critical, function($key, $value, $args, $handler){ return (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE | FILTER_FLAG_NO_PRIV_RANGE) === false && filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE) !== false); }); $this->next->handler = $this->handler; return $this->next; } public function ValidateRegex($key, $error_message, $pattern, $critical = false) { $this->next = new CPHPFormValidatorPromise($this, $this->handler, $key, array("pattern" => $pattern), "regex", $error_message, $critical, function($key, $value, $args, $handler){ return preg_match($args["pattern"], $value) === 1; }); $this->next->handler = $this->handler; return $this->next; } public function ValidateValue($key, $error_message, $values, $critical = false) { $this->next = new CPHPFormValidatorPromise($this, $this->handler, $key, array("values" => $values), "value", $error_message, $critical, function($key, $value, $args, $handler){ if(is_array($args["values"])) { return in_array($value, $args["values"]); } else { return ($args["values"] == $value); } }); $this->next->handler = $this->handler; return $this->next; } public function ValidateCustom($key, $error_message, $validator, $critical = false) { $this->next = new CPHPFormValidatorPromise($this, $this->handler, $key, array(), "custom", $error_message, $critical, $validator); $this->next->handler = $this->handler; return $this->next; } } class CPHPFormValidatorPromise extends CPHPFormValidatorPromiseBaseClass { public function __construct($creator, $handler, $key, $args, $error_type, $error_message, $critical, $function) { parent::__construct($creator); $this->key = $key; $this->func = $function; $this->args = $args; $this->error_type = $error_type; $this->error_message = $error_message; $this->critical = $critical; $this->handler = $handler; } public function Resolve($results) { $func = $this->func; /* WTF PHP? Why can't I call $this->func directly? */ $exceptions = array(); $values = isset($this->handler->formdata[$this->key]) ? $this->handler->formdata[$this->key] : null; if(is_array($values) === true) { /* Array */ foreach($values as $i => $value) { if($func($this->key, $value, $this->args, $this->handler) !== true) { $exceptions[] = array( "type" => "array_value", "key" => $this->key, "index" => $i, "error_type" => $this->error_type, "error_msg" => $this->error_message ); } } } else { /* Single value */ if($func($this->key, $values, $this->args, $this->handler) !== true) { $exceptions[] = array( "type" => "single", "key" => $this->key, "index" => 0, "error_type" => $this->error_type, "error_msg" => $this->error_message ); } } if(count($exceptions) > 0 && $this->critical === true) { $results[] = $exceptions; throw new ImmediateAbort("Critical validation did not pass.", $results); } if(count($exceptions) == 0) { return null; } else { return $exceptions; } } } class CPHPFormValidatorAbortIfErrors extends CPHPFormValidatorPromiseBaseClass { public function __construct($creator, $handler) { parent::__construct($creator); $this->handler = $handler; } public function Resolve($exceptions) { if(count($exceptions) > 0) { throw new FormValidationException("One or more validation errors before an AbortIfErrors statement.", $exceptions); } else { return null; } } } class CPHPFormValidatorOperator extends CPHPFormValidatorPromiseBaseClass { public function __construct($creator, $error_message, $children) { parent::__construct($creator); $this->error_message = $error_message; $this->children = $children; } } class CPHPFormValidatorOperatorEither extends CPHPFormValidatorOperator { public function Resolve($results) { $exceptions = array(); foreach($this->children as $child) { $result = $child->Resolve($exceptions); if(is_null($result) === false) { $exceptions[] = $result; } } if(count($exceptions) == count($this->children)) { return array(array( "type" => "operator", "error_type" => "either", "error_msg" => $this->error_message, "children" => $exceptions )); } else { return null; } } } class CPHPFormValidatorOperatorAll extends CPHPFormValidatorOperator { public function Resolve($results) { $exceptions = array(); foreach($this->children as $child) { $result = $child->Resolve($exceptions); if(is_null($result) === false) { $exceptions[] = $result; } } if(count($exceptions) > 0) { return array(array( "type" => "operator", "error_type" => "both", "error_msg" => $this->error_message, "children" => $exceptions )); } else { return null; } } } class CPHPFormValidatorOperatorSwitch extends CPHPFormValidatorOperator { /* The 'case' operator has a different constructor; it needs to accept both * an error message, and a variable to check. */ public function __construct($creator, $varname, $error_message, $children) { $this->varname = $varname; parent::__construct($creator, $error_message, $children); } public function Resolve($results) { $exceptions = array(); foreach($this->children as $child) { /* We have to set the variable name in the child here... only at * runtime can we establish the link between parent and child. */ $child->varname = $this->varname; $result = $child->Resolve($exceptions); if(is_null($result) === false) { $exceptions[] = $result; } } if(count($exceptions) > 0) { return array(array( "type" => "operator", "error_type" => "switch", "error_msg" => $this->error_message, "children" => $exceptions )); } else { return null; } } } class CPHPFormValidatorOperatorCase extends CPHPFormValidatorOperator { /* The 'case' operator has a different constructor; instead of an error message, * it is passed a "trigger value"; that is, the value on which it will execute. */ public function __construct($creator, $value, $children) { /* FIXME: Check if the parent really is a Switch operator... */ /* Grab the variable name to check from the parent. */ $this->value = $value; parent::__construct($creator, "", $children); } public function Resolve($results) { if(in_array($this->value, $this->handler->GetValues($this->varname))) { $exceptions = array(); foreach($this->children as $child) { $result = $child->Resolve($exceptions); if(is_null($result) === false) { $exceptions[] = $result; } } if(count($exceptions) > 0) { return array(array( /* TODO: Unpack case errors upon handling? Insignificant wrapper.. */ "type" => "operator", "error_type" => "case", "error_msg" => "Errors occurred.", "children" => $exceptions )); } else { return null; } } else { /* Case didn't trigger; always treat as success in that case. */ return null; } } } class CPHPFormHandler extends CPHPFormValidatorPromiseBaseClass { public function __construct($formdata = null, $no_csrf = false) { if(is_null($formdata)) { $this->formdata = $_POST; } else { $this->formdata = $formdata; } $this->no_csrf = $no_csrf; $this->handler = $this; $this->validation_exceptions = array(); $this->exception_buffer = array(); $this->first_validation = true; if($no_csrf === false) { CSRF::VerifyToken($this->formdata); } } public function StoreValidationException($exception, $validator_object) { if($validator_object == $this) { if($this->first_validation === true) { $this->first_validation = false; $this->validation_exceptions[] = $exception; } else { $this->exception_buffer[] = $exception; } } else { $this->validation_exceptions[] = $exception; } } public function RaiseValidationExceptions($aborted) { if(count($this->validation_exceptions) > 0) { throw new FormValidationException("One or more validation errors occurred.", $this->validation_exceptions); } $this->validation_exceptions = array(); } public function GetGroupedValues() { /* Returns an array of associative arrays. This is used for forms that have * multiple array inputs, and where each input has a corresponding element * for another input name. */ $keys = func_get_args(); $sCounts = array(); foreach($keys as $key) { $sCounts[] = count($this->formdata[$key]); } $sTotalItems = max($sCounts); $sAllValues = array(); for($i = 0; $i < $sTotalItems; $i++) { $sValues = array(); foreach($keys as $key) { $sValues[$key] = $this->formdata[$key][$i]; } $sAllValues[] = $sValues; } return $sAllValues; } public function GetValues($key) { /* Returns an array with zero or more values for the given key. If the key * does not exist, an empty array is returned. */ if(!isset($this->formdata[$key])) { return array(); } elseif(is_array($this->formdata[$key])) { return $this->formdata[$key]; } else { return array($this->formdata[$key]); } } public function GetValue($key, $default=null) { /* Returns a single value for the given key. If the key contains an array, it * will return the first element. If the key does not exist, it will return null. */ if(!isset($this->formdata[$key])) { return $default; } elseif(is_array($this->formdata[$key])) { return $this->formdata[$key][0]; } else { return $this->formdata[$key]; } } }