mirror of
https://github.com/php-curl-class/php-curl-class.git
synced 2026-08-17 16:53:34 +00:00
Implement CookieArray for handling decoded cookie name lookup (#1078)
This commit is contained in:
@@ -56,7 +56,7 @@ class CaseInsensitiveArray implements \ArrayAccess, \Countable, \Iterator
|
||||
*
|
||||
* Set data at a specified offset. Converts the offset to lowercase, and
|
||||
* stores the case-sensitive offset and the data at the lowercase indexes in
|
||||
* $this->keys and @this->data.
|
||||
* $this->keys and $this->data.
|
||||
*
|
||||
* @param string $offset The offset to store the data at (case-insensitive).
|
||||
* @param mixed $value The data to store at the specified offset.
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Curl;
|
||||
|
||||
/**
|
||||
* Cookie Array
|
||||
*
|
||||
* An array-like container for response cookies that transparently decodes
|
||||
* percent-encoded keys on access. Keys are normalized with rawurldecode()
|
||||
* on both storage and lookup, so an entry stored as "a;b" can be retrieved
|
||||
* via either $cookies['a;b'] or $cookies['a%3Bb'].
|
||||
*
|
||||
* @psalm-suppress MissingTemplateParam
|
||||
*/
|
||||
class CookieArray implements \ArrayAccess, \Countable, \Iterator
|
||||
{
|
||||
/**
|
||||
* @var mixed[] Data storage with url-decoded keys.
|
||||
*
|
||||
* @see offsetSet()
|
||||
* @see offsetExists()
|
||||
* @see offsetUnset()
|
||||
* @see offsetGet()
|
||||
* @see count()
|
||||
* @see current()
|
||||
* @see next()
|
||||
* @see key()
|
||||
* @see valid()
|
||||
* @see rewind()
|
||||
*/
|
||||
private $data = [];
|
||||
|
||||
/**
|
||||
* Offset Set
|
||||
*
|
||||
* Set data at a specified offset. Decodes the offset using rawurldecode
|
||||
* and stores the data at the decoded index.
|
||||
*
|
||||
* @param string $offset The offset to store the data at.
|
||||
* @param mixed $value The data to store at the specified offset.
|
||||
* @return void
|
||||
* @see https://www.php.net/manual/en/arrayaccess.offsetset.php
|
||||
*/
|
||||
#[\Override]
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
if ($offset === null) {
|
||||
$this->data[] = $value;
|
||||
} else {
|
||||
$this->data[rawurldecode((string) $offset)] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset Exists
|
||||
*
|
||||
* Checks if the offset exists in data storage. The index is looked up with
|
||||
* the url-decoded version of the provided offset.
|
||||
*
|
||||
* @param string $offset The offset to check.
|
||||
* @return bool If the offset exists.
|
||||
* @see https://www.php.net/manual/en/arrayaccess.offsetexists.php
|
||||
*/
|
||||
#[\Override]
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetExists($offset)
|
||||
{
|
||||
return array_key_exists(rawurldecode((string) $offset), $this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset Unset
|
||||
*
|
||||
* Unsets the specified offset. Decodes the provided offset using
|
||||
* rawurldecode and unsets the stored data.
|
||||
*
|
||||
* @param string $offset The offset to unset.
|
||||
* @return void
|
||||
* @see https://www.php.net/manual/en/arrayaccess.offsetunset.php
|
||||
*/
|
||||
#[\Override]
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetUnset($offset)
|
||||
{
|
||||
unset($this->data[rawurldecode((string) $offset)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset Get
|
||||
*
|
||||
* Return the stored data at the provided offset. The offset is decoded
|
||||
* using rawurldecode and the lookup is done on the data store directly.
|
||||
*
|
||||
* @param string $offset The offset to look up.
|
||||
* @return mixed The data stored at the offset.
|
||||
* @see https://www.php.net/manual/en/arrayaccess.offsetget.php
|
||||
*/
|
||||
#[\Override]
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
return $this->data[rawurldecode((string) $offset)] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count
|
||||
*
|
||||
* @return int The number of elements stored in the array.
|
||||
* @see https://www.php.net/manual/en/countable.count.php
|
||||
*/
|
||||
#[\Override]
|
||||
#[\ReturnTypeWillChange]
|
||||
public function count()
|
||||
{
|
||||
return count($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Current
|
||||
*
|
||||
* @return mixed Data at the current position.
|
||||
* @see https://www.php.net/manual/en/iterator.current.php
|
||||
*/
|
||||
#[\Override]
|
||||
#[\ReturnTypeWillChange]
|
||||
public function current()
|
||||
{
|
||||
return current($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Next
|
||||
*
|
||||
* @return void
|
||||
* @see https://www.php.net/manual/en/iterator.next.php
|
||||
*/
|
||||
#[\Override]
|
||||
#[\ReturnTypeWillChange]
|
||||
public function next()
|
||||
{
|
||||
next($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Key
|
||||
*
|
||||
* @return mixed Decoded key at current position.
|
||||
* @see https://www.php.net/manual/en/iterator.key.php
|
||||
*/
|
||||
#[\Override]
|
||||
#[\ReturnTypeWillChange]
|
||||
public function key()
|
||||
{
|
||||
return key($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid
|
||||
*
|
||||
* @return bool If the current position is valid.
|
||||
* @see https://www.php.net/manual/en/iterator.valid.php
|
||||
*/
|
||||
#[\Override]
|
||||
#[\ReturnTypeWillChange]
|
||||
public function valid()
|
||||
{
|
||||
return (key($this->data) !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind
|
||||
*
|
||||
* @return void
|
||||
* @see https://www.php.net/manual/en/iterator.rewind.php
|
||||
*/
|
||||
#[\Override]
|
||||
#[\ReturnTypeWillChange]
|
||||
public function rewind()
|
||||
{
|
||||
reset($this->data);
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -29,7 +29,7 @@ class Curl extends BaseCurl
|
||||
|
||||
public $responseHeaders = null;
|
||||
public $rawResponseHeaders = '';
|
||||
public $responseCookies = [];
|
||||
public $responseCookies = null;
|
||||
public $response = null;
|
||||
public $rawResponse = null;
|
||||
|
||||
@@ -489,7 +489,7 @@ class Curl extends BaseCurl
|
||||
}
|
||||
|
||||
if ($ch === null) {
|
||||
$this->responseCookies = [];
|
||||
$this->responseCookies = new CookieArray();
|
||||
$this->call($this->beforeSendCallback);
|
||||
$this->rawResponse = curl_exec($this->curl);
|
||||
$this->curlErrorCode = curl_errno($this->curl);
|
||||
@@ -511,7 +511,7 @@ class Curl extends BaseCurl
|
||||
$this->rawResponseHeaders = $this->headerCallbackData->rawResponseHeaders;
|
||||
$this->responseCookies = $this->headerCallbackData->responseCookies;
|
||||
$this->headerCallbackData->rawResponseHeaders = '';
|
||||
$this->headerCallbackData->responseCookies = [];
|
||||
$this->headerCallbackData->responseCookies = new CookieArray();
|
||||
$this->headerCallbackData->stopRequestDecider = null;
|
||||
$this->headerCallbackData->stopRequest = false;
|
||||
|
||||
@@ -2150,7 +2150,7 @@ class Curl extends BaseCurl
|
||||
// Create a placeholder to temporarily store the header callback data.
|
||||
$header_callback_data = new \stdClass();
|
||||
$header_callback_data->rawResponseHeaders = '';
|
||||
$header_callback_data->responseCookies = [];
|
||||
$header_callback_data->responseCookies = new CookieArray();
|
||||
$header_callback_data->stopRequestDecider = null;
|
||||
$header_callback_data->stopRequest = false;
|
||||
$this->headerCallbackData = $header_callback_data;
|
||||
@@ -2159,6 +2159,7 @@ class Curl extends BaseCurl
|
||||
|
||||
$this->setOptInternal(CURLOPT_RETURNTRANSFER, true);
|
||||
$this->headers = new CaseInsensitiveArray();
|
||||
$this->responseCookies = new CookieArray();
|
||||
|
||||
if ($base_url !== null) {
|
||||
$this->setUrl($base_url);
|
||||
@@ -2230,7 +2231,7 @@ function createHeaderCallback($header_callback_data)
|
||||
{
|
||||
return function ($ch, $header) use ($header_callback_data) {
|
||||
if (preg_match('/^Set-Cookie:\s*([^=]+)=([^;]+)/mi', $header, $cookie) === 1) {
|
||||
$header_callback_data->responseCookies[$cookie[1]] = trim($cookie[2], " \n\r\t\0\x0B");
|
||||
$header_callback_data->responseCookies[$cookie[1]] = rawurldecode(trim($cookie[2], " \n\r\t\0\x0B"));
|
||||
}
|
||||
|
||||
if ($header_callback_data->stopRequestDecider !== null) {
|
||||
|
||||
@@ -1238,6 +1238,49 @@ class PHPCurlClassTest extends \PHPUnit\Framework\TestCase
|
||||
$this->assertEquals('mouthwatering', $test->curl->responseCookies['cookie2']);
|
||||
}
|
||||
|
||||
public function testEncodedCookieResponse()
|
||||
{
|
||||
$test = new Test();
|
||||
$test->server('encoded_cookie', 'GET');
|
||||
|
||||
// Ensure decoded key lookup returns decoded values.
|
||||
$this->assertEquals('foo;bar', $test->curl->getCookie('dingus'));
|
||||
$this->assertEquals('foo;bar', $test->curl->getCookie('a;b'));
|
||||
|
||||
// Ensure encoded key lookup still works for backwards compatibility.
|
||||
$this->assertEquals('foo;bar', $test->curl->getCookie('a%3Bb'));
|
||||
|
||||
// Ensure percent-encoded spaces in values are decoded.
|
||||
$this->assertEquals('hello world', $test->curl->getCookie('spaced'));
|
||||
|
||||
// Ensure missing cookies return null.
|
||||
$this->assertNull($test->curl->getCookie('nonexistent'));
|
||||
|
||||
// Ensure direct property access works with both decoded and encoded keys.
|
||||
$this->assertEquals('foo;bar', $test->curl->responseCookies['a;b']);
|
||||
$this->assertEquals('foo;bar', $test->curl->responseCookies['a%3Bb']);
|
||||
|
||||
// Ensure isset() works with both decoded and encoded keys.
|
||||
$this->assertTrue(isset($test->curl->responseCookies['a;b']));
|
||||
$this->assertTrue(isset($test->curl->responseCookies['a%3Bb']));
|
||||
$this->assertFalse(isset($test->curl->responseCookies['nonexistent']));
|
||||
|
||||
// Ensure iteration yields decoded keys only.
|
||||
$iterated_keys = [];
|
||||
foreach ($test->curl->responseCookies as $key => $value) {
|
||||
$iterated_keys[] = $key;
|
||||
}
|
||||
$this->assertEqualsCanonicalizing(['dingus', 'a;b', 'spaced'], $iterated_keys);
|
||||
|
||||
// Ensure unset() via the encoded key removes the decoded entry.
|
||||
unset($test->curl->responseCookies['a%3Bb']);
|
||||
$this->assertFalse(isset($test->curl->responseCookies['a;b']));
|
||||
$this->assertFalse(isset($test->curl->responseCookies['a%3Bb']));
|
||||
|
||||
// Ensure responseCookies stores decoded keys only without duplicates.
|
||||
$this->assertCount(2, $test->curl->getResponseCookies());
|
||||
}
|
||||
|
||||
public function testDefaultTimeout()
|
||||
{
|
||||
if ($this->skip_slow_tests) {
|
||||
|
||||
@@ -176,6 +176,11 @@ if ($test === 'http_basic_auth') {
|
||||
setcookie('cookie1', 'scrumptious');
|
||||
setcookie('cookie2', 'mouthwatering');
|
||||
exit;
|
||||
} elseif ($test === 'encoded_cookie') {
|
||||
header('Set-Cookie: dingus=foo%3Bbar; path=/');
|
||||
header('Set-Cookie: a%3Bb=foo%3Bbar; path=/', false);
|
||||
header('Set-Cookie: spaced=hello%20world; path=/', false);
|
||||
exit;
|
||||
} elseif ($test === 'response_header') {
|
||||
header('Content-Type: application/json');
|
||||
header('ETag: ' . md5('worldpeace'));
|
||||
|
||||
Reference in New Issue
Block a user