Skip to content

Feature: Cookie Builder #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
operating-system: [ ubuntu-latest ]
php-versions: [ '7.2','7.3', '7.4', '8.0', '8.1' ]
php-versions: [ '7.4', '8.0', '8.1' ]

runs-on: ${{ matrix.operating-system }}

Expand Down
3 changes: 2 additions & 1 deletion .mddoc.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
<section title="Documentation">
<recursiveDirectory name="src" file-filter="/.*
(?&lt;!Factory\.php)
(?&lt;!Interface\.php)
(?&lt;!AuthorizationParts\.php)
$/x"/>
</section>
</section>
</docpage>
</mddoc>
</mddoc>
1 change: 1 addition & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ The MIT License

Copyright (c) 2021 Jesse G. Donat
Copyright (c) 2017 Woody Gilk
Copyright (c) 2017 Hans Ott [email protected]

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "library",
"require": {
"psr/http-message": "^1.0",
"php": ">=7.2"
"php": ">=7.4"
},
"require-dev": {
"phpunit/phpunit": "^6.5 || ^9.3",
Expand Down
175 changes: 175 additions & 0 deletions src/Cookie/CookieBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<?php

namespace Corpus\HttpMessageUtils\Cookie;

/**
* Utility to build an HTTP Cookie header
*/
class CookieBuilder implements CookieInterface {

private string $name;
private string $value;
private int $expiration;
private string $path;
private string $domain;
private bool $secure;
private bool $httpOnly;
private string $sameSite;

/**
* @param string $name The name of the cookie
* @param int $expiration The number of seconds for which this cookie will be valid. `time() + $expiration`
* @param string $path The path on which this cookie is available
* @param string $domain The domain to which the cookie is sent
* @param bool $secure Indicates that the cookie should only be transmitted over a secure HTTPS connection
* @param bool $httpOnly When true the cookie will be made accessible only through the HTTP protocol, e.g. Not
* JavaScript
* @param string $sameSite Set the SameSite value - expects "None", "Lax" or "Strict". If the samesite element is
* empty, no SameSite cookie attribute is set
*/
public function __construct(
string $name,
string $value = '',
int $expiration = 0,
string $path = '',
string $domain = '',
bool $secure = false,
bool $httpOnly = false,
string $sameSite = ''
) {
$this->name = $name;
$this->value = $value;
$this->expiration = $expiration;
$this->path = $path;
$this->domain = $domain;
$this->secure = $secure;
$this->httpOnly = $httpOnly;
$this->sameSite = $sameSite;
}

/**
* Return an instance with the specified name.
*/
public function withName( string $name ) : self {
$clone = clone $this;
$clone->name = $name;

return $clone;
}

/**
* Return an instance with the specified value.
*/
public function withValue( string $value ) : self {
$that = clone $this;
$that->value = $value;

return $that;
}

/**
* Return an instance with an expiration in the past and a cleared value.
*/
public function withExpireNow() : self {
$that = clone $this;
$that->expiration = -604800;
$that->value = '';

return $that;
}

/**
* Return an instance with the specified duration.
*
* @param int $expiration The number of seconds for which this cookie will be valid. `time() + $expiration`
*/
public function withExpiration( int $expiration ) : self {
$that = clone $this;
$that->expiration = $expiration;

return $that;
}

/**
* Return an instance with the specified path.
*/
public function withPath( string $path ) : self {
$that = clone $this;
$that->path = $path;

return $that;
}

/**
* Return an instance with the specified domain.
*/
public function withDomain( string $domain ) : self {
$that = clone $this;
$that->domain = $domain;

return $that;
}

/**
* Return an instance with the specified secure flag.
*/
public function withSecure( bool $secure ) : self {
$that = clone $this;
$that->secure = $secure;

return $that;
}

/**
* Return an instance with the specified httpOnly flag.
*/
public function withHttpOnly( bool $httpOnly ) : self {
$that = clone $this;
$that->httpOnly = $httpOnly;

return $that;
}

/**
* Return an instance with the specified SameSite value.
*/
public function withSameSite( string $sameSite ) : self {
$that = clone $this;
$that->sameSite = $sameSite;

return $that;
}

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

public function getValue() : string {
return $this->value;
}

public function getExpiration() : int {
return $this->expiration;
}

public function getPath() : string {
return $this->path;
}

public function getDomain() : string {
return $this->domain;
}

public function isSecure() : bool {
return $this->secure;
}

public function isHttpOnly() : bool {
return $this->httpOnly;
}

public function getSameSite() : string {
return $this->sameSite;
}

}
95 changes: 95 additions & 0 deletions src/Cookie/CookieEncoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

namespace Corpus\HttpMessageUtils\Cookie;

use Psr\Http\Message\ResponseInterface;

/**
* Utility to encode a Cookie into a `Set-Cookie` header
*
* Based on `hansott/psr7-cookies`
* MIT License Copyright (c) 2017 Hans Ott [email protected]
*
* @see https://github.com/hansott/psr7-cookies/blob/ec7bc4b3393677730b1e607c987328655d27dfbf/src/SetCookie.php#L160-L192
*/
class CookieEncoder {

/**
* Encode the given Cookie to a `Set-Cookie` compatible header string
*/
public function encode( CookieInterface $cookie ) : string {
$headerValue = sprintf('%s=%s', $cookie->getName(), urlencode($cookie->getValue()));

if( $cookie->getExpiration() !== 0 ) {
$headerValue .= sprintf(
'; expires=%s',
gmdate(DATE_RFC1123, time() + $cookie->getExpiration())
);
}

if( $cookie->getPath() ) {
$headerValue .= sprintf('; path=%s', $cookie->getPath());
}

if( $cookie->getDomain() ) {
$headerValue .= sprintf('; domain=%s', $cookie->getDomain());
}

if( $cookie->isSecure() ) {
$headerValue .= '; secure';
}

if( $cookie->isHttpOnly() ) {
$headerValue .= '; httponly';
}

if( $cookie->getSameSite() ) {
$headerValue .= sprintf('; samesite=%s', $cookie->getSameSite());
}

return $headerValue;
}

/**
* Apply the Cookie to `setcookie` a callable matching the signature of PHP 7.4+
* `setcookie(string $name, string $value = "", array $options = []) : bool`
*
* @param callable|null $callee The `setcookie` compatible callback to be used.
* If set to null, the default setcookie()
*/
public function apply( CookieInterface $cookie, ?callable $callee = null ) : bool {
if( $callee === null ) {
$callee = '\\setcookie';
}

return $callee(
$cookie->getName(),
$cookie->getValue(),
[
'expires' => $cookie->getExpiration() + time(),
'path' => $cookie->getPath(),
'domain' => $cookie->getDomain(),
'secure' => $cookie->isSecure(),
'httponly' => $cookie->isHttpOnly(),
'samesite' => $cookie->getSameSite(),
],
);
}

/**
* Given a \Psr\Http\Message\ResponseInterface returns a new instance of ResponseInterface with an added
* `Set-Cookie` header representing this Cookie.
*/
public function responseWithHeaderAdded( CookieInterface $cookie, ResponseInterface $response ) : ResponseInterface {
return $response->withAddedHeader('Set-Cookie', $this->encode($cookie));
}

/**
* Given a \Psr\Http\Message\ResponseInterface returns a new instance of ResponseInterface replacing any
* `Set-Cookie` headers with one representing this Cookie.
*/
public function responseWithHeader( CookieInterface $cookie, ResponseInterface $response ) : ResponseInterface {
return $response->withHeader('Set-Cookie', $this->encode($cookie));
}

}
47 changes: 47 additions & 0 deletions src/Cookie/CookieInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace Corpus\HttpMessageUtils\Cookie;

interface CookieInterface {

/**
* Get the cookie name.
*/
public function getName() : string;

/**
* Get the cookie value.
*/
public function getValue() : string;

/**
* Get the cookie expiration.
*/
public function getExpiration() : int;

/**
* Get the cookie path.
*/
public function getPath() : string;

/**
* Get the cookie domain.
*/
public function getDomain() : string;

/**
* Get the cookie SameSite value.
*/
public function getSameSite() : string;

/**
* Get the cookie httpOnly flag.
*/
public function isHttpOnly() : bool;

/**
* Get the cookie secure flag.
*/
public function isSecure() : bool;

}
Loading