Skip to content

Commit

Permalink
Update EasyHandle in order to avoid bad writes of properties relate…
Browse files Browse the repository at this point in the history
…d to the handle
  • Loading branch information
phansys committed Apr 1, 2024
1 parent 04ff56e commit 267b28b
Show file tree
Hide file tree
Showing 4 changed files with 306 additions and 23 deletions.
5 changes: 4 additions & 1 deletion phpstan-baseline.neon
Expand Up @@ -194,4 +194,7 @@ parameters:
message: "#^Parameter \\#3 \\$depth of function json_encode expects int\\<1, max\\>, int given\\.$#"
count: 1
path: src/Utils.php

-
message: "#^Class CurlHandle not found\\.$#"
count: 3
path: src/Handler/EasyHandle.php
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
124 changes: 104 additions & 20 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 array Received HTTP headers so far
* @var RequestInterface Request being sent
*/
public $headers = [];
private $request;

/**
* @var ResponseInterface|null Received response (if any)
* @var array Request options
*/
public $response;
private $options = [];

/**
* @var RequestInterface Request being sent
* @var int cURL error number (if any)
*/
public $request;
private $errno = \CURLE_OK;

/**
* @var array Request options
* @var array Received HTTP headers so far
*/
public $options = [];
private $headers = [];

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

/**
* @var \Throwable|null Exception during on_headers (if any)
*/
public $onHeadersException;
private $onHeadersException;

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

/**
* @var \Exception|null Exception during createResponse (if any)
* @var bool Tells if the EasyHandle has been initialized
*/
public $createResponseException;
private $initialized = false;

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

/**
* @param string $name
*
* @return void
* @return mixed
*
* @throws \BadMethodCallException
*/
public function __get($name)
public function &__get(string $name)
{
$msg = $name === 'handle' ? 'The EasyHandle has been released' : 'Invalid property: '.$name;
if (('handle' !== $name && property_exists($this, $name)) || $this->initialized && isset($this->handle)) {
return $this->{$name};
}

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

throw new \BadMethodCallException($msg);
}

/**
* @param mixed $value
*
* @throws \UnexpectedValueException|\LogicException|\TypeError
*/
public function __set(string $name, $value): void
{
if ($this->initialized && !isset($this->handle)) {
throw new \UnexpectedValueException('The EasyHandle has been released, please use a new EasyHandle instead.');
}

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 / 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));
}
} 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)) {
throw new \UnexpectedValueException(sprintf('Property %s::$%s could not be set when there isn\'t a valid handle.', __CLASS__, $name));
}

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 \Error
*/
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 267b28b

Please sign in to comment.