Skip to content

Commit f558b3e

Browse files
committed
Fix FR #71885 (Allow escaping question mark placeholders)
1 parent 7faaeec commit f558b3e

File tree

5 files changed

+233
-45
lines changed

5 files changed

+233
-45
lines changed

ext/pdo/pdo_sql_parser.re

Lines changed: 54 additions & 22 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,11 @@ 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 */
143-
ret = 0;
144-
goto clean_up;
162+
newbuffer_len = inquery_len;
163+
goto rewrite;
145164
}
146165

147-
if (stmt->named_rewrite_template) {
166+
if (query_type == PDO_PLACEHOLDER_NAMED && stmt->named_rewrite_template) {
148167
/* magic/hack.
149168
* We we pretend that the query was positional even if
150169
* it was named so that we fall into the
@@ -155,14 +174,7 @@ PDO_API int pdo_parse_params(pdo_stmt_t *stmt, char *inquery, size_t inquery_len
155174

156175
params = stmt->bound_params;
157176

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) {
177+
if (bindno && stmt->supports_placeholders == PDO_PLACEHOLDER_NONE && params && bindno != zend_hash_num_elements(params)) {
166178
/* extra bit of validation for instances when same params are bound more than once */
167179
if (query_type != PDO_PLACEHOLDER_POSITIONAL && bindno > zend_hash_num_elements(params)) {
168180
int ok = 1;
@@ -188,7 +200,16 @@ safe:
188200
newbuffer_len = inquery_len;
189201

190202
/* let's quote all the values */
191-
for (plc = placeholders; plc; plc = plc->next) {
203+
for (plc = placeholders; plc && params; plc = plc->next) {
204+
if (plc->bindno == PDO_PARSER_BINDNO_ESCAPED_CHAR) {
205+
/* escaped character */
206+
continue;
207+
}
208+
209+
if (query_type == PDO_PLACEHOLDER_NONE) {
210+
continue;
211+
}
212+
192213
if (query_type == PDO_PLACEHOLDER_POSITIONAL) {
193214
param = zend_hash_index_find_ptr(params, plc->bindno);
194215
} else {
@@ -302,7 +323,7 @@ safe:
302323

303324
rewrite:
304325
/* allocate output buffer */
305-
newbuffer = emalloc(newbuffer_len + 1);
326+
newbuffer = emalloc(newbuffer_len - escapes + 1);
306327
*outquery = newbuffer;
307328

308329
/* and build the query */
@@ -315,8 +336,13 @@ rewrite:
315336
memcpy(newbuffer, ptr, t);
316337
newbuffer += t;
317338
}
318-
memcpy(newbuffer, plc->quoted, plc->qlen);
319-
newbuffer += plc->qlen;
339+
if (plc->quoted) {
340+
memcpy(newbuffer, plc->quoted, plc->qlen);
341+
newbuffer += plc->qlen;
342+
} else {
343+
memcpy(newbuffer, plc->pos, plc->len);
344+
newbuffer += plc->len;
345+
}
320346
ptr = plc->pos + plc->len;
321347

322348
plc = plc->next;
@@ -349,6 +375,11 @@ rewrite:
349375
for (plc = placeholders; plc; plc = plc->next) {
350376
int skip_map = 0;
351377
char *p;
378+
379+
if (plc->bindno == PDO_PARSER_BINDNO_ESCAPED_CHAR) {
380+
continue;
381+
}
382+
352383
name = estrndup(plc->pos, plc->len);
353384

354385
/* check if bound parameter is already available */
@@ -394,6 +425,7 @@ rewrite:
394425
efree(name);
395426
plc->quoted = "?";
396427
plc->qlen = 1;
428+
newbuffer_len -= plc->len - 1;
397429
}
398430

399431
goto rewrite;

ext/pdo/tests/bug_71885.phpt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
require_once $dir . 'pdo_test.inc';
10+
PDOTest::skip();
11+
?>
12+
--FILE--
13+
<?php
14+
if (getenv('REDIR_TEST_DIR') === false) putenv('REDIR_TEST_DIR='.dirname(__FILE__) . '/../../pdo/tests/');
15+
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
16+
$db = PDOTest::factory();
17+
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
18+
19+
$db->exec("CREATE TABLE test (a int)");
20+
21+
$sql = "SELECT * FROM test WHERE a ?? 1";
22+
23+
try {
24+
$db->exec($sql);
25+
} catch (PDOException $e) {
26+
var_dump(strpos($e->getMessage(), "?") !== false);
27+
}
28+
29+
try {
30+
$stmt = $db->prepare($sql);
31+
$stmt->execute();
32+
} catch (PDOException $e) {
33+
var_dump(strpos($e->getMessage(), "?") !== false);
34+
}
35+
36+
if ($db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql') {
37+
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, 1);
38+
}
39+
40+
try {
41+
$stmt = $db->prepare($sql);
42+
$stmt->execute();
43+
} catch (PDOException $e) {
44+
var_dump(strpos($e->getMessage(), "?") !== false);
45+
}
46+
47+
?>
48+
===DONE===
49+
--EXPECT--
50+
bool(true)
51+
bool(true)
52+
bool(true)
53+
===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==

0 commit comments

Comments
 (0)