From cb454da41164fff23358050bc3478d6335846f3f Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 10 Jun 2017 22:28:42 +0200 Subject: [PATCH 01/11] Multipart parser --- composer.json | 4 +- src/StreamingBodyParser/MultipartParser.php | 313 ++++++++++++++++++ .../MultipartParserTest.php | 229 +++++++++++++ 3 files changed, 544 insertions(+), 2 deletions(-) create mode 100644 src/StreamingBodyParser/MultipartParser.php create mode 100644 tests/StreamingBodyParser/MultipartParserTest.php diff --git a/composer.json b/composer.json index e73e29c7..a99878a2 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,8 @@ "react/socket": "^1.0 || ^0.8 || ^0.7 || ^0.6 || ^0.5", "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.6", "react/promise": "^2.1 || ^1.2.1", - "evenement/evenement": "^2.0 || ^1.0" + "evenement/evenement": "^2.0 || ^1.0", + "react/promise-stream": "^0.1" }, "autoload": { "psr-4": { @@ -18,7 +19,6 @@ }, "require-dev": { "phpunit/phpunit": "^4.8.10||^5.0", - "react/promise-stream": "^0.1.1", "react/socket": "^1.0 || ^0.8 || ^0.7", "clue/block-react": "^1.1" } diff --git a/src/StreamingBodyParser/MultipartParser.php b/src/StreamingBodyParser/MultipartParser.php new file mode 100644 index 00000000..fa538924 --- /dev/null +++ b/src/StreamingBodyParser/MultipartParser.php @@ -0,0 +1,313 @@ +onDataCallable = [$this, 'onData']; + $this->promise = (new Deferred(function () { + $this->body->removeListener('data', $this->onDataCallable); + $this->body->close(); + }))->promise(); + $this->request = $request; + $this->body = $this->request->getBody(); + + $dataMethod = $this->determineOnDataMethod(); + $this->setOnDataListener([$this, $dataMethod]); + } + + protected function determineOnDataMethod() + { + if (!$this->request->hasHeader('content-type')) { + return 'findBoundary'; + } + + $contentType = $this->request->getHeaderLine('content-type'); + preg_match('/boundary="?(.*)"?$/', $contentType, $matches); + if (isset($matches[1])) { + $this->setBoundary($matches[1]); + return 'onData'; + } + + return 'findBoundary'; + } + + 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) { + $boundary = substr($this->buffer, 2, strpos($this->buffer, "\r\n")); + $boundary = substr($boundary, 0, -2); + $this->setBoundary($boundary); + $this->setOnDataListener([$this, 'onData']); + } + } + + public function onData($data) + { + $this->buffer .= $data; + $ending = strpos($this->buffer, $this->ending) == strlen($this->buffer) - $this->endingSize; + + if ( + 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); + $this->buffer = array_pop($chunks); + foreach ($chunks as $chunk) { + $chunk = $this->stripTrainingEOL($chunk); + $this->parseChunk($chunk); + } + + $split = explode("\r\n\r\n", $this->buffer, 2); + if (count($split) <= 1) { + return; + } + + //$chunks = preg_split('/-+' . $this->boundary . '/', trim($split[0]), -1, PREG_SPLIT_NO_EMPTY); + $chunks = preg_split('/-+' . $this->boundary . '/', $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 = ''; + } + } + + protected function parseChunk($chunk) + { + if ($chunk === '') { + return; + } + + list ($header, $body) = explode("\r\n\r\n", $chunk, 2); + $headers = $this->parseHeaders($header); + + if (!isset($headers['content-disposition'])) { + 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', [ + $this->getFieldFromHeader($headers['content-disposition'], 'name'), + new UploadedFile( + new HttpBodyStream($stream, null), + 0, + UPLOAD_ERR_OK, + $this->getFieldFromHeader($headers['content-disposition'], 'filename'), + $headers['content-type'][0] + ), + $headers, + ]); + + if (!$streaming) { + $stream->end($body); + return; + } + + $this->setOnDataListener($this->chunkStreamFunc($stream)); + $stream->write($body); + } + + protected function chunkStreamFunc(ThroughStream $stream) + { + $buffer = ''; + $func = function($data) use (&$buffer, $stream) { + $buffer .= $data; + if (strpos($buffer, $this->boundary) !== false) { + $chunks = preg_split('/-+' . $this->boundary . '/', $buffer); + $chunk = array_shift($chunks); + $chunk = $this->stripTrainingEOL($chunk); + $stream->end($chunk); + + $this->setOnDataListener([$this, 'onData']); + + if (count($chunks) == 1) { + array_unshift($chunks, ''); + } + + $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); + $this->emit('post', [ + $matches[1], + $body, + $headers, + ]); + } + } + } + + protected function parseHeaders($header) + { + $headers = []; + + foreach (explode("\r\n", trim($header)) as $line) { + list($key, $values) = explode(':', $line, 2); + $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; + } + + protected function getFieldFromHeader(array $header, $field) + { + foreach ($header as $part) { + if (strpos($part, $field) === 0) { + preg_match('/' . $field . '="?(.*)"$/', $part, $matches); + return $matches[1]; + } + } + + return ''; + } + + protected function setOnDataListener(callable $callable) + { + $this->body->removeListener('data', $this->onDataCallable); + $this->onDataCallable = $callable; + $this->body->on('data', $this->onDataCallable); + } + + public function cancel() + { + $this->promise->cancel(); + } + + private function stripTrainingEOL($chunk) + { + if (substr($chunk, -2) === "\r\n") { + return substr($chunk, 0, -2); + } + + return $chunk; + } +} diff --git a/tests/StreamingBodyParser/MultipartParserTest.php b/tests/StreamingBodyParser/MultipartParserTest.php new file mode 100644 index 00000000..1edbe87d --- /dev/null +++ b/tests/StreamingBodyParser/MultipartParserTest.php @@ -0,0 +1,229 @@ + 'multipart/mixed; boundary=' . $boundary, + ), new HttpBodyStream($stream, 0), 1.1); + + $parser = new MultipartParser($request); + $parser->on('post', function ($key, $value) use (&$post) { + $post[$key] = $value; + }); + $parser->on('file', function ($name, UploadedFileInterface $file) use (&$files) { + $files[] = [$name, $file]; + }); + + $data = "--$boundary\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=\"users[two]\"\r\n"; + $data .= "\r\n"; + $data .= "second\r\n"; + $data .= "--$boundary--\r\n"; + + $stream->write($data); + + $this->assertEmpty($files); + $this->assertEquals( + [ + 'users[one]' => 'single', + 'users[two]' => 'second', + ], + $post + ); + } + + public function testFileUpload() + { + $files = []; + $post = []; + + $stream = new ThroughStream(); + $boundary = "---------------------------12758086162038677464950549563"; + + $request = new Request('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data', + ), new HttpBodyStream($stream, 0), 1.1); + + $multipart = new MultipartParser($request); + + $multipart->on('post', function ($key, $value) use (&$post) { + $post[] = [$key => $value]; + }); + $multipart->on('file', function ($name, UploadedFileInterface $file, $headers) use (&$files) { + Stream\buffer($file->getStream())->done(function ($buffer) use ($name, $file, $headers, &$files) { + $body = new BufferStream(strlen($buffer)); + $body->write($buffer); + $files[] = [ + $name, + new UploadedFile( + $body, + strlen($buffer), + $file->getError(), + $file->getClientFilename(), + $file->getClientMediaType() + ), + $headers, + ]; + }); + }); + + $file = base64_decode("R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="); + + $data = "--$boundary\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=\"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=\"Us er.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("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" . + "Content-Type: text/php\r\n" . + "\r\n" . + "write("\r\n" . + "--$boundary\r\n" . + "Content-Disposition: form-data; name=\"files[]\"; filename=\"Owner.php\"\r\n" . + "Content-Type: text/php\r\n" . + "\r\n" . + "assertEquals(6, count($post)); + $this->assertEquals( + [ + ['users[one]' => 'single'], + ['users[two]' => 'second'], + ['user' => 'single'], + ['user2' => 'second'], + ['users[]' => 'first in array'], + ['users[]' => 'second in array'], + ], + $post + ); + + $this->assertEquals(4, count($files)); + $this->assertEquals('file', $files[0][0]); + $this->assertEquals('Us er.php', $files[0][1]->getClientFilename()); + $this->assertEquals('text/php', $files[0][1]->getClientMediaType()); + $this->assertEquals("getStream()->getContents()); + $this->assertEquals([ + 'content-disposition' => [ + 'form-data', + 'name="file"', + 'filename="Us er.php"', + ], + 'content-type' => [ + 'text/php', + ], + ], $files[0][2]); + + $this->assertEquals('files[]', $files[1][0]); + $this->assertEquals('blank.gif', $files[1][1]->getClientFilename()); + $this->assertEquals('image/gif', $files[1][1]->getClientMediaType()); + $this->assertEquals($file, $files[1][1]->getStream()->getContents()); + $this->assertEquals([ + 'content-disposition' => [ + 'form-data', + 'name="files[]"', + 'filename="blank.gif"', + ], + 'content-type' => [ + 'image/gif', + ], + 'x-foo-bar' => [ + 'base64', + ], + ], $files[1][2]); + + $this->assertEquals('files[]', $files[2][0]); + $this->assertEquals('User.php', $files[2][1]->getClientFilename()); + $this->assertEquals('text/php', $files[2][1]->getClientMediaType()); + $this->assertEquals("getStream()->getContents()); + $this->assertEquals([ + 'content-disposition' => [ + 'form-data', + 'name="files[]"', + 'filename="User.php"', + ], + 'content-type' => [ + 'text/php', + ], + ], $files[2][2]); + + $this->assertEquals('files[]', $files[3][0]); + $this->assertEquals('Owner.php', $files[3][1]->getClientFilename()); + $this->assertEquals('text/php', $files[3][1]->getClientMediaType()); + $this->assertEquals("getStream()->getContents()); + $this->assertEquals([ + 'content-disposition' => [ + 'form-data', + 'name="files[]"', + 'filename="Owner.php"', + ], + 'content-type' => [ + 'text/php', + ], + ], $files[3][2]); + } +} From 6692b63649263241b19fc256417679263ea4bc32 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 12 Jun 2017 08:27:28 +0200 Subject: [PATCH 02/11] Removed commented out line --- src/StreamingBodyParser/MultipartParser.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/StreamingBodyParser/MultipartParser.php b/src/StreamingBodyParser/MultipartParser.php index fa538924..1ede0325 100644 --- a/src/StreamingBodyParser/MultipartParser.php +++ b/src/StreamingBodyParser/MultipartParser.php @@ -133,7 +133,6 @@ protected function parseBuffer() return; } - //$chunks = preg_split('/-+' . $this->boundary . '/', trim($split[0]), -1, PREG_SPLIT_NO_EMPTY); $chunks = preg_split('/-+' . $this->boundary . '/', $split[0], -1, PREG_SPLIT_NO_EMPTY); $headers = $this->parseHeaders(trim($chunks[0])); if (isset($headers['content-disposition']) && $this->headerStartsWith($headers['content-disposition'], 'filename')) { From 72b13b6511375021649140be45f4be163e15a1d1 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 12 Jun 2017 08:41:18 +0200 Subject: [PATCH 03/11] PHP 5.3 fixes --- .../MultipartParserTest.php | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/tests/StreamingBodyParser/MultipartParserTest.php b/tests/StreamingBodyParser/MultipartParserTest.php index 1edbe87d..8f59b4a1 100644 --- a/tests/StreamingBodyParser/MultipartParserTest.php +++ b/tests/StreamingBodyParser/MultipartParserTest.php @@ -17,8 +17,8 @@ class MultipartParserTest extends TestCase public function testPostKey() { - $files = []; - $post = []; + $files = array(); + $post = array(); $stream = new ThroughStream(); $boundary = "---------------------------5844729766471062541057622570"; @@ -32,7 +32,7 @@ public function testPostKey() $post[$key] = $value; }); $parser->on('file', function ($name, UploadedFileInterface $file) use (&$files) { - $files[] = [$name, $file]; + $files[] = array($name, $file); }); $data = "--$boundary\r\n"; @@ -49,18 +49,18 @@ public function testPostKey() $this->assertEmpty($files); $this->assertEquals( - [ + array( 'users[one]' => 'single', 'users[two]' => 'second', - ], + ), $post ); } public function testFileUpload() { - $files = []; - $post = []; + $files = array(); + $post = array(); $stream = new ThroughStream(); $boundary = "---------------------------12758086162038677464950549563"; @@ -72,13 +72,13 @@ public function testFileUpload() $multipart = new MultipartParser($request); $multipart->on('post', function ($key, $value) use (&$post) { - $post[] = [$key => $value]; + $post[] = array($key => $value); }); $multipart->on('file', function ($name, UploadedFileInterface $file, $headers) use (&$files) { Stream\buffer($file->getStream())->done(function ($buffer) use ($name, $file, $headers, &$files) { $body = new BufferStream(strlen($buffer)); $body->write($buffer); - $files[] = [ + $files[] = array( $name, new UploadedFile( $body, @@ -88,7 +88,7 @@ public function testFileUpload() $file->getClientMediaType() ), $headers, - ]; + ); }); }); @@ -151,14 +151,14 @@ public function testFileUpload() $this->assertEquals(6, count($post)); $this->assertEquals( - [ - ['users[one]' => 'single'], - ['users[two]' => 'second'], - ['user' => 'single'], - ['user2' => 'second'], - ['users[]' => 'first in array'], - ['users[]' => 'second in array'], - ], + array( + array('users[one]' => 'single'), + array('users[two]' => 'second'), + array('user' => 'single'), + array('user2' => 'second'), + array('users[]' => 'first in array'), + array('users[]' => 'second in array'), + ), $post ); @@ -167,63 +167,63 @@ public function testFileUpload() $this->assertEquals('Us er.php', $files[0][1]->getClientFilename()); $this->assertEquals('text/php', $files[0][1]->getClientMediaType()); $this->assertEquals("getStream()->getContents()); - $this->assertEquals([ - 'content-disposition' => [ + $this->assertEquals(array( + 'content-disposition' => array( 'form-data', 'name="file"', 'filename="Us er.php"', - ], - 'content-type' => [ + ), + 'content-type' => array( 'text/php', - ], - ], $files[0][2]); + ), + ), $files[0][2]); $this->assertEquals('files[]', $files[1][0]); $this->assertEquals('blank.gif', $files[1][1]->getClientFilename()); $this->assertEquals('image/gif', $files[1][1]->getClientMediaType()); $this->assertEquals($file, $files[1][1]->getStream()->getContents()); - $this->assertEquals([ - 'content-disposition' => [ + $this->assertEquals(array( + 'content-disposition' => array( 'form-data', 'name="files[]"', 'filename="blank.gif"', - ], - 'content-type' => [ + ), + 'content-type' => array( 'image/gif', - ], - 'x-foo-bar' => [ + ), + 'x-foo-bar' => array( 'base64', - ], - ], $files[1][2]); + ), + ), $files[1][2]); $this->assertEquals('files[]', $files[2][0]); $this->assertEquals('User.php', $files[2][1]->getClientFilename()); $this->assertEquals('text/php', $files[2][1]->getClientMediaType()); $this->assertEquals("getStream()->getContents()); - $this->assertEquals([ - 'content-disposition' => [ + $this->assertEquals(array( + 'content-disposition' => array( 'form-data', 'name="files[]"', 'filename="User.php"', - ], - 'content-type' => [ + ), + 'content-type' => array( 'text/php', - ], - ], $files[2][2]); + ), + ), $files[2][2]); $this->assertEquals('files[]', $files[3][0]); $this->assertEquals('Owner.php', $files[3][1]->getClientFilename()); $this->assertEquals('text/php', $files[3][1]->getClientMediaType()); $this->assertEquals("getStream()->getContents()); - $this->assertEquals([ - 'content-disposition' => [ + $this->assertEquals(array( + 'content-disposition' => array( 'form-data', 'name="files[]"', 'filename="Owner.php"', - ], - 'content-type' => [ + ), + 'content-type' => array( 'text/php', - ], - ], $files[3][2]); + ), + ), $files[3][2]); } } From a1049c7ca89b6d764114eba6da11a1ef3d014a21 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 12 Jun 2017 08:47:32 +0200 Subject: [PATCH 04/11] Use EventEmitter instead of EventEmitterTrait --- src/StreamingBodyParser/MultipartParser.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/StreamingBodyParser/MultipartParser.php b/src/StreamingBodyParser/MultipartParser.php index 1ede0325..5aef98ff 100644 --- a/src/StreamingBodyParser/MultipartParser.php +++ b/src/StreamingBodyParser/MultipartParser.php @@ -2,7 +2,7 @@ namespace React\Http\StreamingBodyParser; -use Evenement\EventEmitterTrait; +use Evenement\EventEmitter; use Psr\Http\Message\RequestInterface; use React\Http\HttpBodyStream; use React\Http\UploadedFile; @@ -10,10 +10,8 @@ use React\Promise\Deferred; use React\Stream\ThroughStream; -final class MultipartParser +final class MultipartParser extends EventEmitter { - use EventEmitterTrait; - /** * @var string */ From 8bb666c2818dd273e2cec25322e6b92dca0dbf0d Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 14 Jun 2017 12:48:26 +0200 Subject: [PATCH 05/11] PHP 5.3 array notation --- src/StreamingBodyParser/MultipartParser.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/StreamingBodyParser/MultipartParser.php b/src/StreamingBodyParser/MultipartParser.php index 5aef98ff..112a98d6 100644 --- a/src/StreamingBodyParser/MultipartParser.php +++ b/src/StreamingBodyParser/MultipartParser.php @@ -54,7 +54,7 @@ final class MultipartParser extends EventEmitter public function __construct(RequestInterface $request) { - $this->onDataCallable = [$this, 'onData']; + $this->onDataCallable = array($this, 'onData'); $this->promise = (new Deferred(function () { $this->body->removeListener('data', $this->onDataCallable); $this->body->close(); @@ -63,7 +63,7 @@ public function __construct(RequestInterface $request) $this->body = $this->request->getBody(); $dataMethod = $this->determineOnDataMethod(); - $this->setOnDataListener([$this, $dataMethod]); + $this->setOnDataListener(array($this, $dataMethod)); } protected function determineOnDataMethod() @@ -97,7 +97,7 @@ public function findBoundary($data) $boundary = substr($this->buffer, 2, strpos($this->buffer, "\r\n")); $boundary = substr($boundary, 0, -2); $this->setBoundary($boundary); - $this->setOnDataListener([$this, 'onData']); + $this->setOnDataListener(array($this, 'onData')); } } @@ -173,7 +173,7 @@ protected function parseFile($headers, $body, $streaming = true) } $stream = new ThroughStream(); - $this->emit('file', [ + $this->emit('file', array( $this->getFieldFromHeader($headers['content-disposition'], 'name'), new UploadedFile( new HttpBodyStream($stream, null), @@ -183,7 +183,7 @@ protected function parseFile($headers, $body, $streaming = true) $headers['content-type'][0] ), $headers, - ]); + )); if (!$streaming) { $stream->end($body); @@ -205,7 +205,7 @@ protected function chunkStreamFunc(ThroughStream $stream) $chunk = $this->stripTrainingEOL($chunk); $stream->end($chunk); - $this->setOnDataListener([$this, 'onData']); + $this->setOnDataListener(array($this, 'onData')); if (count($chunks) == 1) { array_unshift($chunks, ''); @@ -228,18 +228,18 @@ protected function parsePost($headers, $body) foreach ($headers['content-disposition'] as $part) { if (strpos($part, 'name') === 0) { preg_match('/name="?(.*)"$/', $part, $matches); - $this->emit('post', [ + $this->emit('post', array( $matches[1], $body, $headers, - ]); + )); } } } protected function parseHeaders($header) { - $headers = []; + $headers = array(); foreach (explode("\r\n", trim($header)) as $line) { list($key, $values) = explode(':', $line, 2); From 82a7572151ae60752dee651c27942af16ef1a36d Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 14 Jun 2017 12:55:18 +0200 Subject: [PATCH 06/11] PHP 5.3 doesn't support (new Class())->method() --- src/StreamingBodyParser/MultipartParser.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/StreamingBodyParser/MultipartParser.php b/src/StreamingBodyParser/MultipartParser.php index 112a98d6..fdc4f93c 100644 --- a/src/StreamingBodyParser/MultipartParser.php +++ b/src/StreamingBodyParser/MultipartParser.php @@ -55,10 +55,11 @@ final class MultipartParser extends EventEmitter public function __construct(RequestInterface $request) { $this->onDataCallable = array($this, 'onData'); - $this->promise = (new Deferred(function () { + $deferred = new Deferred(function () { $this->body->removeListener('data', $this->onDataCallable); $this->body->close(); - }))->promise(); + }); + $this->promise = $deferred->promise(); $this->request = $request; $this->body = $this->request->getBody(); From 4040c759f1f2a1d5835278ac0398fafd2d4876bc Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 14 Jun 2017 17:30:44 +0200 Subject: [PATCH 07/11] PHP 5.3 doesn't have the callable typehint working as expected --- src/StreamingBodyParser/MultipartParser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StreamingBodyParser/MultipartParser.php b/src/StreamingBodyParser/MultipartParser.php index fdc4f93c..a3024c25 100644 --- a/src/StreamingBodyParser/MultipartParser.php +++ b/src/StreamingBodyParser/MultipartParser.php @@ -288,7 +288,7 @@ protected function getFieldFromHeader(array $header, $field) return ''; } - protected function setOnDataListener(callable $callable) + protected function setOnDataListener($callable) { $this->body->removeListener('data', $this->onDataCallable); $this->onDataCallable = $callable; From 24ae38febfa7fec94d1ce85b28583c44b7441c06 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 14 Jun 2017 17:32:13 +0200 Subject: [PATCH 08/11] No done in older promise versions --- tests/StreamingBodyParser/MultipartParserTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/StreamingBodyParser/MultipartParserTest.php b/tests/StreamingBodyParser/MultipartParserTest.php index 8f59b4a1..470688d1 100644 --- a/tests/StreamingBodyParser/MultipartParserTest.php +++ b/tests/StreamingBodyParser/MultipartParserTest.php @@ -75,7 +75,7 @@ public function testFileUpload() $post[] = array($key => $value); }); $multipart->on('file', function ($name, UploadedFileInterface $file, $headers) use (&$files) { - Stream\buffer($file->getStream())->done(function ($buffer) use ($name, $file, $headers, &$files) { + Stream\buffer($file->getStream())->then(function ($buffer) use ($name, $file, $headers, &$files) { $body = new BufferStream(strlen($buffer)); $body->write($buffer); $files[] = array( @@ -89,6 +89,8 @@ public function testFileUpload() ), $headers, ); + }, function ($t) { + throw $t; }); }); From 64a80d1fb90dc89d48f5062bf6f7e10880e61a37 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 14 Jun 2017 18:21:51 +0200 Subject: [PATCH 09/11] PHP 5.3 compatibility fixes --- src/StreamingBodyParser/MultipartParser.php | 26 ++++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/StreamingBodyParser/MultipartParser.php b/src/StreamingBodyParser/MultipartParser.php index a3024c25..629bede6 100644 --- a/src/StreamingBodyParser/MultipartParser.php +++ b/src/StreamingBodyParser/MultipartParser.php @@ -197,26 +197,28 @@ protected function parseFile($headers, $body, $streaming = true) protected function chunkStreamFunc(ThroughStream $stream) { + $that = $this; + $boundary = $this->boundary; $buffer = ''; - $func = function($data) use (&$buffer, $stream) { + $func = function($data) use (&$buffer, $stream, $that, $boundary) { $buffer .= $data; - if (strpos($buffer, $this->boundary) !== false) { - $chunks = preg_split('/-+' . $this->boundary . '/', $buffer); + if (strpos($buffer, $boundary) !== false) { + $chunks = preg_split('/-+' . $boundary . '/', $buffer); $chunk = array_shift($chunks); - $chunk = $this->stripTrainingEOL($chunk); + $chunk = $that->stripTrainingEOL($chunk); $stream->end($chunk); - $this->setOnDataListener(array($this, 'onData')); + $that->setOnDataListener(array($that, 'onData')); if (count($chunks) == 1) { array_unshift($chunks, ''); } - $this->onData(implode('-' . $this->boundary, $chunks)); + $that->onData(implode('-' . $boundary, $chunks)); return; } - if (strlen($buffer) >= strlen($this->boundary) * 3) { + if (strlen($buffer) >= strlen($boundary) * 3) { $stream->write($buffer); $buffer = ''; } @@ -288,7 +290,10 @@ protected function getFieldFromHeader(array $header, $field) return ''; } - protected function setOnDataListener($callable) + /** + * @internal + */ + public function setOnDataListener($callable) { $this->body->removeListener('data', $this->onDataCallable); $this->onDataCallable = $callable; @@ -300,7 +305,10 @@ public function cancel() $this->promise->cancel(); } - private function stripTrainingEOL($chunk) + /** + * @internal + */ + public function stripTrainingEOL($chunk) { if (substr($chunk, -2) === "\r\n") { return substr($chunk, 0, -2); From bbe712c20c681b84ffd6498a45639719b71c444c Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 17 Jun 2017 00:44:27 +0200 Subject: [PATCH 10/11] Fixed typo as pointed out by @jsor at https://github.com/reactphp/http/pull/200#discussion_r122370415 --- src/StreamingBodyParser/MultipartParser.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StreamingBodyParser/MultipartParser.php b/src/StreamingBodyParser/MultipartParser.php index 629bede6..0e9eaabc 100644 --- a/src/StreamingBodyParser/MultipartParser.php +++ b/src/StreamingBodyParser/MultipartParser.php @@ -123,7 +123,7 @@ protected function parseBuffer() $chunks = preg_split('/-+' . $this->boundary . '/', $this->buffer); $this->buffer = array_pop($chunks); foreach ($chunks as $chunk) { - $chunk = $this->stripTrainingEOL($chunk); + $chunk = $this->stripTrailingEOL($chunk); $this->parseChunk($chunk); } @@ -205,7 +205,7 @@ protected function chunkStreamFunc(ThroughStream $stream) if (strpos($buffer, $boundary) !== false) { $chunks = preg_split('/-+' . $boundary . '/', $buffer); $chunk = array_shift($chunks); - $chunk = $that->stripTrainingEOL($chunk); + $chunk = $that->stripTrailingEOL($chunk); $stream->end($chunk); $that->setOnDataListener(array($that, 'onData')); @@ -308,7 +308,7 @@ public function cancel() /** * @internal */ - public function stripTrainingEOL($chunk) + public function stripTrailingEOL($chunk) { if (substr($chunk, -2) === "\r\n") { return substr($chunk, 0, -2); From d0d2ae32c4dd04e28252a3edf03ca0df3c714b8e Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 17 Jun 2017 00:47:41 +0200 Subject: [PATCH 11/11] Removed promise usage for cancel, as pointed out by @jsor at https://github.com/reactphp/http/pull/200#discussion_r122370242 --- src/StreamingBodyParser/MultipartParser.php | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/StreamingBodyParser/MultipartParser.php b/src/StreamingBodyParser/MultipartParser.php index 0e9eaabc..44aa53cd 100644 --- a/src/StreamingBodyParser/MultipartParser.php +++ b/src/StreamingBodyParser/MultipartParser.php @@ -6,8 +6,6 @@ use Psr\Http\Message\RequestInterface; use React\Http\HttpBodyStream; use React\Http\UploadedFile; -use React\Promise\CancellablePromiseInterface; -use React\Promise\Deferred; use React\Stream\ThroughStream; final class MultipartParser extends EventEmitter @@ -42,11 +40,6 @@ final class MultipartParser extends EventEmitter */ protected $body; - /** - * @var CancellablePromiseInterface - */ - protected $promise; - /** * @var callable */ @@ -55,14 +48,8 @@ final class MultipartParser extends EventEmitter public function __construct(RequestInterface $request) { $this->onDataCallable = array($this, 'onData'); - $deferred = new Deferred(function () { - $this->body->removeListener('data', $this->onDataCallable); - $this->body->close(); - }); - $this->promise = $deferred->promise(); $this->request = $request; $this->body = $this->request->getBody(); - $dataMethod = $this->determineOnDataMethod(); $this->setOnDataListener(array($this, $dataMethod)); } @@ -302,7 +289,8 @@ public function setOnDataListener($callable) public function cancel() { - $this->promise->cancel(); + $this->body->removeListener('data', $this->onDataCallable); + $this->body->close(); } /**