Skip to content

Commit b19fdc1

Browse files
committed
Fix FR #71885 (Allow escaping question mark placeholders)
1 parent 5d827c8 commit b19fdc1

File tree

5 files changed

+236
-42
lines changed

5 files changed

+236
-42
lines changed

ext/pdo/pdo_sql_parser.re

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
#define PDO_PARSER_TEXT 1
2424
#define PDO_PARSER_BIND 2
2525
#define PDO_PARSER_BIND_POS 3
26-
#define PDO_PARSER_EOI 4
26+
#define PDO_PARSER_ESCAPED_QUESTION 4
27+
#define PDO_PARSER_EOI 5
28+
29+
#define PDO_PARSER_BINDNO_ESCAPED_CHAR -1
2730

2831
#define RET(i) {s->cur = cursor; return i; }
2932
#define SKIP_ONE(i) {s->cur = s->tok + 1; return i; }
@@ -46,16 +49,18 @@ static int scan(Scanner *s)
4649
/*!re2c
4750
BINDCHR = [:][a-zA-Z0-9_]+;
4851
QUESTION = [?];
52+
ESCQUESTION = [?][?];
4953
COMMENTS = ("/*"([^*]+|[*]+[^/*])*[*]*"*/"|"--"[^\r\n]*);
5054
SPECIALS = [:?"'-/];
51-
MULTICHAR = ([:]{2,}|[?]{2,});
55+
MULTICHAR = [:]{2,};
5256
ANYNOEOF = [\001-\377];
5357
*/
5458
5559
/*!re2c
5660
(["](([\\]ANYNOEOF)|ANYNOEOF\["\\])*["]) { RET(PDO_PARSER_TEXT); }
5761
(['](([\\]ANYNOEOF)|ANYNOEOF\['\\])*[']) { RET(PDO_PARSER_TEXT); }
5862
MULTICHAR { RET(PDO_PARSER_TEXT); }
63+
ESCQUESTION { RET(PDO_PARSER_ESCAPED_QUESTION); }
5964
BINDCHR { RET(PDO_PARSER_BIND); }
6065
QUESTION { RET(PDO_PARSER_BIND_POS); }
6166
SPECIALS { SKIP_ONE(PDO_PARSER_TEXT); }
@@ -85,7 +90,7 @@ PDO_API int pdo_parse_params(pdo_stmt_t *stmt, char *inquery, size_t inquery_len
8590
char *ptr, *newbuffer;
8691
ptrdiff_t t;
8792
uint32_t bindno = 0;
88-
int ret = 0;
93+
int ret = 0, escapes = 0;
8994
size_t newbuffer_len;
9095
HashTable *params;
9196
struct pdo_bound_param_data *param;
@@ -98,14 +103,19 @@ PDO_API int pdo_parse_params(pdo_stmt_t *stmt, char *inquery, size_t inquery_len
98103

99104
/* phase 1: look for args */
100105
while((t = scan(&s)) != PDO_PARSER_EOI) {
101-
if (t == PDO_PARSER_BIND || t == PDO_PARSER_BIND_POS) {
106+
if (t == PDO_PARSER_BIND || t == PDO_PARSER_BIND_POS || t == PDO_PARSER_ESCAPED_QUESTION) {
107+
if (t == PDO_PARSER_ESCAPED_QUESTION && stmt->supports_placeholders == PDO_PLACEHOLDER_POSITIONAL) {
108+
/* escaped question marks unsupported, treat as text */
109+
continue;
110+
}
111+
102112
if (t == PDO_PARSER_BIND) {
103113
ptrdiff_t len = s.cur - s.tok;
104114
if ((inquery < (s.cur - len)) && isalnum(*(s.cur - len - 1))) {
105115
continue;
106116
}
107117
query_type |= PDO_PLACEHOLDER_NAMED;
108-
} else {
118+
} else if (t == PDO_PARSER_BIND_POS) {
109119
query_type |= PDO_PLACEHOLDER_POSITIONAL;
110120
}
111121

@@ -114,7 +124,16 @@ PDO_API int pdo_parse_params(pdo_stmt_t *stmt, char *inquery, size_t inquery_len
114124
plc->next = NULL;
115125
plc->pos = s.tok;
116126
plc->len = s.cur - s.tok;
117-
plc->bindno = bindno++;
127+
128+
if (t == PDO_PARSER_ESCAPED_QUESTION) {
129+
plc->bindno = PDO_PARSER_BINDNO_ESCAPED_CHAR;
130+
plc->quoted = "?";
131+
plc->qlen = 1;
132+
plc->freeq = 0;
133+
escapes++;
134+
} else {
135+
plc->bindno = bindno++;
136+
}
118137

119138
if (placetail) {
120139
placetail->next = plc;
@@ -125,7 +144,7 @@ PDO_API int pdo_parse_params(pdo_stmt_t *stmt, char *inquery, size_t inquery_len
125144
}
126145
}
127146

128-
if (bindno == 0) {
147+
if (!placeholders) {
129148
/* nothing to do; good! */
130149
return 0;
131150
}
@@ -140,11 +159,16 @@ PDO_API int pdo_parse_params(pdo_stmt_t *stmt, char *inquery, size_t inquery_len
140159

141160
if (stmt->supports_placeholders == query_type && !stmt->named_rewrite_template) {
142161
/* query matches native syntax */
162+
if (escapes) {
163+
newbuffer_len = inquery_len;
164+
goto rewrite;
165+
}
166+
143167
ret = 0;
144168
goto clean_up;
145169
}
146170

147-
if (stmt->named_rewrite_template) {
171+
if (query_type == PDO_PLACEHOLDER_NAMED && stmt->named_rewrite_template) {
148172
/* magic/hack.
149173
* We we pretend that the query was positional even if
150174
* it was named so that we fall into the
@@ -155,14 +179,7 @@ PDO_API int pdo_parse_params(pdo_stmt_t *stmt, char *inquery, size_t inquery_len
155179

156180
params = stmt->bound_params;
157181

158-
/* Do we have placeholders but no bound params */
159-
if (bindno && !params && stmt->supports_placeholders == PDO_PLACEHOLDER_NONE) {
160-
pdo_raise_impl_error(stmt->dbh, stmt, "HY093", "no parameters were bound");
161-
ret = -1;
162-
goto clean_up;
163-
}
164-
165-
if (params && bindno != zend_hash_num_elements(params) && stmt->supports_placeholders == PDO_PLACEHOLDER_NONE) {
182+
if (bindno && stmt->supports_placeholders == PDO_PLACEHOLDER_NONE && params && bindno != zend_hash_num_elements(params)) {
166183
/* extra bit of validation for instances when same params are bound more than once */
167184
if (query_type != PDO_PLACEHOLDER_POSITIONAL && bindno > zend_hash_num_elements(params)) {
168185
int ok = 1;
@@ -188,7 +205,16 @@ safe:
188205
newbuffer_len = inquery_len;
189206

190207
/* let's quote all the values */
191-
for (plc = placeholders; plc; plc = plc->next) {
208+
for (plc = placeholders; plc && params; plc = plc->next) {
209+
if (plc->bindno == PDO_PARSER_BINDNO_ESCAPED_CHAR) {
210+
/* escaped character */
211+
continue;
212+
}
213+
214+
if (query_type == PDO_PLACEHOLDER_NONE) {
215+
continue;
216+
}
217+
192218
if (query_type == PDO_PLACEHOLDER_POSITIONAL) {
193219
param = zend_hash_index_find_ptr(params, plc->bindno);
194220
} else {
@@ -316,8 +342,13 @@ rewrite:
316342
memcpy(newbuffer, ptr, t);
317343
newbuffer += t;
318344
}
319-
memcpy(newbuffer, plc->quoted, plc->qlen);
320-
newbuffer += plc->qlen;
345+
if (plc->quoted) {
346+
memcpy(newbuffer, plc->quoted, plc->qlen);
347+
newbuffer += plc->qlen;
348+
} else {
349+
memcpy(newbuffer, plc->pos, plc->len);
350+
newbuffer += plc->len;
351+
}
321352
ptr = plc->pos + plc->len;
322353

323354
plc = plc->next;
@@ -350,6 +381,11 @@ rewrite:
350381
for (plc = placeholders; plc; plc = plc->next) {
351382
int skip_map = 0;
352383
char *p;
384+
385+
if (plc->bindno == PDO_PARSER_BINDNO_ESCAPED_CHAR) {
386+
continue;
387+
}
388+
353389
name = estrndup(plc->pos, plc->len);
354390

355391
/* check if bound parameter is already available */
@@ -395,6 +431,7 @@ rewrite:
395431
efree(name);
396432
plc->quoted = "?";
397433
plc->qlen = 1;
434+
newbuffer_len -= plc->len - 1;
398435
}
399436

400437
goto rewrite;

ext/pdo/tests/bug_71885.phpt

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
--TEST--
2+
PDO Common: FR #71885 (Allow escaping question mark placeholders)
3+
--SKIPIF--
4+
<?php
5+
if (!extension_loaded('pdo')) die('skip');
6+
$dir = getenv('REDIR_TEST_DIR');
7+
if (false == $dir) die('skip no driver');
8+
if (!strncasecmp(getenv('PDOTEST_DSN'), 'pgsql', strlen('pgsql'))) die('skip not relevant for pgsql driver');
9+
if (!strncasecmp(getenv('PDOTEST_DSN'), 'odbc', strlen('odbc'))) die('skip inconsistent error message with odbc');
10+
require_once $dir . 'pdo_test.inc';
11+
PDOTest::skip();
12+
?>
13+
--FILE--
14+
<?php
15+
if (getenv('REDIR_TEST_DIR') === false) putenv('REDIR_TEST_DIR='.dirname(__FILE__) . '/../../pdo/tests/');
16+
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
17+
$db = PDOTest::factory();
18+
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
19+
20+
$db->exec("CREATE TABLE test (a int)");
21+
22+
$sql = "SELECT * FROM test WHERE a ?? 1";
23+
24+
try {
25+
$db->exec($sql);
26+
} catch (PDOException $e) {
27+
var_dump(strpos($e->getMessage(), "?") !== false);
28+
}
29+
30+
try {
31+
$stmt = $db->prepare($sql);
32+
$stmt->execute();
33+
} catch (PDOException $e) {
34+
var_dump(strpos($e->getMessage(), "?") !== false);
35+
}
36+
37+
if ($db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql') {
38+
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, 1);
39+
}
40+
41+
try {
42+
$stmt = $db->prepare($sql);
43+
$stmt->execute();
44+
} catch (PDOException $e) {
45+
var_dump(strpos($e->getMessage(), "?") !== false);
46+
}
47+
48+
?>
49+
===DONE===
50+
--EXPECT--
51+
bool(true)
52+
bool(true)
53+
bool(true)
54+
===DONE===

ext/pdo_pgsql/pgsql_driver.c

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -256,36 +256,36 @@ static int pgsql_handle_preparer(pdo_dbh_t *dbh, const char *sql, size_t sql_len
256256
execute_only = H->disable_prepares;
257257
}
258258

259-
if (!emulate && PQprotocolVersion(H->server) > 2) {
259+
if (!emulate && PQprotocolVersion(H->server) <= 2) {
260+
emulate = 1;
261+
}
262+
263+
if (emulate) {
264+
stmt->supports_placeholders = PDO_PLACEHOLDER_NONE;
265+
} else {
260266
stmt->supports_placeholders = PDO_PLACEHOLDER_NAMED;
261267
stmt->named_rewrite_template = "$%d";
262-
ret = pdo_parse_params(stmt, (char*)sql, sql_len, &nsql, &nsql_len);
263-
264-
if (ret == 1) {
265-
/* query was re-written */
266-
sql = nsql;
267-
} else if (ret == -1) {
268-
/* couldn't grok it */
269-
strcpy(dbh->error_code, stmt->error_code);
270-
return 0;
271-
}
268+
}
272269

273-
if (!execute_only) {
274-
/* prepared query: set the query name and defer the
275-
actual prepare until the first execute call */
276-
spprintf(&S->stmt_name, 0, "pdo_stmt_%08x", ++H->stmt_counter);
277-
}
270+
ret = pdo_parse_params(stmt, (char*)sql, sql_len, &nsql, &nsql_len);
278271

279-
if (nsql) {
280-
S->query = nsql;
281-
} else {
282-
S->query = estrdup(sql);
283-
}
272+
if (ret == -1) {
273+
/* couldn't grok it */
274+
strcpy(dbh->error_code, stmt->error_code);
275+
return 0;
276+
} else if (ret == 1) {
277+
/* query was re-written */
278+
S->query = nsql;
279+
} else {
280+
S->query = estrdup(sql);
281+
}
284282

285-
return 1;
283+
if (!emulate && !execute_only) {
284+
/* prepared query: set the query name and defer the
285+
actual prepare until the first execute call */
286+
spprintf(&S->stmt_name, 0, "pdo_stmt_%08x", ++H->stmt_counter);
286287
}
287288

288-
stmt->supports_placeholders = PDO_PLACEHOLDER_NONE;
289289
return 1;
290290
}
291291

ext/pdo_pgsql/tests/bug71885.phpt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
--TEST--
2+
Request #71855 (PDO placeholder escaping)
3+
--SKIPIF--
4+
<?php
5+
if (!extension_loaded('pdo') || !extension_loaded('pdo_pgsql')) die('skip not loaded');
6+
require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
7+
require_once dirname(__FILE__) . '/config.inc';
8+
PDOTest::skip();
9+
?>
10+
--FILE--
11+
<?php
12+
require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
13+
require_once dirname(__FILE__) . '/config.inc';
14+
$db = PDOTest::test_factory(dirname(__FILE__) . '/common.phpt');
15+
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
16+
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_NUM);
17+
18+
foreach ([false, true] as $emulate) {
19+
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, $emulate);
20+
21+
try {
22+
$stmt = $db->prepare('select ?- lseg \'((-1,0),(1,0))\'');
23+
$stmt->execute();
24+
} catch (PDOException $e) {
25+
var_dump('ERR');
26+
}
27+
28+
$stmt = $db->prepare('select ??- lseg \'((-1,0),(1,0))\'');
29+
$stmt->execute();
30+
31+
var_dump($stmt->fetch());
32+
}
33+
34+
?>
35+
==OK==
36+
--EXPECT--
37+
string(3) "ERR"
38+
array(1) {
39+
[0]=>
40+
bool(true)
41+
}
42+
array(1) {
43+
[0]=>
44+
bool(true)
45+
}
46+
==OK==

ext/pdo_pgsql/tests/bug71885_2.phpt

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
--TEST--
2+
Request #71855 (PDO placeholder escaping, part 2)
3+
--SKIPIF--
4+
<?php
5+
if (!extension_loaded('pdo') || !extension_loaded('pdo_pgsql')) die('skip not loaded');
6+
require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
7+
require_once dirname(__FILE__) . '/config.inc';
8+
PDOTest::skip();
9+
10+
$db = PDOTest::factory();
11+
if (version_compare($db->getAttribute(PDO::ATTR_SERVER_VERSION), '9.4.0') < 0) {
12+
die("skip Requires 9.4+");
13+
}
14+
15+
?>
16+
--FILE--
17+
<?php
18+
require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
19+
require_once dirname(__FILE__) . '/config.inc';
20+
$db = PDOTest::test_factory(dirname(__FILE__) . '/common.phpt');
21+
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
22+
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_NUM);
23+
24+
$jsonb = $db->quote(json_encode(['a' => 1]));
25+
26+
foreach ([false, true] as $emulate) {
27+
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, $emulate);
28+
29+
$stmt = $db->prepare("SELECT {$jsonb}::jsonb ?? ?");
30+
$stmt->execute(['b']);
31+
var_dump($stmt->fetch());
32+
33+
$stmt = $db->prepare("SELECT {$jsonb}::jsonb ???");
34+
$stmt->execute(['a']);
35+
var_dump($stmt->fetch());
36+
}
37+
38+
?>
39+
==OK==
40+
--EXPECT--
41+
array(1) {
42+
[0]=>
43+
bool(false)
44+
}
45+
array(1) {
46+
[0]=>
47+
bool(true)
48+
}
49+
array(1) {
50+
[0]=>
51+
bool(false)
52+
}
53+
array(1) {
54+
[0]=>
55+
bool(true)
56+
}
57+
==OK==

0 commit comments

Comments
 (0)