PHP 原始碼探祕 – 在解析外部變數時的一個問題

周夢康發表於2019-02-20

安利

原文:我的個人部落格 mengkang.net/1301.html
工作了兩三年,技術停滯不前,迷茫沒有方向,安利一波我的直播 [PHP 進階之路][1]

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 "
";
var_export($_POST);
echo "
";
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	%s
",var);
    printf("var_len	%zu
",var_len);

}
複製程式碼

根據php_register_variable_ex裡面的規則:

  • name裡面的.都被替換成_
  • name裡遇到[則認為是陣列,陣列的key為[前面的字串,後面的都被捨去。

上面我模擬了表單提交一個nameid 1.2[]_3時,輸出結果就是

var	id_1_2
var_len	6
複製程式碼

思考為什麼

上面的替換規則在官方手冊中有說明

php.net/manual/zh/l…
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的值直接丟失了。

所以

  1. php在接受這樣命名的(foo[]boo)外部變數名是不符合規範的,手冊文件需要補全
  2. php在接受這樣不符合命名規範的(foo[]boo)外部變數的時候是強制轉換成陣列,還是直接丟棄呢?

相關文章