Skip to content

Commit

Permalink
Merge pull request #58 from vdechenaux/multipart-form-data
Browse files Browse the repository at this point in the history
 Add multipart/form-data support
  • Loading branch information
mnapoli authored Sep 11, 2018
2 parents 2f268d2 + 40d10d0 commit fb9970b
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 11 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"psr/http-server-handler": "^1.0",
"zendframework/zend-diactoros": "^1.6",
"jolicode/jolinotif": "^2.0",
"matomo/ini": "^2.0"
"matomo/ini": "^2.0",
"riverline/multipart-parser": "^1.2"
},
"require-dev": {
"phpunit/phpunit": "^6.5",
Expand Down
80 changes: 74 additions & 6 deletions src/Bridge/Psr7/RequestFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Riverline\MultiPartParser\Part;
use Zend\Diactoros\ServerRequest;
use Zend\Diactoros\Stream;
use Zend\Diactoros\UploadedFile;

/**
* Creates PSR-7 requests.
Expand All @@ -23,13 +25,17 @@ public static function fromLambdaEvent(array $event) : ServerRequestInterface
$method = $event['httpMethod'] ?? 'GET';
$query = [];
$bodyString = $event['body'] ?? '';
$body = self::createBodyStream($bodyString);
$parsedBody = null;
$files = [];
$uri = $event['requestContext']['path'] ?? '/';
$headers = $event['headers'] ?? [];
$protocolVersion = $event['requestContext']['protocol'] ?? '1.1';

if ($event['isBase64Encoded'] ?? false) {
$bodyString = base64_decode($bodyString);
}
$body = self::createBodyStream($bodyString);

/*
* queryStringParameters does not handle correctly arrays in parameters
* ?array[key]=value gives ['array[key]' => 'value'] while we want ['array' => ['key' = > 'value']]
Expand All @@ -51,11 +57,29 @@ public static function fromLambdaEvent(array $event) : ServerRequestInterface
}

$contentType = $headers['content-type'] ?? $headers['Content-Type'] ?? null;
/*
* TODO Multipart form uploads are not supported yet.
*/
if ($method === 'POST' && $contentType === 'application/x-www-form-urlencoded') {
parse_str($bodyString, $parsedBody);
if ($method === 'POST' && $contentType !== null) {
/** @var string $contentType */

if ($contentType === 'application/x-www-form-urlencoded') {
parse_str($bodyString, $parsedBody);
} else {
$document = new Part("Content-type: $contentType\r\n\r\n".$bodyString);
if ($document->isMultiPart()) {
$parsedBody = [];

foreach ($document->getParts() as $part) {
if ($part->isFile()) {
$tmpPath = tempnam(sys_get_temp_dir(), 'bref_upload_');
file_put_contents($tmpPath, $part->getBody());
$file = new UploadedFile($tmpPath, filesize($tmpPath), UPLOAD_ERR_OK, $part->getFileName(), $part->getMimeType());

self::parseKeyAndInsertValueInArray($files, $part->getName(), $file);
} else {
self::parseKeyAndInsertValueInArray($parsedBody, $part->getName(), $part->getBody());
}
}
}
}
}

$server = [
Expand Down Expand Up @@ -89,4 +113,48 @@ private static function createBodyStream(string $body) : StreamInterface

return new Stream($stream);
}

/**
* Parse a string key like "files[id_cards][jpg][]" and do $array['files']['id_cards']['jpg'][] = $value
* @param mixed $value
*/
private static function parseKeyAndInsertValueInArray(array &$array, string $key, $value) : void
{
if (strpos($key, '[') === false) {
$array[$key] = $value;

return;
}

$parts = explode('[', $key); // files[id_cards][jpg][] => [ 'files', 'id_cards]', 'jpg]', ']' ]
$pointer = &$array;

foreach ($parts as $k => $part) {
if ($k === 0) {
$pointer = &$pointer[$part];

continue;
}

// Skip two special cases:
// [[ in the key produces empty string
// [test : starts with [ but does not end with ]
if ($part === '' || substr($part, -1) !== ']') {
// Malformed key, we use it "as is"
$array[$key] = $value;

return;
}

$part = substr($part, 0, -1); // The last char is a ] => remove it to have the real key

if ($part === '') { // [] case
$pointer = &$pointer[];
} else {
$pointer = &$pointer[$part];
}
}

$pointer = $value;
}
}
131 changes: 127 additions & 4 deletions tests/Bridge/Psr7/RequestFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use Bref\Bridge\Psr7\RequestFactory;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\UploadedFileInterface;

class RequestFactoryTest extends TestCase
{
Expand Down Expand Up @@ -97,17 +98,27 @@ public function test POST JSON body is not parsed()
self::assertEquals(['foo' => 'bar'], json_decode($request->getBody()->getContents(), true));
}

public function test multipart form data is not supported()
public function test multipart form data is supported()
{
$request = RequestFactory::fromLambdaEvent([
'httpMethod' => 'POST',
'headers' => [
'Content-Type' => 'multipart/form-data',
'Content-Type' => 'multipart/form-data; boundary=testBoundary',
],
'body' => 'abcd',
'body' =>
"--testBoundary\r
Content-Disposition: form-data; name=\"foo\"\r
\r
bar\r
--testBoundary\r
Content-Disposition: form-data; name=\"bim\"\r
\r
baz\r
--testBoundary--\r
",
]);
self::assertEquals('POST', $request->getMethod());
self::assertNull(null, $request->getParsedBody());
self::assertEquals(['foo' => 'bar', 'bim' => 'baz'], $request->getParsedBody());
}

public function test cookies are supported()
Expand Down Expand Up @@ -144,4 +155,116 @@ public function test arrays in query string are supported()
]
], $request->getQueryParams());
}

