Skip to content

Commit

Permalink
[HttpFoundation] added withers to Cookie class
Browse files Browse the repository at this point in the history
  • Loading branch information
ns3777k authored and fabpot committed Jan 30, 2020
1 parent 897bf05 commit 6d41d7e
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 13 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ CHANGELOG
5.1.0
-----

* added `Cookie::withValue`, `Cookie::withDomain`, `Cookie::withExpires`,
`Cookie::withPath`, `Cookie::withSecure`, `Cookie::withHttpOnly`,
`Cookie::withRaw`, `Cookie::withSameSite`
* Deprecate `Response::create()`, `JsonResponse::create()`,
`RedirectResponse::create()`, and `StreamedResponse::create()` methods (use
`__construct()` instead)
Expand Down
127 changes: 118 additions & 9 deletions Cookie.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,52 @@ public function __construct(string $name, string $value = null, $expire = 0, ?st
throw new \InvalidArgumentException('The cookie name cannot be empty.');
}

$this->name = $name;
$this->value = $value;
$this->domain = $domain;
$this->expire = $this->withExpires($expire)->expire;
$this->path = empty($path) ? '/' : $path;
$this->secure = $secure;
$this->httpOnly = $httpOnly;
$this->raw = $raw;
$this->sameSite = $this->withSameSite($sameSite)->sameSite;
}

/**
* Creates a cookie copy with a new value.
*
* @return static
*/
public function withValue(?string $value): self
{
$cookie = clone $this;
$cookie->value = $value;

return $cookie;
}

/**
* Creates a cookie copy with a new domain that the cookie is available to.
*
* @return static
*/
public function withDomain(?string $domain): self
{
$cookie = clone $this;
$cookie->domain = $domain;

return $cookie;
}

/**
* Creates a cookie copy with a new time the cookie expires.
*
* @param int|string|\DateTimeInterface $expire
*
* @return static
*/
public function withExpires($expire = 0): self
{
// convert expiration time to a Unix timestamp
if ($expire instanceof \DateTimeInterface) {
$expire = $expire->format('U');
Expand All @@ -110,15 +156,75 @@ public function __construct(string $name, string $value = null, $expire = 0, ?st
}
}

$this->name = $name;
$this->value = $value;
$this->domain = $domain;
$this->expire = 0 < $expire ? (int) $expire : 0;
$this->path = empty($path) ? '/' : $path;
$this->secure = $secure;
$this->httpOnly = $httpOnly;
$this->raw = $raw;
$cookie = clone $this;
$cookie->expire = 0 < $expire ? (int) $expire : 0;

return $cookie;
}

/**
* Creates a cookie copy with a new path on the server in which the cookie will be available on.
*
* @return static
*/
public function withPath(string $path): self
{
$cookie = clone $this;
$cookie->path = '' === $path ? '/' : $path;

return $cookie;
}

/**
* Creates a cookie copy that only be transmitted over a secure HTTPS connection from the client.
*
* @return static
*/
public function withSecure(bool $secure = true): self
{
$cookie = clone $this;
$cookie->secure = $secure;

return $cookie;
}

/**
* Creates a cookie copy that be accessible only through the HTTP protocol.
*
* @return static
*/
public function withHttpOnly(bool $httpOnly = true): self
{
$cookie = clone $this;
$cookie->httpOnly = $httpOnly;

return $cookie;
}

/**
* Creates a cookie copy that uses no url encoding.
*
* @return static
*/
public function withRaw(bool $raw = true): self
{
if ($raw && false !== strpbrk($this->name, self::$reservedCharsList)) {
throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $this->name));
}

$cookie = clone $this;
$cookie->raw = $raw;

return $cookie;
}

/**
* Creates a cookie copy with SameSite attribute.
*
* @return static
*/
public function withSameSite(?string $sameSite): self
{
if ('' === $sameSite) {
$sameSite = null;
} elseif (null !== $sameSite) {
Expand All @@ -129,7 +235,10 @@ public function __construct(string $name, string $value = null, $expire = 0, ?st
throw new \InvalidArgumentException('The "sameSite" parameter value is not valid.');
}

$this->sameSite = $sameSite;
$cookie = clone $this;
$cookie->sameSite = $sameSite;

return $cookie;
}

/**
Expand Down
110 changes: 106 additions & 4 deletions Tests/CookieTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ public function testInstantiationThrowsExceptionIfRawCookieNameContainsSpecialCh
Cookie::create($name, null, 0, null, null, null, false, true);
}

/**
* @dataProvider namesWithSpecialCharacters
*/
public function testWithRawThrowsExceptionIfCookieNameContainsSpecialCharacters($name)
{
$this->expectException('InvalidArgumentException');
Cookie::create($name)->withRaw();
}

/**
* @dataProvider namesWithSpecialCharacters
*/
Expand All @@ -72,6 +81,10 @@ public function testNegativeExpirationIsNotPossible()
$cookie = Cookie::create('foo', 'bar', -100);

$this->assertSame(0, $cookie->getExpiresTime());

$cookie = Cookie::create('foo', 'bar')->withExpires(-100);

$this->assertSame(0, $cookie->getExpiresTime());
}

