diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a238b8..44989f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: operating-system: [ ubuntu-latest ] - php-versions: [ '7.2','7.3', '7.4', '8.0', '8.1' ] + php-versions: [ '7.4', '8.0', '8.1' ] runs-on: ${{ matrix.operating-system }} diff --git a/.mddoc.xml.dist b/.mddoc.xml.dist index f1bb507..b88401c 100644 --- a/.mddoc.xml.dist +++ b/.mddoc.xml.dist @@ -18,9 +18,10 @@
- \ No newline at end of file + diff --git a/LICENSE.md b/LICENSE.md index 90a5463..bd7f42b 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -3,6 +3,7 @@ The MIT License Copyright (c) 2021 Jesse G. Donat Copyright (c) 2017 Woody Gilk +Copyright (c) 2017 Hans Ott hansott@hotmail.be Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/composer.json b/composer.json index 0b78ce5..f9851d7 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "type": "library", "require": { "psr/http-message": "^1.0", - "php": ">=7.2" + "php": ">=7.4" }, "require-dev": { "phpunit/phpunit": "^6.5 || ^9.3", diff --git a/src/Cookie/CookieBuilder.php b/src/Cookie/CookieBuilder.php new file mode 100644 index 0000000..b0bf556 --- /dev/null +++ b/src/Cookie/CookieBuilder.php @@ -0,0 +1,175 @@ +name = $name; + $this->value = $value; + $this->expiration = $expiration; + $this->path = $path; + $this->domain = $domain; + $this->secure = $secure; + $this->httpOnly = $httpOnly; + $this->sameSite = $sameSite; + } + + /** + * Return an instance with the specified name. + */ + public function withName( string $name ) : self { + $clone = clone $this; + $clone->name = $name; + + return $clone; + } + + /** + * Return an instance with the specified value. + */ + public function withValue( string $value ) : self { + $that = clone $this; + $that->value = $value; + + return $that; + } + + /** + * Return an instance with an expiration in the past and a cleared value. + */ + public function withExpireNow() : self { + $that = clone $this; + $that->expiration = -604800; + $that->value = ''; + + return $that; + } + + /** + * Return an instance with the specified duration. + * + * @param int $expiration The number of seconds for which this cookie will be valid. `time() + $expiration` + */ + public function withExpiration( int $expiration ) : self { + $that = clone $this; + $that->expiration = $expiration; + + return $that; + } + + /** + * Return an instance with the specified path. + */ + public function withPath( string $path ) : self { + $that = clone $this; + $that->path = $path; + + return $that; + } + + /** + * Return an instance with the specified domain. + */ + public function withDomain( string $domain ) : self { + $that = clone $this; + $that->domain = $domain; + + return $that; + } + + /** + * Return an instance with the specified secure flag. + */ + public function withSecure( bool $secure ) : self { + $that = clone $this; + $that->secure = $secure; + + return $that; + } + + /** + * Return an instance with the specified httpOnly flag. + */ + public function withHttpOnly( bool $httpOnly ) : self { + $that = clone $this; + $that->httpOnly = $httpOnly; + + return $that; + } + + /** + * Return an instance with the specified SameSite value. + */ + public function withSameSite( string $sameSite ) : self { + $that = clone $this; + $that->sameSite = $sameSite; + + return $that; + } + + public function getName() : string { + return $this->name; + } + + public function getValue() : string { + return $this->value; + } + + public function getExpiration() : int { + return $this->expiration; + } + + public function getPath() : string { + return $this->path; + } + + public function getDomain() : string { + return $this->domain; + } + + public function isSecure() : bool { + return $this->secure; + } + + public function isHttpOnly() : bool { + return $this->httpOnly; + } + + public function getSameSite() : string { + return $this->sameSite; + } + +} diff --git a/src/Cookie/CookieEncoder.php b/src/Cookie/CookieEncoder.php new file mode 100644 index 0000000..c07f36a --- /dev/null +++ b/src/Cookie/CookieEncoder.php @@ -0,0 +1,95 @@ +getName(), urlencode($cookie->getValue())); + + if( $cookie->getExpiration() !== 0 ) { + $headerValue .= sprintf( + '; expires=%s', + gmdate(DATE_RFC1123, time() + $cookie->getExpiration()) + ); + } + + if( $cookie->getPath() ) { + $headerValue .= sprintf('; path=%s', $cookie->getPath()); + } + + if( $cookie->getDomain() ) { + $headerValue .= sprintf('; domain=%s', $cookie->getDomain()); + } + + if( $cookie->isSecure() ) { + $headerValue .= '; secure'; + } + + if( $cookie->isHttpOnly() ) { + $headerValue .= '; httponly'; + } + + if( $cookie->getSameSite() ) { + $headerValue .= sprintf('; samesite=%s', $cookie->getSameSite()); + } + + return $headerValue; + } + + /** + * Apply the Cookie to `setcookie` a callable matching the signature of PHP 7.4+ + * `setcookie(string $name, string $value = "", array $options = []) : bool` + * + * @param callable|null $callee The `setcookie` compatible callback to be used. + * If set to null, the default setcookie() + */ + public function apply( CookieInterface $cookie, ?callable $callee = null ) : bool { + if( $callee === null ) { + $callee = '\\setcookie'; + } + + return $callee( + $cookie->getName(), + $cookie->getValue(), + [ + 'expires' => $cookie->getExpiration() + time(), + 'path' => $cookie->getPath(), + 'domain' => $cookie->getDomain(), + 'secure' => $cookie->isSecure(), + 'httponly' => $cookie->isHttpOnly(), + 'samesite' => $cookie->getSameSite(), + ], + ); + } + + /** + * Given a \Psr\Http\Message\ResponseInterface returns a new instance of ResponseInterface with an added + * `Set-Cookie` header representing this Cookie. + */ + public function responseWithHeaderAdded( CookieInterface $cookie, ResponseInterface $response ) : ResponseInterface { + return $response->withAddedHeader('Set-Cookie', $this->encode($cookie)); + } + + /** + * Given a \Psr\Http\Message\ResponseInterface returns a new instance of ResponseInterface replacing any + * `Set-Cookie` headers with one representing this Cookie. + */ + public function responseWithHeader( CookieInterface $cookie, ResponseInterface $response ) : ResponseInterface { + return $response->withHeader('Set-Cookie', $this->encode($cookie)); + } + +} diff --git a/src/Cookie/CookieInterface.php b/src/Cookie/CookieInterface.php new file mode 100644 index 0000000..95978d3 --- /dev/null +++ b/src/Cookie/CookieInterface.php @@ -0,0 +1,47 @@ +withName('WayDifferentCookie'); + $this->assertSame('WayDifferentCookie', $valueCheck->getName()); + $this->assertSame('ExampleCookie', $cookieBuilder->getName()); + + $valueCheck = $cookieBuilder->withValue('test'); + $this->assertSame('test', $valueCheck->getValue()); + $this->assertSame('', $cookieBuilder->getValue()); + + $valueCheck = $cookieBuilder->withExpiration(123); + $this->assertSame(123, $valueCheck->getExpiration()); + $this->assertSame(0, $cookieBuilder->getExpiration()); + + $valueCheck = $cookieBuilder->withPath('/test'); + $this->assertSame('/test', $valueCheck->getPath()); + $this->assertSame('', $cookieBuilder->getPath()); + + $valueCheck = $cookieBuilder->withDomain('www.example.dev'); + $this->assertSame('www.example.dev', $valueCheck->getDomain()); + $this->assertSame('', $cookieBuilder->getDomain()); + + $valueCheck = $cookieBuilder->withHttpOnly(true); + $this->assertTrue($valueCheck->isHttpOnly()); + $this->assertFalse($cookieBuilder->isHttpOnly()); + + $valueCheck = $cookieBuilder->withSecure(true); + $this->assertTrue($valueCheck->isSecure()); + $this->assertFalse($cookieBuilder->isSecure()); + + $valueCheck = $cookieBuilder->withSameSite('Lax'); + $this->assertSame('Lax', $valueCheck->getSameSite()); + $this->assertSame('', $cookieBuilder->getSameSite()); + } + + public function test_withExpireNow() : void { + $cookieBuilder = (new CookieBuilder('ExampleCookie')) + ->withValue('bye bye birdie') + ->withExpiration(123); + + $cookieBuilder = $cookieBuilder->withExpireNow(); + $this->assertSame('', $cookieBuilder->getValue()); + $this->assertLessThan(0, $cookieBuilder->getExpiration()); + } + +}