More work on the query parser

This commit is contained in:
Kijin Sung 2020-06-26 16:52:41 +09:00
parent 92ff69591f
commit 6eca8736c1
6 changed files with 422 additions and 76 deletions

View file

@ -14,63 +14,229 @@ class VariableBase
public $default;
/**
* Get the value of this variable.
* Convert an operator into real SQL.
*
* @return Expression
* @param array $args
* @return array
*/
public function getValue(): Expression
public function getQueryStringAndParams(array $args): array
{
if ($this->var)
// Return if this method is called on an invalid child class.
if (!isset($this->column) || !isset($this->operation))
{
return new Expression('var', $this->var);
throw new \Rhymix\Framework\Exceptions\QueryError('Invalid invocation of getQueryStringAndParams()');
}
// Process the variable or default value.
if ($this->var && isset($args[$this->var]) && !empty($args[$this->var]))
{
$this->filterValue($args[$this->var]);
$value = $args[$this->var];
}
elseif ($this->default !== null)
{
$value = $this->getDefaultValue();
}
elseif ($this->not_null)
{
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' is not set');
}
else
{
return $this->getDefaultValue();
return ['', []];
}
// Quote the column name.
$column = Query::quoteName($this->column);
$where = '';
$params = array();
// Prepare the target value.
$list_ops = array('in' => true, 'notin' => true, 'not_in' => true, 'between' => true);
if (isset($list_ops[$this->operation]) && !is_array($value) && $value !== '')
{
$value = explode(',', preg_replace('/[\s\']/', '', $value));
}
// Apply the operator.
switch ($this->operation)
{
case 'equal':
$where = sprintf('%s = ?', $column);
$params[] = $value;
break;
case 'like':
$where = sprintf('%s LIKE ?', $column);
$params[] = '%' . $value . '%';
break;
case 'like_prefix':
case 'like_head':
$where = sprintf('%s LIKE ?', $column);
$params[] = $value . '%';
break;
case 'like_suffix':
case 'like_tail':
$where = sprintf('%s LIKE ?', $column);
$params[] = '%' . $value;
break;
case 'notlike':
$where = sprintf('%s NOT LIKE ?', $column);
$params[] = '%' . $value . '%';
break;
case 'notlike_prefix':
case 'notlike_head':
$where = sprintf('%s NOT LIKE ?', $column);
$params[] = $value . '%';
break;
case 'notlike_suffix':
case 'notlike_tail':
$where = sprintf('%s NOT LIKE ?', $column);
$params[] = '%' . $value;
break;
case 'in':
$count = count($value);
$placeholders = implode(', ', array_fill(0, $count, '?'));
$where = sprintf('%s IN (%s)', $column, $placeholders);
foreach ($value as $item)
{
$params[] = $item;
}
break;
case 'notin':
case 'not_in':
$count = count($value);
$placeholders = implode(', ', array_fill(0, $count, '?'));
$where = sprintf('%s NOT IN (%s)', $column, $placeholders);
foreach ($value as $item)
{
$params[] = $item;
}
break;
case 'between':
$where = sprintf('%s BETWEEN ? AND ?', $column);
foreach ($value as $item)
{
$params[] = $item;
}
break;
}
// Return the complete condition and parameters.
return [$where, $params];
}
/**
* Get the default value of this variable.
*
* @return Expression
* @return mixed
*/
public function getDefaultValue(): Expression
public function getDefaultValue()
{
// If the default value is not set, return null.
$val = $this->default;
if ($val === null)
// If the default value is a column name, escape it.
if (preg_match('/^[a-z0-9_]+(?:\.[a-z0-9_]+)+$/', $this->default))
{
return new Expression('null');
return Query::quoteName($this->default);
}
elseif (isset($this->column) && preg_match('/_srl$/', $this->column) && !ctype_digit($this->default))
{
return Query::quoteName($this->default);
}
// If the default value is a function shortcut, return an appropriate value.
switch ($val)
switch ($this->default)
{
case 'ipaddress()':
return new Expression('string', \RX_CLIENT_IP);
return "'" . \RX_CLIENT_IP . "'";
case 'unixtime()':
return new Expression('string', time());
return time();
case 'curdate()':
case 'date()':
return new Expression('string', date('YmdHis'));
return "'" . date('YmdHis') . "'";
case 'sequence()':
return new Expression('int', getNextSequence());
return getNextSequence();
}
// If the default value is a calculation based on the current value, return a query string.
if (isset($this->column) && preg_match('/^(plus|minus|multiply)\(([0-9]+)\)$/', $val, $matches))
if (isset($this->column) && preg_match('/^(plus|minus|multiply)\(([0-9]+)\)$/', $this->default, $matches))
{
switch ($matches[1])
{
case 'plus':
return sprintf('%s + %d', Query::quoteName($this->column), $matches[2]);
case 'minus':
return sprintf('%s - %d', Query::quoteName($this->column), $matches[2]);
case 'multiply':
return sprintf('%s * %d', Query::quoteName($this->column), $matches[2]);
}
}
// If the default value is a column name, return the column name.20
if (\Rhymix\Framework\Parsers\DBQueryParser::isValidColumnName($val))
// Otherwise, just return the literal value.
return $this->default;
}
/**
* Filter a value.
*
* @param mixed $value
* @return void
*/
public function filterValue($value)
{
// Apply filters.
switch (isset($this->filter) ? $this->filter : '')
{
case 'email':
case 'email_address':
if (!preg_match('/^[\w-]+((?:\.|\+|\~)[\w-]+)*@[\w-]+(\.[\w-]+)+$/', $value))
{
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' must contain a valid e-mail address');
}
break;
case 'homepage':
case 'url':
if (!preg_match('/^(http|https)+(:\/\/)+[0-9a-z_-]+\.[^ ]+$/i', $value))
{
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' must contain a valid URL');
}
break;
case 'userid':
case 'user_id':
if (!preg_match('/^[a-zA-Z]+([_0-9a-zA-Z]+)*$/', $value))
{
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' must contain a valid user ID');
}
break;
case 'number':
case 'numbers':
if (!preg_match('/^(-?)[0-9]+(,\-?[0-9]+)*$/', is_array($value) ? implode(',', $value) : $value))
{
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' must contain a valid number');
}
break;
case 'alpha':
if (!ctype_alpha($value))
{
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' must contain only alphabets');
}
break;
case 'alnum':
case 'alpha_number':
if (!ctype_alnum($value))
{
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' must contain only alphanumeric characters');
}
break;
}
// Otherwise, return the literal value.
return new Expression('string', $val);
// Check minimum and maximum lengths.
$length = iconv_strlen($value, 'UTF-8');
if (isset($this->minlength) && $this->minlength > 0 && $length < $this->minlength)
{
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' must contain no less than ' . $this->minlength . ' characters');
}
if (isset($this->maxlength) && $this->maxlength > 0 && $length > $this->maxlength)
{
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' must contain no more than ' . $this->minlength . ' characters');
}
}
}