使用 PHP 手动解析原始多部分/表单数据数据

Posted

技术标签:

【中文标题】使用 PHP 手动解析原始多部分/表单数据数据【英文标题】:Manually parse raw multipart/form-data data with PHP 【发布时间】:2011-07-25 22:14:08 【问题描述】:

我似乎找不到这个问题的真正答案,所以我开始了:

如何在 php 中解析 multipart/form-data 格式的原始 HTTP 请求数据?我知道如果格式正确,原始 POST 会自动解析,但我所指的数据来自 PUT 请求,PHP 不会自动解析该请求。数据是多部分的,看起来像:

------------------------------b2449e94a11c
Content-Disposition: form-data; name="user_id"

3
------------------------------b2449e94a11c
Content-Disposition: form-data; name="post_id"

5
------------------------------b2449e94a11c
Content-Disposition: form-data; name="image"; filename="/tmp/current_file"
Content-Type: application/octet-stream

�����JFIF���������... a bunch of binary data

我正在使用 libcurl 发送数据(伪代码):

curl_setopt_array(
  CURLOPT_POSTFIELDS => array(
    'user_id' => 3, 
    'post_id' => 5, 
    'image' => '@/tmp/current_file'),
  CURLOPT_CUSTOMREQUEST => 'PUT'
  );

如果我删除 CURLOPT_CUSTOMREQUEST 位,请求将在服务器上作为 POST 处理,一切都被解析得很好。

有没有办法手动调用 PHP 的 HTTP 数据解析器或其他一些不错的方法? 是的,我必须将请求作为 PUT 发送:)

【问题讨论】:

php.net/manual/en/function.http-parse-headers.php 看看这个问题的python版本以获得一些想法:How do I deal with the uploaded file data manually?。基本上你只需要拆分二进制数据,重新组合它们并重建原始文件。 解析简单的PDF表单,试试***.com/questions/46515906/… 【参考方案1】:

你看过fopen("php://input", "r")来解析内容吗?

标题也可以找到$_SERVER['HTTP_*'],名称总是大写,破折号变成下划线,例如$_SERVER['HTTP_ACCEPT_LANGUAGE']

【讨论】:

fopen('php://input') 只会读取内容,不会解析它?我希望解析的值不在 $_SERVER 变量中。 如何使用 mod_rewrite 将其重定向为 POST 没关系,对仅编码的 R 标志感到困惑。但是您可以通过重构 HTTP 请求使用 PHP 重定向它,但将其修改为 POST 请求并调用另一个脚本来解析请求。 如何将请求重写为 POST?这必须发生在服务器上。 好吧,您可以在端口 80 上打开一个到服务器的套接字并将请求提供给它。可以使用 readfile 将响应发送回客户端。请添加 Connection: close 标头以在处理请求后关闭连接。【参考方案2】:

编辑 - 请先阅读: 这个答案在 7 年后仍然经常被点击。从那以后我再也没有使用过这段代码,也不知道这些天是否有更好的方法。请查看下面的 cmets 并知道此代码在许多情况下不起作用。使用风险自负。

--

好的,根据 Dave 和 Everts 的建议,我决定手动解析原始请求数据。在搜索了大约一天后,我没有找到任何其他方法来做到这一点。

我从这个thread 得到了一些帮助。我没有像在引用的线程中那样篡改原始数据的运气,因为这会破坏正在上传的文件。所以这都是正则表达式。这没有经过很好的测试,但似乎适用于我的工作案例。事不宜迟,希望有一天这可能对其他人有所帮助:

function parse_raw_http_request(array &$a_data)

  // read incoming data
  $input = file_get_contents('php://input');

  // grab multipart boundary from content type header
  preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches);
  $boundary = $matches[1];

  // split content by boundary and get rid of last -- element
  $a_blocks = preg_split("/-+$boundary/", $input);
  array_pop($a_blocks);

  // loop data blocks
  foreach ($a_blocks as $id => $block)
  
    if (empty($block))
      continue;

    // you'll have to var_dump $block to understand this and maybe replace \n or \r with a visibile char

    // parse uploaded files
    if (strpos($block, 'application/octet-stream') !== FALSE)
    
      // match "name", then everything after "stream" (optional) except for prepending newlines 
      preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches);
    
    // parse all other fields
    else
    
      // match "name" and optional value in between newline sequences
      preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches);
    
    $a_data[$matches[1]] = $matches[2];
          

引用使用(以免过多复制数据):

$a_data = array();
parse_raw_http_request($a_data);
var_dump($a_data);

【讨论】:

