From b3e9253adb7cfeb16171887e3c021df6133b1fe5 Mon Sep 17 00:00:00 2001 From: Cees-Jan Date: Thu, 1 Oct 2015 14:57:32 +0200 Subject: [PATCH 01/64] All request bodies now come in streaming --- src/FormUrlencodedParser.php | 60 ++++++++++ src/MultipartParser.php | 181 +++++++++++++---------------- src/Request.php | 35 ++++-- src/RequestParser.php | 78 +++++-------- src/Server.php | 4 +- tests/FormUrlencodedParserTest.php | 23 ++++ tests/MultipartParserTest.php | 156 +++++++++++++++++-------- tests/RequestParserTest.php | 151 ++++++++++++++++++------ 8 files changed, 445 insertions(+), 243 deletions(-) create mode 100644 src/FormUrlencodedParser.php create mode 100644 tests/FormUrlencodedParserTest.php diff --git a/src/FormUrlencodedParser.php b/src/FormUrlencodedParser.php new file mode 100644 index 00000000..5d4e7be9 --- /dev/null +++ b/src/FormUrlencodedParser.php @@ -0,0 +1,60 @@ +stream = $stream; + $this->request = $request; + + $this->stream->on('data', [$this, 'feed']); + $this->stream->on('close', [$this, 'finish']); + } + + /** + * @param string $data + */ + public function feed($data) + { + $this->buffer .= $data; + + if ( + array_key_exists('Content-Length', $this->request->getHeaders()) && + strlen($this->buffer) >= $this->request->getHeaders()['Content-Length'] + ) { + $this->buffer = substr($this->buffer, 0, $this->request->getHeaders()['Content-Length']); + $this->finish(); + } + } + + public function finish() + { + $this->stream->removeListener('data', [$this, 'feed']); + $this->stream->removeListener('close', [$this, 'finish']); + parse_str(urldecode(trim($this->buffer)), $result); + $this->request->setPost($result); + } +} diff --git a/src/MultipartParser.php b/src/MultipartParser.php index d4c0f4b9..05783f1f 100644 --- a/src/MultipartParser.php +++ b/src/MultipartParser.php @@ -2,6 +2,9 @@ namespace React\Http; +use React\Stream\ReadableStreamInterface; +use React\Stream\ThroughStream; + /** * Parse a multipart body * @@ -13,10 +16,12 @@ */ class MultipartParser { + protected $buffer = ''; + /** * @var string */ - protected $input; + protected $stream; /** * @var string @@ -24,71 +29,55 @@ class MultipartParser protected $boundary; /** - * Contains the resolved posts - * - * @var array - */ - protected $post = []; - - /** - * Contains the resolved files - * - * @var array + * @var Request */ - protected $files = []; + protected $request; /** - * @param $input - * @param $boundary + * @param ReadableStreamInterface $stream + * @param string $boundary + * @param Request $request */ - public function __construct($input, $boundary) + public function __construct(ReadableStreamInterface $stream, $boundary, Request $request) { - $this->input = $input; + $this->stream = $stream; $this->boundary = $boundary; - } + $this->request = $request; - /** - * @return array - */ - public function getPost() - { - return $this->post; - } - - /** - * @return array - */ - public function getFiles() - { - return $this->files; + $this->stream->on('data', [$this, 'feed']); } /** * Do the actual parsing + * + * @param string $data */ - public function parse() + public function feed($data) { - $blocks = $this->split($this->boundary); + $this->buffer .= $data; - foreach ($blocks as $value) { - if (empty($value)) { - continue; - } + $ending = $this->boundary . "--\r\n"; + $endSize = strlen($ending); - $this->parseBlock($value); + if (strpos($this->buffer, $this->boundary) < strrpos($this->buffer, "\r\n\r\n") || substr($this->buffer, -1, $endSize) === $ending) { + $this->processBuffer(); } } - /** - * @param $boundary string - * @returns Array - */ - protected function split($boundary) + protected function processBuffer() { - $boundary = preg_quote($boundary); - $result = preg_split("/\\-+$boundary/", $this->input); - array_pop($result); - return $result; + $chunks = preg_split("/\\-+$this->boundary/", $this->buffer); + $this->buffer = array_pop($chunks); + foreach ($chunks as $chunk) { + $this->parseBlock($chunk); + } + + $lines = explode("\r\n", $this->buffer); + if (isset($lines[1]) && strpos($lines[1], 'filename') !== false) { + $this->file($this->buffer); + $this->buffer = ''; + return; + } } /** @@ -99,15 +88,20 @@ protected function split($boundary) */ protected function parseBlock($string) { - if (strpos($string, 'filename') !== false) { - $this->file($string); + if ($string == '') { + return; + } + + list(, $firstLine) = explode("\r\n", $string); + if (strpos($firstLine, 'filename') !== false) { + $this->file($string, false); 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) { + if (strpos($firstLine, 'application/octet-stream') !== false) { $this->octetStream($string); return; } @@ -125,45 +119,58 @@ protected function octetStream($string) { preg_match('/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s', $string, $match); - $this->addResolved('post', $match[1], $match[2]); + $this->request->emit('post', [$match[1], $match[2]]); } /** * Parse a file * - * @param $string - * @return array + * @param string $firstChunk */ - protected function file($string) + protected function file($firstChunk, $streaming = true) { - preg_match('/name=\"([^\"]*)\"; filename=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match); - preg_match('/Content-Type: (.*)?/', $match[3], $mime); + preg_match('/name=\"([^\"]*)\"; filename=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $firstChunk, $match); + preg_match('/Content-Type: ([^\"]*)/', $match[3], $mime); $content = preg_replace('/Content-Type: (.*)[^\n\r]/', '', $match[3]); $content = ltrim($content, "\r\n"); // Put content in a stream - $stream = fopen('php://memory', 'r+'); - if ($content !== '') { - fwrite($stream, $content); - fseek($stream, 0); + $stream = new ThroughStream(); + + if ($streaming) { + $this->stream->removeListener('data', [$this, 'feed']); + $buffer = ''; + $func = function($data) use (&$func, &$buffer, $stream) { + $buffer .= $data; + if (strpos($buffer, $this->boundary) !== false) { + $chunks = preg_split("/\\-+$this->boundary/", $buffer); + $chunk = array_shift($chunks); + $stream->end($chunk); + + $this->stream->removeListener('data', $func); + $this->stream->on('data', [$this, 'feed']); + + $this->stream->emit('data', [implode($this->boundary, $chunks)]); + return; + } + + if (strlen($buffer) >= strlen($this->boundary) * 3) { + $stream->write($buffer); + $buffer = ''; + } + }; + $this->stream->on('data', $func); } - $data = [ - 'name' => $match[2], - 'type' => trim($mime[1]), - 'stream' => $stream, // Instead of writing to a file, we write to a stream. - 'error' => UPLOAD_ERR_OK, - 'size' => function_exists('mb_strlen')? mb_strlen($content, '8bit') : strlen($content), - ]; - - //TODO :: have an option to write to files to emulate the same functionality as a real php server - //$path = tempnam(sys_get_temp_dir(), "php"); - //$err = file_put_contents($path, $content); - //$data['tmp_name'] = $path; - //$data['error'] = ($err === false) ? UPLOAD_ERR_NO_FILE : UPLOAD_ERR_OK; - - $this->addResolved('files', $match[1], $data); + $this->request->emit('file', [ + $match[1], // name + $match[2], // filename + trim($mime[1]), // type + $stream, + ]); + + $stream->write($content); } /** @@ -176,28 +183,6 @@ 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; - } + $this->request->emit('post', [$match[1], $match[2]]); } } diff --git a/src/Request.php b/src/Request.php index 0607b8e7..4dfc26da 100644 --- a/src/Request.php +++ b/src/Request.php @@ -17,7 +17,6 @@ class Request extends EventEmitter implements ReadableStreamInterface private $headers; private $body; private $post = []; - private $files = []; // metadata, implicitly added externally public $remoteAddress; @@ -30,6 +29,10 @@ public function __construct($method, $url, $query = array(), $httpVersion = '1.1 $this->httpVersion = $httpVersion; $this->headers = $headers; $this->body = $body; + + $this->on('post', function ($key, $value) { + $this->addPost($key, $value); + }); } public function getMethod() @@ -72,14 +75,9 @@ public function setBody($body) $this->body = $body; } - public function getFiles() - { - return $this->files; - } - - public function setFiles($files) + public function appendBody($data) { - $this->files = $files; + $this->body .= $data; } public function getPost() @@ -130,4 +128,25 @@ public function pipe(WritableStreamInterface $dest, array $options = array()) return $dest; } + + /** + * Put the file or post where it belongs, + * The key names can be simple, or containing [] + * it can also be a named key + * + * @param $key + * @param $content + */ + protected function addPost($key, $content) + { + if (preg_match('/^(.*)\[(.*)\]$/i', $key, $tmp)) { + if (!empty($tmp[2])) { + $this->post[$tmp[1]][$tmp[2]] = $content; + } else { + $this->post[$tmp[1]][] = $content; + } + } else { + $this->post[$key] = $content; + } + } } diff --git a/src/RequestParser.php b/src/RequestParser.php index 2a5c2873..fb6b81b9 100644 --- a/src/RequestParser.php +++ b/src/RequestParser.php @@ -4,6 +4,7 @@ use Evenement\EventEmitter; use GuzzleHttp\Psr7 as gPsr; +use React\Stream\ReadableStreamInterface; /** * @event headers @@ -11,6 +12,8 @@ */ class RequestParser extends EventEmitter { + private $stream; + private $buffer = ''; private $maxSize = 4096; @@ -18,7 +21,13 @@ class RequestParser extends EventEmitter * @var Request */ private $request; - private $length = 0; + + public function __construct(ReadableStreamInterface $conn) + { + $this->stream = $conn; + + $this->stream->on('data', [$this, 'feed']); + } public function feed($data) { @@ -28,7 +37,7 @@ public function feed($data) // Extract the header from the buffer // in case the content isn't complete - list($headers, $this->buffer) = explode("\r\n\r\n", $this->buffer, 2); + list($headers, $buffer) = explode("\r\n\r\n", $this->buffer, 2); // Fail before parsing if the if (strlen($headers) > $this->maxSize) { @@ -36,15 +45,11 @@ public function feed($data) return; } + $this->stream->removeListener('data', [$this, 'feed']); $this->request = $this->parseHeaders($headers . "\r\n\r\n"); - } - // if there is a request (meaning the headers are parsed) and - // we have the right content size, we can finish the parsing - if ($this->request && $this->isRequestComplete()) { - $this->parseBody(substr($this->buffer, 0, $this->length)); - $this->finishParsing(); - return; + $this->emit('headers', array($this->request, $this->parseBody($buffer))); + $this->removeAllListeners(); } // fail if the header hasn't finished but it is already too large @@ -54,36 +59,6 @@ public function feed($data) } } - protected function isRequestComplete() - { - $headers = $this->request->getHeaders(); - - // if there is no content length, there should - // be no content so we can say it's done - if (!array_key_exists('Content-Length', $headers)) { - return true; - } - - // if the content is present and has the - // right length, we're good to go - if (array_key_exists('Content-Length', $headers) && strlen($this->buffer) >= $headers['Content-Length']) { - - // store the expected content length - $this->length = $this->request->getHeaders()['Content-Length']; - - return true; - } - - return false; - } - - protected function finishParsing() - { - $this->emit('headers', array($this->request, $this->request->getBody())); - $this->removeAllListeners(); - $this->request = null; - } - protected function headerSizeExceeded() { $this->emit('error', array(new \OverflowException("Maximum header size of {$this->maxSize} exceeded."), $this)); @@ -122,28 +97,27 @@ public function parseBody($content) if (array_key_exists('Content-Type', $headers)) { if (strpos($headers['Content-Type'], 'multipart/') === 0) { - //TODO :: parse the content while it is streaming preg_match("/boundary=\"?(.*)\"?$/", $headers['Content-Type'], $matches); $boundary = $matches[1]; - $parser = new MultipartParser($content, $boundary); - $parser->parse(); - - $this->request->setPost($parser->getPost()); - $this->request->setFiles($parser->getFiles()); - return; + $parser = new MultipartParser($this->stream, $boundary, $this->request); + $this->once('headers', function () use ($parser, $content) { + $parser->feed($content); + }); + return ''; } if (strtolower($headers['Content-Type']) == 'application/x-www-form-urlencoded') { - parse_str(urldecode($content), $result); - $this->request->setPost($result); - - return; + $parser = new FormUrlencodedParser($this->stream, $this->request); + $this->once('headers', function () use ($parser, $content) { + $parser->feed($content); + }); + return ''; } } - - $this->request->setBody($content); + $this->stream->on('data', [$this->request, 'appendBody']); + return $content; } } diff --git a/src/Server.php b/src/Server.php index 5e420a40..5bf22abb 100644 --- a/src/Server.php +++ b/src/Server.php @@ -19,7 +19,7 @@ public function __construct(SocketServerInterface $io) // TODO: http 1.1 keep-alive // TODO: chunked transfer encoding (also for outgoing data) - $parser = new RequestParser(); + $parser = new RequestParser($conn); $parser->on('headers', function (Request $request, $bodyBuffer) use ($conn, $parser) { // attach remote ip to the request as metadata $request->remoteAddress = $conn->getRemoteAddress(); @@ -40,8 +40,6 @@ public function __construct(SocketServerInterface $io) $conn->emit('resume'); }); }); - - $conn->on('data', array($parser, 'feed')); }); } diff --git a/tests/FormUrlencodedParserTest.php b/tests/FormUrlencodedParserTest.php new file mode 100644 index 00000000..6b6e7382 --- /dev/null +++ b/tests/FormUrlencodedParserTest.php @@ -0,0 +1,23 @@ +write('user=single&user2=second&us'); + $stream->end('ers%5B%5D=first+in+array&users%5B%5D=second+in+array'); + $this->assertEquals( + ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']], + $request->getPost() + ); + } +} diff --git a/tests/MultipartParserTest.php b/tests/MultipartParserTest.php index 39a02c71..76e4ccbf 100644 --- a/tests/MultipartParserTest.php +++ b/tests/MultipartParserTest.php @@ -3,10 +3,26 @@ namespace React\Tests\Http; use React\Http\MultipartParser; - -class MultipartParserTest extends TestCase { - - public function testPostKey() { +use React\Http\Request; +use React\Stream\ReadableStreamInterface; +use React\Stream\ThroughStream; + +class MultipartParserTest extends TestCase +{ + + public function testPostKey() + { + $files = []; + $post = []; + + $stream = new ThroughStream(); + $request = new Request('POST', 'http://example.com/'); + $request->on('post', function ($key, $value) use (&$post) { + $post[$key] = $value; + }); + $request->on('file', function ($name, $filename, $type, ReadableStreamInterface $stream) use (&$files) { + $files[] = $name; + }); $boundary = "---------------------------5844729766471062541057622570"; @@ -20,67 +36,115 @@ public function testPostKey() { $data .= "second\r\n"; $data .= "--$boundary--\r\n"; - $parser = new MultipartParser($data, $boundary); - $parser->parse(); + new MultipartParser($stream, $boundary, $request); + $stream->write($data); - $this->assertEmpty($parser->getFiles()); + $this->assertEmpty($files); $this->assertEquals( - ['users' => ['one' => 'single', 'two' => 'second']], - $parser->getPost() + [ + 'users[one]' => 'single', + 'users[two]' => 'second', + ], + $post ); } - public function testFileUpload() { + public function testFileUpload() + { + $files = []; + $post = []; + + $stream = new ThroughStream(); + $request = new Request('POST', 'http://example.com/'); + $request->on('post', function ($key, $value) use (&$post) { + $post[] = [$key => $value]; + }); + $request->on('file', function ($name, $filename, $type, ReadableStreamInterface $stream) use (&$files) { + $files[] = [$name, $filename, $type, $stream]; + }); $file = base64_decode("R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="); $boundary = "---------------------------12758086162038677464950549563"; + new MultipartParser($stream, $boundary, $request); + $data = "--$boundary\r\n"; - $data .= "Content-Disposition: form-data; name=\"user\"\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[one]\"\r\n"; $data .= "\r\n"; $data .= "single\r\n"; $data .= "--$boundary\r\n"; - $data .= "Content-Disposition: form-data; name=\"user2\"\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[two]\"\r\n"; $data .= "\r\n"; $data .= "second\r\n"; - $data .= "--$boundary\r\n"; - $data .= "Content-Disposition: form-data; name=\"users[]\"\r\n"; - $data .= "\r\n"; - $data .= "first in array\r\n"; - $data .= "--$boundary\r\n"; - $data .= "Content-Disposition: form-data; name=\"users[]\"\r\n"; - $data .= "\r\n"; - $data .= "second in array\r\n"; - $data .= "--$boundary\r\n"; - $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"User.php\"\r\n"; - $data .= "Content-Type: text/php\r\n"; - $data .= "\r\n"; - $data .= "parse(); - - $this->assertEquals(2, count($parser->getFiles())); - $this->assertEquals(2, count($parser->getFiles()['files'])); + $stream->write($data); + $stream->write("--$boundary\r\n"); + $stream->write("Content-Disposition: form-data; name=\"user\"\r\n"); + $stream->write("\r\n"); + $stream->write("single\r\n"); + $stream->write("--$boundary\r\n"); + $stream->write("Content-Disposition: form-data; name=\"user2\"\r\n"); + $stream->write("\r\n"); + $stream->write("second\r\n"); + $stream->write("--$boundary\r\n"); + $stream->write("Content-Disposition: form-data; name=\"users[]\"\r\n"); + $stream->write("\r\n"); + $stream->write("first in array\r\n"); + $stream->write("--$boundary\r\n"); + $stream->write("Content-Disposition: form-data; name=\"users[]\"\r\n"); + $stream->write("\r\n"); + $stream->write("second in array\r\n"); + $stream->write("--$boundary\r\n"); + $stream->write("Content-Disposition: form-data; name=\"file\"; filename=\"User.php\"\r\n"); + $stream->write("Content-Type: text/php\r\n"); + $stream->write("\r\n"); + $stream->write("write("\r\n"); + $line = "--$boundary"; + $lines = str_split($line, round(strlen($line) / 2)); + $stream->write($lines[0]); + $stream->write($lines[1]); + $stream->write("\r\n"); + $stream->write("Content-Disposition: form-data; name=\"files[]\"; filename=\"blank.gif\"\r\n"); + $stream->write("Content-Type: image/gif\r\n"); + $stream->write("\r\n"); + $stream->write($file . "\r\n"); + $stream->write("--$boundary\r\n"); + $stream->write("Content-Disposition: form-data; name=\"files[]\"; filename=\"User.php\"\r\n"); + $stream->write("Content-Type: text/php\r\n"); + $stream->write("\r\n"); + $stream->write("write("\r\n"); + $stream->write("--$boundary--\r\n"); + + $this->assertEquals(6, count($post)); $this->assertEquals( - ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']], - $parser->getPost() + [ + ['users[one]' => 'single'], + ['users[two]' => 'second'], + ['user' => 'single'], + ['user2' => 'second'], + ['users[]' => 'first in array'], + ['users[]' => 'second in array'], + ], + $post ); + $this->assertEquals(3, count($files)); + $this->assertEquals('file', $files[0][0]); + $this->assertEquals('User.php', $files[0][1]); + $this->assertEquals('text/php', $files[0][2]); + + $this->assertEquals('files[]', $files[1][0]); + $this->assertEquals('blank.gif', $files[1][1]); + $this->assertEquals('image/gif', $files[1][2]); + + $this->assertEquals('files[]', $files[2][0]); + $this->assertEquals('User.php', $files[2][1]); + $this->assertEquals('text/php', $files[2][2]); + + return; + $uploaded_blank = $parser->getFiles()['files'][0]; // The original test was `file_get_contents($uploaded_blank['tmp_name'])` diff --git a/tests/RequestParserTest.php b/tests/RequestParserTest.php index 728166b3..f0c43af9 100644 --- a/tests/RequestParserTest.php +++ b/tests/RequestParserTest.php @@ -3,31 +3,34 @@ namespace React\Tests\Http; use React\Http\RequestParser; +use React\Stream\ThroughStream; class RequestParserTest extends TestCase { public function testSplitShouldHappenOnDoubleCrlf() { - $parser = new RequestParser(); + $stream = new ThroughStream(); + $parser = new RequestParser($stream); $parser->on('headers', $this->expectCallableNever()); - $parser->feed("GET / HTTP/1.1\r\n"); - $parser->feed("Host: example.com:80\r\n"); - $parser->feed("Connection: close\r\n"); + $stream->write("GET / HTTP/1.1\r\n"); + $stream->write("Host: example.com:80\r\n"); + $stream->write("Connection: close\r\n"); $parser->removeAllListeners(); $parser->on('headers', $this->expectCallableOnce()); - $parser->feed("\r\n"); + $stream->write("\r\n"); } public function testFeedInOneGo() { - $parser = new RequestParser(); + $stream = new ThroughStream(); + $parser = new RequestParser($stream); $parser->on('headers', $this->expectCallableOnce()); $data = $this->createGetRequest(); - $parser->feed($data); + $stream->write($data); } public function testHeadersEventShouldReturnRequestAndBodyBuffer() @@ -35,14 +38,15 @@ public function testHeadersEventShouldReturnRequestAndBodyBuffer() $request = null; $bodyBuffer = null; - $parser = new RequestParser(); + $stream = new ThroughStream(); + $parser = new RequestParser($stream); $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$bodyBuffer) { $request = $parsedRequest; $bodyBuffer = $parsedBodyBuffer; }); $data = $this->createGetRequest('RANDOM DATA', 11); - $parser->feed($data); + $stream->write($data); $this->assertInstanceOf('React\Http\Request', $request); $this->assertSame('GET', $request->getMethod()); @@ -61,13 +65,14 @@ public function testHeadersEventShouldReturnBinaryBodyBuffer() { $bodyBuffer = null; - $parser = new RequestParser(); + $stream = new ThroughStream(); + $parser = new RequestParser($stream); $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$bodyBuffer) { $bodyBuffer = $parsedBodyBuffer; }); $data = $this->createGetRequest("\0x01\0x02\0x03\0x04\0x05", strlen("\0x01\0x02\0x03\0x04\0x05")); - $parser->feed($data); + $stream->write($data); $this->assertSame("\0x01\0x02\0x03\0x04\0x05", $bodyBuffer); } @@ -76,13 +81,14 @@ public function testHeadersEventShouldParsePathAndQueryString() { $request = null; - $parser = new RequestParser(); + $stream = new ThroughStream(); + $parser = new RequestParser($stream); $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request) { $request = $parsedRequest; }); $data = $this->createAdvancedPostRequest(); - $parser->feed($data); + $stream->write($data); $this->assertInstanceOf('React\Http\Request', $request); $this->assertSame('POST', $request->getMethod()); @@ -99,48 +105,78 @@ public function testHeadersEventShouldParsePathAndQueryString() public function testShouldReceiveBodyContent() { - $content1 = "{\"test\":"; $content2 = " \"value\"}"; + $content1 = "{\"test\":"; + $content2 = " \"value\"}"; $request = null; $body = null; - $parser = new RequestParser(); + $stream = new ThroughStream(); + $parser = new RequestParser($stream); $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$body) { $request = $parsedRequest; $body = $parsedBodyBuffer; }); $data = $this->createAdvancedPostRequest('', 17); - $parser->feed($data); - $parser->feed($content1); - $parser->feed($content2 . "\r\n"); + $stream->write($data); + $stream->write($content1); + $stream->write($content2); $this->assertInstanceOf('React\Http\Request', $request); $this->assertEquals($content1 . $content2, $request->getBody()); - $this->assertSame($body, $request->getBody()); + $this->assertSame($body, ''); } - public function testShouldReceiveMultiPartBody() + public function testShouldReceiveBodyContentPartial() { + $content1 = "{\"test\":"; + $content2 = " \"value\"}"; $request = null; $body = null; - $parser = new RequestParser(); + $stream = new ThroughStream(); + $parser = new RequestParser($stream); $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$body) { $request = $parsedRequest; $body = $parsedBodyBuffer; }); - $parser->feed($this->createMultipartRequest()); + $data = $this->createAdvancedPostRequest('', 17); + $stream->write($data . $content1); + $stream->write($content2); + + $this->assertInstanceOf('React\Http\Request', $request); + $this->assertEquals($content1 . $content2, $request->getBody()); + $this->assertSame($body, $content1); + } + + public function testShouldReceiveMultiPartBody() + { + + $request = null; + $body = null; + $files = []; + + $stream = new ThroughStream(); + $parser = new RequestParser($stream); + $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$body, &$files) { + $request = $parsedRequest; + $body = $parsedBodyBuffer; + $request->on('file', function ($name) use (&$files) { + $files[] = $name; + }); + }); + + $stream->write($this->createMultipartRequest()); $this->assertInstanceOf('React\Http\Request', $request); $this->assertEquals( - $request->getPost(), - ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']] + ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']], + $request->getPost() ); - $this->assertEquals(2, count($request->getFiles())); - $this->assertEquals(2, count($request->getFiles()['files'])); + $this->assertEquals(3, count($files)); } public function testShouldReceivePostInBody() @@ -148,19 +184,44 @@ public function testShouldReceivePostInBody() $request = null; $body = null; - $parser = new RequestParser(); + $stream = new ThroughStream(); + $parser = new RequestParser($stream); $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$body) { $request = $parsedRequest; $body = $parsedBodyBuffer; }); - $parser->feed($this->createPostWithContent()); + $stream->write($this->createPostWithContent()); $this->assertInstanceOf('React\Http\Request', $request); $this->assertSame('', $body); $this->assertEquals( - $request->getPost(), - ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']] + ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']], + $request->getPost() + ); + } + + public function testShouldReceivePostInBodySplit() + { + $request = null; + $body = null; + + $stream = new ThroughStream(); + $parser = new RequestParser($stream); + $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$body) { + $request = $parsedRequest; + $body = $parsedBodyBuffer; + }); + + list($data, $data2) = $this->createPostWithContentSplit(); + $stream->write($data); + $stream->write($data2); + + $this->assertInstanceOf('React\Http\Request', $request); + $this->assertSame('', $body); + $this->assertEquals( + ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']], + $request->getPost() ); } @@ -168,14 +229,15 @@ public function testHeaderOverflowShouldEmitError() { $error = null; - $parser = new RequestParser(); + $stream = new ThroughStream(); + $parser = new RequestParser($stream); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message) use (&$error) { $error = $message; }); $data = str_repeat('A', 4097); - $parser->feed($data); + $stream->write($data); $this->assertInstanceOf('OverflowException', $error); $this->assertSame('Maximum header size of 4096 exceeded.', $error->getMessage()); @@ -185,7 +247,8 @@ public function testOnePassHeaderTooLarge() { $error = null; - $parser = new RequestParser(); + $stream = new ThroughStream(); + $parser = new RequestParser($stream); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message) use (&$error) { $error = $message; @@ -195,7 +258,7 @@ public function testOnePassHeaderTooLarge() $data .= "Host: example.com:80\r\n"; $data .= "Cookie: " . str_repeat('A', 4097) . "\r\n"; $data .= "\r\n"; - $parser->feed($data); + $stream->write($data); $this->assertInstanceOf('OverflowException', $error); $this->assertSame('Maximum header size of 4096 exceeded.', $error->getMessage()); @@ -205,14 +268,15 @@ public function testBodyShouldNotOverflowHeader() { $error = null; - $parser = new RequestParser(); + $stream = new ThroughStream(); + $parser = new RequestParser($stream); $parser->on('headers', $this->expectCallableOnce()); $parser->on('error', function ($message) use (&$error) { $error = $message; }); $data = str_repeat('A', 4097); - $parser->feed($this->createAdvancedPostRequest() . $data); + $stream->write($this->createAdvancedPostRequest() . $data); $this->assertNull($error); } @@ -260,6 +324,21 @@ private function createPostWithContent() return $data; } + private function createPostWithContentSplit() + { + $data = "POST /foo?bar=baz HTTP/1.1\r\n"; + $data .= "Host: localhost:8080\r\n"; + $data .= "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:32.0) Gecko/20100101 Firefox/32.0\r\n"; + $data .= "Connection: close\r\n"; + $data .= "Content-Type: application/x-www-form-urlencoded\r\n"; + $data .= "Content-Length: 79\r\n"; + $data .= "\r\n"; + $data .= "user=single&user2=second&us"; + $data2 = "ers%5B%5D=first+in+array&users%5B%5D=second+in+array\r\n"; + + return [$data, $data2]; + } + private function createMultipartRequest() { $data = "POST / HTTP/1.1\r\n"; From e1b0d038f674877fcb1fb39c97cbce45ba958e9e Mon Sep 17 00:00:00 2001 From: Cees-Jan Date: Thu, 1 Oct 2015 16:12:44 +0200 Subject: [PATCH 02/64] Moved parsers to their own namespace --- .../FormUrlencoded.php} | 5 +++-- src/{MultipartParser.php => Parser/Multipart.php} | 5 +++-- src/RequestParser.php | 6 ++++-- .../FormUrlencodedTest.php} | 9 +++++---- .../MultipartTest.php} | 9 +++++---- 5 files changed, 20 insertions(+), 14 deletions(-) rename src/{FormUrlencodedParser.php => Parser/FormUrlencoded.php} (94%) rename src/{MultipartParser.php => Parser/Multipart.php} (98%) rename tests/{FormUrlencodedParserTest.php => Parser/FormUrlencodedTest.php} (73%) rename tests/{MultipartParserTest.php => Parser/MultipartTest.php} (96%) diff --git a/src/FormUrlencodedParser.php b/src/Parser/FormUrlencoded.php similarity index 94% rename from src/FormUrlencodedParser.php rename to src/Parser/FormUrlencoded.php index 5d4e7be9..738a51c4 100644 --- a/src/FormUrlencodedParser.php +++ b/src/Parser/FormUrlencoded.php @@ -1,10 +1,11 @@ stream, $boundary, $this->request); + $parser = new Multipart($this->stream, $boundary, $this->request); $this->once('headers', function () use ($parser, $content) { $parser->feed($content); }); @@ -108,7 +110,7 @@ public function parseBody($content) } if (strtolower($headers['Content-Type']) == 'application/x-www-form-urlencoded') { - $parser = new FormUrlencodedParser($this->stream, $this->request); + $parser = new FormUrlencoded($this->stream, $this->request); $this->once('headers', function () use ($parser, $content) { $parser->feed($content); }); diff --git a/tests/FormUrlencodedParserTest.php b/tests/Parser/FormUrlencodedTest.php similarity index 73% rename from tests/FormUrlencodedParserTest.php rename to tests/Parser/FormUrlencodedTest.php index 6b6e7382..23e8dd5b 100644 --- a/tests/FormUrlencodedParserTest.php +++ b/tests/Parser/FormUrlencodedTest.php @@ -1,18 +1,19 @@ write('user=single&user2=second&us'); $stream->end('ers%5B%5D=first+in+array&users%5B%5D=second+in+array'); $this->assertEquals( diff --git a/tests/MultipartParserTest.php b/tests/Parser/MultipartTest.php similarity index 96% rename from tests/MultipartParserTest.php rename to tests/Parser/MultipartTest.php index 76e4ccbf..91172218 100644 --- a/tests/MultipartParserTest.php +++ b/tests/Parser/MultipartTest.php @@ -1,11 +1,12 @@ write($data); $this->assertEmpty($files); @@ -67,7 +68,7 @@ public function testFileUpload() $boundary = "---------------------------12758086162038677464950549563"; - new MultipartParser($stream, $boundary, $request); + new Multipart($stream, $boundary, $request); $data = "--$boundary\r\n"; $data .= "Content-Disposition: form-data; name=\"users[one]\"\r\n"; From dca940dd7a0eed4f7c8f432f0ae7aaf367df434d Mon Sep 17 00:00:00 2001 From: Cees-Jan Date: Thu, 1 Oct 2015 16:26:03 +0200 Subject: [PATCH 03/64] File object representing uploaded files --- src/File.php | 74 ++++++++++++++++++++++++++++++++++ src/FileInterface.php | 28 +++++++++++++ src/Parser/Multipart.php | 7 ++-- tests/FileTest.php | 22 ++++++++++ tests/Parser/MultipartTest.php | 28 ++++++------- 5 files changed, 142 insertions(+), 17 deletions(-) create mode 100644 src/File.php create mode 100644 src/FileInterface.php create mode 100644 tests/FileTest.php diff --git a/src/File.php b/src/File.php new file mode 100644 index 00000000..34116984 --- /dev/null +++ b/src/File.php @@ -0,0 +1,74 @@ +name = $name; + $this->filename = $filename; + $this->type = $type; + $this->stream = $stream; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return string + */ + public function getFilename() + { + return $this->filename; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * @return ReadableStreamInterface + */ + public function getStream() + { + return $this->stream; + } +} diff --git a/src/FileInterface.php b/src/FileInterface.php new file mode 100644 index 00000000..880259e9 --- /dev/null +++ b/src/FileInterface.php @@ -0,0 +1,28 @@ +stream->on('data', $func); } - $this->request->emit('file', [ + $this->request->emit('file', [new File( $match[1], // name $match[2], // filename trim($mime[1]), // type - $stream, - ]); + $stream + )]); $stream->write($content); } diff --git a/tests/FileTest.php b/tests/FileTest.php new file mode 100644 index 00000000..657bc793 --- /dev/null +++ b/tests/FileTest.php @@ -0,0 +1,22 @@ +assertEquals($name, $file->getName()); + $this->assertEquals($filename, $file->getFilename()); + $this->assertEquals($type, $file->getType()); + $this->assertEquals($stream, $file->getStream()); + } +} diff --git a/tests/Parser/MultipartTest.php b/tests/Parser/MultipartTest.php index 91172218..588797a5 100644 --- a/tests/Parser/MultipartTest.php +++ b/tests/Parser/MultipartTest.php @@ -2,9 +2,9 @@ namespace React\Tests\Http\Parser; +use React\Http\FileInterface; use React\Http\Parser\Multipart; use React\Http\Request; -use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; @@ -21,8 +21,8 @@ public function testPostKey() $request->on('post', function ($key, $value) use (&$post) { $post[$key] = $value; }); - $request->on('file', function ($name, $filename, $type, ReadableStreamInterface $stream) use (&$files) { - $files[] = $name; + $request->on('file', function (FileInterface $file) use (&$files) { + $files[] = $file; }); $boundary = "---------------------------5844729766471062541057622570"; @@ -60,8 +60,8 @@ public function testFileUpload() $request->on('post', function ($key, $value) use (&$post) { $post[] = [$key => $value]; }); - $request->on('file', function ($name, $filename, $type, ReadableStreamInterface $stream) use (&$files) { - $files[] = [$name, $filename, $type, $stream]; + $request->on('file', function (FileInterface $file) use (&$files) { + $files[] = $file; }); $file = base64_decode("R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="); @@ -132,17 +132,17 @@ public function testFileUpload() ); $this->assertEquals(3, count($files)); - $this->assertEquals('file', $files[0][0]); - $this->assertEquals('User.php', $files[0][1]); - $this->assertEquals('text/php', $files[0][2]); + $this->assertEquals('file', $files[0]->getName()); + $this->assertEquals('User.php', $files[0]->getFilename()); + $this->assertEquals('text/php', $files[0]->getType()); - $this->assertEquals('files[]', $files[1][0]); - $this->assertEquals('blank.gif', $files[1][1]); - $this->assertEquals('image/gif', $files[1][2]); + $this->assertEquals('files[]', $files[1]->getName()); + $this->assertEquals('blank.gif', $files[1]->getFilename()); + $this->assertEquals('image/gif', $files[1]->getType()); - $this->assertEquals('files[]', $files[2][0]); - $this->assertEquals('User.php', $files[2][1]); - $this->assertEquals('text/php', $files[2][2]); + $this->assertEquals('files[]', $files[2]->getName()); + $this->assertEquals('User.php', $files[2]->getFilename()); + $this->assertEquals('text/php', $files[2]->getType()); return; From 816d1c699d9e3fc8a492b38fad2b05395ee32942 Mon Sep 17 00:00:00 2001 From: Cees-Jan Date: Thu, 1 Oct 2015 16:26:38 +0200 Subject: [PATCH 04/64] Cleaned up old test code I've forgotten to remove --- tests/Parser/MultipartTest.php | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/tests/Parser/MultipartTest.php b/tests/Parser/MultipartTest.php index 588797a5..d614077d 100644 --- a/tests/Parser/MultipartTest.php +++ b/tests/Parser/MultipartTest.php @@ -143,25 +143,5 @@ public function testFileUpload() $this->assertEquals('files[]', $files[2]->getName()); $this->assertEquals('User.php', $files[2]->getFilename()); $this->assertEquals('text/php', $files[2]->getType()); - - return; - - $uploaded_blank = $parser->getFiles()['files'][0]; - - // The original test was `file_get_contents($uploaded_blank['tmp_name'])` - // but as we moved to resources, we can't use that anymore, this is the only - // difference with a stock php implementation - $this->assertEquals($file, stream_get_contents($uploaded_blank['stream'])); - - $uploaded_blank['stream'] = 'file'; //override the resource as it is random - $expected_file = [ - 'name' => 'blank.gif', - 'type' => 'image/gif', - 'stream' => 'file', - 'error' => 0, - 'size' => 43, - ]; - - $this->assertEquals($expected_file, $uploaded_blank); } } From 9eb1a932686704e4ba5f0a14a9d2837005936bbb Mon Sep 17 00:00:00 2001 From: Cees-Jan Date: Wed, 28 Oct 2015 19:23:07 +0100 Subject: [PATCH 05/64] https://github.com/reactphp/http/issues/44#issuecomment-151941120 --- src/Parser/FormUrlencoded.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Parser/FormUrlencoded.php b/src/Parser/FormUrlencoded.php index 738a51c4..2322acb6 100644 --- a/src/Parser/FormUrlencoded.php +++ b/src/Parser/FormUrlencoded.php @@ -55,7 +55,7 @@ public function finish() { $this->stream->removeListener('data', [$this, 'feed']); $this->stream->removeListener('close', [$this, 'finish']); - parse_str(urldecode(trim($this->buffer)), $result); + parse_str(trim($this->buffer), $result); $this->request->setPost($result); } } From 9bb6116403def1c85dfd88797741e9545e5a23d9 Mon Sep 17 00:00:00 2001 From: Cees-Jan Date: Sat, 19 Mar 2016 18:30:31 +0100 Subject: [PATCH 06/64] End the stream when not streaming --- src/Parser/Multipart.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Parser/Multipart.php b/src/Parser/Multipart.php index 7dbecada..2dc82c4c 100644 --- a/src/Parser/Multipart.php +++ b/src/Parser/Multipart.php @@ -172,7 +172,11 @@ protected function file($firstChunk, $streaming = true) $stream )]); - $stream->write($content); + if ($streaming) { + $stream->write($content); + } else { + $stream->end($content); + } } /** From 41eaa9209e3cfe5bf87ae82aa624c63445dce800 Mon Sep 17 00:00:00 2001 From: Cees-Jan Date: Sat, 19 Mar 2016 19:40:43 +0100 Subject: [PATCH 07/64] Properly trim off the file contents --- src/Parser/Multipart.php | 2 +- tests/Parser/MultipartTest.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Parser/Multipart.php b/src/Parser/Multipart.php index 2dc82c4c..d3ab7281 100644 --- a/src/Parser/Multipart.php +++ b/src/Parser/Multipart.php @@ -168,7 +168,7 @@ protected function file($firstChunk, $streaming = true) $this->request->emit('file', [new File( $match[1], // name $match[2], // filename - trim($mime[1]), // type + explode("\r\n", trim($mime[1]))[0], // type $stream )]); diff --git a/tests/Parser/MultipartTest.php b/tests/Parser/MultipartTest.php index d614077d..607595df 100644 --- a/tests/Parser/MultipartTest.php +++ b/tests/Parser/MultipartTest.php @@ -111,10 +111,10 @@ public function testFileUpload() $stream->write("\r\n"); $stream->write($file . "\r\n"); $stream->write("--$boundary\r\n"); - $stream->write("Content-Disposition: form-data; name=\"files[]\"; filename=\"User.php\"\r\n"); - $stream->write("Content-Type: text/php\r\n"); - $stream->write("\r\n"); - $stream->write("write("Content-Disposition: form-data; name=\"files[]\"; filename=\"User.php\"\r\n" . + "Content-Type: text/php\r\n" . + "\r\n" . + "write("\r\n"); $stream->write("--$boundary--\r\n"); From 2860f106b60c083752f025546b085e068486f029 Mon Sep 17 00:00:00 2001 From: Cees-Jan Date: Sat, 19 Mar 2016 19:43:56 +0100 Subject: [PATCH 08/64] Make sure extra headers are ignored --- tests/Parser/MultipartTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Parser/MultipartTest.php b/tests/Parser/MultipartTest.php index 607595df..177dc9e6 100644 --- a/tests/Parser/MultipartTest.php +++ b/tests/Parser/MultipartTest.php @@ -108,6 +108,7 @@ public function testFileUpload() $stream->write("\r\n"); $stream->write("Content-Disposition: form-data; name=\"files[]\"; filename=\"blank.gif\"\r\n"); $stream->write("Content-Type: image/gif\r\n"); + $stream->write("X-Foo-Bar: base64\r\n"); $stream->write("\r\n"); $stream->write($file . "\r\n"); $stream->write("--$boundary\r\n"); From 64446d173f0d531b648c3b8a0254af86721cf926 Mon Sep 17 00:00:00 2001 From: Cees-Jan Date: Sun, 20 Mar 2016 16:33:16 +0100 Subject: [PATCH 09/64] Emit the post vars instead of setting them from the FormUrlencoded parser --- src/Parser/FormUrlencoded.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Parser/FormUrlencoded.php b/src/Parser/FormUrlencoded.php index 2322acb6..1978dd34 100644 --- a/src/Parser/FormUrlencoded.php +++ b/src/Parser/FormUrlencoded.php @@ -56,6 +56,8 @@ public function finish() $this->stream->removeListener('data', [$this, 'feed']); $this->stream->removeListener('close', [$this, 'finish']); parse_str(trim($this->buffer), $result); - $this->request->setPost($result); + foreach ($result as $key => $value) { + $this->request->emit('post', [$key, $value]); + } } } From 25f471862af81adebb1ac4f2d5444b421e576c32 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 21 Mar 2016 22:42:42 +0100 Subject: [PATCH 10/64] FormParserFactory --- src/FormParserFactory.php | 25 +++++++++++++++++++++++++ tests/FormParserFactoryTest.php | 27 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/FormParserFactory.php create mode 100644 tests/FormParserFactoryTest.php diff --git a/src/FormParserFactory.php b/src/FormParserFactory.php new file mode 100644 index 00000000..a0d4e5f4 --- /dev/null +++ b/src/FormParserFactory.php @@ -0,0 +1,25 @@ +getHeaders(); + + if (!array_key_exists('Content-Type', $headers)) { + } + + if (strpos($headers['Content-Type'], 'multipart/') === 0) { + return new Multipart($request); + } + + if (strtolower($headers['Content-Type']) == 'application/x-www-form-urlencoded') { + return new FormUrlencoded($request); + } + } +} diff --git a/tests/FormParserFactoryTest.php b/tests/FormParserFactoryTest.php new file mode 100644 index 00000000..1efc5b71 --- /dev/null +++ b/tests/FormParserFactoryTest.php @@ -0,0 +1,27 @@ + 'multipart/mixed; boundary=---------------------------12758086162038677464950549563', + ]); + $parser = FormParserFactory::create($request); + $this->assertInstanceOf('React\Http\Parser\Multipart', $parser); + } + + public function testFormUrlencoded() + { + $request = new Request('POST', 'http://example.com/', [], 1.1, [ + 'Content-Type' => 'application/x-www-form-urlencoded', + ]); + $parser = FormParserFactory::create($request); + $this->assertInstanceOf('React\Http\Parser\FormUrlencoded', $parser); + } +} From db682301d4f934949022da775284f94ca6132725 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 21 Mar 2016 22:43:15 +0100 Subject: [PATCH 11/64] Changed both parsers to work with only the request --- src/Parser/FormUrlencoded.php | 17 ++--- src/Parser/Multipart.php | 26 ++++---- tests/Parser/FormUrlencodedTest.php | 8 +-- tests/Parser/MultipartTest.php | 96 +++++++++++++++-------------- 4 files changed, 69 insertions(+), 78 deletions(-) diff --git a/src/Parser/FormUrlencoded.php b/src/Parser/FormUrlencoded.php index 1978dd34..d99b1519 100644 --- a/src/Parser/FormUrlencoded.php +++ b/src/Parser/FormUrlencoded.php @@ -12,27 +12,20 @@ class FormUrlencoded */ protected $buffer = ''; - /** - * @var ReadableStreamInterface - */ - protected $stream; - /** * @var Request */ protected $request; /** - * @param ReadableStreamInterface $stream * @param Request $request */ - public function __construct(ReadableStreamInterface $stream, Request $request) + public function __construct(Request $request) { - $this->stream = $stream; $this->request = $request; - $this->stream->on('data', [$this, 'feed']); - $this->stream->on('close', [$this, 'finish']); + $this->request->on('data', [$this, 'feed']); + $this->request->on('close', [$this, 'finish']); } /** @@ -53,8 +46,8 @@ public function feed($data) public function finish() { - $this->stream->removeListener('data', [$this, 'feed']); - $this->stream->removeListener('close', [$this, 'finish']); + $this->request->removeListener('data', [$this, 'feed']); + $this->request->removeListener('close', [$this, 'finish']); parse_str(trim($this->buffer), $result); foreach ($result as $key => $value) { $this->request->emit('post', [$key, $value]); diff --git a/src/Parser/Multipart.php b/src/Parser/Multipart.php index d3ab7281..bfd66542 100644 --- a/src/Parser/Multipart.php +++ b/src/Parser/Multipart.php @@ -20,11 +20,6 @@ class Multipart { protected $buffer = ''; - /** - * @var string - */ - protected $stream; - /** * @var string */ @@ -36,17 +31,18 @@ class Multipart protected $request; /** - * @param ReadableStreamInterface $stream * @param string $boundary * @param Request $request */ - public function __construct(ReadableStreamInterface $stream, $boundary, Request $request) + public function __construct(Request $request) { - $this->stream = $stream; - $this->boundary = $boundary; + $headers = $request->getHeaders(); + preg_match("/boundary=\"?(.*)\"?$/", $headers['Content-Type'], $matches); + + $this->boundary = $matches[1]; $this->request = $request; - $this->stream->on('data', [$this, 'feed']); + $this->request->on('data', [$this, 'feed']); } /** @@ -141,7 +137,7 @@ protected function file($firstChunk, $streaming = true) $stream = new ThroughStream(); if ($streaming) { - $this->stream->removeListener('data', [$this, 'feed']); + $this->request->removeListener('data', [$this, 'feed']); $buffer = ''; $func = function($data) use (&$func, &$buffer, $stream) { $buffer .= $data; @@ -150,10 +146,10 @@ protected function file($firstChunk, $streaming = true) $chunk = array_shift($chunks); $stream->end($chunk); - $this->stream->removeListener('data', $func); - $this->stream->on('data', [$this, 'feed']); + $this->request->removeListener('data', $func); + $this->request->on('data', [$this, 'feed']); - $this->stream->emit('data', [implode($this->boundary, $chunks)]); + $this->request->emit('data', [implode($this->boundary, $chunks)]); return; } @@ -162,7 +158,7 @@ protected function file($firstChunk, $streaming = true) $buffer = ''; } }; - $this->stream->on('data', $func); + $this->request->on('data', $func); } $this->request->emit('file', [new File( diff --git a/tests/Parser/FormUrlencodedTest.php b/tests/Parser/FormUrlencodedTest.php index 23e8dd5b..3910c116 100644 --- a/tests/Parser/FormUrlencodedTest.php +++ b/tests/Parser/FormUrlencodedTest.php @@ -11,11 +11,11 @@ class FormUrlencodedTest extends TestCase { public function testParse() { - $stream = new ThroughStream(); $request = new Request('POST', 'http://example.com/'); - new FormUrlencoded($stream, $request); - $stream->write('user=single&user2=second&us'); - $stream->end('ers%5B%5D=first+in+array&users%5B%5D=second+in+array'); + new FormUrlencoded($request); + $request->emit('data', ['user=single&user2=second&us']); + $request->emit('data', ['ers%5B%5D=first+in+array&users%5B%5D=second+in+array']); + $request->emit('close'); $this->assertEquals( ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']], $request->getPost() diff --git a/tests/Parser/MultipartTest.php b/tests/Parser/MultipartTest.php index 177dc9e6..604162b3 100644 --- a/tests/Parser/MultipartTest.php +++ b/tests/Parser/MultipartTest.php @@ -16,8 +16,11 @@ public function testPostKey() $files = []; $post = []; - $stream = new ThroughStream(); - $request = new Request('POST', 'http://example.com/'); + $boundary = "---------------------------5844729766471062541057622570"; + + $request = new Request('POST', 'http://example.com/', [], 1.1, [ + 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + ]); $request->on('post', function ($key, $value) use (&$post) { $post[$key] = $value; }); @@ -25,8 +28,6 @@ public function testPostKey() $files[] = $file; }); - $boundary = "---------------------------5844729766471062541057622570"; - $data = "--$boundary\r\n"; $data .= "Content-Disposition: form-data; name=\"users[one]\"\r\n"; $data .= "\r\n"; @@ -37,8 +38,8 @@ public function testPostKey() $data .= "second\r\n"; $data .= "--$boundary--\r\n"; - new Multipart($stream, $boundary, $request); - $stream->write($data); + new Multipart($request); + $request->emit('data', [$data]); $this->assertEmpty($files); $this->assertEquals( @@ -55,8 +56,11 @@ public function testFileUpload() $files = []; $post = []; - $stream = new ThroughStream(); - $request = new Request('POST', 'http://example.com/'); + $boundary = "---------------------------12758086162038677464950549563"; + + $request = new Request('POST', 'http://example.com/', [], 1.1, [ + 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + ]); $request->on('post', function ($key, $value) use (&$post) { $post[] = [$key => $value]; }); @@ -66,9 +70,7 @@ public function testFileUpload() $file = base64_decode("R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="); - $boundary = "---------------------------12758086162038677464950549563"; - - new Multipart($stream, $boundary, $request); + new Multipart($request); $data = "--$boundary\r\n"; $data .= "Content-Disposition: form-data; name=\"users[one]\"\r\n"; @@ -78,46 +80,46 @@ public function testFileUpload() $data .= "Content-Disposition: form-data; name=\"users[two]\"\r\n"; $data .= "\r\n"; $data .= "second\r\n"; - $stream->write($data); - $stream->write("--$boundary\r\n"); - $stream->write("Content-Disposition: form-data; name=\"user\"\r\n"); - $stream->write("\r\n"); - $stream->write("single\r\n"); - $stream->write("--$boundary\r\n"); - $stream->write("Content-Disposition: form-data; name=\"user2\"\r\n"); - $stream->write("\r\n"); - $stream->write("second\r\n"); - $stream->write("--$boundary\r\n"); - $stream->write("Content-Disposition: form-data; name=\"users[]\"\r\n"); - $stream->write("\r\n"); - $stream->write("first in array\r\n"); - $stream->write("--$boundary\r\n"); - $stream->write("Content-Disposition: form-data; name=\"users[]\"\r\n"); - $stream->write("\r\n"); - $stream->write("second in array\r\n"); - $stream->write("--$boundary\r\n"); - $stream->write("Content-Disposition: form-data; name=\"file\"; filename=\"User.php\"\r\n"); - $stream->write("Content-Type: text/php\r\n"); - $stream->write("\r\n"); - $stream->write("write("\r\n"); + $request->emit('data', [$data]); + $request->emit('data', ["--$boundary\r\n"]); + $request->emit('data', ["Content-Disposition: form-data; name=\"user\"\r\n"]); + $request->emit('data', ["\r\n"]); + $request->emit('data', ["single\r\n"]); + $request->emit('data', ["--$boundary\r\n"]); + $request->emit('data', ["Content-Disposition: form-data; name=\"user2\"\r\n"]); + $request->emit('data', ["\r\n"]); + $request->emit('data', ["second\r\n"]); + $request->emit('data', ["--$boundary\r\n"]); + $request->emit('data', ["Content-Disposition: form-data; name=\"users[]\"\r\n"]); + $request->emit('data', ["\r\n"]); + $request->emit('data', ["first in array\r\n"]); + $request->emit('data', ["--$boundary\r\n"]); + $request->emit('data', ["Content-Disposition: form-data; name=\"users[]\"\r\n"]); + $request->emit('data', ["\r\n"]); + $request->emit('data', ["second in array\r\n"]); + $request->emit('data', ["--$boundary\r\n"]); + $request->emit('data', ["Content-Disposition: form-data; name=\"file\"; filename=\"User.php\"\r\n"]); + $request->emit('data', ["Content-Type: text/php\r\n"]); + $request->emit('data', ["\r\n"]); + $request->emit('data', ["emit('data', ["\r\n"]); $line = "--$boundary"; $lines = str_split($line, round(strlen($line) / 2)); - $stream->write($lines[0]); - $stream->write($lines[1]); - $stream->write("\r\n"); - $stream->write("Content-Disposition: form-data; name=\"files[]\"; filename=\"blank.gif\"\r\n"); - $stream->write("Content-Type: image/gif\r\n"); - $stream->write("X-Foo-Bar: base64\r\n"); - $stream->write("\r\n"); - $stream->write($file . "\r\n"); - $stream->write("--$boundary\r\n"); - $stream->write("Content-Disposition: form-data; name=\"files[]\"; filename=\"User.php\"\r\n" . + $request->emit('data', [$lines[0]]); + $request->emit('data', [$lines[1]]); + $request->emit('data', ["\r\n"]); + $request->emit('data', ["Content-Disposition: form-data; name=\"files[]\"; filename=\"blank.gif\"\r\n"]); + $request->emit('data', ["Content-Type: image/gif\r\n"]); + $request->emit('data', ["X-Foo-Bar: base64\r\n"]); + $request->emit('data', ["\r\n"]); + $request->emit('data', [$file . "\r\n"]); + $request->emit('data', ["--$boundary\r\n"]); + $request->emit('data', ["Content-Disposition: form-data; name=\"files[]\"; filename=\"User.php\"\r\n" . "Content-Type: text/php\r\n" . "\r\n" . - "write("\r\n"); - $stream->write("--$boundary--\r\n"); + "emit('data', ["\r\n"]); + $request->emit('data', ["--$boundary--\r\n"]); $this->assertEquals(6, count($post)); $this->assertEquals( From ab99c8f1fa61dcf5f58288b5369b6757851d4344 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 22 Mar 2016 17:59:11 +0100 Subject: [PATCH 12/64] Revert old behavior and set request (partial) body --- src/RequestParser.php | 33 ++------------------------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/src/RequestParser.php b/src/RequestParser.php index cb97dd7e..aeceb0cb 100644 --- a/src/RequestParser.php +++ b/src/RequestParser.php @@ -49,8 +49,9 @@ public function feed($data) $this->stream->removeListener('data', [$this, 'feed']); $this->request = $this->parseHeaders($headers . "\r\n\r\n"); + $this->request->setBody($buffer); - $this->emit('headers', array($this->request, $this->parseBody($buffer))); + $this->emit('headers', array($this->request, $buffer)); $this->removeAllListeners(); } @@ -92,34 +93,4 @@ public function parseHeaders($data) $headers ); } - - public function parseBody($content) - { - $headers = $this->request->getHeaders(); - - if (array_key_exists('Content-Type', $headers)) { - if (strpos($headers['Content-Type'], 'multipart/') === 0) { - preg_match("/boundary=\"?(.*)\"?$/", $headers['Content-Type'], $matches); - $boundary = $matches[1]; - - $parser = new Multipart($this->stream, $boundary, $this->request); - $this->once('headers', function () use ($parser, $content) { - $parser->feed($content); - }); - return ''; - } - - if (strtolower($headers['Content-Type']) == 'application/x-www-form-urlencoded') { - $parser = new FormUrlencoded($this->stream, $this->request); - $this->once('headers', function () use ($parser, $content) { - $parser->feed($content); - }); - return ''; - } - } - - $this->request->setBody($content); - $this->stream->on('data', [$this->request, 'appendBody']); - return $content; - } } From 20682f65095eba4a3a0ba5c4827d366d1dc0d134 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 22 Mar 2016 17:59:35 +0100 Subject: [PATCH 13/64] Updated tests and removed multipart and urlencoded tests --- tests/RequestParserTest.php | 155 +----------------------------------- 1 file changed, 3 insertions(+), 152 deletions(-) diff --git a/tests/RequestParserTest.php b/tests/RequestParserTest.php index f0c43af9..cdf15d94 100644 --- a/tests/RequestParserTest.php +++ b/tests/RequestParserTest.php @@ -103,7 +103,7 @@ public function testHeadersEventShouldParsePathAndQueryString() $this->assertSame($headers, $request->getHeaders()); } - public function testShouldReceiveBodyContent() + public function testShouldNotReceiveBodyContent() { $content1 = "{\"test\":"; $content2 = " \"value\"}"; @@ -124,7 +124,7 @@ public function testShouldReceiveBodyContent() $stream->write($content2); $this->assertInstanceOf('React\Http\Request', $request); - $this->assertEquals($content1 . $content2, $request->getBody()); + $this->assertEquals('', $request->getBody()); $this->assertSame($body, ''); } @@ -148,83 +148,10 @@ public function testShouldReceiveBodyContentPartial() $stream->write($content2); $this->assertInstanceOf('React\Http\Request', $request); - $this->assertEquals($content1 . $content2, $request->getBody()); + $this->assertEquals($content1, $request->getBody()); $this->assertSame($body, $content1); } - public function testShouldReceiveMultiPartBody() - { - - $request = null; - $body = null; - $files = []; - - $stream = new ThroughStream(); - $parser = new RequestParser($stream); - $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$body, &$files) { - $request = $parsedRequest; - $body = $parsedBodyBuffer; - $request->on('file', function ($name) use (&$files) { - $files[] = $name; - }); - }); - - $stream->write($this->createMultipartRequest()); - - $this->assertInstanceOf('React\Http\Request', $request); - $this->assertEquals( - ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']], - $request->getPost() - ); - $this->assertEquals(3, count($files)); - } - - public function testShouldReceivePostInBody() - { - $request = null; - $body = null; - - $stream = new ThroughStream(); - $parser = new RequestParser($stream); - $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$body) { - $request = $parsedRequest; - $body = $parsedBodyBuffer; - }); - - $stream->write($this->createPostWithContent()); - - $this->assertInstanceOf('React\Http\Request', $request); - $this->assertSame('', $body); - $this->assertEquals( - ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']], - $request->getPost() - ); - } - - public function testShouldReceivePostInBodySplit() - { - $request = null; - $body = null; - - $stream = new ThroughStream(); - $parser = new RequestParser($stream); - $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$body) { - $request = $parsedRequest; - $body = $parsedBodyBuffer; - }); - - list($data, $data2) = $this->createPostWithContentSplit(); - $stream->write($data); - $stream->write($data2); - - $this->assertInstanceOf('React\Http\Request', $request); - $this->assertSame('', $body); - $this->assertEquals( - ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']], - $request->getPost() - ); - } - public function testHeaderOverflowShouldEmitError() { $error = null; @@ -309,80 +236,4 @@ private function createAdvancedPostRequest($content = '', $len = 0) return $data; } - - private function createPostWithContent() - { - $data = "POST /foo?bar=baz HTTP/1.1\r\n"; - $data .= "Host: localhost:8080\r\n"; - $data .= "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:32.0) Gecko/20100101 Firefox/32.0\r\n"; - $data .= "Connection: close\r\n"; - $data .= "Content-Type: application/x-www-form-urlencoded\r\n"; - $data .= "Content-Length: 79\r\n"; - $data .= "\r\n"; - $data .= "user=single&user2=second&users%5B%5D=first+in+array&users%5B%5D=second+in+array\r\n"; - - return $data; - } - - private function createPostWithContentSplit() - { - $data = "POST /foo?bar=baz HTTP/1.1\r\n"; - $data .= "Host: localhost:8080\r\n"; - $data .= "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:32.0) Gecko/20100101 Firefox/32.0\r\n"; - $data .= "Connection: close\r\n"; - $data .= "Content-Type: application/x-www-form-urlencoded\r\n"; - $data .= "Content-Length: 79\r\n"; - $data .= "\r\n"; - $data .= "user=single&user2=second&us"; - $data2 = "ers%5B%5D=first+in+array&users%5B%5D=second+in+array\r\n"; - - return [$data, $data2]; - } - - private function createMultipartRequest() - { - $data = "POST / HTTP/1.1\r\n"; - $data .= "Host: localhost:8080\r\n"; - $data .= "Connection: close\r\n"; - $data .= "Content-Type: multipart/form-data; boundary=---------------------------12758086162038677464950549563\r\n"; - $data .= "Content-Length: 1097\r\n"; - $data .= "\r\n"; - - $data .= "-----------------------------12758086162038677464950549563\r\n"; - $data .= "Content-Disposition: form-data; name=\"user\"\r\n"; - $data .= "\r\n"; - $data .= "single\r\n"; - $data .= "-----------------------------12758086162038677464950549563\r\n"; - $data .= "Content-Disposition: form-data; name=\"user2\"\r\n"; - $data .= "\r\n"; - $data .= "second\r\n"; - $data .= "-----------------------------12758086162038677464950549563\r\n"; - $data .= "Content-Disposition: form-data; name=\"users[]\"\r\n"; - $data .= "\r\n"; - $data .= "first in array\r\n"; - $data .= "-----------------------------12758086162038677464950549563\r\n"; - $data .= "Content-Disposition: form-data; name=\"users[]\"\r\n"; - $data .= "\r\n"; - $data .= "second in array\r\n"; - $data .= "-----------------------------12758086162038677464950549563\r\n"; - $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"User.php\"\r\n"; - $data .= "Content-Type: text/php\r\n"; - $data .= "\r\n"; - $data .= " Date: Wed, 23 Mar 2016 07:43:37 +0100 Subject: [PATCH 14/64] Replaced array_key_exists with isset --- src/Parser/FormUrlencoded.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Parser/FormUrlencoded.php b/src/Parser/FormUrlencoded.php index d99b1519..baf3aaed 100644 --- a/src/Parser/FormUrlencoded.php +++ b/src/Parser/FormUrlencoded.php @@ -36,7 +36,7 @@ public function feed($data) $this->buffer .= $data; if ( - array_key_exists('Content-Length', $this->request->getHeaders()) && + isset($this->request->getHeaders()['Content-Length']) && strlen($this->buffer) >= $this->request->getHeaders()['Content-Length'] ) { $this->buffer = substr($this->buffer, 0, $this->request->getHeaders()['Content-Length']); From 8c0b93e39e583cbec0bd85569c88b182b212d973 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 23 Mar 2016 07:47:58 +0100 Subject: [PATCH 15/64] Don't exactly match content type for FormUrlencoded in case extra data is passed in the header --- src/FormParserFactory.php | 2 +- tests/FormParserFactoryTest.php | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/FormParserFactory.php b/src/FormParserFactory.php index a0d4e5f4..613cc878 100644 --- a/src/FormParserFactory.php +++ b/src/FormParserFactory.php @@ -18,7 +18,7 @@ public static function create(Request $request) return new Multipart($request); } - if (strtolower($headers['Content-Type']) == 'application/x-www-form-urlencoded') { + if (strpos(strtolower($headers['Content-Type']), 'application/x-www-form-urlencoded') === 0) { return new FormUrlencoded($request); } } diff --git a/tests/FormParserFactoryTest.php b/tests/FormParserFactoryTest.php index 1efc5b71..ca2d9b00 100644 --- a/tests/FormParserFactoryTest.php +++ b/tests/FormParserFactoryTest.php @@ -16,6 +16,15 @@ public function testMultipart() $this->assertInstanceOf('React\Http\Parser\Multipart', $parser); } + public function testMultipartUTF8() + { + $request = new Request('POST', 'http://example.com/', [], 1.1, [ + 'Content-Type' => 'multipart/mixed; boundary=---------------------------12758086162038677464950549563; charset=utf8', + ]); + $parser = FormParserFactory::create($request); + $this->assertInstanceOf('React\Http\Parser\Multipart', $parser); + } + public function testFormUrlencoded() { $request = new Request('POST', 'http://example.com/', [], 1.1, [ @@ -24,4 +33,13 @@ public function testFormUrlencoded() $parser = FormParserFactory::create($request); $this->assertInstanceOf('React\Http\Parser\FormUrlencoded', $parser); } + + public function testFormUrlencodedUTF8() + { + $request = new Request('POST', 'http://example.com/', [], 1.1, [ + 'Content-Type' => 'application/x-www-form-urlencoded; charset=utf8', + ]); + $parser = FormParserFactory::create($request); + $this->assertInstanceOf('React\Http\Parser\FormUrlencoded', $parser); + } } From a09ec5f676d09b50f0140da4675647a72bcdd3cf Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 23 Mar 2016 07:49:41 +0100 Subject: [PATCH 16/64] Fetch content type and force it's contents to lowercase so we only have to do that once --- src/FormParserFactory.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/FormParserFactory.php b/src/FormParserFactory.php index 613cc878..f85393dc 100644 --- a/src/FormParserFactory.php +++ b/src/FormParserFactory.php @@ -14,11 +14,13 @@ public static function create(Request $request) if (!array_key_exists('Content-Type', $headers)) { } - if (strpos($headers['Content-Type'], 'multipart/') === 0) { + $contentType = strtolower($headers['Content-Type']); + + if (strpos($contentType, 'multipart/') === 0) { return new Multipart($request); } - if (strpos(strtolower($headers['Content-Type']), 'application/x-www-form-urlencoded') === 0) { + if (strpos($contentType, 'application/x-www-form-urlencoded') === 0) { return new FormUrlencoded($request); } } From e4925f95e727e76e0bd292c6e8c731052964c0e7 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 23 Mar 2016 17:22:13 +0100 Subject: [PATCH 17/64] Moved initial content length header detection to the constructor --- src/Parser/FormUrlencoded.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Parser/FormUrlencoded.php b/src/Parser/FormUrlencoded.php index baf3aaed..c516ff56 100644 --- a/src/Parser/FormUrlencoded.php +++ b/src/Parser/FormUrlencoded.php @@ -17,6 +17,11 @@ class FormUrlencoded */ protected $request; + /** + * @var bool|integer + */ + protected $contentLength = false; + /** * @param Request $request */ @@ -26,6 +31,10 @@ public function __construct(Request $request) $this->request->on('data', [$this, 'feed']); $this->request->on('close', [$this, 'finish']); + + if (isset($this->request->getHeaders()['Content-Length'])) { + $this->contentLength = $this->request->getHeaders()['Content-Length']; + } } /** @@ -36,10 +45,10 @@ public function feed($data) $this->buffer .= $data; if ( - isset($this->request->getHeaders()['Content-Length']) && - strlen($this->buffer) >= $this->request->getHeaders()['Content-Length'] + $this->contentLength !== false && + strlen($this->buffer) >= $this->contentLength ) { - $this->buffer = substr($this->buffer, 0, $this->request->getHeaders()['Content-Length']); + $this->buffer = substr($this->buffer, 0, $this->contentLength); $this->finish(); } } From 085e01ddec979a34ee27589b1c6cfaa600119d20 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 23 Mar 2016 17:33:26 +0100 Subject: [PATCH 18/64] Removed post property from Request --- src/Request.php | 15 --------------- tests/Parser/FormUrlencodedTest.php | 12 ++++++++++-- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/Request.php b/src/Request.php index 4dfc26da..38ad2900 100644 --- a/src/Request.php +++ b/src/Request.php @@ -16,7 +16,6 @@ class Request extends EventEmitter implements ReadableStreamInterface private $httpVersion; private $headers; private $body; - private $post = []; // metadata, implicitly added externally public $remoteAddress; @@ -29,10 +28,6 @@ public function __construct($method, $url, $query = array(), $httpVersion = '1.1 $this->httpVersion = $httpVersion; $this->headers = $headers; $this->body = $body; - - $this->on('post', function ($key, $value) { - $this->addPost($key, $value); - }); } public function getMethod() @@ -80,16 +75,6 @@ public function appendBody($data) $this->body .= $data; } - public function getPost() - { - return $this->post; - } - - public function setPost($post) - { - $this->post = $post; - } - public function getRemoteAddress() { return $this->remoteAddress; diff --git a/tests/Parser/FormUrlencodedTest.php b/tests/Parser/FormUrlencodedTest.php index 3910c116..540c2595 100644 --- a/tests/Parser/FormUrlencodedTest.php +++ b/tests/Parser/FormUrlencodedTest.php @@ -11,14 +11,22 @@ class FormUrlencodedTest extends TestCase { public function testParse() { + $post = []; $request = new Request('POST', 'http://example.com/'); + $request->on('post', function ($key, $value) use (&$post) { + $post[] = [$key, $value]; + }); new FormUrlencoded($request); $request->emit('data', ['user=single&user2=second&us']); $request->emit('data', ['ers%5B%5D=first+in+array&users%5B%5D=second+in+array']); $request->emit('close'); $this->assertEquals( - ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']], - $request->getPost() + [ + ['user', 'single'], + ['user2', 'second'], + ['users', ['first in array', 'second in array']], + ], + $post ); } } From 92b6a73d7257afc767d36d6dd12d803d57de1072 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 23 Mar 2016 17:41:49 +0100 Subject: [PATCH 19/64] Remove body property from Request, restoring it to the state it was in with 0.4.1 --- src/Request.php | 40 +------------------------------------ src/RequestParser.php | 1 - tests/RequestParserTest.php | 2 -- 3 files changed, 1 insertion(+), 42 deletions(-) diff --git a/src/Request.php b/src/Request.php index 38ad2900..03c7ea00 100644 --- a/src/Request.php +++ b/src/Request.php @@ -15,19 +15,17 @@ class Request extends EventEmitter implements ReadableStreamInterface private $query; private $httpVersion; private $headers; - private $body; // metadata, implicitly added externally public $remoteAddress; - public function __construct($method, $url, $query = array(), $httpVersion = '1.1', $headers = array(), $body = '') + public function __construct($method, $url, $query = array(), $httpVersion = '1.1', $headers = array()) { $this->method = $method; $this->url = $url; $this->query = $query; $this->httpVersion = $httpVersion; $this->headers = $headers; - $this->body = $body; } public function getMethod() @@ -60,21 +58,6 @@ public function getHeaders() return $this->headers; } - public function getBody() - { - return $this->body; - } - - public function setBody($body) - { - $this->body = $body; - } - - public function appendBody($data) - { - $this->body .= $data; - } - public function getRemoteAddress() { return $this->remoteAddress; @@ -113,25 +96,4 @@ public function pipe(WritableStreamInterface $dest, array $options = array()) return $dest; } - - /** - * Put the file or post where it belongs, - * The key names can be simple, or containing [] - * it can also be a named key - * - * @param $key - * @param $content - */ - protected function addPost($key, $content) - { - if (preg_match('/^(.*)\[(.*)\]$/i', $key, $tmp)) { - if (!empty($tmp[2])) { - $this->post[$tmp[1]][$tmp[2]] = $content; - } else { - $this->post[$tmp[1]][] = $content; - } - } else { - $this->post[$key] = $content; - } - } } diff --git a/src/RequestParser.php b/src/RequestParser.php index aeceb0cb..87518433 100644 --- a/src/RequestParser.php +++ b/src/RequestParser.php @@ -49,7 +49,6 @@ public function feed($data) $this->stream->removeListener('data', [$this, 'feed']); $this->request = $this->parseHeaders($headers . "\r\n\r\n"); - $this->request->setBody($buffer); $this->emit('headers', array($this->request, $buffer)); $this->removeAllListeners(); diff --git a/tests/RequestParserTest.php b/tests/RequestParserTest.php index cdf15d94..33b0f4b7 100644 --- a/tests/RequestParserTest.php +++ b/tests/RequestParserTest.php @@ -124,7 +124,6 @@ public function testShouldNotReceiveBodyContent() $stream->write($content2); $this->assertInstanceOf('React\Http\Request', $request); - $this->assertEquals('', $request->getBody()); $this->assertSame($body, ''); } @@ -148,7 +147,6 @@ public function testShouldReceiveBodyContentPartial() $stream->write($content2); $this->assertInstanceOf('React\Http\Request', $request); - $this->assertEquals($content1, $request->getBody()); $this->assertSame($body, $content1); } From 650057958bb57666d0a6d642b5334f9536633715 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 24 Mar 2016 07:47:44 +0100 Subject: [PATCH 20/64] Case insensitive multipart chunk headers --- src/Parser/Multipart.php | 4 ++-- tests/Parser/MultipartTest.php | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Parser/Multipart.php b/src/Parser/Multipart.php index bfd66542..50bd3feb 100644 --- a/src/Parser/Multipart.php +++ b/src/Parser/Multipart.php @@ -128,9 +128,9 @@ protected function octetStream($string) protected function file($firstChunk, $streaming = true) { preg_match('/name=\"([^\"]*)\"; filename=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $firstChunk, $match); - preg_match('/Content-Type: ([^\"]*)/', $match[3], $mime); + preg_match('/Content-Type: ([^\"]*)/i', $match[3], $mime); - $content = preg_replace('/Content-Type: (.*)[^\n\r]/', '', $match[3]); + $content = preg_replace('/Content-Type: (.*)[^\n\r]/i', '', $match[3]); $content = ltrim($content, "\r\n"); // Put content in a stream diff --git a/tests/Parser/MultipartTest.php b/tests/Parser/MultipartTest.php index 604162b3..c807b527 100644 --- a/tests/Parser/MultipartTest.php +++ b/tests/Parser/MultipartTest.php @@ -82,11 +82,11 @@ public function testFileUpload() $data .= "second\r\n"; $request->emit('data', [$data]); $request->emit('data', ["--$boundary\r\n"]); - $request->emit('data', ["Content-Disposition: form-data; name=\"user\"\r\n"]); + $request->emit('data', ["Content-disposition: form-data; name=\"user\"\r\n"]); $request->emit('data', ["\r\n"]); $request->emit('data', ["single\r\n"]); $request->emit('data', ["--$boundary\r\n"]); - $request->emit('data', ["Content-Disposition: form-data; name=\"user2\"\r\n"]); + $request->emit('data', ["content-Disposition: form-data; name=\"user2\"\r\n"]); $request->emit('data', ["\r\n"]); $request->emit('data', ["second\r\n"]); $request->emit('data', ["--$boundary\r\n"]); @@ -99,7 +99,7 @@ public function testFileUpload() $request->emit('data', ["second in array\r\n"]); $request->emit('data', ["--$boundary\r\n"]); $request->emit('data', ["Content-Disposition: form-data; name=\"file\"; filename=\"User.php\"\r\n"]); - $request->emit('data', ["Content-Type: text/php\r\n"]); + $request->emit('data', ["Content-type: text/php\r\n"]); $request->emit('data', ["\r\n"]); $request->emit('data', ["emit('data', ["\r\n"]); @@ -109,7 +109,7 @@ public function testFileUpload() $request->emit('data', [$lines[1]]); $request->emit('data', ["\r\n"]); $request->emit('data', ["Content-Disposition: form-data; name=\"files[]\"; filename=\"blank.gif\"\r\n"]); - $request->emit('data', ["Content-Type: image/gif\r\n"]); + $request->emit('data', ["content-Type: image/gif\r\n"]); $request->emit('data', ["X-Foo-Bar: base64\r\n"]); $request->emit('data', ["\r\n"]); $request->emit('data', [$file . "\r\n"]); From d525755623ae8a2776ff196bae0d0523ebb77087 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 24 Mar 2016 07:56:49 +0100 Subject: [PATCH 21/64] Headers are made case insensitive after getHeaders --- src/FormParserFactory.php | 5 +++-- src/Parser/FormUrlencoded.php | 7 +++++-- src/Parser/Multipart.php | 3 ++- tests/FormParserFactoryTest.php | 18 ++++++++++++++++++ 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/FormParserFactory.php b/src/FormParserFactory.php index f85393dc..fbad89ce 100644 --- a/src/FormParserFactory.php +++ b/src/FormParserFactory.php @@ -10,11 +10,12 @@ class FormParserFactory public static function create(Request $request) { $headers = $request->getHeaders(); + $headers = array_change_key_case($headers, CASE_LOWER); - if (!array_key_exists('Content-Type', $headers)) { + if (!array_key_exists('content-type', $headers)) { } - $contentType = strtolower($headers['Content-Type']); + $contentType = strtolower($headers['content-type']); if (strpos($contentType, 'multipart/') === 0) { return new Multipart($request); diff --git a/src/Parser/FormUrlencoded.php b/src/Parser/FormUrlencoded.php index c516ff56..3fa46daa 100644 --- a/src/Parser/FormUrlencoded.php +++ b/src/Parser/FormUrlencoded.php @@ -32,8 +32,11 @@ public function __construct(Request $request) $this->request->on('data', [$this, 'feed']); $this->request->on('close', [$this, 'finish']); - if (isset($this->request->getHeaders()['Content-Length'])) { - $this->contentLength = $this->request->getHeaders()['Content-Length']; + $headers = $this->request->getHeaders(); + $headers = array_change_key_case($headers, CASE_LOWER); + + if (isset($headers['content-length'])) { + $this->contentLength = $headers['content-length']; } } diff --git a/src/Parser/Multipart.php b/src/Parser/Multipart.php index 50bd3feb..df0a049c 100644 --- a/src/Parser/Multipart.php +++ b/src/Parser/Multipart.php @@ -37,7 +37,8 @@ class Multipart public function __construct(Request $request) { $headers = $request->getHeaders(); - preg_match("/boundary=\"?(.*)\"?$/", $headers['Content-Type'], $matches); + $headers = array_change_key_case($headers, CASE_LOWER); + preg_match("/boundary=\"?(.*)\"?$/", $headers['content-type'], $matches); $this->boundary = $matches[1]; $this->request = $request; diff --git a/tests/FormParserFactoryTest.php b/tests/FormParserFactoryTest.php index ca2d9b00..6190541d 100644 --- a/tests/FormParserFactoryTest.php +++ b/tests/FormParserFactoryTest.php @@ -25,6 +25,15 @@ public function testMultipartUTF8() $this->assertInstanceOf('React\Http\Parser\Multipart', $parser); } + public function testMultipartHeaderCaseInsensitive() + { + $request = new Request('POST', 'http://example.com/', [], 1.1, [ + 'CONTENT-TYPE' => 'multipart/mixed; boundary=---------------------------12758086162038677464950549563', + ]); + $parser = FormParserFactory::create($request); + $this->assertInstanceOf('React\Http\Parser\Multipart', $parser); + } + public function testFormUrlencoded() { $request = new Request('POST', 'http://example.com/', [], 1.1, [ @@ -42,4 +51,13 @@ public function testFormUrlencodedUTF8() $parser = FormParserFactory::create($request); $this->assertInstanceOf('React\Http\Parser\FormUrlencoded', $parser); } + + public function testFormUrlencodedHeaderCaseInsensitive() + { + $request = new Request('POST', 'http://example.com/', [], 1.1, [ + 'content-type' => 'application/x-www-form-urlencoded', + ]); + $parser = FormParserFactory::create($request); + $this->assertInstanceOf('React\Http\Parser\FormUrlencoded', $parser); + } } From 17552300216a37bfed44bc5efdbe66df7944e3d4 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 24 Mar 2016 18:45:16 +0100 Subject: [PATCH 22/64] Removed un-used imports --- src/RequestParser.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/RequestParser.php b/src/RequestParser.php index 87518433..56c8ce62 100644 --- a/src/RequestParser.php +++ b/src/RequestParser.php @@ -4,8 +4,6 @@ use Evenement\EventEmitter; use GuzzleHttp\Psr7 as gPsr; -use React\Http\Parser\FormUrlencoded; -use React\Http\Parser\Multipart; use React\Stream\ReadableStreamInterface; /** From 578f288089dea754ad0704fc5d3ae9e9601676b1 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 14 Apr 2016 08:01:35 +0200 Subject: [PATCH 23/64] NoBody object return from the form parser factory in case we don't have anything to parse but we do return an object in that case --- src/FormParserFactory.php | 4 ++++ src/Parser/NoBody.php | 8 ++++++++ 2 files changed, 12 insertions(+) create mode 100644 src/Parser/NoBody.php diff --git a/src/FormParserFactory.php b/src/FormParserFactory.php index fbad89ce..f59edf05 100644 --- a/src/FormParserFactory.php +++ b/src/FormParserFactory.php @@ -4,6 +4,7 @@ use React\Http\Parser\FormUrlencoded; use React\Http\Parser\Multipart; +use React\Http\Parser\NoBody; class FormParserFactory { @@ -13,6 +14,7 @@ public static function create(Request $request) $headers = array_change_key_case($headers, CASE_LOWER); if (!array_key_exists('content-type', $headers)) { + return new NoBody(); } $contentType = strtolower($headers['content-type']); @@ -24,5 +26,7 @@ public static function create(Request $request) if (strpos($contentType, 'application/x-www-form-urlencoded') === 0) { return new FormUrlencoded($request); } + + return new NoBody(); } } diff --git a/src/Parser/NoBody.php b/src/Parser/NoBody.php new file mode 100644 index 00000000..208a9fff --- /dev/null +++ b/src/Parser/NoBody.php @@ -0,0 +1,8 @@ + Date: Fri, 15 Apr 2016 08:00:05 +0200 Subject: [PATCH 24/64] Renamed to GPL as we are going to replace it --- src/Parser/{Multipart.php => MultipartGPL.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Parser/{Multipart.php => MultipartGPL.php} (100%) diff --git a/src/Parser/Multipart.php b/src/Parser/MultipartGPL.php similarity index 100% rename from src/Parser/Multipart.php rename to src/Parser/MultipartGPL.php From 3b0de3aca570605ecdca635bf257619bc996f53e Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 17 Apr 2016 21:43:14 +0200 Subject: [PATCH 25/64] Bare skeleton of the new Multipart parser --- src/Parser/Multipart.php | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/Parser/Multipart.php diff --git a/src/Parser/Multipart.php b/src/Parser/Multipart.php new file mode 100644 index 00000000..3510fb77 --- /dev/null +++ b/src/Parser/Multipart.php @@ -0,0 +1,42 @@ +request = $request; + $headers = $this->request->getHeaders(); + $headers = array_change_key_case($headers, CASE_LOWER); + $this->boundary = preg_match('/boundary="?(.*)"?$/', $headers['content-type'], $matches)[0]; + + $this->request->on('data', [$this, 'onData']); + } + + public function onData($data) + { + + } +} From e9bac7c208b1da61a21f474207bc5263f03da2ca Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 17 Apr 2016 22:25:18 +0200 Subject: [PATCH 26/64] Parser interface --- src/Parser/ParserInterface.php | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/Parser/ParserInterface.php diff --git a/src/Parser/ParserInterface.php b/src/Parser/ParserInterface.php new file mode 100644 index 00000000..fbe53d36 --- /dev/null +++ b/src/Parser/ParserInterface.php @@ -0,0 +1,11 @@ + Date: Sun, 17 Apr 2016 22:34:52 +0200 Subject: [PATCH 27/64] Detect boundary if none is present in headers; https://github.com/reactphp/http/pull/41/files#r57567088 --- src/Parser/Multipart.php | 24 ++++++++++++++++++++---- tests/Parser/MultipartTest.php | 2 +- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/Parser/Multipart.php b/src/Parser/Multipart.php index 3510fb77..9ef87c70 100644 --- a/src/Parser/Multipart.php +++ b/src/Parser/Multipart.php @@ -25,18 +25,34 @@ class Multipart implements ParserInterface protected $request; - public function _construct(Request $request) + public function __construct(Request $request) { $this->request = $request; $headers = $this->request->getHeaders(); $headers = array_change_key_case($headers, CASE_LOWER); - $this->boundary = preg_match('/boundary="?(.*)"?$/', $headers['content-type'], $matches)[0]; + preg_match('/boundary="?(.*)"?$/', $headers['content-type'], $matches); + + $dataMethod = 'findBoundary'; + if (isset($matches[1])) { + $this->boundary = $matches[1]; + $dataMethod = 'onData'; + } + $this->request->on('data', [$this, $dataMethod]); + } + + public function findBoundary($data) + { + $this->buffer .= $data; - $this->request->on('data', [$this, 'onData']); + if (substr($this->buffer, 0, 3) === '---' && strpos($this->buffer, "\r\n") !== false) { + $this->boundary = substr($this->buffer, 0, strpos($this->buffer, "\r\n")); + $this->request->removeListener('data', [$this, 'findBoundary']); + $this->request->on('data', [$this, 'onData']); + } } public function onData($data) { - + $this->buffer .= $data; } } diff --git a/tests/Parser/MultipartTest.php b/tests/Parser/MultipartTest.php index c807b527..ef63e346 100644 --- a/tests/Parser/MultipartTest.php +++ b/tests/Parser/MultipartTest.php @@ -59,7 +59,7 @@ public function testFileUpload() $boundary = "---------------------------12758086162038677464950549563"; $request = new Request('POST', 'http://example.com/', [], 1.1, [ - 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + 'Content-Type' => 'multipart/form-data', ]); $request->on('post', function ($key, $value) use (&$post) { $post[] = [$key => $value]; From 4de83e171f4939ca584d2c37e45cf8ba58f3612c Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 18 Apr 2016 13:17:07 +0200 Subject: [PATCH 28/64] Pre set boundary ending and size --- src/Parser/Multipart.php | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Parser/Multipart.php b/src/Parser/Multipart.php index 9ef87c70..907cce86 100644 --- a/src/Parser/Multipart.php +++ b/src/Parser/Multipart.php @@ -14,6 +14,16 @@ class Multipart implements ParserInterface */ protected $buffer = ''; + /** + * @var string + */ + protected $ending = ''; + + /** + * @var int + */ + protected $endingSize = 0; + /** * @var string */ @@ -34,18 +44,25 @@ public function __construct(Request $request) $dataMethod = 'findBoundary'; if (isset($matches[1])) { - $this->boundary = $matches[1]; + $this->setBoundary($matches[1]); $dataMethod = 'onData'; } $this->request->on('data', [$this, $dataMethod]); } + protected function setBoundary($boundary) + { + $this->boundary = $boundary; + $this->ending = $this->boundary . "--\r\n"; + $this->endingSize = strlen($this->ending); + } + public function findBoundary($data) { $this->buffer .= $data; if (substr($this->buffer, 0, 3) === '---' && strpos($this->buffer, "\r\n") !== false) { - $this->boundary = substr($this->buffer, 0, strpos($this->buffer, "\r\n")); + $this->setBoundary(substr($this->buffer, 0, strpos($this->buffer, "\r\n"))); $this->request->removeListener('data', [$this, 'findBoundary']); $this->request->on('data', [$this, 'onData']); } From 3c76b9f7bd59c1dd0d4304864b893ee92def40e3 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 19 Apr 2016 17:24:11 +0200 Subject: [PATCH 29/64] Parse header and post chunks --- src/Parser/Multipart.php | 87 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/Parser/Multipart.php b/src/Parser/Multipart.php index 907cce86..80003d49 100644 --- a/src/Parser/Multipart.php +++ b/src/Parser/Multipart.php @@ -71,5 +71,92 @@ public function findBoundary($data) public function onData($data) { $this->buffer .= $data; + + if ( + strpos($this->buffer, $this->boundary) < strrpos($this->buffer, "\r\n\r\n") || + substr($this->buffer, -1, $this->endingSize) === $this->ending + ) { + $this->parseBuffer(); + } + } + + protected function parseBuffer() + { + $chunks = explode($this->boundary, $this->buffer); + $this->buffer = array_pop($chunks); + foreach ($chunks as $chunk) { + $this->parseChunk(ltrim($chunk)); + } + } + + protected function parseChunk($chunk) + { + if ($chunk == '') { + return; + } + + list ($header, $body) = explode("\r\n\r\n", $chunk); + $headers = $this->parseHeaders($header); + + if (!isset($headers['content-disposition'])) { + return; + } + + if ($this->headerStartsWith($headers['content-disposition'], 'name')) { + $this->parsePost($headers, $body); + return; + } + } + + protected function parsePost($headers, $body) + { + foreach ($headers['content-disposition'] as $part) { + if (strpos($part, 'name') === 0) { + preg_match('/name="?(.*)"?$/', $part, $matches); + $this->emit('post', [ + $matches[1], + $body, + $headers, + ]); + } + } + } + + protected function parseHeaders($header) + { + $headers = []; + + foreach (explode("\r\n", $header) as $line) { + list($key, $values) = explode(':', $line); + $key = trim($key); + $key = strtolower($key); + $values = explode(';', $values); + $values = array_map('trim', $values); + $headers[$key] = $values; + } + + return $headers; + } + + protected function headerStartsWith(array $header, $needle) + { + foreach ($header as $part) { + if (strpos($part, $needle) === 0) { + return true; + } + } + + return false; + } + + protected function headerContains(array $header, $needle) + { + foreach ($header as $part) { + if (strpos($part, $needle) !== false) { + return true; + } + } + + return false; } } From 64ff430b8ba73a530c15d0013d071860b07612bd Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 23 Apr 2016 21:57:11 +0200 Subject: [PATCH 30/64] Return NoBody parser in case the factory can't find out which parser to use --- src/FormParserFactory.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/FormParserFactory.php b/src/FormParserFactory.php index f59edf05..aa04100d 100644 --- a/src/FormParserFactory.php +++ b/src/FormParserFactory.php @@ -5,16 +5,21 @@ use React\Http\Parser\FormUrlencoded; use React\Http\Parser\Multipart; use React\Http\Parser\NoBody; +use React\Http\Parser\ParserInterface; class FormParserFactory { + /** + * @param Request $request + * @return ParserInterface + */ public static function create(Request $request) { $headers = $request->getHeaders(); $headers = array_change_key_case($headers, CASE_LOWER); if (!array_key_exists('content-type', $headers)) { - return new NoBody(); + return new NoBody($request); } $contentType = strtolower($headers['content-type']); @@ -27,6 +32,6 @@ public static function create(Request $request) return new FormUrlencoded($request); } - return new NoBody(); + return new NoBody($request); } } From 4b8655365dc44c246bb660ea0418736940abab61 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 23 Apr 2016 21:58:18 +0200 Subject: [PATCH 31/64] Added file support to the multipart parser --- src/Parser/Multipart.php | 101 +++++++++++++++++++++++++++++++-- tests/Parser/MultipartTest.php | 74 +++++++++++++++++------- 2 files changed, 149 insertions(+), 26 deletions(-) diff --git a/src/Parser/Multipart.php b/src/Parser/Multipart.php index 80003d49..b81e33dc 100644 --- a/src/Parser/Multipart.php +++ b/src/Parser/Multipart.php @@ -3,7 +3,10 @@ namespace React\Http\Parser; use Evenement\EventEmitterTrait; +use React\Http\File; use React\Http\Request; +use React\Stream\ThroughStream; +use React\Stream\Util; class Multipart implements ParserInterface { @@ -48,12 +51,15 @@ public function __construct(Request $request) $dataMethod = 'onData'; } $this->request->on('data', [$this, $dataMethod]); + Util::forwardEvents($this->request, $this, [ + 'close', + ]); } protected function setBoundary($boundary) { - $this->boundary = $boundary; - $this->ending = $this->boundary . "--\r\n"; + $this->boundary = substr($boundary, 1); + $this->ending = '--' . $this->boundary . "--\r\n"; $this->endingSize = strlen($this->ending); } @@ -82,11 +88,22 @@ public function onData($data) protected function parseBuffer() { - $chunks = explode($this->boundary, $this->buffer); + $chunks = preg_split('/\\-+' . $this->boundary . '/', $this->buffer); $this->buffer = array_pop($chunks); foreach ($chunks as $chunk) { $this->parseChunk(ltrim($chunk)); } + + $split = explode("\r\n\r\n", $this->buffer); + if (count($split) <= 1) { + return; + } + + $headers = $this->parseHeaders($split[0]); + if (isset($headers['content-disposition']) && $this->headerStartsWith($headers['content-disposition'], 'filename')) { + $this->parseFile($headers, $split[1]); + $this->buffer = ''; + } } protected function parseChunk($chunk) @@ -102,20 +119,80 @@ protected function parseChunk($chunk) return; } + if ($this->headerStartsWith($headers['content-disposition'], 'filename')) { + $this->parseFile($headers, $body, false); + return; + } + if ($this->headerStartsWith($headers['content-disposition'], 'name')) { $this->parsePost($headers, $body); return; } } + protected function parseFile($headers, $body, $streaming = true) + { + if ( + !$this->headerContains($headers['content-disposition'], 'name=') || + !$this->headerContains($headers['content-disposition'], 'filename=') + ) { + return; + } + + $stream = new ThroughStream(); + $this->emit('file', [ + new File( + $this->getFieldFromHeader($headers['content-disposition'], 'name'), + $this->getFieldFromHeader($headers['content-disposition'], 'filename'), + $headers['content-type'][0], + $stream + ), + $headers, + ]); + + if (!$streaming) { + $stream->end($body); + return; + } + + $this->request->removeListener('data', [$this, 'onData']); + $this->request->on('data', $this->chunkStreamFunc($stream)); + $stream->write($body); + } + + protected function chunkStreamFunc(ThroughStream $stream) + { + $buffer = ''; + $func = function($data) use (&$func, &$buffer, $stream) { + $buffer .= $data; + if (strpos($buffer, $this->boundary) !== false) { + $chunks = preg_split('/\\-+' . $this->boundary . '/', $this->buffer); + $chunk = array_shift($chunks); + $stream->end($chunk); + + $this->request->removeListener('data', $func); + $this->request->on('data', [$this, 'onData']); + + $this->onData(implode($this->boundary, $chunks)); + return; + } + + if (strlen($buffer) >= strlen($this->boundary) * 3) { + $stream->write($buffer); + $buffer = ''; + } + }; + return $func; + } + protected function parsePost($headers, $body) { foreach ($headers['content-disposition'] as $part) { if (strpos($part, 'name') === 0) { - preg_match('/name="?(.*)"?$/', $part, $matches); + preg_match('/name="?(.*)"$/', $part, $matches); $this->emit('post', [ $matches[1], - $body, + trim($body), $headers, ]); } @@ -126,7 +203,7 @@ protected function parseHeaders($header) { $headers = []; - foreach (explode("\r\n", $header) as $line) { + foreach (explode("\r\n", trim($header)) as $line) { list($key, $values) = explode(':', $line); $key = trim($key); $key = strtolower($key); @@ -159,4 +236,16 @@ protected function headerContains(array $header, $needle) return false; } + + protected function getFieldFromHeader(array $header, $field) + { + foreach ($header as $part) { + if (strpos($part, $field) === 0) { + preg_match('/' . $field . '="?(.*)"$/', $part, $matches); + return $matches[1]; + } + } + + return ''; + } } diff --git a/tests/Parser/MultipartTest.php b/tests/Parser/MultipartTest.php index ef63e346..56a8f54c 100644 --- a/tests/Parser/MultipartTest.php +++ b/tests/Parser/MultipartTest.php @@ -5,7 +5,6 @@ use React\Http\FileInterface; use React\Http\Parser\Multipart; use React\Http\Request; -use React\Stream\ThroughStream; use React\Tests\Http\TestCase; class MultipartParserTest extends TestCase @@ -21,10 +20,12 @@ public function testPostKey() $request = new Request('POST', 'http://example.com/', [], 1.1, [ 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, ]); - $request->on('post', function ($key, $value) use (&$post) { + + $parser = new Multipart($request); + $parser->on('post', function ($key, $value) use (&$post) { $post[$key] = $value; }); - $request->on('file', function (FileInterface $file) use (&$files) { + $parser->on('file', function (FileInterface $file) use (&$files) { $files[] = $file; }); @@ -38,7 +39,6 @@ public function testPostKey() $data .= "second\r\n"; $data .= "--$boundary--\r\n"; - new Multipart($request); $request->emit('data', [$data]); $this->assertEmpty($files); @@ -61,17 +61,18 @@ public function testFileUpload() $request = new Request('POST', 'http://example.com/', [], 1.1, [ 'Content-Type' => 'multipart/form-data', ]); - $request->on('post', function ($key, $value) use (&$post) { + + $multipart = new Multipart($request); + + $multipart->on('post', function ($key, $value) use (&$post) { $post[] = [$key => $value]; }); - $request->on('file', function (FileInterface $file) use (&$files) { - $files[] = $file; + $multipart->on('file', function (FileInterface $file, $headers) use (&$files) { + $files[] = [$file, $headers]; }); $file = base64_decode("R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="); - new Multipart($request); - $data = "--$boundary\r\n"; $data .= "Content-Disposition: form-data; name=\"users[one]\"\r\n"; $data .= "\r\n"; @@ -135,16 +136,49 @@ public function testFileUpload() ); $this->assertEquals(3, count($files)); - $this->assertEquals('file', $files[0]->getName()); - $this->assertEquals('User.php', $files[0]->getFilename()); - $this->assertEquals('text/php', $files[0]->getType()); - - $this->assertEquals('files[]', $files[1]->getName()); - $this->assertEquals('blank.gif', $files[1]->getFilename()); - $this->assertEquals('image/gif', $files[1]->getType()); - - $this->assertEquals('files[]', $files[2]->getName()); - $this->assertEquals('User.php', $files[2]->getFilename()); - $this->assertEquals('text/php', $files[2]->getType()); + $this->assertEquals('file', $files[0][0]->getName()); + $this->assertEquals('User.php', $files[0][0]->getFilename()); + $this->assertEquals('text/php', $files[0][0]->getType()); + $this->assertEquals([ + 'content-disposition' => [ + 'form-data', + 'name="file"', + 'filename="User.php"', + ], + 'content-type' => [ + 'text/php', + ], + ], $files[0][1]); + + $this->assertEquals('files[]', $files[1][0]->getName()); + $this->assertEquals('blank.gif', $files[1][0]->getFilename()); + $this->assertEquals('image/gif', $files[1][0]->getType()); + $this->assertEquals([ + 'content-disposition' => [ + 'form-data', + 'name="files[]"', + 'filename="blank.gif"', + ], + 'content-type' => [ + 'image/gif', + ], + 'x-foo-bar' => [ + 'base64', + ], + ], $files[1][1]); + + $this->assertEquals('files[]', $files[2][0]->getName()); + $this->assertEquals('User.php', $files[2][0]->getFilename()); + $this->assertEquals('text/php', $files[2][0]->getType()); + $this->assertEquals([ + 'content-disposition' => [ + 'form-data', + 'name="files[]"', + 'filename="User.php"', + ], + 'content-type' => [ + 'text/php', + ], + ], $files[2][1]); } } From 9b959d73029f7df684e26dc9aab84d98edf2e58b Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 23 Apr 2016 21:58:39 +0200 Subject: [PATCH 32/64] Removed GPL multipart parser --- src/Parser/MultipartGPL.php | 191 ------------------------------------ 1 file changed, 191 deletions(-) delete mode 100644 src/Parser/MultipartGPL.php diff --git a/src/Parser/MultipartGPL.php b/src/Parser/MultipartGPL.php deleted file mode 100644 index df0a049c..00000000 --- a/src/Parser/MultipartGPL.php +++ /dev/null @@ -1,191 +0,0 @@ -getHeaders(); - $headers = array_change_key_case($headers, CASE_LOWER); - preg_match("/boundary=\"?(.*)\"?$/", $headers['content-type'], $matches); - - $this->boundary = $matches[1]; - $this->request = $request; - - $this->request->on('data', [$this, 'feed']); - } - - /** - * Do the actual parsing - * - * @param string $data - */ - public function feed($data) - { - $this->buffer .= $data; - - $ending = $this->boundary . "--\r\n"; - $endSize = strlen($ending); - - if (strpos($this->buffer, $this->boundary) < strrpos($this->buffer, "\r\n\r\n") || substr($this->buffer, -1, $endSize) === $ending) { - $this->processBuffer(); - } - } - - protected function processBuffer() - { - $chunks = preg_split("/\\-+$this->boundary/", $this->buffer); - $this->buffer = array_pop($chunks); - foreach ($chunks as $chunk) { - $this->parseBlock($chunk); - } - - $lines = explode("\r\n", $this->buffer); - if (isset($lines[1]) && strpos($lines[1], 'filename') !== false) { - $this->file($this->buffer); - $this->buffer = ''; - return; - } - } - - /** - * Decide if we handle a file, post value or octet stream - * - * @param $string string - * @returns void - */ - protected function parseBlock($string) - { - if ($string == '') { - return; - } - - list(, $firstLine) = explode("\r\n", $string); - if (strpos($firstLine, 'filename') !== false) { - $this->file($string, false); - return; - } - - // This may never be called, if an octet stream - // has a filename it is catched by the previous - // condition already. - if (strpos($firstLine, '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->request->emit('post', [$match[1], $match[2]]); - } - - /** - * Parse a file - * - * @param string $firstChunk - */ - protected function file($firstChunk, $streaming = true) - { - preg_match('/name=\"([^\"]*)\"; filename=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $firstChunk, $match); - preg_match('/Content-Type: ([^\"]*)/i', $match[3], $mime); - - $content = preg_replace('/Content-Type: (.*)[^\n\r]/i', '', $match[3]); - $content = ltrim($content, "\r\n"); - - // Put content in a stream - $stream = new ThroughStream(); - - if ($streaming) { - $this->request->removeListener('data', [$this, 'feed']); - $buffer = ''; - $func = function($data) use (&$func, &$buffer, $stream) { - $buffer .= $data; - if (strpos($buffer, $this->boundary) !== false) { - $chunks = preg_split("/\\-+$this->boundary/", $buffer); - $chunk = array_shift($chunks); - $stream->end($chunk); - - $this->request->removeListener('data', $func); - $this->request->on('data', [$this, 'feed']); - - $this->request->emit('data', [implode($this->boundary, $chunks)]); - return; - } - - if (strlen($buffer) >= strlen($this->boundary) * 3) { - $stream->write($buffer); - $buffer = ''; - } - }; - $this->request->on('data', $func); - } - - $this->request->emit('file', [new File( - $match[1], // name - $match[2], // filename - explode("\r\n", trim($mime[1]))[0], // type - $stream - )]); - - if ($streaming) { - $stream->write($content); - } else { - $stream->end($content); - } - } - - /** - * Parse POST values - * - * @param $string - * @return array - */ - protected function post($string) - { - preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match); - - $this->request->emit('post', [$match[1], $match[2]]); - } -} From 2928193487c65e5e791c60ae7c4006e16145973e Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 23 Apr 2016 21:58:56 +0200 Subject: [PATCH 33/64] Added close event forwarding --- src/Parser/NoBody.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Parser/NoBody.php b/src/Parser/NoBody.php index 208a9fff..f75186df 100644 --- a/src/Parser/NoBody.php +++ b/src/Parser/NoBody.php @@ -2,7 +2,18 @@ namespace React\Http\Parser; -class NoBody +use Evenement\EventEmitterTrait; +use React\Http\Request; +use React\Stream\Util; + +class NoBody implements ParserInterface { + use EventEmitterTrait; + public function __construct(Request $request) + { + Util::forwardEvents($this->request, $this, [ + 'close', + ]); + } } From 8c34c921848a033073f639356f91ee5cb7bf3df7 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 23 Apr 2016 21:59:54 +0200 Subject: [PATCH 34/64] Emit events on parser instead of request --- src/Parser/FormUrlencoded.php | 8 +++++--- tests/Parser/FormUrlencodedTest.php | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Parser/FormUrlencoded.php b/src/Parser/FormUrlencoded.php index 3fa46daa..59758574 100644 --- a/src/Parser/FormUrlencoded.php +++ b/src/Parser/FormUrlencoded.php @@ -2,11 +2,13 @@ namespace React\Http\Parser; +use Evenement\EventEmitterTrait; use React\Http\Request; -use React\Stream\ReadableStreamInterface; -class FormUrlencoded +class FormUrlencoded implements ParserInterface { + use EventEmitterTrait; + /** * @var string */ @@ -62,7 +64,7 @@ public function finish() $this->request->removeListener('close', [$this, 'finish']); parse_str(trim($this->buffer), $result); foreach ($result as $key => $value) { - $this->request->emit('post', [$key, $value]); + $this->emit('post', [$key, $value]); } } } diff --git a/tests/Parser/FormUrlencodedTest.php b/tests/Parser/FormUrlencodedTest.php index 540c2595..cebdd0a7 100644 --- a/tests/Parser/FormUrlencodedTest.php +++ b/tests/Parser/FormUrlencodedTest.php @@ -13,10 +13,10 @@ public function testParse() { $post = []; $request = new Request('POST', 'http://example.com/'); - $request->on('post', function ($key, $value) use (&$post) { + $parser = new FormUrlencoded($request); + $parser->on('post', function ($key, $value) use (&$post) { $post[] = [$key, $value]; }); - new FormUrlencoded($request); $request->emit('data', ['user=single&user2=second&us']); $request->emit('data', ['ers%5B%5D=first+in+array&users%5B%5D=second+in+array']); $request->emit('close'); From 598d50f48907776ae5f1b3e493a408ffdabd192c Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 24 Apr 2016 22:46:43 +0200 Subject: [PATCH 35/64] Typo, $this->request doesn't exists --- src/Parser/NoBody.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Parser/NoBody.php b/src/Parser/NoBody.php index f75186df..8a3349f2 100644 --- a/src/Parser/NoBody.php +++ b/src/Parser/NoBody.php @@ -12,7 +12,7 @@ class NoBody implements ParserInterface public function __construct(Request $request) { - Util::forwardEvents($this->request, $this, [ + Util::forwardEvents($request, $this, [ 'close', ]); } From 44f2517b1957d0f9f95362ab0b8576da387cdd4b Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 2 May 2016 14:32:37 +0200 Subject: [PATCH 36/64] Deferred stream that waits until the request emits end --- composer.json | 6 ++- src/DeferredStream.php | 76 +++++++++++++++++++++++++++++++++ tests/DeferredStreamTest.php | 83 ++++++++++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 src/DeferredStream.php create mode 100644 tests/DeferredStreamTest.php diff --git a/composer.json b/composer.json index cd1194da..a7689d31 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,8 @@ "guzzlehttp/psr7": "^1.0", "react/socket": "^0.4", "react/stream": "^0.4", - "evenement/evenement": "^2.0" + "evenement/evenement": "^2.0", + "react/promise": "^1.0|^2.0" }, "autoload": { "psr-4": { @@ -19,5 +20,8 @@ "branch-alias": { "dev-master": "0.5-dev" } + }, + "require-dev": { + "clue/block-react": "^1.1" } } diff --git a/src/DeferredStream.php b/src/DeferredStream.php new file mode 100644 index 00000000..70720694 --- /dev/null +++ b/src/DeferredStream.php @@ -0,0 +1,76 @@ +on('post', function ($key, $value) use (&$postFields) { + self::extractPost($postFields, $key, $value); + }); + $parser->on('file', function (File $file) use (&$files) { + BufferedSink::createPromise($file->getStream())->then(function ($buffer) use ($file, &$files) { + $files[] = [ + 'file' => $file, + 'buffer' => $buffer, + ]; + }); + }); + $parser->on('end', function () use ($deferred, &$postFields, &$files) { + $deferred->resolve([ + 'post' => $postFields, + 'files' => $files, + ]); + }); + + return $deferred->promise(); + } + + public static function extractPost(&$postFields, $key, $value) + { + $chunks = explode('[', $key); + if (count($chunks) == 1) { + $postFields[$key] = $value; + return; + } + + $chunkKey = $chunks[0]; + if (!isset($postFields[$chunkKey])) { + $postFields[$chunkKey] = []; + } + + $parent = &$postFields; + for ($i = 1; $i < count($chunks); $i++) { + $previousChunkKey = $chunkKey; + if (!isset($parent[$previousChunkKey])) { + $parent[$previousChunkKey] = []; + } + $parent = &$parent[$previousChunkKey]; + $chunkKey = $chunks[$i]; + + if ($chunkKey == ']') { + $parent[] = $value; + return; + } + + $chunkKey = rtrim($chunkKey, ']'); + if ($i == count($chunks) - 1) { + $parent[$chunkKey] = $value; + return; + } + } + } +} diff --git a/tests/DeferredStreamTest.php b/tests/DeferredStreamTest.php new file mode 100644 index 00000000..fe73a5cc --- /dev/null +++ b/tests/DeferredStreamTest.php @@ -0,0 +1,83 @@ +emit('post', ['foo', 'bar']); + $parser->emit('post', ['array[]', 'foo']); + $parser->emit('post', ['array[]', 'bar']); + $parser->emit('post', ['dem[two]', 'bar']); + $parser->emit('post', ['dom[two][]', 'bar']); + + $stream = new ThroughStream(); + $file = new File('foo', 'bar.ext', 'text', $stream); + $parser->emit('file', [$file]); + $stream->end('foo.bar'); + + $parser->emit('end'); + + $result = Block\await($deferredStream, Factory::create(), 10); + $this->assertSame([ + 'foo' => 'bar', + 'array' => [ + 'foo', + 'bar', + ], + 'dem' => [ + 'two' => 'bar', + ], + 'dom' => [ + 'two' => [ + 'bar', + ], + ], + ], $result['post']); + + $this->assertSame('foo', $result['files'][0]['file']->getName()); + $this->assertSame('bar.ext', $result['files'][0]['file']->getFilename()); + $this->assertSame('text', $result['files'][0]['file']->getType()); + $this->assertSame('foo.bar', $result['files'][0]['buffer']); + } + + public function testExtractPost() + { + $postFields = []; + DeferredStream::extractPost($postFields, 'dem', 'value'); + DeferredStream::extractPost($postFields, 'dom[one][two][]', 'value_a'); + DeferredStream::extractPost($postFields, 'dom[one][two][]', 'value_b'); + DeferredStream::extractPost($postFields, 'dam[]', 'value_a'); + DeferredStream::extractPost($postFields, 'dam[]', 'value_b'); + DeferredStream::extractPost($postFields, 'dum[sum]', 'value'); + $this->assertSame([ + 'dem' => 'value', + 'dom' => [ + 'one' => [ + 'two' => [ + 'value_a', + 'value_b', + ], + ], + ], + 'dam' => [ + 'value_a', + 'value_b', + ], + 'dum' => [ + 'sum' => 'value', + ], + ], $postFields); + } +} From eb72ded3bfa393aa052563ee9cbdbde050fa6e83 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 2 May 2016 14:35:51 +0200 Subject: [PATCH 37/64] Emit end (not close) when the full body has been parsed --- src/Parser/FormUrlencoded.php | 3 ++- src/Parser/Multipart.php | 2 +- src/Parser/NoBody.php | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Parser/FormUrlencoded.php b/src/Parser/FormUrlencoded.php index 59758574..a52b009f 100644 --- a/src/Parser/FormUrlencoded.php +++ b/src/Parser/FormUrlencoded.php @@ -32,7 +32,7 @@ public function __construct(Request $request) $this->request = $request; $this->request->on('data', [$this, 'feed']); - $this->request->on('close', [$this, 'finish']); + $this->request->on('end', [$this, 'finish']); $headers = $this->request->getHeaders(); $headers = array_change_key_case($headers, CASE_LOWER); @@ -66,5 +66,6 @@ public function finish() foreach ($result as $key => $value) { $this->emit('post', [$key, $value]); } + $this->emit('end'); } } diff --git a/src/Parser/Multipart.php b/src/Parser/Multipart.php index b81e33dc..4e4781d6 100644 --- a/src/Parser/Multipart.php +++ b/src/Parser/Multipart.php @@ -52,7 +52,7 @@ public function __construct(Request $request) } $this->request->on('data', [$this, $dataMethod]); Util::forwardEvents($this->request, $this, [ - 'close', + 'end', ]); } diff --git a/src/Parser/NoBody.php b/src/Parser/NoBody.php index 8a3349f2..7ac778e2 100644 --- a/src/Parser/NoBody.php +++ b/src/Parser/NoBody.php @@ -13,7 +13,7 @@ class NoBody implements ParserInterface public function __construct(Request $request) { Util::forwardEvents($request, $this, [ - 'close', + 'end', ]); } } From 621ee717ba5121d10ffc4b1cb53fecbb562f16b6 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 2 May 2016 14:38:50 +0200 Subject: [PATCH 38/64] Missed an emit close --- tests/Parser/FormUrlencodedTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Parser/FormUrlencodedTest.php b/tests/Parser/FormUrlencodedTest.php index cebdd0a7..146b31e2 100644 --- a/tests/Parser/FormUrlencodedTest.php +++ b/tests/Parser/FormUrlencodedTest.php @@ -19,7 +19,7 @@ public function testParse() }); $request->emit('data', ['user=single&user2=second&us']); $request->emit('data', ['ers%5B%5D=first+in+array&users%5B%5D=second+in+array']); - $request->emit('close'); + $request->emit('end'); $this->assertEquals( [ ['user', 'single'], From 87cf49eb69b2005eb0b9d1aa4031f9b3dc8e2e98 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 5 May 2016 22:17:56 +0200 Subject: [PATCH 39/64] Detect body end instead of waiting for end event --- src/Parser/Multipart.php | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Parser/Multipart.php b/src/Parser/Multipart.php index 4e4781d6..ac31a7b9 100644 --- a/src/Parser/Multipart.php +++ b/src/Parser/Multipart.php @@ -51,15 +51,12 @@ public function __construct(Request $request) $dataMethod = 'onData'; } $this->request->on('data', [$this, $dataMethod]); - Util::forwardEvents($this->request, $this, [ - 'end', - ]); } protected function setBoundary($boundary) { $this->boundary = substr($boundary, 1); - $this->ending = '--' . $this->boundary . "--\r\n"; + $this->ending = $this->boundary . "--\r\n"; $this->endingSize = strlen($this->ending); } @@ -77,18 +74,22 @@ public function findBoundary($data) public function onData($data) { $this->buffer .= $data; + $ending = strpos($this->buffer, $this->ending) == strlen($this->buffer) - $this->endingSize; if ( - strpos($this->buffer, $this->boundary) < strrpos($this->buffer, "\r\n\r\n") || - substr($this->buffer, -1, $this->endingSize) === $this->ending + strrpos($this->buffer, $this->boundary) < strrpos($this->buffer, "\r\n\r\n") || $ending ) { $this->parseBuffer(); } + + if ($ending) { + $this->emit('end'); + } } protected function parseBuffer() { - $chunks = preg_split('/\\-+' . $this->boundary . '/', $this->buffer); + $chunks = preg_split('/-+' . $this->boundary . '/', $this->buffer); $this->buffer = array_pop($chunks); foreach ($chunks as $chunk) { $this->parseChunk(ltrim($chunk)); @@ -99,7 +100,8 @@ protected function parseBuffer() return; } - $headers = $this->parseHeaders($split[0]); + $chunks = preg_split('/' . $this->boundary . '/', trim($split[0]), -1, PREG_SPLIT_NO_EMPTY); + $headers = $this->parseHeaders(trim($chunks[0])); if (isset($headers['content-disposition']) && $this->headerStartsWith($headers['content-disposition'], 'filename')) { $this->parseFile($headers, $split[1]); $this->buffer = ''; @@ -166,13 +168,17 @@ protected function chunkStreamFunc(ThroughStream $stream) $func = function($data) use (&$func, &$buffer, $stream) { $buffer .= $data; if (strpos($buffer, $this->boundary) !== false) { - $chunks = preg_split('/\\-+' . $this->boundary . '/', $this->buffer); + $chunks = preg_split('/-+' . $this->boundary . '/', $buffer); $chunk = array_shift($chunks); $stream->end($chunk); $this->request->removeListener('data', $func); $this->request->on('data', [$this, 'onData']); + if (count($chunks) == 1) { + array_unshift($chunks, ''); + } + $this->onData(implode($this->boundary, $chunks)); return; } From 1384f811a025c368d58dcf5262c6507688c3cc14 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 5 May 2016 22:54:57 +0200 Subject: [PATCH 40/64] Added isDone method to parsers for those cases where an end would be emitted within the constructor --- src/DeferredStream.php | 7 +++++++ src/Parser/DoneTrait.php | 18 ++++++++++++++++++ src/Parser/FormUrlencoded.php | 14 ++++++++------ src/Parser/Multipart.php | 3 ++- src/Parser/NoBody.php | 6 ++---- src/Parser/ParserInterface.php | 5 +++++ tests/DeferredStreamTest.php | 16 +++++++++++++++- tests/Parser/DummyParser.php | 23 +++++++++++++++++++++++ tests/Parser/FormUrlencodedTest.php | 12 ++++++++++-- tests/Parser/NoBodyTest.php | 17 +++++++++++++++++ 10 files changed, 107 insertions(+), 14 deletions(-) create mode 100644 src/Parser/DoneTrait.php create mode 100644 tests/Parser/DummyParser.php create mode 100644 tests/Parser/NoBodyTest.php diff --git a/src/DeferredStream.php b/src/DeferredStream.php index 70720694..589562b8 100644 --- a/src/DeferredStream.php +++ b/src/DeferredStream.php @@ -15,6 +15,13 @@ class DeferredStream */ public static function create(ParserInterface $parser) { + if ($parser->isDone()) { + return \React\Promise\resolve([ + 'post' => [], + 'files' => [], + ]); + } + $deferred = new Deferred(); $postFields = []; $files = []; diff --git a/src/Parser/DoneTrait.php b/src/Parser/DoneTrait.php new file mode 100644 index 00000000..dfbb3c5b --- /dev/null +++ b/src/Parser/DoneTrait.php @@ -0,0 +1,18 @@ +done; + } + + protected function markDone() + { + $this->done = true; + } +} diff --git a/src/Parser/FormUrlencoded.php b/src/Parser/FormUrlencoded.php index a52b009f..06dc67a6 100644 --- a/src/Parser/FormUrlencoded.php +++ b/src/Parser/FormUrlencoded.php @@ -8,6 +8,7 @@ class FormUrlencoded implements ParserInterface { use EventEmitterTrait; + use DoneTrait; /** * @var string @@ -31,15 +32,16 @@ public function __construct(Request $request) { $this->request = $request; - $this->request->on('data', [$this, 'feed']); - $this->request->on('end', [$this, 'finish']); - $headers = $this->request->getHeaders(); $headers = array_change_key_case($headers, CASE_LOWER); - if (isset($headers['content-length'])) { - $this->contentLength = $headers['content-length']; + if (!isset($headers['content-length'])) { + $this->markDone(); + return; } + + $this->contentLength = $headers['content-length']; + $this->request->on('data', [$this, 'feed']); } /** @@ -61,11 +63,11 @@ public function feed($data) public function finish() { $this->request->removeListener('data', [$this, 'feed']); - $this->request->removeListener('close', [$this, 'finish']); parse_str(trim($this->buffer), $result); foreach ($result as $key => $value) { $this->emit('post', [$key, $value]); } + $this->markDone(); $this->emit('end'); } } diff --git a/src/Parser/Multipart.php b/src/Parser/Multipart.php index ac31a7b9..560c78dc 100644 --- a/src/Parser/Multipart.php +++ b/src/Parser/Multipart.php @@ -6,11 +6,11 @@ use React\Http\File; use React\Http\Request; use React\Stream\ThroughStream; -use React\Stream\Util; class Multipart implements ParserInterface { use EventEmitterTrait; + use DoneTrait; /** * @var string @@ -83,6 +83,7 @@ public function onData($data) } if ($ending) { + $this->markDone(); $this->emit('end'); } } diff --git a/src/Parser/NoBody.php b/src/Parser/NoBody.php index 7ac778e2..224aedf9 100644 --- a/src/Parser/NoBody.php +++ b/src/Parser/NoBody.php @@ -4,16 +4,14 @@ use Evenement\EventEmitterTrait; use React\Http\Request; -use React\Stream\Util; class NoBody implements ParserInterface { use EventEmitterTrait; + use DoneTrait; public function __construct(Request $request) { - Util::forwardEvents($request, $this, [ - 'end', - ]); + $this->markDone(); } } diff --git a/src/Parser/ParserInterface.php b/src/Parser/ParserInterface.php index fbe53d36..76c47126 100644 --- a/src/Parser/ParserInterface.php +++ b/src/Parser/ParserInterface.php @@ -8,4 +8,9 @@ interface ParserInterface extends EventEmitterInterface { public function __construct(Request $request); + + /** + * @return bool + */ + public function isDone(); } diff --git a/tests/DeferredStreamTest.php b/tests/DeferredStreamTest.php index fe73a5cc..da6c78fa 100644 --- a/tests/DeferredStreamTest.php +++ b/tests/DeferredStreamTest.php @@ -9,12 +9,25 @@ use React\Http\Parser\NoBody; use React\Http\Request; use React\Stream\ThroughStream; +use React\Tests\Http\Parser\DummyParser; class DeferredStreamTest extends TestCase { + public function testDoneParser() + { + $parser = new DummyParser(new Request('get', 'http://example.com')); + $parser->setDone(); + $deferredStream = DeferredStream::create($parser); + $result = Block\await($deferredStream, Factory::create(), 10); + $this->assertSame([ + 'post' => [], + 'files' => [], + ], $result); + } + public function testDeferredStream() { - $parser = new NoBody(new Request('get', 'http://example.com')); + $parser = new DummyParser(new Request('get', 'http://example.com')); $deferredStream = DeferredStream::create($parser); $parser->emit('post', ['foo', 'bar']); $parser->emit('post', ['array[]', 'foo']); @@ -27,6 +40,7 @@ public function testDeferredStream() $parser->emit('file', [$file]); $stream->end('foo.bar'); + $parser->setDone(); $parser->emit('end'); $result = Block\await($deferredStream, Factory::create(), 10); diff --git a/tests/Parser/DummyParser.php b/tests/Parser/DummyParser.php new file mode 100644 index 00000000..b9472951 --- /dev/null +++ b/tests/Parser/DummyParser.php @@ -0,0 +1,23 @@ +markDone(); + } +} diff --git a/tests/Parser/FormUrlencodedTest.php b/tests/Parser/FormUrlencodedTest.php index 146b31e2..d58f494f 100644 --- a/tests/Parser/FormUrlencodedTest.php +++ b/tests/Parser/FormUrlencodedTest.php @@ -12,14 +12,15 @@ class FormUrlencodedTest extends TestCase public function testParse() { $post = []; - $request = new Request('POST', 'http://example.com/'); + $request = new Request('POST', 'http://example.com/', [], '1.1', [ + 'Content-Length' => 79, + ]); $parser = new FormUrlencoded($request); $parser->on('post', function ($key, $value) use (&$post) { $post[] = [$key, $value]; }); $request->emit('data', ['user=single&user2=second&us']); $request->emit('data', ['ers%5B%5D=first+in+array&users%5B%5D=second+in+array']); - $request->emit('end'); $this->assertEquals( [ ['user', 'single'], @@ -29,4 +30,11 @@ public function testParse() $post ); } + + public function testNoContentLength() + { + $request = new Request('POST', 'http://example.com/'); + $parser = new FormUrlencoded($request); + $this->assertTrue($parser->isDone()); + } } diff --git a/tests/Parser/NoBodyTest.php b/tests/Parser/NoBodyTest.php new file mode 100644 index 00000000..244bcb21 --- /dev/null +++ b/tests/Parser/NoBodyTest.php @@ -0,0 +1,17 @@ +assertTrue($parser->isDone()); + } +} From c68bb16d58c9676a778be88920341666cc5ac05f Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 10 May 2016 07:41:24 +0200 Subject: [PATCH 41/64] Dropped the done trait --- src/Parser/DoneTrait.php | 18 ------------------ src/Parser/FormUrlencoded.php | 7 ------- src/Parser/Multipart.php | 2 -- src/Parser/NoBody.php | 2 -- src/Parser/ParserInterface.php | 5 ----- tests/Parser/DummyParser.php | 7 ------- tests/Parser/FormUrlencodedTest.php | 7 ------- 7 files changed, 48 deletions(-) delete mode 100644 src/Parser/DoneTrait.php diff --git a/src/Parser/DoneTrait.php b/src/Parser/DoneTrait.php deleted file mode 100644 index dfbb3c5b..00000000 --- a/src/Parser/DoneTrait.php +++ /dev/null @@ -1,18 +0,0 @@ -done; - } - - protected function markDone() - { - $this->done = true; - } -} diff --git a/src/Parser/FormUrlencoded.php b/src/Parser/FormUrlencoded.php index 06dc67a6..a4ef30df 100644 --- a/src/Parser/FormUrlencoded.php +++ b/src/Parser/FormUrlencoded.php @@ -8,7 +8,6 @@ class FormUrlencoded implements ParserInterface { use EventEmitterTrait; - use DoneTrait; /** * @var string @@ -35,11 +34,6 @@ public function __construct(Request $request) $headers = $this->request->getHeaders(); $headers = array_change_key_case($headers, CASE_LOWER); - if (!isset($headers['content-length'])) { - $this->markDone(); - return; - } - $this->contentLength = $headers['content-length']; $this->request->on('data', [$this, 'feed']); } @@ -67,7 +61,6 @@ public function finish() foreach ($result as $key => $value) { $this->emit('post', [$key, $value]); } - $this->markDone(); $this->emit('end'); } } diff --git a/src/Parser/Multipart.php b/src/Parser/Multipart.php index 560c78dc..ffa88377 100644 --- a/src/Parser/Multipart.php +++ b/src/Parser/Multipart.php @@ -10,7 +10,6 @@ class Multipart implements ParserInterface { use EventEmitterTrait; - use DoneTrait; /** * @var string @@ -83,7 +82,6 @@ public function onData($data) } if ($ending) { - $this->markDone(); $this->emit('end'); } } diff --git a/src/Parser/NoBody.php b/src/Parser/NoBody.php index 224aedf9..98e7bae5 100644 --- a/src/Parser/NoBody.php +++ b/src/Parser/NoBody.php @@ -8,10 +8,8 @@ class NoBody implements ParserInterface { use EventEmitterTrait; - use DoneTrait; public function __construct(Request $request) { - $this->markDone(); } } diff --git a/src/Parser/ParserInterface.php b/src/Parser/ParserInterface.php index 76c47126..fbe53d36 100644 --- a/src/Parser/ParserInterface.php +++ b/src/Parser/ParserInterface.php @@ -8,9 +8,4 @@ interface ParserInterface extends EventEmitterInterface { public function __construct(Request $request); - - /** - * @return bool - */ - public function isDone(); } diff --git a/tests/Parser/DummyParser.php b/tests/Parser/DummyParser.php index b9472951..07d953c4 100644 --- a/tests/Parser/DummyParser.php +++ b/tests/Parser/DummyParser.php @@ -3,21 +3,14 @@ namespace React\Tests\Http\Parser; use Evenement\EventEmitterTrait; -use React\Http\Parser\DoneTrait; use React\Http\Parser\ParserInterface; use React\Http\Request; class DummyParser implements ParserInterface { use EventEmitterTrait; - use DoneTrait; public function __construct(Request $request) { } - - public function setDone() - { - $this->markDone(); - } } diff --git a/tests/Parser/FormUrlencodedTest.php b/tests/Parser/FormUrlencodedTest.php index d58f494f..9bddf0be 100644 --- a/tests/Parser/FormUrlencodedTest.php +++ b/tests/Parser/FormUrlencodedTest.php @@ -30,11 +30,4 @@ public function testParse() $post ); } - - public function testNoContentLength() - { - $request = new Request('POST', 'http://example.com/'); - $parser = new FormUrlencoded($request); - $this->assertTrue($parser->isDone()); - } } From b80c74580ac052cd2e759ac3e022ecc81f4214e2 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 10 May 2016 07:43:52 +0200 Subject: [PATCH 42/64] Introducing the RawBody for when we can't determin the content type but have a content length --- src/DeferredStream.php | 11 +++++++++-- src/FormParserFactory.php | 17 +++++++---------- src/Parser/RawBody.php | 33 +++++++++++++++++++++++++++++++++ tests/DeferredStreamTest.php | 9 ++++++--- tests/FormParserFactoryTest.php | 3 +++ tests/Parser/NoBodyTest.php | 17 ----------------- tests/Parser/RawBodyTest.php | 25 +++++++++++++++++++++++++ 7 files changed, 83 insertions(+), 32 deletions(-) create mode 100644 src/Parser/RawBody.php delete mode 100644 tests/Parser/NoBodyTest.php create mode 100644 tests/Parser/RawBodyTest.php diff --git a/src/DeferredStream.php b/src/DeferredStream.php index 589562b8..0d14e8d8 100644 --- a/src/DeferredStream.php +++ b/src/DeferredStream.php @@ -2,6 +2,7 @@ namespace React\Http; +use React\Http\Parser\NoBody; use React\Http\Parser\ParserInterface; use React\Promise\Deferred; use React\Promise\PromiseInterface; @@ -15,16 +16,18 @@ class DeferredStream */ public static function create(ParserInterface $parser) { - if ($parser->isDone()) { + if ($parser instanceof NoBody) { return \React\Promise\resolve([ 'post' => [], 'files' => [], + 'body' => '', ]); } $deferred = new Deferred(); $postFields = []; $files = []; + $body = ''; $parser->on('post', function ($key, $value) use (&$postFields) { self::extractPost($postFields, $key, $value); }); @@ -36,10 +39,14 @@ public static function create(ParserInterface $parser) ]; }); }); - $parser->on('end', function () use ($deferred, &$postFields, &$files) { + $parser->on('body', function ($rawBody) use (&$body) { + $body = $rawBody; + }); + $parser->on('end', function () use ($deferred, &$postFields, &$files, &$body) { $deferred->resolve([ 'post' => $postFields, 'files' => $files, + 'body' => $body, ]); }); diff --git a/src/FormParserFactory.php b/src/FormParserFactory.php index aa04100d..35e6baef 100644 --- a/src/FormParserFactory.php +++ b/src/FormParserFactory.php @@ -2,36 +2,33 @@ namespace React\Http; -use React\Http\Parser\FormUrlencoded; -use React\Http\Parser\Multipart; -use React\Http\Parser\NoBody; -use React\Http\Parser\ParserInterface; +use React\Http\Parser; class FormParserFactory { /** * @param Request $request - * @return ParserInterface + * @return Parser\ParserInterface */ public static function create(Request $request) { $headers = $request->getHeaders(); $headers = array_change_key_case($headers, CASE_LOWER); - if (!array_key_exists('content-type', $headers)) { - return new NoBody($request); + if (!isset($headers['content-type']) && !isset($headers['content-length'])) { + return new Parser\NoBody($request); } $contentType = strtolower($headers['content-type']); if (strpos($contentType, 'multipart/') === 0) { - return new Multipart($request); + return new Parser\Multipart($request); } if (strpos($contentType, 'application/x-www-form-urlencoded') === 0) { - return new FormUrlencoded($request); + return new Parser\FormUrlencoded($request); } - return new NoBody($request); + return new Parser\RawBody($request); } } diff --git a/src/Parser/RawBody.php b/src/Parser/RawBody.php new file mode 100644 index 00000000..2b2d16de --- /dev/null +++ b/src/Parser/RawBody.php @@ -0,0 +1,33 @@ +getHeaders(); + $headers = array_change_key_case($headers, CASE_LOWER); + + $this->contentLength = $headers['content-length']; + $request->on('data', [$this, 'feed']); + } + + public function feed($data) + { + $this->buffer .= $data; + + if (strlen($this->buffer) >= $this->contentLength) { + $this->emit('body', [$this->buffer]); + $this->emit('end'); + } + } +} diff --git a/tests/DeferredStreamTest.php b/tests/DeferredStreamTest.php index da6c78fa..5511b0d5 100644 --- a/tests/DeferredStreamTest.php +++ b/tests/DeferredStreamTest.php @@ -15,13 +15,13 @@ class DeferredStreamTest extends TestCase { public function testDoneParser() { - $parser = new DummyParser(new Request('get', 'http://example.com')); - $parser->setDone(); + $parser = new NoBody(new Request('get', 'http://example.com')); $deferredStream = DeferredStream::create($parser); $result = Block\await($deferredStream, Factory::create(), 10); $this->assertSame([ 'post' => [], 'files' => [], + 'body' => '', ], $result); } @@ -40,7 +40,8 @@ public function testDeferredStream() $parser->emit('file', [$file]); $stream->end('foo.bar'); - $parser->setDone(); + $parser->emit('body', ['abc']); + $parser->emit('end'); $result = Block\await($deferredStream, Factory::create(), 10); @@ -64,6 +65,8 @@ public function testDeferredStream() $this->assertSame('bar.ext', $result['files'][0]['file']->getFilename()); $this->assertSame('text', $result['files'][0]['file']->getType()); $this->assertSame('foo.bar', $result['files'][0]['buffer']); + + $this->assertSame('abc', $result['body']); } public function testExtractPost() diff --git a/tests/FormParserFactoryTest.php b/tests/FormParserFactoryTest.php index 6190541d..3c78403d 100644 --- a/tests/FormParserFactoryTest.php +++ b/tests/FormParserFactoryTest.php @@ -38,6 +38,7 @@ public function testFormUrlencoded() { $request = new Request('POST', 'http://example.com/', [], 1.1, [ 'Content-Type' => 'application/x-www-form-urlencoded', + 'content-length' => 123, ]); $parser = FormParserFactory::create($request); $this->assertInstanceOf('React\Http\Parser\FormUrlencoded', $parser); @@ -47,6 +48,7 @@ public function testFormUrlencodedUTF8() { $request = new Request('POST', 'http://example.com/', [], 1.1, [ 'Content-Type' => 'application/x-www-form-urlencoded; charset=utf8', + 'content-length' => 123, ]); $parser = FormParserFactory::create($request); $this->assertInstanceOf('React\Http\Parser\FormUrlencoded', $parser); @@ -56,6 +58,7 @@ public function testFormUrlencodedHeaderCaseInsensitive() { $request = new Request('POST', 'http://example.com/', [], 1.1, [ 'content-type' => 'application/x-www-form-urlencoded', + 'content-length' => 123, ]); $parser = FormParserFactory::create($request); $this->assertInstanceOf('React\Http\Parser\FormUrlencoded', $parser); diff --git a/tests/Parser/NoBodyTest.php b/tests/Parser/NoBodyTest.php deleted file mode 100644 index 244bcb21..00000000 --- a/tests/Parser/NoBodyTest.php +++ /dev/null @@ -1,17 +0,0 @@ -assertTrue($parser->isDone()); - } -} diff --git a/tests/Parser/RawBodyTest.php b/tests/Parser/RawBodyTest.php new file mode 100644 index 00000000..73501b48 --- /dev/null +++ b/tests/Parser/RawBodyTest.php @@ -0,0 +1,25 @@ + 3, + ]); + $parser = new RawBody($request); + $parser->on('body', function ($rawBody) use (&$body) { + $body = $rawBody; + }); + $request->emit('data', ['abc']); + $this->assertSame('abc', $body); + } +} From 78b38ca44d0a0c64ee26fba2169b43ac97a32d31 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 10 May 2016 07:50:20 +0200 Subject: [PATCH 43/64] Section in the readme on FormParserFactory and DeferredStream --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index f39b025f..703f57f9 100644 --- a/README.md +++ b/README.md @@ -31,3 +31,25 @@ This is an HTTP server which responds with `Hello World` to every request. $socket->listen(1337); $loop->run(); ``` + +## FormParserFactory and DeferredStream Usage + +The `FormParserFactory` parses a quest and determines which body parser to use (multipart, formurlencoded, raw body, or no body). Those body parsers emit events on `post` fields, `file` on files, and raw body emits `body` when it received the whole body. `DeferredStream` listens for those events and returns them through a promise when done. + +```php + $loop = React\EventLoop\Factory::create(); + $socket = new React\Socket\Server($loop); + + $http = new React\Http\Server($socket); + $http->on('request', function ($request, $response) { + $parser = FormParserFactory::create($request); + DeferredStream::create($parser)->then(function ($result) use ($response) { + var_export($result); + $response->writeHead(200, array('Content-Type' => 'text/plain')); + $response->end("Hello World!\n"); + }); + }); + + $socket->listen(1337); + $loop->run(); +``` From b888423ea0ee65e89868c677b97f03bc66b176ae Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 17 May 2016 09:25:24 +0200 Subject: [PATCH 44/64] Typo: quest => request --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 703f57f9..c2a7e5ac 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ This is an HTTP server which responds with `Hello World` to every request. ## FormParserFactory and DeferredStream Usage -The `FormParserFactory` parses a quest and determines which body parser to use (multipart, formurlencoded, raw body, or no body). Those body parsers emit events on `post` fields, `file` on files, and raw body emits `body` when it received the whole body. `DeferredStream` listens for those events and returns them through a promise when done. +The `FormParserFactory` parses a request and determines which body parser to use (multipart, formurlencoded, raw body, or no body). Those body parsers emit events on `post` fields, `file` on files, and raw body emits `body` when it received the whole body. `DeferredStream` listens for those events and returns them through a promise when done. ```php $loop = React\EventLoop\Factory::create(); From dbc2356c42a13a532f89739f466c021ba0ca78c3 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 3 Jul 2016 14:54:12 +0200 Subject: [PATCH 45/64] Correctly handle file names with colons in them --- src/Parser/Multipart.php | 2 +- tests/Parser/MultipartTest.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Parser/Multipart.php b/src/Parser/Multipart.php index ffa88377..b1673b32 100644 --- a/src/Parser/Multipart.php +++ b/src/Parser/Multipart.php @@ -209,7 +209,7 @@ protected function parseHeaders($header) $headers = []; foreach (explode("\r\n", trim($header)) as $line) { - list($key, $values) = explode(':', $line); + list($key, $values) = explode(':', $line, 2); $key = trim($key); $key = strtolower($key); $values = explode(';', $values); diff --git a/tests/Parser/MultipartTest.php b/tests/Parser/MultipartTest.php index 56a8f54c..4e7bcc06 100644 --- a/tests/Parser/MultipartTest.php +++ b/tests/Parser/MultipartTest.php @@ -99,7 +99,7 @@ public function testFileUpload() $request->emit('data', ["\r\n"]); $request->emit('data', ["second in array\r\n"]); $request->emit('data', ["--$boundary\r\n"]); - $request->emit('data', ["Content-Disposition: form-data; name=\"file\"; filename=\"User.php\"\r\n"]); + $request->emit('data', ["Content-Disposition: form-data; name=\"file\"; filename=\"Us er.php\"\r\n"]); $request->emit('data', ["Content-type: text/php\r\n"]); $request->emit('data', ["\r\n"]); $request->emit('data', ["assertEquals(3, count($files)); $this->assertEquals('file', $files[0][0]->getName()); - $this->assertEquals('User.php', $files[0][0]->getFilename()); + $this->assertEquals('Us er.php', $files[0][0]->getFilename()); $this->assertEquals('text/php', $files[0][0]->getType()); $this->assertEquals([ 'content-disposition' => [ 'form-data', 'name="file"', - 'filename="User.php"', + 'filename="Us er.php"', ], 'content-type' => [ 'text/php', From 74c215042fbbd5373205e177470dd0db6a407565 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 3 Jul 2016 15:35:51 +0200 Subject: [PATCH 46/64] Ensure we always have content type --- src/FormParserFactory.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/FormParserFactory.php b/src/FormParserFactory.php index 35e6baef..7a56ca95 100644 --- a/src/FormParserFactory.php +++ b/src/FormParserFactory.php @@ -15,7 +15,10 @@ public static function create(Request $request) $headers = $request->getHeaders(); $headers = array_change_key_case($headers, CASE_LOWER); - if (!isset($headers['content-type']) && !isset($headers['content-length'])) { + if ( + !isset($headers['content-type']) || + (!isset($headers['content-type']) && !isset($headers['content-length'])) + ) { return new Parser\NoBody($request); } From 29f50ed11aaeeccaf6a013f571bc0773ec31c61c Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 3 Jul 2016 15:36:15 +0200 Subject: [PATCH 47/64] Ensure correct multipart boundary handling --- src/Parser/Multipart.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Parser/Multipart.php b/src/Parser/Multipart.php index b1673b32..221574b9 100644 --- a/src/Parser/Multipart.php +++ b/src/Parser/Multipart.php @@ -54,7 +54,7 @@ public function __construct(Request $request) protected function setBoundary($boundary) { - $this->boundary = substr($boundary, 1); + $this->boundary = $boundary; $this->ending = $this->boundary . "--\r\n"; $this->endingSize = strlen($this->ending); } @@ -99,7 +99,7 @@ protected function parseBuffer() return; } - $chunks = preg_split('/' . $this->boundary . '/', trim($split[0]), -1, PREG_SPLIT_NO_EMPTY); + $chunks = preg_split('/-+' . $this->boundary . '/', trim($split[0]), -1, PREG_SPLIT_NO_EMPTY); $headers = $this->parseHeaders(trim($chunks[0])); if (isset($headers['content-disposition']) && $this->headerStartsWith($headers['content-disposition'], 'filename')) { $this->parseFile($headers, $split[1]); @@ -178,7 +178,7 @@ protected function chunkStreamFunc(ThroughStream $stream) array_unshift($chunks, ''); } - $this->onData(implode($this->boundary, $chunks)); + $this->onData(implode('-' . $this->boundary, $chunks)); return; } From 29a9e5d6116f6682a1e275e0bee70b7ddd605810 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 3 Jul 2016 16:43:39 +0200 Subject: [PATCH 48/64] Slice off two characters at the beginning of detected boundaries --- src/Parser/Multipart.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Parser/Multipart.php b/src/Parser/Multipart.php index 221574b9..194b9c81 100644 --- a/src/Parser/Multipart.php +++ b/src/Parser/Multipart.php @@ -64,7 +64,9 @@ public function findBoundary($data) $this->buffer .= $data; if (substr($this->buffer, 0, 3) === '---' && strpos($this->buffer, "\r\n") !== false) { - $this->setBoundary(substr($this->buffer, 0, strpos($this->buffer, "\r\n"))); + $boundary = substr($this->buffer, 2, strpos($this->buffer, "\r\n")); + $boundary = substr($boundary, 0, -2); + $this->setBoundary($boundary); $this->request->removeListener('data', [$this, 'findBoundary']); $this->request->on('data', [$this, 'onData']); } From 3794e36453640a2ae1d5d5bbcae8b6f9ec9cb87a Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 3 Jul 2016 22:44:16 +0200 Subject: [PATCH 49/64] Resolved issues that come up when merging master into this branch --- src/RequestParser.php | 3 +-- src/Server.php | 6 +++--- tests/ServerTest.php | 12 ++++++++---- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/RequestParser.php b/src/RequestParser.php index 89ddb15a..55f439f3 100644 --- a/src/RequestParser.php +++ b/src/RequestParser.php @@ -48,10 +48,9 @@ public function feed($data) $this->stream->removeListener('data', [$this, 'feed']); $this->request = $this->parseHeaders($headers . "\r\n\r\n"); - if($this->request->expectsContinue()) { + if ($this->request->expectsContinue()) { $this->emit('expects_continue'); } - } $this->emit('headers', array($this->request, $buffer)); $this->removeAllListeners(); diff --git a/src/Server.php b/src/Server.php index 86038687..d1ba9bf9 100644 --- a/src/Server.php +++ b/src/Server.php @@ -41,8 +41,6 @@ public function __construct(SocketServerInterface $io) }); }); - $conn->on('data', array($parser, 'feed')); - $parser->on('expects_continue', function() use($conn) { $conn->write("HTTP/1.1 100 Continue\r\n\r\n"); }); @@ -61,6 +59,8 @@ public function handleRequest(ConnectionInterface $conn, Request $request, $body } $this->emit('request', array($request, $response)); - $request->emit('data', array($bodyBuffer)); + if ($bodyBuffer !== '') { + $request->emit('data', array($bodyBuffer)); + } } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 449e3793..33583838 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -87,13 +87,17 @@ public function testServerRespondsToExpectContinue() $postBody = '{"React":true}'; $httpRequestText = $this->createPostRequestWithExpect($postBody); - $conn->emit('data', array($httpRequestText)); - $server->on('request', function ($request, $_) use (&$requestReceived, $postBody) { - $requestReceived = true; - $this->assertEquals($postBody, $request->getBody()); + $func = function ($data) use ($request, &$requestReceived, $postBody, &$func) { + $requestReceived = true; + $this->assertEquals($postBody, $data); + $request->removeListener('data', $func); + }; + $request->on('data', $func); }); + $conn->emit('data', array($httpRequestText)); + // If server received Expect: 100-continue - the client won't send the body right away $this->assertEquals(false, $requestReceived); From 92f347bf384c99593a79a2eb39d0372e405f4bca Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 7 Jul 2016 17:09:04 +0200 Subject: [PATCH 50/64] Renamed File::getType to File::getContentType as a result of: https://github.com/reactphp/http/pull/41#discussion_r69882675 --- src/File.php | 12 ++++++------ src/FileInterface.php | 2 +- tests/DeferredStreamTest.php | 2 +- tests/FileTest.php | 2 +- tests/Parser/MultipartTest.php | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/File.php b/src/File.php index 34116984..a9bdc921 100644 --- a/src/File.php +++ b/src/File.php @@ -19,7 +19,7 @@ class File implements FileInterface /** * @var string */ - protected $type; + protected $contentType; /** * @var ReadableStreamInterface @@ -29,14 +29,14 @@ class File implements FileInterface /** * @param string $name * @param string $filename - * @param string $type + * @param string $contentType * @param ReadableStreamInterface $stream */ - public function __construct($name, $filename, $type, ReadableStreamInterface $stream) + public function __construct($name, $filename, $contentType, ReadableStreamInterface $stream) { $this->name = $name; $this->filename = $filename; - $this->type = $type; + $this->contentType = $contentType; $this->stream = $stream; } @@ -59,9 +59,9 @@ public function getFilename() /** * @return string */ - public function getType() + public function getContentType() { - return $this->type; + return $this->contentType; } /** diff --git a/src/FileInterface.php b/src/FileInterface.php index 880259e9..3171e2f0 100644 --- a/src/FileInterface.php +++ b/src/FileInterface.php @@ -19,7 +19,7 @@ public function getFilename(); /** * @return string */ - public function getType(); + public function getContentType(); /** * @return ReadableStreamInterface diff --git a/tests/DeferredStreamTest.php b/tests/DeferredStreamTest.php index 5511b0d5..42138ea9 100644 --- a/tests/DeferredStreamTest.php +++ b/tests/DeferredStreamTest.php @@ -63,7 +63,7 @@ public function testDeferredStream() $this->assertSame('foo', $result['files'][0]['file']->getName()); $this->assertSame('bar.ext', $result['files'][0]['file']->getFilename()); - $this->assertSame('text', $result['files'][0]['file']->getType()); + $this->assertSame('text', $result['files'][0]['file']->getContentType()); $this->assertSame('foo.bar', $result['files'][0]['buffer']); $this->assertSame('abc', $result['body']); diff --git a/tests/FileTest.php b/tests/FileTest.php index 657bc793..c47cafb2 100644 --- a/tests/FileTest.php +++ b/tests/FileTest.php @@ -16,7 +16,7 @@ public function testGetters() $file = new File($name, $filename, $type, $stream); $this->assertEquals($name, $file->getName()); $this->assertEquals($filename, $file->getFilename()); - $this->assertEquals($type, $file->getType()); + $this->assertEquals($type, $file->getContentType()); $this->assertEquals($stream, $file->getStream()); } } diff --git a/tests/Parser/MultipartTest.php b/tests/Parser/MultipartTest.php index 4e7bcc06..2196e620 100644 --- a/tests/Parser/MultipartTest.php +++ b/tests/Parser/MultipartTest.php @@ -138,7 +138,7 @@ public function testFileUpload() $this->assertEquals(3, count($files)); $this->assertEquals('file', $files[0][0]->getName()); $this->assertEquals('Us er.php', $files[0][0]->getFilename()); - $this->assertEquals('text/php', $files[0][0]->getType()); + $this->assertEquals('text/php', $files[0][0]->getContentType()); $this->assertEquals([ 'content-disposition' => [ 'form-data', @@ -152,7 +152,7 @@ public function testFileUpload() $this->assertEquals('files[]', $files[1][0]->getName()); $this->assertEquals('blank.gif', $files[1][0]->getFilename()); - $this->assertEquals('image/gif', $files[1][0]->getType()); + $this->assertEquals('image/gif', $files[1][0]->getContentType()); $this->assertEquals([ 'content-disposition' => [ 'form-data', @@ -169,7 +169,7 @@ public function testFileUpload() $this->assertEquals('files[]', $files[2][0]->getName()); $this->assertEquals('User.php', $files[2][0]->getFilename()); - $this->assertEquals('text/php', $files[2][0]->getType()); + $this->assertEquals('text/php', $files[2][0]->getContentType()); $this->assertEquals([ 'content-disposition' => [ 'form-data', From 7c835954b4267faabcfe5e22779856c4a230f858 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 7 Jul 2016 17:11:20 +0200 Subject: [PATCH 51/64] Removed branch alias after merge --- composer.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/composer.json b/composer.json index a7689d31..01d1b2c4 100644 --- a/composer.json +++ b/composer.json @@ -16,11 +16,6 @@ "React\\Http\\": "src" } }, - "extra": { - "branch-alias": { - "dev-master": "0.5-dev" - } - }, "require-dev": { "clue/block-react": "^1.1" } From b2fddd2c2dd80ab8bdb4d77a27ec74ab5edccb0c Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 7 Jul 2016 17:11:58 +0200 Subject: [PATCH 52/64] Corrected promise targetting: https://github.com/reactphp/http/pull/41#discussion_r69882343 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 01d1b2c4..f59f93d5 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ "react/socket": "^0.4", "react/stream": "^0.4", "evenement/evenement": "^2.0", - "react/promise": "^1.0|^2.0" + "react/promise": "^2.0 || ^1.1" }, "autoload": { "psr-4": { From 77fd1b2bf7f10c98b4839cdbab0ae230cbe4555e Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 7 Jul 2016 17:13:03 +0200 Subject: [PATCH 53/64] Simplefied content type and length detection: https://github.com/reactphp/http/pull/41#discussion_r69882862 --- src/FormParserFactory.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/FormParserFactory.php b/src/FormParserFactory.php index 7a56ca95..b4223724 100644 --- a/src/FormParserFactory.php +++ b/src/FormParserFactory.php @@ -16,8 +16,7 @@ public static function create(Request $request) $headers = array_change_key_case($headers, CASE_LOWER); if ( - !isset($headers['content-type']) || - (!isset($headers['content-type']) && !isset($headers['content-length'])) + !isset($headers['content-type']) || !isset($headers['content-length']) ) { return new Parser\NoBody($request); } From 7d75b008dafa2d639d248c800db63ae4ef96fa0d Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 7 Jul 2016 17:15:48 +0200 Subject: [PATCH 54/64] Forgot to replace || with && --- src/FormParserFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FormParserFactory.php b/src/FormParserFactory.php index b4223724..cce049f6 100644 --- a/src/FormParserFactory.php +++ b/src/FormParserFactory.php @@ -16,7 +16,7 @@ public static function create(Request $request) $headers = array_change_key_case($headers, CASE_LOWER); if ( - !isset($headers['content-type']) || !isset($headers['content-length']) + !isset($headers['content-type']) && !isset($headers['content-length']) ) { return new Parser\NoBody($request); } From 7873ca09f65661e863183199639e9f55bb912d5e Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Fri, 8 Jul 2016 17:40:46 +0200 Subject: [PATCH 55/64] Renamed Parser namespace to BodyParser and also moved the FormParserFactory: https://github.com/reactphp/http/pull/41#discussion_r69938047 --- README.md | 2 +- .../Factory.php} | 16 +++++----- src/{Parser => BodyParser}/FormUrlencoded.php | 2 +- src/{Parser => BodyParser}/Multipart.php | 2 +- src/{Parser => BodyParser}/NoBody.php | 2 +- .../ParserInterface.php | 2 +- src/{Parser => BodyParser}/RawBody.php | 2 +- src/DeferredStream.php | 4 +-- tests/{Parser => BodyParser}/DummyParser.php | 4 +-- .../FactoryTest.php} | 31 ++++++++++--------- .../FormUrlencodedTest.php | 5 ++- .../{Parser => BodyParser}/MultipartTest.php | 4 +-- tests/{Parser => BodyParser}/RawBodyTest.php | 5 ++- tests/DeferredStreamTest.php | 4 +-- 14 files changed, 42 insertions(+), 43 deletions(-) rename src/{FormParserFactory.php => BodyParser/Factory.php} (64%) rename src/{Parser => BodyParser}/FormUrlencoded.php (97%) rename src/{Parser => BodyParser}/Multipart.php (99%) rename src/{Parser => BodyParser}/NoBody.php (85%) rename src/{Parser => BodyParser}/ParserInterface.php (84%) rename src/{Parser => BodyParser}/RawBody.php (95%) rename tests/{Parser => BodyParser}/DummyParser.php (71%) rename tests/{FormParserFactoryTest.php => BodyParser/FactoryTest.php} (64%) rename tests/{Parser => BodyParser}/FormUrlencodedTest.php (88%) rename tests/{Parser => BodyParser}/MultipartTest.php (98%) rename tests/{Parser => BodyParser}/RawBodyTest.php (84%) diff --git a/README.md b/README.md index c2a7e5ac..9e85fe1e 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ The `FormParserFactory` parses a request and determines which body parser to use $http = new React\Http\Server($socket); $http->on('request', function ($request, $response) { - $parser = FormParserFactory::create($request); + $parser = React\Http\BodyParser\Factory::create($request); DeferredStream::create($parser)->then(function ($result) use ($response) { var_export($result); $response->writeHead(200, array('Content-Type' => 'text/plain')); diff --git a/src/FormParserFactory.php b/src/BodyParser/Factory.php similarity index 64% rename from src/FormParserFactory.php rename to src/BodyParser/Factory.php index cce049f6..2fa6de21 100644 --- a/src/FormParserFactory.php +++ b/src/BodyParser/Factory.php @@ -1,14 +1,14 @@ 'multipart/mixed; boundary=---------------------------12758086162038677464950549563', ]); - $parser = FormParserFactory::create($request); - $this->assertInstanceOf('React\Http\Parser\Multipart', $parser); + $parser = Factory::create($request); + $this->assertInstanceOf('React\Http\BodyParser\Multipart', $parser); } public function testMultipartUTF8() @@ -21,8 +22,8 @@ public function testMultipartUTF8() $request = new Request('POST', 'http://example.com/', [], 1.1, [ 'Content-Type' => 'multipart/mixed; boundary=---------------------------12758086162038677464950549563; charset=utf8', ]); - $parser = FormParserFactory::create($request); - $this->assertInstanceOf('React\Http\Parser\Multipart', $parser); + $parser = Factory::create($request); + $this->assertInstanceOf('React\Http\BodyParser\Multipart', $parser); } public function testMultipartHeaderCaseInsensitive() @@ -30,8 +31,8 @@ public function testMultipartHeaderCaseInsensitive() $request = new Request('POST', 'http://example.com/', [], 1.1, [ 'CONTENT-TYPE' => 'multipart/mixed; boundary=---------------------------12758086162038677464950549563', ]); - $parser = FormParserFactory::create($request); - $this->assertInstanceOf('React\Http\Parser\Multipart', $parser); + $parser = Factory::create($request); + $this->assertInstanceOf('React\Http\BodyParser\Multipart', $parser); } public function testFormUrlencoded() @@ -40,8 +41,8 @@ public function testFormUrlencoded() 'Content-Type' => 'application/x-www-form-urlencoded', 'content-length' => 123, ]); - $parser = FormParserFactory::create($request); - $this->assertInstanceOf('React\Http\Parser\FormUrlencoded', $parser); + $parser = Factory::create($request); + $this->assertInstanceOf('React\Http\BodyParser\FormUrlencoded', $parser); } public function testFormUrlencodedUTF8() @@ -50,8 +51,8 @@ public function testFormUrlencodedUTF8() 'Content-Type' => 'application/x-www-form-urlencoded; charset=utf8', 'content-length' => 123, ]); - $parser = FormParserFactory::create($request); - $this->assertInstanceOf('React\Http\Parser\FormUrlencoded', $parser); + $parser = Factory::create($request); + $this->assertInstanceOf('React\Http\BodyParser\FormUrlencoded', $parser); } public function testFormUrlencodedHeaderCaseInsensitive() @@ -60,7 +61,7 @@ public function testFormUrlencodedHeaderCaseInsensitive() 'content-type' => 'application/x-www-form-urlencoded', 'content-length' => 123, ]); - $parser = FormParserFactory::create($request); - $this->assertInstanceOf('React\Http\Parser\FormUrlencoded', $parser); + $parser = Factory::create($request); + $this->assertInstanceOf('React\Http\BodyParser\FormUrlencoded', $parser); } } diff --git a/tests/Parser/FormUrlencodedTest.php b/tests/BodyParser/FormUrlencodedTest.php similarity index 88% rename from tests/Parser/FormUrlencodedTest.php rename to tests/BodyParser/FormUrlencodedTest.php index 9bddf0be..675e81a4 100644 --- a/tests/Parser/FormUrlencodedTest.php +++ b/tests/BodyParser/FormUrlencodedTest.php @@ -1,10 +1,9 @@ Date: Fri, 8 Jul 2016 17:46:18 +0200 Subject: [PATCH 56/64] Renamed BodyParser to StreamingBodyParser to make it more explicit what those parsers are doing --- README.md | 4 ++-- src/DeferredStream.php | 8 ++++---- .../Factory.php | 4 ++-- .../FormUrlencoded.php | 4 ++-- .../Multipart.php | 4 ++-- .../NoBody.php | 4 ++-- .../RawBody.php | 4 ++-- .../StreamingParserInterface.php} | 4 ++-- tests/BodyParser/DummyParser.php | 16 ---------------- tests/DeferredStreamTest.php | 4 ++-- tests/StreamingBodyParser/DummyParser.php | 16 ++++++++++++++++ .../FactoryTest.php | 16 ++++++++-------- .../FormUrlencodedTest.php | 4 ++-- .../MultipartTest.php | 4 ++-- .../RawBodyTest.php | 4 ++-- 15 files changed, 50 insertions(+), 50 deletions(-) rename src/{BodyParser => StreamingBodyParser}/Factory.php (90%) rename src/{BodyParser => StreamingBodyParser}/FormUrlencoded.php (93%) rename src/{BodyParser => StreamingBodyParser}/Multipart.php (98%) rename src/{BodyParser => StreamingBodyParser}/NoBody.php (63%) rename src/{BodyParser => StreamingBodyParser}/RawBody.php (87%) rename src/{BodyParser/ParserInterface.php => StreamingBodyParser/StreamingParserInterface.php} (53%) delete mode 100644 tests/BodyParser/DummyParser.php create mode 100644 tests/StreamingBodyParser/DummyParser.php rename tests/{BodyParser => StreamingBodyParser}/FactoryTest.php (75%) rename tests/{BodyParser => StreamingBodyParser}/FormUrlencodedTest.php (89%) rename tests/{BodyParser => StreamingBodyParser}/MultipartTest.php (98%) rename tests/{BodyParser => StreamingBodyParser}/RawBodyTest.php (85%) diff --git a/README.md b/README.md index 9e85fe1e..4e3385e1 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ This is an HTTP server which responds with `Hello World` to every request. $loop->run(); ``` -## FormParserFactory and DeferredStream Usage +## StreamingBodyParser\Factory and DeferredStream Usage The `FormParserFactory` parses a request and determines which body parser to use (multipart, formurlencoded, raw body, or no body). Those body parsers emit events on `post` fields, `file` on files, and raw body emits `body` when it received the whole body. `DeferredStream` listens for those events and returns them through a promise when done. @@ -42,7 +42,7 @@ The `FormParserFactory` parses a request and determines which body parser to use $http = new React\Http\Server($socket); $http->on('request', function ($request, $response) { - $parser = React\Http\BodyParser\Factory::create($request); + $parser = React\Http\StreamingBodyParser\Factory::create($request); DeferredStream::create($parser)->then(function ($result) use ($response) { var_export($result); $response->writeHead(200, array('Content-Type' => 'text/plain')); diff --git a/src/DeferredStream.php b/src/DeferredStream.php index d872dfa8..45c450ef 100644 --- a/src/DeferredStream.php +++ b/src/DeferredStream.php @@ -2,8 +2,8 @@ namespace React\Http; -use React\Http\BodyParser\NoBody; -use React\Http\BodyParser\ParserInterface; +use React\Http\StreamingBodyParser\NoBody; +use React\Http\StreamingBodyParser\StreamingParserInterface; use React\Promise\Deferred; use React\Promise\PromiseInterface; use React\Stream\BufferedSink; @@ -11,10 +11,10 @@ class DeferredStream { /** - * @param ParserInterface $parser + * @param StreamingParserInterface $parser * @return PromiseInterface */ - public static function create(ParserInterface $parser) + public static function create(StreamingParserInterface $parser) { if ($parser instanceof NoBody) { return \React\Promise\resolve([ diff --git a/src/BodyParser/Factory.php b/src/StreamingBodyParser/Factory.php similarity index 90% rename from src/BodyParser/Factory.php rename to src/StreamingBodyParser/Factory.php index 2fa6de21..d0ba0f62 100644 --- a/src/BodyParser/Factory.php +++ b/src/StreamingBodyParser/Factory.php @@ -1,6 +1,6 @@ 'multipart/mixed; boundary=---------------------------12758086162038677464950549563', ]); $parser = Factory::create($request); - $this->assertInstanceOf('React\Http\BodyParser\Multipart', $parser); + $this->assertInstanceOf('React\Http\StreamingBodyParser\Multipart', $parser); } public function testMultipartUTF8() @@ -23,7 +23,7 @@ public function testMultipartUTF8() 'Content-Type' => 'multipart/mixed; boundary=---------------------------12758086162038677464950549563; charset=utf8', ]); $parser = Factory::create($request); - $this->assertInstanceOf('React\Http\BodyParser\Multipart', $parser); + $this->assertInstanceOf('React\Http\StreamingBodyParser\Multipart', $parser); } public function testMultipartHeaderCaseInsensitive() @@ -32,7 +32,7 @@ public function testMultipartHeaderCaseInsensitive() 'CONTENT-TYPE' => 'multipart/mixed; boundary=---------------------------12758086162038677464950549563', ]); $parser = Factory::create($request); - $this->assertInstanceOf('React\Http\BodyParser\Multipart', $parser); + $this->assertInstanceOf('React\Http\StreamingBodyParser\Multipart', $parser); } public function testFormUrlencoded() @@ -42,7 +42,7 @@ public function testFormUrlencoded() 'content-length' => 123, ]); $parser = Factory::create($request); - $this->assertInstanceOf('React\Http\BodyParser\FormUrlencoded', $parser); + $this->assertInstanceOf('React\Http\StreamingBodyParser\FormUrlencoded', $parser); } public function testFormUrlencodedUTF8() @@ -52,7 +52,7 @@ public function testFormUrlencodedUTF8() 'content-length' => 123, ]); $parser = Factory::create($request); - $this->assertInstanceOf('React\Http\BodyParser\FormUrlencoded', $parser); + $this->assertInstanceOf('React\Http\StreamingBodyParser\FormUrlencoded', $parser); } public function testFormUrlencodedHeaderCaseInsensitive() @@ -62,6 +62,6 @@ public function testFormUrlencodedHeaderCaseInsensitive() 'content-length' => 123, ]); $parser = Factory::create($request); - $this->assertInstanceOf('React\Http\BodyParser\FormUrlencoded', $parser); + $this->assertInstanceOf('React\Http\StreamingBodyParser\FormUrlencoded', $parser); } } diff --git a/tests/BodyParser/FormUrlencodedTest.php b/tests/StreamingBodyParser/FormUrlencodedTest.php similarity index 89% rename from tests/BodyParser/FormUrlencodedTest.php rename to tests/StreamingBodyParser/FormUrlencodedTest.php index 675e81a4..d9dd5cfe 100644 --- a/tests/BodyParser/FormUrlencodedTest.php +++ b/tests/StreamingBodyParser/FormUrlencodedTest.php @@ -1,8 +1,8 @@ Date: Fri, 8 Jul 2016 17:53:13 +0200 Subject: [PATCH 57/64] Don't attach fieldname to File object, emit it with file instead: https://github.com/reactphp/http/pull/41#discussion_r69926787 --- src/DeferredStream.php | 5 ++-- src/File.php | 17 +---------- src/FileInterface.php | 5 ---- src/StreamingBodyParser/Multipart.php | 2 +- tests/DeferredStreamTest.php | 6 ++-- tests/FileTest.php | 4 +-- tests/StreamingBodyParser/MultipartTest.php | 32 ++++++++++----------- 7 files changed, 25 insertions(+), 46 deletions(-) diff --git a/src/DeferredStream.php b/src/DeferredStream.php index 45c450ef..d1c77ee1 100644 --- a/src/DeferredStream.php +++ b/src/DeferredStream.php @@ -31,9 +31,10 @@ public static function create(StreamingParserInterface $parser) $parser->on('post', function ($key, $value) use (&$postFields) { self::extractPost($postFields, $key, $value); }); - $parser->on('file', function (File $file) use (&$files) { - BufferedSink::createPromise($file->getStream())->then(function ($buffer) use ($file, &$files) { + $parser->on('file', function ($name, File $file) use (&$files) { + BufferedSink::createPromise($file->getStream())->then(function ($buffer) use ($name, $file, &$files) { $files[] = [ + 'name' => $name, 'file' => $file, 'buffer' => $buffer, ]; diff --git a/src/File.php b/src/File.php index a9bdc921..61e14cc8 100644 --- a/src/File.php +++ b/src/File.php @@ -6,11 +6,6 @@ class File implements FileInterface { - /** - * @var string - */ - protected $name; - /** * @var string */ @@ -27,27 +22,17 @@ class File implements FileInterface protected $stream; /** - * @param string $name * @param string $filename * @param string $contentType * @param ReadableStreamInterface $stream */ - public function __construct($name, $filename, $contentType, ReadableStreamInterface $stream) + public function __construct($filename, $contentType, ReadableStreamInterface $stream) { - $this->name = $name; $this->filename = $filename; $this->contentType = $contentType; $this->stream = $stream; } - /** - * @return string - */ - public function getName() - { - return $this->name; - } - /** * @return string */ diff --git a/src/FileInterface.php b/src/FileInterface.php index 3171e2f0..227b8feb 100644 --- a/src/FileInterface.php +++ b/src/FileInterface.php @@ -6,11 +6,6 @@ interface FileInterface { - /** - * @return string - */ - public function getName(); - /** * @return string */ diff --git a/src/StreamingBodyParser/Multipart.php b/src/StreamingBodyParser/Multipart.php index 3ef85d61..ccaa29cd 100644 --- a/src/StreamingBodyParser/Multipart.php +++ b/src/StreamingBodyParser/Multipart.php @@ -144,8 +144,8 @@ protected function parseFile($headers, $body, $streaming = true) $stream = new ThroughStream(); $this->emit('file', [ + $this->getFieldFromHeader($headers['content-disposition'], 'name'), new File( - $this->getFieldFromHeader($headers['content-disposition'], 'name'), $this->getFieldFromHeader($headers['content-disposition'], 'filename'), $headers['content-type'][0], $stream diff --git a/tests/DeferredStreamTest.php b/tests/DeferredStreamTest.php index 697f4523..b0badb3a 100644 --- a/tests/DeferredStreamTest.php +++ b/tests/DeferredStreamTest.php @@ -36,8 +36,8 @@ public function testDeferredStream() $parser->emit('post', ['dom[two][]', 'bar']); $stream = new ThroughStream(); - $file = new File('foo', 'bar.ext', 'text', $stream); - $parser->emit('file', [$file]); + $file = new File('bar.ext', 'text', $stream); + $parser->emit('file', ['foo', $file]); $stream->end('foo.bar'); $parser->emit('body', ['abc']); @@ -61,7 +61,7 @@ public function testDeferredStream() ], ], $result['post']); - $this->assertSame('foo', $result['files'][0]['file']->getName()); + $this->assertSame('foo', $result['files'][0]['name']); $this->assertSame('bar.ext', $result['files'][0]['file']->getFilename()); $this->assertSame('text', $result['files'][0]['file']->getContentType()); $this->assertSame('foo.bar', $result['files'][0]['buffer']); diff --git a/tests/FileTest.php b/tests/FileTest.php index c47cafb2..7b9b116b 100644 --- a/tests/FileTest.php +++ b/tests/FileTest.php @@ -9,12 +9,10 @@ class FileTest extends TestCase { public function testGetters() { - $name = 'foo'; $filename = 'bar.txt'; $type = 'text/text'; $stream = new ThroughStream(); - $file = new File($name, $filename, $type, $stream); - $this->assertEquals($name, $file->getName()); + $file = new File($filename, $type, $stream); $this->assertEquals($filename, $file->getFilename()); $this->assertEquals($type, $file->getContentType()); $this->assertEquals($stream, $file->getStream()); diff --git a/tests/StreamingBodyParser/MultipartTest.php b/tests/StreamingBodyParser/MultipartTest.php index f2864f8c..b5ec865c 100644 --- a/tests/StreamingBodyParser/MultipartTest.php +++ b/tests/StreamingBodyParser/MultipartTest.php @@ -25,8 +25,8 @@ public function testPostKey() $parser->on('post', function ($key, $value) use (&$post) { $post[$key] = $value; }); - $parser->on('file', function (FileInterface $file) use (&$files) { - $files[] = $file; + $parser->on('file', function ($name, FileInterface $file) use (&$files) { + $files[] = [$name, $file]; }); $data = "--$boundary\r\n"; @@ -67,8 +67,8 @@ public function testFileUpload() $multipart->on('post', function ($key, $value) use (&$post) { $post[] = [$key => $value]; }); - $multipart->on('file', function (FileInterface $file, $headers) use (&$files) { - $files[] = [$file, $headers]; + $multipart->on('file', function ($name, FileInterface $file, $headers) use (&$files) { + $files[] = [$name, $file, $headers]; }); $file = base64_decode("R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="); @@ -136,9 +136,9 @@ public function testFileUpload() ); $this->assertEquals(3, count($files)); - $this->assertEquals('file', $files[0][0]->getName()); - $this->assertEquals('Us er.php', $files[0][0]->getFilename()); - $this->assertEquals('text/php', $files[0][0]->getContentType()); + $this->assertEquals('file', $files[0][0]); + $this->assertEquals('Us er.php', $files[0][1]->getFilename()); + $this->assertEquals('text/php', $files[0][1]->getContentType()); $this->assertEquals([ 'content-disposition' => [ 'form-data', @@ -148,11 +148,11 @@ public function testFileUpload() 'content-type' => [ 'text/php', ], - ], $files[0][1]); + ], $files[0][2]); - $this->assertEquals('files[]', $files[1][0]->getName()); - $this->assertEquals('blank.gif', $files[1][0]->getFilename()); - $this->assertEquals('image/gif', $files[1][0]->getContentType()); + $this->assertEquals('files[]', $files[1][0]); + $this->assertEquals('blank.gif', $files[1][1]->getFilename()); + $this->assertEquals('image/gif', $files[1][1]->getContentType()); $this->assertEquals([ 'content-disposition' => [ 'form-data', @@ -165,11 +165,11 @@ public function testFileUpload() 'x-foo-bar' => [ 'base64', ], - ], $files[1][1]); + ], $files[1][2]); - $this->assertEquals('files[]', $files[2][0]->getName()); - $this->assertEquals('User.php', $files[2][0]->getFilename()); - $this->assertEquals('text/php', $files[2][0]->getContentType()); + $this->assertEquals('files[]', $files[2][0]); + $this->assertEquals('User.php', $files[2][1]->getFilename()); + $this->assertEquals('text/php', $files[2][1]->getContentType()); $this->assertEquals([ 'content-disposition' => [ 'form-data', @@ -179,6 +179,6 @@ public function testFileUpload() 'content-type' => [ 'text/php', ], - ], $files[2][1]); + ], $files[2][2]); } } From 32edca3068dd09e3484654b7d40ac769039d290f Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Fri, 8 Jul 2016 19:41:00 +0200 Subject: [PATCH 58/64] Addressed @clue's concerns for the mixing concerns, by creating ContentLengthBufferedSink and handling the buffering until limit is reached in there: https://github.com/reactphp/http/pull/41#discussion_r69883476 --- .../ContentLengthBufferedSink.php | 73 +++++++++++++++++++ src/StreamingBodyParser/FormUrlencoded.php | 45 ++---------- src/StreamingBodyParser/RawBody.php | 25 ++++--- .../ContentLengthBufferedSinkTest.php | 24 ++++++ 4 files changed, 118 insertions(+), 49 deletions(-) create mode 100644 src/StreamingBodyParser/ContentLengthBufferedSink.php create mode 100644 tests/StreamingBodyParser/ContentLengthBufferedSinkTest.php diff --git a/src/StreamingBodyParser/ContentLengthBufferedSink.php b/src/StreamingBodyParser/ContentLengthBufferedSink.php new file mode 100644 index 00000000..cea7546f --- /dev/null +++ b/src/StreamingBodyParser/ContentLengthBufferedSink.php @@ -0,0 +1,73 @@ +promise(); + } + + /** + * @param Deferred $deferred + * @param Request $request + * @param $length + */ + protected function __construct(Deferred $deferred, Request $request, $length) + { + $this->deferred = $deferred; + $this->request = $request; + $this->length = $length; + $this->request->on('data', [$this, 'feed']); + } + + /** + * @param string $data + */ + public function feed($data) + { + $this->buffer .= $data; + + if ( + $this->length !== false && + strlen($this->buffer) >= $this->length + ) { + $this->buffer = substr($this->buffer, 0, $this->length); + $this->request->removeListener('data', [$this, 'feed']); + $this->deferred->resolve($this->buffer); + } + } + +} diff --git a/src/StreamingBodyParser/FormUrlencoded.php b/src/StreamingBodyParser/FormUrlencoded.php index 6c87f7ca..59f3f961 100644 --- a/src/StreamingBodyParser/FormUrlencoded.php +++ b/src/StreamingBodyParser/FormUrlencoded.php @@ -9,55 +9,26 @@ class FormUrlencoded implements StreamingParserInterface { use EventEmitterTrait; - /** - * @var string - */ - protected $buffer = ''; - - /** - * @var Request - */ - protected $request; - - /** - * @var bool|integer - */ - protected $contentLength = false; - /** * @param Request $request */ public function __construct(Request $request) { - $this->request = $request; - - $headers = $this->request->getHeaders(); + $headers = $request->getHeaders(); $headers = array_change_key_case($headers, CASE_LOWER); - $this->contentLength = $headers['content-length']; - $this->request->on('data', [$this, 'feed']); + ContentLengthBufferedSink::createPromise( + $request, + $headers['content-length'] + )->then([$this, 'finish']); } /** - * @param string $data + * @param string $buffer */ - public function feed($data) - { - $this->buffer .= $data; - - if ( - $this->contentLength !== false && - strlen($this->buffer) >= $this->contentLength - ) { - $this->buffer = substr($this->buffer, 0, $this->contentLength); - $this->finish(); - } - } - - public function finish() + public function finish($buffer) { - $this->request->removeListener('data', [$this, 'feed']); - parse_str(trim($this->buffer), $result); + parse_str(trim($buffer), $result); foreach ($result as $key => $value) { $this->emit('post', [$key, $value]); } diff --git a/src/StreamingBodyParser/RawBody.php b/src/StreamingBodyParser/RawBody.php index c68b48c3..e6e9ca86 100644 --- a/src/StreamingBodyParser/RawBody.php +++ b/src/StreamingBodyParser/RawBody.php @@ -9,25 +9,26 @@ class RawBody implements StreamingParserInterface { use EventEmitterTrait; - protected $buffer = ''; - protected $contentLength; - + /** + * @param Request $request + */ public function __construct(Request $request) { $headers = $request->getHeaders(); $headers = array_change_key_case($headers, CASE_LOWER); - $this->contentLength = $headers['content-length']; - $request->on('data', [$this, 'feed']); + ContentLengthBufferedSink::createPromise( + $request, + $headers['content-length'] + )->then([$this, 'finish']); } - public function feed($data) + /** + * @param string $buffer + */ + public function finish($buffer) { - $this->buffer .= $data; - - if (strlen($this->buffer) >= $this->contentLength) { - $this->emit('body', [$this->buffer]); - $this->emit('end'); - } + $this->emit('body', [$buffer]); + $this->emit('end'); } } diff --git a/tests/StreamingBodyParser/ContentLengthBufferedSinkTest.php b/tests/StreamingBodyParser/ContentLengthBufferedSinkTest.php new file mode 100644 index 00000000..a3e2eea7 --- /dev/null +++ b/tests/StreamingBodyParser/ContentLengthBufferedSinkTest.php @@ -0,0 +1,24 @@ +then(function ($buffer) use (&$catchedBuffer) { + $catchedBuffer = $buffer; + }); + $request->emit('data', ['012345678']); + $request->emit('data', ['90123456789']); + $this->assertSame($expectedBuffer, $catchedBuffer); + } +} From 57bdc26d0453d7d0a2985e36951b15b799323f59 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 3 Aug 2016 09:55:49 +0200 Subject: [PATCH 59/64] Appended Parser to all streaming body parsers as suggested by @jsor at https://github.com/reactphp/http/pull/41#issuecomment-237160339 --- src/DeferredStream.php | 4 ++-- src/StreamingBodyParser/Factory.php | 8 ++++---- .../{FormUrlencoded.php => FormUrlencodedParser.php} | 2 +- .../{Multipart.php => MultipartParser.php} | 2 +- .../{NoBody.php => NoBodyParser.php} | 2 +- .../{RawBody.php => RawBodyParser.php} | 2 +- tests/DeferredStreamTest.php | 4 ++-- tests/StreamingBodyParser/FactoryTest.php | 12 ++++++------ ...lencodedTest.php => FormUrlencodedParserTest.php} | 6 +++--- .../{MultipartTest.php => MultipartParserTest.php} | 6 +++--- .../{RawBodyTest.php => RawBodyParserTest.php} | 6 +++--- 11 files changed, 27 insertions(+), 27 deletions(-) rename src/StreamingBodyParser/{FormUrlencoded.php => FormUrlencodedParser.php} (92%) rename src/StreamingBodyParser/{Multipart.php => MultipartParser.php} (99%) rename src/StreamingBodyParser/{NoBody.php => NoBodyParser.php} (78%) rename src/StreamingBodyParser/{RawBody.php => RawBodyParser.php} (92%) rename tests/StreamingBodyParser/{FormUrlencodedTest.php => FormUrlencodedParserTest.php} (83%) rename tests/StreamingBodyParser/{MultipartTest.php => MultipartParserTest.php} (97%) rename tests/StreamingBodyParser/{RawBodyTest.php => RawBodyParserTest.php} (78%) diff --git a/src/DeferredStream.php b/src/DeferredStream.php index d1c77ee1..325fbc51 100644 --- a/src/DeferredStream.php +++ b/src/DeferredStream.php @@ -2,7 +2,7 @@ namespace React\Http; -use React\Http\StreamingBodyParser\NoBody; +use React\Http\StreamingBodyParser\NoBodyParser; use React\Http\StreamingBodyParser\StreamingParserInterface; use React\Promise\Deferred; use React\Promise\PromiseInterface; @@ -16,7 +16,7 @@ class DeferredStream */ public static function create(StreamingParserInterface $parser) { - if ($parser instanceof NoBody) { + if ($parser instanceof NoBodyParser) { return \React\Promise\resolve([ 'post' => [], 'files' => [], diff --git a/src/StreamingBodyParser/Factory.php b/src/StreamingBodyParser/Factory.php index d0ba0f62..e43b177d 100644 --- a/src/StreamingBodyParser/Factory.php +++ b/src/StreamingBodyParser/Factory.php @@ -18,19 +18,19 @@ public static function create(Request $request) if ( !isset($headers['content-type']) && !isset($headers['content-length']) ) { - return new NoBody($request); + return new NoBodyParser($request); } $contentType = strtolower($headers['content-type']); if (strpos($contentType, 'multipart/') === 0) { - return new Multipart($request); + return new MultipartParser($request); } if (strpos($contentType, 'application/x-www-form-urlencoded') === 0) { - return new FormUrlencoded($request); + return new FormUrlencodedParser($request); } - return new RawBody($request); + return new RawBodyParser($request); } } diff --git a/src/StreamingBodyParser/FormUrlencoded.php b/src/StreamingBodyParser/FormUrlencodedParser.php similarity index 92% rename from src/StreamingBodyParser/FormUrlencoded.php rename to src/StreamingBodyParser/FormUrlencodedParser.php index 59f3f961..666a7956 100644 --- a/src/StreamingBodyParser/FormUrlencoded.php +++ b/src/StreamingBodyParser/FormUrlencodedParser.php @@ -5,7 +5,7 @@ use Evenement\EventEmitterTrait; use React\Http\Request; -class FormUrlencoded implements StreamingParserInterface +class FormUrlencodedParser implements StreamingParserInterface { use EventEmitterTrait; diff --git a/src/StreamingBodyParser/Multipart.php b/src/StreamingBodyParser/MultipartParser.php similarity index 99% rename from src/StreamingBodyParser/Multipart.php rename to src/StreamingBodyParser/MultipartParser.php index ccaa29cd..5b170449 100644 --- a/src/StreamingBodyParser/Multipart.php +++ b/src/StreamingBodyParser/MultipartParser.php @@ -7,7 +7,7 @@ use React\Http\Request; use React\Stream\ThroughStream; -class Multipart implements StreamingParserInterface +class MultipartParser implements StreamingParserInterface { use EventEmitterTrait; diff --git a/src/StreamingBodyParser/NoBody.php b/src/StreamingBodyParser/NoBodyParser.php similarity index 78% rename from src/StreamingBodyParser/NoBody.php rename to src/StreamingBodyParser/NoBodyParser.php index 93a56ec1..abe5adf2 100644 --- a/src/StreamingBodyParser/NoBody.php +++ b/src/StreamingBodyParser/NoBodyParser.php @@ -5,7 +5,7 @@ use Evenement\EventEmitterTrait; use React\Http\Request; -class NoBody implements StreamingParserInterface +class NoBodyParser implements StreamingParserInterface { use EventEmitterTrait; diff --git a/src/StreamingBodyParser/RawBody.php b/src/StreamingBodyParser/RawBodyParser.php similarity index 92% rename from src/StreamingBodyParser/RawBody.php rename to src/StreamingBodyParser/RawBodyParser.php index e6e9ca86..c510fc3a 100644 --- a/src/StreamingBodyParser/RawBody.php +++ b/src/StreamingBodyParser/RawBodyParser.php @@ -5,7 +5,7 @@ use Evenement\EventEmitterTrait; use React\Http\Request; -class RawBody implements StreamingParserInterface +class RawBodyParser implements StreamingParserInterface { use EventEmitterTrait; diff --git a/tests/DeferredStreamTest.php b/tests/DeferredStreamTest.php index b0badb3a..712fb480 100644 --- a/tests/DeferredStreamTest.php +++ b/tests/DeferredStreamTest.php @@ -6,7 +6,7 @@ use React\EventLoop\Factory; use React\Http\DeferredStream; use React\Http\File; -use React\Http\StreamingBodyParser\NoBody; +use React\Http\StreamingBodyParser\NoBodyParser; use React\Http\Request; use React\Stream\ThroughStream; use React\Tests\Http\StreamingBodyParser\DummyParser; @@ -15,7 +15,7 @@ class DeferredStreamTest extends TestCase { public function testDoneParser() { - $parser = new NoBody(new Request('get', 'http://example.com')); + $parser = new NoBodyParser(new Request('get', 'http://example.com')); $deferredStream = DeferredStream::create($parser); $result = Block\await($deferredStream, Factory::create(), 10); $this->assertSame([ diff --git a/tests/StreamingBodyParser/FactoryTest.php b/tests/StreamingBodyParser/FactoryTest.php index 3dfc9a19..740d8376 100644 --- a/tests/StreamingBodyParser/FactoryTest.php +++ b/tests/StreamingBodyParser/FactoryTest.php @@ -14,7 +14,7 @@ public function testMultipart() 'Content-Type' => 'multipart/mixed; boundary=---------------------------12758086162038677464950549563', ]); $parser = Factory::create($request); - $this->assertInstanceOf('React\Http\StreamingBodyParser\Multipart', $parser); + $this->assertInstanceOf('React\Http\StreamingBodyParser\MultipartParser', $parser); } public function testMultipartUTF8() @@ -23,7 +23,7 @@ public function testMultipartUTF8() 'Content-Type' => 'multipart/mixed; boundary=---------------------------12758086162038677464950549563; charset=utf8', ]); $parser = Factory::create($request); - $this->assertInstanceOf('React\Http\StreamingBodyParser\Multipart', $parser); + $this->assertInstanceOf('React\Http\StreamingBodyParser\MultipartParser', $parser); } public function testMultipartHeaderCaseInsensitive() @@ -32,7 +32,7 @@ public function testMultipartHeaderCaseInsensitive() 'CONTENT-TYPE' => 'multipart/mixed; boundary=---------------------------12758086162038677464950549563', ]); $parser = Factory::create($request); - $this->assertInstanceOf('React\Http\StreamingBodyParser\Multipart', $parser); + $this->assertInstanceOf('React\Http\StreamingBodyParser\MultipartParser', $parser); } public function testFormUrlencoded() @@ -42,7 +42,7 @@ public function testFormUrlencoded() 'content-length' => 123, ]); $parser = Factory::create($request); - $this->assertInstanceOf('React\Http\StreamingBodyParser\FormUrlencoded', $parser); + $this->assertInstanceOf('React\Http\StreamingBodyParser\FormUrlencodedParser', $parser); } public function testFormUrlencodedUTF8() @@ -52,7 +52,7 @@ public function testFormUrlencodedUTF8() 'content-length' => 123, ]); $parser = Factory::create($request); - $this->assertInstanceOf('React\Http\StreamingBodyParser\FormUrlencoded', $parser); + $this->assertInstanceOf('React\Http\StreamingBodyParser\FormUrlencodedParser', $parser); } public function testFormUrlencodedHeaderCaseInsensitive() @@ -62,6 +62,6 @@ public function testFormUrlencodedHeaderCaseInsensitive() 'content-length' => 123, ]); $parser = Factory::create($request); - $this->assertInstanceOf('React\Http\StreamingBodyParser\FormUrlencoded', $parser); + $this->assertInstanceOf('React\Http\StreamingBodyParser\FormUrlencodedParser', $parser); } } diff --git a/tests/StreamingBodyParser/FormUrlencodedTest.php b/tests/StreamingBodyParser/FormUrlencodedParserTest.php similarity index 83% rename from tests/StreamingBodyParser/FormUrlencodedTest.php rename to tests/StreamingBodyParser/FormUrlencodedParserTest.php index d9dd5cfe..81bbb605 100644 --- a/tests/StreamingBodyParser/FormUrlencodedTest.php +++ b/tests/StreamingBodyParser/FormUrlencodedParserTest.php @@ -2,11 +2,11 @@ namespace React\Tests\Http\StreamingBodyParser; -use React\Http\StreamingBodyParser\FormUrlencoded; +use React\Http\StreamingBodyParser\FormUrlencodedParser; use React\Http\Request; use React\Tests\Http\TestCase; -class FormUrlencodedTest extends TestCase +class FormUrlencodedParserTest extends TestCase { public function testParse() { @@ -14,7 +14,7 @@ public function testParse() $request = new Request('POST', 'http://example.com/', [], '1.1', [ 'Content-Length' => 79, ]); - $parser = new FormUrlencoded($request); + $parser = new FormUrlencodedParser($request); $parser->on('post', function ($key, $value) use (&$post) { $post[] = [$key, $value]; }); diff --git a/tests/StreamingBodyParser/MultipartTest.php b/tests/StreamingBodyParser/MultipartParserTest.php similarity index 97% rename from tests/StreamingBodyParser/MultipartTest.php rename to tests/StreamingBodyParser/MultipartParserTest.php index b5ec865c..27863388 100644 --- a/tests/StreamingBodyParser/MultipartTest.php +++ b/tests/StreamingBodyParser/MultipartParserTest.php @@ -3,7 +3,7 @@ namespace React\Tests\Http\StreamingBodyParser; use React\Http\FileInterface; -use React\Http\StreamingBodyParser\Multipart; +use React\Http\StreamingBodyParser\MultipartParser; use React\Http\Request; use React\Tests\Http\TestCase; @@ -21,7 +21,7 @@ public function testPostKey() 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, ]); - $parser = new Multipart($request); + $parser = new MultipartParser($request); $parser->on('post', function ($key, $value) use (&$post) { $post[$key] = $value; }); @@ -62,7 +62,7 @@ public function testFileUpload() 'Content-Type' => 'multipart/form-data', ]); - $multipart = new Multipart($request); + $multipart = new MultipartParser($request); $multipart->on('post', function ($key, $value) use (&$post) { $post[] = [$key => $value]; diff --git a/tests/StreamingBodyParser/RawBodyTest.php b/tests/StreamingBodyParser/RawBodyParserTest.php similarity index 78% rename from tests/StreamingBodyParser/RawBodyTest.php rename to tests/StreamingBodyParser/RawBodyParserTest.php index 601e5266..c9a25a83 100644 --- a/tests/StreamingBodyParser/RawBodyTest.php +++ b/tests/StreamingBodyParser/RawBodyParserTest.php @@ -2,11 +2,11 @@ namespace React\Tests\Http\StreamingBodyParser; -use React\Http\StreamingBodyParser\RawBody; +use React\Http\StreamingBodyParser\RawBodyParser; use React\Http\Request; use React\Tests\Http\TestCase; -class RawBodyTest extends TestCase +class RawBodyParserTest extends TestCase { public function testNoContentLength() { @@ -14,7 +14,7 @@ public function testNoContentLength() $request = new Request('POST', 'http://example.com/', [], 1.1, [ 'content-length' => 3, ]); - $parser = new RawBody($request); + $parser = new RawBodyParser($request); $parser->on('body', function ($rawBody) use (&$body) { $body = $rawBody; }); From ae172a5634b5dbe3ec6dca717874304ac941ff12 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 3 Aug 2016 10:36:51 +0200 Subject: [PATCH 60/64] Renamed DeferredStream to BufferedSink and modled it more like react/stream's BufferedSink as suggested by @jsor at https://github.com/reactphp/http/pull/41#issuecomment-237160339 --- README.md | 4 ++-- .../BufferedSink.php} | 13 +++++------ .../BufferedSinkTest.php} | 22 +++++++++---------- 3 files changed, 19 insertions(+), 20 deletions(-) rename src/{DeferredStream.php => StreamingBodyParser/BufferedSink.php} (86%) rename tests/{DeferredStreamTest.php => StreamingBodyParser/BufferedSinkTest.php} (78%) diff --git a/README.md b/README.md index 4e3385e1..407655c7 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ This is an HTTP server which responds with `Hello World` to every request. $loop->run(); ``` -## StreamingBodyParser\Factory and DeferredStream Usage +## StreamingBodyParser\Factory and BufferedSink Usage The `FormParserFactory` parses a request and determines which body parser to use (multipart, formurlencoded, raw body, or no body). Those body parsers emit events on `post` fields, `file` on files, and raw body emits `body` when it received the whole body. `DeferredStream` listens for those events and returns them through a promise when done. @@ -43,7 +43,7 @@ The `FormParserFactory` parses a request and determines which body parser to use $http = new React\Http\Server($socket); $http->on('request', function ($request, $response) { $parser = React\Http\StreamingBodyParser\Factory::create($request); - DeferredStream::create($parser)->then(function ($result) use ($response) { + BufferedSink::createPromise($parser)->then(function ($result) use ($response) { var_export($result); $response->writeHead(200, array('Content-Type' => 'text/plain')); $response->end("Hello World!\n"); diff --git a/src/DeferredStream.php b/src/StreamingBodyParser/BufferedSink.php similarity index 86% rename from src/DeferredStream.php rename to src/StreamingBodyParser/BufferedSink.php index 325fbc51..77afad29 100644 --- a/src/DeferredStream.php +++ b/src/StreamingBodyParser/BufferedSink.php @@ -1,20 +1,19 @@ on('file', function ($name, File $file) use (&$files) { - BufferedSink::createPromise($file->getStream())->then(function ($buffer) use ($name, $file, &$files) { + StreamBufferedSink::createPromise($file->getStream())->then(function ($buffer) use ($name, $file, &$files) { $files[] = [ 'name' => $name, 'file' => $file, diff --git a/tests/DeferredStreamTest.php b/tests/StreamingBodyParser/BufferedSinkTest.php similarity index 78% rename from tests/DeferredStreamTest.php rename to tests/StreamingBodyParser/BufferedSinkTest.php index 712fb480..27623bb8 100644 --- a/tests/DeferredStreamTest.php +++ b/tests/StreamingBodyParser/BufferedSinkTest.php @@ -1,22 +1,22 @@ assertSame([ 'post' => [], @@ -28,7 +28,7 @@ public function testDoneParser() public function testDeferredStream() { $parser = new DummyParser(new Request('get', 'http://example.com')); - $deferredStream = DeferredStream::create($parser); + $deferredStream = BufferedSink::createPromise($parser); $parser->emit('post', ['foo', 'bar']); $parser->emit('post', ['array[]', 'foo']); $parser->emit('post', ['array[]', 'bar']); @@ -72,12 +72,12 @@ public function testDeferredStream() public function testExtractPost() { $postFields = []; - DeferredStream::extractPost($postFields, 'dem', 'value'); - DeferredStream::extractPost($postFields, 'dom[one][two][]', 'value_a'); - DeferredStream::extractPost($postFields, 'dom[one][two][]', 'value_b'); - DeferredStream::extractPost($postFields, 'dam[]', 'value_a'); - DeferredStream::extractPost($postFields, 'dam[]', 'value_b'); - DeferredStream::extractPost($postFields, 'dum[sum]', 'value'); + BufferedSink::extractPost($postFields, 'dem', 'value'); + BufferedSink::extractPost($postFields, 'dom[one][two][]', 'value_a'); + BufferedSink::extractPost($postFields, 'dom[one][two][]', 'value_b'); + BufferedSink::extractPost($postFields, 'dam[]', 'value_a'); + BufferedSink::extractPost($postFields, 'dam[]', 'value_b'); + BufferedSink::extractPost($postFields, 'dum[sum]', 'value'); $this->assertSame([ 'dem' => 'value', 'dom' => [ From 359781cb47d8301422696f687c2edd6d6e794998 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 3 Aug 2016 10:47:40 +0200 Subject: [PATCH 61/64] Renamed StreamingParserInterface to ParserInterface as suggested by @jsor at https://github.com/reactphp/http/pull/41#commitcomment-18500090 --- src/StreamingBodyParser/BufferedSink.php | 4 ++-- src/StreamingBodyParser/Factory.php | 2 +- src/StreamingBodyParser/FormUrlencodedParser.php | 2 +- src/StreamingBodyParser/MultipartParser.php | 2 +- src/StreamingBodyParser/NoBodyParser.php | 2 +- .../{StreamingParserInterface.php => ParserInterface.php} | 2 +- src/StreamingBodyParser/RawBodyParser.php | 2 +- tests/StreamingBodyParser/DummyParser.php | 4 ++-- 8 files changed, 10 insertions(+), 10 deletions(-) rename src/StreamingBodyParser/{StreamingParserInterface.php => ParserInterface.php} (71%) diff --git a/src/StreamingBodyParser/BufferedSink.php b/src/StreamingBodyParser/BufferedSink.php index 77afad29..aecc9663 100644 --- a/src/StreamingBodyParser/BufferedSink.php +++ b/src/StreamingBodyParser/BufferedSink.php @@ -10,10 +10,10 @@ class BufferedSink { /** - * @param StreamingParserInterface $parser + * @param ParserInterface $parser * @return PromiseInterface */ - public static function createPromise(StreamingParserInterface $parser) + public static function createPromise(ParserInterface $parser) { if ($parser instanceof NoBodyParser) { return \React\Promise\resolve([ diff --git a/src/StreamingBodyParser/Factory.php b/src/StreamingBodyParser/Factory.php index e43b177d..fb09256e 100644 --- a/src/StreamingBodyParser/Factory.php +++ b/src/StreamingBodyParser/Factory.php @@ -8,7 +8,7 @@ class Factory { /** * @param Request $request - * @return StreamingParserInterface + * @return ParserInterface */ public static function create(Request $request) { diff --git a/src/StreamingBodyParser/FormUrlencodedParser.php b/src/StreamingBodyParser/FormUrlencodedParser.php index 666a7956..a3f6f88b 100644 --- a/src/StreamingBodyParser/FormUrlencodedParser.php +++ b/src/StreamingBodyParser/FormUrlencodedParser.php @@ -5,7 +5,7 @@ use Evenement\EventEmitterTrait; use React\Http\Request; -class FormUrlencodedParser implements StreamingParserInterface +class FormUrlencodedParser implements ParserInterface { use EventEmitterTrait; diff --git a/src/StreamingBodyParser/MultipartParser.php b/src/StreamingBodyParser/MultipartParser.php index 5b170449..3c6665d1 100644 --- a/src/StreamingBodyParser/MultipartParser.php +++ b/src/StreamingBodyParser/MultipartParser.php @@ -7,7 +7,7 @@ use React\Http\Request; use React\Stream\ThroughStream; -class MultipartParser implements StreamingParserInterface +class MultipartParser implements ParserInterface { use EventEmitterTrait; diff --git a/src/StreamingBodyParser/NoBodyParser.php b/src/StreamingBodyParser/NoBodyParser.php index abe5adf2..6ac9c953 100644 --- a/src/StreamingBodyParser/NoBodyParser.php +++ b/src/StreamingBodyParser/NoBodyParser.php @@ -5,7 +5,7 @@ use Evenement\EventEmitterTrait; use React\Http\Request; -class NoBodyParser implements StreamingParserInterface +class NoBodyParser implements ParserInterface { use EventEmitterTrait; diff --git a/src/StreamingBodyParser/StreamingParserInterface.php b/src/StreamingBodyParser/ParserInterface.php similarity index 71% rename from src/StreamingBodyParser/StreamingParserInterface.php rename to src/StreamingBodyParser/ParserInterface.php index a7a469a0..8539b1c1 100644 --- a/src/StreamingBodyParser/StreamingParserInterface.php +++ b/src/StreamingBodyParser/ParserInterface.php @@ -5,7 +5,7 @@ use Evenement\EventEmitterInterface; use React\Http\Request; -interface StreamingParserInterface extends EventEmitterInterface +interface ParserInterface extends EventEmitterInterface { public function __construct(Request $request); } diff --git a/src/StreamingBodyParser/RawBodyParser.php b/src/StreamingBodyParser/RawBodyParser.php index c510fc3a..b3681df1 100644 --- a/src/StreamingBodyParser/RawBodyParser.php +++ b/src/StreamingBodyParser/RawBodyParser.php @@ -5,7 +5,7 @@ use Evenement\EventEmitterTrait; use React\Http\Request; -class RawBodyParser implements StreamingParserInterface +class RawBodyParser implements ParserInterface { use EventEmitterTrait; diff --git a/tests/StreamingBodyParser/DummyParser.php b/tests/StreamingBodyParser/DummyParser.php index 36fd5c30..7385a82f 100644 --- a/tests/StreamingBodyParser/DummyParser.php +++ b/tests/StreamingBodyParser/DummyParser.php @@ -3,10 +3,10 @@ namespace React\Tests\Http\StreamingBodyParser; use Evenement\EventEmitterTrait; -use React\Http\StreamingBodyParser\StreamingParserInterface; +use React\Http\StreamingBodyParser\ParserInterface; use React\Http\Request; -class DummyParser implements StreamingParserInterface +class DummyParser implements ParserInterface { use EventEmitterTrait; From f045fc4c6f94ad69d724720f73e9b56333c9480a Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 3 Aug 2016 11:12:10 +0200 Subject: [PATCH 62/64] Readme clarifications (suggested by @jsor) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 407655c7..6a30530f 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ This is an HTTP server which responds with `Hello World` to every request. $loop->run(); ``` -## StreamingBodyParser\Factory and BufferedSink Usage +## StreamingBodyParser\Factory and StreamingBodyParser\BufferedSink Usage -The `FormParserFactory` parses a request and determines which body parser to use (multipart, formurlencoded, raw body, or no body). Those body parsers emit events on `post` fields, `file` on files, and raw body emits `body` when it received the whole body. `DeferredStream` listens for those events and returns them through a promise when done. +The `StreamingBodyParser\FormParserFactory` parses a request and determines which body parser to use (multipart, formurlencoded, raw body, or no body). Those body parsers emit events on `post` fields, `file` on files, and raw body emits `body` when it received the whole body. `StreamingBodyParser\BufferedSink` listens for those events and returns them through a promise when done. ```php $loop = React\EventLoop\Factory::create(); @@ -43,7 +43,7 @@ The `FormParserFactory` parses a request and determines which body parser to use $http = new React\Http\Server($socket); $http->on('request', function ($request, $response) { $parser = React\Http\StreamingBodyParser\Factory::create($request); - BufferedSink::createPromise($parser)->then(function ($result) use ($response) { + React\Http\StreamingBodyParser\BufferedSink::createPromise($parser)->then(function ($result) use ($response) { var_export($result); $response->writeHead(200, array('Content-Type' => 'text/plain')); $response->end("Hello World!\n"); From abb4ca0cf0569f5012ed59f54a93547f92ac076c Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 6 Sep 2016 19:12:56 +0200 Subject: [PATCH 63/64] Catch the case where no content-type header is send --- src/StreamingBodyParser/Factory.php | 4 ++++ tests/StreamingBodyParser/FactoryTest.php | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/src/StreamingBodyParser/Factory.php b/src/StreamingBodyParser/Factory.php index fb09256e..fd4908ff 100644 --- a/src/StreamingBodyParser/Factory.php +++ b/src/StreamingBodyParser/Factory.php @@ -21,6 +21,10 @@ public static function create(Request $request) return new NoBodyParser($request); } + if (!isset($headers['content-type'])) { + return new RawBodyParser($request); + } + $contentType = strtolower($headers['content-type']); if (strpos($contentType, 'multipart/') === 0) { diff --git a/tests/StreamingBodyParser/FactoryTest.php b/tests/StreamingBodyParser/FactoryTest.php index 740d8376..e7158438 100644 --- a/tests/StreamingBodyParser/FactoryTest.php +++ b/tests/StreamingBodyParser/FactoryTest.php @@ -64,4 +64,13 @@ public function testFormUrlencodedHeaderCaseInsensitive() $parser = Factory::create($request); $this->assertInstanceOf('React\Http\StreamingBodyParser\FormUrlencodedParser', $parser); } + + public function testNoContentType() + { + $request = new Request('POST', 'http://example.com/', [], 1.1, [ + 'content-length' => 123, + ]); + $parser = Factory::create($request); + $this->assertInstanceOf('React\Http\StreamingBodyParser\RawBodyParser', $parser); + } } From d4cef86dc9c6d415ce88cf14c28e60e82c4458e3 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 7 Sep 2016 21:06:43 +0200 Subject: [PATCH 64/64] Deal with an edge case where a zero length will never fulfill the promise --- src/StreamingBodyParser/ContentLengthBufferedSink.php | 9 +++++++-- .../ContentLengthBufferedSinkTest.php | 10 ++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/StreamingBodyParser/ContentLengthBufferedSink.php b/src/StreamingBodyParser/ContentLengthBufferedSink.php index cea7546f..ac6107f8 100644 --- a/src/StreamingBodyParser/ContentLengthBufferedSink.php +++ b/src/StreamingBodyParser/ContentLengthBufferedSink.php @@ -21,7 +21,7 @@ class ContentLengthBufferedSink /** * @var string */ - protected $buffer; + protected $buffer = ''; /** * @var int @@ -51,6 +51,7 @@ protected function __construct(Deferred $deferred, Request $request, $length) $this->request = $request; $this->length = $length; $this->request->on('data', [$this, 'feed']); + $this->check(); } /** @@ -60,6 +61,11 @@ public function feed($data) { $this->buffer .= $data; + $this->check(); + } + + protected function check() + { if ( $this->length !== false && strlen($this->buffer) >= $this->length @@ -69,5 +75,4 @@ public function feed($data) $this->deferred->resolve($this->buffer); } } - } diff --git a/tests/StreamingBodyParser/ContentLengthBufferedSinkTest.php b/tests/StreamingBodyParser/ContentLengthBufferedSinkTest.php index a3e2eea7..966884f0 100644 --- a/tests/StreamingBodyParser/ContentLengthBufferedSinkTest.php +++ b/tests/StreamingBodyParser/ContentLengthBufferedSinkTest.php @@ -21,4 +21,14 @@ public function testCreatePromise() $request->emit('data', ['90123456789']); $this->assertSame($expectedBuffer, $catchedBuffer); } + + public function testZeroLengthBuffer() + { + $catchedBuffer = null; + $request = new Request('GET', 'http://example.com/'); + ContentLengthBufferedSink::createPromise($request, 0)->then(function ($buffer) use (&$catchedBuffer) { + $catchedBuffer = $buffer; + }); + $this->assertSame('', $catchedBuffer); + } }