Skip to content

Commit

Permalink
Added support for string and DateTimeInterface as Cookie::$expire (#1…
Browse files Browse the repository at this point in the history
…9920)

* Added support for string as Cookie::$expire

* Updated changelog for #19920 (Broadened the accepted type of `Cookie::$expire` from `int` to `int|string|null`)

* Fixed `\yiiunit\framework\web\ResponseTest::parseHeaderCookies()` to overwrite existing cookie.

* Added support for `\DateTimeInterface` in `\yii\web\Cookie::$expire`

* Fixed `\yiiunit\framework\web\ResponseTest::cookiesTestProvider()` for PHP 5.4 and commited missing code for \DateTimeInterface processing in `\yii\web\Response::sendCookies()`
  • Loading branch information
rhertogh committed Aug 15, 2023
1 parent 84c15dc commit 73902f0
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 16 deletions.
1 change: 1 addition & 0 deletions framework/CHANGELOG.md
Expand Up @@ -15,6 +15,7 @@ Yii Framework 2 Change Log
- Enh #19884: Added support Enums in Query Builder (sk1t0n)
- Bug #19908: Fix associative array cell content rendering in Table widget (rhertogh)
- Bug #19906: Fixed multiline strings in the `\yii\console\widgets\Table` widget (rhertogh)
- Enh #19920: Broadened the accepted type of `Cookie::$expire` from `int` to `int|string|\DateTimeInterface|null` (rhertogh)


2.0.48.1 May 24, 2023
Expand Down
4 changes: 2 additions & 2 deletions framework/web/Cookie.php
Expand Up @@ -57,8 +57,8 @@ class Cookie extends \yii\base\BaseObject
*/
public $domain = '';
/**
* @var int the timestamp at which the cookie expires. This is the server timestamp.
* Defaults to 0, meaning "until the browser is closed".
* @var int|string|\DateTimeInterface|null the timestamp or date at which the cookie expires. This is the server timestamp.
* Defaults to 0, meaning "until the browser is closed" (the same applies to `null`).
*/
public $expire = 0;
/**
Expand Down
17 changes: 14 additions & 3 deletions framework/web/CookieCollection.php
Expand Up @@ -51,7 +51,7 @@ public function __construct($cookies = [], $config = [])
* Returns an iterator for traversing the cookies in the collection.
* This method is required by the SPL interface [[\IteratorAggregate]].
* It will be implicitly called when you use `foreach` to traverse the collection.
* @return ArrayIterator an iterator for traversing the cookies in the collection.
* @return ArrayIterator<string, Cookie> an iterator for traversing the cookies in the collection.
*/
#[\ReturnTypeWillChange]
public function getIterator()
Expand Down Expand Up @@ -113,7 +113,18 @@ public function getValue($name, $defaultValue = null)
public function has($name)
{
return isset($this->_cookies[$name]) && $this->_cookies[$name]->value !== ''
&& ($this->_cookies[$name]->expire === null || $this->_cookies[$name]->expire === 0 || $this->_cookies[$name]->expire >= time());
&& ($this->_cookies[$name]->expire === null
|| $this->_cookies[$name]->expire === 0
|| (
(is_string($this->_cookies[$name]->expire) && strtotime($this->_cookies[$name]->expire) >= time())
|| (
interface_exists('\\DateTimeInterface')
&& $this->_cookies[$name]->expire instanceof \DateTimeInterface
&& $this->_cookies[$name]->expire->getTimestamp() >= time()
)
|| $this->_cookies[$name]->expire >= time()
)
);
}

