xref: /php-src/ext/standard/browscap.c (revision 04c417a3)
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: Zeev Suraski <zeev@php.net>                                  |
14    +----------------------------------------------------------------------+
15  */
16 
17 #include "php.h"
18 #include "php_browscap.h"
19 #include "php_ini.h"
20 #include "php_string.h"
21 
22 #include "zend_ini_scanner.h"
23 #include "zend_globals.h"
24 
25 #define BROWSCAP_NUM_CONTAINS 5
26 
27 typedef struct {
28 	zend_string *key;
29 	zend_string *value;
30 } browscap_kv;
31 
32 typedef struct {
33 	zend_string *pattern;
34 	zend_string *parent;
35 	uint32_t kv_start;
36 	uint32_t kv_end;
37 	/* We ensure that the length fits in 16 bits, so this is fine */
38 	uint16_t contains_start[BROWSCAP_NUM_CONTAINS];
39 	uint8_t contains_len[BROWSCAP_NUM_CONTAINS];
40 	uint8_t prefix_len;
41 } browscap_entry;
42 
43 typedef struct {
44 	HashTable *htab;
45 	browscap_kv *kv;
46 	uint32_t kv_used;
47 	uint32_t kv_size;
48 	char filename[MAXPATHLEN];
49 } browser_data;
50 
51 /* browser data defined in startup phase, eagerly loaded in MINIT */
52 static browser_data global_bdata = {0};
53 
54 /* browser data defined in activation phase, lazily loaded in get_browser.
55  * Per request and per thread, if applicable */
ZEND_BEGIN_MODULE_GLOBALS(browscap)56 ZEND_BEGIN_MODULE_GLOBALS(browscap)
57 	browser_data activation_bdata;
58 ZEND_END_MODULE_GLOBALS(browscap)
59 
60 ZEND_DECLARE_MODULE_GLOBALS(browscap)
61 #define BROWSCAP_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(browscap, v)
62 
63 #define DEFAULT_SECTION_NAME "Default Browser Capability Settings"
64 
65 /* OBJECTS_FIXME: This whole extension needs going through. The use of objects looks pretty broken here */
66 
67 static void browscap_entry_dtor(zval *zvalue)
68 {
69 	browscap_entry *entry = Z_PTR_P(zvalue);
70 	zend_string_release_ex(entry->pattern, 0);
71 	if (entry->parent) {
72 		zend_string_release_ex(entry->parent, 0);
73 	}
74 	efree(entry);
75 }
76 
browscap_entry_dtor_persistent(zval * zvalue)77 static void browscap_entry_dtor_persistent(zval *zvalue)
78 {
79 	browscap_entry *entry = Z_PTR_P(zvalue);
80 	zend_string_release_ex(entry->pattern, 1);
81 	if (entry->parent) {
82 		zend_string_release_ex(entry->parent, 1);
83 	}
84 	pefree(entry, 1);
85 }
86 
is_placeholder(char c)87 static inline bool is_placeholder(char c) {
88 	return c == '?' || c == '*';
89 }
90 
91 /* Length of prefix not containing any wildcards */
browscap_compute_prefix_len(zend_string * pattern)92 static uint8_t browscap_compute_prefix_len(zend_string *pattern) {
93 	size_t i;
94 	for (i = 0; i < ZSTR_LEN(pattern); i++) {
95 		if (is_placeholder(ZSTR_VAL(pattern)[i])) {
96 			break;
97 		}
98 	}
99 	return (uint8_t)MIN(i, UINT8_MAX);
100 }
101 
browscap_compute_contains(zend_string * pattern,size_t start_pos,uint16_t * contains_start,uint8_t * contains_len)102 static size_t browscap_compute_contains(
103 		zend_string *pattern, size_t start_pos,
104 		uint16_t *contains_start, uint8_t *contains_len) {
105 	size_t i = start_pos;
106 	/* Find first non-placeholder character after prefix */
107 	for (; i < ZSTR_LEN(pattern); i++) {
108 		if (!is_placeholder(ZSTR_VAL(pattern)[i])) {
109 			/* Skip the case of a single non-placeholder character.
110 			 * Let's try to find something longer instead. */
111 			if (i + 1 < ZSTR_LEN(pattern) &&
112 					!is_placeholder(ZSTR_VAL(pattern)[i + 1])) {
113 				break;
114 			}
115 		}
116 	}
117 	*contains_start = (uint16_t)i;
118 
119 	/* Find first placeholder character after that */
120 	for (; i < ZSTR_LEN(pattern); i++) {
121 		if (is_placeholder(ZSTR_VAL(pattern)[i])) {
122 			break;
123 		}
124 	}
125 	*contains_len = (uint8_t)MIN(i - *contains_start, UINT8_MAX);
126 	return i;
127 }
128 
129 /* Length of regex, including escapes, anchors, etc. */
browscap_compute_regex_len(zend_string * pattern)130 static size_t browscap_compute_regex_len(zend_string *pattern) {
131 	size_t i, len = ZSTR_LEN(pattern);
132 	for (i = 0; i < ZSTR_LEN(pattern); i++) {
133 		switch (ZSTR_VAL(pattern)[i]) {
134 			case '*':
135 			case '.':
136 			case '\\':
137 			case '(':
138 			case ')':
139 			case '~':
140 			case '+':
141 				len++;
142 				break;
143 		}
144 	}
145 
146 	return len + sizeof("~^$~")-1;
147 }
148 
browscap_convert_pattern(zend_string * pattern,int persistent)149 static zend_string *browscap_convert_pattern(zend_string *pattern, int persistent) /* {{{ */
150 {
151 	size_t i, j=0;
152 	char *t;
153 	zend_string *res;
154 
155 	res = zend_string_alloc(browscap_compute_regex_len(pattern), persistent);
156 	t = ZSTR_VAL(res);
157 
158 	t[j++] = '~';
159 	t[j++] = '^';
160 
161 	for (i = 0; i < ZSTR_LEN(pattern); i++, j++) {
162 		char c = ZSTR_VAL(pattern)[i];
163 		switch (c) {
164 			case '?':
165 				t[j] = '.';
166 				break;
167 			case '*':
168 				t[j++] = '.';
169 				t[j] = '*';
170 				break;
171 			case '.':
172 				t[j++] = '\\';
173 				t[j] = '.';
174 				break;
175 			case '\\':
176 				t[j++] = '\\';
177 				t[j] = '\\';
178 				break;
179 			case '(':
180 				t[j++] = '\\';
181 				t[j] = '(';
182 				break;
183 			case ')':
184 				t[j++] = '\\';
185 				t[j] = ')';
186 				break;
187 			case '~':
188 				t[j++] = '\\';
189 				t[j] = '~';
190 				break;
191 			case '+':
192 				t[j++] = '\\';
193 				t[j] = '+';
194 				break;
195 			default:
196 				t[j] = zend_tolower_ascii(c);
197 				break;
198 		}
199 	}
200 
201 	t[j++] = '$';
202 	t[j++] = '~';
203 	t[j]=0;
204 
205 	ZSTR_LEN(res) = j;
206 	return res;
207 }
208 /* }}} */
209 
210 typedef struct _browscap_parser_ctx {
211 	browser_data *bdata;
212 	browscap_entry *current_entry;
213 	zend_string *current_section_name;
214 	HashTable str_interned;
215 } browscap_parser_ctx;
216 
browscap_intern_str(browscap_parser_ctx * ctx,zend_string * str,bool persistent)217 static zend_string *browscap_intern_str(
218 		browscap_parser_ctx *ctx, zend_string *str, bool persistent) {
219 	zend_string *interned = zend_hash_find_ptr(&ctx->str_interned, str);
220 	if (interned) {
221 		zend_string_addref(interned);
222 	} else {
223 		interned = zend_string_copy(str);
224 		if (persistent) {
225 			interned = zend_new_interned_string(interned);
226 		}
227 		zend_hash_add_new_ptr(&ctx->str_interned, interned, interned);
228 	}
229 
230 	return interned;
231 }
232 
browscap_intern_str_ci(browscap_parser_ctx * ctx,zend_string * str,bool persistent)233 static zend_string *browscap_intern_str_ci(
234 		browscap_parser_ctx *ctx, zend_string *str, bool persistent) {
235 	zend_string *lcname;
236 	zend_string *interned;
237 	ALLOCA_FLAG(use_heap);
238 
239 	ZSTR_ALLOCA_ALLOC(lcname, ZSTR_LEN(str), use_heap);
240 	zend_str_tolower_copy(ZSTR_VAL(lcname), ZSTR_VAL(str), ZSTR_LEN(str));
241 	interned = zend_hash_find_ptr(&ctx->str_interned, lcname);
242 
243 	if (interned) {
244 		zend_string_addref(interned);
245 	} else {
246 		interned = zend_string_init(ZSTR_VAL(lcname), ZSTR_LEN(lcname), persistent);
247 		if (persistent) {
248 			interned = zend_new_interned_string(interned);
249 		}
250 		zend_hash_add_new_ptr(&ctx->str_interned, interned, interned);
251 	}
252 
253 	ZSTR_ALLOCA_FREE(lcname, use_heap);
254 	return interned;
255 }
256 
browscap_add_kv(browser_data * bdata,zend_string * key,zend_string * value,bool persistent)257 static void browscap_add_kv(
258 		browser_data *bdata, zend_string *key, zend_string *value, bool persistent) {
259 	if (bdata->kv_used == bdata->kv_size) {
260 		bdata->kv_size *= 2;
261 		bdata->kv = safe_perealloc(bdata->kv, sizeof(browscap_kv), bdata->kv_size, 0, persistent);
262 	}
263 
264 	bdata->kv[bdata->kv_used].key = key;
265 	bdata->kv[bdata->kv_used].value = value;
266 	bdata->kv_used++;
267 }
268 
browscap_entry_add_kv_to_existing_array(browser_data * bdata,browscap_entry * entry,HashTable * ht)269 static void browscap_entry_add_kv_to_existing_array(browser_data *bdata, browscap_entry *entry, HashTable *ht) {
270 	for (uint32_t i = entry->kv_start; i < entry->kv_end; i++) {
271 		zval tmp;
272 		ZVAL_STR_COPY(&tmp, bdata->kv[i].value);
273 		zend_hash_add(ht, bdata->kv[i].key, &tmp);
274 	}
275 }
276 
browscap_entry_to_array(browser_data * bdata,browscap_entry * entry)277 static HashTable *browscap_entry_to_array(browser_data *bdata, browscap_entry *entry) {
278 	zval tmp;
279 	HashTable *ht = zend_new_array(2 + (entry->parent ? 1 : 0) + (entry->kv_end - entry->kv_start));
280 
281 	ZVAL_STR(&tmp, browscap_convert_pattern(entry->pattern, 0));
282 	zend_string *key = ZSTR_INIT_LITERAL("browser_name_regex", 0);
283 	ZSTR_H(key) = zend_inline_hash_func("browser_name_regex", sizeof("browser_name_regex")-1);
284 	zend_hash_add_new(ht, key, &tmp);
285 	zend_string_release_ex(key, false);
286 
287 	ZVAL_STR_COPY(&tmp, entry->pattern);
288 	key = ZSTR_INIT_LITERAL("browser_name_pattern", 0);
289 	ZSTR_H(key) = zend_inline_hash_func("browser_name_pattern", sizeof("browser_name_pattern")-1);
290 	zend_hash_add_new(ht, key, &tmp);
291 	zend_string_release_ex(key, false);
292 
293 	if (entry->parent) {
294 		ZVAL_STR_COPY(&tmp, entry->parent);
295 		key = ZSTR_INIT_LITERAL("parent", 0);
296 		ZSTR_H(key) = zend_inline_hash_func("parent", sizeof("parent")-1);
297 		zend_hash_add_new(ht, key, &tmp);
298 		zend_string_release_ex(key, false);
299 	}
300 
301 	browscap_entry_add_kv_to_existing_array(bdata, entry, ht);
302 
303 	return ht;
304 }
305 
php_browscap_parser_cb(zval * arg1,zval * arg2,zval * arg3,int callback_type,void * arg)306 static void php_browscap_parser_cb(zval *arg1, zval *arg2, zval *arg3, int callback_type, void *arg) /* {{{ */
307 {
308 	browscap_parser_ctx *ctx = arg;
309 	browser_data *bdata = ctx->bdata;
310 	int persistent = GC_FLAGS(bdata->htab) & IS_ARRAY_PERSISTENT;
311 
312 	if (!arg1) {
313 		return;
314 	}
315 
316 	switch (callback_type) {
317 		case ZEND_INI_PARSER_ENTRY:
318 			if (ctx->current_entry != NULL && arg2) {
319 				zend_string *new_key, *new_value;
320 
321 				/* Set proper value for true/false settings */
322 				if (zend_string_equals_literal_ci(Z_STR_P(arg2), "on")
323 					|| zend_string_equals_literal_ci(Z_STR_P(arg2), "yes")
324 					|| zend_string_equals_literal_ci(Z_STR_P(arg2), "true")
325 				) {
326 					new_value = ZSTR_CHAR('1');
327 				} else if (zend_string_equals_literal_ci(Z_STR_P(arg2), "no")
328 					|| zend_string_equals_literal_ci(Z_STR_P(arg2), "off")
329 					|| zend_string_equals_literal_ci(Z_STR_P(arg2), "none")
330 					|| zend_string_equals_literal_ci(Z_STR_P(arg2), "false")
331 				) {
332 					new_value = ZSTR_EMPTY_ALLOC();
333 				} else { /* Other than true/false setting */
334 					new_value = browscap_intern_str(ctx, Z_STR_P(arg2), persistent);
335 				}
336 
337 				if (zend_string_equals_literal_ci(Z_STR_P(arg1), "parent")) {
338 					/* parent entry cannot be same as current section -> causes infinite loop! */
339 					if (ctx->current_section_name != NULL &&
340 						zend_string_equals_ci(ctx->current_section_name, Z_STR_P(arg2))
341 					) {
342 						zend_error(E_CORE_ERROR, "Invalid browscap ini file: "
343 							"'Parent' value cannot be same as the section name: %s "
344 							"(in file %s)", ZSTR_VAL(ctx->current_section_name), INI_STR("browscap"));
345 						return;
346 					}
347 
348 					if (ctx->current_entry->parent) {
349 						zend_string_release(ctx->current_entry->parent);
350 					}
351 
352 					ctx->current_entry->parent = new_value;
353 				} else {
354 					new_key = browscap_intern_str_ci(ctx, Z_STR_P(arg1), persistent);
355 					browscap_add_kv(bdata, new_key, new_value, persistent);
356 					ctx->current_entry->kv_end = bdata->kv_used;
357 				}
358 			}
359 			break;
360 		case ZEND_INI_PARSER_SECTION:
361 		{
362 			browscap_entry *entry;
363 			zend_string *pattern = Z_STR_P(arg1);
364 			size_t pos;
365 			int i;
366 
367 			if (ZSTR_LEN(pattern) > UINT16_MAX) {
368 				php_error_docref(NULL, E_WARNING,
369 					"Skipping excessively long pattern of length %zd", ZSTR_LEN(pattern));
370 				break;
371 			}
372 
373 			if (persistent) {
374 				pattern = zend_new_interned_string(zend_string_copy(pattern));
375 				if (ZSTR_IS_INTERNED(pattern)) {
376 					Z_TYPE_FLAGS_P(arg1) = 0;
377 				} else {
378 					zend_string_release(pattern);
379 				}
380 			}
381 
382 			entry = ctx->current_entry
383 				= pemalloc(sizeof(browscap_entry), persistent);
384 			zend_hash_update_ptr(bdata->htab, pattern, entry);
385 
386 			if (ctx->current_section_name) {
387 				zend_string_release(ctx->current_section_name);
388 			}
389 			ctx->current_section_name = zend_string_copy(pattern);
390 
391 			entry->pattern = zend_string_copy(pattern);
392 			entry->kv_end = entry->kv_start = bdata->kv_used;
393 			entry->parent = NULL;
394 
395 			pos = entry->prefix_len = browscap_compute_prefix_len(pattern);
396 			for (i = 0; i < BROWSCAP_NUM_CONTAINS; i++) {
397 				pos = browscap_compute_contains(pattern, pos,
398 					&entry->contains_start[i], &entry->contains_len[i]);
399 			}
400 			break;
401 		}
402 	}
403 }
404 /* }}} */
405 
browscap_read_file(char * filename,browser_data * browdata,int persistent)406 static int browscap_read_file(char *filename, browser_data *browdata, int persistent) /* {{{ */
407 {
408 	zend_file_handle fh;
409 	browscap_parser_ctx ctx = {0};
410 	FILE *fp;
411 
412 	if (filename == NULL || filename[0] == '\0') {
413 		return FAILURE;
414 	}
415 
416 	fp = VCWD_FOPEN(filename, "r");
417 	if (!fp) {
418 		zend_error(E_CORE_WARNING, "Cannot open \"%s\" for reading", filename);
419 		return FAILURE;
420 	}
421 	zend_stream_init_fp(&fh, fp, filename);
422 
423 	browdata->htab = pemalloc(sizeof *browdata->htab, persistent);
424 	zend_hash_init(browdata->htab, 0, NULL,
425 		persistent ? browscap_entry_dtor_persistent : browscap_entry_dtor, persistent);
426 
427 	browdata->kv_size = 16 * 1024;
428 	browdata->kv_used = 0;
429 	browdata->kv = pemalloc(sizeof(browscap_kv) * browdata->kv_size, persistent);
430 
431 	/* Create parser context */
432 	ctx.bdata = browdata;
433 	ctx.current_entry = NULL;
434 	ctx.current_section_name = NULL;
435 	/* No dtor because we don't inc the refcount for the reference stored within the hash table's entry value
436 	 * as the hash table is only temporary anyway. */
437 	zend_hash_init(&ctx.str_interned, 8, NULL, NULL, persistent);
438 
439 	zend_parse_ini_file(&fh, persistent, ZEND_INI_SCANNER_RAW,
440 			(zend_ini_parser_cb_t) php_browscap_parser_cb, &ctx);
441 
442 	/* Destroy parser context */
443 	if (ctx.current_section_name) {
444 		zend_string_release(ctx.current_section_name);
445 	}
446 	zend_hash_destroy(&ctx.str_interned);
447 	zend_destroy_file_handle(&fh);
448 
449 	return SUCCESS;
450 }
451 /* }}} */
452 
453 #ifdef ZTS
browscap_globals_ctor(zend_browscap_globals * browscap_globals)454 static void browscap_globals_ctor(zend_browscap_globals *browscap_globals) /* {{{ */
455 {
456 	browscap_globals->activation_bdata.htab = NULL;
457 	browscap_globals->activation_bdata.kv = NULL;
458 	browscap_globals->activation_bdata.filename[0] = '\0';
459 }
460 /* }}} */
461 #endif
462 
browscap_bdata_dtor(browser_data * bdata,int persistent)463 static void browscap_bdata_dtor(browser_data *bdata, int persistent) /* {{{ */
464 {
465 	if (bdata->htab != NULL) {
466 		uint32_t i;
467 
468 		zend_hash_destroy(bdata->htab);
469 		pefree(bdata->htab, persistent);
470 		bdata->htab = NULL;
471 
472 		for (i = 0; i < bdata->kv_used; i++) {
473 			zend_string_release(bdata->kv[i].key);
474 			zend_string_release(bdata->kv[i].value);
475 		}
476 		pefree(bdata->kv, persistent);
477 		bdata->kv = NULL;
478 	}
479 	bdata->filename[0] = '\0';
480 }
481 /* }}} */
482 
483 /* {{{ PHP_INI_MH */
PHP_INI_MH(OnChangeBrowscap)484 PHP_INI_MH(OnChangeBrowscap)
485 {
486 	if (stage == PHP_INI_STAGE_STARTUP) {
487 		/* value handled in browscap.c's MINIT */
488 		return SUCCESS;
489 	} else if (stage == PHP_INI_STAGE_ACTIVATE) {
490 		browser_data *bdata = &BROWSCAP_G(activation_bdata);
491 		if (bdata->filename[0] != '\0') {
492 			browscap_bdata_dtor(bdata, 0);
493 		}
494 		if (VCWD_REALPATH(ZSTR_VAL(new_value), bdata->filename) == NULL) {
495 			return FAILURE;
496 		}
497 		return SUCCESS;
498 	}
499 
500 	return FAILURE;
501 }
502 /* }}} */
503 
PHP_MINIT_FUNCTION(browscap)504 PHP_MINIT_FUNCTION(browscap) /* {{{ */
505 {
506 	char *browscap = INI_STR("browscap");
507 
508 #ifdef ZTS
509 	ts_allocate_id(&browscap_globals_id, sizeof(browser_data), (ts_allocate_ctor) browscap_globals_ctor, NULL);
510 #endif
511 	/* ctor call not really needed for non-ZTS */
512 
513 	if (browscap && browscap[0]) {
514 		if (browscap_read_file(browscap, &global_bdata, 1) == FAILURE) {
515 			return FAILURE;
516 		}
517 	}
518 
519 	return SUCCESS;
520 }
521 /* }}} */
522 
PHP_RSHUTDOWN_FUNCTION(browscap)523 PHP_RSHUTDOWN_FUNCTION(browscap) /* {{{ */
524 {
525 	browser_data *bdata = &BROWSCAP_G(activation_bdata);
526 	if (bdata->filename[0] != '\0') {
527 		browscap_bdata_dtor(bdata, 0);
528 	}
529 
530 	return SUCCESS;
531 }
532 /* }}} */
533 
PHP_MSHUTDOWN_FUNCTION(browscap)534 PHP_MSHUTDOWN_FUNCTION(browscap) /* {{{ */
535 {
536 	browscap_bdata_dtor(&global_bdata, 1);
537 
538 	return SUCCESS;
539 }
540 /* }}} */
541 
browscap_get_minimum_length(browscap_entry * entry)542 static inline size_t browscap_get_minimum_length(browscap_entry *entry) {
543 	size_t len = entry->prefix_len;
544 	int i;
545 	for (i = 0; i < BROWSCAP_NUM_CONTAINS; i++) {
546 		len += entry->contains_len[i];
547 	}
548 	return len;
549 }
550 
browscap_match_string_wildcard(const char * s,const char * s_end,const char * pattern,const char * pattern_end)551 static bool browscap_match_string_wildcard(const char *s, const char *s_end, const char *pattern, const char *pattern_end)
552 {
553 	const char *pattern_current = pattern;
554 	const char *s_current = s;
555 
556 	const char *wildcard_pattern_restore_pos = NULL;
557 	const char *wildcard_s_restore_pos = NULL;
558 
559 	while (s_current < s_end) {
560 		char pattern_char = *pattern_current;
561 		char s_char = *s_current;
562 
563 		if (pattern_char == '*') {
564 			/* Collapse wildcards */
565 			pattern_current++;
566 			while (pattern_current < pattern_end && *pattern_current == '*') {
567 				pattern_current++;
568 			}
569 
570 			/* If we're at the end of the pattern, it means that the ending was just '*', so this is a trivial match */
571 			if (pattern_current == pattern_end) {
572 				return true;
573 			}
574 
575 			/* Optimization: if there is a non-wildcard character X after a *, then we can immediately jump to the first
576 			 * character X in s starting from s_current because it is the only way to match beyond the *. */
577 			if (*pattern_current != '?') {
578 				while (s_current < s_end && *s_current != *pattern_current) {
579 					s_current++;
580 				}
581 			}
582 
583 			/* We will first assume the skipped part by * is a 0-length string (or n-length if the optimization above skipped n characters).
584 			 * When a mismatch happens we will backtrack and move s one position to assume * skipped a 1-length string.
585 			 * Then 2, 3, 4, ... */
586 			wildcard_pattern_restore_pos = pattern_current;
587 			wildcard_s_restore_pos = s_current;
588 
589 			continue;
590 		} else if (pattern_char == s_char || pattern_char == '?') {
591 			/* Match */
592 			pattern_current++;
593 			s_current++;
594 
595 			/* If this was the last character of the pattern, we either fully matched s, or we have a mismatch */
596 			if (pattern_current == pattern_end) {
597 				if (s_current == s_end) {
598 					return true;
599 				}
600 				/* Fallthrough to mismatch */
601 			} else {
602 				continue;
603 			}
604 		}
605 
606 		/* Mismatch */
607 		if (wildcard_pattern_restore_pos) {
608 			pattern_current = wildcard_pattern_restore_pos;
609 			wildcard_s_restore_pos++;
610 			s_current = wildcard_s_restore_pos;
611 		} else {
612 			/* No wildcard is active, so it is impossible to match */
613 			return false;
614 		}
615 	}
616 
617 	/* Skip remaining * wildcards, they match nothing here as we are at the end of s */
618 	while (pattern_current < pattern_end && *pattern_current == '*') {
619 		pattern_current++;
620 	}
621 
622 	ZEND_ASSERT(s_current == s_end);
623 	return pattern_current == pattern_end;
624 }
625 
browser_reg_compare(browscap_entry * entry,zend_string * agent_name,browscap_entry ** found_entry_ptr,size_t * cached_prev_len)626 static int browser_reg_compare(browscap_entry *entry, zend_string *agent_name, browscap_entry **found_entry_ptr, size_t *cached_prev_len) /* {{{ */
627 {
628 	browscap_entry *found_entry = *found_entry_ptr;
629 	ALLOCA_FLAG(use_heap)
630 	zend_string *pattern_lc;
631 	const char *cur;
632 	int i;
633 
634 	/* Lowercase the pattern, the agent name is already lowercase */
635 	ZSTR_ALLOCA_ALLOC(pattern_lc, ZSTR_LEN(entry->pattern), use_heap);
636 	zend_str_tolower_copy(ZSTR_VAL(pattern_lc), ZSTR_VAL(entry->pattern), ZSTR_LEN(entry->pattern));
637 
638 	/* Check if the agent contains the "contains" portions */
639 	cur = ZSTR_VAL(agent_name) + entry->prefix_len;
640 	for (i = 0; i < BROWSCAP_NUM_CONTAINS; i++) {
641 		if (entry->contains_len[i] != 0) {
642 			cur = zend_memnstr(cur,
643 				ZSTR_VAL(pattern_lc) + entry->contains_start[i],
644 				entry->contains_len[i],
645 				ZSTR_VAL(agent_name) + ZSTR_LEN(agent_name));
646 			if (!cur) {
647 				ZSTR_ALLOCA_FREE(pattern_lc, use_heap);
648 				return 0;
649 			}
650 			cur += entry->contains_len[i];
651 		}
652 	}
653 
654 	/* See if we have an exact match, if so, we're done... */
655 	if (zend_string_equals(agent_name, pattern_lc)) {
656 		*found_entry_ptr = entry;
657 		/* cached_prev_len doesn't matter here because we end the search when an exact match is found. */
658 		ZSTR_ALLOCA_FREE(pattern_lc, use_heap);
659 		return 1;
660 	}
661 
662 	if (browscap_match_string_wildcard(
663 		ZSTR_VAL(agent_name) + entry->prefix_len,
664 		ZSTR_VAL(agent_name) + ZSTR_LEN(agent_name),
665 		ZSTR_VAL(pattern_lc) + entry->prefix_len,
666 		ZSTR_VAL(pattern_lc) + ZSTR_LEN(pattern_lc)
667 	)) {
668 		/* If we've found a possible browser, we need to do a comparison of the
669 		   number of characters changed in the user agent being checked versus
670 		   the previous match found and the current match. */
671 		size_t curr_len = entry->prefix_len; /* Start from the prefix because the prefix is free of wildcards */
672 		zend_string *current_match = entry->pattern;
673 		for (size_t i = curr_len; i < ZSTR_LEN(current_match); i++) {
674 			switch (ZSTR_VAL(current_match)[i]) {
675 				case '?':
676 				case '*':
677 					/* do nothing, ignore these characters in the count */
678 				break;
679 
680 				default:
681 					++curr_len;
682 			}
683 		}
684 
685 		if (found_entry) {
686 			/* Pick which browser pattern replaces the least amount of
687 			   characters when compared to the original user agent string... */
688 			if (*cached_prev_len < curr_len) {
689 				*found_entry_ptr = entry;
690 				*cached_prev_len = curr_len;
691 			}
692 		} else {
693 			*found_entry_ptr = entry;
694 			*cached_prev_len = curr_len;
695 		}
696 	}
697 
698 	ZSTR_ALLOCA_FREE(pattern_lc, use_heap);
699 	return 0;
700 }
701 /* }}} */
702 
703 /* {{{ Get information about the capabilities of a browser. If browser_name is omitted or null, HTTP_USER_AGENT is used. Returns an object by default; if return_array is true, returns an array. */
PHP_FUNCTION(get_browser)704 PHP_FUNCTION(get_browser)
705 {
706 	zend_string *agent_name = NULL, *lookup_browser_name;
707 	bool return_array = 0;
708 	browser_data *bdata;
709 	browscap_entry *found_entry = NULL;
710 	HashTable *agent_ht;
711 
712 	ZEND_PARSE_PARAMETERS_START(0, 2)
713 		Z_PARAM_OPTIONAL
714 		Z_PARAM_STR_OR_NULL(agent_name)
715 		Z_PARAM_BOOL(return_array)
716 	ZEND_PARSE_PARAMETERS_END();
717 
718 	if (BROWSCAP_G(activation_bdata).filename[0] != '\0') {
719 		bdata = &BROWSCAP_G(activation_bdata);
720 		if (bdata->htab == NULL) { /* not initialized yet */
721 			if (browscap_read_file(bdata->filename, bdata, 0) == FAILURE) {
722 				RETURN_FALSE;
723 			}
724 		}
725 	} else {
726 		if (!global_bdata.htab) {
727 			php_error_docref(NULL, E_WARNING, "browscap ini directive not set");
728 			RETURN_FALSE;
729 		}
730 		bdata = &global_bdata;
731 	}
732 
733 	if (agent_name == NULL) {
734 		zval *http_user_agent = NULL;
735 		if (Z_TYPE(PG(http_globals)[TRACK_VARS_SERVER]) == IS_ARRAY
736 				|| zend_is_auto_global(ZSTR_KNOWN(ZEND_STR_AUTOGLOBAL_SERVER))) {
737 			http_user_agent = zend_hash_str_find(
738 				Z_ARRVAL_P(&PG(http_globals)[TRACK_VARS_SERVER]),
739 				"HTTP_USER_AGENT", sizeof("HTTP_USER_AGENT")-1);
740 		}
741 		if (http_user_agent == NULL) {
742 			php_error_docref(NULL, E_WARNING, "HTTP_USER_AGENT variable is not set, cannot determine user agent name");
743 			RETURN_FALSE;
744 		}
745 		agent_name = Z_STR_P(http_user_agent);
746 	}
747 
748 	lookup_browser_name = zend_string_tolower(agent_name);
749 	found_entry = zend_hash_find_ptr(bdata->htab, lookup_browser_name);
750 	if (found_entry == NULL) {
751 		browscap_entry *entry;
752 		size_t cached_prev_len = 0; /* silence compiler warning */
753 
754 		ZEND_HASH_MAP_FOREACH_PTR(bdata->htab, entry) {
755 			/* The following two early-skip checks are inside this loop instead of inside browser_reg_compare().
756 			 * That's because we want to avoid the call frame overhead, especially as browser_reg_compare() is
757 			 * a function that uses alloca(). */
758 
759 			/* Agent name too short */
760 			if (ZSTR_LEN(lookup_browser_name) < browscap_get_minimum_length(entry)) {
761 				continue;
762 			}
763 
764 			/* Quickly discard patterns where the prefix doesn't match. */
765 			bool prefix_matches = true;
766 			for (size_t i = 0; i < entry->prefix_len; i++) {
767 				if (ZSTR_VAL(lookup_browser_name)[i] != zend_tolower_ascii(ZSTR_VAL(entry->pattern)[i])) {
768 					prefix_matches = false;
769 					break;
770 				}
771 			}
772 			if (!prefix_matches) {
773 				continue;
774 			}
775 
776 			if (browser_reg_compare(entry, lookup_browser_name, &found_entry, &cached_prev_len)) {
777 				break;
778 			}
779 		} ZEND_HASH_FOREACH_END();
780 
781 		if (found_entry == NULL) {
782 			found_entry = zend_hash_str_find_ptr(bdata->htab,
783 				DEFAULT_SECTION_NAME, sizeof(DEFAULT_SECTION_NAME)-1);
784 			if (found_entry == NULL) {
785 				zend_string_release_ex(lookup_browser_name, false);
786 				RETURN_FALSE;
787 			}
788 		}
789 	}
790 
791 	zend_string_release_ex(lookup_browser_name, false);
792 
793 	agent_ht = browscap_entry_to_array(bdata, found_entry);
794 
795 	if (return_array) {
796 		RETVAL_ARR(agent_ht);
797 	} else {
798 		object_and_properties_init(return_value, zend_standard_class_def, agent_ht);
799 	}
800 
801 	HashTable *target_ht = return_array ? Z_ARRVAL_P(return_value) : Z_OBJPROP_P(return_value);
802 
803 	while (found_entry->parent) {
804 		found_entry = zend_hash_find_ptr(bdata->htab, found_entry->parent);
805 		if (found_entry == NULL) {
806 			break;
807 		}
808 
809 		browscap_entry_add_kv_to_existing_array(bdata, found_entry, target_ht);
810 	}
811 }
812 /* }}} */
813