* @copyright 2010 Luracast * @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @link http://luracast.com/products/restler/ * */ class Validator implements iValidate { public static $holdException = false; public static $exceptions = array(); public static $preFilters = array( //'*' => 'some_global_filter', //applied to all parameters 'string' => 'trim', //apply filter function by type (string) //'string' => 'strip_tags', //'string' => 'htmlspecialchars', //'int' => 'abs', //'float' => 'abs', //'CustomClass' => 'MyFilterClass::custom', // please note that you wont get an instance // of CustomClass. you will get an array instead ); /** * Validate alphabetic characters. * * Check that given value contains only alphabetic characters. * * @param $input * @param ValidationInfo $info * * @return string * * @throws Invalid */ public static function alpha($input, ValidationInfo $info = null) { if (ctype_alpha($input)) { return $input; } if ($info && $info->fix) { //remove non alpha characters return preg_replace("/[^a-z]/i", "", $input); } throw new Invalid('Expecting only alphabetic characters.'); } /** * Validate UUID strings. * * Check that given value contains only alpha numeric characters and the length is 36 chars. * * @param $input * @param ValidationInfo $info * * @return string * * @throws Invalid */ public static function uuid($input, ValidationInfo $info = null) { if (is_string($input) && preg_match( '/^\{?[0-9a-f]{8}\-?[0-9a-f]{4}\-?[0-9a-f]{4}\-?[0-9a-f]{4}\-?[0-9a-f]{12}\}?$/i', $input )) { return strtolower($input); } throw new Invalid('Expecting a Universally Unique IDentifier (UUID) string.'); } /** * Validate alpha numeric characters. * * Check that given value contains only alpha numeric characters. * * @param $input * @param ValidationInfo $info * * @return string * * @throws Invalid */ public static function alphanumeric($input, ValidationInfo $info = null) { if (ctype_alnum($input)) { return $input; } if ($info && $info->fix) { //remove non alpha numeric and space characters return preg_replace("/[^a-z0-9 ]/i", "", $input); } throw new Invalid('Expecting only alpha numeric characters.'); } /** * Validate printable characters. * * Check that given value contains only printable characters. * * @param $input * @param ValidationInfo $info * * @return string * * @throws Invalid */ public static function printable($input, ValidationInfo $info = null) { if (ctype_print($input)) { return $input; } if ($info && $info->fix) { //remove non printable characters return preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $input); } throw new Invalid('Expecting only printable characters.'); } /** * Validate hexadecimal digits. * * Check that given value contains only hexadecimal digits. * * @param $input * @param ValidationInfo $info * * @return string * * @throws Invalid */ public static function hex($input, ValidationInfo $info = null) { if (ctype_xdigit($input)) { return $input; } throw new Invalid('Expecting only hexadecimal digits.'); } /** * Color specified as hexadecimals * * Check that given value contains only color. * * @param $input * @param ValidationInfo|null $info * * @return string * @throws Invalid */ public static function color($input, ValidationInfo $info = null) { if (preg_match('/^#[a-f0-9]{6}$/i', $input)) { return $input; } throw new Invalid('Expecting color as hexadecimal digits.'); } /** * Validate Telephone number * * Check if the given value is numeric with or without a `+` prefix * * @param $input * @param ValidationInfo $info * * @return string * * @throws Invalid */ public static function tel($input, ValidationInfo $info = null) { if (is_numeric($input) && '-' != substr($input, 0, 1)) { return $input; } throw new Invalid('Expecting phone number, a numeric value ' . 'with optional `+` prefix'); } /** * Validate Email * * Check if the given string is a valid email * * @param String $input * @param ValidationInfo $info * * @return string * @throws Invalid */ public static function email($input, ValidationInfo $info = null) { $r = filter_var($input, FILTER_VALIDATE_EMAIL); if ($r) { return $r; } elseif ($info && $info->fix) { $r = filter_var($input, FILTER_SANITIZE_EMAIL); return static::email($r); } throw new Invalid('Expecting email in `name@example.com` format'); } /** * Validate IP Address * * Check if the given string is a valid ip address * * @param String $input * @param ValidationInfo $info * * @return string * @throws Invalid */ public static function ip($input, ValidationInfo $info = null) { $r = filter_var($input, FILTER_VALIDATE_IP); if ($r) { return $r; } throw new Invalid('Expecting IP address in IPV6 or IPV4 format'); } /** * Validate Url * * Check if the given string is a valid url * * @param String $input * @param ValidationInfo $info * * @return string * @throws Invalid */ public static function url($input, ValidationInfo $info = null) { $r = filter_var($input, FILTER_VALIDATE_URL); if ($r) { return $r; } elseif ($info && $info->fix) { $r = filter_var($input, FILTER_SANITIZE_URL); return static::url($r); } throw new Invalid('Expecting url in `http://example.com` format'); } /** * MySQL Date * * Check if the given string is a valid date in YYYY-MM-DD format * * @param String $input * @param ValidationInfo $info * * @return string * @throws Invalid */ public static function date($input, ValidationInfo $info = null) { if ( preg_match( '#^(?P\d{2}|\d{4})-(?P\d{1,2})-(?P\d{1,2})$#', $input, $date ) && checkdate($date['month'], $date['day'], $date['year']) ) { return $input; } throw new Invalid( 'Expecting date in `YYYY-MM-DD` format, such as `' . date("Y-m-d") . '`' ); } /** * MySQL DateTime * * Check if the given string is a valid date and time in YYY-MM-DD HH:MM:SS format * * @param String $input * @param ValidationInfo $info * * @return string * @throws Invalid */ public static function datetime($input, ValidationInfo $info = null) { if ( preg_match('/^(?P19\d\d|20\d\d)\-(?P0[1-9]|1[0-2])\-' . '(?P0\d|[1-2]\d|3[0-1]) (?P0\d|1\d|2[0-3]' . ')\:(?P[0-5][0-9])\:(?P[0-5][0-9])$/', $input, $date) && checkdate($date['month'], $date['day'], $date['year']) ) { return $input; } throw new Invalid( 'Expecting date and time in `YYYY-MM-DD HH:MM:SS` format, such as `' . date("Y-m-d H:i:s") . '`' ); } /** * Alias for Time * * Check if the given string is a valid time in HH:MM:SS format * * @param String $input * @param ValidationInfo $info * * @return string * @throws Invalid */ public static function time24($input, ValidationInfo $info = null) { return static::time($input, $info); } /** * Time * * Check if the given string is a valid time in HH:MM:SS format * * @param String $input * @param ValidationInfo $info * * @return string * @throws Invalid */ public static function time($input, ValidationInfo $info = null) { if (preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/', $input)) { return $input; } throw new Invalid( 'Expecting time in `HH:MM:SS` format, such as `' . date("H:i:s") . '`' ); } /** * Time in 12 hour format * * Check if the given string is a valid time 12 hour format * * @param String $input * @param ValidationInfo $info * * @return string * @throws Invalid */ public static function time12($input, ValidationInfo $info = null) { if (preg_match( '/^([1-9]|1[0-2]|0[1-9]){1}(:[0-5][0-9])?\s?([aApP][mM]{1})?$/', $input) ) { return $input; } throw new Invalid( 'Expecting time in 12 hour format, such as `08:00AM` and `10:05:11`' ); } /** * Unix Timestamp * * Check if the given value is a valid timestamp * * @param String $input * @param ValidationInfo $info * * @return int * @throws Invalid */ public static function timestamp($input, ValidationInfo $info = null) { if ((string)(int)$input == $input && ($input <= PHP_INT_MAX) && ($input >= ~PHP_INT_MAX) ) { return (int)$input; } throw new Invalid('Expecting unix timestamp, such as ' . time()); } /** * Validate the given input * * Validates the input and attempts to fix it when fix is requested * * @param mixed $input * @param ValidationInfo $info * @param null $full * * @throws \Exception * @return array|bool|float|int|mixed|null|number|string */ public static function validate($input, ValidationInfo $info, $full = null) { $html = Scope::get('Restler')->responseFormat instanceof HtmlFormat; $name = $html ? "$info->label" : "`$info->name`"; if ( isset(static::$preFilters['*']) && is_scalar($input) && is_callable($func = static::$preFilters['*']) ) { $input = $func($input); } if ( isset(static::$preFilters[$info->type]) && (is_scalar($input) || !empty($info->children)) && is_callable($func = static::$preFilters[$info->type]) ) { $input = $func($input); } try { if (is_null($input)) { if ($info->required) { throw new RestException (400, "$name is required."); } return null; } $error = isset ($info->message) ? $info->message : "Invalid value specified for $name"; //if a validation method is specified if (!empty($info->method)) { $method = $info->method; $info->method = ''; $r = self::validate($input, $info); return $info->apiClassInstance->{$method} ($r); } // when type is an array check if it passes for any type if (is_array($info->type)) { //trace("types are ".print_r($info->type, true)); $types = $info->type; foreach ($types as $type) { $info->type = $type; try { $r = self::validate($input, $info); if ($r !== false) { return $r; } } catch (RestException $e) { // just continue } } throw new RestException (400, $error); } //patterns are supported only for non numeric types if (isset ($info->pattern) && $info->type != 'int' && $info->type != 'float' && $info->type != 'number' ) { if (!preg_match($info->pattern, $input)) { throw new RestException (400, $error); } } if (isset ($info->choice)) { if (!$info->required && empty($input)) { //since its optional, and empty let it pass. $input = null; } elseif (is_array($input)) { foreach ($input as $i) { if (!in_array($i, $info->choice)) { $error .= ". Expected one of (" . implode(',', $info->choice) . ")."; throw new RestException (400, $error); } } } elseif (!in_array($input, $info->choice)) { $error .= ". Expected one of (" . implode(',', $info->choice) . ")."; throw new RestException (400, $error); } } if (method_exists($class = get_called_class(), $info->type) && $info->type != 'validate') { if (!$info->required && empty($input)) { //optional parameter with a empty value assume null return null; } try { return call_user_func("$class::$info->type", $input, $info); } catch (Invalid $e) { throw new RestException(400, $error . '. ' . $e->getMessage()); } } switch ($info->type) { case 'int' : case 'float' : case 'number' : if (!is_numeric($input)) { $error .= '. Expecting ' . ($info->type == 'int' ? 'integer' : 'numeric') . ' value'; break; } if ($info->type == 'int' && (int)$input != $input) { if ($info->fix) { $r = (int)$input; } else { $error .= '. Expecting integer value'; break; } } else { $r = $info->numericValue($input); } if (isset ($info->min) && $r < $info->min) { if ($info->fix) { $r = $info->min; } else { $error .= ". Minimum required value is $info->min."; break; } } if (isset ($info->max) && $r > $info->max) { if ($info->fix) { $r = $info->max; } else { $error .= ". Maximum allowed value is $info->max."; break; } } return $r; case 'string' : case 'password' : //password fields with string case 'search' : //search field with string if (is_bool($input)) $input = $input ? 'true' : 'false'; if (!is_string($input)) { $error .= '. Expecting alpha numeric value'; break; } if ($info->required && $input === '') { $error = "$name is required."; break; } $r = strlen($input); if (isset ($info->min) && $r < $info->min) { if ($info->fix) { $input = str_pad($input, $info->min, $input); } else { $char = $info->min > 1 ? 'characters' : 'character'; $error .= ". Minimum $info->min $char required."; break; } } if (isset ($info->max) && $r > $info->max) { if ($info->fix) { $input = substr($input, 0, $info->max); } else { $char = $info->max > 1 ? 'characters' : 'character'; $error .= ". Maximum $info->max $char allowed."; break; } } return $input; case 'bool': case 'boolean': if (is_bool($input)) { return $input; } if (is_numeric($input)) { if ($input == 1) { return true; } if ($input == 0) { return false; } } elseif (is_string($input)) { switch (strtolower($input)) { case 'true': return true; case 'false': return false; } } if ($info->fix) { return $input ? true : false; } $error .= '. Expecting boolean value'; break; case 'array': if ($info->fix && is_string($input)) { $input = explode(CommentParser::$arrayDelimiter, $input); } if (is_array($input)) { $contentType = Util::nestedValue($info, 'contentType') ?: null; if ($info->fix) { if ($contentType == 'indexed') { $input = $info->filterArray($input, true); } elseif ($contentType == 'associative') { $input = $info->filterArray($input, false); } } elseif ( $contentType == 'indexed' && array_values($input) != $input ) { $error .= '. Expecting a list of items but an item is given'; break; } elseif ( $contentType == 'associative' && array_values($input) == $input && count($input) ) { $error .= '. Expecting an item but a list is given'; break; } $r = count($input); if (isset ($info->min) && $r < $info->min) { $item = $info->max > 1 ? 'items' : 'item'; $error .= ". Minimum $info->min $item required."; break; } if (isset ($info->max) && $r > $info->max) { if ($info->fix) { $input = array_slice($input, 0, $info->max); } else { $item = $info->max > 1 ? 'items' : 'item'; $error .= ". Maximum $info->max $item allowed."; break; } } if ( isset($contentType) && $contentType != 'associative' && $contentType != 'indexed' ) { $name = $info->name; $info->type = $contentType; unset($info->contentType); unset($info->min); unset($info->max); foreach ($input as $key => $chinput) { $info->name = "{$name}[$key]"; $input[$key] = static::validate($chinput, $info); } } return $input; } elseif (isset($contentType)) { $error .= '. Expecting items of type ' . ($html ? "$contentType" : "`$contentType`"); break; } break; case 'mixed': case 'unknown_type': case 'unknown': case null: //treat as unknown return $input; default : if (!is_array($input)) { break; } //do type conversion if (class_exists($info->type)) { $input = $info->filterArray($input, false); $implements = class_implements($info->type); if ( is_array($implements) && in_array('Luracast\\Restler\\Data\\iValueObject', $implements) ) { return call_user_func( "{$info->type}::__set_state", $input ); } $class = $info->type; $instance = new $class(); if (is_array($info->children)) { if ( empty($input) || !is_array($input) || $input === array_values($input) ) { $error .= '. Expecting an item of type ' . ($html ? "$info->type" : "`$info->type`"); break; } foreach ($info->children as $key => $value) { $cv = new ValidationInfo($value); $cv->name = "{$info->name}[$key]"; if (array_key_exists($key, $input) || $cv->required) { $instance->{$key} = static::validate( Util::nestedValue($input, $key), $cv ); } } } return $instance; } } throw new RestException (400, $error); } catch (\Exception $e) { static::$exceptions[$info->name] = $e; if (static::$holdException) { return null; } throw $e; } } }