From 9cdef117e401fa753b05d44e42cae30b467f3ccf Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 30 Jul 2018 16:07:55 +0200 Subject: [PATCH 01/10] Create HttpBatchExtension.php so the services.yml file would be loaded automatically --- DependencyInjection/HttpBatchExtension.php | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 DependencyInjection/HttpBatchExtension.php diff --git a/DependencyInjection/HttpBatchExtension.php b/DependencyInjection/HttpBatchExtension.php new file mode 100644 index 0000000..4f53f90 --- /dev/null +++ b/DependencyInjection/HttpBatchExtension.php @@ -0,0 +1,24 @@ +load( 'services.yml' ); + + } // load + +} // HttpBatchExtension From 775566ac29856731d0ec3c800cc04462ec534230 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 31 Jul 2018 07:57:36 +0200 Subject: [PATCH 02/10] Update README.md Removed unnecessary installation notice --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index 4f980cd..90b3ff6 100644 --- a/README.md +++ b/README.md @@ -28,14 +28,6 @@ http_batch: type: annotation ``` -#### Sevice Registration -Register HttpBatchBundle services. Add this line to your services.yml -```yml -imports: - ... - - { resource: "@HttpBatchBundle/Resources/config/services.yml" } -``` - That's all. Now you can use http batch implementation on your symfony project. Your batch request url is http://your-domain/batch. From 1b81be703664fc0d159e114bf18db251f6ad8d13 Mon Sep 17 00:00:00 2001 From: Christopher Wittor Date: Tue, 7 Aug 2018 11:17:27 +0200 Subject: [PATCH 03/10] Http content parser added --- HTTP/ContentParser.php | 285 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 HTTP/ContentParser.php diff --git a/HTTP/ContentParser.php b/HTTP/ContentParser.php new file mode 100644 index 0000000..481d4c8 --- /dev/null +++ b/HTTP/ContentParser.php @@ -0,0 +1,285 @@ +input = $content; + + if ( strpos( $boundary, 'boundary=' ) !== false ) { + $boundary = $this->boundary( $boundary ); + } + + $blocks = $this->split( $boundary ); + + $data = $this->blocks( $blocks ); + + return $data; + + } // __construct + + /** + * @param string $boundary + * @param string $content + * + * @return array + * @throws \HttpHeaderException + */ + public static function parse( $boundary, $content ) { + + $params = []; + + new self( $boundary, $content, $params ); + + return $params; + } + + /** + * @param string $contentType + * + * @return array + * @throws \HttpHeaderException + */ + private function boundary( $contentType ) { + + if ( ! $contentType ) { + throw new \HttpHeaderException( "Content-type can not be found in header" ); + } // if + $contentTypeData = explode( ";", $contentType ); + + foreach ( $contentTypeData as $data ) { + $contentTypePart = explode( "=", $data ); + if ( sizeof( $contentTypePart ) == 2 && trim( $contentTypePart[ 0 ] ) == "boundary" ) { + $boundary = trim( $contentTypePart[ 1 ] ); + break; + } // if + } // foreach + + if ( isset( $boundary ) ) { + return $boundary; + } else { + throw new \HttpHeaderException( "Boundary can not be found." ); + } // if + + } // boundary + + /** + * @param $boundary string + * + * @return array + */ + private function split( $boundary ) { + + $result = preg_split( "/-+$boundary/", $this->input ); + array_pop( $result ); + + return $result; + + } // split + + /** + * @param $array array + * + * @return array + */ + private function blocks( $array ) { + + $results = [ + 'post' => [], + // 'file' => [], + ]; + + foreach ( $array as $key => $value ) { + if ( empty( $value ) ) { + continue; + } // if + + $block = $this->decide( $value ); + + if ( count( $block[ 'post' ] ) > 0 ) { + array_push( $results[ 'post' ], $block[ 'post' ] ); + } // if + +// if ( count( $block[ 'file' ] ) > 0 ) { +// array_push( $results[ 'file' ], $block[ 'file' ] ); +// } // if + } // foreach + + return $this->merge( $results ); + + } // blocks + + /** + * @param $string string + * + * @return array + */ + private function decide( $string ) { + +// if ( strpos( $string, 'application/octet-stream' ) !== false ) { +// return [ +// 'post' => $this->file( $string ), +// 'file' => [], +// ]; +// } // if + +// if ( strpos( $string, 'filename' ) !== false ) { +// return [ +// 'post' => [], +// 'file' => $this->file_stream( $string ), +// ]; +// } // if + + return [ + 'post' => $this->post( $string ), + // 'file' => [], + ]; + + } // decide + + /** + * @param $string + * + * @return array + */ +// private function file( $string ) { +// +// preg_match( '/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s', $string, $match ); +// +// return [ +// $match[ 1 ] => ( ! empty( $match[ 2 ] ) ? $match[ 2 ] : '' ), +// ]; +// +// } // file + + /** + * @param $string + * + * @return array + */ +// private function file_stream( $string ) { +// +// $data = []; +// +// preg_match( '/name=\"([^\"]*)\"; filename=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match ); +// preg_match( '/Content-Type: (.*)?/', $match[ 3 ], $mime ); +// +// $image = preg_replace( '/Content-Type: (.*)[^\n\r]/', '', $match[ 3 ] ); +// +// $path = sys_get_temp_dir() . '/php' . substr( sha1( rand() ), 0, 6 ); +// +// $err = file_put_contents( $path, ltrim( $image ) ); +// +// if ( preg_match( '/^(.*)\[\]$/i', $match[ 1 ], $tmp ) ) { +// $index = $tmp[ 1 ]; +// } else { +// $index = $match[ 1 ]; +// } // if +// +// $data[ $index ][ 'name' ][] = $match[ 2 ]; +// $data[ $index ][ 'type' ][] = $mime[ 1 ]; +// $data[ $index ][ 'tmp_name' ][] = $path; +// $data[ $index ][ 'error' ][] = ( $err === false ) ? $err : 0; +// $data[ $index ][ 'size' ][] = filesize( $path ); +// +// return $data; +// +// } // file_stream + + /** + * @param $string + * + * @return array + */ + private function post( $string ) { + + $data = []; + + preg_match( '/name=\"([^\"]*)\"[\n|\r]+([^\n\r]*)?$/s', $string, $match ); + + if ( preg_match( '/^(.*)\[\]$/i', $match[ 1 ], $tmp ) ) { + $data[ $tmp[ 1 ] ][] = ( ! empty( $match[ 2 ] ) ? $match[ 2 ] : '' ); + } else { + $data[ $match[ 1 ] ] = ( ! empty( $match[ 2 ] ) ? $match[ 2 ] : '' ); + } // if + + return $data; + + } // post + + /** + * @param $array array + * + * Ugly ugly ugly + * + * @return array + */ + private function merge( $array ) { + + $results = [ + 'post' => [], + // 'file' => [], + ]; + + if ( count( $array[ 'post' ] ) > 0 ) { + foreach ( $array[ 'post' ] as $key => $value ) { + foreach ( $value as $k => $v ) { + if ( is_array( $v ) ) { + foreach ( $v as $kk => $vv ) { + $results[ 'post' ][ $k ][] = $vv; + } // foreach + } else { + $results[ 'post' ][ $k ] = $v; + } // if + } // foeach + } // foeach + } // if + +// if ( count( $array[ 'file' ] ) > 0 ) { +// foreach ( $array[ 'file' ] as $key => $value ) { +// foreach ( $value as $k => $v ) { +// if ( is_array( $v ) ) { +// foreach ( $v as $kk => $vv ) { +// if ( is_array( $vv ) && ( count( $vv ) === 1 ) ) { +// $results[ 'file' ][ $k ][ $kk ] = $vv[ 0 ]; +// } else { +// $results[ 'file' ][ $k ][ $kk ][] = $vv[ 0 ]; +// } // if +// } // foreach +// } else { +// $results[ 'file' ][ $k ][ $key ] = $v; +// } // if +// } // foreach +// } // foreach +// } // if + + return $results; + + } // merge + +} // ContentParser \ No newline at end of file From 136f8a0751794816b4365debb619bcd4f427efda Mon Sep 17 00:00:00 2001 From: Christopher Wittor Date: Tue, 7 Aug 2018 11:17:54 +0200 Subject: [PATCH 04/10] new message class "Transaction" added --- Message/Transaction.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 Message/Transaction.php diff --git a/Message/Transaction.php b/Message/Transaction.php new file mode 100644 index 0000000..c11bae1 --- /dev/null +++ b/Message/Transaction.php @@ -0,0 +1,26 @@ + Date: Tue, 7 Aug 2018 11:19:13 +0200 Subject: [PATCH 05/10] Handling of subrequest improved: - return correct content-id - some PHP_EOL added - added handling of multipart/form-data request --- Handler.php | 453 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 267 insertions(+), 186 deletions(-) diff --git a/Handler.php b/Handler.php index 4b8061e..326a314 100644 --- a/Handler.php +++ b/Handler.php @@ -1,196 +1,277 @@ kernel = $kernel; - } - - /** - * @param Request $request - * @return Response - */ - public function handle(Request $request) - { - $this->batchRequest = $request; - return $this->parseRequest($request); - } - - /** - * @param Request $request - * @return Response - */ - private function parseRequest(Request $request) - { - $this->getBatchHeader($request); - $subRequests = $this->getSubRequests($request); - return $this->getBatchRequestResponse($subRequests); - } - - /** - * @param Request $request - * @return mixed - */ - private function getBatchHeader(Request $request) - { - $headers = $request->headers->all(); - $contentType = $request->headers->get("content-type"); - $this->boundary = $this->parseBoundary($contentType); - return $headers; - } - - /** - * @param string $contentType - * @return string - * @throws \HttpHeaderException - */ - private function parseBoundary($contentType) - { - if (!$contentType) { - throw new \HttpHeaderException("Content-type can not be found in header"); - } - $contentTypeData = explode(";", $contentType); - - foreach ($contentTypeData as $data) { - $contentTypePart = explode("=", $data); - if (sizeof($contentTypePart) == 2 && trim($contentTypePart[0]) == "boundary") { - $boundary = trim($contentTypePart[1]); - break; - } - } - if (isset($boundary)) { - return $boundary; - } else { - throw new BadRequestHttpException("Boundary can not be found."); - } - } - - /** - * @param Request $request - * @return array - */ - private function getSubRequests(Request $request) - { - $subRequests = []; - $content = explode("--" . $this->boundary . "--", $request->getContent())[0]; - $subRequestsAsString = explode("--" . $this->boundary, $content); - array_map(function ($data) { - trim($data); - }, $subRequestsAsString); - $subRequestsAsString = array_filter($subRequestsAsString, function ($data) { - return strlen($data) > 0; - }); - $subRequestsAsString = array_values($subRequestsAsString); - foreach ($subRequestsAsString as $item) { - $item = explode(PHP_EOL . PHP_EOL, $item, 2); - $requestString = $item[1]; - - $subRequests[] = $this->convertGuzzleRequestToSymfonyRequest(\GuzzleHttp\Psr7\parse_request($requestString)); - - } - - return $subRequests; - } - - /** - * @param \GuzzleHttp\Psr7\Request $guzzleRequest - * @return Request - */ - private function convertGuzzleRequestToSymfonyRequest(\GuzzleHttp\Psr7\Request $guzzleRequest) - { - $url = $guzzleRequest->getUri()->getPath(); - $method = $guzzleRequest->getMethod(); - parse_str($guzzleRequest->getUri()->getQuery(), $params); - $cookies = []; - $files = []; - $server = []; - $content = $guzzleRequest->getBody(); - - $symfonyRequest = Request::create($url, $method, $params, $cookies, $files, $server, $content); - foreach ($guzzleRequest->getHeaders() as $key => $value) { - $symfonyRequest->headers->set($key, $value); - } - return $symfonyRequest; - } - - /** - * @param array $subRequests - * @return Response - */ - private function getBatchRequestResponse(array $subRequests) - { - $subResponses = []; - foreach ($subRequests as $subRequest) { - $subResponses[] = $this->kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST); - } - return $this->generateBatchResponseFromSubResponses($subResponses); - } - - /** - * @param Response[] $subResponses - * @return Response - */ - private function generateBatchResponseFromSubResponses($subResponses) - { - $response = new Response(); - $version = $response->getProtocolVersion(); - $contentType = 'multipart/batch;type="application/http;type=' . $version . '";boundary=' . $this->boundary; - $response->headers->set("Content-Type", $contentType); - - $contentForSubResponses = []; - foreach ($subResponses as $subResponse) { - $contentForSubResponses[] = $this->generateSubResponseFromContent($subResponse); - } - $content = "--" . $this->boundary . PHP_EOL; - $content .= implode(PHP_EOL . "--" . $this->boundary . PHP_EOL, $contentForSubResponses); - $content .= "--" . $this->boundary . "--" . PHP_EOL; - $response->setContent($content); - return $response; - } - - /** - * @param \Symfony\Component\HttpFoundation\Response $response - * @return string - */ - private function generateSubResponseFromContent(\Symfony\Component\HttpFoundation\Response $response) - { - $content = ''; - $content .= 'Content-Type: application/http;version=' . $response->getProtocolVersion() . PHP_EOL; - $content .= 'Content-Transfer-Encoding: binary'; - $content .= 'In-Reply-To: <' . $this->boundary . '@' . $this->batchRequest->getHost() . '>' . PHP_EOL; - $content .= PHP_EOL; - $content .= "HTTP/" . $response->getProtocolVersion() . " " . $response->getStatusCode() . " " . Response::$statusTexts[$response->getStatusCode()] . PHP_EOL; - foreach ($response->headers->allPreserveCase() as $key => $value) { - $content .= sprintf("%s:%s" . PHP_EOL, $key, implode(",", $value)); - } - $content .= PHP_EOL; - $content .= trim($response->getContent()); - return $content; - } +class Handler { + + + /** + * @var string + */ + private $boundary; + + /** + * @var Kernel $kernel + */ + private $kernel; + + /** + * @var Request $batchRequest + */ + private $batchRequest; + + /** + * @param HttpKernelInterface $kernel + */ + public function __construct( HttpKernelInterface $kernel ) { + + $this->kernel = $kernel; + + } + + /** + * @param Request $request + * + * @return Response + * @throws \HttpHeaderException + */ + public function handle( Request $request ) { + + $this->batchRequest = $request; + + return $this->parseRequest( $request ); + + } + + /** + * @param Request $request + * + * @return Response + * @throws \HttpHeaderException + */ + private function parseRequest( Request $request ) { + + $this->getBatchHeader( $request ); + $transactions = $this->getTransactions( $request ); + + return $this->getBatchRequestResponse( $transactions ); + + } + + /** + * @param Request $request + * + * @return mixed + * @throws \HttpHeaderException + */ + private function getBatchHeader( Request $request ) { + + $headers = $request->headers->all(); + $contentType = $request->headers->get( "content-type" ); + $this->boundary = $this->parseBoundary( $contentType ); + + return $headers; + + } + + /** + * @param string $contentType + * + * @return string + * @throws \HttpHeaderException + */ + private function parseBoundary( $contentType ) { + + if ( ! $contentType ) { + throw new \HttpHeaderException( "Content-type can not be found in header" ); + } + $contentTypeData = explode( ";", $contentType ); + + foreach ( $contentTypeData as $data ) { + $contentTypePart = explode( "=", $data ); + if ( sizeof( $contentTypePart ) == 2 && trim( $contentTypePart[ 0 ] ) == "boundary" ) { + $boundary = trim( $contentTypePart[ 1 ] ); + break; + } + } + if ( isset( $boundary ) ) { + return $boundary; + } else { + throw new BadRequestHttpException( "Boundary can not be found." ); + } + + } + + private function parseContentId( $request_header ) { + + if ( ! $request_header ) { + throw new \HttpHeaderException( "Subrequest header can not be found" ); + } + $request_header_data = explode( PHP_EOL, $request_header ); + + foreach ( $request_header_data as $data ) { + $item = explode( ':', $data, 2 ); + array_walk( $item, + function ( &$value ) { + + $value = trim( strtolower( $value ) ); + } ); + + if ( $item[ 0 ] === 'content-id' ) { + return $item[ 1 ]; + } // if + } // foreach + + throw new \HttpHeaderException( "Content-id can not be found in subrequest header" ); + + } // perseContentId + + /** + * @param Request $request + * + * @return array + * @throws \HttpHeaderException + */ + private function getTransactions( Request $request ) { + + $transactions = []; + $content = explode( "--" . $this->boundary . "--", $request->getContent() )[ 0 ]; + $subRequestsAsString = explode( "--" . $this->boundary, $content ); + array_map( function ( $data ) { + + trim( $data ); + }, + $subRequestsAsString ); + $subRequestsAsString = array_filter( $subRequestsAsString, + function ( $data ) { + + return strlen( $data ) > 0; + } ); + $subRequestsAsString = array_values( $subRequestsAsString ); + foreach ( $subRequestsAsString as $item ) { + $item = explode( PHP_EOL . PHP_EOL, $item, 2 ); + $requestHeader = $item[ 0 ]; + $requestString = $item[ 1 ]; + + $transaction = $this->convertGuzzleRequestToTransactions( + \GuzzleHttp\Psr7\parse_request( $requestString ) + ); + $transaction->content_id = $this->parseContentId( $requestHeader ); + + $transactions[] = $transaction; + } + + return $transactions; + + } + + /** + * @param \GuzzleHttp\Psr7\Request $guzzleRequest + * + * @return Transaction + * @throws \HttpHeaderException + */ + private function convertGuzzleRequestToTransactions( \GuzzleHttp\Psr7\Request $guzzleRequest ) { + + $url = $guzzleRequest->getUri()->getPath(); + $method = $guzzleRequest->getMethod(); + $cookies = []; + $files = []; + $server = $this->batchRequest->server->all(); + $content = null; + $params = ContentParser::parse( $guzzleRequest->getHeader( 'content-type' )[ 0 ], + $guzzleRequest->getBody() ); + + $transaction = new Transaction(); + + $transaction->request = Request::create( $url, + $method, + $params[ 'post' ], + $cookies, + $files, + $server, + $content ); + + foreach ( $guzzleRequest->getHeaders() as $key => $value ) { + $transaction->request->headers->set( $key, $value ); + } + + return $transaction; + + } + + /** + * @param Transaction[] $transactions + * + * @return Response + * @throws \Exception + */ + private function getBatchRequestResponse( array $transactions ) { + + foreach ( $transactions as $transaction ) { + $transaction->response = $this->kernel->handle( $transaction->request, HttpKernelInterface::SUB_REQUEST ); + } + + return $this->generateBatchResponseFromSubResponses( $transactions ); + + } + + /** + * @param Transaction[] $transactions + * + * @return Response + */ + private function generateBatchResponseFromSubResponses( $transactions ) { + + $response = new Response(); + $version = $response->getProtocolVersion(); + $contentType = 'multipart/batch;type="application/http;type=' . $version . '";boundary=' . $this->boundary; + $response->headers->set( "Content-Type", $contentType ); + + $contentForSubResponses = []; + foreach ( $transactions as $transaction ) { + $contentForSubResponses[] = $this->generateSubResponseFromContent( $transaction ); + } + $content = "--" . $this->boundary . PHP_EOL; + $content .= implode( PHP_EOL . "--" . $this->boundary . PHP_EOL, $contentForSubResponses ) . PHP_EOL; + $content .= "--" . $this->boundary . "--" . PHP_EOL; + $response->setContent( $content ); + + return $response; + + } + + /** + * @param Transaction $transaction + * + * @return string + */ + private function generateSubResponseFromContent( Transaction $transaction ) { + + $content = ''; + $content .= 'Content-Type: application/http;version=' . $transaction->response->getProtocolVersion() . PHP_EOL; + $content .= 'Content-Transfer-Encoding: binary' . PHP_EOL; + $content .= 'In-Reply-To: ' . $transaction->content_id . PHP_EOL; + $content .= PHP_EOL; + $content .= "HTTP/" . $transaction->response->getProtocolVersion() . " " . + $transaction->response->getStatusCode() . + " " . + Response::$statusTexts[ $transaction->response->getStatusCode() ] . PHP_EOL; + foreach ( $transaction->response->headers->allPreserveCase() as $key => $value ) { + $content .= sprintf( "%s:%s" . PHP_EOL, $key, implode( ",", $value ) ); + } + $content .= PHP_EOL; + $content .= trim( $transaction->response->getContent() ); + + return $content; + + } } From 2c5257086b4945bfe061f2f078968cc2336b04dd Mon Sep 17 00:00:00 2001 From: Christopher Wittor Date: Tue, 7 Aug 2018 11:19:30 +0200 Subject: [PATCH 06/10] extended contributer list --- composer.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3c0aec4..e9082c7 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,11 @@ { "name": "IdeasoftLabs", "email": "labs@ideasoft.com.tr" - } + }, + { + "name": "Christoher Wittor - proWIN Winter GmbH", + "email": "christopher.wittor@prowin.net" + } ], "require": { "php": ">=5.5.9", From 53cc10e97eb6d3858cf4a37919283f9f6476a876 Mon Sep 17 00:00:00 2001 From: Christopher Wittor Date: Fri, 17 Aug 2018 09:31:35 +0200 Subject: [PATCH 07/10] Maximum call limits implemented --- .../HttpBatchConfiguration.php | 36 +++++++++++++++++++ DependencyInjection/HttpBatchExtension.php | 7 ++++ Handler.php | 33 ++++++++++++++++- Resources/config/services.yml | 3 +- 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 DependencyInjection/HttpBatchConfiguration.php diff --git a/DependencyInjection/HttpBatchConfiguration.php b/DependencyInjection/HttpBatchConfiguration.php new file mode 100644 index 0000000..87aefae --- /dev/null +++ b/DependencyInjection/HttpBatchConfiguration.php @@ -0,0 +1,36 @@ +root( 'http_batch' ); + + // @formatter:off + $rootNode + ->children() + ->integerNode( 'max_calls_in_a_request' ) + ->info( 'Limits the amount of calls in a single batch request' ) + ->defaultValue( 100 ) + ->min( 1 ) + ->end() + ->end(); + // @formatter:on + + return $treeBuilder; + + } // getConfigTreeBuilder + + +} // HttpBatchConfiguration \ No newline at end of file diff --git a/DependencyInjection/HttpBatchExtension.php b/DependencyInjection/HttpBatchExtension.php index 4f53f90..0d70444 100644 --- a/DependencyInjection/HttpBatchExtension.php +++ b/DependencyInjection/HttpBatchExtension.php @@ -19,6 +19,13 @@ public function load( array $configs, ContainerBuilder $container ) { $loader->load( 'services.yml' ); + $configuration = new HttpBatchConfiguration(); + + $config = $this->processConfiguration( $configuration, $configs ); + + $container->getDefinition( 'http_batch.handler' ) + ->replaceArgument( '$max_calls', $config[ 'max_calls_in_a_request' ] ); + } // load } // HttpBatchExtension diff --git a/Handler.php b/Handler.php index 326a314..0a7e4b1 100644 --- a/Handler.php +++ b/Handler.php @@ -5,8 +5,10 @@ use Ideasoft\HttpBatchBundle\HTTP\ContentParser; use Ideasoft\HttpBatchBundle\Message\Response; use Ideasoft\HttpBatchBundle\Message\Transaction; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Kernel; @@ -28,13 +30,20 @@ class Handler { */ private $batchRequest; + /** + * @var integer + */ + private $max_calls; + /** * @param HttpKernelInterface $kernel */ - public function __construct( HttpKernelInterface $kernel ) { + public function __construct( HttpKernelInterface $kernel, $max_calls ) { $this->kernel = $kernel; + $this->max_calls = $max_calls; + } /** @@ -61,6 +70,20 @@ private function parseRequest( Request $request ) { $this->getBatchHeader( $request ); $transactions = $this->getTransactions( $request ); + try { + $transactions = $this->getTransactions( $request ); + } + catch ( HttpException $e ) { + return new JsonResponse( array( + 'result' => 'error', + 'errors' => array( + array( + 'message' => $e->getMessage(), + 'type' => 'client_error', + ), + ), + ), $e->getStatusCode() ); + } return $this->getBatchRequestResponse( $transactions ); @@ -155,6 +178,14 @@ function ( $data ) { return strlen( $data ) > 0; } ); + + if ( count( $subRequestsAsString ) > $this->max_calls ) { + throw new HttpException( Response::HTTP_REQUEST_ENTITY_TOO_LARGE, + sprintf( "Maximum call limit exceeded (found %d). Please consider to send only %d calls per request.", + count( $subRequestsAsString ), + $this->max_calls ) ); + } + $subRequestsAsString = array_values( $subRequestsAsString ); foreach ( $subRequestsAsString as $item ) { $item = explode( PHP_EOL . PHP_EOL, $item, 2 ); diff --git a/Resources/config/services.yml b/Resources/config/services.yml index c25284d..07e6688 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -2,7 +2,8 @@ services: http_batch.handler: class: Ideasoft\HttpBatchBundle\Handler arguments: - - '@kernel' + $kernel: '@kernel' + $max_calls: 0 http_batch.request.listener: class: Ideasoft\HttpBatchBundle\EventListener\RequestListener From 43552ab526e425b165db071cc84ccf0d65de63f2 Mon Sep 17 00:00:00 2001 From: Christopher Wittor Date: Fri, 17 Aug 2018 09:32:29 +0200 Subject: [PATCH 08/10] Replaced HttpHeaderException with BadRequestHttpException, so you doesn't need ext-http anymore --- Handler.php | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/Handler.php b/Handler.php index 0a7e4b1..ea2f051 100644 --- a/Handler.php +++ b/Handler.php @@ -3,10 +3,10 @@ namespace Ideasoft\HttpBatchBundle; use Ideasoft\HttpBatchBundle\HTTP\ContentParser; -use Ideasoft\HttpBatchBundle\Message\Response; use Ideasoft\HttpBatchBundle\Message\Transaction; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; @@ -50,7 +50,7 @@ public function __construct( HttpKernelInterface $kernel, $max_calls ) { * @param Request $request * * @return Response - * @throws \HttpHeaderException + * @throws \Exception */ public function handle( Request $request ) { @@ -64,12 +64,11 @@ public function handle( Request $request ) { * @param Request $request * * @return Response - * @throws \HttpHeaderException + * @throws \Exception */ private function parseRequest( Request $request ) { $this->getBatchHeader( $request ); - $transactions = $this->getTransactions( $request ); try { $transactions = $this->getTransactions( $request ); } @@ -84,6 +83,14 @@ private function parseRequest( Request $request ) { ), ), $e->getStatusCode() ); } + catch ( \Exception $e ) { + return new JsonResponse( array( + 'result' => 'error', + 'errors' => array( + array( 'message' => $e->getMessage(), 'type' => 'system_error' ), + ), + ), Response::HTTP_INTERNAL_SERVER_ERROR ); + } return $this->getBatchRequestResponse( $transactions ); @@ -93,7 +100,6 @@ private function parseRequest( Request $request ) { * @param Request $request * * @return mixed - * @throws \HttpHeaderException */ private function getBatchHeader( Request $request ) { @@ -109,12 +115,12 @@ private function getBatchHeader( Request $request ) { * @param string $contentType * * @return string - * @throws \HttpHeaderException + * @throws BadRequestHttpException */ private function parseBoundary( $contentType ) { if ( ! $contentType ) { - throw new \HttpHeaderException( "Content-type can not be found in header" ); + throw new BadRequestHttpException( "Content-type can not be found in header" ); } $contentTypeData = explode( ";", $contentType ); @@ -136,7 +142,7 @@ private function parseBoundary( $contentType ) { private function parseContentId( $request_header ) { if ( ! $request_header ) { - throw new \HttpHeaderException( "Subrequest header can not be found" ); + throw new BadRequestHttpException( "Subrequest header can not be found" ); } $request_header_data = explode( PHP_EOL, $request_header ); @@ -153,7 +159,7 @@ function ( &$value ) { } // if } // foreach - throw new \HttpHeaderException( "Content-id can not be found in subrequest header" ); + throw new BadRequestHttpException( "Content-id can not be found in subrequest header" ); } // perseContentId @@ -161,6 +167,7 @@ function ( &$value ) { * @param Request $request * * @return array + * @throws HttpException * @throws \HttpHeaderException */ private function getTransactions( Request $request ) { From f5e8e1563c666c7481fe8cb8121dc8d9e983b170 Mon Sep 17 00:00:00 2001 From: Christopher Wittor Date: Thu, 20 Sep 2018 13:39:40 +0200 Subject: [PATCH 09/10] Bugfixed linebreak splits with a new regex --- HTTP/ContentParser.php | 2 +- Handler.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HTTP/ContentParser.php b/HTTP/ContentParser.php index 481d4c8..102caaa 100644 --- a/HTTP/ContentParser.php +++ b/HTTP/ContentParser.php @@ -220,7 +220,7 @@ private function post( $string ) { $data = []; - preg_match( '/name=\"([^\"]*)\"[\n|\r]+([^\n\r]*)?$/s', $string, $match ); + preg_match( '/name=\"([^\"]*)\"(?>\r{2}|\n{2}|(?>\r\n){2})(.*)$/s', trim($string), $match ); if ( preg_match( '/^(.*)\[\]$/i', $match[ 1 ], $tmp ) ) { $data[ $tmp[ 1 ] ][] = ( ! empty( $match[ 2 ] ) ? $match[ 2 ] : '' ); diff --git a/Handler.php b/Handler.php index ea2f051..5a815cb 100644 --- a/Handler.php +++ b/Handler.php @@ -195,7 +195,7 @@ function ( $data ) { $subRequestsAsString = array_values( $subRequestsAsString ); foreach ( $subRequestsAsString as $item ) { - $item = explode( PHP_EOL . PHP_EOL, $item, 2 ); + $item = preg_split("/(?>\r{2}|\n{2}|(?>\r\n){2})/", trim($item), 2); $requestHeader = $item[ 0 ]; $requestString = $item[ 1 ]; From 22bb4439fa8a574dd93b600ff44557a8c2685633 Mon Sep 17 00:00:00 2001 From: Christopher Wittor Date: Mon, 24 Sep 2018 15:00:41 +0200 Subject: [PATCH 10/10] Bugfix: ContentParser accep url-encoded now --- HTTP/ContentParser.php | 396 +++++++++++++------------- Handler.php | 616 +++++++++++++++++++++-------------------- 2 files changed, 526 insertions(+), 486 deletions(-) diff --git a/HTTP/ContentParser.php b/HTTP/ContentParser.php index 102caaa..746b305 100644 --- a/HTTP/ContentParser.php +++ b/HTTP/ContentParser.php @@ -7,139 +7,159 @@ /** * Class ContentParser * Handles a input stream with http messages + * * @see https://gist.github.com/jas-/5c3fdc26fedd11cb9fb5#file-class-stream-php * @see https://stackoverflow.com/questions/5483851/manually-parse-raw-multipart-form-data-data-with-php * * @package Ideasoft\HttpBatchBundle\HTTP */ -class ContentParser { - - - /** - * @abstract Raw input stream - */ - protected $input; - - /** - * @param string $boundary - * @param string $content - * @param array $data stream - * - * @throws \HttpHeaderException - */ - private function __construct( $boundary, $content, array &$data ) { - - $this->input = $content; - - if ( strpos( $boundary, 'boundary=' ) !== false ) { - $boundary = $this->boundary( $boundary ); - } - - $blocks = $this->split( $boundary ); - - $data = $this->blocks( $blocks ); - - return $data; - - } // __construct - - /** - * @param string $boundary - * @param string $content - * - * @return array - * @throws \HttpHeaderException - */ - public static function parse( $boundary, $content ) { - - $params = []; - - new self( $boundary, $content, $params ); - - return $params; - } - - /** - * @param string $contentType - * - * @return array - * @throws \HttpHeaderException - */ - private function boundary( $contentType ) { - - if ( ! $contentType ) { - throw new \HttpHeaderException( "Content-type can not be found in header" ); - } // if - $contentTypeData = explode( ";", $contentType ); - - foreach ( $contentTypeData as $data ) { - $contentTypePart = explode( "=", $data ); - if ( sizeof( $contentTypePart ) == 2 && trim( $contentTypePart[ 0 ] ) == "boundary" ) { - $boundary = trim( $contentTypePart[ 1 ] ); - break; - } // if - } // foreach - - if ( isset( $boundary ) ) { - return $boundary; - } else { - throw new \HttpHeaderException( "Boundary can not be found." ); - } // if - - } // boundary - - /** - * @param $boundary string - * - * @return array - */ - private function split( $boundary ) { - - $result = preg_split( "/-+$boundary/", $this->input ); - array_pop( $result ); - - return $result; - - } // split - - /** - * @param $array array - * - * @return array - */ - private function blocks( $array ) { - - $results = [ - 'post' => [], - // 'file' => [], - ]; - - foreach ( $array as $key => $value ) { - if ( empty( $value ) ) { - continue; - } // if - - $block = $this->decide( $value ); - - if ( count( $block[ 'post' ] ) > 0 ) { - array_push( $results[ 'post' ], $block[ 'post' ] ); - } // if +class ContentParser +{ + + + /** + * @abstract Raw input stream + */ + protected $input; + + /** + * @param string $content_type + * @param string $content + * @param array $data stream + * + * @throws \HttpHeaderException + */ + private function __construct( $content_type, $content, array &$data ) + { + + $this->input = $content; + + if ( strpos( $content_type, 'boundary=' ) !== false ) { + $boundary = $this->boundary( $content_type ); + } + + $blocks = $this->split( $boundary ); + + $data = $this->blocks( $blocks ); + + return $data; + + } // __construct + + /** + * @param string $content_type + * @param string $content + * + * @return array + * @throws \HttpHeaderException + */ + public static function parse( $content_type, $content ) + { + + $params = []; + + if ( strpos( $content_type, 'application/x-www-form-urlencoded' ) !== false ) { + parse_str( $content, $params ); + } + else { + new self( $content_type, $content, $params ); + if ( array_key_exists( 'post', $params ) ) { + $params = array_pop($params); + } + else { + $params = []; + } // if else + } // if else + + return $params; + } + + /** + * @param string $contentType + * + * @return array + * @throws \HttpHeaderException + */ + private function boundary( $contentType ) + { + + if ( !$contentType ) { + throw new \HttpHeaderException( "Content-type can not be found in header" ); + } // if + $contentTypeData = explode( ";", $contentType ); + + foreach ( $contentTypeData as $data ) { + $contentTypePart = explode( "=", $data ); + if ( sizeof( $contentTypePart ) == 2 && trim( $contentTypePart[ 0 ] ) == "boundary" ) { + $boundary = trim( $contentTypePart[ 1 ] ); + break; + } // if + } // foreach + + if ( isset( $boundary ) ) { + return $boundary; + } + else { + throw new \HttpHeaderException( "Boundary can not be found." ); + } // if + + } // boundary + + /** + * @param $boundary string + * + * @return array + */ + private function split( $boundary ) + { + + $result = preg_split( "/-+$boundary/", $this->input ); + array_pop( $result ); + + return $result; + + } // split + + /** + * @param $array array + * + * @return array + */ + private function blocks( $array ) + { + + $results = [ + 'post' => [], + // 'file' => [], + ]; + + foreach ( $array as $key => $value ) { + if ( empty( $value ) ) { + continue; + } // if + + $block = $this->decide( $value ); + + if ( count( $block[ 'post' ] ) > 0 ) { + array_push( $results[ 'post' ], $block[ 'post' ] ); + } // if // if ( count( $block[ 'file' ] ) > 0 ) { // array_push( $results[ 'file' ], $block[ 'file' ] ); // } // if - } // foreach + } // foreach - return $this->merge( $results ); + return $this->merge( $results ); - } // blocks + } // blocks - /** - * @param $string string - * - * @return array - */ - private function decide( $string ) { + /** + * @param $string string + * + * @return array + */ + private function decide( $string ) + { // if ( strpos( $string, 'application/octet-stream' ) !== false ) { // return [ @@ -155,18 +175,18 @@ private function decide( $string ) { // ]; // } // if - return [ - 'post' => $this->post( $string ), - // 'file' => [], - ]; + return [ + 'post' => $this->post( $string ), + // 'file' => [], + ]; - } // decide + } // decide - /** - * @param $string - * - * @return array - */ + /** + * @param $string + * + * @return array + */ // private function file( $string ) { // // preg_match( '/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s', $string, $match ); @@ -177,11 +197,11 @@ private function decide( $string ) { // // } // file - /** - * @param $string - * - * @return array - */ + /** + * @param $string + * + * @return array + */ // private function file_stream( $string ) { // // $data = []; @@ -211,54 +231,58 @@ private function decide( $string ) { // // } // file_stream - /** - * @param $string - * - * @return array - */ - private function post( $string ) { - - $data = []; - - preg_match( '/name=\"([^\"]*)\"(?>\r{2}|\n{2}|(?>\r\n){2})(.*)$/s', trim($string), $match ); - - if ( preg_match( '/^(.*)\[\]$/i', $match[ 1 ], $tmp ) ) { - $data[ $tmp[ 1 ] ][] = ( ! empty( $match[ 2 ] ) ? $match[ 2 ] : '' ); - } else { - $data[ $match[ 1 ] ] = ( ! empty( $match[ 2 ] ) ? $match[ 2 ] : '' ); - } // if - - return $data; - - } // post - - /** - * @param $array array - * - * Ugly ugly ugly - * - * @return array - */ - private function merge( $array ) { - - $results = [ - 'post' => [], - // 'file' => [], - ]; - - if ( count( $array[ 'post' ] ) > 0 ) { - foreach ( $array[ 'post' ] as $key => $value ) { - foreach ( $value as $k => $v ) { - if ( is_array( $v ) ) { - foreach ( $v as $kk => $vv ) { - $results[ 'post' ][ $k ][] = $vv; - } // foreach - } else { - $results[ 'post' ][ $k ] = $v; - } // if - } // foeach - } // foeach - } // if + /** + * @param $string + * + * @return array + */ + private function post( $string ) + { + + $data = []; + + preg_match( '/name=\"([^\"]*)\"(?>\r{2}|\n{2}|(?>\r\n){2})(.*)$/s', trim( $string ), $match ); + + if ( preg_match( '/^(.*)\[\]$/i', $match[ 1 ], $tmp ) ) { + $data[ $tmp[ 1 ] ][] = ( !empty( $match[ 2 ] ) ? $match[ 2 ] : '' ); + } + else { + $data[ $match[ 1 ] ] = ( !empty( $match[ 2 ] ) ? $match[ 2 ] : '' ); + } // if + + return $data; + + } // post + + /** + * @param $array array + * + * Ugly ugly ugly + * + * @return array + */ + private function merge( $array ) + { + + $results = [ + 'post' => [], + // 'file' => [], + ]; + + if ( count( $array[ 'post' ] ) > 0 ) { + foreach ( $array[ 'post' ] as $key => $value ) { + foreach ( $value as $k => $v ) { + if ( is_array( $v ) ) { + foreach ( $v as $kk => $vv ) { + $results[ 'post' ][ $k ][] = $vv; + } // foreach + } + else { + $results[ 'post' ][ $k ] = $v; + } // if + } // foeach + } // foeach + } // if // if ( count( $array[ 'file' ] ) > 0 ) { // foreach ( $array[ 'file' ] as $key => $value ) { @@ -278,8 +302,8 @@ private function merge( $array ) { // } // foreach // } // if - return $results; + return $results; - } // merge + } // merge } // ContentParser \ No newline at end of file diff --git a/Handler.php b/Handler.php index 5a815cb..c50fed0 100644 --- a/Handler.php +++ b/Handler.php @@ -12,304 +12,320 @@ use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Kernel; -class Handler { - - - /** - * @var string - */ - private $boundary; - - /** - * @var Kernel $kernel - */ - private $kernel; - - /** - * @var Request $batchRequest - */ - private $batchRequest; - - /** - * @var integer - */ - private $max_calls; - - /** - * @param HttpKernelInterface $kernel - */ - public function __construct( HttpKernelInterface $kernel, $max_calls ) { - - $this->kernel = $kernel; - - $this->max_calls = $max_calls; - - } - - /** - * @param Request $request - * - * @return Response - * @throws \Exception - */ - public function handle( Request $request ) { - - $this->batchRequest = $request; - - return $this->parseRequest( $request ); - - } - - /** - * @param Request $request - * - * @return Response - * @throws \Exception - */ - private function parseRequest( Request $request ) { - - $this->getBatchHeader( $request ); - try { - $transactions = $this->getTransactions( $request ); - } - catch ( HttpException $e ) { - return new JsonResponse( array( - 'result' => 'error', - 'errors' => array( - array( - 'message' => $e->getMessage(), - 'type' => 'client_error', - ), - ), - ), $e->getStatusCode() ); - } - catch ( \Exception $e ) { - return new JsonResponse( array( - 'result' => 'error', - 'errors' => array( - array( 'message' => $e->getMessage(), 'type' => 'system_error' ), - ), - ), Response::HTTP_INTERNAL_SERVER_ERROR ); - } - - return $this->getBatchRequestResponse( $transactions ); - - } - - /** - * @param Request $request - * - * @return mixed - */ - private function getBatchHeader( Request $request ) { - - $headers = $request->headers->all(); - $contentType = $request->headers->get( "content-type" ); - $this->boundary = $this->parseBoundary( $contentType ); - - return $headers; - - } - - /** - * @param string $contentType - * - * @return string - * @throws BadRequestHttpException - */ - private function parseBoundary( $contentType ) { - - if ( ! $contentType ) { - throw new BadRequestHttpException( "Content-type can not be found in header" ); - } - $contentTypeData = explode( ";", $contentType ); - - foreach ( $contentTypeData as $data ) { - $contentTypePart = explode( "=", $data ); - if ( sizeof( $contentTypePart ) == 2 && trim( $contentTypePart[ 0 ] ) == "boundary" ) { - $boundary = trim( $contentTypePart[ 1 ] ); - break; - } - } - if ( isset( $boundary ) ) { - return $boundary; - } else { - throw new BadRequestHttpException( "Boundary can not be found." ); - } - - } - - private function parseContentId( $request_header ) { - - if ( ! $request_header ) { - throw new BadRequestHttpException( "Subrequest header can not be found" ); - } - $request_header_data = explode( PHP_EOL, $request_header ); - - foreach ( $request_header_data as $data ) { - $item = explode( ':', $data, 2 ); - array_walk( $item, - function ( &$value ) { - - $value = trim( strtolower( $value ) ); - } ); - - if ( $item[ 0 ] === 'content-id' ) { - return $item[ 1 ]; - } // if - } // foreach - - throw new BadRequestHttpException( "Content-id can not be found in subrequest header" ); - - } // perseContentId - - /** - * @param Request $request - * - * @return array - * @throws HttpException - * @throws \HttpHeaderException - */ - private function getTransactions( Request $request ) { - - $transactions = []; - $content = explode( "--" . $this->boundary . "--", $request->getContent() )[ 0 ]; - $subRequestsAsString = explode( "--" . $this->boundary, $content ); - array_map( function ( $data ) { - - trim( $data ); - }, - $subRequestsAsString ); - $subRequestsAsString = array_filter( $subRequestsAsString, - function ( $data ) { - - return strlen( $data ) > 0; - } ); - - if ( count( $subRequestsAsString ) > $this->max_calls ) { - throw new HttpException( Response::HTTP_REQUEST_ENTITY_TOO_LARGE, - sprintf( "Maximum call limit exceeded (found %d). Please consider to send only %d calls per request.", - count( $subRequestsAsString ), - $this->max_calls ) ); - } - - $subRequestsAsString = array_values( $subRequestsAsString ); - foreach ( $subRequestsAsString as $item ) { - $item = preg_split("/(?>\r{2}|\n{2}|(?>\r\n){2})/", trim($item), 2); - $requestHeader = $item[ 0 ]; - $requestString = $item[ 1 ]; - - $transaction = $this->convertGuzzleRequestToTransactions( - \GuzzleHttp\Psr7\parse_request( $requestString ) - ); - $transaction->content_id = $this->parseContentId( $requestHeader ); - - $transactions[] = $transaction; - } - - return $transactions; - - } - - /** - * @param \GuzzleHttp\Psr7\Request $guzzleRequest - * - * @return Transaction - * @throws \HttpHeaderException - */ - private function convertGuzzleRequestToTransactions( \GuzzleHttp\Psr7\Request $guzzleRequest ) { - - $url = $guzzleRequest->getUri()->getPath(); - $method = $guzzleRequest->getMethod(); - $cookies = []; - $files = []; - $server = $this->batchRequest->server->all(); - $content = null; - $params = ContentParser::parse( $guzzleRequest->getHeader( 'content-type' )[ 0 ], - $guzzleRequest->getBody() ); - - $transaction = new Transaction(); - - $transaction->request = Request::create( $url, - $method, - $params[ 'post' ], - $cookies, - $files, - $server, - $content ); - - foreach ( $guzzleRequest->getHeaders() as $key => $value ) { - $transaction->request->headers->set( $key, $value ); - } - - return $transaction; - - } - - /** - * @param Transaction[] $transactions - * - * @return Response - * @throws \Exception - */ - private function getBatchRequestResponse( array $transactions ) { - - foreach ( $transactions as $transaction ) { - $transaction->response = $this->kernel->handle( $transaction->request, HttpKernelInterface::SUB_REQUEST ); - } - - return $this->generateBatchResponseFromSubResponses( $transactions ); - - } - - /** - * @param Transaction[] $transactions - * - * @return Response - */ - private function generateBatchResponseFromSubResponses( $transactions ) { - - $response = new Response(); - $version = $response->getProtocolVersion(); - $contentType = 'multipart/batch;type="application/http;type=' . $version . '";boundary=' . $this->boundary; - $response->headers->set( "Content-Type", $contentType ); - - $contentForSubResponses = []; - foreach ( $transactions as $transaction ) { - $contentForSubResponses[] = $this->generateSubResponseFromContent( $transaction ); - } - $content = "--" . $this->boundary . PHP_EOL; - $content .= implode( PHP_EOL . "--" . $this->boundary . PHP_EOL, $contentForSubResponses ) . PHP_EOL; - $content .= "--" . $this->boundary . "--" . PHP_EOL; - $response->setContent( $content ); - - return $response; - - } - - /** - * @param Transaction $transaction - * - * @return string - */ - private function generateSubResponseFromContent( Transaction $transaction ) { - - $content = ''; - $content .= 'Content-Type: application/http;version=' . $transaction->response->getProtocolVersion() . PHP_EOL; - $content .= 'Content-Transfer-Encoding: binary' . PHP_EOL; - $content .= 'In-Reply-To: ' . $transaction->content_id . PHP_EOL; - $content .= PHP_EOL; - $content .= "HTTP/" . $transaction->response->getProtocolVersion() . " " . - $transaction->response->getStatusCode() . - " " . - Response::$statusTexts[ $transaction->response->getStatusCode() ] . PHP_EOL; - foreach ( $transaction->response->headers->allPreserveCase() as $key => $value ) { - $content .= sprintf( "%s:%s" . PHP_EOL, $key, implode( ",", $value ) ); - } - $content .= PHP_EOL; - $content .= trim( $transaction->response->getContent() ); - - return $content; - - } +class Handler +{ + + + /** + * @var string + */ + private $boundary; + + /** + * @var Kernel $kernel + */ + private $kernel; + + /** + * @var Request $batchRequest + */ + private $batchRequest; + + /** + * @var integer + */ + private $max_calls; + + /** + * @param HttpKernelInterface $kernel + */ + public function __construct( HttpKernelInterface $kernel, $max_calls ) + { + + $this->kernel = $kernel; + + $this->max_calls = $max_calls; + + } + + /** + * @param Request $request + * + * @return Response + * @throws \Exception + */ + public function handle( Request $request ) + { + + $this->batchRequest = $request; + + return $this->parseRequest( $request ); + + } + + /** + * @param Request $request + * + * @return Response + * @throws \Exception + */ + private function parseRequest( Request $request ) + { + + $this->getBatchHeader( $request ); + try { + $transactions = $this->getTransactions( $request ); + } + catch ( HttpException $e ) { + return new JsonResponse( [ + 'result' => 'error', + 'errors' => [ + [ + 'message' => $e->getMessage(), + 'type' => 'client_error', + ], + ], + ], $e->getStatusCode() ); + } + catch ( \Exception $e ) { + return new JsonResponse( [ + 'result' => 'error', + 'errors' => [ + [ 'message' => $e->getMessage(), 'type' => 'system_error' ], + ], + ], Response::HTTP_INTERNAL_SERVER_ERROR ); + } + + return $this->getBatchRequestResponse( $transactions ); + + } + + /** + * @param Request $request + * + * @return mixed + */ + private function getBatchHeader( Request $request ) + { + + $headers = $request->headers->all(); + $contentType = $request->headers->get( "content-type" ); + $this->boundary = $this->parseBoundary( $contentType ); + + return $headers; + + } + + /** + * @param string $contentType + * + * @return string + * @throws BadRequestHttpException + */ + private function parseBoundary( $contentType ) + { + + if ( !$contentType ) { + throw new BadRequestHttpException( "Content-type can not be found in header" ); + } + $contentTypeData = explode( ";", $contentType ); + + foreach ( $contentTypeData as $data ) { + $contentTypePart = explode( "=", $data ); + if ( sizeof( $contentTypePart ) == 2 && trim( $contentTypePart[ 0 ] ) == "boundary" ) { + $boundary = trim( $contentTypePart[ 1 ] ); + break; + } + } + if ( isset( $boundary ) ) { + return $boundary; + } + else { + throw new BadRequestHttpException( "Boundary can not be found." ); + } + + } + + private function parseContentId( $request_header ) + { + + if ( !$request_header ) { + throw new BadRequestHttpException( "Subrequest header can not be found" ); + } + $request_header_data = explode( PHP_EOL, $request_header ); + + foreach ( $request_header_data as $data ) { + $item = explode( ':', $data, 2 ); + array_walk( $item, + function ( &$value ) { + + $value = trim( strtolower( $value ) ); + } ); + + if ( $item[ 0 ] === 'content-id' ) { + return $item[ 1 ]; + } // if + } // foreach + + throw new BadRequestHttpException( "Content-id can not be found in subrequest header" ); + + } // perseContentId + + /** + * @param Request $request + * + * @return array + * @throws HttpException + * @throws \HttpHeaderException + */ + private function getTransactions( Request $request ) + { + + $transactions = []; + $content = explode( "--" . $this->boundary . "--", $request->getContent() )[ 0 ]; + $subRequestsAsString = explode( "--" . $this->boundary, $content ); + array_map( function ( $data ) { + + trim( $data ); + }, + $subRequestsAsString ); + $subRequestsAsString = array_filter( $subRequestsAsString, + function ( $data ) { + + return strlen( $data ) > 0; + } ); + + if ( count( $subRequestsAsString ) > $this->max_calls ) { + throw new HttpException( Response::HTTP_REQUEST_ENTITY_TOO_LARGE, + sprintf( "Maximum call limit exceeded (found %d). Please consider to send only %d calls per request.", + count( $subRequestsAsString ), + $this->max_calls ) ); + } + + $subRequestsAsString = array_values( $subRequestsAsString ); + foreach ( $subRequestsAsString as $item ) { + $item = preg_split( "/(?>\r{2}|\n{2}|(?>\r\n){2})/", trim( $item ), 2 ); + $requestHeader = $item[ 0 ]; + $requestString = $item[ 1 ]; + + $transaction = $this->convertGuzzleRequestToTransactions( + \GuzzleHttp\Psr7\parse_request( $requestString ) + ); + $transaction->content_id = $this->parseContentId( $requestHeader ); + + $transactions[] = $transaction; + } + + return $transactions; + + } + + /** + * @param \GuzzleHttp\Psr7\Request $guzzleRequest + * + * @return Transaction + * @throws \HttpHeaderException + */ + private function convertGuzzleRequestToTransactions( \GuzzleHttp\Psr7\Request $guzzleRequest ) + { + + $url = $guzzleRequest->getUri()->getPath(); + $method = $guzzleRequest->getMethod(); + $cookies = []; + $files = []; + $server = $this->batchRequest->server->all(); + $content = null; + + $content_type = $guzzleRequest->getHeader( 'content-type' )[ 0 ]; + + $params = ContentParser::parse( $content_type, + $guzzleRequest->getBody() ); + + $transaction = new Transaction(); + + $transaction->request = Request::create( $url, + $method, + $params, + $cookies, + $files, + $server, + $content ); + + foreach ( $guzzleRequest->getHeaders() as $key => $value ) { + $transaction->request->headers->set( $key, $value ); + } + + return $transaction; + + } + + /** + * @param Transaction[] $transactions + * + * @return Response + * @throws \Exception + */ + private function getBatchRequestResponse( array $transactions ) + { + + foreach ( $transactions as $transaction ) { + $transaction->response = $this->kernel->handle( $transaction->request, HttpKernelInterface::SUB_REQUEST ); + } + + return $this->generateBatchResponseFromSubResponses( $transactions ); + + } + + /** + * @param Transaction[] $transactions + * + * @return Response + */ + private function generateBatchResponseFromSubResponses( $transactions ) + { + + $response = new Response(); + $version = $response->getProtocolVersion(); + $contentType = 'multipart/batch;type="application/http;type=' . $version . '";boundary=' . $this->boundary; + $response->headers->set( "Content-Type", $contentType ); + + $contentForSubResponses = []; + foreach ( $transactions as $transaction ) { + $contentForSubResponses[] = $this->generateSubResponseFromContent( $transaction ); + } + $content = "--" . $this->boundary . PHP_EOL; + $content .= implode( PHP_EOL . "--" . $this->boundary . PHP_EOL, $contentForSubResponses ) . PHP_EOL; + $content .= "--" . $this->boundary . "--" . PHP_EOL; + $response->setContent( $content ); + + return $response; + + } + + /** + * @param Transaction $transaction + * + * @return string + */ + private function generateSubResponseFromContent( Transaction $transaction ) + { + + $content = ''; + $content .= 'Content-Type: application/http;version=' . $transaction->response->getProtocolVersion() . PHP_EOL; + $content .= 'Content-Transfer-Encoding: binary' . PHP_EOL; + $content .= 'In-Reply-To: ' . $transaction->content_id . PHP_EOL; + $content .= PHP_EOL; + $content .= "HTTP/" . $transaction->response->getProtocolVersion() . " " . + $transaction->response->getStatusCode() . + " " . + Response::$statusTexts[ $transaction->response->getStatusCode() ] . PHP_EOL; + foreach ( $transaction->response->headers->allPreserveCase() as $key => $value ) { + $content .= sprintf( "%s:%s" . PHP_EOL, $key, implode( ",", $value ) ); + } + $content .= PHP_EOL; + $content .= trim( $transaction->response->getContent() ); + + return $content; + + } }