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