<?php
namespace XF\Legacy;
use function array_key_exists, floatval, intval, is_array, is_int, is_scalar, is_string, strlen, strval;
class Input
{
const STRING = 'string';
const NUM = 'num';
const UNUM = 'unum';
const INT = 'int';
const UINT = 'uint';
const FLOAT = 'float';
const BOOLEAN = 'boolean';
const BINARY = 'binary';
const ARRAY_SIMPLE = 'array_simple';
const JSON_ARRAY = 'json_array';
const DATE_TIME = 'dateTime';
/**
* Default values for the input types
*
* @var array
*/
protected static $_DEFAULTS = [
self::STRING => '',
self::NUM => 0,
self::UNUM => 0,
self::INT => 0,
self::UINT => 0,
self::FLOAT => 0.0,
self::BOOLEAN => false,
self::BINARY => '',
self::ARRAY_SIMPLE => [],
self::JSON_ARRAY => [],
self::DATE_TIME => 0
];
/**
* Map of from-to pairs of things to manipulate in strings.
*
* @var array
*/
protected static $_strClean = [
// strip a bunch of control characters
"\x00" => '', // null
"\x01" => '', // start of heading
"\x02" => '', // start of text
"\x03" => '', // end of text
"\x04" => '', // end of transmission
"\x05" => '', // enquiry
"\x06" => '', // ack
"\x07" => '', // bell
"\x08" => '', // backspace
"\x0B" => '', // vertical tab
"\x0C" => '', // form feed
"\x0D" => '', // carriage returns, because jQuery does so in .val()
"\x0E" => '', // shift out
"\x0F" => '', // shift in
"\x10" => '', // data link escape
"\x11" => '', // device ctrl 1
"\x12" => '', // device ctrl 2
"\x13" => '', // device ctrl 3
"\x14" => '', // device ctrl 4
"\x15" => '', // negative ack
"\x16" => '', // sync idle
"\x17" => '', // end of transmission block
"\x18" => '', // cancel
"\x19" => '', // end of medium
"\x1A" => '', // substitute
"\x1B" => '', // escape
"\x1C" => '', // file sep
"\x1D" => '', // group sep
"\x1E" => '', // record sep
"\x1F" => '', // unit sep
"\xC2\xA0" => ' ', // nbsp
"\xC2\xAD" => '', // soft hyphen
"\xE2\x80\x8B" => '', // zero width space
"\xEF\xBB\xBF" => '' // zero width nbsp
];
/**
* Cached cleaned variables. Key is the variable name as it was pulled
*
* @var array
*/
protected $_cleanedVariables = [];
/**
* The request object that variables will be read from. May be null
* if source data is populated instead
*
* @var \XF\Http\Request|null
*/
protected $_request = null;
/**
* Alternative to the request, data can come from an array.
*
* @var array|null
*/
protected $_sourceData = null;
/**
* Constructor
*
* @param \XF\Http\Request|array $source Source of input
*/
public function __construct($source)
{
if ($source instanceof \XF\Http\Request)
{
$this->_request = $source;
}
else if (is_array($source))
{
$this->_sourceData = $source;
}
else
{
throw new \InvalidArgumentException('Must pass an array or Zend_Controller_Request_Http object to XenForo_Input');
}
}
/**
* Filter an individual item
*
* @param string $variableName Name of the input variable
* @param mixed $filterData Filter information, can be a single constant or an array containing a filter and options
* @param array $options Filtering options
*
* @return mixed Value after being cleaned
*/
public function filterSingle($variableName, $filterData, array $options = [])
{
if (is_string($filterData))
{
$filters = [$filterData];
}
else if (is_array($filterData) && isset($filterData[0]))
{
$filters = is_array($filterData[0]) ? $filterData[0] : [$filterData[0]];
if (isset($filterData[1]) && is_array($filterData[1]))
{
$options = array_merge($options, $filterData[1]);
}
else
{
unset($filterData[0]);
$options = array_merge($options, $filterData);
}
}
else
{
throw new \InvalidArgumentException("Invalid data passed to " . __CLASS__ . "::" . __METHOD__);
}
$firstFilter = reset($filters);
if (isset($options['default']))
{
$defaultData = $options['default'];
}
else if (array_key_exists($firstFilter, self::$_DEFAULTS))
{
$defaultData = self::$_DEFAULTS[$firstFilter];
}
else
{
$defaultData = null;
}
if ($this->_request)
{
$data = $this->_request->get($variableName, null);
}
else
{
$data = ($this->_sourceData[$variableName] ?? null);
}
if ($data === null)
{
$data = $defaultData;
}
foreach ($filters AS $filterName)
{
if (isset($options['array']))
{
if (is_array($data))
{
foreach (array_keys($data) AS $key)
{
$data[$key] = self::_doClean($filterName, $options, $data[$key], $defaultData);
}
}
else
{
$data = [];
break;
}
}
else
{
$data = self::_doClean($filterName, $options, $data, $defaultData);
}
}
$this->_cleanedVariables[$variableName] = $data;
return $data;
}
protected static function _doClean($filterName, array $filterOptions, $data, $defaultData)
{
switch ($filterName)
{
case self::STRING:
$data = is_scalar($data) ? strval($data) : $defaultData;
if (strlen($data) && !preg_match('/./su', $data))
{
$data = $defaultData;
}
$data = self::cleanString($data);
if (empty($filterOptions['noTrim']))
{
$data = trim($data);
}
break;
case self::NUM:
$data = strval($data) + 0;
break;
case self::UNUM:
$data = strval($data) + 0;
$data = ($data < 0) ? $defaultData : $data;
break;
case self::INT:
$data = intval($data);
break;
case self::UINT:
$data = ($data = intval($data)) < 0 ? $defaultData : $data;
break;
case self::FLOAT:
$data = floatval($data);
break;
case self::BOOLEAN:
if ($data === 'n' || $data == 'no' || $data === 'N')
{
$data = false;
}
else
{
$data = (boolean)$data;
}
break;
case self::BINARY:
$data = strval($data);
break;
case self::ARRAY_SIMPLE:
if (!is_array($data))
{
$data = $defaultData;
}
$data = self::cleanStringArray($data);
break;
case self::JSON_ARRAY:
if (is_string($data))
{
$data = json_decode($data, true);
}
if (!is_array($data))
{
$data = $defaultData;
}
$data = self::cleanStringArray($data);
break;
case self::DATE_TIME:
if (!$data)
{
$data = 0;
}
else if (is_string($data))
{
$data = trim($data);
if ($data === strval(intval($data)))
{
// data looks like an int, treat as timestamp
$data = intval($data);
}
else
{
$tz = (XenForo_Visitor::hasInstance() ? XenForo_Locale::getDefaultTimeZone() : null);
try
{
$date = new \DateTime($data, $tz);
if (!empty($filterOptions['dayEnd']))
{
$date->setTime(23, 59, 59);
}
$data = $date->format('U');
}
catch (\Exception $e)
{
$data = 0;
}
}
}
if (!is_int($data))
{
$data = intval($data);
}
break;
default:
throw new \InvalidArgumentException("Unknown input type in " . __CLASS__ . "::" . __METHOD__);
}
return $data;
}
/**
* Cleans invalid characters out of a string, such as nulls, nbsp, \r, etc.
* Characters may not strictly be invalid, but can cause confusion/bugs.
*
* @param string $string
*
* @return string
*/
public static function cleanString($string)
{
// only cover the BMP as MySQL only supports that
$string = preg_replace('/[\xF0-\xF7].../', '', $string);
return strtr(strval($string), self::$_strClean);
}
/**
* Recursively run clean string on all strings in an array
*
* @param array $array
*
* @return array
*/
public static function cleanStringArray(array $array)
{
foreach ($array AS &$v)
{
if (is_string($v))
{
$v = self::cleanString($v);
}
else if (is_array($v))
{
$v = self::cleanStringArray($v);
}
}
return $array;
}
/**
* Filter an array of items
*
* @param array $filters Key-value pairs with the value being in the format expected by filterSingle. {@link XenForo_Input::filterSingle()}
*
* @return array key-value pairs with the cleaned value
*/
public function filter(array $filters)
{
$data = [];
foreach ($filters AS $variableName => $filterData)
{
$data[$variableName] = $this->filterSingle($variableName, $filterData);
}
return $data;
}
/**
* Statically filters a piece of data as the requested type.
*
* @param mixed $data
* @param mixed $filterName
* @param array $options
*
* @return mixed
*/
public static function rawFilter($data, $filterName, array $options = [])
{
return self::_doClean($filterName, $options, $data, self::$_DEFAULTS[$filterName]);
}
/**
* Returns true if the given key was included in the request at all.
*
* @param string $key
*
* @return boolean
*/
public function inRequest($key)
{
if ($this->_request)
{
return isset($this->_request->$key);
}
else
{
return isset($this->_sourceData[$key]);
}
}
public function __get($key)
{
if (array_key_exists($key, $this->_cleanedVariables))
{
return $this->_cleanedVariables[$key];
}
else
{
return null;
}
}
public function __isset($key)
{
return array_key_exists($key, $this->_cleanedVariables);
}
}