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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/9464935/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-26 06:53:03  来源:igfitidea点击:

PHP multipart form data PUT request?

phphttpcodeigniterrestcurl

提问by Josh

I'm writing a RESTful API. I'm having trouble with uploading images using the different verbs.

我正在编写一个 RESTful API。我在使用不同的动词上传图像时遇到问题。

Consider:

考虑:

I have an object which can be created/modified/deleted/viewed via a post/put/delete/get request to a URL. The request is multi part form when there is a file to upload, or application/xml when there's just text to process.

我有一个对象,可以通过对 URL 的 post/put/delete/get 请求创建/修改/删除/查看。当有文件要上传时,请求是多部分形式,当只有文本要处理时,请求是应用程序/xml。

To handle the image uploads which are associated with the object I am doing something like:

为了处理与对象关联的图像上传,我正在执行以下操作:

    if(isset($_FILES['userfile'])) {
        $data = $this->image_model->upload_image();
        if($data['error']){
            $this->response(array('error' => $error['error']));
        }
        $xml_data = (array)simplexml_load_string( urldecode($_POST['xml']) );           
        $object = (array)$xml_data['object'];
    } else {
        $object = $this->body('object');
    }

The major problem here is when trying to handle a put request, obviously $_POST doesn't contain the put data (as far as I can tell!).

这里的主要问题是在尝试处理放置请求时,显然 $_POST 不包含放置数据(据我所知!)。

For reference this is how I'm building the requests:

作为参考,这是我构建请求的方式:

curl -F userfile=@./image.png -F xml="<xml><object>stuff to edit</object></xml>" 
  http://example.com/object -X PUT

Does anyone have any ideas how I can access the xmlvariable in my PUT request?

有没有人知道如何访问xmlPUT 请求中的变量?

回答by netcoder

First of all, $_FILESis not populated when handling PUT requests. It is only populated by PHP when handling POST requests.

首先,$_FILES在处理 PUT 请求时不会填充。它仅在处理 POST 请求时由 PHP 填充。

You need to parse it manually. That goes for "regular" fields as well:

您需要手动解析它。这也适用于“常规”字段:

// Fetch content and determine boundary
$raw_data = file_get_contents('php://input');
$boundary = substr($raw_data, 0, strpos($raw_data, "\r\n"));

// Fetch each part
$parts = array_slice(explode($boundary, $raw_data), 1);
$data = array();

foreach ($parts as $part) {
    // If this is the last part, break
    if ($part == "--\r\n") break; 

    // Separate content from headers
    $part = ltrim($part, "\r\n");
    list($raw_headers, $body) = explode("\r\n\r\n", $part, 2);

    // Parse the headers list
    $raw_headers = explode("\r\n", $raw_headers);
    $headers = array();
    foreach ($raw_headers as $header) {
        list($name, $value) = explode(':', $header);
        $headers[strtolower($name)] = ltrim($value, ' '); 
    } 

    // Parse the Content-Disposition to get the field name, etc.
    if (isset($headers['content-disposition'])) {
        $filename = null;
        preg_match(
            '/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/', 
            $headers['content-disposition'], 
            $matches
        );
        list(, $type, $name) = $matches;
        isset($matches[4]) and $filename = $matches[4]; 

        // handle your fields here
        switch ($name) {
            // this is a file upload
            case 'userfile':
                 file_put_contents($filename, $body);
                 break;

            // default for all other files is to populate $data
            default: 
                 $data[$name] = substr($body, 0, strlen($body) - 2);
                 break;
        } 
    }

}

At each iteration, the $dataarray will be populated with your parameters, and the $headersarray will be populated with the headers for each part (e.g.: Content-Type, etc.), and $filenamewill contain the original filename, if supplied in the request and is applicable to the field.

