Skip to content

Commit

Permalink
Update Client to use new Cookie jar object.
Browse files Browse the repository at this point in the history
This makes the cookie implementation better follow the specs,
and reduces the internal complexity of Client as well.
  • Loading branch information
markstory committed Jan 14, 2013
1 parent c1a93e9 commit 4c3c1b2
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 243 deletions.
86 changes: 19 additions & 67 deletions lib/Cake/Network/Http/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use Cake\Core\App;
use Cake\Error;
use Cake\Network\Http\Cookies;
use Cake\Network\Http\Request;
use Cake\Network\Http\Response;
use Cake\Utility\Hash;
Expand Down Expand Up @@ -47,6 +48,10 @@
* `Expires`, `Path` and `Domain` attributes. You can get the list of
* currently stored cookies using the cookies() method.
*
* You can use the 'cookieJar' constructor option to provide a custom
* cookie jar instance you've restored from cache/disk. By default
* an empty instance of Cake\Network\Http\Cookies will be created.
*
* ### Sending request bodies
*
* By default any POST/PUT/PATCH/DELETE request with $data will
Expand Down Expand Up @@ -104,9 +109,9 @@ class Client {
* Cookies are indexed by the cookie's domain or
* request host name.
*
* @var array
* @var Cake\Network\Http\Cookies
*/
protected $_cookies = [];
protected $_cookies;

/**
* Adapter for sending requests. Defaults to
Expand Down Expand Up @@ -143,6 +148,14 @@ public function __construct($config = []) {
$adapter = $config['adapter'];
unset($config['adapter']);
}
if (isset($config['cookieJar'])) {
$this->_cookies = $config['cookieJar'];
unset($config['cookieJar']);
}
if (empty($this->_cookies)) {
$this->_cookies = new Cookies();
}

$this->config($config);

if (is_string($adapter)) {
Expand Down Expand Up @@ -173,7 +186,7 @@ public function config($config = null) {
*
* Returns an array of cookie data arrays.
*
* @return array
* @return Cake\Network\Http\Cookies
*/
public function cookies() {
return $this->_cookies;
Expand Down Expand Up @@ -302,74 +315,13 @@ protected function _mergeOptions($options) {
*/
public function send(Request $request, $options = []) {
$responses = $this->_adapter->send($request, $options);
$host = parse_url($request->url(), PHP_URL_HOST);
$url = $request->url();
foreach ($responses as $response) {
$this->_storeCookies($response, $host);
$this->_cookies->store($response, $url);
}
return array_pop($responses);
}

/**
* Store cookies in a response to be used in future requests.
*
* Non-expired cookies will be stored for use in future requests
* made with the same Client instance. Cookies are not saved
* between instances.
*
* @param Response $response The response to read cookies from
* @param string $host The request host, used for getting host names
* in case the cookies didn't set a domain.
* @return void
*/
protected function _storeCookies(Response $response, $host) {
$cookies = $response->cookies();
foreach ($cookies as $name => $cookie) {
$expires = isset($cookie['expires']) ? $cookie['expires'] : false;
if ($expires) {
$expires = \DateTime::createFromFormat('D, j-M-Y H:i:s e', $expires);
}
if ($expires && $expires->getTimestamp() <= time()) {
continue;
}
if (empty($cookie['domain'])) {
$cookie['domain'] = $host;
}
$cookie['domain'] = trim($cookie['domain'], '.');
if (empty($cookie['path'])) {
$cookie['path'] = '/';
}
$this->_cookies[] = $cookie;
}
}

/**
* Adds cookies stored in the client to the request.
*
* Uses the request's host to find matching cookies.
* Walks backwards through subdomains to find cookies
* defined on parent domains.
*
* @param Request $request
* @return void
*/
protected function _addCookies(Request $request) {
$url = $request->url();
$path = parse_url($url, PHP_URL_PATH);
$parts = explode('.', parse_url($url, PHP_URL_HOST));
$domains = [];
for ($i = 0, $len = count($parts); $i < $len - 1; $i++) {
$domains[] = implode('.', array_slice($parts, $i));
}
foreach ($this->_cookies as $cookie) {
if (
in_array($cookie['domain'], $domains) &&
strpos($path, $cookie['path']) === 0
) {
$request->cookie($cookie['name'], $cookie['value']);
}
}
}

/**
* Generate a URL based on the scoped client options.
*
Expand Down Expand Up @@ -427,7 +379,7 @@ protected function _createRequest($method, $url, $data, $options) {
if (isset($options['headers'])) {
$request->header($options['headers']);
}
$this->_addCookies($request);
$request->cookie($this->_cookies->get($url));
if (isset($options['cookies'])) {
$request->cookie($options['cookies']);
}
Expand Down
193 changes: 17 additions & 176 deletions lib/Cake/Test/TestCase/Network/Http/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -405,203 +405,44 @@ public function testExceptionOnUnknownType() {
}

/**
* Test that Client keeps cookies from responses and holds onto non expired cookies.
* Test that Client stores cookies
*
* @return void
*/
public function testCookiesExpiring() {
$mock = $this->getMock(
public function testCookieStorage() {
$adapter = $this->getMock(
'Cake\Network\Http\Adapter\Stream',
['send']
);
$cookieJar = $this->getMock('Cake\Network\Http\Cookies');

$firstHeaders = [
$headers = [
'HTTP/1.0 200 Ok',
'Set-Cookie: first=1',
'Set-Cookie: expiring=now; Expires=Wed, 09-Jun-1999 10:18:14 GMT'
];
$firstResponse = new Response($firstHeaders, '');

$secondHeaders = [
'HTTP/1.0 200 Ok',
'Set-Cookie: second=2',
];
$secondResponse = new Response($secondHeaders, '');

$mock->expects($this->at(0))
->method('send')
->will($this->returnValue([$firstResponse]));

$mock->expects($this->at(1))
->method('send')
->with($this->attributeEqualTo(
'_cookies',
['first' => '1']
))
->will($this->returnValue([$secondResponse]));

$http = new Client([
'host' => 'cakephp.org',
'adapter' => $mock
]);

$http->get('/projects');
$http->get('/projects/two');

$result = $http->cookies();
$expected = [
[
'name' => 'first',
'value' => '1',
'domain' => 'cakephp.org',
'path' => '/'
],
[
'name' => 'second',
'value' => '2',
'domain' => 'cakephp.org',
'path' => '/'
],
];
$this->assertEquals($expected, $result);
}

/**
* Test cookies with domain set.
*
* @return void
*/
public function testCookiesWithDomain() {
$firstHeaders = [
'HTTP/1.0 200 Ok',
'Set-Cookie: first=1; Domain=.cakephp.org',
'Set-Cookie: second=2',
];
$firstResponse = new Response($firstHeaders, '');

$secondHeaders = [
'HTTP/1.0 200 Ok',
'Set-Cookie: third=3',
];
$secondResponse = new Response($secondHeaders, '');
$thirdResponse = new Response([], '');

$mock = $this->getMock(
'Cake\Network\Http\Adapter\Stream',
['send']
);

$mock->expects($this->at(0))
->method('send')
->will($this->returnValue([$firstResponse]));

$mock->expects($this->at(1))
->method('send')
->with($this->attributeEqualTo(
'_cookies',
['first' => '1']
))
->will($this->returnValue([$secondResponse]));

$mock->expects($this->at(2))
->method('send')
->with($this->attributeEqualTo(
'_cookies',
['first' => '1', 'second' => '2', 'third' => '3']
))
->will($this->returnValue([$thirdResponse]));

$http = new Client([
'host' => 'test.cakephp.org',
'adapter' => $mock
]);

$http->get('/projects');
$http->get('http://cakephp.org/versions');
$http->get('/projects');

$result = $http->cookies();
$expected = [
[
'domain' => 'cakephp.org',
'path' => '/',
'name' => 'first',
'value' => 1,
],
[
'domain' => 'test.cakephp.org',
'path' => '/',
'name' => 'second',
'value' => 2,
],
[
'domain' => 'cakephp.org',
'path' => '/',
'name' => 'third',
'value' => 3,
],
];
$this->assertEquals($expected, $result);
}

/**
* Test path based cookies
*
* @return void
*/
public function testCookiesWithPath() {
$firstHeaders = [
'HTTP/1.0 200 Ok',
'Set-Cookie: first=1; Domain=.cakephp.org; Path=/projects',
];
$firstResponse = new Response($firstHeaders, '');

$secondResponse = new Response([], '');

$mock = $this->getMock(
'Cake\Network\Http\Adapter\Stream',
['send']
);
$response = new Response($headers, '');

$mock->expects($this->at(0))
->method('send')
->will($this->returnValue([$firstResponse]));
$cookieJar->expects($this->at(0))
->method('get')
->with('http://cakephp.org/projects')
->will($this->returnValue([]));

$mock->expects($this->at(1))
->method('send')
->with($this->attributeEqualTo(
'_cookies',
[]
))
->will($this->returnValue([$secondResponse]));
$cookieJar->expects($this->at(1))
->method('store')
->with($response);

$mock->expects($this->at(2))
$adapter->expects($this->at(0))
->method('send')
->with($this->attributeEqualTo(
'_cookies',
['first' => '1']
))
->will($this->returnValue([$secondResponse]));
->will($this->returnValue([$response]));

$http = new Client([
'host' => 'cakephp.org',
'adapter' => $mock
'adapter' => $adapter,
'cookieJar' => $cookieJar
]);

$http->get('/projects');
$http->get('/versions');
$http->get('/projects/foo');

$result = $http->cookies();
$expected = [
[
'domain' => 'cakephp.org',
'name' => 'first',
'value' => 1,
'path' => '/projects'
]
];
$this->assertEquals($expected, $result);
}

}

0 comments on commit 4c3c1b2

Please sign in to comment.