mirror of
https://github.com/notrab/dumbo.git
synced 2025-01-16 13:50:03 +01:00
feat(CookieHelper): add get and clear all methods
This commit is contained in:
parent
dd4b9153f6
commit
11cdba6253
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,3 +7,4 @@ composer.lock
|
||||
.vscode
|
||||
cache
|
||||
file.db
|
||||
.phpunit.result.cache
|
||||
|
@ -13,6 +13,29 @@ class Cookie
|
||||
public const PREFIX_SECURE = "secure";
|
||||
public const PREFIX_HOST = "host";
|
||||
|
||||
/**
|
||||
* Check if a cookie exists
|
||||
*
|
||||
* @param Context $context The context object containing request information
|
||||
* @param string $name The name of the cookie to check
|
||||
* @param string|null $value Optional value to check against
|
||||
* @param string $prefix Optional prefix for the cookie name
|
||||
* @return bool True if the cookie exists (and optionally matches the value), false otherwise
|
||||
*/
|
||||
public static function hasCookie(
|
||||
Context $context,
|
||||
string $name,
|
||||
?string $value = null,
|
||||
string $prefix = ""
|
||||
): bool {
|
||||
$cookies = self::parseCookies($context->req->header("Cookie") ?? "");
|
||||
$fullName = self::getPrefixedName($name, $prefix);
|
||||
$cookieValue = $cookies[$fullName] ?? null;
|
||||
|
||||
return $cookieValue !== null &&
|
||||
($value === null || $cookieValue === $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all cookies or a specific cookie.
|
||||
*
|
||||
@ -26,19 +49,15 @@ class Cookie
|
||||
?string $name = null,
|
||||
?string $prefix = null
|
||||
): array|string|null {
|
||||
$cookies = $context->req->header("Cookie");
|
||||
if (!$cookies) {
|
||||
return $name ? null : [];
|
||||
}
|
||||
|
||||
$parsedCookies = self::parseCookies($cookies);
|
||||
$cookies = self::parseCookies($context->req->header("Cookie") ?? "");
|
||||
|
||||
if ($name === null) {
|
||||
return $parsedCookies;
|
||||
return $cookies;
|
||||
}
|
||||
|
||||
$fullName = self::getPrefixedName($name, $prefix);
|
||||
return $parsedCookies[$fullName] ?? null;
|
||||
|
||||
return $cookies[$fullName] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -72,9 +91,17 @@ class Cookie
|
||||
string $name,
|
||||
array $options = []
|
||||
): ?string {
|
||||
$value = self::getCookie($context, $name);
|
||||
$prefix = $options["prefix"] ?? "";
|
||||
$value = self::getCookie($context, $name, $prefix);
|
||||
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$options["expires"] = 1;
|
||||
$options["path"] = $options["path"] ?? "/";
|
||||
self::setCookie($context, $name, "", $options);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
@ -93,25 +120,8 @@ class Cookie
|
||||
?string $name = null,
|
||||
?string $prefix = null
|
||||
): mixed {
|
||||
if ($name === null) {
|
||||
$allCookies = self::getCookie($context);
|
||||
$signedCookies = [];
|
||||
|
||||
foreach ($allCookies as $cookieName => $cookieValue) {
|
||||
$verifiedValue = self::verifySignedCookie(
|
||||
$cookieValue,
|
||||
$secret
|
||||
);
|
||||
|
||||
if ($verifiedValue !== false) {
|
||||
$signedCookies[$cookieName] = $verifiedValue;
|
||||
}
|
||||
}
|
||||
|
||||
return $signedCookies;
|
||||
}
|
||||
|
||||
$value = self::getCookie($context, $name, $prefix);
|
||||
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
@ -137,9 +147,52 @@ class Cookie
|
||||
): void {
|
||||
$signature = self::sign($value, $secret);
|
||||
$signedValue = $value . "." . $signature;
|
||||
|
||||
self::setCookie($context, $name, $signedValue, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh a cookie's expiration time if it exists.
|
||||
*
|
||||
* @param Context $context The context object for setting the response header
|
||||
* @param string $name The name of the cookie to refresh
|
||||
* @param array $options Additional options for the cookie
|
||||
* @return bool True if the cookie was refreshed, false if it doesn't exist
|
||||
*/
|
||||
public static function refreshCookie(
|
||||
Context $context,
|
||||
string $name,
|
||||
array $options = []
|
||||
): bool {
|
||||
$prefix = $options["prefix"] ?? "";
|
||||
$value = self::getCookie($context, $name, $prefix);
|
||||
|
||||
if ($value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self::setCookie($context, $name, $value, $options);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cookies.
|
||||
*
|
||||
* @param Context $context The context object for setting the response header
|
||||
*/
|
||||
public static function clearAllCookies(Context $context): void
|
||||
{
|
||||
$cookies = self::getCookie($context);
|
||||
|
||||
foreach ($cookies as $name => $value) {
|
||||
$context->header(
|
||||
"Set-Cookie",
|
||||
$name . "=; Expires=Thu, 01 Jan 1970 00:00:01 GMT"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a cookie string into an associative array.
|
||||
*
|
||||
@ -149,13 +202,19 @@ class Cookie
|
||||
private static function parseCookies(string $cookieString): array
|
||||
{
|
||||
$cookies = [];
|
||||
if (empty($cookieString)) {
|
||||
return $cookies;
|
||||
}
|
||||
|
||||
$pairs = explode("; ", $cookieString);
|
||||
|
||||
foreach ($pairs as $pair) {
|
||||
$parts = explode("=", $pair, 2);
|
||||
if (count($parts) === 2) {
|
||||
$cookies[urldecode($parts[0])] = urldecode($parts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return $cookies;
|
||||
}
|
||||
|
||||
@ -208,10 +267,6 @@ class Cookie
|
||||
$parts[] = "SameSite=" . $options["sameSite"];
|
||||
}
|
||||
|
||||
if (isset($options["partitioned"]) && $options["partitioned"]) {
|
||||
$parts[] = "Partitioned";
|
||||
}
|
||||
|
||||
return implode("; ", $parts);
|
||||
}
|
||||
|
||||
@ -258,11 +313,13 @@ class Cookie
|
||||
string $secret
|
||||
): string|false {
|
||||
$parts = explode(".", $value, 2);
|
||||
|
||||
if (count($parts) !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
list($value, $signature) = $parts;
|
||||
|
||||
$expectedSignature = self::sign($value, $secret);
|
||||
|
||||
if (!hash_equals($signature, $expectedSignature)) {
|
||||
|
175
tests/Helpers/CookieTest.php
Normal file
175
tests/Helpers/CookieTest.php
Normal file
@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace Dumbo\Tests\Helpers;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Dumbo\Helpers\Cookie;
|
||||
use Dumbo\Context;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
|
||||
class CookieTest extends TestCase
|
||||
{
|
||||
private $context;
|
||||
private $request;
|
||||
private $response;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->request = $this->createMock(ServerRequestInterface::class);
|
||||
$this->response = new Response();
|
||||
$this->context = new TestContext($this->request, $this->response);
|
||||
}
|
||||
|
||||
private function setRequestCookie(string $cookieString): void
|
||||
{
|
||||
$this->request
|
||||
->method("getHeader")
|
||||
->with("Cookie")
|
||||
->willReturn([$cookieString]);
|
||||
}
|
||||
|
||||
public function testHasCookie()
|
||||
{
|
||||
$this->setRequestCookie("test_cookie=value; other_cookie=other_value");
|
||||
|
||||
$this->assertTrue(Cookie::hasCookie($this->context, "test_cookie"));
|
||||
$this->assertFalse(
|
||||
Cookie::hasCookie($this->context, "non_existent_cookie")
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetCookie()
|
||||
{
|
||||
$this->setRequestCookie("test_cookie=value; other_cookie=other_value");
|
||||
|
||||
$this->assertEquals(
|
||||
"value",
|
||||
Cookie::getCookie($this->context, "test_cookie")
|
||||
);
|
||||
$this->assertNull(
|
||||
Cookie::getCookie($this->context, "non_existent_cookie")
|
||||
);
|
||||
}
|
||||
|
||||
public function testSetCookie()
|
||||
{
|
||||
Cookie::setCookie($this->context, "test_cookie", "new_value");
|
||||
|
||||
$setCookieHeaders = $this->context
|
||||
->getResponse()
|
||||
->getHeader("Set-Cookie");
|
||||
$this->assertCount(1, $setCookieHeaders);
|
||||
$this->assertStringContainsString(
|
||||
"test_cookie=new_value",
|
||||
$setCookieHeaders[0]
|
||||
);
|
||||
}
|
||||
|
||||
public function testDeleteCookie()
|
||||
{
|
||||
$this->setRequestCookie("test_cookie=value; other_cookie=other_value");
|
||||
|
||||
$deletedValue = Cookie::deleteCookie($this->context, "test_cookie");
|
||||
|
||||
$this->assertEquals("value", $deletedValue);
|
||||
$setCookieHeaders = $this->context
|
||||
->getResponse()
|
||||
->getHeader("Set-Cookie");
|
||||
$this->assertCount(1, $setCookieHeaders);
|
||||
$this->assertStringContainsString("test_cookie=", $setCookieHeaders[0]);
|
||||
$this->assertStringContainsString("Expires=", $setCookieHeaders[0]);
|
||||
}
|
||||
|
||||
public function testGetSignedCookie()
|
||||
{
|
||||
$secret = "test_secret";
|
||||
$value = "test_value";
|
||||
$signature = hash_hmac("sha256", $value, $secret);
|
||||
$signedValue = $value . "." . $signature;
|
||||
|
||||
$this->setRequestCookie("signed_cookie=$signedValue");
|
||||
|
||||
$this->assertEquals(
|
||||
$value,
|
||||
Cookie::getSignedCookie($this->context, $secret, "signed_cookie")
|
||||
);
|
||||
}
|
||||
|
||||
public function testSetSignedCookie()
|
||||
{
|
||||
$secret = "test_secret";
|
||||
$name = "signed_cookie";
|
||||
$value = "test_value";
|
||||
|
||||
Cookie::setSignedCookie($this->context, $name, $value, $secret);
|
||||
|
||||
$setCookieHeaders = $this->context
|
||||
->getResponse()
|
||||
->getHeader("Set-Cookie");
|
||||
$this->assertCount(1, $setCookieHeaders);
|
||||
$this->assertStringContainsString("$name=", $setCookieHeaders[0]);
|
||||
}
|
||||
|
||||
public function testRefreshCookie()
|
||||
{
|
||||
$this->setRequestCookie("test_cookie=old_value");
|
||||
|
||||
$refreshed = Cookie::refreshCookie($this->context, "test_cookie");
|
||||
|
||||
$this->assertTrue($refreshed);
|
||||
$setCookieHeaders = $this->context
|
||||
->getResponse()
|
||||
->getHeader("Set-Cookie");
|
||||
$this->assertCount(1, $setCookieHeaders);
|
||||
$this->assertStringContainsString(
|
||||
"test_cookie=old_value",
|
||||
$setCookieHeaders[0]
|
||||
);
|
||||
}
|
||||
|
||||
public function testClearAllCookies()
|
||||
{
|
||||
$this->setRequestCookie("cookie1=value1; cookie2=value2");
|
||||
|
||||
Cookie::clearAllCookies($this->context);
|
||||
|
||||
$setCookieHeaders = $this->context
|
||||
->getResponse()
|
||||
->getHeader("Set-Cookie");
|
||||
$this->assertCount(2, $setCookieHeaders);
|
||||
$this->assertStringContainsString(
|
||||
"cookie1=; Expires=",
|
||||
$setCookieHeaders[0]
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
"cookie2=; Expires=",
|
||||
$setCookieHeaders[1]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TestContext extends Context
|
||||
{
|
||||
private $response;
|
||||
|
||||
public function __construct(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response
|
||||
) {
|
||||
parent::__construct($request, [], "");
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
public function getResponse(): ResponseInterface
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
public function header(string $name, string $value): self
|
||||
{
|
||||
$this->response = $this->response->withAddedHeader($name, $value);
|
||||
return $this;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user