在每次迭代中,该$data数组将填充您的参数,并且该$headers数组将填充每个部分的标头(例如:Content-Type等),$filename并将包含原始文件名(如果在请求中提供并且适用于场地。

Take note the above will only work for multipartcontent types. Make sure to check the request Content-Typeheader before using the above to parse the body.

请注意,以上仅适用于multipart内容类型。Content-Type在使用上述内容解析正文之前,请务必检查请求标头。

回答by GreenDot

Please don't delete this again, it's helpful to a majority of people coming here! All previous answers were partial answers that don't cover the solution as a majority of people asking this question would want.

请不要再次删除此内容,它对大多数来这里的人有帮助!以前的所有答案都是部分答案,不包括大多数提出此问题的人想要的解决方案。

This takes what has been said above and additionally handles multiple file uploads and places them in $_FILES as someone would expect. To get this to work, you have to add 'Script PUT /put.php' to your Virtual Host for the project per Documentation. I also suspect I'll have to setup a cron to cleanup any '.tmp' files.

这需要上面所说的内容,并另外处理多个文件上传并将它们放在 $_FILES 中,正如人们所期望的那样。要使其正常工作,您必须根据Documentation将“Script PUT /put.php”添加到项目的虚拟主机中。我还怀疑我必须设置一个 cron 来清理任何“.tmp”文件。

private function _parsePut(  )
{
    global $_PUT;

    /* PUT data comes in on the stdin stream */
    $putdata = fopen("php://input", "r");

    /* Open a file for writing */
    // $fp = fopen("myputfile.ext", "w");

    $raw_data = '';

    /* Read the data 1 KB at a time
       and write to the file */
    while ($chunk = fread($putdata, 1024))
        $raw_data .= $chunk;

    /* Close the streams */
    fclose($putdata);

    // Fetch content and determine boundary
    $boundary = substr($raw_data, 0, strpos($raw_data, "\r\n"));

    if(empty($boundary)){
        parse_str($raw_data,$data);
        $GLOBALS[ '_PUT' ] = $data;
        return;
    }

    // Fetch each part
    $parts = array_slice(explode($boundary, $raw_data), 1);
    $data = array();

    foreach ($parts as $part) {
        // If this is the last part, break
        if ($part == "--\r\n") break;

        // Separate content from headers
        $part = ltrim($part, "\r\n");
        list($raw_headers, $body) = explode("\r\n\r\n", $part, 2);

        // Parse the headers list
        $raw_headers = explode("\r\n", $raw_headers);
        $headers = array();
        foreach ($raw_headers as $header) {
            list($name, $value) = explode(':', $header);
            $headers[strtolower($name)] = ltrim($value, ' ');
        }

        // Parse the Content-Disposition to get the field name, etc.
        if (isset($headers['content-disposition'])) {
            $filename = null;
            $tmp_name = null;
            preg_match(
                '/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/',
                $headers['content-disposition'],
                $matches
            );
            list(, $type, $name) = $matches;

            //Parse File
            if( isset($matches[4]) )
            {
                //if labeled the same as previous, skip
                if( isset( $_FILES[ $matches[ 2 ] ] ) )
                {
                    continue;
                }

                //get filename
                $filename = $matches[4];

                //get tmp name
                $filename_parts = pathinfo( $filename );
                $tmp_name = tempnam( ini_get('upload_tmp_dir'), $filename_parts['filename']);

                //populate $_FILES with information, size may be off in multibyte situation
                $_FILES[ $matches[ 2 ] ] = array(
                    'error'=>0,
                    'name'=>$filename,
                    'tmp_name'=>$tmp_name,
                    'size'=>strlen( $body ),
                    'type'=>$value
                );

                //place in temporary directory
                file_put_contents($tmp_name, $body);
            }
            //Parse Field
            else
            {
                $data[$name] = substr($body, 0, strlen($body) - 2);
            }
        }

    }
    $GLOBALS[ '_PUT' ] = $data;
    return;
}

回答by Pham Quang

For whom using Apiato (Laravel) framework: create new Middleware like file below, then declair this file in your laravel kernel file within the protected $middlewareGroups variable (inside web or api, whatever you want) like this:

对于使用 Apiato (Laravel) 框架的人:创建新的中间件,如下面的文件,然后在受保护的 $middlewareGroups 变量(在 web 或 api 中,无论你想要什么)的 Laravel 内核文件中声明这个文件,如下所示:

protected $middlewareGroups = [
    'web' => [],
    'api' => [HandlePutFormData::class],
];


<?php

namespace App\Ship\Middlewares\Http;

use Closure;
use Symfony\Component\HttpFoundation\ParameterBag;

/**
 * @author Quang Pham
 */
class HandlePutFormData
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Closure                 $next
     *
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if ($request->method() == 'POST' or $request->method() == 'GET') {
            return $next($request);
        }
        if (preg_match('/multipart\/form-data/', $request->headers->get('Content-Type')) or
            preg_match('/multipart\/form-data/', $request->headers->get('content-type'))) {
            $parameters = $this->decode();

            $request->merge($parameters['inputs']);
            $request->files->add($parameters['files']);
        }

        return $next($request);
    }

    public function decode()
    {
        $files = [];
        $data  = [];
        // Fetch content and determine boundary
        $rawData  = file_get_contents('php://input');
        $boundary = substr($rawData, 0, strpos($rawData, "\r\n"));
        // Fetch and process each part
        $parts = $rawData ? array_slice(explode($boundary, $rawData), 1) : [];
        foreach ($parts as $part) {
            // If this is the last part, break
            if ($part == "--\r\n") {
                break;
            }
            // Separate content from headers
            $part = ltrim($part, "\r\n");
            list($rawHeaders, $content) = explode("\r\n\r\n", $part, 2);
            $content = substr($content, 0, strlen($content) - 2);
            // Parse the headers list
            $rawHeaders = explode("\r\n", $rawHeaders);
            $headers    = array();
            foreach ($rawHeaders as $header) {
                list($name, $value) = explode(':', $header);
                $headers[strtolower($name)] = ltrim($value, ' ');
            }
            // Parse the Content-Disposition to get the field name, etc.
            if (isset($headers['content-disposition'])) {
                $filename = null;
                preg_match(
                    '/^form-data; *name="([^"]+)"(; *filename="([^"]+)")?/',
                    $headers['content-disposition'],
                    $matches
                );
                $fieldName = $matches[1];
                $fileName  = (isset($matches[3]) ? $matches[3] : null);
                // If we have a file, save it. Otherwise, save the data.
                if ($fileName !== null) {
                    $localFileName = tempnam(sys_get_temp_dir(), 'sfy');
                    file_put_contents($localFileName, $content);
                    $files = $this->transformData($files, $fieldName, [
                        'name'     => $fileName,
                        'type'     => $headers['content-type'],
                        'tmp_name' => $localFileName,
                        'error'    => 0,
                        'size'     => filesize($localFileName)
                    ]);
                    // register a shutdown function to cleanup the temporary file
                    register_shutdown_function(function () use ($localFileName) {
                        unlink($localFileName);
                    });
                } else {
                    $data = $this->transformData($data, $fieldName, $content);
                }
            }
        }
        $fields = new ParameterBag($data);

        return ["inputs" => $fields->all(), "files" => $files];
    }

    private function transformData($data, $name, $value)
    {
        $isArray = strpos($name, '[]');
        if ($isArray && (($isArray + 2) == strlen($name))) {
            $name = str_replace('[]', '', $name);
            $data[$name][]= $value;
        } else {
            $data[$name] = $value;
        }
        return $data;
    }
}

Pls note: Those codes above not all mine, some from above comment, some modified by me.

请注意:上面的那些代码不是我的,一些来自上面的评论,一些是我修改的。

回答by sudip

Quoting netcoder reply : "Take note the above will only work for multipart content types"

引用 netcoder 回复:“请注意,上述内容仅适用于多部分内容类型”

To work with any content type I have added the following lines to Mr. netcoder's solution :

为了处理任何内容类型,我在 netcoder 先生的解决方案中添加了以下几行:

   // Fetch content and determine boundary
   $raw_data = file_get_contents('php://input');
   $boundary = substr($raw_data, 0, strpos($raw_data, "\r\n"));

   /*...... My edit --------- */
    if(empty($boundary)){
        parse_str($raw_data,$data);
        return $data;
    }
   /* ........... My edit ends ......... */
    // Fetch each part
    $parts = array_slice(explode($boundary, $raw_data), 1);
    $data = array();
    ............
    ...............