Browse Source

Initial implementation of addFromResponse()

Build out the initial code which will replace Client\Response cookie
parsing. This duplicates a bunch of code that I want to remove from the
Client once this cookie collection is complete.
Mark Story 9 years ago
parent
commit
32d2f13517

+ 30 - 0
src/Http/Cookie/Cookie.php

@@ -293,6 +293,16 @@ class Cookie implements CookieInterface
     }
 
     /**
+     * Get the path attribute.
+     *
+     * @return string
+     */
+    public function getPath()
+    {
+        return $this->path;
+    }
+
+    /**
      * Create a cookie with an updated domain
      *
      * @param string $domain Domain to set
@@ -308,6 +318,16 @@ class Cookie implements CookieInterface
     }
 
     /**
+     * Get the domain attribute.
+     *
+     * @return string
+     */
+    public function getDomain()
+    {
+        return $this->domain;
+    }
+
+    /**
      * Validate that an argument is a string
      *
      * @param string $value The value to validate.
@@ -406,6 +426,16 @@ class Cookie implements CookieInterface
     }
 
     /**
+     * Get the current expiry time
+     *
+     * @return int|null Timestamp of expiry or null
+     */
+    public function getExpiry()
+    {
+        return $this->expiresAt;
+    }
+
+    /**
      * Create a new cookie that will virtually never expire.
      *
      * @return static

+ 106 - 0
src/Http/Cookie/CookieCollection.php

@@ -15,8 +15,11 @@ namespace Cake\Http\Cookie;
 
 use ArrayIterator;
 use Countable;
+use DateTime;
 use InvalidArgumentException;
 use IteratorAggregate;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
 
 /**
  * Cookie Collection
@@ -154,4 +157,107 @@ class CookieCollection implements IteratorAggregate, Countable
     {
         return new ArrayIterator($this->cookies);
     }
+
+    /**
+     * Create a new collection that includes cookies from the response.
+     *
+     * @param \Psr\Http\Message\ResponseInterface $response Response to extract cookies from.
+     * @param \Psr\Http\Message\RequestInterface $request Request to get cookie context from.
+     * @return static
+     */
+    public function addFromResponse(ResponseInterface $response, RequestInterface $request)
+    {
+        $uri = $request->getUri();
+        $host = $uri->getHost();
+        $path = $uri->getPath() ?: '/';
+
+        $header = $response->getHeader('Set-Cookie');
+        $cookies = $this->parseSetCookieHeader($header);
+        $new = clone $this;
+        foreach ($cookies as $name => $cookie) {
+            // Apply path/domain from request if the cookie
+            // didn't have one.
+            if (!$cookie->getDomain()) {
+                $cookie = $cookie->withDomain($host);
+            }
+            if (!$cookie->getPath()) {
+                $cookie = $cookie->withPath($path);
+            }
+
+            $expires = $cookie->getExpiry();
+            // Don't store expired cookies
+            if ($expires && $expires <= time()) {
+                continue;
+            }
+            $key = mb_strtolower($cookie->getName());
+            $new->cookies[$key] = $cookie;
+        }
+
+        return $new;
+    }
+
+    /**
+     * Parse Set-Cookie headers into array
+     *
+     * @param array $values List of Set-Cookie Header values.
+     * @return \Cake\Http\Cookie\Cookie[] An array of cookie objects
+     */
+    protected function parseSetCookieHeader($values)
+    {
+        $cookies = [];
+        foreach ($values as $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;
+            $cookie = [
+                'value' => '',
+                'path' => '',
+                'domain' => '',
+                'secure' => false,
+                'httponly' => false,
+                'expires' => null
+            ];
+            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 (!strlen($cookie[$key])) {
+                    $cookie[$key] = $value;
+                }
+            }
+            $expires = null;
+            if ($cookie['expires']) {
+                $expires = new DateTime($cookie['expires']);
+            }
+
+            $cookies[] = new Cookie(
+                $name,
+                $cookie['value'],
+                $expires,
+                $cookie['path'],
+                $cookie['domain'],
+                $cookie['secure'],
+                $cookie['httponly']
+            );
+        }
+
+        return $cookies;
+    }
 }

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

@@ -14,6 +14,8 @@ namespace Cake\Test\TestCase\Http\Cookie;
 
 use Cake\Http\Cookie\Cookie;
 use Cake\Http\Cookie\CookieCollection;
+use Cake\Http\ServerRequest;
+use Cake\Http\Response;
 use Cake\TestSuite\TestCase;
 
 /**
@@ -174,4 +176,40 @@ class CookieCollectionTest extends TestCase
 
         new CookieCollection($array);
     }
+
+    /**
+     * Test adding cookies from a response.
+     *
+     * @return void
+     */
+    public function testAddFromResponse()
+    {
+        $collection = new CookieCollection();
+        $request = new ServerRequest([
+            'url' => '/app'
+        ]);
+        $response = (new Response())
+            ->withAddedHeader('Set-Cookie', 'test=value')
+            ->withAddedHeader('Set-Cookie', 'expiring=soon; Expires=Wed, 09-Jun-2021 10:18:14 GMT; Path=/; HttpOnly; Secure;')
+            ->withAddedHeader('Set-Cookie', 'session=123abc');
+        $new = $collection->addFromResponse($response, $request);
+        $this->assertNotSame($new, $collection, 'Should clone collection');
+
+        $this->assertTrue($new->has('test'));
+        $this->assertTrue($new->has('session'));
+        $this->assertTrue($new->has('expiring'));
+        $this->assertSame('value', $new->get('test')->getValue());
+        $this->assertSame('123abc', $new->get('session')->getValue());
+        $this->assertSame('soon', $new->get('expiring')->getValue());
+
+        $this->assertSame('/app', $new->get('test')->getPath(), 'cookies should inherit request path');
+        $this->assertSame('/', $new->get('expiring')->getPath(), 'path attribute should be used.');
+
+        $this->assertSame(0, $new->get('session')->getExpiry(), 'No expiry');
+        $this->assertSame(
+            '2021-06-09 10:18:14',
+            date('Y-m-d H:i:s', $new->get('expiring')->getExpiry()),
+            'Has expiry'
+        );
+    }
 }