1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-12 09:44:38 +02:00

Add new $sanitizer methods: range(), min(), max(), bit(), maxLength(), maxBytes(), methodExists(), validate() and valid(). Also added support for executing multiple sanitizers in one call with the new sanitize() method.

This commit is contained in:
Ryan Cramer
2019-01-25 11:42:38 -05:00
parent 4a7919c9a9
commit e879baf189

View File

@@ -933,9 +933,10 @@ class Sanitizer extends Wire {
/**
* Sanitize and validate an email address
*
* Returns valid email address, or blank string if it isn't valid.
* Returns valid email address, or blank string if it isnt valid.
*
* #pw-group-strings
* #pw-group-validate
*
* @param string $value Email address to sanitize and validate.
* @return string Sanitized, valid email address, or blank string on failure.
@@ -1323,6 +1324,7 @@ class Sanitizer extends Wire {
* ~~~~~~
*
* #pw-group-strings
* #pw-group-validate
*
* @param string $value URL to validate
* @param bool|array $options Array of options to modify default behavior, including:
@@ -2753,6 +2755,95 @@ class Sanitizer extends Wire {
if(!isset($options['min'])) $options['min'] = PHP_INT_MAX * -1;
return $this->int($value, $options);
}
/**
* Sanitize value to be within the given min and max range
*
* If float or decimal string specified for $min argument, return value will be a float,
* otherwise an integer is returned.
*
* ~~~~~
* $n = 10;
* $sanitizer->range($n, 100, 200); // returns 100
* $sanitizer->range($n, 0, 1.0); // returns 1.0
* $sanitizer->range($n, 1.1, 100.5); // returns 10.0
* ~~~~~
*
* #pw-group-numbers
*
* @param int|float|string $value
* @param int|float|string|null $min Minimum allowed value or null for no minimum (default=null)
* @param int|float|string|null $max Maximum allowed value or null for no maximum (default=null)
* @return int|float
* @since 3.0.125
*
*/
public function range($value, $min = null, $max = null) {
if(is_string($min)) $min = ctype_digit($min) ? (float) $min : (int) $min;
if(is_string($max)) $max = ctype_digit($max) ? (float) $max : (int) $max;
$value = is_float($min) || is_float($max) ? (float) $value : (int) $value;
if($min === null) $min = is_float($value) ? PHP_FLOAT_MIN : PHP_INT_MIN;
if($max === null) $max = is_float($value) ? PHP_FLOAT_MAX : PHP_INT_MAX;
if($min > $max) list($min, $max) = array($max, $min); // swap args if necessary
if($value < $min) {
$value = $min;
} else if($value > $max) {
$value = $max;
}
return $value;
}
/**
* Sanitize value to be at least the given $min value
*
* If float or decimal string specified for $min argument, return value will be a float,
* otherwise an integer is returned.
*
* ~~~~~
* $n = 10;
* $sanitizer->min(100); // returns 100
* $sanitizer->min(5); // returns 10
* $sanitizer->min(1.0); // returns 10.0
* ~~~~~
*
* #pw-group-numbers
*
* @param int|float|string $value
* @param int|float|string $min Minimum allowed value
* @return int|float
* @since 3.0.125
* @see Sanitizer::max()
*
*/
public function min($value, $min = PHP_INT_MIN) {
return $this->range($value, $min, null);
}
/**
* Sanitize value to be at least the given $min value
*
* If float or decimal string specified for $min argument, return value will be a float,
* otherwise an integer is returned.
*
* ~~~~~
* $n = 10;
* $sanitizer->max(5); // returns 5
* $sanitizer->max(100); // returns 10
* $sanitizer->max(100.0); // returns 10.0
* ~~~~~
*
* #pw-group-numbers
*
* @param int|float|string $value
* @param int|float|string $max Maximum allowed value
* @return int|float
* @since 3.0.125
* @see Sanitizer::min()
*
*/
public function max($value, $max = PHP_INT_MAX) {
return $this->range($value, null, $max);
}
/**
* Sanitize to floating point value
@@ -3036,7 +3127,7 @@ class Sanitizer extends Wire {
* @return string|int|null
*
*/
public function option($value, array $allowedValues) {
public function option($value, array $allowedValues = array()) {
$key = array_search($value, $allowedValues);
if($key === false) return null;
return $allowedValues[$key];
@@ -3052,7 +3143,7 @@ class Sanitizer extends Wire {
* @return array
*
*/
public function options(array $values, array $allowedValues) {
public function options(array $values, array $allowedValues = array()) {
$a = array();
foreach($values as $value) {
$key = array_search($value, $allowedValues);
@@ -3098,6 +3189,100 @@ class Sanitizer extends Wire {
}
return (bool) $value;
}
/**
* Sanitize to a bit, returning only integer 0 or 1
*
* This works the same as the bool sanitizer except that it returns 0 or 1 rather than false or true.
*
* #pw-group-other
* #pw-group-numbers
*
* @param string|int|array $value
* @return int
* @see Sanitizer::bool()
* @since 3.0.125
*
*/
public function bit($value) {
return $this->bool($value) ? 1 : 0;
}
/**
* Limit length of given value to that specified
*
* - For strings, this limits the length to that many characters.
* - For arrays, the maxLength is assumed to be the max allowed array items.
* - For integers maxLength is assumed to be the max allowed digits.
* - For floats, maxLength is assumed to be max allowed digits (including decimal point).
* - Returns the same type it is given: string, array, int or float
*
* #pw-group-other
* #pw-group-strings
*
* @param string|int|array|float $value
* @param int $maxLength Maximum length (default=128)
* @param null|int $maxBytes Maximum allowed bytes (used for string types only)
* @return array|bool|float|int|string
* @since 3.0.125
*
*/
public function maxLength($value, $maxLength = 128, $maxBytes = null) {
if($maxLength < 0) $maxLength = abs($maxLength);
if(is_array($value)) {
if(count($value) > $maxLength) {
$value = $maxLength ? array_slice($value, 0, $maxLength) : array();
}
} else if(is_int($value)) {
$n = $maxLength;
while(strlen("$value") > $maxLength && $n) {
$value = (int) substr("$value", 0, $n);
$n--;
}
} else if(is_float($value)) {
$n = $maxLength;
while(strlen("$value") > $maxLength && $n) {
$value = (float) substr("$value", 0, $n);
$n--;
}
} else {
if(!is_string($value)) $value = $this->string($value);
if($this->multibyteSupport) {
if(mb_strlen($value) > $maxLength) {
$value = mb_substr($value, 0, $maxLength);
}
if($maxBytes) {
while(strlen($value) > $maxBytes) {
$value = mb_substr($value, 0, mb_strlen($value)-1);
}
}
} else {
if(strlen($value) > $maxLength) {
$value = substr($value, 0, $maxLength);
}
}
}
return $value;
}
/**
* Limit bytes used by given string to max specified
*
* - This function will not break multibyte characters so long as PHP has mb_string.
* - This function works only with strings and if given a non-string it will be converted to one.
*
* #pw-group-strings
*
* @param string $value
* @param int $maxBytes
* @return string
* @since 3.0.125
*
*/
public function maxBytes($value, $maxBytes = 128) {
if(!is_string($value)) $value = $this->string($value);
return $this->maxLength($value, $maxBytes, $maxBytes);
}
/**
* Run value through all sanitizers, return array indexed by sanitizer name and resulting value
@@ -3112,12 +3297,14 @@ class Sanitizer extends Wire {
*/
public function testAll($value) {
$sanitizers = array(
'alpha',
'alpha',
'alphanumeric',
'array',
'bit',
'bool',
'camelCase',
'date',
'digits',
'digits',
'email',
'emailHeader',
'entities',
@@ -3126,31 +3313,49 @@ class Sanitizer extends Wire {
'fieldName',
'filename',
'float',
'hyphenCase',
'int',
'intArray',
'intSigned',
'intUnsigned',
'kebabCase',
'markupToLine',
'markupToText',
'max',
'maxBytes',
'maxLength',
'min',
'minArray',
'name',
'names',
'normalizeWhitespace',
'pageName',
'pageNameTranslate',
'pageNameUTF8',
'pagePathName',
'pagePathNameUTF8',
'pascalCase',
'path',
'purify',
'range',
'reduceWhitespace',
'removeMB4',
'removeNewlines',
'removeWhitespace',
'sanitize',
'selectorField',
'selectorValue',
'snakeCase',
'string',
'templateName',
'text',
'textarea',
'trim',
'truncate',
'unentities',
'url',
'valid',
'validate',
'varName',
);
$results = array();
@@ -3236,10 +3441,281 @@ class Sanitizer extends Wire {
return $isValid;
}
/**********************************************************************************************************************
* CLASS HELPERS
*
*/
public function __toString() {
return "Sanitizer";
}
/**
* Parse method name into methods array and arguments
*
* #pw-internal
*
* @param string $method
* @param string $delim Multi-method delimiter or omit to auto-detect (underscore, comma or space)
* @return array
* @since 3.0.125
*
*/
protected function parseMethod($method, $delim = '') {
static $cache = array();
if(isset($cache[$method])) return $cache[$method];
$methods = array();
if(!ctype_alnum($method)) {
// may contain delimiter, determine what it is
$method = trim($method);
if($delim === '') {
// no delimiter specified in arguments
foreach(array('_', ',', ' ') as $d) {
if(strpos($method, $d) === false) continue;
$delim = $d;
break;
}
} else if(!strpos($method, $delim)) {
// delimiter specified, but is not present
$delim = '';
}
$parts = $delim === '' ? array($method) : explode($delim, $method);
} else {
// single "method" or "method123"
$parts = array($method);
}
foreach($parts as $name) {
$name = trim($name);
if(empty($name)) continue;
$maxLength = 0;
if(ctype_digit($name)) {
// number-only assumed to be maxLength method
$exists = true;
$maxLength = (int) $name;
$name = 'maxLength';
} else if($this->methodExists($name, false)) {
// method exists already
$exists = true;
} else {
// method name needs parsing
$n = 0;
$s = '';
do {
$maxLength = $s;
$s = substr($name, -1 * (++$n));
} while(ctype_digit($s));
if(ctype_digit($maxLength)) {
$name = substr($name, 0, -1 * strlen($maxLength));
$maxLength = (int) $maxLength;
} else {
$maxLength = 0;
}
$exists = $this->methodExists($name, false);
}
$methods[] = array(
'name' => $name,
'maxLength' => $maxLength,
'exists' => $exists,
);
}
$cache[$method] = $methods;
return $methods;
}
/**
* Does the given sanitizer method name exist?
*
* #pw-internal
*
* @param string $name
* @param bool $allowCombos Allow check to include combo-methods that combine multiple sanitizers and/or max-length? (default=true)
* @return bool
* @since 3.0.125
*
*/
public function methodExists($name, $allowCombos = true) {
$exists = method_exists($this, $name) || method_exists($this, "___$name") || $this->hasHook($name);
if(!$exists && $allowCombos) {
$methods = $this->parseMethod($name, '_');
if(empty($methods)) return false;
$exists = true;
foreach($methods as $method) {
if($method['exists']) continue;
$exists = false;
break;
}
}
return $exists;
}
/**
* Call a sanitizer method indirectly where method name can contain combined/combo methods
*
* This method is primarily here to support predefined sanitizers in strings, like those that might
* be specified in settings for a module or field. For regular use, you probably want to call the
* sanitizer methods directly rather than through this method.
*
* ~~~~~
* // sanitize with text then entities sanitizers
* $value = $sanitizer->sanitize($value, 'text,entities');
*
* // numbers appended to text sanitizers imply max length
* $value = $sanitizer->sanitize($value, 'text128,entities');
* ~~~~~
*
* @param mixed $value
* @param string $method Method name "method", or combined method name(s) "method1,method2,method3"
* @return string|int|array|float|null
* @since 3.0.125
*
*/
public function sanitize($value, $method = 'text') {
$maxLengthMethods = array(
'maxLength' => 4,
// method($val, $beautify, $maxLength)
'name' => 3,
'fieldName' => 3,
'templateName' => 3,
'pageName' => 3,
'filename' => 3,
'pagePathName' => 3,
'alpha' => 3,
'alphanumeric' => 3,
// method($val, $maxLength)
'pageNameTranslate' => 2,
'pageNameUTF8' => 2,
'digits' => 2,
'truncate' => 2,
'min' => 2, // min123 where 123 is minimum allowed value
'max' => 2, // max123 where 123 is maximum allowed value
// method($val, options[ 'maxLength' => 123 ])
'path' => 1,
'text' => 1,
'textarea' => 1,
'url' => 1,
'selectorValue' => 1,
);
$methods = $this->parseMethod($method);
foreach($methods as $method) {
$methodName = $method['name'];
if(!$method['exists']) throw new WireException("Unknown sanitizer: $methodName");
if(!empty($method['maxLength'])) {
$maxLength = $method['maxLength'];
$n = isset($maxLengthMethods[$methodName]) ? $maxLengthMethods[$methodName] : 0;
switch($n) {
case 4: $value = $this->maxLength($value, $maxLength); break;
case 3: $value = $this->$methodName($value, false, $maxLength); break;
case 2: $value = $this->$methodName($value, $maxLength); break;
case 1: $value = $this->$methodName($value, array('maxLength' => $maxLength)); break;
default: $value = $this->maxLength($this->$methodName($value), $maxLength);
}
} else {
$value = $this->$methodName($value);
}
}
return $value;
}
/**
* Validate that value remains unchanged by given sanitizer method, or return null if not
*
* If change is just a type conversion change or surrounding whitespace (that gets trimmed)
* then this is still considered valid.
*
* Returns NULL or given $fallback value if value does not validate. Note that if results like
* 0, false or blank string are considered valid values, then this method can return them. So for
* cases like that you should compare the return value with NULL (or whatever your $fallback is).
*
* things like 0 or false (if that is a valid value) compare the return value with null before
* assuming a value is not valid.
*
* #pw-group-validate
*
* ~~~~~
* $sanitizer->validate('abc', 'alpha'); // valid: returns 'abc'
* $sanitizer->validate('abc123', 'alpha'); invalid: returns null
* ~~~~~
*
* @param string|int|array|float $value Value to validate
* @param string $method Saniatizer method name or CSV names combo
* @param null|mixed mixed $fallback Optionally return this fallback value (rather than null) if value does not validate
* @return null|mixed Returns sanitized value if it validates or null (or given fallback) if value does not validate
* @since 3.0.125
*
*/
public function validate($value, $method = 'text', $fallback = null) {
$valid = $this->sanitize($value, $method);
if(is_array($valid)) {
if(is_array($value) && $valid == $value) return $valid;
} else if(is_bool($valid)) {
if($valid == $value) return $valid;
} else {
if(is_string($value)) $value = trim($value);
if(is_string($valid)) $valid = trim($valid);
if($valid == $value && strlen("$valid") == strlen("$value")) return $valid;
}
return $fallback;
}
/**
* Is given value valid? (i.e. unchanged by given sanitizer method)
*
* ~~~~~~
* if($sanitizer->valid('abc123', 'alphanumeric')) {
* // value is valid
* }
* ~~~~~~
*
* #pw-group-validate
*
* @param string|int|array|float $value Value to check if valid
* @param string $method Method name or CSV method names
* @param bool $strict When true, sanitized value must be identical in type to the one given
* @return bool
* @since 3.0.125
*
*/
public function valid($value, $method = 'text', $strict = false) {
$valid = $this->validate($value, $method);
if($valid === null) return false;
if($strict && $value !== $valid) return false;
return true;
}
/**
* Map to sanitizers
*
* @param $method
* @param $arguments
*
* @return string|int|array|float|null Returns null when input variable does not exist
* @throws WireException
*
*/
public function ___callUnknown($method, $arguments) {
if($this->methodExists($method) && count($arguments)) {
return $this->sanitize($arguments[0], $method);
} else {
return parent::___callUnknown($method, $arguments);
}
}
}