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