Skip to content

Commit

Permalink
Merge pull request #118 from Lemon-Framework/cookies
Browse files Browse the repository at this point in the history
[Http] CookieJar
  • Loading branch information
tenmajkl authored Jan 7, 2023
2 parents 52dc65f + 6885746 commit b8c1735
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 24 deletions.
18 changes: 18 additions & 0 deletions src/Lemon/Contracts/Http/CookieJar.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Lemon\Contracts\Http;

interface CookieJar
{
public function get(string $name): ?string;

public function set(string $name, string $value, int $expires = 0): static;

public function delete(string $name): static;

public function has(string $name): bool;

public function cookies(): array;
}
5 changes: 3 additions & 2 deletions src/Lemon/Debug/Handling/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ public function handle(\Throwable $problem): void
}

if ($this->config->get('debug.debug')) {
(new Reporter($problem, $this->application->get('request'), $this->application))->report();
$request = $this->application->has('request') ? $this->application->get('request') : null ;
(new Reporter($problem, $request, $this->application))->report();
} else {
$this->response->error(500)->send();
$this->response->error(500)->send($this->application);
$this->logger->error((string) $problem);
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/Lemon/Debug/Handling/Reporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ class Reporter

public function __construct(
private \Throwable $exception,
private Request $request,
private ?Request $request,
private Application $application
) {
$this->consultant = new Consultant();
}

public function report(): void
{
(new TemplateResponse($this->getTemplate(), 500))->send();
(new TemplateResponse($this->getTemplate(), 500))->send($this->application);
}

public function getTemplate(): Template
Expand All @@ -50,7 +50,7 @@ public function getData(): array
'message' => $problem->getMessage(),
'hint' => $this->consultant->giveAdvice($problem->getMessage()),
'trace' => $this->getTrace(),
'request' => $this->request->toArray(),
'request' => is_null($this->request) ? [] : $this->request->toArray(),
];
}

Expand Down
49 changes: 49 additions & 0 deletions src/Lemon/Http/CookieJar.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Lemon\Http;

use Lemon\Contracts\Http\CookieJar as CookieJarContract;
use Lemon\Kernel\Application;

class CookieJar implements CookieJarContract
{
private array $set_cookies = [];

public function __construct(
private Request $request,
) {

}

public function get(string $name): ?string
{
return $this->request->getCookie($name);
}

public function set(string $name, string $value, int $expires = 0): static
{
$this->set_cookies[] = [$name, $value, $expires];
return $this;
}

public function delete(string $name): static
{
if (!$this->request->hasCookie($name)) {
return $this;
}
$this->set_cookies[] = [$name, '', -1];
return $this;
}

public function has(string $name): bool
{
return $this->request->hasCookie($name);
}

public function cookies(): array
{
return $this->set_cookies;
}
}
12 changes: 8 additions & 4 deletions src/Lemon/Http/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

namespace Lemon\Http;

use Lemon\Contracts\Http\CookieJar;
use Lemon\Kernel\Application;


/**
* Represents Http Response.
*
Expand Down Expand Up @@ -106,12 +110,12 @@ public function __toString(): string
/**
* Sends response data back to user.
*/
public function send(): static
public function send(Application $app): static
{
$body = $this->parseBody();
$this->handleHeaders();
$this->handleStatusCode();
$this->handleCookies();
$this->handleCookies($app->has('cookies') ? $app->get('cookies')->cookies() : []);
$this->handleBody($body);

return $this;
Expand Down Expand Up @@ -216,9 +220,9 @@ public function handleBody(string $body): void
echo $body;
}

