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 new file mode 100644 index 0000000..0d70444 --- /dev/null +++ b/DependencyInjection/HttpBatchExtension.php @@ -0,0 +1,31 @@ +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/HTTP/ContentParser.php b/HTTP/ContentParser.php new file mode 100644 index 0000000..746b305 --- /dev/null +++ b/HTTP/ContentParser.php @@ -0,0 +1,309 @@ +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 + + 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=\"([^\"]*)\"(?>\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 ) { +// 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 diff --git a/Handler.php b/Handler.php index 4b8061e..c50fed0 100644 --- a/Handler.php +++ b/Handler.php @@ -1,14 +1,21 @@ kernel = $kernel; + + $this->max_calls = $max_calls; + } /** * @param Request $request + * * @return Response + * @throws \Exception */ - public function handle(Request $request) + public function handle( Request $request ) { + $this->batchRequest = $request; - return $this->parseRequest($request); + + return $this->parseRequest( $request ); + } /** * @param Request $request + * * @return Response + * @throws \Exception */ - private function parseRequest(Request $request) + private function parseRequest( Request $request ) { - $this->getBatchHeader($request); - $subRequests = $this->getSubRequests($request); - return $this->getBatchRequestResponse($subRequests); + + $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) + private function getBatchHeader( Request $request ) { - $headers = $request->headers->all(); - $contentType = $request->headers->get("content-type"); - $this->boundary = $this->parseBoundary($contentType); + + $headers = $request->headers->all(); + $contentType = $request->headers->get( "content-type" ); + $this->boundary = $this->parseBoundary( $contentType ); + return $headers; + } /** * @param string $contentType + * * @return string - * @throws \HttpHeaderException + * @throws BadRequestHttpException */ - private function parseBoundary($contentType) + private function parseBoundary( $contentType ) { - if (!$contentType) { - throw new \HttpHeaderException("Content-type can not be found in header"); + + if ( !$contentType ) { + throw new BadRequestHttpException( "Content-type can not be found in header" ); } - $contentTypeData = explode(";", $contentType); + $contentTypeData = explode( ";", $contentType ); - foreach ($contentTypeData as $data) { - $contentTypePart = explode("=", $data); - if (sizeof($contentTypePart) == 2 && trim($contentTypePart[0]) == "boundary") { - $boundary = trim($contentTypePart[1]); + foreach ( $contentTypeData as $data ) { + $contentTypePart = explode( "=", $data ); + if ( sizeof( $contentTypePart ) == 2 && trim( $contentTypePart[ 0 ] ) == "boundary" ) { + $boundary = trim( $contentTypePart[ 1 ] ); break; } } - if (isset($boundary)) { + if ( isset( $boundary ) ) { return $boundary; - } else { - throw new BadRequestHttpException("Boundary can not be found."); } + 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 getSubRequests(Request $request) + private function getTransactions( 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)); + $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 ) ); } - return $subRequests; + $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 Request + * + * @return Transaction + * @throws \HttpHeaderException */ - private function convertGuzzleRequestToSymfonyRequest(\GuzzleHttp\Psr7\Request $guzzleRequest) + private function convertGuzzleRequestToTransactions( \GuzzleHttp\Psr7\Request $guzzleRequest ) { - $url = $guzzleRequest->getUri()->getPath(); - $method = $guzzleRequest->getMethod(); - parse_str($guzzleRequest->getUri()->getQuery(), $params); + + $url = $guzzleRequest->getUri()->getPath(); + $method = $guzzleRequest->getMethod(); $cookies = []; - $files = []; - $server = []; - $content = $guzzleRequest->getBody(); + $files = []; + $server = $this->batchRequest->server->all(); + $content = null; - $symfonyRequest = Request::create($url, $method, $params, $cookies, $files, $server, $content); - foreach ($guzzleRequest->getHeaders() as $key => $value) { - $symfonyRequest->headers->set($key, $value); + $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 $symfonyRequest; + + return $transaction; + } /** - * @param array $subRequests + * @param Transaction[] $transactions + * * @return Response + * @throws \Exception */ - private function getBatchRequestResponse(array $subRequests) + private function getBatchRequestResponse( array $transactions ) { - $subResponses = []; - foreach ($subRequests as $subRequest) { - $subResponses[] = $this->kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST); + + foreach ( $transactions as $transaction ) { + $transaction->response = $this->kernel->handle( $transaction->request, HttpKernelInterface::SUB_REQUEST ); } - return $this->generateBatchResponseFromSubResponses($subResponses); + + return $this->generateBatchResponseFromSubResponses( $transactions ); + } /** - * @param Response[] $subResponses + * @param Transaction[] $transactions + * * @return Response */ - private function generateBatchResponseFromSubResponses($subResponses) + private function generateBatchResponseFromSubResponses( $transactions ) { - $response = new Response(); - $version = $response->getProtocolVersion(); + + $response = new Response(); + $version = $response->getProtocolVersion(); $contentType = 'multipart/batch;type="application/http;type=' . $version . '";boundary=' . $this->boundary; - $response->headers->set("Content-Type", $contentType); + $response->headers->set( "Content-Type", $contentType ); $contentForSubResponses = []; - foreach ($subResponses as $subResponse) { - $contentForSubResponses[] = $this->generateSubResponseFromContent($subResponse); + foreach ( $transactions as $transaction ) { + $contentForSubResponses[] = $this->generateSubResponseFromContent( $transaction ); } $content = "--" . $this->boundary . PHP_EOL; - $content .= implode(PHP_EOL . "--" . $this->boundary . PHP_EOL, $contentForSubResponses); + $content .= implode( PHP_EOL . "--" . $this->boundary . PHP_EOL, $contentForSubResponses ) . PHP_EOL; $content .= "--" . $this->boundary . "--" . PHP_EOL; - $response->setContent($content); + $response->setContent( $content ); + return $response; + } /** - * @param \Symfony\Component\HttpFoundation\Response $response + * @param Transaction $transaction + * * @return string */ - private function generateSubResponseFromContent(\Symfony\Component\HttpFoundation\Response $response) + private function generateSubResponseFromContent( Transaction $transaction ) { + $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 .= '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/" . $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 .= "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($response->getContent()); + $content .= trim( $transaction->response->getContent() ); + return $content; + } } 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 @@ +=5.5.9",