diff --git a/src/xPDO/Om/mysql/xPDOQuery.php b/src/xPDO/Om/mysql/xPDOQuery.php index 38988237..54dc1420 100644 --- a/src/xPDO/Om/mysql/xPDOQuery.php +++ b/src/xPDO/Om/mysql/xPDOQuery.php @@ -133,4 +133,15 @@ public function construct() { $this->sql= $sql; return (!empty ($this->sql)); } + + protected function isColumnIdentifier($key) + { + $unQuoted = '(?![\d]+(?:\.|$))[A-Za-z0-9$_\x80-\xff]{1,64}'; + $escapeChars = '`(?:[^`\x00]|``){1,128}`'; + $dblQuote = '"(?:[^"\x00]|""){1,128}"'; + $ident = "(?:{$escapeChars}|{$dblQuote}|{$unQuoted})"; + $columnIdent = "(?:{$ident}\.)?{$ident}"; + + return (bool) preg_match("/^{$columnIdent}$/", trim($key)); + } } diff --git a/src/xPDO/Om/pgsql/xPDOQuery.php b/src/xPDO/Om/pgsql/xPDOQuery.php index 095eeec0..1bbbb0a5 100644 --- a/src/xPDO/Om/pgsql/xPDOQuery.php +++ b/src/xPDO/Om/pgsql/xPDOQuery.php @@ -124,4 +124,15 @@ public function construct() { $this->sql= $sql; return (!empty ($this->sql)); } + + protected function isColumnIdentifier($key) + { + $unQuoted = '[A-Za-z_\x80-\xff][A-Za-z0-9_$\x80-\xff]{0,62}'; + $dblQuote = '"(?:[^"\x00]|""){1,126}"'; + $unicode = 'U&"(?:[^"\x00]|""|\\\\[0-9A-Fa-f]{4}|\\\\\\+[0-9A-Fa-f]{6}){1,}"(?:\s*UESCAPE\s*\'[^0-9A-Fa-f+\x27\\\\]\s*\')?'; + $ident = "(?:{$unicode}|{$dblQuote}|{$unQuoted})"; + $columnIdent = "(?:{$ident}\.)?{$ident}"; + + return (bool) preg_match("/^{$columnIdent}$/", trim($key)); + } } diff --git a/src/xPDO/Om/sqlite/xPDOQuery.php b/src/xPDO/Om/sqlite/xPDOQuery.php index f2364402..19a6f0b9 100644 --- a/src/xPDO/Om/sqlite/xPDOQuery.php +++ b/src/xPDO/Om/sqlite/xPDOQuery.php @@ -133,4 +133,16 @@ public function construct() { $this->sql= $sql; return (!empty ($this->sql)); } + + protected function isColumnIdentifier($key) + { + $unQuoted = '[A-Za-z_][A-Za-z0-9_]*'; + $backtick = '`(?:[^`\x00]|``){1,128}`'; + $bracket = '\[[^\]\x00]+\]'; + $dblQuote = '"(?:[^"\x00]|""){1,}"'; + $ident = "(?:{$dblQuote}|{$backtick}|{$bracket}|{$unQuoted})"; + $columnIdent = "(?:{$ident}\.)?{$ident}"; + + return (bool) preg_match("/^{$columnIdent}$/", trim($key)); + } } diff --git a/src/xPDO/Om/sqlsrv/xPDOQuery.php b/src/xPDO/Om/sqlsrv/xPDOQuery.php index d191ea95..be20f248 100644 --- a/src/xPDO/Om/sqlsrv/xPDOQuery.php +++ b/src/xPDO/Om/sqlsrv/xPDOQuery.php @@ -296,4 +296,15 @@ public function construct() { $this->sql= $sql; return (!empty ($this->sql)); } + + protected function isColumnIdentifier($key) + { + $unQuoted = '(?![\d]+(?:\.|$))[A-Za-z_@#\x80-\xff][A-Za-z0-9_@#$\x80-\xff]{0,127}'; + $bracket = '\[(?:[^\]\x00\xff\xff]|\]\]){1,128}\]'; + $dblQuote = '"(?:[^"\x00]|""){1,128}"'; + $ident = "(?:{$bracket}|{$dblQuote}|{$unQuoted})"; + $columnIdent = "(?:{$ident}\.)?{$ident}"; + + return (bool) preg_match("/^{$columnIdent}$/", trim($key)); + } } diff --git a/src/xPDO/Om/xPDOCriteria.php b/src/xPDO/Om/xPDOCriteria.php index d506eae7..40172226 100644 --- a/src/xPDO/Om/xPDOCriteria.php +++ b/src/xPDO/Om/xPDOCriteria.php @@ -18,6 +18,7 @@ * @package xPDO\Om */ class xPDOCriteria { + /** @var xPDO $xpdo */ public $xpdo= null; public $sql= ''; public $stmt= null; diff --git a/src/xPDO/Om/xPDOQuery.php b/src/xPDO/Om/xPDOQuery.php index c4e98907..f5ab862e 100644 --- a/src/xPDO/Om/xPDOQuery.php +++ b/src/xPDO/Om/xPDOQuery.php @@ -66,7 +66,22 @@ abstract class xPDOQuery extends xPDOCriteria { 'MIN(', 'AVG(' ); - protected $_quotable= array ('string', 'password', 'date', 'datetime', 'timestamp', 'time', 'json', 'array', 'float'); + protected $_quotable = [ + 'string', + 'password', + 'date', + 'datetime', + 'timestamp', + 'time', + 'json', + 'array', + 'float', + 'double', + 'object', + 'resource', + 'unknown type', + 'resource (closed)' + ]; protected $_class= null; protected $_alias= null; protected $_tableClass = null; @@ -737,30 +752,42 @@ public function parseConditions($conditions, $conjunction = xPDOQuery::SQL_AND) $key= $key_operator[1]; $operator= strtoupper($key_operator[2]); } - if (strpos($key, '.') !== false) { - $key_parts= explode('.', $key); - $alias= trim($key_parts[0], " {$this->xpdo->_escapeCharOpen}{$this->xpdo->_escapeCharClose}"); - $key= $key_parts[1]; - } - if (!array_key_exists($key, $fieldMeta)) { - if (array_key_exists($key, $fieldAliases)) { - $key= $fieldAliases[$key]; - } elseif ($this->isConditionalClause($key)) { - continue; + $operand = $key; + $isColumnIdentifier = $this->isColumnIdentifier($key); + if ($isColumnIdentifier) { + if (strpos($key, '.') !== false) { + $key_parts = explode('.', $key); + $alias = trim( + $key_parts[0], + " {$this->xpdo->_escapeCharOpen}{$this->xpdo->_escapeCharClose}" + ); + $key = $key_parts[1]; } + if (!array_key_exists($key, $fieldMeta)) { + if (array_key_exists($key, $fieldAliases)) { + $key = $fieldAliases[$key]; + } + } + $operand = "{$this->xpdo->escape($alias)}.{$this->xpdo->escape($key)}"; } - if (!empty($key)) { + if (!empty($key) && !$this->isConditionalClause($key)) { if ($val === null) { $type= \PDO::PARAM_NULL; if (!in_array($operator, array('IS', 'IS NOT'))) { $operator= $operator === '!=' ? 'IS NOT' : 'IS'; } } - elseif (isset($fieldMeta[$key]) && !in_array($fieldMeta[$key]['phptype'], $this->_quotable)) { - $type= \PDO::PARAM_INT; - } - else { - $type= \PDO::PARAM_STR; + elseif ($isColumnIdentifier) { + if (isset($fieldMeta[$key]) && !in_array( + $fieldMeta[$key]['phptype'], + $this->_quotable + )) { + $type = \PDO::PARAM_INT; + } else { + $type = \PDO::PARAM_STR; + } + } else { + $type = $this->isQuotable($val) ? \PDO::PARAM_STR : \PDO::PARAM_INT; } if (in_array($operator, array('IN', 'NOT IN')) && is_array($val)) { $vals = array(); @@ -785,12 +812,12 @@ public function parseConditions($conditions, $conjunction = xPDOQuery::SQL_AND) $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Encountered empty {$operator} condition with key {$key}"); } $val = "(" . implode(',', $vals) . ")"; - $sql = "{$this->xpdo->escape($alias)}.{$this->xpdo->escape($key)} {$operator} {$val}"; + $sql = "{$operand} {$operator} {$val}"; $result[]= new xPDOQueryCondition(array('sql' => $sql, 'binding' => null, 'conjunction' => $conj)); continue; } $field= array (); - $field['sql']= $this->xpdo->escape($alias) . '.' . $this->xpdo->escape($key) . ' ' . $operator . ' ?'; + $field['sql']= $operand . ' ' . $operator . ' ?'; $field['binding']= array ( 'value' => $val, 'type' => $type, @@ -808,8 +835,8 @@ public function parseConditions($conditions, $conjunction = xPDOQuery::SQL_AND) elseif ($this->isConditionalClause($conditions)) { $result= new xPDOQueryCondition(array( 'sql' => $conditions - ,'binding' => null - ,'conjunction' => $conjunction + ,'binding' => null + ,'conjunction' => $conjunction )); } elseif (($pktype == 'integer' && is_numeric($conditions)) || ($pktype == 'string' && is_string($conditions) && static::isValidClause($conditions))) { @@ -937,4 +964,10 @@ public function __debugInfo() 'bindings' => $this->bindings, ]; } + + protected function isQuotable($value) { + return in_array(gettype($value), $this->_quotable); + } + + abstract protected function isColumnIdentifier($key); } diff --git a/test/complete.phpunit.xml b/test/complete.phpunit.xml index 1531af41..fd191595 100644 --- a/test/complete.phpunit.xml +++ b/test/complete.phpunit.xml @@ -22,6 +22,7 @@ ./xPDO/Test/Om/xPDOObjectTest.php ./xPDO/Test/Om/xPDOObjectSingleTableInheritanceTest.php ./xPDO/Test/Om/xPDOQueryTest.php + ./xPDO/Test/Om/xPDOQueryConditionsTest.php ./xPDO/Test/Om/xPDOQueryHavingTest.php ./xPDO/Test/Om/xPDOQueryLimitTest.php ./xPDO/Test/Om/xPDOQuerySortByTest.php diff --git a/test/mysql.phpunit.xml b/test/mysql.phpunit.xml index 1531af41..fd191595 100644 --- a/test/mysql.phpunit.xml +++ b/test/mysql.phpunit.xml @@ -22,6 +22,7 @@ ./xPDO/Test/Om/xPDOObjectTest.php ./xPDO/Test/Om/xPDOObjectSingleTableInheritanceTest.php ./xPDO/Test/Om/xPDOQueryTest.php + ./xPDO/Test/Om/xPDOQueryConditionsTest.php ./xPDO/Test/Om/xPDOQueryHavingTest.php ./xPDO/Test/Om/xPDOQueryLimitTest.php ./xPDO/Test/Om/xPDOQuerySortByTest.php diff --git a/test/pgsql.phpunit.xml b/test/pgsql.phpunit.xml index bcc32639..1ba49964 100644 --- a/test/pgsql.phpunit.xml +++ b/test/pgsql.phpunit.xml @@ -22,6 +22,7 @@ ./xPDO/Test/Om/xPDOObjectTest.php ./xPDO/Test/Om/xPDOObjectSingleTableInheritanceTest.php ./xPDO/Test/Om/xPDOQueryTest.php + ./xPDO/Test/Om/xPDOQueryConditionsTest.php ./xPDO/Test/Om/xPDOQueryHavingTest.php ./xPDO/Test/Om/xPDOQueryLimitTest.php ./xPDO/Test/Om/xPDOQuerySortByTest.php diff --git a/test/sqlite.phpunit.xml b/test/sqlite.phpunit.xml index c21b4068..b6d1ea27 100644 --- a/test/sqlite.phpunit.xml +++ b/test/sqlite.phpunit.xml @@ -22,6 +22,7 @@ ./xPDO/Test/Om/xPDOObjectTest.php ./xPDO/Test/Om/xPDOObjectSingleTableInheritanceTest.php ./xPDO/Test/Om/xPDOQueryTest.php + ./xPDO/Test/Om/xPDOQueryConditionsTest.php ./xPDO/Test/Om/xPDOQueryHavingTest.php ./xPDO/Test/Om/xPDOQueryLimitTest.php ./xPDO/Test/Om/xPDOQuerySortByTest.php diff --git a/test/xPDO/Test/Om/xPDOQueryConditionsTest.php b/test/xPDO/Test/Om/xPDOQueryConditionsTest.php index 61cdb358..d4443940 100644 --- a/test/xPDO/Test/Om/xPDOQueryConditionsTest.php +++ b/test/xPDO/Test/Om/xPDOQueryConditionsTest.php @@ -29,7 +29,7 @@ public function setUpFixtures() $this->xpdo->manager->createObjectContainer('xPDO\Test\Sample\xPDOSample'); - $sample = $this->xpdo->newObject('xPDO\Test\Sample\xPDOSample', [ + $this->xpdo->newObject('xPDO\Test\Sample\xPDOSample', [ 'parent' => 0, 'unique_varchar' => uniqid('prefix_'), 'varchar' => 'varchar', @@ -54,13 +54,13 @@ public function setUpFixtures() */ public function tearDownFixtures() { - parent::tearDownFixtures(); - try { $this->xpdo->manager->removeObjectContainer('xPDO\Test\Sample\xPDOSample'); } catch (\Exception $e) { $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, $e->getMessage(), '', __METHOD__, __FILE__, __LINE__); } + + parent::tearDownFixtures(); } /** @@ -79,7 +79,7 @@ public function testSelectConditions($condition) public function providerSelectConditions() { - return [ + $data = [ [ ['parent' => 0] ], @@ -94,7 +94,154 @@ public function providerSelectConditions() ], [ ['date_time' => '2018-03-24 00:00:00'] - ] + ], + [ + [ + 'integer' => 1999, + 'OR:date_time:=' => '2018-03-24 00:00:00' + ] + ], ]; + + switch (self::$properties['xpdo_driver']) { + case 'mysql': + $data = array_merge($data, [ + [ + [ + '`integer` - 1' => 1998, + ] + ], + [ + [ + 'enum' => 'T', + [ + 'OR:`integer` - 1:=' => 1998, + 'date_time' => '2018-03-24 00:00:00' + ] + ] + ], + [ + [ + 'LOG(`integer`)' => 7.6009, + [ + 'OR:text:=' => 'text', + 'date_time' => '2018-03-24 00:00:00' + ] + ] + ], + [ + [ + 'date_time' => null, + 'OR:LOG(`integer`):=' => 7.6009, + 'OR:float:=' => 3.14159, + ] + ], + ]); + break; + case 'sqlite': + $data = array_merge($data, [ + [ + [ + '`integer` - 1' => 1998, + ] + ], + [ + [ + 'enum' => 'T', + [ + 'OR:`integer` - 1:=' => 1998, + 'date_time' => '2018-03-24 00:00:00' + ] + ] + ], + [ + [ + 'LOG(`integer`)' => 7.6009, + [ + 'OR:text:=' => 'text', + 'date_time' => '2018-03-24 00:00:00' + ] + ] + ], + [ + [ + 'date_time' => null, + 'OR:LOG(`integer`):=' => 7.6009, + 'OR:float:=' => 3.14159, + ] + ], + ]); + break; + case 'pgsql': + $data = array_merge($data, [ + [ + [ + '"integer" - 1' => 1998, + ] + ], + [ + [ + 'enum' => 'T', + [ + 'OR:"integer" - 1:=' => 1998, + 'date_time' => '2018-03-24 00:00:00' + ] + ] + ], + [ + [ + 'LOG("integer")' => 7.6009, + [ + 'OR:text:=' => 'text', + 'date_time' => '2018-03-24 00:00:00' + ] + ] + ], + [ + [ + 'date_time' => null, + 'OR:LOG("integer"):=' => 7.6009, + 'OR:float:=' => 3.14159, + ] + ], + ]); + break; + case 'sqlsrv': + $data = array_merge($data, [ + [ + [ + '[integer] - 1' => 1998, + ] + ], + [ + [ + 'enum' => 'T', + [ + 'OR:[integer] - 1:=' => 1998, + 'date_time' => '2018-03-24 00:00:00' + ] + ] + ], + [ + [ + 'LOG([integer])' => 7.6009, + [ + 'OR:text:=' => 'text', + 'date_time' => '2018-03-24 00:00:00' + ] + ] + ], + [ + [ + 'date_time' => null, + 'OR:LOG([integer]):=' => 7.6009, + 'OR:float:=' => 3.14159, + ] + ], + ]); + break; + } + + return $data; } }