diff --git a/composer.json b/composer.json index 69262e6..35db76d 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,8 @@ }, "require-dev": { "phpspec/phpspec": "^2.4", - "henrikbjorn/phpspec-code-coverage" : "^1.0" + "henrikbjorn/phpspec-code-coverage" : "^1.0", + "guzzlehttp/psr7": "^1.4" }, "suggest": { "php-http/logger-plugin": "PSR-3 Logger plugin", diff --git a/spec/Plugin/ContentTypePluginSpec.php b/spec/Plugin/ContentTypePluginSpec.php new file mode 100644 index 0000000..3df7d87 --- /dev/null +++ b/spec/Plugin/ContentTypePluginSpec.php @@ -0,0 +1,109 @@ +shouldHaveType('Http\Client\Common\Plugin\ContentTypePlugin'); + } + + function it_is_a_plugin() + { + $this->shouldImplement('Http\Client\Common\Plugin'); + } + + function it_adds_json_content_type_header(RequestInterface $request) + { + $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); + $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for(json_encode(['foo' => 'bar']))); + $request->withHeader('Content-Type', 'application/json')->shouldBeCalled()->willReturn($request); + + $this->handleRequest($request, function () {}, function () {}); + } + + function it_adds_xml_content_type_header(RequestInterface $request) + { + $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); + $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('bar')); + $request->withHeader('Content-Type', 'application/xml')->shouldBeCalled()->willReturn($request); + + $this->handleRequest($request, function () {}, function () {}); + } + + function it_does_not_set_content_type_header(RequestInterface $request) + { + $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); + $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('foo')); + $request->withHeader('Content-Type', null)->shouldNotBeCalled(); + + $this->handleRequest($request, function () {}, function () {}); + } + + function it_does_not_set_content_type_header_if_already_one(RequestInterface $request) + { + $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(true); + $request->getBody()->shouldNotBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('foo')); + $request->withHeader('Content-Type', null)->shouldNotBeCalled(); + + $this->handleRequest($request, function () {}, function () {}); + } + + function it_does_not_set_content_type_header_if_size_0_or_unknown(RequestInterface $request) + { + $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); + $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for()); + $request->withHeader('Content-Type', null)->shouldNotBeCalled(); + + $this->handleRequest($request, function () {}, function () {}); + } + + function it_adds_xml_content_type_header_if_size_limit_is_not_reached_using_default_value(RequestInterface $request) + { + $this->beConstructedWith([ + 'skip_detection' => true + ]); + + $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); + $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('bar')); + $request->withHeader('Content-Type', 'application/xml')->shouldBeCalled()->willReturn($request); + + $this->handleRequest($request, function () {}, function () {}); + } + + function it_adds_xml_content_type_header_if_size_limit_is_not_reached(RequestInterface $request) + { + $this->beConstructedWith([ + 'skip_detection' => true, + 'size_limit' => 32000000 + ]); + + $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); + $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('bar')); + $request->withHeader('Content-Type', 'application/xml')->shouldBeCalled()->willReturn($request); + + $this->handleRequest($request, function () {}, function () {}); + } + + function it_does_not_set_content_type_header_if_size_limit_is_reached(RequestInterface $request) + { + $this->beConstructedWith([ + 'skip_detection' => true, + 'size_limit' => 8 + ]); + + $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); + $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('bar')); + $request->withHeader('Content-Type', null)->shouldNotBeCalled(); + + $this->handleRequest($request, function () {}, function () {}); + } + +} diff --git a/src/Plugin/ContentTypePlugin.php b/src/Plugin/ContentTypePlugin.php new file mode 100644 index 0000000..038b3e4 --- /dev/null +++ b/src/Plugin/ContentTypePlugin.php @@ -0,0 +1,123 @@ + + */ +final class ContentTypePlugin implements Plugin +{ + /** + * Allow to disable the content type detection when stream is too large (as it can consume a lot of resource). + * + * @var bool + * + * true skip the content type detection + * false detect the content type (default value) + */ + protected $skipDetection; + + /** + * Determine the size stream limit for which the detection as to be skipped (default to 16Mb). + * + * @var int + */ + protected $sizeLimit; + + /** + * @param array $config { + * + * @var bool $skip_detection True skip detection if stream size is bigger than $size_limit. + * @var int $size_limit size stream limit for which the detection as to be skipped. + * } + */ + public function __construct(array $config = []) + { + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'skip_detection' => false, + 'size_limit' => 16000000, + ]); + $resolver->setAllowedTypes('skip_detection', 'bool'); + $resolver->setAllowedTypes('size_limit', 'int'); + + $options = $resolver->resolve($config); + + $this->skipDetection = $options['skip_detection']; + $this->sizeLimit = $options['size_limit']; + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first) + { + if (!$request->hasHeader('Content-Type')) { + $stream = $request->getBody(); + $streamSize = $stream->getSize(); + + if (!$stream->isSeekable()) { + return $next($request); + } + + if (0 === $streamSize) { + return $next($request); + } + + if ($this->skipDetection && (null === $streamSize || $streamSize >= $this->sizeLimit)) { + return $next($request); + } + + if ($this->isJson($stream)) { + $request = $request->withHeader('Content-Type', 'application/json'); + + return $next($request); + } + + if ($this->isXml($stream)) { + $request = $request->withHeader('Content-Type', 'application/xml'); + + return $next($request); + } + } + + return $next($request); + } + + /** + * @param $stream StreamInterface + * + * @return bool + */ + private function isJson($stream) + { + $stream->rewind(); + + json_decode($stream->getContents()); + + return json_last_error() == JSON_ERROR_NONE; + } + + /** + * @param $stream StreamInterface + * + * @return \SimpleXMLElement|false + */ + private function isXml($stream) + { + $stream->rewind(); + + $previousValue = libxml_use_internal_errors(true); + $isXml = simplexml_load_string($stream->getContents()); + libxml_use_internal_errors($previousValue); + + return $isXml; + } +}