Skip to content

Commit

Permalink
Add cookies page (#238)
Browse files Browse the repository at this point in the history
The list is generated automatically, and except the framework cookies,
you can't create a cookie without adding an item into the `CookieName`
enum. Which means that a test can be written, and was written, that
makes sure that all cookies are described.

Have to admit this is mostly a flex 😁
  • Loading branch information
spaze authored Sep 29, 2023
2 parents 8255311 + c65d170 commit 4a39963
Show file tree
Hide file tree
Showing 25 changed files with 479 additions and 53 deletions.
22 changes: 11 additions & 11 deletions site/app/Application/Theme.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,19 @@

namespace MichalSpacekCz\Application;

use MichalSpacekCz\Http\HttpInput;
use Nette\Http\IResponse;
use Nette\Http\Response;
use MichalSpacekCz\Http\Cookies\CookieName;
use MichalSpacekCz\Http\Cookies\Cookies;

class Theme
{

private const COOKIE = 'future';

private const DARK = 'dark';

private const LIGHT = 'bright';


public function __construct(
private readonly HttpInput $httpInput,
private readonly IResponse $httpResponse,
private readonly Cookies $cookies,
) {
}

Expand All @@ -38,16 +34,20 @@ public function setLightMode(): void

public function isDarkMode(): ?bool
{
$cookie = $this->httpInput->getCookieString(self::COOKIE);
$cookie = $this->cookies->getString(CookieName::Theme);
return $cookie === self::DARK ? true : ($cookie === self::LIGHT ? false : null);
}


private function setCookie(string $mode): void
{
/** @var Response $response Not IResponse because https://github.com/nette/http/issues/200, can't use instanceof check because it's a different Response in tests */
$response = $this->httpResponse;
$response->setCookie(self::COOKIE, $mode, '+10 years', null, null, null, null, 'None');
$this->cookies->set(CookieName::Theme, $mode, $this->getCookieLifetime(), sameSite: 'None');
}


public function getCookieLifetime(): string
{
return '365 days';
}

}
20 changes: 20 additions & 0 deletions site/app/DateTime/DateTime.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

namespace MichalSpacekCz\DateTime;

use Exception;
use MichalSpacekCz\ShouldNotHappenException;
use Nette\Utils\DateTime as NetteDateTime;

class DateTime
{

Expand All @@ -11,4 +15,20 @@ class DateTime
*/
public const DATE_RFC3339_MICROSECONDS = 'Y-m-d\TH:i:s.uP';


public function getDaysFromString(string $interval): int
{
$now = new NetteDateTime();
try {
$then = NetteDateTime::from($interval);
} catch (Exception $e) {
throw new ShouldNotHappenException("Cannot create an object from {$interval}", previous: $e);
}
$days = $now->diff($then)->days;
if ($days === false) {
throw new ShouldNotHappenException(sprintf('Cannot diff %s and %s', $now->format(DATE_RFC3339_EXTENDED), $then->format(DATE_RFC3339_EXTENDED)));
}
return $days;
}

}
43 changes: 43 additions & 0 deletions site/app/Http/Cookies/CookieDescription.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php
declare(strict_types = 1);

namespace MichalSpacekCz\Http\Cookies;

use Nette\Utils\Html;

class CookieDescription
{

public function __construct(
private readonly string $name,
private readonly bool $internal,
private readonly Html $description,
private readonly ?int $validDays,
) {
}


public function getName(): string
{
return $this->name;
}


public function isInternal(): bool
{
return $this->internal;
}


public function getDescription(): Html
{
return $this->description;
}


public function getValidDays(): ?int
{
return $this->validDays;
}

}
72 changes: 72 additions & 0 deletions site/app/Http/Cookies/CookieDescriptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
declare(strict_types = 1);

namespace MichalSpacekCz\Http\Cookies;

use MichalSpacekCz\Application\Theme;
use MichalSpacekCz\DateTime\DateTime;
use MichalSpacekCz\Formatter\TexyFormatter;
use MichalSpacekCz\ShouldNotHappenException;
use MichalSpacekCz\User\Manager;
use Nette\Http\Helpers;
use Nette\Http\Session;

class CookieDescriptions
{

public function __construct(
private readonly Manager $authenticator,
private readonly Theme $theme,
private readonly Session $sessionHandler,
private readonly TexyFormatter $texyFormatter,
private readonly DateTime $dateTime,
) {
}


/**
* @return list<CookieDescription>
*/
public function get(): array
{
$options = $this->sessionHandler->getOptions();
$cookieLifetime = $options['cookie_lifetime'];
if (!is_int($cookieLifetime)) {
throw new ShouldNotHappenException("The cookie_lifetime option should be an int, but it's a " . get_debug_type($cookieLifetime));
}
/** @noinspection PhpInternalEntityUsedInspection */
return [
new CookieDescription(
CookieName::PermanentLogin->value,
true,
$this->texyFormatter->translate('messages.cookies.cookie.permanentLogin'),
$this->dateTime->getDaysFromString($this->authenticator->getPermanentLoginCookieLifetime()),
),
new CookieDescription(
CookieName::ReturningUser->value,
true,
$this->texyFormatter->translate('messages.cookies.cookie.returningUser'),
$this->dateTime->getDaysFromString($this->authenticator->getReturningUserCookieLifetime()),
),
new CookieDescription(
CookieName::Theme->value,
false,
$this->texyFormatter->translate('messages.cookies.cookie.theme'),
$this->dateTime->getDaysFromString($this->theme->getCookieLifetime()),
),
new CookieDescription(
$this->sessionHandler->getName(),
false,
$this->texyFormatter->translate('messages.cookies.cookie.netteSession'),
$this->dateTime->getDaysFromString($cookieLifetime . ' seconds'),
),
new CookieDescription(
Helpers::StrictCookieName,
false,
$this->texyFormatter->translate('messages.cookies.cookie.netteSameSiteCheck'),
null,
),
];
}

}
13 changes: 13 additions & 0 deletions site/app/Http/Cookies/CookieName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php
declare(strict_types = 1);

namespace MichalSpacekCz\Http\Cookies;

enum CookieName: string
{

case PermanentLogin = '__Secure-permanent';
case ReturningUser = '__Secure-beenhere';
case Theme = 'future';

}
52 changes: 52 additions & 0 deletions site/app/Http/Cookies/Cookies.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php
declare(strict_types = 1);

namespace MichalSpacekCz\Http\Cookies;

use DateTimeInterface;
use Nette\Http\IRequest;
use Nette\Http\IResponse;
use Nette\Http\Response;

class Cookies
{

public function __construct(
private readonly IRequest $request,
private readonly IResponse $response,
) {
}


public function getString(CookieName $name): ?string
{
$cookie = $this->request->getCookie($name->value);
if (!is_string($cookie)) {
return null;
}
return $cookie;
}


public function set(
CookieName $name,
string $value,
DateTimeInterface|int|string $expire,
?string $path = null,
?string $domain = null,
?bool $secure = null,
?bool $httpOnly = null,
?string $sameSite = null,
): void {
/** @var Response $response Not IResponse because https://github.com/nette/http/issues/200, can't use instanceof check because it's a different Response in tests */
$response = $this->response;
$response->setCookie($name->value, $value, $expire, $path, $domain, $secure, $httpOnly, $sameSite);
}


public function delete(CookieName $name, ?string $path = null, ?string $domain = null, ?bool $secure = null): void
{
$this->response->deleteCookie($name->value, $path, $domain, $secure);
}

}
10 changes: 0 additions & 10 deletions site/app/Http/HttpInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,6 @@ public function __construct(
}


public function getCookieString(string $key): ?string
{
$cookie = $this->request->getCookie($key);
if (!is_string($cookie)) {
return null;
}
return $cookie;
}


public function getPostString(string $key): ?string
{
$data = $this->request->getPost($key);
Expand Down
31 changes: 20 additions & 11 deletions site/app/User/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

use DateTimeInterface;
use Exception;
use MichalSpacekCz\Http\HttpInput;
use MichalSpacekCz\Http\Cookies\CookieName;
use MichalSpacekCz\Http\Cookies\Cookies;
use MichalSpacekCz\User\Exceptions\IdentityException;
use MichalSpacekCz\User\Exceptions\IdentityIdNotIntException;
use MichalSpacekCz\User\Exceptions\IdentityNotSimpleIdentityException;
Expand All @@ -16,7 +17,6 @@
use Nette\Database\Row;
use Nette\Database\UniqueConstraintViolationException;
use Nette\Http\IRequest;
use Nette\Http\Response;
use Nette\Http\Url;
use Nette\Security\AuthenticationException;
use Nette\Security\Authenticator;
Expand Down Expand Up @@ -45,13 +45,10 @@ class Manager implements Authenticator
public function __construct(
private readonly Explorer $database,
private readonly IRequest $httpRequest,
private readonly Response $httpResponse, // Not IResponse because https://github.com/nette/http/issues/200
private readonly HttpInput $httpInput,
private readonly Cookies $cookies,
private readonly Passwords $passwords,
private readonly StaticKey $passwordEncryption,
LinkGenerator $linkGenerator,
private readonly string $returningUserCookie,
private readonly string $permanentLoginCookie,
private readonly string $permanentLoginInterval,
) {
$this->authCookiesPath = (new Url($linkGenerator->link('Admin:Sign:in')))->getPath();
Expand Down Expand Up @@ -190,13 +187,13 @@ public function isForbidden(): bool

public function setReturningUser(string $value): void
{
$this->httpResponse->setCookie($this->returningUserCookie, $value, '+10 years', $this->authCookiesPath, null, null, null, 'Strict');
$this->cookies->set(CookieName::ReturningUser, $value, $this->getReturningUserCookieLifetime(), $this->authCookiesPath, sameSite: 'Strict');
}


public function isReturningUser(): bool
{
$cookie = $this->httpInput->getCookieString($this->returningUserCookie);
$cookie = $this->cookies->getString(CookieName::ReturningUser);
return ($cookie && $this->verifyReturningUser($cookie));
}

Expand Down Expand Up @@ -256,7 +253,7 @@ private function insertToken(User $user, int $type): string
public function storePermanentLogin(User $user): void
{
$value = $this->insertToken($user, self::TOKEN_PERMANENT_LOGIN);
$this->httpResponse->setCookie($this->permanentLoginCookie, $value, $this->permanentLoginInterval, $this->authCookiesPath, null, null, null, 'Strict');
$this->cookies->set(CookieName::PermanentLogin, $value, $this->permanentLoginInterval, $this->authCookiesPath, sameSite: 'Strict');
}


Expand All @@ -268,7 +265,7 @@ public function storePermanentLogin(User $user): void
public function clearPermanentLogin(User $user): void
{
$this->database->query('DELETE FROM auth_tokens WHERE key_user = ? AND type = ?', $user->getId(), self::TOKEN_PERMANENT_LOGIN);
$this->httpResponse->deleteCookie($this->permanentLoginCookie, $this->authCookiesPath);
$this->cookies->delete(CookieName::PermanentLogin, $this->authCookiesPath);
}


Expand All @@ -292,7 +289,7 @@ public function regeneratePermanentLogin(User $user): void
*/
public function verifyPermanentLogin(): ?Row
{
$cookie = $this->httpInput->getCookieString($this->permanentLoginCookie) ?? '';
$cookie = $this->cookies->getString(CookieName::PermanentLogin) ?? '';
return $this->verifyToken($cookie, DateTime::from("-{$this->permanentLoginInterval}"), self::TOKEN_PERMANENT_LOGIN);
}

Expand Down Expand Up @@ -354,4 +351,16 @@ private function verifyToken(string $value, DateTimeInterface $validity, int $ty
return $result;
}


public function getPermanentLoginCookieLifetime(): string
{
return $this->permanentLoginInterval;
}


public function getReturningUserCookieLifetime(): string
{
return '365 days';
}

}
Loading

0 comments on commit 4a39963

Please sign in to comment.