/**
Expand Down Expand Up @@ -174,7 +185,7 @@ public function removeAll()

/**
* Returns the collection as a PHP array.
* @return array the array representation of the collection.
* @return Cookie[] the array representation of the collection.
* The array keys are cookie names, and the array values are the corresponding cookie objects.
*/
public function toArray()
Expand Down
15 changes: 12 additions & 3 deletions framework/web/Response.php
Expand Up @@ -401,12 +401,21 @@ protected function sendCookies()
}
foreach ($this->getCookies() as $cookie) {
$value = $cookie->value;
if ($cookie->expire != 1 && isset($validationKey)) {
$expire = $cookie->expire;
if (is_string($expire)) {
$expire = strtotime($expire);
} elseif (interface_exists('\\DateTimeInterface') && $expire instanceof \DateTimeInterface) {
$expire = $expire->getTimestamp();
}
if ($expire === null || $expire === false) {
$expire = 0;
}
if ($expire != 1 && isset($validationKey)) {
$value = Yii::$app->getSecurity()->hashData(serialize([$cookie->name, $value]), $validationKey);
}
if (PHP_VERSION_ID >= 70300) {
setcookie($cookie->name, $value, [
'expires' => $cookie->expire,
'expires' => $expire,
'path' => $cookie->path,
'domain' => $cookie->domain,
'secure' => $cookie->secure,
Expand All @@ -420,7 +429,7 @@ protected function sendCookies()
if (!is_null($cookie->sameSite)) {
$cookiePath .= '; samesite=' . $cookie->sameSite;
}
setcookie($cookie->name, $value, $cookie->expire, $cookiePath, $cookie->domain, $cookie->secure, $cookie->httpOnly);
setcookie($cookie->name, $value, $expire, $cookiePath, $cookie->domain, $cookie->secure, $cookie->httpOnly);
}
}
}
Expand Down
111 changes: 103 additions & 8 deletions tests/framework/web/ResponseTest.php
Expand Up @@ -380,21 +380,116 @@ public function testSendFileWithInvalidCharactersInFileName()
);
}

public function testSameSiteCookie()
/**
* @dataProvider cookiesTestProvider
*/
public function testCookies($cookieConfig, $expected)
{
$response = new Response();
$response->cookies->add(new Cookie([
'name' => 'test',
'value' => 'testValue',
'sameSite' => Cookie::SAME_SITE_STRICT,
]));
$response->cookies->add(new Cookie(array_merge(
[
'name' => 'test',
'value' => 'testValue',
],
$cookieConfig
)));

ob_start();
$response->send();
$content = ob_get_clean();

// Only way to test is that it doesn't create any errors
$this->assertEquals('', $content);
$cookies = $this->parseHeaderCookies();
if ($cookies === false) {
// Unable to resolve cookies, only way to test is that it doesn't create any errors
$this->assertEquals('', $content);
} else {
$testCookie = $cookies['test'];
$actual = array_intersect_key($testCookie, $expected);
ksort($actual);
ksort($expected);
$this->assertEquals($expected, $actual);
}
}

public function cookiesTestProvider()
{
$expireInt = time() + 3600;
$expireString = date('D, d-M-Y H:i:s', $expireInt) . ' GMT';

$testCases = [
'same-site' => [
['sameSite' => Cookie::SAME_SITE_STRICT],
['samesite' => Cookie::SAME_SITE_STRICT],
],
'expire-as-int' => [
['expire' => $expireInt],
['expires' => $expireString],
],
'expire-as-string' => [
['expire' => $expireString],
['expires' => $expireString],
],
];

if (version_compare(PHP_VERSION, '5.5.0', '>=')) {
$testCases = array_merge($testCases, [
'expire-as-date_time' => [
['expire' => new \DateTime('@' . $expireInt)],
['expires' => $expireString],
],
'expire-as-date_time_immutable' => [
['expire' => new \DateTimeImmutable('@' . $expireInt)],
['expires' => $expireString],
],
]);
}

return $testCases;
}

/**
* Tries to parse cookies set in the response headers.
* When running PHP on the CLI headers are not available (the `headers_list()` function always returns an
* empty array). If possible use xDebug: http://xdebug.org/docs/all_functions#xdebug_get_headers
* @param $name
* @return array|false
*/
protected function parseHeaderCookies() {

if (!function_exists('xdebug_get_headers')) {
return false;
}

$cookies = [];
foreach(xdebug_get_headers() as $header) {
if (strpos($header, 'Set-Cookie: ') !== 0) {
continue;
}

$name = null;
$params = [];
$pairs = explode(';', substr($header, 12));
foreach ($pairs as $index => $pair) {
$pair = trim($pair);
if (strpos($pair, '=') === false) {
$params[strtolower($pair)] = true;
} else {
list($paramName, $paramValue) = explode('=', $pair, 2);
if ($index === 0) {
$name = $paramName;
$params['value'] = urldecode($paramValue);
} else {
$params[strtolower($paramName)] = urldecode($paramValue);
}
}
}
if ($name === null) {
throw new \Exception('Could not determine cookie name for header "' . $header . '".');
}
$cookies[$name] = $params;
}

return $cookies;
}

/**
Expand Down

0 comments on commit 73902f0

Please sign in to comment.