Skip to content

Commit

Permalink
Add some protection for EasyHandle
Browse files Browse the repository at this point in the history
  • Loading branch information
phansys committed Apr 1, 2024
1 parent 6526da0 commit ed9dfc1
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 79 deletions.
4 changes: 2 additions & 2 deletions src/Handler/CurlFactory.php
Expand Up @@ -147,7 +147,7 @@ public static function finish(callable $handler, EasyHandle $easy, CurlFactoryIn
self::invokeStats($easy);
}

if (!$easy->response || $easy->errno) {
if (!$easy->response || \CURLE_OK !== $easy->errno) {
return self::finishError($handler, $easy, $factory);
}

Expand Down Expand Up @@ -192,7 +192,7 @@ private static function finishError(callable $handler, EasyHandle $easy, CurlFac
$factory->release($easy);

// Retry when nothing is present or when curl failed to rewind.
if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) {
if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == /* \CURLE_SEND_FAIL_REWIND */ 65)) {
return self::retryFailedRewind($handler, $easy, $ctx);
}

Expand Down
119 changes: 88 additions & 31 deletions src/Handler/EasyHandle.php
Expand Up @@ -11,54 +11,69 @@
/**
* Represents a cURL easy handle and the data it populates.
*
* @property resource|\CurlHandle $handle resource cURL resource
* @property StreamInterface $sink Where data is being written
* @property array $headers Received HTTP headers so far
* @property ResponseInterface|null $response Received response (if any)
* @property RequestInterface $request Request being sent
* @property array $options Request options
* @property int $errno int cURL error number
* @property \Throwable|null $onHeadersException Exception during on_headers (if any)
* @property \Throwable|null $createResponseException Exception during createResponse (if any)
*
* @internal
*/
final class EasyHandle
{
/**
* @var resource|\CurlHandle cURL resource
*/
public $handle;
private $handle;

/**
* @var StreamInterface Where data is being written
*/
public $sink;
private $sink;

/**
* @var RequestInterface Request being sent
*/
public $request;
private $request;

/**
* @var array Request options
*/
public $options = [];
private $options = [];

/**
* @var \Throwable|null Exception during on_headers (if any)
* @var int cURL error number (if any)
*/
public $onHeadersException;
private $errno = \CURLE_OK;

/**
* @var \Exception|null Exception during createResponse (if any)
* @var array Received HTTP headers so far
*/
public $createResponseException;
private $headers = [];

/**
* @var int cURL error number (if any)
* @var ResponseInterface|null Received response (if any)
*/
private $errno = CURLE_OK;
private $response;

/**
* @var array Received HTTP headers so far
* @var \Throwable|null Exception during on_headers (if any)
*/
private $headers = [];
private $onHeadersException;

/**
* @var ResponseInterface|null Received response (if any)
* @var \Throwable|null Exception during createResponse (if any)
*/
private $response;
private $createResponseException;

/**
* @var bool Tells if the EasyHandle has been initialized
*/
private $initialized = false;

/**
* Attach a response to the easy handle based on the received headers.
Expand Down Expand Up @@ -98,42 +113,84 @@ public function createResponse(): void
}

/**
* @param string $name
*
* @return void
* @return mixed
*
* @throws \BadMethodCallException
*/
public function &__get($name)
public function &__get(string $name)
{
if (in_array($name, ['errno', 'headers', 'onHeadersException', 'response'], true)) {
if (('handle' !== $name && property_exists($this, $name)) || $this->initialized && isset($this->handle)) {
return $this->{$name};
}

$msg = $name === 'handle'
? 'The EasyHandle has been released'
? 'The EasyHandle '.($this->initialized ? 'has been released' : 'is not initialized')
: sprintf('Undefined property: %s::$%s', __CLASS__, $name);

throw new \BadMethodCallException($msg);
}

public function __set($name, $value)
/**
* @param mixed $value
*
* @throws \UnexpectedValueException|\LogicException
*/
public function __set(string $name, $value): void
{
if (in_array($name, ['errno', 'headers', 'onHeadersException', 'response'], true)) {
if ('response' === $name) {
// BC: Change to `\Error` when bumping PHP version to ^7.0
throw new \LogicException(sprintf('Cannot set private property %s::$%s', __CLASS__, $name));
}
if ($this->initialized && !isset($this->handle)) {
throw new \UnexpectedValueException('The EasyHandle has been released, please use a new EasyHandle instead.');
}

if (!isset($this->handle) || !is_resource($this->handle) || 'curl' !== get_resource_type($this->handle)) {
throw new \LogicException(sprintf('Property %s::$%s could not be set when there isn\'t a valid handle', __CLASS__, $name));
}
if (in_array($name, ['response', 'initialized'], true)) {
throw new \LogicException(sprintf('Cannot set private property %s::$%s.', __CLASS__, $name));
}

if (in_array($name, ['errno', 'handle', 'headers', 'onHeadersException', 'createResponseException'], true)) {
if ('handle' === $name) {
if (isset($this->handle)) {
throw new \UnexpectedValueException(sprintf('Property %s::$%s is already set, please use a new EasyHandle instead.', __CLASS__, $name));
}

if (\PHP_VERSION_ID >= 80000) {
if (!$value instanceof \CurlHandle) {

Check failure on line 155 in src/Handler/EasyHandle.php

View workflow job for this annotation

GitHub Actions / PHPStan

Class CurlHandle not found.

Check failure on line 155 in src/Handler/EasyHandle.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Handler/EasyHandle.php:155:44: UndefinedClass: Class, interface or enum named CurlHandle does not exist (see https://psalm.dev/019)
throw new \TypeError(sprintf('Property %s::$%s can only accept an object of type "%s".', __CLASS__, $name, \CurlHandle::class));

Check failure on line 156 in src/Handler/EasyHandle.php

View workflow job for this annotation

GitHub Actions / PHPStan

Class CurlHandle not found.
}
} elseif (!is_resource($value) || 'curl' !== get_resource_type($value)) {
throw new \TypeError(sprintf('Property %s::$%s can only accept a resource of type "curl".', __CLASS__, $name));
}

$this->initialized = true;
} else {
if (!isset($this->handle) || !$this->handle instanceof \CurlHandle || !is_resource($this->handle) || 'curl' !== get_resource_type($this->handle)) {

Check failure on line 164 in src/Handler/EasyHandle.php

View workflow job for this annotation

GitHub Actions / PHPStan

Class CurlHandle not found.
throw new \UnexpectedValueException(sprintf('Property %s::$%s could not be set when there isn\'t a valid handle.', __CLASS__, $name));
}

if ('errno' === $name && CURLE_OK !== ($handleErrno = curl_errno($this->handle)) && $value !== $handleErrno) {
throw new \LogicException(sprintf('Property %s::$errno could not be set with %u since the handle is reporting error %u', __CLASS__, $value, $handleErrno));
if ('errno' === $name) {
if (!is_int($value)) {
throw new \TypeError(sprintf('Property %s::$errno can only accept a value of type int, %s given.', __CLASS__, gettype($value)));
}

$handleErrno = curl_errno($this->handle);

if (\CURLE_OK !== $handleErrno && $value !== $handleErrno) {
throw new \UnexpectedValueException(sprintf('Property %s::$errno could not be set with %u since the handle is reporting error %u.', __CLASS__, $value, $handleErrno));
}
}
}
}

$this->{$name} = $value;
}

/**
* @throws \LogicException
*/
public function __unset(string $name): void
{
if ('handle' !== $name) {
throw new \Error(sprintf('Cannot unset private property %s::$%s.', __CLASS__, $name));
}

unset($this->{$name});
}
}

0 comments on commit ed9dfc1

Please sign in to comment.