【问题标题】:userland multipart/form-data handler用户态多部分/表单数据处理程序
【发布时间】:2011-07-30 11:38:26
【问题描述】:

我正在寻找一个插入式包含脚本/类,它剖析multipart/form-data 并从中填充$_POST(+raw) 和$_FILES。通常 PHP 会自己做。但是因为自动处理对我来说不够用并且使php://input 无法访问[1] 我可能会使用这样的东西来防止这种情况发生:

RewriteRule .* - [E=CONTENT_TYPE:noparsing/for-you-php]
Does not work. Actual solution requires mod_headers and RequestHeader set...

提取过程可能没有那么复杂。但我宁愿使用经过充分测试的解决方案。最重要的是,我更喜欢使用fgets 进行拆分的实现,并模仿$_FILES 紧密有效地处理。找到二进制有效负载的结尾对我来说似乎相当棘手,特别是当您必须剥离 \r\n 但可能遇到只发送 \n 的客户端时(不允许,但可能)。

我确定存在这样的事情。但我很难用谷歌搜索它。有谁知道一个实现? (PEAR::mimeDecode 可以被破解来处理表单数据,但它会占用内存。)

用例简而言之:需要保留原始字段名称(包括空格和特殊字符)以进行日志记录,但不能始终避免文件上传。


出于装饰目的,这就是 POST 请求的外观:

POST / HTTP/1.1
Host: localhost:8000
Content-Length: 17717
Content-Type: multipart/form-data; boundary=----------3wCuBwquE9P7A4OEylndVx

\r\n\r\n 序列之后,multipart/ 有效负载如下所示:

------------3wCuBwquE9P7A4OEylndVx
Content-Disposition: form-data; name="_charset_"

windows-1252
------------3wCuBwquE9P7A4OEylndVx
Content-Disposition: form-data; name=" text field \\ 1 \";inject=1"

text1 te twj sakfkl
------------3wCuBwquE9P7A4OEylndVx
Content-Disposition: form-data; name="file"; filename="dial.png"
Content-Type: image/png

