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