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