Skip to content

Commit

Permalink
Merge pull request #10466 from cakephp/3next-client-cookie-collection
Browse files Browse the repository at this point in the history
3.next - Use CookieCollection in Client responses
  • Loading branch information
markstory committed Apr 3, 2017
2 parents 9abae69 + 3c95282 commit 0a5c9eb
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 62 deletions.
10 changes: 1 addition & 9 deletions src/Http/Client/CookieCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,7 @@ public function getAll()
{
$out = [];
foreach ($this->cookies as $cookie) {
$out[] = [
'name' => $cookie->getName(),
'value' => $cookie->getValue(),
'path' => $cookie->getPath(),
'domain' => $cookie->getDomain(),
'secure' => $cookie->isSecure(),
'httponly' => $cookie->isHttpOnly(),
'expires' => $cookie->getExpiry()
];
$out[] = $cookie->toArray();
}

return $out;
Expand Down
113 changes: 64 additions & 49 deletions src/Http/Client/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
*/
namespace Cake\Http\Client;

// This alias is necessary to avoid class name conflicts
// with the deprecated class in this namespace.
use Cake\Http\Cookie\CookieCollection as CookiesCollection;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;
use Zend\Diactoros\MessageTrait;
Expand Down Expand Up @@ -102,6 +105,13 @@ class Response extends Message implements ResponseInterface
*/
protected $code;

/**
* Cookie Collection instance
*
* @var \Cake\Http\Cookie\CookieCollection
*/
protected $cookies;

/**
* The reason phrase for the status code
*
Expand Down Expand Up @@ -129,7 +139,7 @@ class Response extends Message implements ResponseInterface
* @var array
*/
protected $_exposedProperties = [
'cookies' => '_cookies',
'cookies' => '_getCookies',
'body' => '_getBody',
'code' => 'code',
'json' => '_getJson',
Expand Down Expand Up @@ -204,9 +214,6 @@ protected function _parseHeaders($headers)
$name = trim($name);

$normalized = strtolower($name);
if ($normalized === 'set-cookie') {
$this->_parseCookie($value);
}

if (isset($this->headers[$name])) {
$this->headers[$name][] = $value;
Expand All @@ -217,46 +224,6 @@ protected function _parseHeaders($headers)
}
}

/**
* Parse a cookie header into data.
*
* @param string $value The cookie value to parse.
* @return void
*/
protected function _parseCookie($value)
{
$value = rtrim($value, ';');
$nestedSemi = '";"';
if (strpos($value, $nestedSemi) !== false) {
$value = str_replace($nestedSemi, "{__cookie_replace__}", $value);
$parts = explode(';', $value);
$parts = str_replace("{__cookie_replace__}", $nestedSemi, $parts);
} else {
$parts = preg_split('/\;[ \t]*/', $value);
}

$name = false;
foreach ($parts as $i => $part) {
if (strpos($part, '=') !== false) {
list($key, $value) = explode('=', $part, 2);
} else {
$key = $part;
$value = true;
}
if ($i === 0) {
$name = $key;
$cookie['value'] = $value;
continue;
}
$key = strtolower($key);
if (!isset($cookie[$key])) {
$cookie[$key] = $value;
}
}
$cookie['name'] = $name;
$this->_cookies[$name] = $cookie;
}

/**
* Check if the response was OK
*
Expand Down Expand Up @@ -427,7 +394,22 @@ public function cookie($name = null, $all = false)
*/
public function getCookies()
{
return $this->_cookies;
return $this->_getCookies();
}

/**
* Get the cookie collection from this response.
*
* This method exposes the response's CookieCollection
* instance allowing you to interact with cookie objects directly.
*
* @return \Cake\Http\Cookie\CookieCollection
*/
public function getCookieCollection()
{
$this->buildCookieCollection();

return $this->cookies;
}

/**
Expand All @@ -438,11 +420,12 @@ public function getCookies()
*/
public function getCookie($name)
{
if (!isset($this->_cookies[$name])) {
$this->buildCookieCollection();
if (!$this->cookies->has($name)) {
return null;
}

return $this->_cookies[$name]['value'];
return $this->cookies->get($name)->getValue();
}

/**
Expand All @@ -453,11 +436,43 @@ public function getCookie($name)
*/
public function getCookieData($name)
{
if (!isset($this->_cookies[$name])) {
$this->buildCookieCollection();

if (!$this->cookies->has($name)) {
return null;
}

return $this->_cookies[$name];
return $this->cookies->get($name)->toArrayCompat();
}

/**
* Lazily build the CookieCollection and cookie objects from the response header
*
* @return void
*/
protected function buildCookieCollection()
{
if ($this->cookies) {
return;
}
$this->cookies = CookiesCollection::createFromHeader($this->getHeader('Set-Cookie'));
}

/**
* Property accessor for `$this->cookies`
*
* @return array Array of Cookie data.
*/
protected function _getCookies()
{
$this->buildCookieCollection();

$cookies = [];
foreach ($this->cookies as $cookie) {
$cookies[$cookie->getName()] = $cookie->toArrayCompat();
}

return $cookies;
}

/**
Expand Down
50 changes: 49 additions & 1 deletion src/Http/Cookie/Cookie.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ class Cookie implements CookieInterface

use CookieCryptTrait;

/**
* Expires attribute format.
*
* @var string
*/
const EXPIRES_FORMAT = 'D, d-M-Y H:i:s T';

/**
* Cookie name
*
Expand Down Expand Up @@ -160,7 +167,7 @@ protected function _buildExpirationValue()
{
return sprintf(
'expires=%s',
gmdate('D, d-M-Y H:i:s T', $this->expiresAt)
gmdate(static::EXPIRES_FORMAT, $this->expiresAt)
);
}

Expand Down Expand Up @@ -596,6 +603,47 @@ public function isExpanded()
return $this->isExpanded;
}

/**
* Convert the cookie into an array of its properties.
*
* Primarily useful where backwards compatibility is needed.
*
* @return array
*/
public function toArray()
{
return [
'name' => $this->getName(),
'value' => $this->getValue(),
'path' => $this->getPath(),
'domain' => $this->getDomain(),
'secure' => $this->isSecure(),
'httponly' => $this->isHttpOnly(),
'expires' => $this->getExpiry()
];
}

/**
* Convert the cookie into an array of its properties.
*
* This method is compatible with older client code that
* expects date strings instead of timestamps.
*
* @return array
*/
public function toArrayCompat()
{
return [
'name' => $this->getName(),
'value' => $this->getValue(),
'path' => $this->getPath(),
'domain' => $this->getDomain(),
'secure' => $this->isSecure(),
'httponly' => $this->isHttpOnly(),
'expires' => gmdate(static::EXPIRES_FORMAT, $this->expiresAt)
];
}

/**
* Implode method to keep keys are multidimensional arrays
*
Expand Down
18 changes: 15 additions & 3 deletions src/Http/Cookie/CookieCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ public function __construct(array $cookies = [])
}
}

/**
* Create a Cookie Collection from an array of Set-Cookie Headers
*
* @param array $header The array of set-cookie header values.
* @return static
*/
public static function createFromHeader(array $header)
{
$cookies = static::parseSetCookieHeader($header);

return new CookieCollection($cookies);
}

/**
* Get the number of cookies in the collection.
*
Expand Down Expand Up @@ -251,8 +264,7 @@ public function addFromResponse(ResponseInterface $response, RequestInterface $r
$host = $uri->getHost();
$path = $uri->getPath() ?: '/';

$header = $response->getHeader('Set-Cookie');
$cookies = $this->parseSetCookieHeader($header);
$cookies = static::parseSetCookieHeader($response->getHeader('Set-Cookie'));
$cookies = $this->setRequestDefaults($cookies, $host, $path);
$new = clone $this;
foreach ($cookies as $cookie) {
Expand Down Expand Up @@ -293,7 +305,7 @@ protected function setRequestDefaults(array $cookies, $host, $path)
* @param array $values List of Set-Cookie Header values.
* @return \Cake\Http\Cookie\Cookie[] An array of cookie objects
*/
protected function parseSetCookieHeader($values)
protected static function parseSetCookieHeader($values)
{
$cookies = [];
foreach ($values as $value) {
Expand Down
24 changes: 24 additions & 0 deletions tests/TestCase/Http/Client/ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace Cake\Test\TestCase\Http\Client;

use Cake\Http\Client\Response;
use Cake\Http\Cookie\CookieCollection;
use Cake\TestSuite\TestCase;

/**
Expand Down Expand Up @@ -330,6 +331,29 @@ public function testGetCookies()
$this->assertArrayHasKey('expiring', $result);
}

/**
* Test accessing cookie collection
*
* @return void
*/
public function testGetCookieCollection()
{
$headers = [
'HTTP/1.0 200 Ok',
'Set-Cookie: test=value',
'Set-Cookie: session=123abc',
'Set-Cookie: expiring=soon; Expires=Wed, 09-Jun-2021 10:18:14 GMT; Path=/; HttpOnly; Secure;',
];
$response = new Response($headers, '');

$cookies = $response->getCookieCollection();
$this->assertInstanceOf(CookieCollection::class, $cookies);
$this->assertTrue($cookies->has('test'));
$this->assertTrue($cookies->has('session'));
$this->assertTrue($cookies->has('expiring'));
$this->assertSame('123abc', $cookies->get('session')->getValue());
}

/**
* Test statusCode()
*
Expand Down
19 changes: 19 additions & 0 deletions tests/TestCase/Http/Cookie/CookieCollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -403,4 +403,23 @@ public function testAddToRequestSecureCrumb()
$request = $collection->addToRequest($request);
$this->assertSame('public=b', $request->getHeaderLine('Cookie'));
}

/**
* test createFromHeader() building cookies from a header string.
*
* @return void
*/
public function testCreateFromHeader()
{
$header = [
'http=name; HttpOnly; Secure;',
'expires=expiring; Expires=Wed, 15-Jun-2022 10:22:22; Path=/api; HttpOnly; Secure;',
'expired=expired; Expires=Wed, 15-Jun-2015 10:22:22;',
];
$cookies = CookieCollection::createFromHeader($header);
$this->assertCount(3, $cookies);
$this->assertTrue($cookies->has('http'));
$this->assertTrue($cookies->has('expires'));
$this->assertTrue($cookies->has('expired'), 'Expired cookies should be present');
}
}
Loading

0 comments on commit 0a5c9eb

Please sign in to comment.