PHP 源码探秘 - 在解析外部变量时的一个 BUG
bug 复现
有个朋友跟我描述了一个bug,要我帮看看是什么情况。原本他有一个表单,如下。
<form method="post"> <input type="text" name="id[]" value="1"> <input type="text" name="id[]" value="2"> <input type="submit"> </form>
但是有一个前端插件会动态插入两个input
,最后ajax
提交的时候是
<form method="post"> <input type="text" name="id[]" value="1"> <input type="text" name="id[]_text" value="a"> <input type="text" name="id[]" value="2"> <input type="text" name="id[]_text" value="b"> <input type="submit"> </form>
后端
当我们用 php 来接收的时候
echo file_get_contents('php://input'); echo "\n"; var_export($_POST); echo "\n"; echo PHP_VERSION;
结果是
id%5B%5D=1&id%5B%5D_text=a&id%5B%5D=2&id%5B%5D_text=b array ( 'id' => array ( 0 => '1', 1 => 'a', 2 => '2', 3 => 'b', ), ) 7.0.10
使用 nodejs 尝试
var http = require('http'); var querystring = require('querystring'); var postHTML = '<form method="post">' + '<input type="text" name="id[]" value="1"><input type="text" name="id[]_text" value="a">' + '<input type="text" name="id[]" value="2"><input type="text" name="id[]_text" value="b">' + '<input type="submit"></form>'; http.createServer(function (req, res) { var body = ""; req.on('data', function (chunk) { body += chunk; console.log(body); body = querystring.parse(body); console.log(body); }); req.on('end', function () { res.writeHead(200, {'Content-Type': 'text/html; charset=utf8'}); res.write(postHTML); res.end(); }); }).listen(3000);
控制台输出的是
id%5B%5D=1&id%5B%5D_text=a&id%5B%5D=2&id%5B%5D_text=b { 'id[]': [ '1', '2' ], 'id[]_text': [ 'a', 'b' ] }
小结
在接收外部变量时,多个相同的外部变量,在nodejs
中会被放在一个数组里面,而php
中则是后者覆盖前者,如果需要传递数组变量,则在变量名后面添加上[]
。这个不兼容,ok,是语言的特性能接受。
但是在php
中在解析id[]_text
的数据的时候都转换成id[]
了,这点就有点坑了。rfc 在这方面也没看到有规定否则不会出现两种语言解析不一致的情况了。
源码分析
也就是说 php 后端在解析的时候的问题。那只能从源码里一探究竟看php是如何解析post数据的了。
我把子进程数修改为1,然后根据pid
来调试
gdb -p 22892 ... (gdb) b /data/soft/php-7.1.10/main/php_variables.c:php_register_variable_ex Breakpoint 1 at 0x812877: file /data/soft/php-7.1.10/main/php_variables.c, line 70. (gdb) i b Num Type Disp Enb Address What 1 breakpoint keep y 0x0000000000812877 in php_register_variable_ex at /data/soft/php-7.1.10/main/php_variables.c:70 (gdb) (gdb) c Continuing. Breakpoint 1, php_register_variable_ex (var_name=0x7fb5b9056218 "id[]", val=0x7ffff23dacd0, track_vars_array=0xf114a0) at /data/soft/php-7.1.10/main/php_variables.c:70 70 if (track_vars_array && Z_TYPE_P(track_vars_array) == IS_ARRAY) { (gdb) bt #0 php_register_variable_ex (var_name=0x7fb5b9056218 "id[]", val=0x7ffff23dacd0, track_vars_array=0xf114a0) at /data/soft/php-7.1.10/main/php_variables.c:70 #1 0x00000000005af0d1 in php_sapi_filter (arg=<value optimized out>, var=0x7fb5b9056218 "id[]", val=0x7ffff23dad48, val_len=1, new_val_len=0x7ffff23dad40) at /data/soft/php-7.1.10/ext/filter/filter.c:465 #2 0x00000000008135d0 in add_post_var (arr=0x7ffff23dce50, var=0x7ffff23dcda0, eof=<value optimized out>) at /data/soft/php-7.1.10/main/php_variables.c:308 #3 0x0000000000813ce6 in add_post_vars (content_type_dup=<value optimized out>, arg=0x7ffff23dce50) at /data/soft/php-7.1.10/main/php_variables.c:324 #4 php_std_post_handler (content_type_dup=<value optimized out>, arg=0x7ffff23dce50) at /data/soft/php-7.1.10/main/php_variables.c:361 #5 0x000000000080cfe0 in sapi_handle_post (arg=<value optimized out>) at /data/soft/php-7.1.10/main/SAPI.c:174 #6 0x00000000008133cf in php_default_treat_data (arg=0, str=0x0, destArray=<value optimized out>) at /data/soft/php-7.1.10/main/php_variables.c:423 #7 0x000000000066c581 in mbstr_treat_data (arg=0, str=0x0, destArray=0x0) at /data/soft/php-7.1.10/ext/mbstring/mb_gpc.c:69 #8 0x0000000000812463 in php_auto_globals_create_post (name=0x7fb5b1ddf768) at /data/soft/php-7.1.10/main/php_variables.c:720 #9 0x000000000084125f in zend_activate_auto_globals () at /data/soft/php-7.1.10/Zend/zend_compile.c:1681 #10 0x000000000081282e in php_hash_environment () at /data/soft/php-7.1.10/main/php_variables.c:690 #11 0x0000000000804c11 in php_request_startup () at /data/soft/php-7.1.10/main/main.c:1672 #12 0x0000000000918282 in main (argc=<value optimized out>, argv=<value optimized out>) at /data/soft/php-7.1.10/sapi/fpm/fpm/fpm_main.c:1904 (gdb)
那么我们看php_register_variable_ex
怎么写的,源码精简了下,如下
#include <stdio.h> #include <assert.h> #include <memory.h> #include <stdlib.h> void php_register_variable_ex(char *var_name); typedef unsigned char zend_bool; int main() { char *var_name = "id 1.2[]_3"; php_register_variable_ex(var_name); return 0; } void php_register_variable_ex(char *var_name) { char *p = NULL; char *ip = NULL; /* index pointer */ char *index; char *var, *var_orig; size_t var_len, index_len; zend_bool is_array = 0; assert(var_name != NULL); /* ignore leading spaces in the variable name */ while (*var_name==' ') { var_name++; } /* * Prepare variable name */ var_len = strlen(var_name); var = var_orig = malloc(var_len + 1); memcpy(var_orig, var_name, var_len + 1); /* ensure that we don't have spaces or dots in the variable name (not binary safe) */ for (p = var; *p; p++) { if (*p == ' ' || *p == '.') { *p='_'; } else if (*p == '[') { is_array = 1; ip = p; *p = 0; break; } } var_len = p - var; printf("var\t%s\n",var); printf("var_len\t%zu\n",var_len); }
根据php_register_variable_ex
里面的规则:
-
name
里面的.
都被替换成_
-
name
里遇到[
则认为是数组,数组的key为[
前面的字符串,后面的都被舍去。
上面我模拟了表单提交一个name
为id 1.2[]_3
时,输出结果就是
var id_1_2 var_len 6
思考为什么
上面的替换规则在官方手册中有说明
http://php.net/manual/zh/language.variables.external.php
Dots and spaces in variable names are converted to underscores.
但是没有看到命名中关于不使用[]
后连接字符串的说明。
extract
难道是因为extract
原因,如果数组key
里面有[]
,则没办法正常执行了。
$foo["id"] = 1; $foo["id[]_text"] = 2; var_export($foo); extract($foo); var_export(get_defined_vars());
试了以上代码,也印证了我的想法id[]_text
的值直接丢失了。
所以
-
php
在接受这样命名的(foo[]boo
)外部变量名是不符合规范的,手册文档需要补全; -
php
在接受这样不符合命名规范的(foo[]boo
)外部变量的时候是强制转换成数组,还是直接丢弃呢?
后续处理方案
- 我提交了 bug https://bugs.php.net/bug.php?id=77172
- 官方修复:在文档上补全说明 http://php.net/manual/zh/language.variables.external.php
- php 8 里面可能设置开关来控制是否对外部变量进行转换 https://bugs.php.net/bug.php?id=34882 不过这样,依然无法绕过我说的
extract
函数报错的坑
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
基于 SpringBoot2.0+优雅整合 SpringBoot+Mybatis
Github 地址:https://github.com/Snailclimb/springboot-integration-examples(SpringBoot和其他常用技术的整合,可能是你遇到的讲解最详细的学习案例,力争新手也能看懂并且能够在看完之后独立实践。基于最新的 SpringBoot2.0+,是你学习SpringBoot 的最佳指南。) ,欢迎各位 Star。 SpringBoot 整合 Mybatis 有两种常用的方式,一种就是我们常见的 xml 的方式 ,还有一种是全注解的方式。我觉得这两者没有谁比谁好,在 SQL 语句不太长的情况下,我觉得全注解的方式一定是比较清晰简洁的。但是,复杂的 SQL 确实不太适合和代码写在一起。 下面就开始吧! 目录: 一 开发前的准备 1.1 环境参数 1.2 创建工程 1.3 创建数据库和 user 用户表 1.4 配置 pom 文件中的相关依赖 1.5 配置 application.properties 1.6 创建用户类 Bean 二 全注解的方式 2.1 Dao 层开发 2.2 service 层 2.3 Controller ...
- 下一篇
全图化引擎(AI·OS)中的编译技术
全图化引擎又称算子执行引擎,它的介绍可以参考从HA3到AI OS -- 全图化引擎破茧之路。本文从算子化的视角介绍了编译技术在全图化引擎中的运用。主要内容有: 1. 通过脚本语言扩展通用算子上的用户订制能力,目前这些通用算子包括scorer算子,filter算子等。这一方面侧重于编译前端,我们开发了一种嵌入引擎的脚本语言cava,解决了用户扩展引擎功能的一些痛点,包括插件的开发测试效率,兼容性,引擎版本升级效率等。 2. 通过codegen技术优化全图化引擎性能,由于全图化引擎是基于tensorflow开发,它天生具备tensorflow xla编译能力,利用kernel的fuse提升性能,这部分内容可以参考XLA Overview。xla主要面向tensorflow内置的kernel,能发挥的场景是在线预测模型算分。但是对于用户自己
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS6,CentOS7官方镜像安装Oracle11G
- CentOS8编译安装MySQL8.0.19
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- SpringBoot2整合Redis,开启缓存,提高访问速度