diff options
Diffstat (limited to 'inc/mailgun/php-http/curl-client/src')
| -rw-r--r-- | inc/mailgun/php-http/curl-client/src/Client.php | 371 | ||||
| -rw-r--r-- | inc/mailgun/php-http/curl-client/src/CurlPromise.php | 108 | ||||
| -rw-r--r-- | inc/mailgun/php-http/curl-client/src/MultiRunner.php | 127 | ||||
| -rw-r--r-- | inc/mailgun/php-http/curl-client/src/PromiseCore.php | 224 | ||||
| -rw-r--r-- | inc/mailgun/php-http/curl-client/src/ResponseBuilder.php | 21 |
5 files changed, 851 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); + } +} diff --git a/inc/mailgun/php-http/curl-client/src/CurlPromise.php b/inc/mailgun/php-http/curl-client/src/CurlPromise.php new file mode 100644 index 0000000..68a775c --- /dev/null +++ b/inc/mailgun/php-http/curl-client/src/CurlPromise.php @@ -0,0 +1,108 @@ +<?php +namespace Http\Client\Curl; + +use Http\Promise\Promise; + +/** + * Promise represents a response that may not be available yet, but will be resolved at some point + * in future. It acts like a proxy to the actual response. + * + * This interface is an extension of the promises/a+ specification https://promisesaplus.com/ + * Value is replaced by an object where its class implement a Psr\Http\Message\RequestInterface. + * Reason is replaced by an object where its class implement a Http\Client\Exception. + * + * @license http://opensource.org/licenses/MIT MIT + * + * @author Михаил Красильников <m.krasilnikov@yandex.ru> + */ +class CurlPromise implements Promise +{ + /** + * Shared promise core + * + * @var PromiseCore + */ + private $core; + + /** + * Requests runner + * + * @var MultiRunner + */ + private $runner; + + /** + * Create new promise. + * + * @param PromiseCore $core Shared promise core + * @param MultiRunner $runner Simultaneous requests runner + */ + public function __construct(PromiseCore $core, MultiRunner $runner) + { + $this->core = $core; + $this->runner = $runner; + } + + /** + * Add behavior for when the promise is resolved or rejected. + * + * If you do not care about one of the cases, you can set the corresponding callable to null + * The callback will be called when the response or exception arrived and never more than once. + * + * @param callable $onFulfilled Called when a response will be available. + * @param callable $onRejected Called when an error happens. + * + * You must always return the Response in the interface or throw an Exception. + * + * @return Promise Always returns a new promise which is resolved with value of the executed + * callback (onFulfilled / onRejected). + */ + public function then(callable $onFulfilled = null, callable $onRejected = null) + { + if ($onFulfilled) { + $this->core->addOnFulfilled($onFulfilled); + } + if ($onRejected) { + $this->core->addOnRejected($onRejected); + } + + return new self($this->core, $this->runner); + } + + /** + * Get the state of the promise, one of PENDING, FULFILLED or REJECTED. + * + * @return string + */ + public function getState() + { + return $this->core->getState(); + } + + /** + * Wait for the promise to be fulfilled or rejected. + * + * When this method returns, the request has been resolved and the appropriate callable has terminated. + * + * When called with the unwrap option + * + * @param bool $unwrap Whether to return resolved value / throw reason or not + * + * @return \Psr\Http\Message\ResponseInterface|null Resolved value, null if $unwrap is set to false + * + * @throws \Http\Client\Exception The rejection reason. + */ + public function wait($unwrap = true) + { + $this->runner->wait($this->core); + + if ($unwrap) { + if ($this->core->getState() === self::REJECTED) { + throw $this->core->getException(); + } + + return $this->core->getResponse(); + } + return null; + } +} diff --git a/inc/mailgun/php-http/curl-client/src/MultiRunner.php b/inc/mailgun/php-http/curl-client/src/MultiRunner.php new file mode 100644 index 0000000..9094c0f --- /dev/null +++ b/inc/mailgun/php-http/curl-client/src/MultiRunner.php @@ -0,0 +1,127 @@ +<?php +namespace Http\Client\Curl; + +use Http\Client\Exception\RequestException; + +/** + * Simultaneous requests runner + * + * @license http://opensource.org/licenses/MIT MIT + * + * @author Михаил Красильников <m.krasilnikov@yandex.ru> + */ +class MultiRunner +{ + /** + * cURL multi handle + * + * @var resource|null + */ + private $multiHandle = null; + + /** + * Awaiting cores + * + * @var PromiseCore[] + */ + private $cores = []; + + /** + * Release resources if still active + */ + public function __destruct() + { + if (is_resource($this->multiHandle)) { + curl_multi_close($this->multiHandle); + } + } + + /** + * Add promise to runner + * + * @param PromiseCore $core + */ + public function add(PromiseCore $core) + { + foreach ($this->cores as $existed) { + if ($existed === $core) { + return; + } + } + + $this->cores[] = $core; + + if (null === $this->multiHandle) { + $this->multiHandle = curl_multi_init(); + } + curl_multi_add_handle($this->multiHandle, $core->getHandle()); + } + + /** + * Remove promise from runner + * + * @param PromiseCore $core + */ + public function remove(PromiseCore $core) + { + foreach ($this->cores as $index => $existed) { + if ($existed === $core) { + curl_multi_remove_handle($this->multiHandle, $core->getHandle()); + unset($this->cores[$index]); + return; + } + } + } + + /** + * Wait for request(s) to be completed. + * + * @param PromiseCore|null $targetCore + */ + public function wait(PromiseCore $targetCore = null) + { + do { + $status = curl_multi_exec($this->multiHandle, $active); + $info = curl_multi_info_read($this->multiHandle); + if (false !== $info) { + $core = $this->findCoreByHandle($info['handle']); + + if (null === $core) { + // We have no promise for this handle. Drop it. + curl_multi_remove_handle($this->multiHandle, $info['handle']); + continue; + } + + if (CURLE_OK === $info['result']) { + $core->fulfill(); + } else { + $error = curl_error($core->getHandle()); + $core->reject(new RequestException($error, $core->getRequest())); + } + $this->remove($core); + + // This is a promise we are waited for. So exiting wait(). + if ($core === $targetCore) { + return; + } + } + } while ($status === CURLM_CALL_MULTI_PERFORM || $active); + } + + /** + * Find core by handle. + * + * @param resource $handle + * + * @return PromiseCore|null + */ + private function findCoreByHandle($handle) + { + foreach ($this->cores as $core) { + if ($core->getHandle() === $handle) { + return $core; + } + } + return null; + } +} diff --git a/inc/mailgun/php-http/curl-client/src/PromiseCore.php b/inc/mailgun/php-http/curl-client/src/PromiseCore.php new file mode 100644 index 0000000..f1a3aa5 --- /dev/null +++ b/inc/mailgun/php-http/curl-client/src/PromiseCore.php @@ -0,0 +1,224 @@ +<?php +namespace Http\Client\Curl; + +use Http\Client\Exception; +use Http\Promise\Promise; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; + +/** + * Shared promises core. + * + * @license http://opensource.org/licenses/MIT MIT + * + * @author Михаил Красильников <m.krasilnikov@yandex.ru> + */ +class PromiseCore +{ + /** + * HTTP request + * + * @var RequestInterface + */ + private $request; + + /** + * cURL handle + * + * @var resource + */ + private $handle; + + /** + * Response builder + * + * @var ResponseBuilder + */ + private $responseBuilder; + + /** + * Promise state + * + * @var string + */ + private $state; + + /** + * Exception + * + * @var Exception|null + */ + private $exception = null; + + /** + * Functions to call when a response will be available. + * + * @var callable[] + */ + private $onFulfilled = []; + + /** + * Functions to call when an error happens. + * + * @var callable[] + */ + private $onRejected = []; + + /** + * Create shared core. + * + * @param RequestInterface $request HTTP request + * @param resource $handle cURL handle + * @param ResponseBuilder $responseBuilder + */ + public function __construct( + RequestInterface $request, + $handle, + ResponseBuilder $responseBuilder + ) { + assert('is_resource($handle)'); + assert('get_resource_type($handle) === "curl"'); + + $this->request = $request; + $this->handle = $handle; + $this->responseBuilder = $responseBuilder; + $this->state = Promise::PENDING; + } + + /** + * Add on fulfilled callback. + * + * @param callable $callback + */ + public function addOnFulfilled(callable $callback) + { + if ($this->getState() === Promise::PENDING) { + $this->onFulfilled[] = $callback; + } elseif ($this->getState() === Promise::FULFILLED) { + $response = call_user_func($callback, $this->responseBuilder->getResponse()); + if ($response instanceof ResponseInterface) { + $this->responseBuilder->setResponse($response); + } + } + } + + /** + * Add on rejected callback. + * + * @param callable $callback + */ + public function addOnRejected(callable $callback) + { + if ($this->getState() === Promise::PENDING) { + $this->onRejected[] = $callback; + } elseif ($this->getState() === Promise::REJECTED) { + $this->exception = call_user_func($callback, $this->exception); + } + } + + /** + * Return cURL handle + * + * @return resource + */ + public function getHandle() + { + return $this->handle; + } + + /** + * Get the state of the promise, one of PENDING, FULFILLED or REJECTED. + * + * @return string + */ + public function getState() + { + return $this->state; + } + + /** + * Return request + * + * @return RequestInterface + */ + public function getRequest() + { + return $this->request; + } + + /** + * Return the value of the promise (fulfilled). + * + * @return ResponseInterface Response Object only when the Promise is fulfilled. + */ + public function getResponse() + { + return $this->responseBuilder->getResponse(); + } + + /** + * Get the reason why the promise was rejected. + * + * If the exception is an instance of Http\Client\Exception\HttpException it will contain + * the response object with the status code and the http reason. + * + * @return Exception Exception Object only when the Promise is rejected. + * + * @throws \LogicException When the promise is not rejected. + */ + public function getException() + { + if (null === $this->exception) { + throw new \LogicException('Promise is not rejected'); + } + + return $this->exception; + } + + /** + * Fulfill promise. + */ + public function fulfill() + { + $this->state = Promise::FULFILLED; + $response = $this->responseBuilder->getResponse(); + try { + $response->getBody()->seek(0); + } catch (\RuntimeException $e) { + $exception = new Exception\TransferException($e->getMessage(), $e->getCode(), $e); + $this->reject($exception); + + return; + } + + while (count($this->onFulfilled) > 0) { + $callback = array_shift($this->onFulfilled); + $response = call_user_func($callback, $response); + } + + if ($response instanceof ResponseInterface) { + $this->responseBuilder->setResponse($response); + } + } + + /** + * Reject promise. + * + * @param Exception $exception Reject reason. + */ + public function reject(Exception $exception) + { + $this->exception = $exception; + $this->state = Promise::REJECTED; + + while (count($this->onRejected) > 0) { + $callback = array_shift($this->onRejected); + try { + $exception = call_user_func($callback, $this->exception); + $this->exception = $exception; + } catch (Exception $exception) { + $this->exception = $exception; + } + } + } +} diff --git a/inc/mailgun/php-http/curl-client/src/ResponseBuilder.php b/inc/mailgun/php-http/curl-client/src/ResponseBuilder.php new file mode 100644 index 0000000..99e79db --- /dev/null +++ b/inc/mailgun/php-http/curl-client/src/ResponseBuilder.php @@ -0,0 +1,21 @@ +<?php +namespace Http\Client\Curl; + +use Http\Message\Builder\ResponseBuilder as OriginalResponseBuilder; +use Psr\Http\Message\ResponseInterface; + +/** + * Extended response builder + */ +class ResponseBuilder extends OriginalResponseBuilder +{ + /** + * Replace response with a new instance + * + * @param ResponseInterface $response + */ + public function setResponse(ResponseInterface $response) + { + $this->response = $response; + } +} |
