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					} else {
246						pdo_raise_impl_error(stmt->dbh, stmt, "HY105", "Expected a stream resource");
247						ret = -1;
248						goto clean_up;
249					}
250				} else {
251					enum pdo_param_type param_type = param->param_type;
252					zend_string *buf = NULL;
253
254					/* assume all types are nullable */
255					if (Z_TYPE_P(parameter) == IS_NULL) {
256						param_type = PDO_PARAM_NULL;
257					}
258
259					switch (param_type) {
260						case PDO_PARAM_BOOL:
261							plc->quoted = zend_is_true(parameter) ? ZSTR_CHAR('1') : ZSTR_CHAR('0');
262							break;
263
264						case PDO_PARAM_INT:
265							plc->quoted = zend_long_to_str(zval_get_long(parameter));
266							break;
267
268						case PDO_PARAM_NULL:
269							plc->quoted = ZSTR_KNOWN(ZEND_STR_NULL);
270							break;
271
272						default: {
273							buf = zval_try_get_string(parameter);
274							/* parameter does not have a string representation, buf == NULL */
275							if (EG(exception)) {
276								/* bork */
277								ret = -1;
278								strncpy(stmt->error_code, stmt->dbh->error_code, 6);
279								goto clean_up;
280							}
281
282							plc->quoted = stmt->dbh->methods->quoter(stmt->dbh, buf, param_type);
283						}
284					}
285
286					if (buf) {
287						zend_string_release_ex(buf, 0);
288					}
289				}
290			} else {
291				zval *parameter;
292				if (Z_ISREF(param->parameter)) {
293					parameter = Z_REFVAL(param->parameter);
294				} else {
295					parameter = &param->parameter;
296				}
297				plc->quoted = zend_string_copy(Z_STR_P(parameter));
298			}
299			newbuffer_len += ZSTR_LEN(plc->quoted);
300		}
301
302rewrite:
303		/* allocate output buffer */
304		*outquery = zend_string_alloc(newbuffer_len, 0);
305		newbuffer = ZSTR_VAL(*outquery);
306
307		/* and build the query */
308		const char *ptr = ZSTR_VAL(inquery);
309		plc = placeholders;
310
311		do {
312			t = plc->pos - ptr;
313			if (t) {
314				memcpy(newbuffer, ptr, t);
315				newbuffer += t;
316			}
317			if (plc->quoted) {
318				memcpy(newbuffer, ZSTR_VAL(plc->quoted), ZSTR_LEN(plc->quoted));
319				newbuffer += ZSTR_LEN(plc->quoted);
320			} else {
321				memcpy(newbuffer, plc->pos, plc->len);
322				newbuffer += plc->len;
323			}
324			ptr = plc->pos + plc->len;
325
326			plc = plc->next;
327		} while (plc);
328
329		t = ZSTR_VAL(inquery) + ZSTR_LEN(inquery) - ptr;
330		if (t) {
331			memcpy(newbuffer, ptr, t);
332			newbuffer += t;
333		}
334		*newbuffer = '\0';
335		ZSTR_LEN(*outquery) = newbuffer - ZSTR_VAL(*outquery);
336
337		ret = 1;
338		goto clean_up;
339
340	} else if (query_type == PDO_PLACEHOLDER_POSITIONAL) {
341		/* rewrite ? to :pdoX */
342		const char *tmpl = stmt->named_rewrite_template ? stmt->named_rewrite_template : ":pdo%d";
343		int bind_no = 1;
344
345		newbuffer_len = ZSTR_LEN(inquery);
346
347		if (stmt->bound_param_map == NULL) {
348			ALLOC_HASHTABLE(stmt->bound_param_map);
349			zend_hash_init(stmt->bound_param_map, 13, NULL, free_param_name, 0);
350		}
351
352		for (plc = placeholders; plc; plc = plc->next) {
353			int skip_map = 0;
354			zend_string *p;
355			zend_string *idxbuf;
356
357			if (plc->bindno == PDO_PARSER_BINDNO_ESCAPED_CHAR) {
358				continue;
359			}
360
361			zend_string *name = zend_string_init(plc->pos, plc->len, 0);
362
363			/* check if bound parameter is already available */
364			if (zend_string_equals_literal(name, "?") || (p = zend_hash_find_ptr(stmt->bound_param_map, name)) == NULL) {
365				idxbuf = zend_strpprintf(0, tmpl, bind_no++);
366			} else {
367				idxbuf = zend_string_copy(p);
368				skip_map = 1;
369			}
370
371			plc->quoted = idxbuf;
372			newbuffer_len += ZSTR_LEN(plc->quoted);
373
374			if (!skip_map && stmt->named_rewrite_template) {
375				/* create a mapping */
376				zend_hash_update_ptr(stmt->bound_param_map, name, zend_string_copy(plc->quoted));
377			}
378
379			/* map number to name */
380			zend_hash_index_update_ptr(stmt->bound_param_map, plc->bindno, zend_string_copy(plc->quoted));
381
382			zend_string_release(name);
383		}
384
385		goto rewrite;
386
387	} else {
388		/* rewrite :name to ? */
389
390		newbuffer_len = ZSTR_LEN(inquery);
391
392		if (stmt->bound_param_map == NULL) {
393			ALLOC_HASHTABLE(stmt->bound_param_map);
394			zend_hash_init(stmt->bound_param_map, 13, NULL, free_param_name, 0);
395		}
396
397		for (plc = placeholders; plc; plc = plc->next) {
398			zend_string *name = zend_string_init(plc->pos, plc->len, 0);
399			zend_hash_index_update_ptr(stmt->bound_param_map, plc->bindno, name);
400			plc->quoted = ZSTR_CHAR('?');
401			newbuffer_len -= plc->len - 1;
402		}
403
404		goto rewrite;
405	}
406
407clean_up:
408
409	while (placeholders) {
410		plc = placeholders;
411		placeholders = plc->next;
412		if (plc->quoted) {
413			zend_string_release_ex(plc->quoted, 0);
414		}
415		efree(plc);
416	}
417
418	return ret;
419}
420