From 6f82019ead915559d2b3d5fecf146c8ba81c8791 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 7 Jun 2018 16:17:07 +0200 Subject: [PATCH 1/3] Separate headers and cookies --- src/main/php/web/Response.class.php | 40 ++++++++++++++++++----------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/main/php/web/Response.class.php b/src/main/php/web/Response.class.php index d6c9c278..7aec2d10 100755 --- a/src/main/php/web/Response.class.php +++ b/src/main/php/web/Response.class.php @@ -13,6 +13,7 @@ class Response { private $flushed= false; private $status= 200; private $message= 'OK'; + private $cookies= []; private $headers= []; /** @param web.io.Output $output */ @@ -32,6 +33,16 @@ public function answer($status, $message= null) { $this->message= $message ?: Status::message($status); } + /** + * Sets a cookie + * + * @param web.Response $cookie + * @return void + */ + public function cookie(Cookie $cookie) { + $this->cookies[]= $cookie; + } + /** * Sets a header * @@ -55,16 +66,6 @@ public function header($name, $value, $append= false) { } } - /** - * Sets a cookie - * - * @param web.Response $cookie - * @return void - */ - public function cookie(Cookie $cookie) { - $this->headers['Set-Cookie'][]= $cookie->header(); - } - /** @return int */ public function status() { return $this->status; } @@ -77,6 +78,9 @@ public function output() { return $this->output; } /** @return bool */ public function flushed() { return $this->flushed; } + /** @return web.Cookie[] */ + public function cookies() { return $this->cookies; } + /** @return [:string|string[]] */ public function headers() { $r= []; @@ -86,6 +90,15 @@ public function headers() { return $r; } + /** @param web.io.Output $output */ + private function begin($output) { + $output->begin($this->status, $this->message, $this->cookies + ? array_merge($this->headers, ['Set-Cookie' => array_map(function($c) { return $c->header(); }, $this->cookies)]) + : $this->headers + ); + $this->flushed= true; + } + /** * Sends headers * @@ -97,9 +110,7 @@ public function flush($output= null) { throw new IllegalStateException('Response already flushed'); } - $output || $output= $this->output; - $output->begin($this->status, $this->message, $this->headers); - $this->flushed= true; + $this->begin($output ?: $this->output); } /** @@ -116,8 +127,7 @@ public function end() { $this->headers['Content-Length']= [0]; } - $this->output->begin($this->status, $this->message, $this->headers); - $this->flushed= true; + $this->begin($this->output); } $this->output->close(); } From be57f190a74d8bd485406f48d74c2fc308c0ca01 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 7 Jun 2018 16:17:27 +0200 Subject: [PATCH 2/3] Add name(), value() and attributes() accessors, implement lang.Value --- src/main/php/web/Cookie.class.php | 38 ++++++++++++++++++- .../php/web/unittest/CookieTest.class.php | 26 +++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/main/php/web/Cookie.class.php b/src/main/php/web/Cookie.class.php index ba062952..91d96bc2 100755 --- a/src/main/php/web/Cookie.class.php +++ b/src/main/php/web/Cookie.class.php @@ -3,6 +3,7 @@ use util\Date; use util\TimeSpan; use lang\IllegalArgumentException; +use lang\Value; /** * A HTTP/1.1 Cookie @@ -12,7 +13,7 @@ * @see https://www.owasp.org/index.php/SameSite * @test xp://web.unittest.CookieTest */ -class Cookie { +class Cookie implements Value { private $name, $value; private $expires= null; @@ -43,6 +44,25 @@ public function __construct($name, $value) { } } + /** @return string */ + public function name() { return $this->name; } + + /** @return string */ + public function value() { return $this->value; } + + /** @return [:var] */ + public function attributes() { + return [ + 'expires' => $this->expires, + 'maxAge' => $this->maxAge, + 'path' => $this->path, + 'domain' => $this->domain, + 'secure' => $this->secure, + 'httpOnly' => $this->httpOnly, + 'sameSite' => $this->sameSite, + ]; + } + /** * Set expiration date * @@ -143,4 +163,20 @@ public function header() { ($this->httpOnly ? '; HttpOnly' : '') ); } + + /** @return string */ + public function hashCode() { return crc32($this->header()); } + + /** @return string */ + public function toString() { return nameof($this).'<'.$this->header().'>'; } + + /** + * Compare + * + * @param var $value + * @return int + */ + public function compareTo($value) { + return $value instanceof self ? strcmp($this->header(), $value->header()) : 1; + } } \ No newline at end of file diff --git a/src/test/php/web/unittest/CookieTest.class.php b/src/test/php/web/unittest/CookieTest.class.php index 57f435ae..4ecdd8cc 100755 --- a/src/test/php/web/unittest/CookieTest.class.php +++ b/src/test/php/web/unittest/CookieTest.class.php @@ -22,6 +22,32 @@ public function cannot_create_with_semicolon() { new Cookie('name', ';'); } + #[@test] + public function name() { + $this->assertEquals('name', (new Cookie('name', 'value'))->name()); + } + + #[@test] + public function value() { + $this->assertEquals('value', (new Cookie('name', 'value'))->value()); + } + + #[@test] + public function attributes() { + $this->assertEquals( + [ + 'expires' => null, + 'maxAge' => null, + 'path' => null, + 'domain' => null, + 'secure' => false, + 'httpOnly' => true, + 'sameSite' => 'Lax', + ], + (new Cookie('name', 'value'))->attributes() + ); + } + #[@test] public function http_only_and_same_site_per_default() { $this->assertEquals( From d2ea3cca4a38a535ca16d38f0c4962416e8d701a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 7 Jun 2018 16:18:11 +0200 Subject: [PATCH 3/3] Rewrite cookie() and cookies() tests to use Response::cookies() --- .../php/web/unittest/ResponseTest.class.php | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/test/php/web/unittest/ResponseTest.class.php b/src/test/php/web/unittest/ResponseTest.class.php index b33f9239..24bff341 100755 --- a/src/test/php/web/unittest/ResponseTest.class.php +++ b/src/test/php/web/unittest/ResponseTest.class.php @@ -97,24 +97,21 @@ public function uri_header() { #[@test] public function cookie() { - $attr= '; SameSite=Lax; HttpOnly'; - $res= new Response(new TestOutput()); $res->cookie(new Cookie('theme', 'light')); - $this->assertEquals(['Set-Cookie' => 'theme=light'.$attr], $res->headers()); + $this->assertEquals('light', $res->cookies()[0]->value()); } #[@test] public function cookies() { - $attr= '; SameSite=Lax; HttpOnly'; + $cookies= [new Cookie('theme', 'Test'), (new Cookie('sessionToken', 'abc123'))->expires('Wed, 09 Jun 2021 10:18:14 GMT')]; $res= new Response(new TestOutput()); - $res->cookie(new Cookie('theme', 'Test')); - $res->cookie((new Cookie('sessionToken', 'abc123'))->expires('Wed, 09 Jun 2021 10:18:14 GMT')); - $this->assertEquals( - ['Set-Cookie' => ['theme=Test'.$attr, 'sessionToken=abc123; Expires=Wed, 09 Jun 2021 10:18:14 GMT'.$attr]], - $res->headers() - ); + foreach ($cookies as $cookie) { + $res->cookie($cookie); + } + + $this->assertEquals($cookies, $res->cookies()); } #[@test, @values([ @@ -205,4 +202,17 @@ public function transfer_stream_buffered() { $out->bytes() ); } + + #[@test] + public function cookies_and_headers_are_merged() { + $res= new Response(new TestOutput()); + $res->header('Content-Type', 'text/html'); + $res->cookie(new Cookie('toggle', 'future')); + $res->flush(); + + $this->assertEquals( + "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nSet-Cookie: toggle=future; SameSite=Lax; HttpOnly\r\n\r\n", + $res->output()->bytes() + ); + } } \ No newline at end of file