Browse Source

Merge pull request #10466 from cakephp/3next-client-cookie-collection

3.next - Use CookieCollection in Client responses
Mark Story 9 years ago
parent
commit
0a5c9eb534

+ 1 - 9
src/Http/Client/CookieCollection.php

@@ -78,15 +78,7 @@ class CookieCollection extends BaseCollection
     {
         $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;

+ 64 - 49
src/Http/Client/Response.php

@@ -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;
@@ -103,6 +106,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
      *
      * @var string
@@ -129,7 +139,7 @@ class Response extends Message implements ResponseInterface
      * @var array
      */
     protected $_exposedProperties = [
-        'cookies' => '_cookies',
+        'cookies' => '_getCookies',
         'body' => '_getBody',
         'code' => 'code',
         'json' => '_getJson',
@@ -204,9 +214,6 @@ class Response extends Message implements ResponseInterface
             $name = trim($name);
 
             $normalized = strtolower($name);
-            if ($normalized === 'set-cookie') {
-                $this->_parseCookie($value);
-            }
 
             if (isset($this->headers[$name])) {
                 $this->headers[$name][] = $value;
@@ -218,46 +225,6 @@ class Response extends Message implements ResponseInterface
     }
 
     /**
-     * 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
      *
      * @return bool
@@ -427,7 +394,22 @@ class Response extends Message implements ResponseInterface
      */
     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;
     }
 
     /**
@@ -438,11 +420,12 @@ class Response extends Message implements ResponseInterface
      */
     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();
     }
 
     /**
@@ -453,11 +436,43 @@ class Response extends Message implements ResponseInterface
      */
     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;
     }
 
     /**

+ 49 - 1
src/Http/Cookie/Cookie.php

@@ -49,6 +49,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
      *
      * @var string
@@ -160,7 +167,7 @@ class Cookie implements CookieInterface
     {
         return sprintf(
             'expires=%s',
-            gmdate('D, d-M-Y H:i:s T', $this->expiresAt)
+            gmdate(static::EXPIRES_FORMAT, $this->expiresAt)
         );
     }
 
@@ -597,6 +604,47 @@ class Cookie implements CookieInterface
     }
 
     /**
+     * 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
      *
      * @param array $array Map of key and values

+ 15 - 3
src/Http/Cookie/CookieCollection.php

@@ -52,6 +52,19 @@ class CookieCollection implements IteratorAggregate, Countable
     }
 
     /**
+     * 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.
      *
      * @return int
@@ -251,8 +264,7 @@ class CookieCollection implements IteratorAggregate, Countable
         $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) {
@@ -293,7 +305,7 @@ class CookieCollection implements IteratorAggregate, Countable
      * @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) {

+ 24 - 0
tests/TestCase/Http/Client/ResponseTest.php

@@ -14,6 +14,7 @@
 namespace Cake\Test\TestCase\Http\Client;
 
 use Cake\Http\Client\Response;
+use Cake\Http\Cookie\CookieCollection;
 use Cake\TestSuite\TestCase;
 
 /**
@@ -331,6 +332,29 @@ XML;
     }
 
     /**
+     * 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()
      *
      * @return void

+ 19 - 0
tests/TestCase/Http/Cookie/CookieCollectionTest.php

@@ -403,4 +403,23 @@ class CookieCollectionTest extends TestCase
         $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');
+    }
 }

+ 52 - 0
tests/TestCase/Http/Cookie/CookieTest.php

@@ -521,4 +521,56 @@ class CookieTest extends TestCase
         $cookie = new Cookie('test', 'val', null, '/path', 'example.com');
         $this->assertEquals('test;example.com;/path', $cookie->getId());
     }
+
+    /**
+     * Test toArray
+     *
+     * @return void
+     */
+    public function testToArray()
+    {
+        $date = Chronos::parse('2017-03-31 12:34:56');
+        $cookie = new Cookie('cakephp', 'cakephp-rocks');
+        $cookie = $cookie->withDomain('cakephp.org')
+            ->withPath('/api')
+            ->withExpiry($date)
+            ->withHttpOnly(true)
+            ->withSecure(true);
+        $expected = [
+            'name' => 'cakephp',
+            'value' => 'cakephp-rocks',
+            'path' => '/api',
+            'domain' => 'cakephp.org',
+            'expires' => (int)$date->format('U'),
+            'secure' => true,
+            'httponly' => true
+        ];
+        $this->assertEquals($expected, $cookie->toArray());
+    }
+
+    /**
+     * Test toArrayCompat
+     *
+     * @return void
+     */
+    public function testToArrayCompat()
+    {
+        $date = Chronos::parse('2017-03-31 12:34:56');
+        $cookie = new Cookie('cakephp', 'cakephp-rocks');
+        $cookie = $cookie->withDomain('cakephp.org')
+            ->withPath('/api')
+            ->withExpiry($date)
+            ->withHttpOnly(true)
+            ->withSecure(true);
+        $expected = [
+            'name' => 'cakephp',
+            'value' => 'cakephp-rocks',
+            'path' => '/api',
+            'domain' => 'cakephp.org',
+            'expires' => 'Fri, 31-Mar-2017 12:34:56 GMT',
+            'secure' => true,
+            'httponly' => true
+        ];
+        $this->assertEquals($expected, $cookie->toArrayCompat());
+    }
 }