Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.0] Update EasyHandle in order to avoid bad writes of properties related to the handle #2293

Open
wants to merge 1 commit into
base: 8.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
3 changes: 3 additions & 0 deletions psalm-baseline.xml
Expand Up @@ -118,6 +118,9 @@
<InvalidReturnType>
<code>void</code>
</InvalidReturnType>
<UndefinedClass>
<code>\CurlHandle</code>
</UndefinedClass>
<UndefinedDocblockClass>
<code>resource|\CurlHandle</code>
</UndefinedDocblockClass>
Expand Down
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) {
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});
}
}