public function test arrays in name are supported with multipart form data()
{
$request = RequestFactory::fromLambdaEvent([
'httpMethod' => 'POST',
'headers' => [
'Content-Type' => 'multipart/form-data; boundary=testBoundary',
],
'body' =>
"--testBoundary\r
Content-Disposition: form-data; name=\"delete[categories][]\"\r
\r
123\r
--testBoundary\r
Content-Disposition: form-data; name=\"delete[categories][]\"\r
\r
456\r
--testBoundary--\r
",
]);
self::assertEquals('POST', $request->getMethod());
self::assertEquals(
[
'delete' => [
'categories' => [
'123',
'456',
],
],
],
$request->getParsedBody()
);
}

public function test files are supported with multipart form data()
{
$request = RequestFactory::fromLambdaEvent([
'httpMethod' => 'POST',
'headers' => [
'Content-Type' => 'multipart/form-data; boundary=testBoundary',
],
'body' =>
"--testBoundary\r
Content-Disposition: form-data; name=\"foo\"; filename=\"lorem.txt\"\r
Content-Type: text/plain\r
\r
Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
\r
--testBoundary\r
Content-Disposition: form-data; name=\"bar\"; filename=\"cars.csv\"\r
\r
Year,Make,Model
1997,Ford,E350
2000,Mercury,Cougar
\r
--testBoundary--\r
",
]);
self::assertEquals('POST', $request->getMethod());
self::assertEquals([], $request->getParsedBody());
self::assertEquals(
[
'foo',
'bar',
],
array_keys($request->getUploadedFiles())
);

/** @var UploadedFileInterface $foo */
$foo = $request->getUploadedFiles()['foo'];
self::assertInstanceOf(UploadedFileInterface::class, $foo);
self::assertEquals('lorem.txt', $foo->getClientFilename());
self::assertEquals('text/plain', $foo->getClientMediaType());
self::assertEquals(UPLOAD_ERR_OK, $foo->getError());
self::assertEquals(57, $foo->getSize());
self::assertEquals(<<<RAW
Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
RAW
, $foo->getStream()->getContents());

/** @var UploadedFileInterface $bar */
$bar = $request->getUploadedFiles()['bar'];
self::assertInstanceOf(UploadedFileInterface::class, $bar);
self::assertEquals('cars.csv', $bar->getClientFilename());
self::assertEquals('application/octet-stream', $bar->getClientMediaType()); // not set: fallback to application/octet-stream
self::assertEquals(UPLOAD_ERR_OK, $bar->getError());
self::assertEquals(51, $bar->getSize());
self::assertEquals(<<<RAW
Year,Make,Model
1997,Ford,E350
2000,Mercury,Cougar
RAW
, $bar->getStream()->getContents());
}

public function test POST base64 encoded body is supported()
{
$request = RequestFactory::fromLambdaEvent([
'httpMethod' => 'POST',
'isBase64Encoded' => true,
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
],
'body' => base64_encode('foo=bar'),
]);
self::assertEquals('POST', $request->getMethod());
self::assertEquals(['foo' => 'bar'], $request->getParsedBody());
}
}

0 comments on commit fb9970b

Please sign in to comment.