xref: /php-src/ext/pdo/pdo_sql_parser.re (revision 53d7c174)
1/*
2  +----------------------------------------------------------------------+
3  | Copyright (c) The PHP Group                                          |
4  +----------------------------------------------------------------------+
5  | This source file is subject to version 3.01 of the PHP license,      |
6  | that is bundled with this package in the file LICENSE, and is        |
7  | available through the world-wide-web at the following url:           |
8  | https://www.php.net/license/3_01.txt                                 |
9  | If you did not receive a copy of the PHP license and are unable to   |
10  | obtain it through the world-wide-web, please send a note to          |
11  | license@php.net so we can mail you a copy immediately.               |
12  +----------------------------------------------------------------------+
13  | Author: George Schlossnagle <george@omniti.com>                      |
14  +----------------------------------------------------------------------+
15*/
16
17#include "php.h"
18#include "php_pdo_driver.h"
19#include "php_pdo_int.h"
20#include "pdo_sql_parser.h"
21
22static int default_scanner(pdo_scanner_t *s)
23{
24	const char *cursor = s->cur;
25
26	s->tok = cursor;
27	/*!re2c
28	BINDCHR		= [:][a-zA-Z0-9_]+;
29	QUESTION	= [?];
30	COMMENTS	= ("/*"([^*]+|[*]+[^/*])*[*]*"*/"|"--".*);
31	SPECIALS	= [:?"'/-];
32	MULTICHAR	= ([:]{2,}|[?]{2,});
33	ANYNOEOF	= [\001-\377];
34	*/
35
36	/*!re2c
37		(["]((["]["])|ANYNOEOF\["])*["])		{ RET(PDO_PARSER_TEXT); }
38		(['](([']['])|ANYNOEOF\['])*['])		{ RET(PDO_PARSER_TEXT); }
39		MULTICHAR								{ RET(PDO_PARSER_TEXT); }
40		BINDCHR									{ RET(PDO_PARSER_BIND); }
41		QUESTION								{ RET(PDO_PARSER_BIND_POS); }
42		SPECIALS								{ SKIP_ONE(PDO_PARSER_TEXT); }
43		COMMENTS								{ RET(PDO_PARSER_TEXT); }
44		(ANYNOEOF\SPECIALS)+ 					{ RET(PDO_PARSER_TEXT); }
45	*/
46}
47
48struct placeholder {
49	const char *pos;
50	size_t len;
51	zend_string *quoted;	/* quoted value */
52	int bindno;
53	struct placeholder *next;
54};
55
56struct custom_quote {
57	const char *pos;
58	size_t len;
59};
60
61static void free_param_name(zval *el) {
62	zend_string_release(Z_PTR_P(el));
63}
64
65PDO_API int pdo_parse_params(pdo_stmt_t *stmt, zend_string *inquery, zend_string **outquery)
66{
67	pdo_scanner_t s;
68	char *newbuffer;
69	ptrdiff_t t;
70	uint32_t bindno = 0;
71	int ret = 0, escapes = 0;
72	size_t newbuffer_len;
73	HashTable *params;
74	struct pdo_bound_param_data *param;
75	int query_type = PDO_PLACEHOLDER_NONE;
76	struct placeholder *placeholders = NULL, *placetail = NULL, *plc = NULL;
77	int (*scan)(pdo_scanner_t *s);
78	struct custom_quote custom_quote = {NULL, 0};
79
80	scan = stmt->dbh->methods->scanner ? stmt->dbh->methods->scanner : default_scanner;
81
82	s.cur = ZSTR_VAL(inquery);
83	s.end = s.cur + ZSTR_LEN(inquery) + 1;
84
85	/* phase 1: look for args */
86	while((t = scan(&s)) != PDO_PARSER_EOI) {
87		if (custom_quote.pos) {
88			/* Inside a custom quote */
89			if (t == PDO_PARSER_CUSTOM_QUOTE && custom_quote.len == s.cur - s.tok && !strncmp(s.tok, custom_quote.pos, custom_quote.len)) {
90				/* Matching closing quote found, end custom quoting */
91				custom_quote.pos = NULL;
92				custom_quote.len = 0;
93			} else if (t == PDO_PARSER_ESCAPED_QUESTION) {
94				/* An escaped question mark has been used inside a dollar quoted string, most likely as a workaround
95				 * as a single "?" would have been parsed as placeholder, due to the lack of support for dollar quoted
96				 * strings. For now, we emit a deprecation notice, but still process it */
97				php_error_docref(NULL, E_DEPRECATED, "Escaping question marks inside dollar quoted strings is not required anymore and is deprecated");
98
99				goto placeholder;
100			}
101
102			continue;
103		}
104
105		if (t == PDO_PARSER_CUSTOM_QUOTE) {
106			/* Start of a custom quote, keep a reference to search for the matching closing quote */
107			custom_quote.pos = s.tok;
108			custom_quote.len = s.cur - s.tok;
109
110			continue;
111		}
112
113		if (t == PDO_PARSER_BIND || t == PDO_PARSER_BIND_POS || t == PDO_PARSER_ESCAPED_QUESTION) {
114			if (t == PDO_PARSER_ESCAPED_QUESTION && stmt->supports_placeholders == PDO_PLACEHOLDER_POSITIONAL) {
115				/* escaped question marks unsupported, treat as text */
116				continue;
117			}
118
119			if (t == PDO_PARSER_BIND) {
120				ptrdiff_t len = s.cur - s.tok;
121				if ((ZSTR_VAL(inquery) < (s.cur - len)) && isalnum(*(s.cur - len - 1))) {
122					continue;
123				}
124				query_type |= PDO_PLACEHOLDER_NAMED;
125			} else if (t == PDO_PARSER_BIND_POS) {
126				query_type |= PDO_PLACEHOLDER_POSITIONAL;
127			}
128
129placeholder:
130			plc = emalloc(sizeof(*plc));
131			memset(plc, 0, sizeof(*plc));
132			plc->next = NULL;
133			plc->pos = s.tok;
134			plc->len = s.cur - s.tok;
135
136			if (t == PDO_PARSER_ESCAPED_QUESTION) {
137				plc->bindno = PDO_PARSER_BINDNO_ESCAPED_CHAR;
138				plc->quoted = ZSTR_CHAR('?');
139				escapes++;
140			} else {
141				plc->bindno = bindno++;
142			}
143
144			if (placetail) {
145				placetail->next = plc;
146			} else {
147				placeholders = plc;
148			}
149			placetail = plc;
150		}
151	}
152
153	/* did the query make sense to me? */
154	if (query_type == (PDO_PLACEHOLDER_NAMED|PDO_PLACEHOLDER_POSITIONAL)) {
155		/* they mixed both types; punt */
156		pdo_raise_impl_error(stmt->dbh, stmt, "HY093", "mixed named and positional parameters");
157		ret = -1;
158		goto clean_up;
159	}
160
161	params = stmt->bound_params;
162	if (stmt->supports_placeholders == PDO_PLACEHOLDER_NONE && params && bindno != zend_hash_num_elements(params)) {
163		/* extra bit of validation for instances when same params are bound more than once */
164		if (query_type != PDO_PLACEHOLDER_POSITIONAL && bindno > zend_hash_num_elements(params)) {
165			int ok = 1;
166			for (plc = placeholders; plc; plc = plc->next) {
167				if ((param = zend_hash_str_find_ptr(params, plc->pos, plc->len)) == NULL) {
168					ok = 0;
169					break;
170				}
171			}
172			if (ok) {
173				goto safe;
174			}
175		}
176		pdo_raise_impl_error(stmt->dbh, stmt, "HY093", "number of bound variables does not match number of tokens");
177		ret = -1;
178		goto clean_up;
179	}
180
181	if (!placeholders) {
182		/* nothing to do; good! */
183		return 0;
184	}
185
186	if (stmt->supports_placeholders == query_type && !stmt->named_rewrite_template) {
187		/* query matches native syntax */
188		if (escapes) {
189			newbuffer_len = ZSTR_LEN(inquery);
190			goto rewrite;
191		}
192
193		ret = 0;
194		goto clean_up;
195	}
196
197	if (query_type == PDO_PLACEHOLDER_NAMED && stmt->named_rewrite_template) {
198		/* magic/hack.
199		 * We we pretend that the query was positional even if
200		 * it was named so that we fall into the
201		 * named rewrite case below.  Not too pretty,
202		 * but it works. */
203		query_type = PDO_PLACEHOLDER_POSITIONAL;
204	}
205
206safe:
207	/* what are we going to do ? */
208	if (stmt->supports_placeholders == PDO_PLACEHOLDER_NONE) {
209		/* query generation */
210
211		newbuffer_len = ZSTR_LEN(inquery);
212
213		/* let's quote all the values */
214		for (plc = placeholders; plc && params; plc = plc->next) {
215			if (plc->bindno == PDO_PARSER_BINDNO_ESCAPED_CHAR) {
216				/* escaped character */
217				continue;
218			}
219
220			if (query_type == PDO_PLACEHOLDER_NONE) {
221				continue;
222			}
223
224			if (query_type == PDO_PLACEHOLDER_POSITIONAL) {
225				param = zend_hash_index_find_ptr(params, plc->bindno);
226			} else {
227				param = zend_hash_str_find_ptr(params, plc->pos, plc->len);
228			}
229			if (param == NULL) {
230				/* parameter was not defined */
231				ret = -1;
232				pdo_raise_impl_error(stmt->dbh, stmt, "HY093", "parameter was not defined");
233				goto clean_up;
234			}
235			if (stmt->dbh->methods->quoter) {
236				zval *parameter;
237				if (Z_ISREF(param->parameter)) {
238					parameter = Z_REFVAL(param->parameter);
239				} else {
240					parameter = &param->parameter;
241				}
242				if (param->param_type == PDO_PARAM_LOB && Z_TYPE_P(parameter) == IS_RESOURCE) {
243					php_stream *stm;
244
245					php_stream_from_zval_no_verify(stm, parameter);
246					if (stm) {
247						zend_string *buf;
248
249						buf = php_stream_copy_to_mem(stm, PHP_STREAM_COPY_ALL, 0);
250						if (!buf) {
251							buf = ZSTR_EMPTY_ALLOC();
252						}
253
254						plc->quoted = stmt->dbh->methods->quoter(stmt->dbh, buf, param->param_type);
255
256						if (buf) {
257							zend_string_release_ex(buf, 0);
258						}
259						if (plc->quoted == NULL) {
260							/* bork */
261							ret = -1;
262							strncpy(stmt->error_code, stmt->dbh->error_code, 6);
263							goto clean_up;
264						}
265
266					} else {
267						pdo_raise_impl_error(stmt->dbh, stmt, "HY105", "Expected a stream resource");
268						ret = -1;
269						goto clean_up;
270					}
271				} else {
272					enum pdo_param_type param_type = param->param_type;
273					zend_string *buf = NULL;
274
275					/* assume all types are nullable */
276					if (Z_TYPE_P(parameter) == IS_NULL) {
277						param_type = PDO_PARAM_NULL;
278					}
279
280					switch (param_type) {
281						case PDO_PARAM_BOOL:
282							plc->quoted = zend_is_true(parameter) ? ZSTR_CHAR('1') : ZSTR_CHAR('0');
283							break;
284
285						case PDO_PARAM_INT:
286							plc->quoted = zend_long_to_str(zval_get_long(parameter));
287							break;
288
289						case PDO_PARAM_NULL:
290							plc->quoted = ZSTR_KNOWN(ZEND_STR_NULL);
291							break;
292
293						default: {
294							buf = zval_try_get_string(parameter);
295							/* parameter does not have a string representation, buf == NULL */
296							if (EG(exception)) {
297								/* bork */
298								ret = -1;
299								strncpy(stmt->error_code, stmt->dbh->error_code, 6);
300								goto clean_up;
301							}
302
303							plc->quoted = stmt->dbh->methods->quoter(stmt->dbh, buf, param_type);
304						}
305					}
306
307					if (buf) {
308						zend_string_release_ex(buf, 0);
309					}
310				}
311			} else {
312				zval *parameter;
313				if (Z_ISREF(param->parameter)) {
314					parameter = Z_REFVAL(param->parameter);
315				} else {
316					parameter = &param->parameter;
317				}
318				plc->quoted = zend_string_copy(Z_STR_P(parameter));
319			}
320			newbuffer_len += ZSTR_LEN(plc->quoted);
321		}
322
323rewrite:
324		/* allocate output buffer */
325		*outquery = zend_string_alloc(newbuffer_len, 0);
326		newbuffer = ZSTR_VAL(*outquery);
327
328		/* and build the query */
329		const char *ptr = ZSTR_VAL(inquery);
330		plc = placeholders;
331
332		do {
333			t = plc->pos - ptr;
334			if (t) {
335				memcpy(newbuffer, ptr, t);
336				newbuffer += t;
337			}
338			if (plc->quoted) {
339				memcpy(newbuffer, ZSTR_VAL(plc->quoted), ZSTR_LEN(plc->quoted));
340				newbuffer += ZSTR_LEN(plc->quoted);
341			} else {
342				memcpy(newbuffer, plc->pos, plc->len);
343				newbuffer += plc->len;
344			}
345			ptr = plc->pos + plc->len;
346
347			plc = plc->next;
348		} while (plc);
349
350		t = ZSTR_VAL(inquery) + ZSTR_LEN(inquery) - ptr;
351		if (t) {
352			memcpy(newbuffer, ptr, t);
353			newbuffer += t;
354		}
355		*newbuffer = '\0';
356		ZSTR_LEN(*outquery) = newbuffer - ZSTR_VAL(*outquery);
357
358		ret = 1;
359		goto clean_up;
360
361	} else if (query_type == PDO_PLACEHOLDER_POSITIONAL) {
362		/* rewrite ? to :pdoX */
363		const char *tmpl = stmt->named_rewrite_template ? stmt->named_rewrite_template : ":pdo%d";
364		int bind_no = 1;
365
366		newbuffer_len = ZSTR_LEN(inquery);
367
368		if (stmt->bound_param_map == NULL) {
369			ALLOC_HASHTABLE(stmt->bound_param_map);
370			zend_hash_init(stmt->bound_param_map, 13, NULL, free_param_name, 0);
371		}
372
373		for (plc = placeholders; plc; plc = plc->next) {
374			int skip_map = 0;
375			zend_string *p;
376			zend_string *idxbuf;
377
378			if (plc->bindno == PDO_PARSER_BINDNO_ESCAPED_CHAR) {
379				continue;
380			}
381
382			zend_string *name = zend_string_init(plc->pos, plc->len, 0);
383
384			/* check if bound parameter is already available */
385			if (zend_string_equals_literal(name, "?") || (p = zend_hash_find_ptr(stmt->bound_param_map, name)) == NULL) {
386				idxbuf = zend_strpprintf(0, tmpl, bind_no++);
387			} else {
388				idxbuf = zend_string_copy(p);
389				skip_map = 1;
390			}
391
392			plc->quoted = idxbuf;
393			newbuffer_len += ZSTR_LEN(plc->quoted);
394
395			if (!skip_map && stmt->named_rewrite_template) {
396				/* create a mapping */
397				zend_hash_update_ptr(stmt->bound_param_map, name, zend_string_copy(plc->quoted));
398			}
399
400			/* map number to name */
401			zend_hash_index_update_ptr(stmt->bound_param_map, plc->bindno, zend_string_copy(plc->quoted));
402
403			zend_string_release(name);
404		}
405
406		goto rewrite;
407
408	} else {
409		/* rewrite :name to ? */
410
411		newbuffer_len = ZSTR_LEN(inquery);
412
413		if (stmt->bound_param_map == NULL) {
414			ALLOC_HASHTABLE(stmt->bound_param_map);
415			zend_hash_init(stmt->bound_param_map, 13, NULL, free_param_name, 0);
416		}
417
418		for (plc = placeholders; plc; plc = plc->next) {
419			zend_string *name = zend_string_init(plc->pos, plc->len, 0);
420			zend_hash_index_update_ptr(stmt->bound_param_map, plc->bindno, name);
421			plc->quoted = ZSTR_CHAR('?');
422			newbuffer_len -= plc->len - 1;
423		}
424
425		goto rewrite;
426	}
427
428clean_up:
429
430	while (placeholders) {
431		plc = placeholders;
432		placeholders = plc->next;
433		if (plc->quoted) {
434			zend_string_release_ex(plc->quoted, 0);
435		}
436		efree(plc);
437	}
438
439	return ret;
440}
441