Browse Source

Allow using "SameSite" attribute for PHP < 7.3.

ADmad 6 years ago
parent
commit
43542aef22

+ 34 - 17
src/Http/ResponseEmitter.php

@@ -21,7 +21,7 @@ declare(strict_types=1);
  */
 namespace Cake\Http;
 
-use Cake\Http\Cookie\Cookie;
+use Cake\Http\Cookie\CookieInterface;
 use Psr\Http\Message\ResponseInterface;
 use Zend\Diactoros\RelativeStream;
 use Zend\HttpHandlerRunner\Emitter\EmitterInterface;
@@ -188,8 +188,8 @@ class ResponseEmitter implements EmitterInterface
     protected function emitHeaders(ResponseInterface $response): void
     {
         $cookies = [];
-        if (method_exists($response, 'getCookies')) {
-            $cookies = $response->getCookies();
+        if (method_exists($response, 'getCookieCollection')) {
+            $cookies = iterator_to_array($response->getCookieCollection());
         }
 
         foreach ($response->getHeaders() as $name => $values) {
@@ -214,30 +214,47 @@ class ResponseEmitter implements EmitterInterface
     /**
      * Emit cookies using setcookie()
      *
-     * @param array $cookies An array of Set-Cookie headers.
+     * @param (string|\Cake\Http\Cookie\CookieInterface)[] $cookies An array of cookies.
      * @return void
      */
     protected function emitCookies(array $cookies): void
     {
         foreach ($cookies as $cookie) {
-            if (is_string($cookie)) {
-                $cookie = Cookie::createFromHeaderString($cookie)->toArray();
-            }
+            $this->setCookie($cookie);
+        }
+    }
 
+    /**
+     * Helper methods to set cookie.
+     *
+     * For PHP 7.3 it uses setcookie() and for PHP < 7.3 it uses header().
+     *
+     * @param string|\Cake\Http\Cookie\CookieInterface $cookie Cookie.
+     * @return bool
+     */
+    protected function setCookie($cookie): bool
+    {
+        if ($cookie instanceof CookieInterface) {
             if (PHP_VERSION_ID >= 70300) {
-                setcookie($cookie['name'], $cookie['value'], $cookie['options']);
-            } else {
-                setcookie(
-                    $cookie['name'],
-                    $cookie['value'],
-                    $cookie['options']['expires'],
-                    $cookie['options']['path'],
-                    $cookie['options']['domain'],
-                    $cookie['options']['secure'],
-                    $cookie['options']['httponly']
+                return setcookie($cookie->getName(), $cookie->getScalarValue(), $cookie->getOptions());
+            } elseif ($cookie->getSameSite() === null) {
+                return setcookie(
+                    $cookie->getName(),
+                    $cookie->getScalarValue(),
+                    $cookie->getExpiresTimestamp() ?: 0,
+                    $cookie->getPath(),
+                    $cookie->getDomain(),
+                    $cookie->isSecure(),
+                    $cookie->isHttpOnly()
                 );
             }
+
+            $cookie = $cookie->toHeaderValue();
         }
+
+        header('Set-Cookie: ' . $cookie);
+
+        return true;
     }
 
     /**

+ 24 - 3
tests/TestCase/Http/ResponseEmitterTest.php

@@ -29,6 +29,9 @@ require_once __DIR__ . '/server_mocks.php';
  */
 class ResponseEmitterTest extends TestCase
 {
+    /**
+     * @var \Cake\Http\ResponseEmitter
+     */
     protected $emitter;
 
     /**
@@ -39,9 +42,26 @@ class ResponseEmitterTest extends TestCase
     public function setUp(): void
     {
         parent::setUp();
+
         $GLOBALS['mockedHeadersSent'] = false;
-        $GLOBALS['mockedHeaders'] = $GLOBALS['mockedCookies'] = [];
-        $this->emitter = new ResponseEmitter();
+        $GLOBALS['mockedHeaders'] = [];
+
+        $this->emitter = $this->getMockBuilder(ResponseEmitter::class)
+            ->setMethods(['setCookie'])
+            ->getMock();
+
+        $this->emitter->expects($this->any())
+            ->method('setCookie')
+            ->will($this->returnCallback(function ($cookie) {
+                if (is_string($cookie)) {
+                    $cookie = Cookie::createFromHeaderString($cookie);
+                }
+
+                $GLOBALS['mockedCookies'][] = ['name' => $cookie->getName(), 'value' => $cookie->getValue()]
+                    + $cookie->getOptions();
+
+                return true;
+            }));
     }
 
     /**
@@ -161,7 +181,7 @@ class ResponseEmitterTest extends TestCase
         $response = (new Response())
             ->withAddedHeader('Set-Cookie', "simple=val;\tSecure")
             ->withAddedHeader('Set-Cookie', 'people=jim,jack,jonny";";Path=/accounts')
-            ->withAddedHeader('Set-Cookie', 'google=not=nice;Path=/accounts; HttpOnly')
+            ->withAddedHeader('Set-Cookie', 'google=not=nice;Path=/accounts; HttpOnly; samesite=Strict')
             ->withAddedHeader('Set-Cookie', 'a=b;  Expires=Wed, 13 Jan 2021 22:23:01 GMT; Domain=www.example.com;')
             ->withAddedHeader('Set-Cookie', 'list%5B%5D=a%20b%20c')
             ->withHeader('Content-Type', 'text/plain');
@@ -204,6 +224,7 @@ class ResponseEmitterTest extends TestCase
                 'domain' => '',
                 'secure' => false,
                 'httponly' => true,
+                'samesite' => 'Strict',
             ],
             [
                 'name' => 'a',

+ 0 - 19
tests/TestCase/Http/server_mocks.php

@@ -15,22 +15,3 @@ function header($header)
 {
     $GLOBALS['mockedHeaders'][] = $header;
 }
-
-function setcookie($name, $value = '', $expires = 0, $path = '', $domain = '', $secure = false, $httponly = false)
-{
-    if (is_array($expires)) {
-        $GLOBALS['mockedCookies'][] = compact('name', 'value') + $expires;
-
-        return;
-    }
-
-    $GLOBALS['mockedCookies'][] = compact(
-        'name',
-        'value',
-        'expires',
-        'path',
-        'domain',
-        'secure',
-        'httponly'
-    );
-}