如果 post 变量包含数组,此函数将不起作用。例如,名称“value[id]”将无法正确解析。内容处置:表单数据; name="elements[_itemname][value]" 内容配置:表单数据; name="array[value]" -- 两者都不能使用。 确实如此。在我的情况下,我不需要嵌套数组。 谢谢。这对我有很大帮助。刚刚修改为通过这些部分之间的两个换行符而不是 Content-Type 来分隔标题/内容。我认为这更好地涵盖了标准 @Chris 我做了一个修改版本来覆盖嵌套数组,这里是代码gist.github.com/cwhsu1984/3419584ad31ce12d2ad5fed6155702e2 不幸的是,解析 HTTP 数据比这段代码复杂得多。它可能在某些情况下有效,但在许多其他情况下无效。例如,实际内容之前可以有多行(例如“Content-Length:XXX”,此代码无法处理。边界的破折号数量可能因 CONTENT_TYPE 和输入内容而异。还有代码不处理存在但没有值的键。【参考方案3】:

我很惊讶没有人提到parse_strmb_parse_str

$result = [];
$rawPost = file_get_contents('php://input');
mb_parse_str($rawPost, $result);
var_dump($result);

http://php.net/manual/en/function.mb-parse-str.php

【讨论】:

我想这对我不起作用,因为我使用的是multipart/form-data Content-Type 形式的二进制文件。 FWMC 这个问题专门针对 MIME 类型为 multipart/form-data 的请求,而不是 application/x-www-form-urlencoded,这正是 parse_str() 的用途。【参考方案4】:

我使用了Chris 的示例函数并添加了一些需要的功能,例如R Porter 需要$_FILES 的数组。希望它可以帮助一些人。

这是class 和示例usage

<?php
include_once('class.stream.php');

$data = array();

new stream($data);

$_PUT = $data['post'];
$_FILES = $data['file'];

/* Handle moving the file(s) */
if (count($_FILES) > 0) 
    foreach($_FILES as $key => $value) 
        if (!is_uploaded_file($value['tmp_name'])) 
            /* Use getimagesize() or fileinfo() to validate file prior to moving here */
            rename($value['tmp_name'], '/path/to/uploads/'.$value['name']);
         else 
            move_uploaded_file($value['tmp_name'], '/path/to/uploads/'.$value['name']);
        
    

【讨论】:

【参考方案5】:

我怀疑最好的方法是“自己做”,尽管您可能会在使用类似(如果不完全相同)格式的多部分电子邮件解析器中找到灵感。

从 Content-Type HTTP 标头中获取边界,并使用它来分解请求的各个部分。如果请求非常大,请记住您可能会将整个请求存储在内存中,甚至可能存储多次。

相关的 RFC 是 RFC2388,幸运的是它很短。

【讨论】:

嗯,Dave Kok 也是这么写的。我想我得去看看。问题是,我的请求内容看起来与我期望的内容类型边界不太一样。我在最初的问题中粘贴了一些内容。你会知道为什么会这样吗? 实际边界不在每个部分的标题中,而是在顶部标题中。所以这将无法通过 php://input 访问,但就像 dave 提到的那样,它应该在 $_SERVER['HTTP_CONTENT_TYPE'] 或 $_SERVER['CONTENT_TYPE'] 属性中。【参考方案6】:

我没有过多处理 http 标头,但发现这段代码可能会有所帮助

function http_parse_headers( $header )

    $retVal = array();
    $fields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $header));
    foreach( $fields as $field ) 
        if( preg_match('/([^:]+): (.+)/m', $field, $match) ) 
            $match[1] = preg_replace('/(?<=^|[\x09\x20\x2D])./e', 'strtoupper("\0")', strtolower(trim($match[1])));
            if( isset($retVal[$match[1]]) ) 
                $retVal[$match[1]] = array($retVal[$match[1]], $match[2]);
             else 
                $retVal[$match[1]] = trim($match[2]);
            
        
    
    return $retVal;

来自http://php.net/manual/en/function.http-parse-headers.php

【讨论】:

谢谢。我今天早些时候看到了这个功能,但结果并没有多大用处。您是否成功使用了该功能?

以上是关于使用 PHP 手动解析原始多部分/表单数据数据的主要内容,如果未能解决你的问题,请参考以下文章

解析多部分/表单数据,从请求后接收

PHP多部分表单数据PUT请求?

WebAPI 无法解析多部分/表单数据帖子

解析Servlet中传入的多部分/表单数据参数的便捷方法[重复]

来自 C# 客户端的多部分表单

在 iphone 中使用多部分表单数据发布图像和其他数据