Skip to content

Commit 29ef4f3

Browse files
committed
Implement cookie handling.
Client now manages cookies from responses and appends them to future requests.
1 parent 521896f commit 29ef4f3

File tree

2 files changed

+191
-8
lines changed

2 files changed

+191
-8
lines changed

lib/Cake/Network/Http/Client.php

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@
3939
* - delete()
4040
* - patch()
4141
*
42+
* ### Cookie management
43+
*
44+
* Client will maintain cookies from the responses done with
45+
* a client instance. These cookies will be automatically added
46+
* to future requests to matching hosts. Cookies will respect the
47+
* `Expires` and `Domain` attributes. You can get the list of
48+
* currently stored cookies using the cookies() method.
49+
*
4250
* ### Sending request bodies
4351
*
4452
* By default any POST/PUT/PATCH/DELETE request with $data will
@@ -67,12 +75,9 @@
6775
* ### Using proxies
6876
*
6977
* By using the `proxy` key you can set authentication credentials for
70-
* a proxy if you need to use one.. The type sub option can be used to
78+
* a proxy if you need to use one.. The type sub option can be used to
7179
* specify which authentication strategy you want to use.
72-
* CakePHP comes with a few built-in strategies:
73-
*
74-
* - Basic
75-
* - Digest
80+
* CakePHP comes with built-in support for basic authentication.
7681
*
7782
*/
7883
class Client {
@@ -93,6 +98,16 @@ class Client {
9398
'redirect' => false,
9499
];
95100

101+
/**
102+
* List of cookies from responses made with this client.
103+
*
104+
* Cookies are indexed by the cookie's domain or
105+
* request host name.
106+
*
107+
* @var array
108+
*/
109+
protected $_cookies = [];
110+
96111
/**
97112
* Adapter for sending requests. Defaults to
98113
* Cake\Network\Http\Stream
@@ -153,6 +168,15 @@ public function config($config = null) {
153168
return $this;
154169
}
155170

171+
/**
172+
* Get the cookies stored in the Client.
173+
*
174+
* @return array
175+
*/
176+
public function cookies() {
177+
return $this->_cookies;
178+
}
179+
156180
/**
157181
* Do a GET request.
158182
*
@@ -275,13 +299,60 @@ protected function _mergeOptions($options) {
275299
* @return Cake\Network\Http\Response
276300
*/
277301
public function send(Request $request, $options = []) {
278-
// TODO possibly implment support for
279-
// holding onto cookies so subsequent requests
280-
// can share cookies.
281302
$responses = $this->_adapter->send($request, $options);
303+
$host = parse_url($request->url(), PHP_URL_HOST);
304+
foreach ($responses as $response) {
305+
$this->_storeCookies($response, $host);
306+
}
282307
return array_pop($responses);
283308
}
284309

310+
/**
311+
* Store cookies in a response to be used in future requests.
312+
*
313+
* Non-expired cookies will be stored for use in future requests
314+
* made with the same Client instance. Cookies are not saved
315+
* between instances.
316+
*
317+
* @param Response $response The response to read cookies from
318+
* @param string $host The request host, used for getting host names
319+
* in case the cookies didn't set a domain.
320+
* @return void
321+
*/
322+
protected function _storeCookies(Response $response, $host) {
323+
$cookies = $response->cookies();
324+
foreach ($cookies as $name => $cookie) {
325+
$expires = isset($cookie['expires']) ? $cookie['expires'] : false;
326+
$domain = isset($cookie['domain']) ? $cookie['domain'] : $host;
327+
$domain = trim($domain, '.');
328+
if ($expires) {
329+
$expires = \DateTime::createFromFormat('D, j-M-Y H:i:s e', $expires);
330+
}
331+
if ($expires && $expires->getTimestamp() <= time()) {
332+
continue;
333+
}
334+
if (empty($this->_cookies[$domain])) {
335+
$this->_cookies[$domain] = [];
336+
}
337+
$this->_cookies[$domain][$name] = $cookie['value'];
338+
}
339+
}
340+
341+
/**
342+
* Adds cookies stored in the client to the request.
343+
*
344+
* Uses the request's host to find matching cookies.
345+
*
346+
* @param Request $request
347+
* @return void
348+
*/
349+
protected function _addCookies(Request $request) {
350+
$host = parse_url($request->url(), PHP_URL_HOST);
351+
if (isset($this->_cookies[$host])) {
352+
$request->cookie($this->_cookies[$host]);
353+
}
354+
}
355+
285356
/**
286357
* Generate a URL based on the scoped client options.
287358
*
@@ -339,6 +410,7 @@ protected function _createRequest($method, $url, $data, $options) {
339410
if (isset($options['headers'])) {
340411
$request->header($options['headers']);
341412
}
413+
$this->_addCookies($request);
342414
if (isset($options['cookies'])) {
343415
$request->cookie($options['cookies']);
344416
}

lib/Cake/Test/TestCase/Network/Http/ClientTest.php

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,4 +404,115 @@ public function testExceptionOnUnknownType() {
404404
$http->post('/projects/add', 'it works', ['type' => 'invalid']);
405405
}
406406

407+
/**
408+
* Test that Client keeps cookies from responses and holds onto non expired cookies.
409+
*
410+
* @return void
411+
*/
412+
public function testCookiesExpiring() {
413+
$mock = $this->getMock(
414+
'Cake\Network\Http\Adapter\Stream',
415+
['send']
416+
);
417+
418+
$firstHeaders = [
419+
'HTTP/1.0 200 Ok',
420+
'Set-Cookie: first=1',
421+
'Set-Cookie: expiring=now; Expires=Wed, 09-Jun-1999 10:18:14 GMT'
422+
];
423+
$firstResponse = new Response($firstHeaders, '');
424+
425+
$secondHeaders = [
426+
'HTTP/1.0 200 Ok',
427+
'Set-Cookie: second=2',
428+
];
429+
$secondResponse = new Response($secondHeaders, '');
430+
431+
$mock->expects($this->at(0))
432+
->method('send')
433+
->will($this->returnValue([$firstResponse]));
434+
435+
$mock->expects($this->at(1))
436+
->method('send')
437+
->with($this->attributeEqualTo(
438+
'_cookies',
439+
['first' => '1']
440+
))
441+
->will($this->returnValue([$secondResponse]));
442+
443+
$http = new Client([
444+
'host' => 'cakephp.org',
445+
'adapter' => $mock
446+
]);
447+
448+
$http->get('/projects');
449+
$http->get('/projects/two');
450+
451+
$result = $http->cookies();
452+
$expected = [
453+
'cakephp.org' => [
454+
'first' => 1,
455+
'second' => 2
456+
]
457+
];
458+
$this->assertEquals($expected, $result);
459+
}
460+
461+
/**
462+
* Test cookies with domain set.
463+
*
464+
* @return void
465+
*/
466+
public function testCookiesWithDomain() {
467+
$firstHeaders = [
468+
'HTTP/1.0 200 Ok',
469+
'Set-Cookie: first=1; Domain=.cakephp.org',
470+
'Set-Cookie: second=2',
471+
];
472+
$firstResponse = new Response($firstHeaders, '');
473+
474+
$secondHeaders = [
475+
'HTTP/1.0 200 Ok',
476+
'Set-Cookie: third=3',
477+
];
478+
$secondResponse = new Response($secondHeaders, '');
479+
480+
$mock = $this->getMock(
481+
'Cake\Network\Http\Adapter\Stream',
482+
['send']
483+
);
484+
485+
$mock->expects($this->at(0))
486+
->method('send')
487+
->will($this->returnValue([$firstResponse]));
488+
489+
$mock->expects($this->at(1))
490+
->method('send')
491+
->with($this->attributeEqualTo(
492+
'_cookies',
493+
['first' => '1']
494+
))
495+
->will($this->returnValue([$secondResponse]));
496+
497+
$http = new Client([
498+
'host' => 'test.cakephp.org',
499+
'adapter' => $mock
500+
]);
501+
502+
$http->get('/projects');
503+
$http->get('http://cakephp.org/versions');
504+
505+
$result = $http->cookies();
506+
$expected = [
507+
'cakephp.org' => [
508+
'first' => '1',
509+
'third' => '3',
510+
],
511+
'test.cakephp.org' => [
512+
'second' => '2',
513+
]
514+
];
515+
$this->assertEquals($expected, $result);
516+
}
517+
407518
}

0 commit comments

Comments
 (0)