diff --git a/src/Cookie/RequestCookie.php b/src/Cookie/RequestCookie.php index c6dec37..76b6650 100644 --- a/src/Cookie/RequestCookie.php +++ b/src/Cookie/RequestCookie.php @@ -80,6 +80,18 @@ public function getName(): string return $this->name; } + public function withName(string $name): self + { + if (!\preg_match('(^[^()<>@,;:\\\"/[\]?={}\x01-\x20\x7F]++$)', $name)) { + throw new InvalidCookieException("Invalid cookie name: '{$name}'"); + } + + $clone = clone $this; + $clone->name = $name; + + return $clone; + } + /** * @return string Value of the cookie. */ @@ -88,6 +100,18 @@ public function getValue(): string return $this->value; } + public function withValue(string $value): self + { + if (!\preg_match('(^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]*+$)', $value)) { + throw new InvalidCookieException("Invalid cookie value: '{$value}'"); + } + + $clone = clone $this; + $clone->value = $value; + + return $clone; + } + /** * @return string Representation of the cookie as in a 'cookie' header. */ diff --git a/src/Cookie/ResponseCookie.php b/src/Cookie/ResponseCookie.php index 8b2169d..006b19f 100644 --- a/src/Cookie/ResponseCookie.php +++ b/src/Cookie/ResponseCookie.php @@ -161,6 +161,18 @@ public function getName(): string return $this->name; } + public function withName(string $name): self + { + if (!\preg_match('(^[^()<>@,;:\\\"/[\]?={}\x01-\x20\x7F]++$)', $name)) { + throw new InvalidCookieException("Invalid cookie name: '{$name}'"); + } + + $clone = clone $this; + $clone->name = $name; + + return $clone; + } + /** * @return string Value of the cookie. */ @@ -169,6 +181,18 @@ public function getValue(): string return $this->value; } + public function withValue(string $value): self + { + if (!\preg_match('(^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]*+$)', $value)) { + throw new InvalidCookieException("Invalid cookie value: '{$value}'"); + } + + $clone = clone $this; + $clone->value = $value; + + return $clone; + } + /** * @return \DateTimeImmutable|null Expiry if set, otherwise `null`. * @@ -179,6 +203,16 @@ public function getExpiry() return $this->attributes->getExpiry(); } + public function withExpiry(\DateTimeInterface $expiry): self + { + return $this->withAttributes($this->attributes->withExpiry($expiry)); + } + + public function withoutExpiry(): self + { + return $this->withAttributes($this->attributes->withoutExpiry()); + } + /** * @return int|null Max-Age if set, otherwise `null`. * @@ -189,6 +223,16 @@ public function getMaxAge() return $this->attributes->getMaxAge(); } + public function withMaxAge(int $maxAge): self + { + return $this->withAttributes($this->attributes->withMaxAge($maxAge)); + } + + public function withoutMaxAge(): self + { + return $this->withAttributes($this->attributes->withoutMaxAge()); + } + /** * @return string Cookie path. * @@ -199,6 +243,11 @@ public function getPath(): string return $this->attributes->getPath(); } + public function withPath(string $path): self + { + return $this->withAttributes($this->attributes->withPath($path)); + } + /** * @return string Cookie domain. * @@ -209,6 +258,11 @@ public function getDomain(): string return $this->attributes->getDomain(); } + public function withDomain(string $domain): self + { + return $this->withAttributes($this->attributes->withDomain($domain)); + } + /** * @return bool Whether the secure flag is enabled or not. * @@ -219,6 +273,16 @@ public function isSecure(): bool return $this->attributes->isSecure(); } + public function withSecure(): self + { + return $this->withAttributes($this->attributes->withSecure()); + } + + public function withoutSecure(): self + { + return $this->withAttributes($this->attributes->withoutSecure()); + } + /** * @return bool Whether the httpOnly flag is enabled or not. * @@ -229,6 +293,16 @@ public function isHttpOnly(): bool return $this->attributes->isHttpOnly(); } + public function withHttpOnly(): self + { + return $this->withAttributes($this->attributes->withHttpOnly()); + } + + public function withoutHttpOnly(): self + { + return $this->withAttributes($this->attributes->withoutHttpOnly()); + } + /** * @return CookieAttributes All cookie attributes. */ @@ -237,6 +311,14 @@ public function getAttributes(): CookieAttributes return $this->attributes; } + public function withAttributes(CookieAttributes $attributes): self + { + $clone = clone $this; + $clone->attributes = $attributes; + + return $clone; + } + /** * @return string Representation of the cookie as in a 'set-cookie' header. */ diff --git a/src/Status.php b/src/Status.php index a978944..a27fc32 100644 --- a/src/Status.php +++ b/src/Status.php @@ -72,10 +72,12 @@ final class Status const NOT_EXTENDED = 510; const NETWORK_AUTHENTICATION_REQUIRED = 511; + // @codeCoverageIgnoreStart private function __construct() { // forbid instances } + // @codeCoverageIgnoreEnd public static function getReason(int $code): string { diff --git a/test/Cookie/RequestCookieTest.php b/test/Cookie/RequestCookieTest.php index 6cb7ce1..c6c3af6 100644 --- a/test/Cookie/RequestCookieTest.php +++ b/test/Cookie/RequestCookieTest.php @@ -27,6 +27,15 @@ public function testInvalidCookieName() new RequestCookie("foo bar"); } + public function testInvalidCookieNameModify() + { + $cookie = new RequestCookie("foobar"); + + $this->expectException(InvalidCookieException::class); + + $cookie->withName('foo bar'); + } + public function testInvalidCookieValue() { $this->expectException(InvalidCookieException::class); @@ -34,6 +43,15 @@ public function testInvalidCookieValue() new RequestCookie("foobar", "what is this"); } + public function testInvalidCookieValueModify() + { + $cookie = new RequestCookie("foobar", "what-is-this"); + + $this->expectException(InvalidCookieException::class); + + $cookie->withValue('what is this'); + } + public function testGetters() { $cookie = new RequestCookie("foobar", "baz"); @@ -42,4 +60,22 @@ public function testGetters() $this->assertSame("baz", $cookie->getValue()); $this->assertSame("foobar=baz", (string) $cookie); } + + public function testModifyName() + { + $cookie = new RequestCookie("foobar", "what-is-this"); + $newCookie = $cookie->withName('bar'); + + $this->assertSame('foobar', $cookie->getName()); + $this->assertSame('bar', $newCookie->getName()); + } + + public function testModifyValue() + { + $cookie = new RequestCookie("foobar", "what-is-this"); + $newCookie = $cookie->withValue('what-is-that'); + + $this->assertSame('what-is-this', $cookie->getValue()); + $this->assertSame('what-is-that', $newCookie->getValue()); + } } diff --git a/test/Cookie/ResponseCookieTest.php b/test/Cookie/ResponseCookieTest.php index e2df32a..7250a71 100644 --- a/test/Cookie/ResponseCookieTest.php +++ b/test/Cookie/ResponseCookieTest.php @@ -126,4 +126,126 @@ public function testToString() $this->assertSame("foobar=xxx; HttpOnly", (string) $cookie); } + + public function testModifyName() + { + $cookie = new ResponseCookie("foobar", "what-is-this"); + $newCookie = $cookie->withName('bar'); + + $this->assertSame('foobar', $cookie->getName()); + $this->assertSame('bar', $newCookie->getName()); + } + + public function testModifyValue() + { + $cookie = new ResponseCookie("foobar", "what-is-this"); + $newCookie = $cookie->withValue('what-is-that'); + + $this->assertSame('what-is-this', $cookie->getValue()); + $this->assertSame('what-is-that', $newCookie->getValue()); + } + + public function testModifyHttpOnly() + { + $cookie = new ResponseCookie("foobar", "what-is-this"); + $newCookie = $cookie->withoutHttpOnly(); + + $this->assertTrue($cookie->isHttpOnly()); + $this->assertTrue($newCookie->withHttpOnly()->isHttpOnly()); + $this->assertFalse($newCookie->isHttpOnly()); + } + + public function testModifySecure() + { + $cookie = new ResponseCookie("foobar", "what-is-this"); + $newCookie = $cookie->withSecure(); + + $this->assertFalse($cookie->isSecure()); + $this->assertFalse($newCookie->withoutSecure()->isSecure()); + $this->assertTrue($newCookie->isSecure()); + } + + public function testModifyDomain() + { + $cookie = new ResponseCookie("foobar", "what-is-this"); + $newCookie = $cookie->withDomain('example.com'); + + $this->assertSame('', $cookie->getDomain()); + $this->assertSame('example.com', $newCookie->getDomain()); + } + + public function testModifyPath() + { + $cookie = new ResponseCookie("foobar", "what-is-this"); + $newCookie = $cookie->withPath('/example'); + + $this->assertSame('', $cookie->getPath()); + $this->assertSame('/example', $newCookie->getPath()); + } + + public function testModifyExpiry() + { + $cookie = new ResponseCookie("foobar", "what-is-this"); + $newCookie = $cookie->withExpiry(\DateTimeImmutable::createFromFormat('Y-m-d', '2019-06-10')); + + $this->assertNull($cookie->getExpiry()); + $this->assertNull($newCookie->withoutExpiry()->getExpiry()); + $this->assertSame('2019-06-10', $newCookie->getExpiry()->format('Y-m-d')); + } + + public function testModifyExpiryMutable() + { + $cookie = new ResponseCookie("foobar", "what-is-this"); + $expiry = \DateTime::createFromFormat('Y-m-d', '2019-06-10'); + $newCookie = $cookie->withExpiry($expiry); + + $this->assertNull($cookie->getExpiry()); + $this->assertNull($newCookie->withoutExpiry()->getExpiry()); + $this->assertSame('2019-06-10', $newCookie->getExpiry()->format('Y-m-d')); + + $expiry->add(new \DateInterval('P2D')); + $this->assertSame('2019-06-10', $newCookie->getExpiry()->format('Y-m-d')); + } + + public function testModifyMaxAge() + { + $cookie = new ResponseCookie("foobar", "what-is-this"); + $newCookie = $cookie->withMaxAge(12); + + $this->assertNull($cookie->getMaxAge()); + $this->assertNull($newCookie->withoutMaxAge()->getMaxAge()); + $this->assertSame(12, $newCookie->getMaxAge()); + } + + public function testInvalidCookieName() + { + $this->expectException(InvalidCookieException::class); + + new ResponseCookie("foo bar"); + } + + public function testInvalidCookieNameModify() + { + $cookie = new ResponseCookie("foobar"); + + $this->expectException(InvalidCookieException::class); + + $cookie->withName('foo bar'); + } + + public function testInvalidCookieValue() + { + $this->expectException(InvalidCookieException::class); + + new ResponseCookie("foobar", "what is this"); + } + + public function testInvalidCookieValueModify() + { + $cookie = new ResponseCookie("foobar", "what-is-this"); + + $this->expectException(InvalidCookieException::class); + + $cookie->withValue('what is this'); + } }