瀏覽代碼

better support for cookie handling, use native PHP function to set cookies

Johannes M. Schmitt 14 年之前
父節點
當前提交
98b52b607c

+ 88 - 0
src/Symfony/Component/HttpFoundation/Cookie.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace Symfony\Component\HttpFoundation;
+
+/**
+ * Represents a cookie
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+class Cookie
+{
+    protected $name;
+    protected $value;
+    protected $domain;
+    protected $expire;
+    protected $path;
+    protected $secure;
+    protected $httponly;
+
+    public function __construct($name, $value = null, $expire = 0, $path = null, $domain = null, $secure = false, $httponly = true)
+    {
+        // from PHP source code
+        if (preg_match("/[=,; \t\r\n\013\014]/", $name)) {
+            throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name));
+        }
+
+        if (preg_match("/[,; \t\r\n\013\014]/", $value)) {
+            throw new \InvalidArgumentException(sprintf('The cookie value "%s" contains invalid characters.', $name));
+        }
+
+        if (empty($name)) {
+            throw new \InvalidArgumentException('The cookie name cannot be empty');
+        }
+
+        $this->name = $name;
+        $this->value = $value;
+        $this->domain = $domain;
+        $this->expire = (integer) $expire;
+        $this->path = $path;
+        $this->secure = (Boolean) $secure;
+        $this->httponly = (Boolean) $httponly;
+    }
+
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    public function getValue()
+    {
+        return $this->value;
+    }
+
+    public function getDomain()
+    {
+        return $this->domain;
+    }
+
+    public function getExpire()
+    {
+        return $this->expire;
+    }
+
+    public function getPath()
+    {
+        return $this->path;
+    }
+
+    public function isSecure()
+    {
+        return $this->secure;
+    }
+
+    public function isHttponly()
+    {
+        return $this->httponly;
+    }
+
+    /**
+     * Whether this cookie is about to be cleared
+     *
+     * @return Boolean
+     */
+    public function isCleared()
+    {
+        return $this->expire < time();
+    }
+}

+ 53 - 26
src/Symfony/Component/HttpFoundation/HeaderBag.php

