Skip to content

Commit

Permalink
Merge pull request #42 from xp-forge/feature/response-cookies
Browse files Browse the repository at this point in the history
Response cookies
  • Loading branch information
thekid authored Jun 7, 2018
2 parents b2c9ce8 + d2ea3cc commit c1485b2
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 26 deletions.
38 changes: 37 additions & 1 deletion src/main/php/web/Cookie.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use util\Date;
use util\TimeSpan;
use lang\IllegalArgumentException;
use lang\Value;

/**
* A HTTP/1.1 Cookie
Expand All @@ -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;
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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;
}
}
40 changes: 25 additions & 15 deletions src/main/php/web/Response.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Response {
private $flushed= false;
private $status= 200;
private $message= 'OK';
private $cookies= [];
private $headers= [];

/** @param web.io.Output $output */
Expand All @@ -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
*
Expand All @@ -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; }

Expand All @@ -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= [];
Expand All @@ -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
*
Expand All @@ -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);
}

/**
Expand All @@ -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();
}
Expand Down
26 changes: 26 additions & 0 deletions src/test/php/web/unittest/CookieTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
30 changes: 20 additions & 10 deletions src/test/php/web/unittest/ResponseTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -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()
);
}
}

0 comments on commit c1485b2

Please sign in to comment.