diff options
Diffstat (limited to 'inc/mailgun/php-http/curl-client/src/Client.php')
| -rw-r--r-- | inc/mailgun/php-http/curl-client/src/Client.php | 371 |
1 files changed, 371 insertions, 0 deletions
diff --git a/inc/mailgun/php-http/curl-client/src/Client.php b/inc/mailgun/php-http/curl-client/src/Client.php new file mode 100644 index 0000000..5696ab3 --- /dev/null +++ b/inc/mailgun/php-http/curl-client/src/Client.php @@ -0,0 +1,371 @@ +<?php +namespace Http\Client\Curl; + +use Http\Client\Exception; +use Http\Client\HttpAsyncClient; +use Http\Client\HttpClient; +use Http\Discovery\MessageFactoryDiscovery; +use Http\Discovery\StreamFactoryDiscovery; +use Http\Message\MessageFactory; +use Http\Message\StreamFactory; +use Http\Promise\Promise; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; + +/** + * PSR-7 compatible cURL based HTTP client + * + * @license http://opensource.org/licenses/MIT MIT + * + * @author Михаил Красильников <m.krasilnikov@yandex.ru> + * @author Blake Williams <github@shabbyrobe.org> + * + * @api + * @since 1.0 + */ +class Client implements HttpClient, HttpAsyncClient +{ + /** + * cURL options + * + * @var array + */ + private $options; + + /** + * PSR-7 message factory + * + * @var MessageFactory + */ + private $messageFactory; + + /** + * PSR-7 stream factory + * + * @var StreamFactory + */ + private $streamFactory; + + /** + * cURL synchronous requests handle + * + * @var resource|null + */ + private $handle = null; + + /** + * Simultaneous requests runner + * + * @var MultiRunner|null + */ + private $multiRunner = null; + + /** + * Create new client + * + * @param MessageFactory|null $messageFactory HTTP Message factory + * @param StreamFactory|null $streamFactory HTTP Stream factory + * @param array $options cURL options (see http://php.net/curl_setopt) + * + * @throws \Http\Discovery\Exception\NotFoundException If factory discovery failed. + * + * @since 1.0 + */ + public function __construct( + MessageFactory $messageFactory = null, + StreamFactory $streamFactory = null, + array $options = [] + ) { + $this->messageFactory = $messageFactory ?: MessageFactoryDiscovery::find(); + $this->streamFactory = $streamFactory ?: StreamFactoryDiscovery::find(); + $this->options = $options; + } + + /** + * Release resources if still active + */ + public function __destruct() + { + if (is_resource($this->handle)) { + curl_close($this->handle); + } + } + + /** + * Sends a PSR-7 request. + * + * @param RequestInterface $request + * + * @return ResponseInterface + * + * @throws \Http\Client\Exception\NetworkException In case of network problems. + * @throws \Http\Client\Exception\RequestException On invalid request. + * @throws \InvalidArgumentException For invalid header names or values. + * @throws \RuntimeException If creating the body stream fails. + * + * @since 1.6 \UnexpectedValueException replaced with RequestException. + * @since 1.6 Throw NetworkException on network errors. + * @since 1.0 + */ + public function sendRequest(RequestInterface $request) + { + $responseBuilder = $this->createResponseBuilder(); + $options = $this->createCurlOptions($request, $responseBuilder); + + if (is_resource($this->handle)) { + curl_reset($this->handle); + } else { + $this->handle = curl_init(); + } + + curl_setopt_array($this->handle, $options); + curl_exec($this->handle); + + $errno = curl_errno($this->handle); + switch ($errno) { + case CURLE_OK: + // All OK, no actions needed. + break; + case CURLE_COULDNT_RESOLVE_PROXY: + case CURLE_COULDNT_RESOLVE_HOST: + case CURLE_COULDNT_CONNECT: + case CURLE_OPERATION_TIMEOUTED: + case CURLE_SSL_CONNECT_ERROR: + throw new Exception\NetworkException(curl_error($this->handle), $request); + default: + throw new Exception\RequestException(curl_error($this->handle), $request); + } + + $response = $responseBuilder->getResponse(); + $response->getBody()->seek(0); + + return $response; + } + + /** + * Sends a PSR-7 request in an asynchronous way. + * + * @param RequestInterface $request + * + * @return Promise + * + * @throws \Http\Client\Exception\RequestException On invalid request. + * @throws \InvalidArgumentException For invalid header names or values. + * @throws \RuntimeException If creating the body stream fails. + * + * @since 1.6 \UnexpectedValueException replaced with RequestException. + * @since 1.0 + */ + public function sendAsyncRequest(RequestInterface $request) + { + if (!$this->multiRunner instanceof MultiRunner) { + $this->multiRunner = new MultiRunner(); + } + + $handle = curl_init(); + $responseBuilder = $this->createResponseBuilder(); + $options = $this->createCurlOptions($request, $responseBuilder); + curl_setopt_array($handle, $options); + + $core = new PromiseCore($request, $handle, $responseBuilder); + $promise = new CurlPromise($core, $this->multiRunner); + $this->multiRunner->add($core); + + return $promise; + } + + /** + * Generates cURL options + * + * @param RequestInterface $request + * @param ResponseBuilder $responseBuilder + * + * @throws \Http\Client\Exception\RequestException On invalid request. + * @throws \InvalidArgumentException For invalid header names or values. + * @throws \RuntimeException if can not read body + * + * @return array + */ + private function createCurlOptions(RequestInterface $request, ResponseBuilder $responseBuilder) + { + $options = $this->options; + + $options[CURLOPT_HEADER] = false; + $options[CURLOPT_RETURNTRANSFER] = false; + $options[CURLOPT_FOLLOWLOCATION] = false; + + try { + $options[CURLOPT_HTTP_VERSION] + = $this->getProtocolVersion($request->getProtocolVersion()); + } catch (\UnexpectedValueException $e) { + throw new Exception\RequestException($e->getMessage(), $request); + } + $options[CURLOPT_URL] = (string) $request->getUri(); + + $options = $this->addRequestBodyOptions($request, $options); + + $options[CURLOPT_HTTPHEADER] = $this->createHeaders($request, $options); + + if ($request->getUri()->getUserInfo()) { + $options[CURLOPT_USERPWD] = $request->getUri()->getUserInfo(); + } + + $options[CURLOPT_HEADERFUNCTION] = function ($ch, $data) use ($responseBuilder) { + $str = trim($data); + if ('' !== $str) { + if (strpos(strtolower($str), 'http/') === 0) { + $responseBuilder->setStatus($str)->getResponse(); + } else { + $responseBuilder->addHeader($str); + } + } + + return strlen($data); + }; + + $options[CURLOPT_WRITEFUNCTION] = function ($ch, $data) use ($responseBuilder) { + return $responseBuilder->getResponse()->getBody()->write($data); + }; + + return $options; + } + + /** + * Return cURL constant for specified HTTP version + * + * @param string $requestVersion + * + * @throws \UnexpectedValueException if unsupported version requested + * + * @return int + */ + private function getProtocolVersion($requestVersion) + { + switch ($requestVersion) { + case '1.0': + return CURL_HTTP_VERSION_1_0; + case '1.1': + return CURL_HTTP_VERSION_1_1; + case '2.0': + if (defined('CURL_HTTP_VERSION_2_0')) { + return CURL_HTTP_VERSION_2_0; + } + throw new \UnexpectedValueException('libcurl 7.33 needed for HTTP 2.0 support'); + } + + return CURL_HTTP_VERSION_NONE; + } + + /** + * Add request body related cURL options. + * + * @param RequestInterface $request + * @param array $options + * + * @return array + */ + private function addRequestBodyOptions(RequestInterface $request, array $options) + { + /* + * Some HTTP methods cannot have payload: + * + * - GET — cURL will automatically change method to PUT or POST if we set CURLOPT_UPLOAD or + * CURLOPT_POSTFIELDS. + * - HEAD — cURL treats HEAD as GET request with a same restrictions. + * - TRACE — According to RFC7231: a client MUST NOT send a message body in a TRACE request. + */ + if (!in_array($request->getMethod(), ['GET', 'HEAD', 'TRACE'], true)) { + $body = $request->getBody(); + $bodySize = $body->getSize(); + if ($bodySize !== 0) { + if ($body->isSeekable()) { + $body->rewind(); + } + + // Message has non empty body. + if (null === $bodySize || $bodySize > 1024 * 1024) { + // Avoid full loading large or unknown size body into memory + $options[CURLOPT_UPLOAD] = true; + if (null !== $bodySize) { + $options[CURLOPT_INFILESIZE] = $bodySize; + } + $options[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) { + return $body->read($length); + }; + } else { + // Small body can be loaded into memory + $options[CURLOPT_POSTFIELDS] = (string) $body; + } + } + } + + if ($request->getMethod() === 'HEAD') { + // This will set HTTP method to "HEAD". + $options[CURLOPT_NOBODY] = true; + } elseif ($request->getMethod() !== 'GET') { + // GET is a default method. Other methods should be specified explicitly. + $options[CURLOPT_CUSTOMREQUEST] = $request->getMethod(); + } + + return $options; + } + + /** + * Create headers array for CURLOPT_HTTPHEADER + * + * @param RequestInterface $request + * @param array $options cURL options + * + * @return string[] + */ + private function createHeaders(RequestInterface $request, array $options) + { + $curlHeaders = []; + $headers = $request->getHeaders(); + foreach ($headers as $name => $values) { + $header = strtolower($name); + if ('expect' === $header) { + // curl-client does not support "Expect-Continue", so dropping "expect" headers + continue; + } + if ('content-length' === $header) { + if (array_key_exists(CURLOPT_POSTFIELDS, $options)) { + // Small body content length can be calculated here. + $values = [strlen($options[CURLOPT_POSTFIELDS])]; + } elseif (!array_key_exists(CURLOPT_READFUNCTION, $options)) { + // Else if there is no body, forcing "Content-length" to 0 + $values = [0]; + } + } + foreach ($values as $value) { + $curlHeaders[] = $name . ': ' . $value; + } + } + /* + * curl-client does not support "Expect-Continue", but cURL adds "Expect" header by default. + * We can not suppress it, but we can set it to empty. + */ + $curlHeaders[] = 'Expect:'; + + return $curlHeaders; + } + + /** + * Create new ResponseBuilder instance + * + * @return ResponseBuilder + * + * @throws \RuntimeException If creating the stream from $body fails. + */ + private function createResponseBuilder() + { + try { + $body = $this->streamFactory->createStream(fopen('php://temp', 'w+b')); + } catch (\InvalidArgumentException $e) { + throw new \RuntimeException('Can not create "php://temp" stream.'); + } + $response = $this->messageFactory->createResponse(200, null, [], $body); + + return new ResponseBuilder($response); + } +} |
