Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multipart handling #13

Merged
merged 6 commits into from
Aug 10, 2015
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions src/MultipartParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
<?php

namespace React\Http;

/**
* Parse a multipart body
*
* Original source is from https://gist.github.com/jas-/5c3fdc26fedd11cb9fb5
*
* @author [email protected]
* @author [email protected]
* @license http://www.gnu.org/licenses/gpl.html GPL License 3
*/
class MultipartParser
{
/**
* @var string
*/
protected $input;

/**
* @var string
*/
protected $boundary;

/**
* Contains the resolved posts
*
* @var array
*/
protected $post = [];

/**
* Contains the resolved files
*
* @var array
*/
protected $files = [];

/**
* @param $input
* @param $boundary
*/
public function __construct($input, $boundary)
{
$this->input = $input;
$this->boundary = $boundary;
}

/**
* @return array
*/
public function getPost()
{
return $this->post;
}

/**
* @return array
*/
public function getFiles()
{
return $this->files;
}

/**
* Do the actual parsing
*/
public function parse()
{
$blocks = $this->split($this->boundary);

foreach ($blocks as $value) {
if (empty($value)) {
continue;
}

$this->parseBlock($value);
}
}

/**
* @param $boundary string
* @returns Array
*/
protected function split($boundary)
{
$boundary = preg_quote($boundary);
$result = preg_split("/\\-+$boundary/", $this->input);
array_pop($result);
return $result;
}

/**
* Decide if we handle a file, post value or octet stream
*
* @param $string string
* @returns void
*/
protected function parseBlock($string)
{
if (strpos($string, 'filename') !== false) {
$this->file($string);
return;
}

// This may never be called, if an octet stream
// has a filename it is catched by the previous
// condition already.
if (strpos($string, 'application/octet-stream') !== false) {
$this->octetStream($string);
return;
}

$this->post($string);
}

/**
* Parse a raw octet stream
*
* @param $string
* @return array
*/
protected function octetStream($string)
{
preg_match('/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s', $string, $match);

$this->addResolved('post', $match[1], $match[2]);
}

/**
* Parse a file
*
* @param $string
* @return array
*/
protected function file($string)
{
preg_match('/name=\"([^\"]*)\"; filename=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match);
preg_match('/Content-Type: (.*)?/', $match[3], $mime);

$content = preg_replace('/Content-Type: (.*)[^\n\r]/', '', $match[3]);
$content = ltrim($content, "\r\n");

$path = tempnam(sys_get_temp_dir(), "php");
$err = file_put_contents($path, $content);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filesystem interactions block. We can't have these in the library.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for your input. but then how could we handle file uploads ?
Keeping them in memory is too dangerous because of the size these uploads could be.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the Single Responsibility Principle the library should be all things handling and serving HTTP. How to transfer a file is defined in the HTTP specification but not how to save it. I think at this point instead of writing to a file we should provide indirection for the user to handle the file in chunks.

For example if I were implementing this I think I would want a callback to receive a file in chunks and as I received each chunk I would pipe it to a file using react/filesystem or perhaps pipe it to a worker script that writes to disk (but blocks).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the idea, I'll figure something out and update the pull request.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imho it would be better when you use streams and pass those along. That way the developer can decide what to do with the contents of the stream.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You think one stream per fileupload or the raw stream ?

I'll look how I can do that, I'm not a stream expert

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A stream per fileupload. Ping me on IRC or on twitter if you have questions about that. Happy to help :).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have suggestion about how to do that:

  • add one more argument (callable or factory) for handling file uploads to server constructor
  • then when it comes to file handling we just call callable or factory (with arguments like file name, mime type) and pass everything to method of object it returned (if there was no callable or factory - just ignore uploaded file, it is too expensive to store it in memory)
  • after whole request parsed we'll have access to array with all objects we've created for uploaded files

What you think about such approach?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your ideas, I will try to make it this week.


$data = [
'name' => $match[2],
'type' => trim($mime[1]),
'tmp_name' => $path,
'error' => ($err === false) ? UPLOAD_ERR_NO_FILE : UPLOAD_ERR_OK,
'size' => filesize($path),
];

$this->addResolved('files', $match[1], $data);
}

/**
* Parse POST values
*
* @param $string
* @return array
*/
protected function post($string)
{
preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match);

$this->addResolved('post', $match[1], $match[2]);
}

/**
* Put the file or post where it belongs,
* The key names can be simple, or containing []
* it can also be a named key
*
* @param $type
* @param $key
* @param $content
*/
protected function addResolved($type, $key, $content)
{
if (preg_match('/^(.*)\[(.*)\]$/i', $key, $tmp)) {
if (!empty($tmp[2])) {
$this->{$type}[$tmp[1]][$tmp[2]] = $content;
} else {
$this->{$type}[$tmp[1]][] = $content;
}
} else {
$this->{$type}[$key] = $content;
}
}
}
52 changes: 48 additions & 4 deletions src/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,25 @@ class Request extends EventEmitter implements ReadableStreamInterface
{
private $readable = true;
private $method;
private $path;
private $url;
private $query;
private $httpVersion;
private $headers;
private $body;
private $post = [];
private $files = [];

// metadata, implicitly added externally
public $remoteAddress;

public function __construct($method, $path, $query = array(), $httpVersion = '1.1', $headers = array())
public function __construct($method, $url, $query = array(), $httpVersion = '1.1', $headers = array(), $body = '')
{
$this->method = $method;
$this->path = $path;
$this->url = $url;
$this->query = $query;
$this->httpVersion = $httpVersion;
$this->headers = $headers;
$this->body = $body;
}

public function getMethod()
Expand All @@ -35,7 +39,12 @@ public function getMethod()

public function getPath()
{
return $this->path;
return $this->url['path'];
}

public function getUrl()
{
return $this->url;
}

public function getQuery()
Expand All @@ -53,6 +62,41 @@ public function getHeaders()
return $this->headers;
}

public function getBody()
{
return $this->body;
}

public function setBody($body)
{
$this->body = $body;
}

public function getFiles()
{
return $this->files;
}

public function setFiles($files)
{
$this->files = $files;
}

public function getPost()
{
return $this->post;
}

public function setPost($post)
{
$this->post = $post;
}

public function getRemoteAddress()
{
return $this->remoteAddress;
}

public function expectsContinue()
{
return isset($this->headers['Expect']) && '100-continue' === $this->headers['Expect'];
Expand Down
57 changes: 0 additions & 57 deletions src/RequestHeaderParser.php

This file was deleted.

Loading