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 NumTypeDisp Enb AddressWhat 1breakpointkeep y0x0000000000812877 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 70if (track_vars_array && Z_TYPE_P(track_vars_array) == IS_ARRAY) { (gdb) bt #0php_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 #10x00000000005af0d1 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 #20x00000000008135d0 in add_post_var (arr=0x7ffff23dce50, var=0x7ffff23dcda0, eof=<value optimized out>) at /data/soft/php-7.1.10/main/php_variables.c:308 #30x0000000000813ce6 in add_post_vars (content_type_dup=<value optimized out>, arg=0x7ffff23dce50) at /data/soft/php-7.1.10/main/php_variables.c:324 #4php_std_post_handler (content_type_dup=<value optimized out>, arg=0x7ffff23dce50) at /data/soft/php-7.1.10/main/php_variables.c:361 #50x000000000080cfe0 in sapi_handle_post (arg=<value optimized out>) at /data/soft/php-7.1.10/main/SAPI.c:174 #60x00000000008133cf 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 #70x000000000066c581 in mbstr_treat_data (arg=0, str=0x0, destArray=0x0) at /data/soft/php-7.1.10/ext/mbstring/mb_gpc.c:69 #80x0000000000812463 in php_auto_globals_create_post (name=0x7fb5b1ddf768) at /data/soft/php-7.1.10/main/php_variables.c:720 #90x000000000084125f 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
時,輸出結果就是
varid_1_2 var_len6
思考
上面的替換規則在官方手冊中應該有說明,果然找到
ofollow,noindex" target="_blank">http://php.net/manual/zh/language.variables.external.php
Dots and spaces in variable names are converted to underscores.
但是沒有看到命名中關於不使用[]
後連線字串的說明。
難道是因為extract
原因,如果陣列key
裡面有[]
,則沒辦法正常執行了。
$foo["id"] = 1; $foo["id[]_text"] = 2; var_export($foo); extract($foo); var_export(get_defined_vars());
試了以上程式碼,也印證了我的想法id[]_text
的值直接丟失了,所以外部變數傳入時的命名裡面不能是[]
後面再連線字串,否則直接當陣列處理。那算不算是一個不想修復的 bug ?至少文件中缺少了說明。
但是這個命名的規範,也始終沒有找到。