= 0, signed if max < 0. * @method int intUnsigned($varName, $min = null, $max = null) Sanitize value to unsigned integer with optional min and max. * @method int intSigned($varName, $min = null, $max = null) Sanitize value to signed integer with optional min and max. * @method float float($varName, $min = null, $max = null, $precision = null) Sanitize value to float with optional min and max values. * @method array array($varName, $sanitizer = null) Sanitize array or CSV String to an array, optionally running elements through specified $sanitizer. * @method array intArray($varName, $min = 0, $max = null) Sanitize array or CSV string to an array of integers with optional min and max values. * @method string|null option($varName, array $allowedValues) Return value of $varName only if it exists in $allowedValues. * @method array options($varName, array $allowedValues) Return all values in array $varName that also exist in $allowedValues. * @method bool bool($varName) Sanitize value to boolean (true or false) * * */ class WireInputData extends Wire implements \ArrayAccess, \IteratorAggregate, \Countable { /** * Whether or not slashes should be stripped * * @var bool|int * */ protected $stripSlashes = false; /** * Input data container * * @var array * */ protected $data = array(); /** * Are we working with lazy data (data by reference)? * * @var bool * */ protected $lazy = false; /** * When lazy mode is active, these are keys of values set in a non-lazy way * * @var array * */ protected $unlazyKeys = array(); /** * Construct * * @param array $input Associative array of variables to store * @param bool $lazy Use lazy loading? * */ public function __construct(&$input = array(), $lazy = false) { parent::__construct(); $this->useFuel(false); if(version_compare(PHP_VERSION, '5.4.0', '<') && function_exists('get_magic_quotes_gpc')) { $this->stripSlashes = get_magic_quotes_gpc(); } if(!empty($input)) { if($lazy) { $this->data = &$input; $this->lazy = true; } else { $this->setArray($input); } } } /** * Set associative array of variables to store * * @param array $input * @return $this * */ public function setArray(array $input) { foreach($input as $key => $value) $this->__set($key, $value); return $this; } /** * Get associative array of all input variables * * @return array * */ public function getArray() { if($this->lazy) { $data = array(); foreach($this->data as $key => $value) { if(isset($this->unlazyKeys[$key])) { $data[$key] = $value; } else { $data[$key] = $this->__get($key); } } return $data; } else { return $this->data; } } /** * Set an input value * * @param string $key * @param mixed $value * */ public function __set($key, $value) { if(is_string($value)) { if($this->stripSlashes) $value = stripslashes($value); } else if(is_array($value)) { $value = $this->cleanArray($value); } $this->data[$key] = $value; if($this->lazy) $this->unlazyKeys[$key] = $key; } /** * Set a value * * @param string $key * @param string|int|float|array|null $value * @return $this * @param array|int|string $options Options not currently used, but available for descending classes or future use * @since 3.0.141 You can also use __set() or set directly for compatibility with all versions * */ public function set($key, $value, $options = array()) { if($options) {} // not currently used by this class $this->__set($key, $value); return $this; } /** * Get a value * * @param string $key * @param array|int|string $options Options not currently used, but available for descending classes or future use * @return string|int|float|array|null $value * @since 3.0.141 You can also get directly or use __get(), both of which are compatible with all versions * */ public function get($key, $options = array()) { if($options) {} // not currently used by this class return $this->__get($key); } /** * Find one input var that matches given pattern in name (or optionally value) * * @param string $pattern Wildcard string or PCRE regular expression * @param array|int|string $options * - `type` (string): Specify "value" to match input value (rather input name), OR prefix pattern with "value=". * - `sanitizer` (string): Name of sanitizer to run values through (default='', none) * - `arrays` (bool): Also find on input varibles that are arrays? (default=false) * @return string|int|float|array|null $value Returns value if found or null if not. * @since 3.0.163 * */ public function findOne($pattern, $options = array()) { if(!strlen($pattern)) return null; if(ctype_alnum(str_replace(array('_', '-', '.'), '', $pattern))) return $this->__get($pattern); $options['limit'] = 1; $value = $this->find($pattern, array_merge($options, $options)); return array_shift($value); // returns null if empty } /** * Find all input vars that match given pattern in name (or optionally value) * * ~~~~~ * // find all input vars having name beginning with "title_" (i.e. title_en, title_de, title_es) * $values = $input->post->find('title_*'); * * // find all input vars having name with "title" anywhere in it (i.e. title, subtitle, titles, title_de) * $values = $input->post->find('*title*'); * * // find all input vars having value with the term "wire" anywhere, regardless of case * $values = $input->post->find('/wire/i', [ 'type' => 'value' ]); * * // example of result from above find operation: * $values = [ * 'title' => 'ProcessWire CMS', * 'subtitle' => 'Have plenty of caffeine to make sure you are wired', * 'sidebar' => 'Learn how to rewire a flux capacitor...', * 'summary' => 'All about the $wire API variable', * ]; * ~~~~~ * * @param string $pattern Wildcard string or PCRE regular expression * @param array $options * - `type` (string): Specify "value" to match input value (rather input name), OR prefix pattern with "value=". * - `limit` (int): Maximum number of items to return (default=0, no limit) * - `sanitizer` (string): Name of sanitizer to run values through (default='', none) * - `arrays` (bool): Also find on input varibles that are arrays? (default=false) * @return array Returns associative array of values `[ name => value ]` if found, or empty array if none found. * @since 3.0.163 * */ public function find($pattern, array $options = array()) { $defaults = array( 'type' => 'name', // match on 'name' or 'value' (default='name') 'limit' => 0, // max allowed matches in return value 'values' => $this, // use these values rather than those from this input class 'sanitizer' => '', // sanitizer name to apply found values 'arrays' => false, // also find on input vars that are arrays? ); if(!strlen($pattern)) return array(); $options = array_merge($defaults, $options); $sanitizer = $this->wire()->sanitizer; $isRE = in_array($pattern[0], array('/', '!', '%', '#', '@')); $items = array(); $count = 0; $type = $options['type']; $tests = array(); if(strpos($pattern, '=')) { // pattern indicates "value=pattern" or "name=pattern" list($type, $pattern) = explode('=', $pattern, 2); } if(!$isRE && strpos($pattern, '*') !== false) { // wildcard, convert to regex $a = explode('*', $pattern); foreach($a as $k => $v) { if(!strlen($v)) continue; $a[$k] = preg_quote($v); $tests[] = $v; } $isRE = true; $pattern = '/^' . implode('.*', $a) . '$/'; } if(!count($tests)) $tests = false; foreach($options['values'] as $name => $value) { if($options['limit'] && $count >= $options['limit']) break; $isArray = is_array($value); if($isArray && !$options['arrays']) { continue; } else if($isArray && $type === 'value') { $v = $this->find($pattern, array_merge($options, array('values' => $value))); if(count($v)) list($items[$name], $count) = array($v, $count + 1); continue; } else if($type === 'value') { $match = $value; } else { $match = $name; } if($tests) { // tests to confirm a preg_match is necessary (wildcard mode only) $passes = true; foreach($tests as $test) { $passes = strpos($match, $test) !== false; if(!$passes) break; } if(!$passes) continue; } if($isRE) { if(!preg_match($pattern, $match)) continue; } else { if(strpos($match, $pattern) === false) continue; } if($options['sanitizer']) { $value = $sanitizer->sanitize($value, $options['sanitizer']); } $items[$name] = $value; $count++; } return $items; } /** * Clean an array of data * * Support multi-dimensional arrays consistent with `$config->wireInputArrayDepth` * setting (3.0.178+) and remove slashes if applicable/necessary. * * @param array $a * @return array * */ protected function cleanArray(array $a) { static $depth = 1; $maxDepth = (int) $this->wire()->config->wireInputArrayDepth; if($maxDepth < 1) $maxDepth = 1; $clean = array(); foreach($a as $key => $value) { if(is_array($value)) { if($depth >= $maxDepth) { // max dimension reached $value = null; } else { // allow another dimension $depth++; $value = $this->cleanArray($value); $depth--; // empty arrays not possible in input vars past 1st dimension if(!count($value)) $value = null; } } else if(is_string($value)) { if($this->stripSlashes) $value = stripslashes($value); } if($value !== null) { $clean[$key] = $value; } } return $clean; } /** * Set whether or not slashes should be stripped * * @param $stripSlashes * */ public function setStripSlashes($stripSlashes) { $this->stripSlashes = $stripSlashes ? true : false; } /** * Get an input value * * @param string $key * @return mixed|null * */ public function __get($key) { if(strpos($key, '|')) { $value = null; foreach(explode('|', $key) as $k) { $value = $this->__get($k); if($value !== null) break; } return $value; } else if(isset($this->data[$key])) { $value = $this->data[$key]; if($this->lazy && !isset($this->unlazyKeys[$key])) { // in lazy mode, value is not cleaned until it is accessed if(is_string($value)) { if($this->stripSlashes) $value = stripslashes($value); } else if(is_array($value)) { $value = $this->cleanArray($value); } } } else { $value = null; } return $value; } #[\ReturnTypeWillChange] public function getIterator() { if($this->lazy) { $data = $this->getArray(); return new \ArrayObject($data); } else { return new \ArrayObject($this->data); } } #[\ReturnTypeWillChange] public function offsetExists($key) { return isset($this->data[$key]); } #[\ReturnTypeWillChange] public function offsetGet($key) { return $this->__get($key); } #[\ReturnTypeWillChange] public function offsetSet($key, $value) { $this->__set($key, $value); } #[\ReturnTypeWillChange] public function offsetUnset($key) { unset($this->data[$key]); if($this->lazy && isset($this->unlazyKeys[$key])) unset($this->unlazyKeys[$key]); } #[\ReturnTypeWillChange] public function count() { return count($this->data); } /** * Remove a value from input * * @param string $key Name of input variable to remove value for * @return $this * */ public function remove($key) { $this->offsetUnset($key); return $this; } /** * Remove all values from input * * @return $this * */ public function removeAll() { $this->data = array(); $this->lazy = false; $this->unlazyKeys = array(); return $this; } public function __isset($key) { return $this->offsetExists($key); } public function __unset($key) { $this->offsetUnset($key); } /** * Return a query string of all input values * * Please note returned query string contains non-sanitized/non-validated variables, so this method * should only be used for specific cases where all input is known to be safe/valid. If that is not * an option then use PHP’s `http_build_query()` function on your own with known safe/valid values. * * #pw-internal * * @param array $overrides Associative array of [ name => value ] containing values to override/replace * @param string $separator String to separate values with, i.e. '&' or '&' (default='&') * @return string * @since 3.0.163 * */ public function queryString($overrides = array(), $separator = '&') { return http_build_query(array_merge($this->getArray(), $overrides), '', $separator); } /** * Maps to Sanitizer functions * * @param string $method * @param array $arguments * @return string|int|array|float|null Returns null when input variable does not exist * @throws WireException * */ public function ___callUnknown($method, $arguments) { $sanitizer = $this->wire()->sanitizer; if(!$sanitizer->methodExists($method)) { try { return parent::___callUnknown($method, $arguments); } catch(\Exception $e) { throw new WireException("Unknown method '$method' - specify a valid Sanitizer name or WireInputData method"); } } if(!isset($arguments[0])) { throw new WireException("For method '$method' specify an input variable name for first argument"); } // swap input name with input value in arguments array $arguments[0] = $this->__get($arguments[0]); if($arguments[0] === null) { // value is not present in input at all, accommodate potential fallback value? } if(count($arguments) > 1) { // more than one argument to sanitizer method return call_user_func_array(array($sanitizer, $method), $arguments); } else { // single argument, pass along to sanitize method return $sanitizer->sanitize($arguments[0], $method); } } public function __debugInfo() { return $this->data; } }