Coverage Report

Created: 2026-06-02 06:36

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