IPNG Z @@@MIHDR@@B`@@B;HF@@@-'.e@@@AsRGB@.N\i@@@FbKGD@?@?@? ='S@@@     
@@@GtIMEGYAAU,#}BRU@@@YtEXtComment@Created with GIMPWANW@@ @IDATxZl]w|

【问题讨论】:

  • 这可能会成为一个赏金问题..
  • 能保证multipart中的每个MIME部分都有一个Content-Length吗?我不记得规范是否需要这个。我想它会的。
  • 令人遗憾的是规范RFC2388 根本没有提到Content-Length。虽然我假设大多数当前浏览器都这样做(并且至少使用 base64 编码),但我实际上是在尝试支持更古怪的客户端。 (编辑:不,甚至 Opera 都没有。)
  • “$argc”变量不包含原始帖子数据吗?还是空字符串 $_POST['']?为什么说自动处理不足? php://stdio 如何为您工作?
  • @David:对于 POST 请求,argcargv 都不存在。而php://stdinphp://input 是空的。 PHP 在main/rfc1867.c 中吸收了完整的 POST 正文。这就是它无法访问的原因。我的问题是前导空格被剥离,许多 ASCII 字符转换为 _ 下划线。

标签: php multipartform-data


【解决方案1】:

已经很晚了,我目前无法对此进行测试,但以下应该可以满足您的要求:

//$boundary = null;

if (is_resource($input = fopen('php://input', 'rb')) === true)
{

    while ((feof($input) !== true) && (($line = fgets($input)) !== false))
    {
        if (isset($boundary) === true)
        {
            $content = null;

            while ((feof($input) !== true) && (($line = fgets($input)) !== false))
            {
                $line = trim($line);

                if (strlen($line) > 0)
                {
                    $content .= $line . ' ';
                }

                else if (empty($line) === true)
                {
                    if (stripos($content, 'name=') !== false)
                    {
                        $name = trim(stripcslashes(preg_replace('~.*name="?(.+)"?.*~i', '$1', $content)));

                        if (stripos($content, 'Content-Type:') !== false)
                        {
                            $tmpname = tempnam(sys_get_temp_dir(), '');

                            if (is_resource($temp = fopen($tmpname, 'wb')) === true)
                            {
                                while ((feof($input) !== true) && (($line = fgets($input)) !== false) && (strpos($line, $boundary) !== 0))
                                {
                                    fwrite($temp, preg_replace('~(?:\r\n|\n)$~', '', $line));
                                }

                                fclose($temp);
                            }

                            $FILES[$name] = array
                            (
                                'name' => trim(stripcslashes(preg_replace('~.*filename="?(.+)"?.*~i', '$1', $content))),
                                'type' => trim(preg_replace('~.*Content-Type: ([^\s]*).*~i', '$1', $content)),
                                'size' => sprintf('%u', filesize($tmpname)),
                                'tmp_name' => $tmpname,
                                'error' => UPLOAD_ERR_OK,
                            );
                        }

                        else
                        {
                            $result = null;

                            while ((feof($input) !== true) && (($line = fgets($input)) !== false) && (strpos($line, $boundary) !== 0))
                            {
                                $result .= preg_replace('~(?:\r\n|\n)$~', '', $line);
                            }

                            if (array_key_exists($name, $POST) === true)
                            {
                                if (is_array($POST[$name]) === true)
                                {
                                    $POST[$name][] = $result;
                                }

                                else
                                {
                                    $POST[$name] = array($POST[$name], $result);
                                }
                            }

                            else
                            {
                                $POST[$name] = $result;
                            }
                        }
                    }

                    if (strpos($line, $boundary) === 0)
                    {
                        //break;
                    }
                }
            }
        }

        else if ((is_null($boundary) === true) && (strpos($line, 'boundary=') !== false))
        {
            $boundary = "--" . trim(preg_replace('~.*boundary="?(.+)"?.*~i', '$1', $line));
        }
    }

    fclose($input);
}

echo '<pre>';
print_r($POST);
echo '</pre>';

echo '<hr />';

echo '<pre>';
print_r($FILES);
echo '</pre>';

【讨论】:

  • 也无法测试。不过看起来可行。 -- 我忘记了任务的一些问题,nameley 重复 name[]name[]name[] 请求变量。而且我的 POST 描述具有误导性,boundary= 绝不是身体的一部分。但至少fgets 方法看起来是可行的!
  • @mario:修复了正则表达式中的一个错字并添加了对 $POST 请求变量的重复键支持。你能澄清一下你说边界永远不是身体的一部分是什么意思吗?
  • 谢谢!我修复了我的问题示例。 ;boundary= 仅出现在 $_SERVER["CONTENT_TYPE"] 中。 ://input 正文实际上从第一个 ------whatever 开始。但我已经事先用一个小的 preg_match() 对其进行了调整;我认为这很有效,因为您的代码有远见地使用 isset() 测试了 $boundary。我要花点时间进行彻底的测试。但是,我认为这看起来还不错,我可以根据其他奇怪的需求进行调整。
  • @mario:哦,我明白了。我在之前的编辑中忘记删除while,现在修复它。至于$FILES 中的重复键,我每次都假设$FILES[$name][] = array(),而不仅仅是$FILES[$name] = array()。实际上,我发现以这种方式横穿 $_FILES 超级全局更容易(请参阅github.com/alixaxel/phunction/blob/…)。
  • 还有一些错误。但我只接受其中一个的指责! :} $boundary 实际上在实际正文中使用了另外两个-- 破折号。并且\r\n 剥离应该只发生在每个可变部分/文件的末尾。还必须禁用break;,因为它跳过了部分,不确定它的目的是什么。但不管怎样,fgets 方法毕竟似乎有效且可行。
【解决方案2】:

也许一个新的 php.ini 指令 enable_post_data_reading 会有所帮助,但它似乎是在 PHP 5.4 中添加的,我仍然有早期版本,所以无法测试它:(

来自PHP Manual

enable_post_data_reading布尔值

禁用此选项会导致 $_POST 和 $_FILES 不被填充。读取 postdata 的唯一方法是 然后通过 php://input 流包装器。这对 代理请求或以高效的内存处理 POST 数据 时尚。

【讨论】:

  • 似乎在这里工作,可能需要用嗅探器验证它是否真的是原始
【解决方案3】:

阅读 cmets,如何在 POST 之前对数据进行编码?让客户端发送 POST 数据为 UTF8 甚至 URLencoded,那么丢失的 ASCII 字符会在不编写自己的 POST 处理程序的情况下传输,这很可能会引入自己的 bug...

【讨论】:

  • 不,不能那样做。这是我在这里的奇怪要求。我需要拦截一个普通的 POST 请求。我对客户没有影响,需要支持标准表格。我可以通过使用application/x-www-urlencoded POST 而不是multipart/form-data 来逃避整个问题。但这违背了目的并使文件上传变得不可能。我将不得不采用解决方法及其所有潜在问题。
猜你喜欢
  • 2012-11-17
  • 2021-02-20
  • 1970-01-01
  • 1970-01-01
  • 2021-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-27
相关资源
最近更新 更多