public function handleCookies(): void
public function handleCookies(array $cookies): void
{
foreach ($this->cookies as $cookie) {
foreach ([...$this->cookies, ...$cookies] as $cookie) {
setcookie(...[...$cookie, 'httponly' => false]);
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/Lemon/Kernel/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ final class Application extends Container
\Lemon\Validation\Validator::class => ['validation', Contracts\Validation\Validator::class],
\Lemon\Translating\Translator::class => ['translator', Contracts\Translating\Translator::class],
\Lemon\Highlighter\Highlighter::class => ['highlighter', Contracts\Highlighter\Highlighter::class],
\Lemon\Http\CookieJar::class => ['cookies', Contracts\Http\CookieJar::class],
];

/**
Expand Down Expand Up @@ -191,7 +192,7 @@ public function runsInTerminal(): bool
public function boot(): void
{
try {
$this->get('routing')->dispatch($this->get(Request::class))->send();
$this->get('routing')->dispatch($this->get(Request::class))->send($this);
} catch (\Exception|\Error $e) {
$this->handle($e);
}
Expand Down
8 changes: 3 additions & 5 deletions src/Lemon/Protection/Middlwares/Csrf.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@

namespace Lemon\Protection\Middlwares;

use Lemon\Contracts\Http\CookieJar;
use Lemon\Contracts\Http\ResponseFactory;
use Lemon\Contracts\Protection\Csrf as ProtectionCsrf;
use Lemon\Http\Request;
use Lemon\Http\Response;
use Lemon\Routing\Attributes\AfterAction;

class Csrf
{
#[AfterAction()]
public function handle(Request $request, ProtectionCsrf $csrf, ResponseFactory $responseFactory, Response $response)
public function handle(Request $request, ProtectionCsrf $csrf, ResponseFactory $responseFactory, CookieJar $cookies)
{
if ('GET' != $request->method) {
$cookie = $request->getCookie('CSRF_TOKEN');
Expand All @@ -22,6 +20,6 @@ public function handle(Request $request, ProtectionCsrf $csrf, ResponseFactory $
}
}

return $response->cookie('CSRF_TOKEN', $csrf->getToken());
$cookies->set('CSRF_TOKEN', $csrf->getToken());
}
}
49 changes: 49 additions & 0 deletions tests/Http/CookieJarTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Lemon\Tests\Http;

use Lemon\Http\CookieJar;
use Lemon\Http\Request;
use Lemon\Tests\TestCase;

class CookieJarTest extends TestCase
{
public function getJar(array $cookies = []): CookieJar
{
return new CookieJar(new Request('', '', '', [], '', $cookies, [], ''));
}

public function testGet()
{
$jar = $this->getJar(['foo' => 'bar']);
$this->assertSame('bar', $jar->get('foo'));
$this->assertNull($jar->get('parek'));
}

public function testSet()
{
$jar = $this->getJar();
$jar->set('foo', 'bar');
$jar->set('bar', 'baz', 3600);

$this->assertSame([['foo', 'bar', 0], ['bar', 'baz', 3600]], $jar->cookies());
}

public function getDelete()
{
$jar = $this->getJar(['foo' => 'bar']);
$jar->delete('foo');
$this->assertSame([['foo', '', -1]], $jar->cookies());
$jar->delete('bar');
$this->assertSame([['foo', '', -1]], $jar->cookies());
}

public function testHas()
{
$jar = $this->getJar(['foo' => 'bar']);
$this->assertTrue($jar->has('foo'));
$this->assertFalse($jar->has('parek'));
}
}
21 changes: 13 additions & 8 deletions tests/Protection/CsrfTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Lemon\Tests\Protection;

use Lemon\Config\Config;
use Lemon\Http\CookieJar;
use Lemon\Http\Request;
use Lemon\Http\ResponseFactory;
use Lemon\Http\Responses\HtmlResponse;
Expand Down Expand Up @@ -43,21 +44,25 @@ public function testMiddleware()
$f = new ResponseFactory(new Factory($cf, new Compiler($cf), $lc), $lc);

$r = new Request('/', '', 'GET', [], '', [], [], ''); // Lets say we have regular get request
$res = new HtmlResponse();
$this->assertSame([['CSRF_TOKEN', $c->getToken(), 0]], $m->handle($r, $c, $f, $res)->cookies()); // Now user has the token in cookie
$cj = new CookieJar($r);
$m->handle($r, $c, $f, $cj);
$this->assertSame([['CSRF_TOKEN', $c->getToken(), 0]], $cj->cookies()); // Now user has the token in cookie

$r = new Request('/', '', 'POST', ['Content-Type' => 'application/x-www-form-urlencoded'], 'CSRF_TOKEN='.$c->getToken(), ['CSRF_TOKEN' => $c->getToken()], [], '');
$res = new HtmlResponse();
$this->assertSame([['CSRF_TOKEN', $c->getToken(), 0]], $m->handle($r, $c, $f, $res)->cookies()); // Now user has new token in cookie
$cj = new CookieJar($r);
$m->handle($r, $c, $f, $cj);
$this->assertSame([['CSRF_TOKEN', $c->getToken(), 0]], $cj->cookies()); // Now user has new token in cookie

$r = new Request('/', '', 'POST', ['Content-Type' => 'application/x-www-form-urlencoded'], 'CSRF_TOKEN='.$c->getToken(), [], [], '');
$res = new HtmlResponse();
$this->assertSame(400, $m->handle($r, $c, $f, $res)->code()); // But when something is missing
$cj = new CookieJar($r);
$this->assertSame(400, $m->handle($r, $c, $f, $cj)->code()); // But when something is missing

$r = new Request('/', '', 'PUT', ['Content-Type' => 'application/x-www-form-urlencoded'], 'CSRF_TOKEN='.$c->getToken(), [], [], '');
$this->assertSame(400, $m->handle($r, $c, $f, $res)->code()); // But when something is missing
$cj = new CookieJar($r);
$this->assertSame(400, $m->handle($r, $c, $f, $cj)->code()); // But when something is missing

$r = new Request('/', '', 'POST', [], '', ['CSRF_TOKEN' => $c->getToken()], [], '');
$this->assertSame(400, $m->handle($r, $c, $f, $res)->code());
$cj = new CookieJar($r);
$this->assertSame(400, $m->handle($r, $c, $f, $cj)->code());
}
}
6 changes: 5 additions & 1 deletion tests/Routing/MiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
namespace Lemon\Tests\Routing;

use Lemon\Config\Config;
use Lemon\Contracts\Http\CookieJar as CookieJarContract;
use Lemon\Contracts\Http\ResponseFactory as ResponseFactoryContract;
use Lemon\Contracts\Protection\Csrf as CsrfContract;
use Lemon\Contracts\Routing\Router as RouterContract;
use Lemon\Http\CookieJar;
use Lemon\Http\Request;
use Lemon\Http\ResponseFactory;
use Lemon\Http\Responses\HtmlResponse;
Expand Down Expand Up @@ -51,6 +53,9 @@ public function testCollection()
$f = new Factory($c, new Compiler($c), $l);
$r = new Router($l, $rs = new ResponseFactory($f, $l));

$l->add(CookieJar::class);
$l->alias(CookieJarContract::class, CookieJar::class);

$l->add(Request::class, $re = new Request('/', '', 'GET', [], '', [], [], ''));

$l->add(ProtectionCsrf::class);
Expand All @@ -66,7 +71,6 @@ public function testCollection()

$r->get('/', fn () => 'foo')->middleware(Csrf::class);


$r->get('admin', fn(Logger $logger) => $logger->log('foo'))->middleware([TestingMiddleware::class, 'onlyAuthenticated']);

$r->get('foo', fn(Logger $logger) => $logger->log('foo'))->middleware([TestingMiddleware::class, 'onlyAuthenticatedButAfter']);
Expand Down

0 comments on commit b8c1735

Please sign in to comment.