Skip to content

Add cookies to request object #175

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

Merged
merged 3 commits into from
May 10, 2017
Merged
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
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ For more details about the request object, check out the documentation of
and
[PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface).

> Currently the cookies and uploaded files are not added by the
> Currently the uploaded files are not added by the
`Server`, but you can add these parameters by yourself using the given methods.
The next versions of this project will cover these features.

Expand Down Expand Up @@ -378,6 +378,43 @@ Allowed).
can in fact use a streaming response body for the tunneled application data.
See also [example #21](examples) for more details.

The `getCookieParams(): string[]` method can be used to
get all cookies sent with the current request.

```php
$http = new Server($socket, function (ServerRequestInterface $request) {
$key = 'react\php';

if (isset($request->getCookieParams()[$key])) {
$body = "Your cookie value is: " . $request->getCookieParams()[$key];

return new Response(
200,
array('Content-Type' => 'text/plain'),
$body
);
}

return new Response(
200,
array(
'Content-Type' => 'text/plain',
'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more')
),
"Your cookie has been set."
);
});
```

The above example will try to set a cookie on first access and
will try to print the cookie value on all subsequent tries.
Note how the example uses the `urlencode()` function to encode
non-alphanumeric characters.
This encoding is also used internally when decoding the name and value of cookies
(which is in line with other implementations, such as PHP's cookie functions).

See also [example #6](examples) for more details.

### Response

The callback function passed to the constructor of the [Server](#server)
Expand Down
38 changes: 38 additions & 0 deletions examples/06-cookie-handling.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

use React\EventLoop\Factory;
use React\Socket\Server;
use React\Http\Response;
use Psr\Http\Message\ServerRequestInterface;

require __DIR__ . '/../vendor/autoload.php';

$loop = Factory::create();
$socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop);

$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) {
$key = 'react\php';

if (isset($request->getCookieParams()[$key])) {
$body = "Your cookie value is: " . $request->getCookieParams()[$key];

return new Response(
200,
array('Content-Type' => 'text/plain'),
$body
);
}

return new Response(
200,
array(
'Content-Type' => 'text/plain',
'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more')
),
"Your cookie has been set."
);
});

echo 'Listening on http://' . $socket->getAddress() . PHP_EOL;

$loop->run();
5 changes: 5 additions & 0 deletions src/RequestHeaderParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ private function parseRequest($data)
$request = $request->withQueryParams($queryParams);
}

$cookies = ServerRequest::parseCookie($request->getHeaderLine('Cookie'));
if ($cookies !== false) {
$request = $request->withCookieParams($cookies);
}

// re-apply actual request target from above
if ($originalTarget !== null) {
$uri = $request->getUri()->withPath('');
Expand Down
31 changes: 31 additions & 0 deletions src/ServerRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,35 @@ public function withoutAttribute($name)
unset($new->attributes[$name]);
return $new;
}

/**
* @internal
* @param string $cookie
* @return boolean|mixed[]
*/
public static function parseCookie($cookie)
{
// PSR-7 `getHeaderLine('Cookies')` will return multiple
// cookie header comma-seperated. Multiple cookie headers
// are not allowed according to https://tools.ietf.org/html/rfc6265#section-5.4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should imho read

... PSR-7 getHeaderLine('Cookie') will return multiple comma-seperated cookie headers.

if (strpos($cookie, ',') !== false) {
return false;
}

$cookieArray = explode(';', $cookie);
$result = array();

foreach ($cookieArray as $pair) {
$pair = trim($pair);
$nameValuePair = explode('=', $pair, 2);

if (count($nameValuePair) === 2) {
$key = urldecode($nameValuePair[0]);
$value = urldecode($nameValuePair[1]);
$result[$key] = $value;
}
}

return $result;
}
}
84 changes: 84 additions & 0 deletions tests/ServerRequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,88 @@ public function testServerRequestParameter()
$this->assertEquals('1.0', $request->getProtocolVersion());
$this->assertEquals('127.0.0.1', $serverParams['SERVER_ADDR']);
}

public function testParseSingleCookieNameValuePairWillReturnValidArray()
{
$cookieString = 'hello=world';
$cookies = ServerRequest::parseCookie($cookieString);
$this->assertEquals(array('hello' => 'world'), $cookies);
}

public function testParseMultipleCookieNameValuePaiWillReturnValidArray()
{
$cookieString = 'hello=world; test=abc';
$cookies = ServerRequest::parseCookie($cookieString);
$this->assertEquals(array('hello' => 'world', 'test' => 'abc'), $cookies);
}