@@ -19,6 +19,7 @@ namespace Symfony\Component\HttpFoundation;
 class HeaderBag
 {
     protected $headers;
+    protected $cookies;
     protected $cacheControl;
 
     /**
@@ -29,6 +30,7 @@ class HeaderBag
     public function __construct(array $headers = array())
     {
         $this->cacheControl = array();
+        $this->cookies = array();
         $this->replace($headers);
     }
 
@@ -163,21 +165,62 @@ class HeaderBag
     /**
      * Sets a cookie.
      *
-     * @param  string $name     The cookie name
-     * @param  string $value    The value of the cookie
-     * @param  string $domain   The domain that the cookie is available
-     * @param  string $expire   The time the cookie expires
-     * @param  string $path     The path on the server in which the cookie will be available on
-     * @param  bool   $secure   Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client
-     * @param  bool   $httponly When TRUE the cookie will not be made accessible to JavaScript, preventing XSS attacks from stealing cookies
+     * @param Cookie $cookie
      *
      * @throws \InvalidArgumentException When the cookie expire parameter is not valid
+     *
+     * @return void
+     */
+    public function setCookie(Cookie $cookie)
+    {
+        $this->cookies[$cookie->getName()] = $cookie;
+    }
+
+    /**
+     * Removes a cookie from the array, but does not unset it in the browser
+     *
+     * @param string $name
+     * @return void
+     */
+    public function removeCookie($name)
+    {
+        unset($this->cookies[$name]);
+    }
+
+    /**
+     * Whether the array contains any cookie with this name
+     *
+     * @param string $name
+     * @return Boolean
+     */
+    public function hasCookie($name)
+    {
+        return isset($this->cookies[$name]);
+    }
+
+    /**
+     * Returns a cookie
+     *
+     * @param string $name
+     * @return Cookie
      */
-    public function setCookie($name, $value, $domain = null, $expires = null, $path = '/', $secure = false, $httponly = true)
+    public function getCookie($name)
     {
-        $this->validateCookie($name, $value);
+        if (!$this->hasCookie($name)) {
+            throw new \InvalidArgumentException(sprintf('There is no cookie with name "%s".', $name));
+        }
+
+        return $this->cookies[$name];
+    }
 
-        return $this->set('Cookie', sprintf('%s=%s', $name, urlencode($value)));
+    /**
+     * Returns an array with all cookies
+     *
+     * @return array
+     */
+    public function getCookies()
+    {
+        return $this->cookies;
     }
 
     /**
@@ -261,20 +304,4 @@ class HeaderBag
 
         return $cacheControl;
     }
-
-    protected function validateCookie($name, $value)
-    {
-        // from PHP source code
-        if (preg_match("/[=,; \t\r\n\013\014]/", $name)) {
-            throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name));
-        }
-
-        if (preg_match("/[,; \t\r\n\013\014]/", $value)) {
-            throw new \InvalidArgumentException(sprintf('The cookie value "%s" contains invalid characters.', $name));
-        }
-
-        if (!$name) {
-            throw new \InvalidArgumentException('The cookie name cannot be empty');
-        }
-    }
 }

+ 5 - 0
src/Symfony/Component/HttpFoundation/Response.php

@@ -141,6 +141,11 @@ class Response
                 header($name.': '.$value);
             }
         }
+
+        // cookies
+        foreach ($this->headers->getCookies() as $cookie) {
+            setcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpire(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly());
+        }
     }
 
     /**

+ 10 - 40
src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php

@@ -62,58 +62,28 @@ class ResponseHeaderBag extends HeaderBag
     /**
      * {@inheritdoc}
      */
-    public function setCookie($name, $value, $domain = null, $expires = null, $path = '/', $secure = false, $httponly = true)
+    public function hasCacheControlDirective($key)
     {
-        $this->validateCookie($name, $value);
-
-        $cookie = sprintf('%s=%s', $name, urlencode($value));
-
-        if (null !== $expires) {
-            if (is_numeric($expires)) {
-                $expires = (int) $expires;
-            } elseif ($expires instanceof \DateTime) {
-                $expires = $expires->getTimestamp();
-            } else {
-                $expires = strtotime($expires);
-                if (false === $expires || -1 == $expires) {
-                    throw new \InvalidArgumentException(sprintf('The "expires" cookie parameter is not valid.', $expires));
-                }
-            }
-
-            $cookie .= '; expires='.substr(\DateTime::createFromFormat('U', $expires, new \DateTimeZone('UTC'))->format('D, d-M-Y H:i:s T'), 0, -5);
-        }
-
-        if ($domain) {
-            $cookie .= '; domain='.$domain;
-        }
-
-        $cookie .= '; path='.$path;
-
-        if ($secure) {
-            $cookie .= '; secure';
-        }
-
-        if ($httponly) {
-            $cookie .= '; httponly';
-        }
-
-        $this->set('Set-Cookie', $cookie, false);
+        return array_key_exists($key, $this->computedCacheControl);
     }
 
     /**
      * {@inheritdoc}
      */
-    public function hasCacheControlDirective($key)
+    public function getCacheControlDirective($key)
     {
-        return array_key_exists($key, $this->computedCacheControl);
+        return array_key_exists($key, $this->computedCacheControl) ? $this->computedCacheControl[$key] : null;
     }
 
     /**
-     * {@inheritdoc}
+     * Clears a cookie in the browser
+     *
+     * @param string $name
+     * @return void
      */
-    public function getCacheControlDirective($key)
+    public function clearCookie($name)
     {
-        return array_key_exists($key, $this->computedCacheControl) ? $this->computedCacheControl[$key] : null;
+        $this->setCookie(new Cookie($name, null, time() - 86400));
     }
 
     /**

+ 6 - 8
src/Symfony/Component/HttpKernel/Security/Logout/CookieClearingLogoutHandler.php

@@ -17,13 +17,13 @@ use Symfony\Component\HttpFoundation\Request;
 
 /**
  * This handler cleares the passed cookies when a user logs out.
- * 
+ *
  * @author Johannes M. Schmitt <schmittjoh@gmail.com>
  */
 class CookieClearingLogoutHandler implements LogoutHandlerInterface
 {
     protected $cookieNames;
-    
+
     /**
      * Constructor
      * @param array $cookieNames An array of cookie names to unset
@@ -32,7 +32,7 @@ class CookieClearingLogoutHandler implements LogoutHandlerInterface
     {
         $this->cookieNames = $cookieNames;
     }
-    
+
     /**
      * Returns the names of the cookies to unset
      * @return array
@@ -41,10 +41,10 @@ class CookieClearingLogoutHandler implements LogoutHandlerInterface
     {
         return $this->cookieNames;
     }
-    
+
     /**
      * Implementation for the LogoutHandlerInterface. Deletes all requested cookies.
-     * 
+     *
      * @param Request $request
      * @param Response $response
      * @param TokenInterface $token
@@ -52,10 +52,8 @@ class CookieClearingLogoutHandler implements LogoutHandlerInterface
      */
     public function logout(Request $request, Response $response, TokenInterface $token)
     {
-        $expires = time() - 86400;
-        
         foreach ($this->cookieNames as $cookieName) {
-            $response->headers->setCookie($cookieName, '', null, $expires);
+            $response->headers->clearCookie($cookieName);
         }
     }
 }

+ 17 - 16
tests/Symfony/Tests/Component/HttpKernel/Security/Logout/CookieClearingLogoutHandlerTest.php

@@ -11,32 +11,33 @@ class CookieClearingLogoutHandlerTest extends \PHPUnit_Framework_TestCase
     public function testConstructor()
     {
         $cookieNames = array('foo', 'foo2', 'foo3');
-        
+
         $handler = new CookieClearingLogoutHandler($cookieNames);
-        
+
         $this->assertEquals($cookieNames, $handler->getCookieNames());
     }
-    
+
     public function testLogout()
     {
         $request = new Request();
         $response = new Response();
         $token = $this->getMock('Symfony\Component\Security\Authentication\Token\TokenInterface');
-        
+
         $handler = new CookieClearingLogoutHandler(array('foo', 'foo2'));
-        
-        $this->assertFalse($response->headers->has('Set-Cookie'));
-        
+
+        $this->assertFalse($response->headers->hasCookie('foo'));
+
         $handler->logout($request, $response, $token);
-        
-        $headers = $response->headers->all();
-        $cookies = $headers['set-cookie'];
+
+        $cookies = $response->headers->getCookies();
         $this->assertEquals(2, count($cookies));
-        
-        $cookie = $cookies[0];
-        $this->assertStringStartsWith('foo=;', $cookie);
-        
-        $cookie = $cookies[1];
-        $this->assertStringStartsWith('foo2=;', $cookie);
+
+        $cookie = $cookies['foo'];
+        $this->assertEquals('foo', $cookie->getName());
+        $this->assertTrue($cookie->isCleared());
+
+        $cookie = $cookies['foo2'];
+        $this->assertStringStartsWith('foo2', $cookie->getName());
+        $this->assertTrue($cookie->isCleared());
     }
 }