browscap.c 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. /*
  2. +----------------------------------------------------------------------+
  3. | PHP Version 5 |
  4. +----------------------------------------------------------------------+
  5. | Copyright (c) 1997-2016 The PHP Group |
  6. +----------------------------------------------------------------------+
  7. | This source file is subject to version 3.01 of the PHP license, |
  8. | that is bundled with this package in the file LICENSE, and is |
  9. | available through the world-wide-web at the following url: |
  10. | http://www.php.net/license/3_01.txt |
  11. | If you did not receive a copy of the PHP license and are unable to |
  12. | obtain it through the world-wide-web, please send a note to |
  13. | license@php.net so we can mail you a copy immediately. |
  14. +----------------------------------------------------------------------+
  15. | Author: Zeev Suraski <zeev@zend.com> |
  16. +----------------------------------------------------------------------+
  17. */
  18. /* $Id$ */
  19. #include "php.h"
  20. #include "php_browscap.h"
  21. #include "php_ini.h"
  22. #include "php_string.h"
  23. #include "ext/pcre/php_pcre.h"
  24. #include "zend_ini_scanner.h"
  25. #include "zend_globals.h"
  26. typedef struct {
  27. HashTable *htab;
  28. zval *current_section;
  29. char *current_section_name;
  30. char filename[MAXPATHLEN];
  31. } browser_data;
  32. /* browser data defined in startup phase, eagerly loaded in MINIT */
  33. static browser_data global_bdata = {0};
  34. /* browser data defined in activation phase, lazily loaded in get_browser.
  35. * Per request and per thread, if applicable */
  36. ZEND_BEGIN_MODULE_GLOBALS(browscap)
  37. browser_data activation_bdata;
  38. ZEND_END_MODULE_GLOBALS(browscap)
  39. ZEND_DECLARE_MODULE_GLOBALS(browscap)
  40. #ifdef ZTS
  41. #define BROWSCAP_G(v) TSRMG(browscap_globals_id, zend_browscap_globals *, v)
  42. #else
  43. #define BROWSCAP_G(v) (browscap_globals.v)
  44. #endif
  45. #define DEFAULT_SECTION_NAME "Default Browser Capability Settings"
  46. /* OBJECTS_FIXME: This whole extension needs going through. The use of objects looks pretty broken here */
  47. static void browscap_entry_dtor_request(zval **zvalue) /* {{{ */
  48. {
  49. if (Z_TYPE_PP(zvalue) == IS_ARRAY) {
  50. zend_hash_destroy(Z_ARRVAL_PP(zvalue));
  51. efree(Z_ARRVAL_PP(zvalue));
  52. } else if (Z_TYPE_PP(zvalue) == IS_STRING) {
  53. if (Z_STRVAL_PP(zvalue)) {
  54. efree(Z_STRVAL_PP(zvalue));
  55. }
  56. }
  57. efree(*zvalue);
  58. }
  59. /* }}} */
  60. static void browscap_entry_dtor_persistent(zval **zvalue) /* {{{ */ {
  61. if (Z_TYPE_PP(zvalue) == IS_ARRAY) {
  62. zend_hash_destroy(Z_ARRVAL_PP(zvalue));
  63. free(Z_ARRVAL_PP(zvalue));
  64. } else if (Z_TYPE_PP(zvalue) == IS_STRING) {
  65. if (Z_STRVAL_PP(zvalue)) {
  66. free(Z_STRVAL_PP(zvalue));
  67. }
  68. }
  69. free(*zvalue);
  70. }
  71. /* }}} */
  72. static void convert_browscap_pattern(zval *pattern, int persistent) /* {{{ */
  73. {
  74. int i, j=0;
  75. char *t;
  76. php_strtolower(Z_STRVAL_P(pattern), Z_STRLEN_P(pattern));
  77. t = (char *) safe_pemalloc(Z_STRLEN_P(pattern), 2, 5, persistent);
  78. t[j++] = '~';
  79. t[j++] = '^';
  80. for (i=0; i<Z_STRLEN_P(pattern); i++, j++) {
  81. switch (Z_STRVAL_P(pattern)[i]) {
  82. case '?':
  83. t[j] = '.';
  84. break;
  85. case '*':
  86. t[j++] = '.';
  87. t[j] = '*';
  88. break;
  89. case '.':
  90. t[j++] = '\\';
  91. t[j] = '.';
  92. break;
  93. case '\\':
  94. t[j++] = '\\';
  95. t[j] = '\\';
  96. break;
  97. case '(':
  98. t[j++] = '\\';
  99. t[j] = '(';
  100. break;
  101. case ')':
  102. t[j++] = '\\';
  103. t[j] = ')';
  104. break;
  105. case '~':
  106. t[j++] = '\\';
  107. t[j] = '~';
  108. break;
  109. case '+':
  110. t[j++] = '\\';
  111. t[j] = '+';
  112. break;
  113. default:
  114. t[j] = Z_STRVAL_P(pattern)[i];
  115. break;
  116. }
  117. }
  118. t[j++] = '$';
  119. t[j++] = '~';
  120. t[j]=0;
  121. Z_STRVAL_P(pattern) = t;
  122. Z_STRLEN_P(pattern) = j;
  123. }
  124. /* }}} */
  125. static void php_browscap_parser_cb(zval *arg1, zval *arg2, zval *arg3, int callback_type, void *arg TSRMLS_DC) /* {{{ */
  126. {
  127. browser_data *bdata = arg;
  128. int persistent = bdata->htab->persistent;
  129. if (!arg1) {
  130. return;
  131. }
  132. switch (callback_type) {
  133. case ZEND_INI_PARSER_ENTRY:
  134. if (bdata->current_section && arg2) {
  135. zval *new_property;
  136. char *new_key;
  137. /* parent entry can not be same as current section -> causes infinite loop! */
  138. if (!strcasecmp(Z_STRVAL_P(arg1), "parent") &&
  139. bdata->current_section_name != NULL &&
  140. !strcasecmp(bdata->current_section_name, Z_STRVAL_P(arg2))
  141. ) {
  142. zend_error(E_CORE_ERROR, "Invalid browscap ini file: "
  143. "'Parent' value cannot be same as the section name: %s "
  144. "(in file %s)", bdata->current_section_name, INI_STR("browscap"));
  145. return;
  146. }
  147. new_property = (zval *) pemalloc(sizeof(zval), persistent);
  148. INIT_PZVAL(new_property);
  149. Z_TYPE_P(new_property) = IS_STRING;
  150. /* Set proper value for true/false settings */
  151. if ((Z_STRLEN_P(arg2) == 2 && !strncasecmp(Z_STRVAL_P(arg2), "on", sizeof("on") - 1)) ||
  152. (Z_STRLEN_P(arg2) == 3 && !strncasecmp(Z_STRVAL_P(arg2), "yes", sizeof("yes") - 1)) ||
  153. (Z_STRLEN_P(arg2) == 4 && !strncasecmp(Z_STRVAL_P(arg2), "true", sizeof("true") - 1))
  154. ) {
  155. Z_STRVAL_P(new_property) = pestrndup("1", 1, persistent);
  156. Z_STRLEN_P(new_property) = 1;
  157. } else if (
  158. (Z_STRLEN_P(arg2) == 2 && !strncasecmp(Z_STRVAL_P(arg2), "no", sizeof("no") - 1)) ||
  159. (Z_STRLEN_P(arg2) == 3 && !strncasecmp(Z_STRVAL_P(arg2), "off", sizeof("off") - 1)) ||
  160. (Z_STRLEN_P(arg2) == 4 && !strncasecmp(Z_STRVAL_P(arg2), "none", sizeof("none") - 1)) ||
  161. (Z_STRLEN_P(arg2) == 5 && !strncasecmp(Z_STRVAL_P(arg2), "false", sizeof("false") - 1))
  162. ) {
  163. Z_STRVAL_P(new_property) = pestrndup("", 0, persistent);
  164. Z_STRLEN_P(new_property) = 0;
  165. } else { /* Other than true/false setting */
  166. Z_STRVAL_P(new_property) = pestrndup(Z_STRVAL_P(arg2),
  167. Z_STRLEN_P(arg2), persistent);
  168. Z_STRLEN_P(new_property) = Z_STRLEN_P(arg2);
  169. }
  170. new_key = pestrndup(Z_STRVAL_P(arg1), Z_STRLEN_P(arg1), persistent);
  171. zend_str_tolower(new_key, Z_STRLEN_P(arg1));
  172. zend_hash_update(Z_ARRVAL_P(bdata->current_section), new_key, Z_STRLEN_P(arg1) + 1, &new_property, sizeof(zval *), NULL);
  173. pefree(new_key, persistent);
  174. }
  175. break;
  176. case ZEND_INI_PARSER_SECTION: {
  177. zval *processed;
  178. zval *unprocessed;
  179. HashTable *section_properties;
  180. /*printf("'%s' (%d)\n",$1.value.str.val,$1.value.str.len + 1);*/
  181. bdata->current_section = (zval *) pemalloc(sizeof(zval), persistent);
  182. INIT_PZVAL(bdata->current_section);
  183. processed = (zval *) pemalloc(sizeof(zval), persistent);
  184. INIT_PZVAL(processed);
  185. unprocessed = (zval *) pemalloc(sizeof(zval), persistent);
  186. INIT_PZVAL(unprocessed);
  187. section_properties = (HashTable *) pemalloc(sizeof(HashTable), persistent);
  188. zend_hash_init(section_properties, 0, NULL,
  189. (dtor_func_t) (persistent?browscap_entry_dtor_persistent
  190. :browscap_entry_dtor_request),
  191. persistent);
  192. Z_ARRVAL_P(bdata->current_section) = section_properties;
  193. Z_TYPE_P(bdata->current_section) = IS_ARRAY;
  194. if (bdata->current_section_name) {
  195. pefree(bdata->current_section_name, persistent);
  196. }
  197. bdata->current_section_name = pestrndup(Z_STRVAL_P(arg1),
  198. Z_STRLEN_P(arg1), persistent);
  199. zend_hash_update(bdata->htab, Z_STRVAL_P(arg1), Z_STRLEN_P(arg1) + 1, (void *) &bdata->current_section, sizeof(zval *), NULL);
  200. Z_STRVAL_P(processed) = Z_STRVAL_P(arg1);
  201. Z_STRLEN_P(processed) = Z_STRLEN_P(arg1);
  202. Z_TYPE_P(processed) = IS_STRING;
  203. Z_STRVAL_P(unprocessed) = Z_STRVAL_P(arg1);
  204. Z_STRLEN_P(unprocessed) = Z_STRLEN_P(arg1);
  205. Z_TYPE_P(unprocessed) = IS_STRING;
  206. Z_STRVAL_P(unprocessed) = pestrndup(Z_STRVAL_P(unprocessed), Z_STRLEN_P(unprocessed), persistent);
  207. convert_browscap_pattern(processed, persistent);
  208. zend_hash_update(section_properties, "browser_name_regex", sizeof("browser_name_regex"), (void *) &processed, sizeof(zval *), NULL);
  209. zend_hash_update(section_properties, "browser_name_pattern", sizeof("browser_name_pattern"), (void *) &unprocessed, sizeof(zval *), NULL);
  210. }
  211. break;
  212. }
  213. }
  214. /* }}} */
  215. static int browscap_read_file(char *filename, browser_data *browdata, int persistent TSRMLS_DC) /* {{{ */
  216. {
  217. zend_file_handle fh = {0};
  218. if (filename == NULL || filename[0] == '\0') {
  219. return FAILURE;
  220. }
  221. browdata->htab = pemalloc(sizeof *browdata->htab, persistent);
  222. if (browdata->htab == NULL) {
  223. return FAILURE;
  224. }
  225. if (zend_hash_init_ex(browdata->htab, 0, NULL,
  226. (dtor_func_t) (persistent?browscap_entry_dtor_persistent
  227. :browscap_entry_dtor_request),
  228. persistent, 0) == FAILURE) {
  229. pefree(browdata->htab, persistent);
  230. browdata->htab = NULL;
  231. return FAILURE;
  232. }
  233. fh.handle.fp = VCWD_FOPEN(filename, "r");
  234. fh.opened_path = NULL;
  235. fh.free_filename = 0;
  236. if (!fh.handle.fp) {
  237. zend_hash_destroy(browdata->htab);
  238. pefree(browdata->htab, persistent);
  239. browdata->htab = NULL;
  240. zend_error(E_CORE_WARNING, "Cannot open '%s' for reading", filename);
  241. return FAILURE;
  242. }
  243. fh.filename = filename;
  244. Z_TYPE(fh) = ZEND_HANDLE_FP;
  245. browdata->current_section_name = NULL;
  246. zend_parse_ini_file(&fh, 1, ZEND_INI_SCANNER_RAW,
  247. (zend_ini_parser_cb_t) php_browscap_parser_cb, browdata TSRMLS_CC);
  248. if (browdata->current_section_name != NULL) {
  249. pefree(browdata->current_section_name, persistent);
  250. browdata->current_section_name = NULL;
  251. }
  252. return SUCCESS;
  253. }
  254. /* }}} */
  255. #ifdef ZTS
  256. static void browscap_globals_ctor(zend_browscap_globals *browscap_globals TSRMLS_DC) /* {{{ */
  257. {
  258. browscap_globals->activation_bdata.htab = NULL;
  259. browscap_globals->activation_bdata.current_section = NULL;
  260. browscap_globals->activation_bdata.current_section_name = NULL;
  261. browscap_globals->activation_bdata.filename[0] = '\0';
  262. }
  263. /* }}} */
  264. #endif
  265. static void browscap_bdata_dtor(browser_data *bdata, int persistent TSRMLS_DC) /* {{{ */
  266. {
  267. if (bdata->htab != NULL) {
  268. zend_hash_destroy(bdata->htab);
  269. pefree(bdata->htab, persistent);
  270. bdata->htab = NULL;
  271. }
  272. bdata->filename[0] = '\0';
  273. /* current_section_* are only used during parsing */
  274. }
  275. /* }}} */
  276. /* {{{ PHP_INI_MH
  277. */
  278. PHP_INI_MH(OnChangeBrowscap)
  279. {
  280. if (stage == PHP_INI_STAGE_STARTUP) {
  281. /* value handled in browscap.c's MINIT */
  282. return SUCCESS;
  283. } else if (stage == PHP_INI_STAGE_ACTIVATE) {
  284. browser_data *bdata = &BROWSCAP_G(activation_bdata);
  285. if (bdata->filename[0] != '\0') {
  286. browscap_bdata_dtor(bdata, 0 TSRMLS_CC);
  287. }
  288. if (VCWD_REALPATH(new_value, bdata->filename) == NULL) {
  289. return FAILURE;
  290. }
  291. return SUCCESS;
  292. }
  293. return FAILURE;
  294. }
  295. /* }}} */
  296. PHP_MINIT_FUNCTION(browscap) /* {{{ */
  297. {
  298. char *browscap = INI_STR("browscap");
  299. #ifdef ZTS
  300. ts_allocate_id(&browscap_globals_id, sizeof(browser_data),
  301. browscap_globals_ctor, NULL);
  302. #endif
  303. /* ctor call not really needed for non-ZTS */
  304. if (browscap && browscap[0]) {
  305. if (browscap_read_file(browscap, &global_bdata, 1 TSRMLS_CC) == FAILURE) {
  306. return FAILURE;
  307. }
  308. }
  309. return SUCCESS;
  310. }
  311. /* }}} */
  312. PHP_RSHUTDOWN_FUNCTION(browscap) /* {{{ */
  313. {
  314. browser_data *bdata = &BROWSCAP_G(activation_bdata);
  315. if (bdata->filename[0] != '\0') {
  316. browscap_bdata_dtor(bdata, 0 TSRMLS_CC);
  317. }
  318. return SUCCESS;
  319. }
  320. /* }}} */
  321. PHP_MSHUTDOWN_FUNCTION(browscap) /* {{{ */
  322. {
  323. browscap_bdata_dtor(&global_bdata, 1 TSRMLS_CC);
  324. return SUCCESS;
  325. }
  326. /* }}} */
  327. static int browser_reg_compare(zval **browser TSRMLS_DC, int num_args, va_list args, zend_hash_key *key) /* {{{ */
  328. {
  329. zval **browser_regex, **previous_match;
  330. pcre *re;
  331. int re_options;
  332. pcre_extra *re_extra;
  333. char *lookup_browser_name = va_arg(args, char *);
  334. int lookup_browser_length = va_arg(args, int);
  335. zval **found_browser_entry = va_arg(args, zval **);
  336. /* See if we have an exact match, if so, we're done... */
  337. if (*found_browser_entry) {
  338. if (zend_hash_find(Z_ARRVAL_PP(found_browser_entry), "browser_name_pattern", sizeof("browser_name_pattern"), (void**) &previous_match) == FAILURE) {
  339. return 0;
  340. }
  341. else if (!strcasecmp(Z_STRVAL_PP(previous_match), lookup_browser_name)) {
  342. return 0;
  343. }
  344. }
  345. if (zend_hash_find(Z_ARRVAL_PP(browser), "browser_name_regex", sizeof("browser_name_regex"), (void **) &browser_regex) == FAILURE) {
  346. return 0;
  347. }
  348. re = pcre_get_compiled_regex(Z_STRVAL_PP(browser_regex), &re_extra, &re_options TSRMLS_CC);
  349. if (re == NULL) {
  350. return 0;
  351. }
  352. if (pcre_exec(re, re_extra, lookup_browser_name, lookup_browser_length, 0, re_options, NULL, 0) == 0) {
  353. /* If we've found a possible browser, we need to do a comparison of the
  354. number of characters changed in the user agent being checked versus
  355. the previous match found and the current match. */
  356. if (*found_browser_entry) {
  357. int i, prev_len = 0, curr_len = 0, ua_len;
  358. zval **current_match;
  359. if (zend_hash_find(Z_ARRVAL_PP(browser), "browser_name_pattern", sizeof("browser_name_pattern"), (void**) &current_match) == FAILURE) {
  360. return 0;
  361. }
  362. ua_len = lookup_browser_length;
  363. for (i = 0; i < Z_STRLEN_PP(previous_match); i++) {
  364. switch (Z_STRVAL_PP(previous_match)[i]) {
  365. case '?':
  366. case '*':
  367. /* do nothing, ignore these characters in the count */
  368. break;
  369. default:
  370. ++prev_len;
  371. }
  372. }
  373. for (i = 0; i < Z_STRLEN_PP(current_match); i++) {
  374. switch (Z_STRVAL_PP(current_match)[i]) {
  375. case '?':
  376. case '*':
  377. /* do nothing, ignore these characters in the count */
  378. break;
  379. default:
  380. ++curr_len;
  381. }
  382. }
  383. /* Pick which browser pattern replaces the least amount of
  384. characters when compared to the original user agent string... */
  385. if (ua_len - prev_len > ua_len - curr_len) {
  386. *found_browser_entry = *browser;
  387. }
  388. }
  389. else {
  390. *found_browser_entry = *browser;
  391. }
  392. }
  393. return 0;
  394. }
  395. /* }}} */
  396. static void browscap_zval_copy_ctor(zval **p) /* {{{ */
  397. {
  398. zval *new;
  399. ALLOC_ZVAL(new);
  400. *new = **p;
  401. zval_copy_ctor(new);
  402. INIT_PZVAL(new);
  403. *p = new;
  404. } /* }}} */
  405. /* {{{ proto mixed get_browser([string browser_name [, bool return_array]])
  406. 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. */
  407. PHP_FUNCTION(get_browser)
  408. {
  409. char *agent_name = NULL;
  410. int agent_name_len = 0;
  411. zend_bool return_array = 0;
  412. zval **agent, **z_agent_name, **http_user_agent;
  413. zval *found_browser_entry, *tmp_copy;
  414. char *lookup_browser_name;
  415. browser_data *bdata;
  416. if (BROWSCAP_G(activation_bdata).filename[0] != '\0') {
  417. bdata = &BROWSCAP_G(activation_bdata);
  418. if (bdata->htab == NULL) { /* not initialized yet */
  419. if (browscap_read_file(bdata->filename, bdata, 0 TSRMLS_CC) == FAILURE) {
  420. RETURN_FALSE;
  421. }
  422. }
  423. } else {
  424. if (!global_bdata.htab) {
  425. php_error_docref(NULL TSRMLS_CC, E_WARNING, "browscap ini directive not set");
  426. RETURN_FALSE;
  427. }
  428. bdata = &global_bdata;
  429. }
  430. if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|s!b", &agent_name, &agent_name_len, &return_array) == FAILURE) {
  431. return;
  432. }
  433. if (agent_name == NULL) {
  434. zend_is_auto_global("_SERVER", sizeof("_SERVER") - 1 TSRMLS_CC);
  435. if (!PG(http_globals)[TRACK_VARS_SERVER] ||
  436. zend_hash_find(HASH_OF(PG(http_globals)[TRACK_VARS_SERVER]), "HTTP_USER_AGENT", sizeof("HTTP_USER_AGENT"), (void **) &http_user_agent) == FAILURE
  437. ) {
  438. php_error_docref(NULL TSRMLS_CC, E_WARNING, "HTTP_USER_AGENT variable is not set, cannot determine user agent name");
  439. RETURN_FALSE;
  440. }
  441. agent_name = Z_STRVAL_PP(http_user_agent);
  442. agent_name_len = Z_STRLEN_PP(http_user_agent);
  443. }
  444. lookup_browser_name = estrndup(agent_name, agent_name_len);
  445. php_strtolower(lookup_browser_name, agent_name_len);
  446. if (zend_hash_find(bdata->htab, lookup_browser_name, agent_name_len + 1, (void **) &agent) == FAILURE) {
  447. found_browser_entry = NULL;
  448. zend_hash_apply_with_arguments(bdata->htab TSRMLS_CC, (apply_func_args_t) browser_reg_compare, 3, lookup_browser_name, agent_name_len, &found_browser_entry);
  449. if (found_browser_entry) {
  450. agent = &found_browser_entry;
  451. } else if (zend_hash_find(bdata->htab, DEFAULT_SECTION_NAME, sizeof(DEFAULT_SECTION_NAME), (void **) &agent) == FAILURE) {
  452. efree(lookup_browser_name);
  453. RETURN_FALSE;
  454. }
  455. }
  456. if (return_array) {
  457. array_init(return_value);
  458. zend_hash_copy(Z_ARRVAL_P(return_value), Z_ARRVAL_PP(agent), (copy_ctor_func_t) browscap_zval_copy_ctor, (void *) &tmp_copy, sizeof(zval *));
  459. }
  460. else {
  461. object_init(return_value);
  462. zend_hash_copy(Z_OBJPROP_P(return_value), Z_ARRVAL_PP(agent), (copy_ctor_func_t) browscap_zval_copy_ctor, (void *) &tmp_copy, sizeof(zval *));
  463. }
  464. while (zend_hash_find(Z_ARRVAL_PP(agent), "parent", sizeof("parent"), (void **) &z_agent_name) == SUCCESS) {
  465. if (zend_hash_find(bdata->htab, Z_STRVAL_PP(z_agent_name), Z_STRLEN_PP(z_agent_name) + 1, (void **)&agent) == FAILURE) {
  466. break;
  467. }
  468. if (return_array) {
  469. zend_hash_merge(Z_ARRVAL_P(return_value), Z_ARRVAL_PP(agent), (copy_ctor_func_t) browscap_zval_copy_ctor, (void *) &tmp_copy, sizeof(zval *), 0);
  470. }
  471. else {
  472. zend_hash_merge(Z_OBJPROP_P(return_value), Z_ARRVAL_PP(agent), (copy_ctor_func_t) browscap_zval_copy_ctor, (void *) &tmp_copy, sizeof(zval *), 0);
  473. }
  474. }
  475. efree(lookup_browser_name);
  476. }
  477. /* }}} */
  478. /*
  479. * Local variables:
  480. * tab-width: 4
  481. * c-basic-offset: 4
  482. * End:
  483. * vim600: sw=4 ts=4 fdm=marker
  484. * vim<600: sw=4 ts=4
  485. */