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
|
.vscode
|
||||||
cache
|
cache
|
||||||
file.db
|
file.db
|
||||||
|
.phpunit.result.cache
|
||||||
|
@ -13,6 +13,29 @@ class Cookie
|
|||||||
public const PREFIX_SECURE = "secure";
|
public const PREFIX_SECURE = "secure";
|
||||||
public const PREFIX_HOST = "host";
|
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.
|
* Get all cookies or a specific cookie.
|
||||||
*
|
*
|
||||||
@ -26,19 +49,15 @@ class Cookie
|
|||||||
?string $name = null,
|
?string $name = null,
|
||||||
?string $prefix = null
|
?string $prefix = null
|
||||||
): array|string|null {
|
): array|string|null {
|
||||||
$cookies = $context->req->header("Cookie");
|
$cookies = self::parseCookies($context->req->header("Cookie") ?? "");
|
||||||
if (!$cookies) {
|
|
||||||
return $name ? null : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$parsedCookies = self::parseCookies($cookies);
|
|
||||||
|
|
||||||
if ($name === null) {
|
if ($name === null) {
|
||||||
return $parsedCookies;
|
return $cookies;
|
||||||
}
|
}
|
||||||
|
|
||||||
$fullName = self::getPrefixedName($name, $prefix);
|
$fullName = self::getPrefixedName($name, $prefix);
|
||||||
return $parsedCookies[$fullName] ?? null;
|
|
||||||
|
return $cookies[$fullName] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,9 +91,17 @@ class Cookie
|
|||||||
string $name,
|
string $name,
|
||||||
array $options = []
|
array $options = []
|
||||||
): ?string {
|
): ?string {
|
||||||
$value = self::getCookie($context, $name);
|
$prefix = $options["prefix"] ?? "";
|
||||||
|
$value = self::getCookie($context, $name, $prefix);
|
||||||
|
|
||||||
|
if ($value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$options["expires"] = 1;
|
$options["expires"] = 1;
|
||||||
|
$options["path"] = $options["path"] ?? "/";
|
||||||
self::setCookie($context, $name, "", $options);
|
self::setCookie($context, $name, "", $options);
|
||||||
|
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,25 +120,8 @@ class Cookie
|
|||||||
?string $name = null,
|
?string $name = null,
|
||||||
?string $prefix = null
|
?string $prefix = null
|
||||||
): mixed {
|
): 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);
|
$value = self::getCookie($context, $name, $prefix);
|
||||||
|
|
||||||
if ($value === null) {
|
if ($value === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -137,9 +147,52 @@ class Cookie
|
|||||||
): void {
|
): void {
|
||||||
$signature = self::sign($value, $secret);
|
$signature = self::sign($value, $secret);
|
||||||
$signedValue = $value . "." . $signature;
|
$signedValue = $value . "." . $signature;
|
||||||
|
|
||||||
self::setCookie($context, $name, $signedValue, $options);
|
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.
|
* Parse a cookie string into an associative array.
|
||||||
*
|
*
|
||||||
@ -149,13 +202,19 @@ class Cookie
|
|||||||
private static function parseCookies(string $cookieString): array
|
private static function parseCookies(string $cookieString): array
|
||||||
{
|
{
|
||||||
$cookies = [];
|
$cookies = [];
|
||||||
|
if (empty($cookieString)) {
|
||||||
|
return $cookies;
|
||||||
|
}
|
||||||
|
|
||||||
$pairs = explode("; ", $cookieString);
|
$pairs = explode("; ", $cookieString);
|
||||||
|
|
||||||
foreach ($pairs as $pair) {
|
foreach ($pairs as $pair) {
|
||||||
$parts = explode("=", $pair, 2);
|
$parts = explode("=", $pair, 2);
|
||||||
if (count($parts) === 2) {
|
if (count($parts) === 2) {
|
||||||
$cookies[urldecode($parts[0])] = urldecode($parts[1]);
|
$cookies[urldecode($parts[0])] = urldecode($parts[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $cookies;
|
return $cookies;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,10 +267,6 @@ class Cookie
|
|||||||
$parts[] = "SameSite=" . $options["sameSite"];
|
$parts[] = "SameSite=" . $options["sameSite"];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($options["partitioned"]) && $options["partitioned"]) {
|
|
||||||
$parts[] = "Partitioned";
|
|
||||||
}
|
|
||||||
|
|
||||||
return implode("; ", $parts);
|
return implode("; ", $parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,11 +313,13 @@ class Cookie
|
|||||||
string $secret
|
string $secret
|
||||||
): string|false {
|
): string|false {
|
||||||
$parts = explode(".", $value, 2);
|
$parts = explode(".", $value, 2);
|
||||||
|
|
||||||
if (count($parts) !== 2) {
|
if (count($parts) !== 2) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
list($value, $signature) = $parts;
|
list($value, $signature) = $parts;
|
||||||
|
|
||||||
$expectedSignature = self::sign($value, $secret);
|
$expectedSignature = self::sign($value, $secret);
|
||||||
|
|
||||||
if (!hash_equals($signature, $expectedSignature)) {
|
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