Skip to content

Commit df38172

Browse files
authored
Merge pull request #1677 from codeigniter4/escapeinserts
Implement Don't Escape feature for db engine
2 parents b510f7d + 113ed76 commit df38172

9 files changed

Lines changed: 274 additions & 72 deletions

File tree

system/Database/BaseBuilder.php

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,9 @@ public function __construct($tableName, ConnectionInterface &$db, array $options
227227

228228
$this->db = $db;
229229

230+
// turn off automatic escape flags
231+
$this->db->setEscapeFlags(false);
232+
230233
$this->from($tableName);
231234

232235
if (! empty($options))
@@ -664,7 +667,7 @@ protected function whereHaving($qb_key, $key, $value = null, $type = 'AND ', $es
664667
$op = $this->getOperator($k);
665668
$k = trim(str_replace($op, '', $k));
666669

667-
$bind = $this->setBind($k, $v);
670+
$bind = $this->setBind($k, $v, $escape);
668671

669672
if (empty($op))
670673
{
@@ -814,7 +817,7 @@ protected function _whereIn($key = null, $values = null, $not = false, $type = '
814817
$not = ($not) ? ' NOT' : '';
815818

816819
$where_in = array_values($values);
817-
$ok = $this->setBind($ok, $where_in);
820+
$ok = $this->setBind($ok, $where_in, $escape);
818821

819822
$prefix = empty($this->QBWhere) ? $this->groupGetType('') : $this->groupGetType($type);
820823

@@ -955,19 +958,19 @@ protected function _like($field, $match = '', $type = 'AND ', $side = 'both', $n
955958

956959
if ($side === 'none')
957960
{
958-
$bind = $this->setBind($k, $v);
961+
$bind = $this->setBind($k, $v, $escape);
959962
}
960963
elseif ($side === 'before')
961964
{
962-
$bind = $this->setBind($k, "%$v");
965+
$bind = $this->setBind($k, "%$v", $escape);
963966
}
964967
elseif ($side === 'after')
965968
{
966-
$bind = $this->setBind($k, "$v%");
969+
$bind = $this->setBind($k, "$v%", $escape);
967970
}
968971
else
969972
{
970-
$bind = $this->setBind($k, "%$v%");
973+
$bind = $this->setBind($k, "%$v%", $escape);
971974
}
972975

973976
$like_statement = $this->_like_statement($prefix, $k, $not, $bind, $insensitiveSearch);
@@ -1345,7 +1348,7 @@ public function set($key, $value = '', $escape = null)
13451348
{
13461349
if ($escape)
13471350
{
1348-
$bind = $this->setBind($k, $v);
1351+
$bind = $this->setBind($k, $v, $escape);
13491352
$this->QBSet[$this->db->protectIdentifiers($k, false, $escape)] = ":$bind:";
13501353
}
13511354
else
@@ -1415,7 +1418,7 @@ public function getCompiledSelect($reset = true)
14151418
protected function compileFinalQuery(string $sql): string
14161419
{
14171420
$query = new Query($this->db);
1418-
$query->setQuery($sql, $this->binds);
1421+
$query->setQuery($sql, $this->binds, false);
14191422

14201423
if (! empty($this->db->swapPre) && ! empty($this->db->DBPrefix))
14211424
{
@@ -1718,7 +1721,7 @@ public function setInsertBatch($key, $value = '', $escape = null)
17181721
$clean = [];
17191722
foreach ($row as $k => $value)
17201723
{
1721-
$clean[] = ':' . $this->setBind($k, $value) . ':';
1724+
$clean[] = ':' . $this->setBind($k, $value, $escape) . ':';
17221725
}
17231726

17241727
$row = $clean;
@@ -2225,7 +2228,7 @@ public function setUpdateBatch($key, $index = '', $escape = null)
22252228
$index_set = true;
22262229
}
22272230

2228-
$bind = $this->setBind($k2, $v2);
2231+
$bind = $this->setBind($k2, $v2, $escape);
22292232

22302233
$clean[$this->db->protectIdentifiers($k2, false, $escape)] = ":$bind:";
22312234
}
@@ -2940,17 +2943,24 @@ protected function getOperator($str)
29402943

29412944
/**
29422945
* Stores a bind value after ensuring that it's unique.
2946+
* While it might be nicer to have named keys for our binds array
2947+
* with PHP 7+ we get a huge memory/performance gain with indexed
2948+
* arrays instead, so lets take advantage of that here.
29432949
*
2944-
* @param string $key
2945-
* @param null $value
2950+
* @param string $key
2951+
* @param null $value
2952+
* @param boolean $escape
29462953
*
29472954
* @return string
29482955
*/
2949-
protected function setBind(string $key, $value = null)
2956+
protected function setBind(string $key, $value = null, bool $escape = true)
29502957
{
29512958
if (! array_key_exists($key, $this->binds))
29522959
{
2953-
$this->binds[$key] = $value;
2960+
$this->binds[$key] = [
2961+
$value,
2962+
$escape,
2963+
];
29542964

29552965
return $key;
29562966
}
@@ -2962,7 +2972,10 @@ protected function setBind(string $key, $value = null)
29622972
++$count;
29632973
}
29642974

2965-
$this->binds[$key . $count] = $value;
2975+
$this->binds[$key . $count] = [
2976+
$value,
2977+
$escape,
2978+
];
29662979

29672980
return $key . $count;
29682981
}

system/Database/BaseConnection.php

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,16 @@ abstract class BaseConnection implements ConnectionInterface
327327
*/
328328
protected $aliasedTables = [];
329329

330+
/**
331+
* Should the bindings be collected with a default escape value?
332+
* The Builder will automatically set this to false, so that any
333+
* $db->query('...', [binds]); Will result in bindings being
334+
* automatically escaped.
335+
*
336+
* @var boolean
337+
*/
338+
protected $setEscapeFlags = true;
339+
330340
//--------------------------------------------------------------------
331341

332342
/**
@@ -577,6 +587,20 @@ public function addTableAlias(string $table)
577587
return $this;
578588
}
579589

590+
/**
591+
* Should query() automatically attach escape flags to each bound var?
592+
*
593+
* @param boolean $setFlags
594+
*
595+
* @return $this
596+
*/
597+
public function setEscapeFlags(bool $setFlags)
598+
{
599+
$this->setEscapeFlags = $setFlags;
600+
601+
return $this;
602+
}
603+
580604
/**
581605
* Executes the query against the database.
582606
*
@@ -596,9 +620,11 @@ abstract protected function execute($sql);
596620
* Should automatically handle different connections for read/write
597621
* queries if needed.
598622
*
599-
* @param string $sql
600-
* @param array ...$binds
601-
* @param string $queryClass
623+
* @param string $sql
624+
* @param array ...$binds
625+
* @param string $queryClass
626+
* @param boolean $setEscape
627+
*
602628
* @return BaseResult|Query|false
603629
*/
604630
public function query(string $sql, $binds = null, $queryClass = 'CodeIgniter\\Database\\Query')
@@ -609,13 +635,12 @@ public function query(string $sql, $binds = null, $queryClass = 'CodeIgniter\\Da
609635
}
610636

611637
$resultClass = str_replace('Connection', 'Result', get_class($this));
612-
613638
/**
614639
* @var Query $query
615640
*/
616641
$query = new $queryClass($this);
617642

618-
$query->setQuery($sql, $binds);
643+
$query->setQuery($sql, $binds, $this->setEscapeFlags);
619644

620645
if (! empty($this->swapPre) && ! empty($this->DBPrefix))
621646
{

system/Database/Query.php

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,17 +129,32 @@ public function __construct(&$db)
129129
/**
130130
* Sets the raw query string to use for this statement.
131131
*
132-
* @param string $sql
133-
* @param array $binds
132+
* @param string $sql
133+
* @param array $binds
134+
* @param boolean $setEscape
134135
*
135136
* @return mixed
136137
*/
137-
public function setQuery(string $sql, $binds = null)
138+
public function setQuery(string $sql, $binds = null, bool $setEscape = true)
138139
{
139140
$this->originalQueryString = $sql;
140141

141142
if (! is_null($binds))
142143
{
144+
if (! is_array($binds))
145+
{
146+
$binds = [$binds];
147+
}
148+
149+
if ($setEscape)
150+
{
151+
array_walk($binds, function (&$item) {
152+
$item = [
153+
$item,
154+
true,
155+
];
156+
});
157+
}
143158
$this->binds = $binds;
144159
}
145160

@@ -407,12 +422,13 @@ protected function matchNamedBinds(string $sql, array $binds)
407422

408423
foreach ($binds as $placeholder => $value)
409424
{
410-
$escapedValue = $this->db->escape($value);
425+
// $value[1] contains the boolean whether should be escaped or not
426+
$escapedValue = $value[1] ? $this->db->escape($value[0]) : $value[0];
411427

412428
// In order to correctly handle backlashes in saved strings
413429
// we will need to preg_quote, so remove the wrapping escape characters
414430
// otherwise it will get escaped.
415-
if (is_array($value))
431+
if (is_array($value[0]))
416432
{
417433
$escapedValue = '(' . implode(',', $escapedValue) . ')';
418434
}
@@ -460,7 +476,7 @@ protected function matchSimpleBinds(string $sql, array $binds, int $bindCount, i
460476
do
461477
{
462478
$c --;
463-
$escapedValue = $this->db->escape($binds[$c]);
479+
$escapedValue = $binds[$c][1] ? $this->db->escape($binds[$c][0]) : $binds[$c[0]];
464480
if (is_array($escapedValue))
465481
{
466482
$escapedValue = '(' . implode(',', $escapedValue) . ')';

tests/_support/Database/MockConnection.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public function query(string $sql, $binds = null, $queryClass = 'CodeIgniter\\Da
2727

2828
$query = new $queryClass($this);
2929

30-
$query->setQuery($sql, $binds);
30+
$query->setQuery($sql, $binds, $this->setEscapeFlags);
3131

3232
if (! empty($this->swapPre) && ! empty($this->DBPrefix))
3333
{

tests/system/Database/Builder/DeleteTest.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@ public function testDelete()
2424
$answer = $builder->delete(['id' => 1], null, true, true);
2525

2626
$expectedSQL = 'DELETE FROM "jobs" WHERE "id" = :id:';
27-
$expectedBinds = ['id' => 1];
27+
$expectedBinds = [
28+
'id' => [
29+
1,
30+
true,
31+
],
32+
];
2833

2934
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $answer));
3035
$this->assertEquals($expectedBinds, $builder->getBinds());

tests/system/Database/Builder/InsertTest.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,16 @@ public function testSimpleInsert()
2929
$builder->insert($insertData, true, true);
3030

3131
$expectedSQL = 'INSERT INTO "jobs" ("id", "name") VALUES (1, \'Grocery Sales\')';
32-
$expectedBinds = $insertData;
32+
$expectedBinds = [
33+
'id' => [
34+
1,
35+
true,
36+
],
37+
'name' => [
38+
'Grocery Sales',
39+
true,
40+
],
41+
];
3342

3443
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledInsert()));
3544
$this->assertEquals($expectedBinds, $builder->getBinds());
@@ -97,7 +106,7 @@ public function testInsertBatchThrowsExceptionOnNoData()
97106

98107
//--------------------------------------------------------------------
99108

100-
public function testInsertBatchThrowsExceptionOnEmptData()
109+
public function testInsertBatchThrowsExceptionOnEmptyData()
101110
{
102111
$builder = $this->db->table('jobs');
103112

0 commit comments

Comments
 (0)