Permalink
Browse files

Add "cookie_encode_callback" to AgaviWebResponse

  • Loading branch information...
thomasbachem committed Nov 25, 2015
1 parent 709fc93 commit 2c0475ffe8f9f60110cbdc35ee3a03cf33d1ec25
View
@@ -31,6 +31,7 @@ ADD: Add support for custom filesystem layouts for modules to the project config
ADD: Response attributes (#1062) (David, TANAKA Koichi)
ADD: Add AgaviJsonValidator (#1566) (Thomas Bachem)
ADD: Add "forms_xpath" setting to AgaviFormPopulationFilter (#1527) (Thomas Bachem)
ADD: Add "cookie_encoding_callback" parameter to AgaviWebResponse (#1482) (Thomas Bachem)
CHG: Make AgaviXmlConfigParser::xinclude() use native sorting for glob (#1476) (Thomas Bachem)
CHG: Improve AgaviFormPopulationFilter's "log_parse_errors" setting to handle severities (#1481) (Thomas Bachem)
@@ -19,7 +19,12 @@
<request class="AgaviWebRequest" />
<response class="AgaviWebResponse" />
<response class="AgaviWebResponse">
<!-- Encode cookies with rawurlencode() instead of urlencode() to make them compliant with RFC 6265. -->
<!-- We sadly cannot change the default encoding of cookies as it would be a breaking change for -->
<!-- existing Agavi projects, but recommend to set this setting to "rawurlencode" for new projects. -->
<ae:parameter name="cookie_encode_callback">rawurlencode</ae:parameter>
</response>
<routing class="AgaviWebRouting" />
@@ -19,7 +19,10 @@
<request class="AgaviWebRequest" />
<response class="AgaviWebResponse" />
<response class="AgaviWebResponse">
<!-- Encode cookies with rawurlencode() instead of urlencode() to make them compliant with RFC 6265 -->
<ae:parameter name="cookie_encode_callback">rawurlencode</ae:parameter>
</response>
<routing class="AgaviWebRouting" />
@@ -166,6 +166,13 @@ public function initialize(AgaviContext $context, array $parameters = array())
'cookie_domain' => isset($parameters['cookie_domain']) ? $parameters['cookie_domain'] : "",
'cookie_secure' => isset($parameters['cookie_secure']) ? $parameters['cookie_secure'] : false,
'cookie_httponly' => isset($parameters['cookie_httponly']) ? $parameters['cookie_httponly'] : false,
// For historical reasons, PHP's setcookie() encodes cookies with urlencode(), which
// is not compliant with RFC 6265 as it encodes spaces as a "+" sign instead of "%20".
// This makes most client-side Javascript cookie libraries decode it not as a space
// but as an actual plus sign. We sadly cannot change the default encoding of cookies
// as it would be a breaking change, but introduced a setting instead, which we
// recommend to set to "rawurlencode" for new projects.
'cookie_encode_callback' => isset($parameters['cookie_encode_callback']) ? $parameters['cookie_encode_callback'] : 'urlencode',
));
switch($request->getProtocol()) {
@@ -308,7 +315,7 @@ public function merge(AgaviResponse $otherResponse)
}
foreach($otherResponse->getCookies() as $name => $cookie) {
if(!$this->hasCookie($name)) {
$this->setCookie($name, $cookie['value'], $cookie['lifetime'], $cookie['path'], $cookie['domain'], $cookie['secure'], $cookie['httponly']);
$this->setCookie($name, $cookie['value'], $cookie['lifetime'], $cookie['path'], $cookie['domain'], $cookie['secure'], $cookie['httponly'], $cookie['encode']);
}
}
if($otherResponse->hasRedirect() && !$this->hasRedirect()) {
@@ -467,38 +474,48 @@ public function setHttpHeader($name, $value, $replace = true)
/**
* Send a cookie.
*
* @param string A cookie name.
* @param mixed Data to store into a cookie. If null or empty cookie
* will be tried to be removed.
* @param mixed The lifetime of the cookie in seconds. When you pass 0
* the cookie will be valid until the browser is closed.
* You can also use a strtotime() string instead of an int.
* @param string The path on the server the cookie will be available on.
* @param string The domain the cookie is available on.
* @param bool Indicates that the cookie should only be transmitted
* over a secure HTTPS connection.
* @param bool Whether the cookie will be made accessible only through
* the HTTP protocol, and not to client-side scripts.
*
* @param string A cookie name.
* @param mixed Data to store into a cookie. If null or empty cookie
* will be tried to be removed.
* @param mixed The lifetime of the cookie in seconds. When you pass 0
* the cookie will be valid until the browser is closed.
* You can also use a strtotime() string instead of an int.
* @param string The path on the server the cookie will be available on.
* @param string The domain the cookie is available on.
* @param bool Indicates that the cookie should only be transmitted
* over a secure HTTPS connection.
* @param bool Whether the cookie will be made accessible only through
* the HTTP protocol, and not to client-side scripts.
* @param callable|bool Callback to encode the cookie value. Set to false
* if you did already encode the value on your own.
*
* @throws AgaviException If $encodeCallback is neither false nor callable.
*
* @author Veikko Mäkinen <mail@veikkomakinen.com>
* @author David Zülke <dz@bitxtender.com>
* @since 0.11.0
*/
public function setCookie($name, $value, $lifetime = null, $path = null, $domain = null, $secure = null, $httponly = null)
public function setCookie($name, $value, $lifetime = null, $path = null, $domain = null, $secure = null, $httponly = null, $encodeCallback = null)
{
$lifetime = $lifetime !== null ? $lifetime : $this->getParameter('cookie_lifetime');
$path = $path !== null ? $path : $this->getParameter('cookie_path');
$domain = $domain !== null ? $domain : $this->getParameter('cookie_domain');
$secure = (bool) ($secure !== null ? $secure : $this->getParameter('cookie_secure'));
$httponly = (bool) ($httponly !== null ? $httponly : $this->getParameter('cookie_httponly'));
$lifetime = $lifetime !== null ? $lifetime : $this->getParameter('cookie_lifetime');
$path = $path !== null ? $path : $this->getParameter('cookie_path');
$domain = $domain !== null ? $domain : $this->getParameter('cookie_domain');
$secure = (bool) ($secure !== null ? $secure : $this->getParameter('cookie_secure'));
$httponly = (bool) ($httponly !== null ? $httponly : $this->getParameter('cookie_httponly'));
$encodeCallback = $encodeCallback !== null ? $encodeCallback : $this->getParameter('cookie_encode_callback');
if($encodeCallback !== false && !is_callable($encodeCallback)) {
throw new AgaviException(sprintf('setCookie() $encodeCallback argument is not callable: %s', $encodeCallback));
}
$this->cookies[$name] = array(
'value' => $value,
'lifetime' => $lifetime,
'path' => $path,
'domain' => $domain,
'secure' => $secure,
'httponly' => $httponly
'httponly' => $httponly,
'encode_callback' => $encodeCallback
);
}
@@ -689,12 +706,16 @@ protected function sendHttpResponseHeaders(AgaviOutputType $outputType = null)
if($values['value'] === false || $values['value'] === null || $values['value'] === '') {
$expire = time() - 3600 * 24;
}
if($values['encode_callback']) {
$values['value'] = call_user_func($values['encode_callback'], $values['value']);
}
if($values['path'] === null) {
$values['path'] = $basePath;
}
setcookie($name, $values['value'], $expire, $values['path'], $values['domain'], $values['secure'], $values['httponly']);
setrawcookie($name, $values['value'], $expire, $values['path'], $values['domain'], $values['secure'], $values['httponly']);
}
// send headers
@@ -1,21 +1,31 @@
<?php
class NoHeadersAgaviWebResponse extends AgaviWebResponse
class TestAgaviWebResponse extends AgaviWebResponse
{
protected function sendHttpResponseHeaders(AgaviOutputType $outputType = null)
{
// don't send headers, it won't work on the command line
return;
// suppress errors when headers cannot be sent
set_error_handler(function($errNo, $errStr) {
return (stripos($errStr, 'headers already sent') !== false);
}, E_WARNING);
parent::sendHttpResponseHeaders($outputType);
restore_error_handler();
}
}
class AgaviWebResponseTest extends AgaviUnitTestCase
{
/**
* @var \TestAgaviWebResponse
*/
private $_r = null;
public function setUp()
{
$this->_r = new NoHeadersAgaviWebResponse();
$this->_r = new TestAgaviWebResponse();
$this->_r->initialize($this->getContext());
}
@@ -175,6 +185,7 @@ public function testSetCookie()
'domain' => '',
'secure' => false,
'httponly' => false,
'encode_callback' => 'urlencode',
);
$r->setCookie('cookieName', 'value');
$this->assertEquals($info_ex, $r->getCookie('cookieName'));
@@ -193,9 +204,79 @@ public function testSetCookie()
'domain' => 'foo.bar',
'secure' => true,
'httponly' => false,
'encode_callback' => 'urlencode',
);
$this->assertEquals($info_ex, $r->getCookie('cookieName2'));
}
/**
* @runInSeparateProcess
*/
public function testCookieEncoding()
{
if(!extension_loaded('xdebug')) {
$this->markTestSkipped('This test requires xdebug for the xdebug_get_headers() function.');
}
$r = $this->_r;
$r->setCookie('spaceCookie', 'my value');
$r->setCookie('plusCookie', 'my+value');
$r->setCookie('customCookie', 'my%01value', null, null, null, null, null, false);
$r->send();
// headers_list() does sadly not work on CLI, but xdebug_get_headers() does
// (see http://www.santiagolizardo.com/article/testing-if-http-headers-were-sent-in-php-and-phpunit)
$headers = xdebug_get_headers();
$encodedCookieValues = array();
foreach($headers as $header) {
list($headerName, $headerValue) = preg_split('/:\s*/', $header, 2);
if($headerName == 'Set-Cookie') {
$parts = preg_split('/;\s*/', $headerValue);
list($cookieName, $cookieValue) = explode('=', $parts[0]);
$encodedCookieValues[$cookieName] = $cookieValue;
}
}
$this->assertEquals('my+value', $encodedCookieValues['spaceCookie']);
$this->assertEquals('my%2Bvalue', $encodedCookieValues['plusCookie']);
$this->assertEquals('my%01value', $encodedCookieValues['customCookie']);
}
/**
* @runInSeparateProcess
*/
public function testRawCookieEncoding()
{
if(!extension_loaded('xdebug')) {
$this->markTestSkipped('This test requires xdebug for the xdebug_get_headers() function.');
}
$r = $this->_r;
$r->setParameter('cookie_encode_callback', 'rawurlencode');
$r->setCookie('spaceCookie', 'my value');
$r->setCookie('plusCookie', 'my+value');
$r->setCookie('customCookie', 'my%01value', null, null, null, null, null, false);
$r->send();
// headers_list() does sadly not work on CLI, but xdebug_get_headers() does
// (see http://www.santiagolizardo.com/article/testing-if-http-headers-were-sent-in-php-and-phpunit)
$headers = xdebug_get_headers();
$encodedCookieValues = array();
foreach($headers as $header) {
list($headerName, $headerValue) = preg_split('/:\s*/', $header, 2);
if($headerName == 'Set-Cookie') {
$parts = preg_split('/;\s*/', $headerValue);
list($cookieName, $cookieValue) = explode('=', $parts[0]);
$encodedCookieValues[$cookieName] = $cookieValue;
}
}
$this->assertEquals('my%20value', $encodedCookieValues['spaceCookie']);
$this->assertEquals('my%2Bvalue', $encodedCookieValues['plusCookie']);
$this->assertEquals('my%01value', $encodedCookieValues['customCookie']);
}
}
?>

0 comments on commit 2c0475f

Please sign in to comment.