public function testParseMultipleCookieNameValuePairWillReturnFalse()
{
// Could be done through multiple 'Cookie' headers
// getHeaderLine('Cookie') will return a value seperated by comma
// e.g.
// GET / HTTP/1.1\r\n
// Host: test.org\r\n
// Cookie: hello=world\r\n
// Cookie: test=abc\r\n\r\n
$cookieString = 'hello=world,test=abc';
$cookies = ServerRequest::parseCookie($cookieString);
$this->assertEquals(false, $cookies);
}

public function testOnlyFirstSetWillBeAddedToCookiesArray()
{
$cookieString = 'hello=world; hello=abc';
$cookies = ServerRequest::parseCookie($cookieString);
$this->assertEquals(array('hello' => 'abc'), $cookies);
}

public function testOtherEqualSignsWillBeAddedToValueAndWillReturnValidArray()
{
$cookieString = 'hello=world=test=php';
$cookies = ServerRequest::parseCookie($cookieString);
$this->assertEquals(array('hello' => 'world=test=php'), $cookies);
}

public function testSingleCookieValueInCookiesReturnsEmptyArray()
{
$cookieString = 'world';
$cookies = ServerRequest::parseCookie($cookieString);
$this->assertEquals(array(), $cookies);
}

public function testSingleMutlipleCookieValuesReturnsEmptyArray()
{
$cookieString = 'world; test';
$cookies = ServerRequest::parseCookie($cookieString);
$this->assertEquals(array(), $cookies);
}

public function testSingleValueIsValidInMultipleValueCookieWillReturnValidArray()
{
$cookieString = 'world; test=php';
$cookies = ServerRequest::parseCookie($cookieString);
$this->assertEquals(array('test' => 'php'), $cookies);
}

public function testUrlEncodingForValueWillReturnValidArray()
{
$cookieString = 'hello=world%21; test=100%25%20coverage';
$cookies = ServerRequest::parseCookie($cookieString);
$this->assertEquals(array('hello' => 'world!', 'test' => '100% coverage'), $cookies);
}

public function testUrlEncodingForKeyWillReturnValidArray()
{
$cookieString = 'react%3Bphp=is%20great';
$cookies = ServerRequest::parseCookie($cookieString);
$this->assertEquals(array('react;php' => 'is great'), $cookies);
}

public function testCookieWithoutSpaceAfterSeparatorWillBeAccepted()
{
$cookieString = 'hello=world;react=php';
$cookies = ServerRequest::parseCookie($cookieString);
$this->assertEquals(array('hello' => 'world', 'react' => 'php'), $cookies);
}
}
62 changes: 62 additions & 0 deletions tests/ServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2390,6 +2390,68 @@ public function testQueryParametersWillBeAddedToRequest()
$this->assertEquals('bar', $queryParams['test']);
}

public function testCookieWillBeAddedToServerRequest()
{
$requestValidation = null;
$server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) {
$requestValidation = $request;
return new Response();
});

$this->socket->emit('connection', array($this->connection));

$data = "GET / HTTP/1.1\r\n";
$data .= "Host: example.com:80\r\n";
$data .= "Connection: close\r\n";
$data .= "Cookie: hello=world\r\n";
$data .= "\r\n";

$this->connection->emit('data', array($data));

$this->assertEquals(array('hello' => 'world'), $requestValidation->getCookieParams());
}

public function testMultipleCookiesWontBeAddedToServerRequest()
{
$requestValidation = null;
$server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) {
$requestValidation = $request;
return new Response();
});

$this->socket->emit('connection', array($this->connection));

$data = "GET / HTTP/1.1\r\n";
$data .= "Host: example.com:80\r\n";
$data .= "Connection: close\r\n";
$data .= "Cookie: hello=world\r\n";
$data .= "Cookie: test=failed\r\n";
$data .= "\r\n";

$this->connection->emit('data', array($data));
$this->assertEquals(array(), $requestValidation->getCookieParams());
}

public function testCookieWithSeparatorWillBeAddedToServerRequest()
{
$requestValidation = null;
$server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) {
$requestValidation = $request;
return new Response();
});

$this->socket->emit('connection', array($this->connection));

$data = "GET / HTTP/1.1\r\n";
$data .= "Host: example.com:80\r\n";
$data .= "Connection: close\r\n";
$data .= "Cookie: hello=world; test=abc\r\n";
$data .= "\r\n";

$this->connection->emit('data', array($data));
$this->assertEquals(array('hello' => 'world', 'test' => 'abc'), $requestValidation->getCookieParams());
}

private function createGetRequest()
{
$data = "GET / HTTP/1.1\r\n";
Expand Down