public function testGetValue()
Expand All @@ -98,13 +111,21 @@ public function testGetExpiresTime()
$cookie = Cookie::create('foo', 'bar', $expire = time() + 3600);

$this->assertEquals($expire, $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date');

$cookie = Cookie::create('foo')->withExpires($expire = time() + 3600);

$this->assertEquals($expire, $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date');
}

public function testGetExpiresTimeIsCastToInt()
{
$cookie = Cookie::create('foo', 'bar', 3600.9);

$this->assertSame(3600, $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date as an integer');

$cookie = Cookie::create('foo')->withExpires(3600.6);

$this->assertSame(3600, $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date as an integer');
}

public function testConstructorWithDateTime()
Expand All @@ -113,6 +134,10 @@ public function testConstructorWithDateTime()
$cookie = Cookie::create('foo', 'bar', $expire);

$this->assertEquals($expire->format('U'), $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date');

$cookie = Cookie::create('foo')->withExpires($expire);

$this->assertEquals($expire->format('U'), $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date');
}

public function testConstructorWithDateTimeImmutable()
Expand All @@ -121,6 +146,10 @@ public function testConstructorWithDateTimeImmutable()
$cookie = Cookie::create('foo', 'bar', $expire);

$this->assertEquals($expire->format('U'), $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date');

$cookie = Cookie::create('foo')->withValue('bar')->withExpires($expire);

$this->assertEquals($expire->format('U'), $cookie->getExpiresTime(), '->getExpiresTime() returns the expire date');
}

public function testGetExpiresTimeWithStringValue()
Expand All @@ -130,34 +159,54 @@ public function testGetExpiresTimeWithStringValue()
$expire = strtotime($value);

$this->assertEqualsWithDelta($expire, $cookie->getExpiresTime(), 1, '->getExpiresTime() returns the expire date');

$cookie = Cookie::create('foo')->withValue('bar')->withExpires($value);

$this->assertEqualsWithDelta($expire, $cookie->getExpiresTime(), 1, '->getExpiresTime() returns the expire date');
}

public function testGetDomain()
{
$cookie = Cookie::create('foo', 'bar', 0, '/', '.myfoodomain.com');

$this->assertEquals('.myfoodomain.com', $cookie->getDomain(), '->getDomain() returns the domain name on which the cookie is valid');

$cookie = Cookie::create('foo')->withDomain('.mybardomain.com');

$this->assertEquals('.mybardomain.com', $cookie->getDomain(), '->getDomain() returns the domain name on which the cookie is valid');
}

public function testIsSecure()
{
$cookie = Cookie::create('foo', 'bar', 0, '/', '.myfoodomain.com', true);

$this->assertTrue($cookie->isSecure(), '->isSecure() returns whether the cookie is transmitted over HTTPS');

$cookie = Cookie::create('foo')->withSecure(true);

$this->assertTrue($cookie->isSecure(), '->isSecure() returns whether the cookie is transmitted over HTTPS');
}

public function testIsHttpOnly()
{
$cookie = Cookie::create('foo', 'bar', 0, '/', '.myfoodomain.com', false, true);

$this->assertTrue($cookie->isHttpOnly(), '->isHttpOnly() returns whether the cookie is only transmitted over HTTP');

$cookie = Cookie::create('foo')->withHttpOnly(true);

$this->assertTrue($cookie->isHttpOnly(), '->isHttpOnly() returns whether the cookie is only transmitted over HTTP');
}

public function testCookieIsNotCleared()
{
$cookie = Cookie::create('foo', 'bar', time() + 3600 * 24);

$this->assertFalse($cookie->isCleared(), '->isCleared() returns false if the cookie did not expire yet');

$cookie = Cookie::create('foo')->withExpires(time() + 3600 * 24);

$this->assertFalse($cookie->isCleared(), '->isCleared() returns false if the cookie did not expire yet');
}

public function testCookieIsCleared()
Expand All @@ -166,6 +215,10 @@ public function testCookieIsCleared()

$this->assertTrue($cookie->isCleared(), '->isCleared() returns true if the cookie has expired');

$cookie = Cookie::create('foo')->withExpires(time() - 20);

$this->assertTrue($cookie->isCleared(), '->isCleared() returns true if the cookie has expired');

$cookie = Cookie::create('foo', 'bar');

$this->assertFalse($cookie->isCleared());
Expand All @@ -177,21 +230,55 @@ public function testCookieIsCleared()
$cookie = Cookie::create('foo', 'bar', -1);

$this->assertFalse($cookie->isCleared());

$cookie = Cookie::create('foo')->withExpires(-1);

$this->assertFalse($cookie->isCleared());
}

public function testToString()
{
$expected = 'foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; Max-Age=0; path=/; domain=.myfoodomain.com; secure; httponly';
$cookie = Cookie::create('foo', 'bar', $expire = strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true, true, false, null);
$this->assertEquals('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; Max-Age=0; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() returns string representation of the cookie');
$this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of the cookie');

$cookie = Cookie::create('foo')
->withValue('bar')
->withExpires(strtotime('Fri, 20-May-2011 15:25:52 GMT'))
->withDomain('.myfoodomain.com')
->withSecure(true)
->withSameSite(null);
$this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of the cookie');

$expected = 'foo=bar%20with%20white%20spaces; expires=Fri, 20-May-2011 15:25:52 GMT; Max-Age=0; path=/; domain=.myfoodomain.com; secure; httponly';
$cookie = Cookie::create('foo', 'bar with white spaces', strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true, true, false, null);
$this->assertEquals('foo=bar%20with%20white%20spaces; expires=Fri, 20-May-2011 15:25:52 GMT; Max-Age=0; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() encodes the value of the cookie according to RFC 3986 (white space = %20)');
$this->assertEquals($expected, (string) $cookie, '->__toString() encodes the value of the cookie according to RFC 3986 (white space = %20)');

$cookie = Cookie::create('foo')
->withValue('bar with white spaces')
->withExpires(strtotime('Fri, 20-May-2011 15:25:52 GMT'))
->withDomain('.myfoodomain.com')
->withSecure(true)
->withSameSite(null);
$this->assertEquals($expected, (string) $cookie, '->__toString() encodes the value of the cookie according to RFC 3986 (white space = %20)');

$expected = 'foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', $expire = time() - 31536001).'; Max-Age=0; path=/admin/; domain=.myfoodomain.com; httponly';
$cookie = Cookie::create('foo', null, 1, '/admin/', '.myfoodomain.com', false, true, false, null);
$this->assertEquals('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', $expire = time() - 31536001).'; Max-Age=0; path=/admin/; domain=.myfoodomain.com; httponly', (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');
$this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');

$cookie = Cookie::create('foo')
->withExpires(1)
->withPath('/admin/')
->withDomain('.myfoodomain.com')
->withSameSite(null);
$this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');

$expected = 'foo=bar; path=/; httponly; samesite=lax';
$cookie = Cookie::create('foo', 'bar');
$this->assertEquals('foo=bar; path=/; httponly; samesite=lax', (string) $cookie);
$this->assertEquals($expected, (string) $cookie);

$cookie = Cookie::create('foo')->withValue('bar');
$this->assertEquals($expected, (string) $cookie);
}

public function testRawCookie()
Expand All @@ -200,9 +287,21 @@ public function testRawCookie()
$this->assertFalse($cookie->isRaw());
$this->assertEquals('foo=b%20a%20r; path=/', (string) $cookie);

$cookie = Cookie::create('test')->withValue('t e s t')->withHttpOnly(false)->withSameSite(null);
$this->assertFalse($cookie->isRaw());
$this->assertEquals('test=t%20e%20s%20t; path=/', (string) $cookie);

$cookie = Cookie::create('foo', 'b+a+r', 0, '/', null, false, false, true, null);
$this->assertTrue($cookie->isRaw());
$this->assertEquals('foo=b+a+r; path=/', (string) $cookie);

$cookie = Cookie::create('foo')
->withValue('t+e+s+t')
->withHttpOnly(false)
->withRaw(true)
->withSameSite(null);
$this->assertTrue($cookie->isRaw());
$this->assertEquals('foo=t+e+s+t; path=/', (string) $cookie);
}

public function testGetMaxAge()
Expand Down Expand Up @@ -245,6 +344,9 @@ public function testSameSiteAttribute()

$cookie = new Cookie('foo', 'bar', 0, '/', null, false, true, false, '');
$this->assertNull($cookie->getSameSite());

$cookie = Cookie::create('foo')->withSameSite('Lax');
$this->assertEquals('lax', $cookie->getSameSite());
}

public function testSetSecureDefault()
Expand Down

0 comments on commit 6d41d7e

Please sign in to comment.