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