From d49e982ac8cb32830275e0086cc8e8e8a11a84b1 Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Fri, 27 Sep 2013 14:36:19 +0200 Subject: [PATCH 01/21] Check if accessor matched request method. It's easier to find errors in the code if an exception is thrown. --- lib/appframework/http/request.php | 3 +++ tests/lib/appframework/http/RequestTest.php | 27 +++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/lib/appframework/http/request.php b/lib/appframework/http/request.php index 34605acdfe..5a86066b48 100644 --- a/lib/appframework/http/request.php +++ b/lib/appframework/http/request.php @@ -152,6 +152,9 @@ class Request implements \ArrayAccess, \Countable, IRequest { switch($name) { case 'get': case 'post': + if($this->method !== strtoupper($name)) { + throw new \BadMethodCallException(sprintf('%s cannot be accessed in a %s request.', $name, $this->method)); + } case 'files': case 'server': case 'env': diff --git a/tests/lib/appframework/http/RequestTest.php b/tests/lib/appframework/http/RequestTest.php index 0371c870cf..ff4a8357f0 100644 --- a/tests/lib/appframework/http/RequestTest.php +++ b/tests/lib/appframework/http/RequestTest.php @@ -14,6 +14,7 @@ class RequestTest extends \PHPUnit_Framework_TestCase { public function testRequestAccessors() { $vars = array( 'get' => array('name' => 'John Q. Public', 'nickname' => 'Joey'), + 'method' => 'GET', ); $request = new Request($vars); @@ -73,4 +74,30 @@ class RequestTest extends \PHPUnit_Framework_TestCase { $request->{'nickname'} = 'Janey'; } + /** + * @expectedException BadMethodCallException + */ + public function testGetTheMethodRight() { + $vars = array( + 'get' => array('name' => 'John Q. Public', 'nickname' => 'Joey'), + 'method' => 'GET', + ); + + $request = new Request($vars); + $result = $request->post; + } + + public function testTheMethodIsRight() { + $vars = array( + 'get' => array('name' => 'John Q. Public', 'nickname' => 'Joey'), + 'method' => 'GET', + ); + + $request = new Request($vars); + $this->assertEquals('GET', $request->method); + $result = $request->get; + $this->assertEquals('John Q. Public', $result['name']); + $this->assertEquals('Joey', $result['nickname']); + } + } From b0d6d30705679fdc6ccaad06a7c33ff99d65a4fb Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Fri, 27 Sep 2013 15:54:18 +0200 Subject: [PATCH 02/21] Add patch method to OC_Route --- lib/route.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/route.php b/lib/route.php index 5901717c09..fb7da456b6 100644 --- a/lib/route.php +++ b/lib/route.php @@ -51,6 +51,14 @@ class OC_Route extends Route { return $this; } + /** + * Specify PATCH as the method to use with this route + */ + public function patch() { + $this->method('PATCH'); + return $this; + } + /** * Defaults to use for this route * From 95cc666742d900bc34a6b146e736ada14f75dc90 Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Fri, 27 Sep 2013 15:55:22 +0200 Subject: [PATCH 03/21] Add interface docs to IRequest. --- lib/public/irequest.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/public/irequest.php b/lib/public/irequest.php index 9f335b06f2..5611180473 100644 --- a/lib/public/irequest.php +++ b/lib/public/irequest.php @@ -22,6 +22,28 @@ namespace OCP; +/** + * This interface provides an immutable object with with accessors to + * request variables and headers. + * + * Access request variables by method and name. + * + * Examples: + * + * $request->post['myvar']; // Only look for POST variables + * $request->myvar; or $request->{'myvar'}; or $request->{$myvar} + * Looks in the combined GET, POST and urlParams array. + * + * If you access e.g. ->post but the current HTTP request method + * is GET a \LogicException will be thrown. + * + * NOTE: + * - When accessing ->put a stream resource is returned and the accessor + * will return false on subsequent access to ->put or ->patch. + * - When accessing ->patch and the Content-Type is either application/json + * or application/x-www-form-urlencoded (most cases) it will act like ->get + * and ->post and return an array. Otherwise the raw data will be returned. + */ interface IRequest { From d124b6965b4ed9aed03396d97bc148038148372b Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Sat, 28 Sep 2013 01:35:24 +0200 Subject: [PATCH 04/21] Implement PUT an PATCH support --- lib/appframework/http/request.php | 101 ++++++++++++----- lib/public/irequest.php | 2 +- tests/lib/appframework/http/RequestTest.php | 101 ++++++++++++++++- tests/lib/appframework/http/requeststream.php | 107 ++++++++++++++++++ 4 files changed, 281 insertions(+), 30 deletions(-) create mode 100644 tests/lib/appframework/http/requeststream.php diff --git a/lib/appframework/http/request.php b/lib/appframework/http/request.php index 5a86066b48..3426f0bf75 100644 --- a/lib/appframework/http/request.php +++ b/lib/appframework/http/request.php @@ -31,10 +31,12 @@ use OCP\IRequest; class Request implements \ArrayAccess, \Countable, IRequest { + protected $content; protected $items = array(); protected $allowedKeys = array( 'get', 'post', + 'patch', 'files', 'server', 'env', @@ -50,7 +52,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { * @param array 'params' the parsed json array * @param array 'urlParams' the parameters which were matched from the URL * @param array 'get' the $_GET array - * @param array 'post' the $_POST array + * @param array|string 'post' the $_POST array or JSON string * @param array 'files' the $_FILES array * @param array 'server' the $_SERVER array * @param array 'env' the $_ENV array @@ -62,11 +64,19 @@ class Request implements \ArrayAccess, \Countable, IRequest { public function __construct(array $vars=array()) { foreach($this->allowedKeys as $name) { - $this->items[$name] = isset($vars[$name]) + $this->items[$name] = isset($vars[$name]) ? $vars[$name] : array(); } + // Only 'application/x-www-form-urlencoded' requests are automatically + // transformed by PHP, 'application/json' must be decoded manually. + if (isset($this->items['post']) + && strpos($this->getHeader('Content-Type'), 'application/json') !== false + && is_string($this->items['post'])) { + $this->items['post'] = json_decode($this->items['post'], true); + } + $this->items['parameters'] = array_merge( $this->items['params'], $this->items['get'], @@ -141,19 +151,21 @@ class Request implements \ArrayAccess, \Countable, IRequest { * $request->myvar; or $request->{'myvar'}; or $request->{$myvar} * Looks in the combined GET, POST and urlParams array. * - * if($request->method !== 'POST') { - * throw new Exception('This function can only be invoked using POST'); - * } + * If you access e.g. ->post but the current HTTP request method + * is GET a \LogicException will be thrown. * * @param string $name The key to look for. + * @throws \LogicException * @return mixed|null */ public function __get($name) { switch($name) { + case 'put': + case 'patch': case 'get': case 'post': if($this->method !== strtoupper($name)) { - throw new \BadMethodCallException(sprintf('%s cannot be accessed in a %s request.', $name, $this->method)); + throw new \LogicException(sprintf('%s cannot be accessed in a %s request.', $name, $this->method)); } case 'files': case 'server': @@ -162,9 +174,13 @@ class Request implements \ArrayAccess, \Countable, IRequest { case 'parameters': case 'params': case 'urlParams': - return isset($this->items[$name]) - ? $this->items[$name] - : null; + if(in_array($name, array('put', 'patch'))) { + return $this->getContent($name); + } else { + return isset($this->items[$name]) + ? $this->items[$name] + : null; + } break; case 'method': return $this->items['method']; @@ -283,28 +299,57 @@ class Request implements \ArrayAccess, \Countable, IRequest { /** * Returns the request body content. * - * @param Boolean $asResource If true, a resource will be returned + * If the HTTP request method is PUT a stream resource is returned, otherwise an + * array or a string depending on the Content-Type. For "normal" use an array + * will be returned. * - * @return string|resource The request body content or a resource to read the body stream. + * @return array|string|resource The request body content or a resource to read the body stream. * * @throws \LogicException */ - function getContent($asResource = false) { - return null; -// if (false === $this->content || (true === $asResource && null !== $this->content)) { -// throw new \LogicException('getContent() can only be called once when using the resource return type.'); -// } -// -// if (true === $asResource) { -// $this->content = false; -// -// return fopen('php://input', 'rb'); -// } -// -// if (null === $this->content) { -// $this->content = file_get_contents('php://input'); -// } -// -// return $this->content; + protected function getContent() { + if ($this->content === false && $this->method === 'PUT') { + throw new \LogicException('"put" can only be accessed once.'); + } + + if (defined('PHPUNIT_RUN') && PHPUNIT_RUN + && in_array('fakeinput', stream_get_wrappers())) { + $stream = 'fakeinput://data'; + } else { + $stream = 'php://input'; + } + + if ($this->method === 'PUT') { + $this->content = false; + return fopen($stream, 'rb'); + } + + if (is_null($this->content)) { + $this->content = file_get_contents($stream); + + if ($this->method === 'PATCH') { + /* + * Normal jquery ajax requests are sent as application/x-www-form-urlencoded + * and in $_GET and $_POST PHP transformes the data into an array. + * The first condition mimics this. + * The second condition allows for sending raw application/json data while + * still getting the result as an array. + * + */ + if (strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') !== false) { + parse_str($this->content, $content); + if(is_array($content)) { + $this->content = $content; + } + } elseif (strpos($this->getHeader('Content-Type'), 'application/json') !== false) { + $content = json_decode($this->content, true); + if(is_array($content)) { + $this->content = $content; + } + } + } + } + + return $this->content; } } diff --git a/lib/public/irequest.php b/lib/public/irequest.php index 5611180473..b9bcc4bbc2 100644 --- a/lib/public/irequest.php +++ b/lib/public/irequest.php @@ -114,5 +114,5 @@ interface IRequest { * @return string|resource The request body content or a resource to read the body stream. * @throws \LogicException */ - function getContent($asResource = false); + //function getContent($asResource = false); } diff --git a/tests/lib/appframework/http/RequestTest.php b/tests/lib/appframework/http/RequestTest.php index ff4a8357f0..847c6610fe 100644 --- a/tests/lib/appframework/http/RequestTest.php +++ b/tests/lib/appframework/http/RequestTest.php @@ -8,6 +8,7 @@ namespace OC\AppFramework\Http; +global $data; class RequestTest extends \PHPUnit_Framework_TestCase { @@ -32,6 +33,8 @@ class RequestTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('Joey', $request->get['nickname']); // Always returns null if variable not set. $this->assertEquals(null, $request->{'flickname'}); + + require_once __DIR__ . '/requeststream.php'; } // urlParams has precedence over POST which has precedence over GET @@ -75,7 +78,7 @@ class RequestTest extends \PHPUnit_Framework_TestCase { } /** - * @expectedException BadMethodCallException + * @expectedException LogicException */ public function testGetTheMethodRight() { $vars = array( @@ -100,4 +103,100 @@ class RequestTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('Joey', $result['nickname']); } + public function testJsonPost() { + $vars = array( + 'post' => '{"name": "John Q. Public", "nickname": "Joey"}', + 'method' => 'POST', + 'server' => array('CONTENT_TYPE' => 'application/json; utf-8'), + ); + + $request = new Request($vars); + $this->assertEquals('POST', $request->method); + $result = $request->post; + $this->assertEquals('John Q. Public', $result['name']); + $this->assertEquals('Joey', $result['nickname']); + } + + public function testPatch() { + global $data; + $data = http_build_query(array('name' => 'John Q. Public', 'nickname' => 'Joey'), '', '&'); + + if (in_array('fakeinput', stream_get_wrappers())) { + stream_wrapper_unregister('fakeinput'); + } + stream_wrapper_register('fakeinput', 'RequestStream'); + + $vars = array( + 'patch' => $data, + 'method' => 'PATCH', + 'server' => array('CONTENT_TYPE' => 'application/x-www-form-urlencoded'), + ); + + $request = new Request($vars); + + $this->assertEquals('PATCH', $request->method); + $result = $request->patch; + + $this->assertEquals('John Q. Public', $result['name']); + $this->assertEquals('Joey', $result['nickname']); + + stream_wrapper_unregister('fakeinput'); + } + + public function testJsonPatch() { + global $data; + $data = '{"name": "John Q. Public", "nickname": null}'; + + if (in_array('fakeinput', stream_get_wrappers())) { + stream_wrapper_unregister('fakeinput'); + } + stream_wrapper_register('fakeinput', 'RequestStream'); + + $vars = array( + 'patch' => $data, + 'method' => 'PATCH', + 'server' => array('CONTENT_TYPE' => 'application/json; utf-8'), + ); + + $request = new Request($vars); + + $this->assertEquals('PATCH', $request->method); + $result = $request->patch; + + $this->assertEquals('John Q. Public', $result['name']); + $this->assertEquals(null, $result['nickname']); + + stream_wrapper_unregister('fakeinput'); + } + + public function testPutSteam() { + global $data; + $data = file_get_contents(__DIR__ . '/../../../data/testimage.png'); + + if (in_array('fakeinput', stream_get_wrappers())) { + stream_wrapper_unregister('fakeinput'); + } + stream_wrapper_register('fakeinput', 'RequestStream'); + + $vars = array( + 'put' => $data, + 'method' => 'PUT', + 'server' => array('CONTENT_TYPE' => 'image/png'), + ); + + $request = new Request($vars); + $this->assertEquals('PUT', $request->method); + $resource = $request->put; + $contents = stream_get_contents($resource); + $this->assertEquals($data, $contents); + + try { + $resource = $request->put; + } catch(\LogicException $e) { + stream_wrapper_unregister('fakeinput'); + return; + } + $this->fail('Expected LogicException.'); + + } } diff --git a/tests/lib/appframework/http/requeststream.php b/tests/lib/appframework/http/requeststream.php new file mode 100644 index 0000000000..e1bf5c2c6b --- /dev/null +++ b/tests/lib/appframework/http/requeststream.php @@ -0,0 +1,107 @@ +varname = $url["host"]; + $this->position = 0; + + return true; + } + + function stream_read($count) { + $ret = substr($GLOBALS[$this->varname], $this->position, $count); + $this->position += strlen($ret); + return $ret; + } + + function stream_write($data) { + $left = substr($GLOBALS[$this->varname], 0, $this->position); + $right = substr($GLOBALS[$this->varname], $this->position + strlen($data)); + $GLOBALS[$this->varname] = $left . $data . $right; + $this->position += strlen($data); + return strlen($data); + } + + function stream_tell() { + return $this->position; + } + + function stream_eof() { + return $this->position >= strlen($GLOBALS[$this->varname]); + } + + function stream_seek($offset, $whence) { + switch ($whence) { + case SEEK_SET: + if ($offset < strlen($GLOBALS[$this->varname]) && $offset >= 0) { + $this->position = $offset; + return true; + } else { + return false; + } + break; + + case SEEK_CUR: + if ($offset >= 0) { + $this->position += $offset; + return true; + } else { + return false; + } + break; + + case SEEK_END: + if (strlen($GLOBALS[$this->varname]) + $offset >= 0) { + $this->position = strlen($GLOBALS[$this->varname]) + $offset; + return true; + } else { + return false; + } + break; + + default: + return false; + } + } + + public function stream_stat() { + $size = strlen($GLOBALS[$this->varname]); + $time = time(); + $data = array( + 'dev' => 0, + 'ino' => 0, + 'mode' => 0777, + 'nlink' => 1, + 'uid' => 0, + 'gid' => 0, + 'rdev' => '', + 'size' => $size, + 'atime' => $time, + 'mtime' => $time, + 'ctime' => $time, + 'blksize' => -1, + 'blocks' => -1, + ); + return array_values($data) + $data; + //return false; + } + + function stream_metadata($path, $option, $var) { + if($option == STREAM_META_TOUCH) { + $url = parse_url($path); + $varname = $url["host"]; + if(!isset($GLOBALS[$varname])) { + $GLOBALS[$varname] = ''; + } + return true; + } + return false; + } +} From 50fc5e8792a7870268f2e4c6fddc3cf4c432fe3c Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Fri, 27 Sep 2013 14:36:19 +0200 Subject: [PATCH 05/21] Check if accessor matched request method. It's easier to find errors in the code if an exception is thrown. --- lib/appframework/http/request.php | 3 +++ tests/lib/appframework/http/RequestTest.php | 27 +++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/lib/appframework/http/request.php b/lib/appframework/http/request.php index 34605acdfe..5a86066b48 100644 --- a/lib/appframework/http/request.php +++ b/lib/appframework/http/request.php @@ -152,6 +152,9 @@ class Request implements \ArrayAccess, \Countable, IRequest { switch($name) { case 'get': case 'post': + if($this->method !== strtoupper($name)) { + throw new \BadMethodCallException(sprintf('%s cannot be accessed in a %s request.', $name, $this->method)); + } case 'files': case 'server': case 'env': diff --git a/tests/lib/appframework/http/RequestTest.php b/tests/lib/appframework/http/RequestTest.php index 0371c870cf..ff4a8357f0 100644 --- a/tests/lib/appframework/http/RequestTest.php +++ b/tests/lib/appframework/http/RequestTest.php @@ -14,6 +14,7 @@ class RequestTest extends \PHPUnit_Framework_TestCase { public function testRequestAccessors() { $vars = array( 'get' => array('name' => 'John Q. Public', 'nickname' => 'Joey'), + 'method' => 'GET', ); $request = new Request($vars); @@ -73,4 +74,30 @@ class RequestTest extends \PHPUnit_Framework_TestCase { $request->{'nickname'} = 'Janey'; } + /** + * @expectedException BadMethodCallException + */ + public function testGetTheMethodRight() { + $vars = array( + 'get' => array('name' => 'John Q. Public', 'nickname' => 'Joey'), + 'method' => 'GET', + ); + + $request = new Request($vars); + $result = $request->post; + } + + public function testTheMethodIsRight() { + $vars = array( + 'get' => array('name' => 'John Q. Public', 'nickname' => 'Joey'), + 'method' => 'GET', + ); + + $request = new Request($vars); + $this->assertEquals('GET', $request->method); + $result = $request->get; + $this->assertEquals('John Q. Public', $result['name']); + $this->assertEquals('Joey', $result['nickname']); + } + } From 35fae7aaf831fcc60903e15275c82c81ed734ed8 Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Fri, 27 Sep 2013 15:54:18 +0200 Subject: [PATCH 06/21] Add patch method to OC_Route --- lib/route.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/route.php b/lib/route.php index 5901717c09..fb7da456b6 100644 --- a/lib/route.php +++ b/lib/route.php @@ -51,6 +51,14 @@ class OC_Route extends Route { return $this; } + /** + * Specify PATCH as the method to use with this route + */ + public function patch() { + $this->method('PATCH'); + return $this; + } + /** * Defaults to use for this route * From 2115db389252d2e9f9057c321080db06acad41d4 Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Fri, 27 Sep 2013 15:55:22 +0200 Subject: [PATCH 07/21] Add interface docs to IRequest. --- lib/public/irequest.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/public/irequest.php b/lib/public/irequest.php index 9f335b06f2..5611180473 100644 --- a/lib/public/irequest.php +++ b/lib/public/irequest.php @@ -22,6 +22,28 @@ namespace OCP; +/** + * This interface provides an immutable object with with accessors to + * request variables and headers. + * + * Access request variables by method and name. + * + * Examples: + * + * $request->post['myvar']; // Only look for POST variables + * $request->myvar; or $request->{'myvar'}; or $request->{$myvar} + * Looks in the combined GET, POST and urlParams array. + * + * If you access e.g. ->post but the current HTTP request method + * is GET a \LogicException will be thrown. + * + * NOTE: + * - When accessing ->put a stream resource is returned and the accessor + * will return false on subsequent access to ->put or ->patch. + * - When accessing ->patch and the Content-Type is either application/json + * or application/x-www-form-urlencoded (most cases) it will act like ->get + * and ->post and return an array. Otherwise the raw data will be returned. + */ interface IRequest { From 092b4e7b5381e9cb96efda9e4535648ffab506cd Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Sat, 28 Sep 2013 01:35:24 +0200 Subject: [PATCH 08/21] Implement PUT an PATCH support --- lib/appframework/http/request.php | 101 ++++++++++++----- lib/public/irequest.php | 2 +- tests/lib/appframework/http/RequestTest.php | 101 ++++++++++++++++- tests/lib/appframework/http/requeststream.php | 107 ++++++++++++++++++ 4 files changed, 281 insertions(+), 30 deletions(-) create mode 100644 tests/lib/appframework/http/requeststream.php diff --git a/lib/appframework/http/request.php b/lib/appframework/http/request.php index 5a86066b48..3426f0bf75 100644 --- a/lib/appframework/http/request.php +++ b/lib/appframework/http/request.php @@ -31,10 +31,12 @@ use OCP\IRequest; class Request implements \ArrayAccess, \Countable, IRequest { + protected $content; protected $items = array(); protected $allowedKeys = array( 'get', 'post', + 'patch', 'files', 'server', 'env', @@ -50,7 +52,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { * @param array 'params' the parsed json array * @param array 'urlParams' the parameters which were matched from the URL * @param array 'get' the $_GET array - * @param array 'post' the $_POST array + * @param array|string 'post' the $_POST array or JSON string * @param array 'files' the $_FILES array * @param array 'server' the $_SERVER array * @param array 'env' the $_ENV array @@ -62,11 +64,19 @@ class Request implements \ArrayAccess, \Countable, IRequest { public function __construct(array $vars=array()) { foreach($this->allowedKeys as $name) { - $this->items[$name] = isset($vars[$name]) + $this->items[$name] = isset($vars[$name]) ? $vars[$name] : array(); } + // Only 'application/x-www-form-urlencoded' requests are automatically + // transformed by PHP, 'application/json' must be decoded manually. + if (isset($this->items['post']) + && strpos($this->getHeader('Content-Type'), 'application/json') !== false + && is_string($this->items['post'])) { + $this->items['post'] = json_decode($this->items['post'], true); + } + $this->items['parameters'] = array_merge( $this->items['params'], $this->items['get'], @@ -141,19 +151,21 @@ class Request implements \ArrayAccess, \Countable, IRequest { * $request->myvar; or $request->{'myvar'}; or $request->{$myvar} * Looks in the combined GET, POST and urlParams array. * - * if($request->method !== 'POST') { - * throw new Exception('This function can only be invoked using POST'); - * } + * If you access e.g. ->post but the current HTTP request method + * is GET a \LogicException will be thrown. * * @param string $name The key to look for. + * @throws \LogicException * @return mixed|null */ public function __get($name) { switch($name) { + case 'put': + case 'patch': case 'get': case 'post': if($this->method !== strtoupper($name)) { - throw new \BadMethodCallException(sprintf('%s cannot be accessed in a %s request.', $name, $this->method)); + throw new \LogicException(sprintf('%s cannot be accessed in a %s request.', $name, $this->method)); } case 'files': case 'server': @@ -162,9 +174,13 @@ class Request implements \ArrayAccess, \Countable, IRequest { case 'parameters': case 'params': case 'urlParams': - return isset($this->items[$name]) - ? $this->items[$name] - : null; + if(in_array($name, array('put', 'patch'))) { + return $this->getContent($name); + } else { + return isset($this->items[$name]) + ? $this->items[$name] + : null; + } break; case 'method': return $this->items['method']; @@ -283,28 +299,57 @@ class Request implements \ArrayAccess, \Countable, IRequest { /** * Returns the request body content. * - * @param Boolean $asResource If true, a resource will be returned + * If the HTTP request method is PUT a stream resource is returned, otherwise an + * array or a string depending on the Content-Type. For "normal" use an array + * will be returned. * - * @return string|resource The request body content or a resource to read the body stream. + * @return array|string|resource The request body content or a resource to read the body stream. * * @throws \LogicException */ - function getContent($asResource = false) { - return null; -// if (false === $this->content || (true === $asResource && null !== $this->content)) { -// throw new \LogicException('getContent() can only be called once when using the resource return type.'); -// } -// -// if (true === $asResource) { -// $this->content = false; -// -// return fopen('php://input', 'rb'); -// } -// -// if (null === $this->content) { -// $this->content = file_get_contents('php://input'); -// } -// -// return $this->content; + protected function getContent() { + if ($this->content === false && $this->method === 'PUT') { + throw new \LogicException('"put" can only be accessed once.'); + } + + if (defined('PHPUNIT_RUN') && PHPUNIT_RUN + && in_array('fakeinput', stream_get_wrappers())) { + $stream = 'fakeinput://data'; + } else { + $stream = 'php://input'; + } + + if ($this->method === 'PUT') { + $this->content = false; + return fopen($stream, 'rb'); + } + + if (is_null($this->content)) { + $this->content = file_get_contents($stream); + + if ($this->method === 'PATCH') { + /* + * Normal jquery ajax requests are sent as application/x-www-form-urlencoded + * and in $_GET and $_POST PHP transformes the data into an array. + * The first condition mimics this. + * The second condition allows for sending raw application/json data while + * still getting the result as an array. + * + */ + if (strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') !== false) { + parse_str($this->content, $content); + if(is_array($content)) { + $this->content = $content; + } + } elseif (strpos($this->getHeader('Content-Type'), 'application/json') !== false) { + $content = json_decode($this->content, true); + if(is_array($content)) { + $this->content = $content; + } + } + } + } + + return $this->content; } } diff --git a/lib/public/irequest.php b/lib/public/irequest.php index 5611180473..b9bcc4bbc2 100644 --- a/lib/public/irequest.php +++ b/lib/public/irequest.php @@ -114,5 +114,5 @@ interface IRequest { * @return string|resource The request body content or a resource to read the body stream. * @throws \LogicException */ - function getContent($asResource = false); + //function getContent($asResource = false); } diff --git a/tests/lib/appframework/http/RequestTest.php b/tests/lib/appframework/http/RequestTest.php index ff4a8357f0..847c6610fe 100644 --- a/tests/lib/appframework/http/RequestTest.php +++ b/tests/lib/appframework/http/RequestTest.php @@ -8,6 +8,7 @@ namespace OC\AppFramework\Http; +global $data; class RequestTest extends \PHPUnit_Framework_TestCase { @@ -32,6 +33,8 @@ class RequestTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('Joey', $request->get['nickname']); // Always returns null if variable not set. $this->assertEquals(null, $request->{'flickname'}); + + require_once __DIR__ . '/requeststream.php'; } // urlParams has precedence over POST which has precedence over GET @@ -75,7 +78,7 @@ class RequestTest extends \PHPUnit_Framework_TestCase { } /** - * @expectedException BadMethodCallException + * @expectedException LogicException */ public function testGetTheMethodRight() { $vars = array( @@ -100,4 +103,100 @@ class RequestTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('Joey', $result['nickname']); } + public function testJsonPost() { + $vars = array( + 'post' => '{"name": "John Q. Public", "nickname": "Joey"}', + 'method' => 'POST', + 'server' => array('CONTENT_TYPE' => 'application/json; utf-8'), + ); + + $request = new Request($vars); + $this->assertEquals('POST', $request->method); + $result = $request->post; + $this->assertEquals('John Q. Public', $result['name']); + $this->assertEquals('Joey', $result['nickname']); + } + + public function testPatch() { + global $data; + $data = http_build_query(array('name' => 'John Q. Public', 'nickname' => 'Joey'), '', '&'); + + if (in_array('fakeinput', stream_get_wrappers())) { + stream_wrapper_unregister('fakeinput'); + } + stream_wrapper_register('fakeinput', 'RequestStream'); + + $vars = array( + 'patch' => $data, + 'method' => 'PATCH', + 'server' => array('CONTENT_TYPE' => 'application/x-www-form-urlencoded'), + ); + + $request = new Request($vars); + + $this->assertEquals('PATCH', $request->method); + $result = $request->patch; + + $this->assertEquals('John Q. Public', $result['name']); + $this->assertEquals('Joey', $result['nickname']); + + stream_wrapper_unregister('fakeinput'); + } + + public function testJsonPatch() { + global $data; + $data = '{"name": "John Q. Public", "nickname": null}'; + + if (in_array('fakeinput', stream_get_wrappers())) { + stream_wrapper_unregister('fakeinput'); + } + stream_wrapper_register('fakeinput', 'RequestStream'); + + $vars = array( + 'patch' => $data, + 'method' => 'PATCH', + 'server' => array('CONTENT_TYPE' => 'application/json; utf-8'), + ); + + $request = new Request($vars); + + $this->assertEquals('PATCH', $request->method); + $result = $request->patch; + + $this->assertEquals('John Q. Public', $result['name']); + $this->assertEquals(null, $result['nickname']); + + stream_wrapper_unregister('fakeinput'); + } + + public function testPutSteam() { + global $data; + $data = file_get_contents(__DIR__ . '/../../../data/testimage.png'); + + if (in_array('fakeinput', stream_get_wrappers())) { + stream_wrapper_unregister('fakeinput'); + } + stream_wrapper_register('fakeinput', 'RequestStream'); + + $vars = array( + 'put' => $data, + 'method' => 'PUT', + 'server' => array('CONTENT_TYPE' => 'image/png'), + ); + + $request = new Request($vars); + $this->assertEquals('PUT', $request->method); + $resource = $request->put; + $contents = stream_get_contents($resource); + $this->assertEquals($data, $contents); + + try { + $resource = $request->put; + } catch(\LogicException $e) { + stream_wrapper_unregister('fakeinput'); + return; + } + $this->fail('Expected LogicException.'); + + } } diff --git a/tests/lib/appframework/http/requeststream.php b/tests/lib/appframework/http/requeststream.php new file mode 100644 index 0000000000..e1bf5c2c6b --- /dev/null +++ b/tests/lib/appframework/http/requeststream.php @@ -0,0 +1,107 @@ +varname = $url["host"]; + $this->position = 0; + + return true; + } + + function stream_read($count) { + $ret = substr($GLOBALS[$this->varname], $this->position, $count); + $this->position += strlen($ret); + return $ret; + } + + function stream_write($data) { + $left = substr($GLOBALS[$this->varname], 0, $this->position); + $right = substr($GLOBALS[$this->varname], $this->position + strlen($data)); + $GLOBALS[$this->varname] = $left . $data . $right; + $this->position += strlen($data); + return strlen($data); + } + + function stream_tell() { + return $this->position; + } + + function stream_eof() { + return $this->position >= strlen($GLOBALS[$this->varname]); + } + + function stream_seek($offset, $whence) { + switch ($whence) { + case SEEK_SET: + if ($offset < strlen($GLOBALS[$this->varname]) && $offset >= 0) { + $this->position = $offset; + return true; + } else { + return false; + } + break; + + case SEEK_CUR: + if ($offset >= 0) { + $this->position += $offset; + return true; + } else { + return false; + } + break; + + case SEEK_END: + if (strlen($GLOBALS[$this->varname]) + $offset >= 0) { + $this->position = strlen($GLOBALS[$this->varname]) + $offset; + return true; + } else { + return false; + } + break; + + default: + return false; + } + } + + public function stream_stat() { + $size = strlen($GLOBALS[$this->varname]); + $time = time(); + $data = array( + 'dev' => 0, + 'ino' => 0, + 'mode' => 0777, + 'nlink' => 1, + 'uid' => 0, + 'gid' => 0, + 'rdev' => '', + 'size' => $size, + 'atime' => $time, + 'mtime' => $time, + 'ctime' => $time, + 'blksize' => -1, + 'blocks' => -1, + ); + return array_values($data) + $data; + //return false; + } + + function stream_metadata($path, $option, $var) { + if($option == STREAM_META_TOUCH) { + $url = parse_url($path); + $varname = $url["host"]; + if(!isset($GLOBALS[$varname])) { + $GLOBALS[$varname] = ''; + } + return true; + } + return false; + } +} From 1d2466c6d1126dc4d0e04ab579ca42758293dc9b Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Mon, 30 Sep 2013 16:23:43 +0200 Subject: [PATCH 09/21] Add assertions for ->params and array access with json --- tests/lib/appframework/http/RequestTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/lib/appframework/http/RequestTest.php b/tests/lib/appframework/http/RequestTest.php index 847c6610fe..aaa2328be1 100644 --- a/tests/lib/appframework/http/RequestTest.php +++ b/tests/lib/appframework/http/RequestTest.php @@ -115,6 +115,8 @@ class RequestTest extends \PHPUnit_Framework_TestCase { $result = $request->post; $this->assertEquals('John Q. Public', $result['name']); $this->assertEquals('Joey', $result['nickname']); + $this->assertEquals('Joey', $request->params['nickname']); + $this->assertEquals('Joey', $request['nickname']); } public function testPatch() { From b0be2c79dce5b1b7da43cc33462c4fe2a981e932 Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Mon, 30 Sep 2013 16:24:47 +0200 Subject: [PATCH 10/21] Remove JSON request parsing from Server --- lib/appframework/http/request.php | 5 +---- lib/server.php | 9 --------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/lib/appframework/http/request.php b/lib/appframework/http/request.php index 3426f0bf75..8c1c2f8ec8 100644 --- a/lib/appframework/http/request.php +++ b/lib/appframework/http/request.php @@ -42,14 +42,12 @@ class Request implements \ArrayAccess, \Countable, IRequest { 'env', 'cookies', 'urlParams', - 'params', 'parameters', 'method' ); /** * @param array $vars An associative array with the following optional values: - * @param array 'params' the parsed json array * @param array 'urlParams' the parameters which were matched from the URL * @param array 'get' the $_GET array * @param array|string 'post' the $_POST array or JSON string @@ -74,11 +72,10 @@ class Request implements \ArrayAccess, \Countable, IRequest { if (isset($this->items['post']) && strpos($this->getHeader('Content-Type'), 'application/json') !== false && is_string($this->items['post'])) { - $this->items['post'] = json_decode($this->items['post'], true); + $this->items['params'] = $this->items['post'] = json_decode($this->items['post'], true); } $this->items['parameters'] = array_merge( - $this->items['params'], $this->items['get'], $this->items['post'], $this->items['urlParams'] diff --git a/lib/server.php b/lib/server.php index cabb15324e..ef2007663c 100644 --- a/lib/server.php +++ b/lib/server.php @@ -22,14 +22,6 @@ class Server extends SimpleContainer implements IServerContainer { return new ContactsManager(); }); $this->registerService('Request', function($c) { - $params = array(); - - // we json decode the body only in case of content type json - if (isset($_SERVER['CONTENT_TYPE']) && stripos($_SERVER['CONTENT_TYPE'],'json') !== false ) { - $params = json_decode(file_get_contents('php://input'), true); - $params = is_array($params) ? $params: array(); - } - return new Request( array( 'get' => $_GET, @@ -41,7 +33,6 @@ class Server extends SimpleContainer implements IServerContainer { 'method' => (isset($_SERVER) && isset($_SERVER['REQUEST_METHOD'])) ? $_SERVER['REQUEST_METHOD'] : null, - 'params' => $params, 'urlParams' => $c['urlParams'] ) ); From 26785c9e0de4a8cba475efe23c3bd996c7b46d0d Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Mon, 30 Sep 2013 16:30:36 +0200 Subject: [PATCH 11/21] Remove getContent() from IRequest --- lib/public/irequest.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/public/irequest.php b/lib/public/irequest.php index b9bcc4bbc2..054f15d9eb 100644 --- a/lib/public/irequest.php +++ b/lib/public/irequest.php @@ -107,12 +107,4 @@ interface IRequest { function getCookie($key); - /** - * Returns the request body content. - * - * @param Boolean $asResource If true, a resource will be returned - * @return string|resource The request body content or a resource to read the body stream. - * @throws \LogicException - */ - //function getContent($asResource = false); } From bdad7697ac333ea442b79b5e0e00f90ad5398bc9 Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Fri, 27 Sep 2013 14:36:19 +0200 Subject: [PATCH 12/21] Check if accessor matched request method. It's easier to find errors in the code if an exception is thrown. --- lib/private/appframework/http/request.php | 3 +++ tests/lib/appframework/http/RequestTest.php | 27 +++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/lib/private/appframework/http/request.php b/lib/private/appframework/http/request.php index 34605acdfe..5a86066b48 100644 --- a/lib/private/appframework/http/request.php +++ b/lib/private/appframework/http/request.php @@ -152,6 +152,9 @@ class Request implements \ArrayAccess, \Countable, IRequest { switch($name) { case 'get': case 'post': + if($this->method !== strtoupper($name)) { + throw new \BadMethodCallException(sprintf('%s cannot be accessed in a %s request.', $name, $this->method)); + } case 'files': case 'server': case 'env': diff --git a/tests/lib/appframework/http/RequestTest.php b/tests/lib/appframework/http/RequestTest.php index 0371c870cf..ff4a8357f0 100644 --- a/tests/lib/appframework/http/RequestTest.php +++ b/tests/lib/appframework/http/RequestTest.php @@ -14,6 +14,7 @@ class RequestTest extends \PHPUnit_Framework_TestCase { public function testRequestAccessors() { $vars = array( 'get' => array('name' => 'John Q. Public', 'nickname' => 'Joey'), + 'method' => 'GET', ); $request = new Request($vars); @@ -73,4 +74,30 @@ class RequestTest extends \PHPUnit_Framework_TestCase { $request->{'nickname'} = 'Janey'; } + /** + * @expectedException BadMethodCallException + */ + public function testGetTheMethodRight() { + $vars = array( + 'get' => array('name' => 'John Q. Public', 'nickname' => 'Joey'), + 'method' => 'GET', + ); + + $request = new Request($vars); + $result = $request->post; + } + + public function testTheMethodIsRight() { + $vars = array( + 'get' => array('name' => 'John Q. Public', 'nickname' => 'Joey'), + 'method' => 'GET', + ); + + $request = new Request($vars); + $this->assertEquals('GET', $request->method); + $result = $request->get; + $this->assertEquals('John Q. Public', $result['name']); + $this->assertEquals('Joey', $result['nickname']); + } + } From cd2e1d0cfe69dbebfd2c30876987586026ffbea8 Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Fri, 27 Sep 2013 15:54:18 +0200 Subject: [PATCH 13/21] Add patch method to OC_Route --- lib/private/route.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/private/route.php b/lib/private/route.php index 5901717c09..fb7da456b6 100644 --- a/lib/private/route.php +++ b/lib/private/route.php @@ -51,6 +51,14 @@ class OC_Route extends Route { return $this; } + /** + * Specify PATCH as the method to use with this route + */ + public function patch() { + $this->method('PATCH'); + return $this; + } + /** * Defaults to use for this route * From 36d1156cf8fbd340ecfcc9d9a06e69004acfe154 Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Fri, 27 Sep 2013 15:55:22 +0200 Subject: [PATCH 14/21] Add interface docs to IRequest. --- lib/public/irequest.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/public/irequest.php b/lib/public/irequest.php index 9f335b06f2..5611180473 100644 --- a/lib/public/irequest.php +++ b/lib/public/irequest.php @@ -22,6 +22,28 @@ namespace OCP; +/** + * This interface provides an immutable object with with accessors to + * request variables and headers. + * + * Access request variables by method and name. + * + * Examples: + * + * $request->post['myvar']; // Only look for POST variables + * $request->myvar; or $request->{'myvar'}; or $request->{$myvar} + * Looks in the combined GET, POST and urlParams array. + * + * If you access e.g. ->post but the current HTTP request method + * is GET a \LogicException will be thrown. + * + * NOTE: + * - When accessing ->put a stream resource is returned and the accessor + * will return false on subsequent access to ->put or ->patch. + * - When accessing ->patch and the Content-Type is either application/json + * or application/x-www-form-urlencoded (most cases) it will act like ->get + * and ->post and return an array. Otherwise the raw data will be returned. + */ interface IRequest { From 973bcccd7cc18867ea22c24f83421ed180b5022c Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Sat, 28 Sep 2013 01:35:24 +0200 Subject: [PATCH 15/21] Implement PUT an PATCH support --- lib/private/appframework/http/request.php | 101 ++++++++++++----- lib/public/irequest.php | 2 +- tests/lib/appframework/http/RequestTest.php | 101 ++++++++++++++++- tests/lib/appframework/http/requeststream.php | 107 ++++++++++++++++++ 4 files changed, 281 insertions(+), 30 deletions(-) create mode 100644 tests/lib/appframework/http/requeststream.php diff --git a/lib/private/appframework/http/request.php b/lib/private/appframework/http/request.php index 5a86066b48..3426f0bf75 100644 --- a/lib/private/appframework/http/request.php +++ b/lib/private/appframework/http/request.php @@ -31,10 +31,12 @@ use OCP\IRequest; class Request implements \ArrayAccess, \Countable, IRequest { + protected $content; protected $items = array(); protected $allowedKeys = array( 'get', 'post', + 'patch', 'files', 'server', 'env', @@ -50,7 +52,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { * @param array 'params' the parsed json array * @param array 'urlParams' the parameters which were matched from the URL * @param array 'get' the $_GET array - * @param array 'post' the $_POST array + * @param array|string 'post' the $_POST array or JSON string * @param array 'files' the $_FILES array * @param array 'server' the $_SERVER array * @param array 'env' the $_ENV array @@ -62,11 +64,19 @@ class Request implements \ArrayAccess, \Countable, IRequest { public function __construct(array $vars=array()) { foreach($this->allowedKeys as $name) { - $this->items[$name] = isset($vars[$name]) + $this->items[$name] = isset($vars[$name]) ? $vars[$name] : array(); } + // Only 'application/x-www-form-urlencoded' requests are automatically + // transformed by PHP, 'application/json' must be decoded manually. + if (isset($this->items['post']) + && strpos($this->getHeader('Content-Type'), 'application/json') !== false + && is_string($this->items['post'])) { + $this->items['post'] = json_decode($this->items['post'], true); + } + $this->items['parameters'] = array_merge( $this->items['params'], $this->items['get'], @@ -141,19 +151,21 @@ class Request implements \ArrayAccess, \Countable, IRequest { * $request->myvar; or $request->{'myvar'}; or $request->{$myvar} * Looks in the combined GET, POST and urlParams array. * - * if($request->method !== 'POST') { - * throw new Exception('This function can only be invoked using POST'); - * } + * If you access e.g. ->post but the current HTTP request method + * is GET a \LogicException will be thrown. * * @param string $name The key to look for. + * @throws \LogicException * @return mixed|null */ public function __get($name) { switch($name) { + case 'put': + case 'patch': case 'get': case 'post': if($this->method !== strtoupper($name)) { - throw new \BadMethodCallException(sprintf('%s cannot be accessed in a %s request.', $name, $this->method)); + throw new \LogicException(sprintf('%s cannot be accessed in a %s request.', $name, $this->method)); } case 'files': case 'server': @@ -162,9 +174,13 @@ class Request implements \ArrayAccess, \Countable, IRequest { case 'parameters': case 'params': case 'urlParams': - return isset($this->items[$name]) - ? $this->items[$name] - : null; + if(in_array($name, array('put', 'patch'))) { + return $this->getContent($name); + } else { + return isset($this->items[$name]) + ? $this->items[$name] + : null; + } break; case 'method': return $this->items['method']; @@ -283,28 +299,57 @@ class Request implements \ArrayAccess, \Countable, IRequest { /** * Returns the request body content. * - * @param Boolean $asResource If true, a resource will be returned + * If the HTTP request method is PUT a stream resource is returned, otherwise an + * array or a string depending on the Content-Type. For "normal" use an array + * will be returned. * - * @return string|resource The request body content or a resource to read the body stream. + * @return array|string|resource The request body content or a resource to read the body stream. * * @throws \LogicException */ - function getContent($asResource = false) { - return null; -// if (false === $this->content || (true === $asResource && null !== $this->content)) { -// throw new \LogicException('getContent() can only be called once when using the resource return type.'); -// } -// -// if (true === $asResource) { -// $this->content = false; -// -// return fopen('php://input', 'rb'); -// } -// -// if (null === $this->content) { -// $this->content = file_get_contents('php://input'); -// } -// -// return $this->content; + protected function getContent() { + if ($this->content === false && $this->method === 'PUT') { + throw new \LogicException('"put" can only be accessed once.'); + } + + if (defined('PHPUNIT_RUN') && PHPUNIT_RUN + && in_array('fakeinput', stream_get_wrappers())) { + $stream = 'fakeinput://data'; + } else { + $stream = 'php://input'; + } + + if ($this->method === 'PUT') { + $this->content = false; + return fopen($stream, 'rb'); + } + + if (is_null($this->content)) { + $this->content = file_get_contents($stream); + + if ($this->method === 'PATCH') { + /* + * Normal jquery ajax requests are sent as application/x-www-form-urlencoded + * and in $_GET and $_POST PHP transformes the data into an array. + * The first condition mimics this. + * The second condition allows for sending raw application/json data while + * still getting the result as an array. + * + */ + if (strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') !== false) { + parse_str($this->content, $content); + if(is_array($content)) { + $this->content = $content; + } + } elseif (strpos($this->getHeader('Content-Type'), 'application/json') !== false) { + $content = json_decode($this->content, true); + if(is_array($content)) { + $this->content = $content; + } + } + } + } + + return $this->content; } } diff --git a/lib/public/irequest.php b/lib/public/irequest.php index 5611180473..b9bcc4bbc2 100644 --- a/lib/public/irequest.php +++ b/lib/public/irequest.php @@ -114,5 +114,5 @@ interface IRequest { * @return string|resource The request body content or a resource to read the body stream. * @throws \LogicException */ - function getContent($asResource = false); + //function getContent($asResource = false); } diff --git a/tests/lib/appframework/http/RequestTest.php b/tests/lib/appframework/http/RequestTest.php index ff4a8357f0..847c6610fe 100644 --- a/tests/lib/appframework/http/RequestTest.php +++ b/tests/lib/appframework/http/RequestTest.php @@ -8,6 +8,7 @@ namespace OC\AppFramework\Http; +global $data; class RequestTest extends \PHPUnit_Framework_TestCase { @@ -32,6 +33,8 @@ class RequestTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('Joey', $request->get['nickname']); // Always returns null if variable not set. $this->assertEquals(null, $request->{'flickname'}); + + require_once __DIR__ . '/requeststream.php'; } // urlParams has precedence over POST which has precedence over GET @@ -75,7 +78,7 @@ class RequestTest extends \PHPUnit_Framework_TestCase { } /** - * @expectedException BadMethodCallException + * @expectedException LogicException */ public function testGetTheMethodRight() { $vars = array( @@ -100,4 +103,100 @@ class RequestTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('Joey', $result['nickname']); } + public function testJsonPost() { + $vars = array( + 'post' => '{"name": "John Q. Public", "nickname": "Joey"}', + 'method' => 'POST', + 'server' => array('CONTENT_TYPE' => 'application/json; utf-8'), + ); + + $request = new Request($vars); + $this->assertEquals('POST', $request->method); + $result = $request->post; + $this->assertEquals('John Q. Public', $result['name']); + $this->assertEquals('Joey', $result['nickname']); + } + + public function testPatch() { + global $data; + $data = http_build_query(array('name' => 'John Q. Public', 'nickname' => 'Joey'), '', '&'); + + if (in_array('fakeinput', stream_get_wrappers())) { + stream_wrapper_unregister('fakeinput'); + } + stream_wrapper_register('fakeinput', 'RequestStream'); + + $vars = array( + 'patch' => $data, + 'method' => 'PATCH', + 'server' => array('CONTENT_TYPE' => 'application/x-www-form-urlencoded'), + ); + + $request = new Request($vars); + + $this->assertEquals('PATCH', $request->method); + $result = $request->patch; + + $this->assertEquals('John Q. Public', $result['name']); + $this->assertEquals('Joey', $result['nickname']); + + stream_wrapper_unregister('fakeinput'); + } + + public function testJsonPatch() { + global $data; + $data = '{"name": "John Q. Public", "nickname": null}'; + + if (in_array('fakeinput', stream_get_wrappers())) { + stream_wrapper_unregister('fakeinput'); + } + stream_wrapper_register('fakeinput', 'RequestStream'); + + $vars = array( + 'patch' => $data, + 'method' => 'PATCH', + 'server' => array('CONTENT_TYPE' => 'application/json; utf-8'), + ); + + $request = new Request($vars); + + $this->assertEquals('PATCH', $request->method); + $result = $request->patch; + + $this->assertEquals('John Q. Public', $result['name']); + $this->assertEquals(null, $result['nickname']); + + stream_wrapper_unregister('fakeinput'); + } + + public function testPutSteam() { + global $data; + $data = file_get_contents(__DIR__ . '/../../../data/testimage.png'); + + if (in_array('fakeinput', stream_get_wrappers())) { + stream_wrapper_unregister('fakeinput'); + } + stream_wrapper_register('fakeinput', 'RequestStream'); + + $vars = array( + 'put' => $data, + 'method' => 'PUT', + 'server' => array('CONTENT_TYPE' => 'image/png'), + ); + + $request = new Request($vars); + $this->assertEquals('PUT', $request->method); + $resource = $request->put; + $contents = stream_get_contents($resource); + $this->assertEquals($data, $contents); + + try { + $resource = $request->put; + } catch(\LogicException $e) { + stream_wrapper_unregister('fakeinput'); + return; + } + $this->fail('Expected LogicException.'); + + } } diff --git a/tests/lib/appframework/http/requeststream.php b/tests/lib/appframework/http/requeststream.php new file mode 100644 index 0000000000..e1bf5c2c6b --- /dev/null +++ b/tests/lib/appframework/http/requeststream.php @@ -0,0 +1,107 @@ +varname = $url["host"]; + $this->position = 0; + + return true; + } + + function stream_read($count) { + $ret = substr($GLOBALS[$this->varname], $this->position, $count); + $this->position += strlen($ret); + return $ret; + } + + function stream_write($data) { + $left = substr($GLOBALS[$this->varname], 0, $this->position); + $right = substr($GLOBALS[$this->varname], $this->position + strlen($data)); + $GLOBALS[$this->varname] = $left . $data . $right; + $this->position += strlen($data); + return strlen($data); + } + + function stream_tell() { + return $this->position; + } + + function stream_eof() { + return $this->position >= strlen($GLOBALS[$this->varname]); + } + + function stream_seek($offset, $whence) { + switch ($whence) { + case SEEK_SET: + if ($offset < strlen($GLOBALS[$this->varname]) && $offset >= 0) { + $this->position = $offset; + return true; + } else { + return false; + } + break; + + case SEEK_CUR: + if ($offset >= 0) { + $this->position += $offset; + return true; + } else { + return false; + } + break; + + case SEEK_END: + if (strlen($GLOBALS[$this->varname]) + $offset >= 0) { + $this->position = strlen($GLOBALS[$this->varname]) + $offset; + return true; + } else { + return false; + } + break; + + default: + return false; + } + } + + public function stream_stat() { + $size = strlen($GLOBALS[$this->varname]); + $time = time(); + $data = array( + 'dev' => 0, + 'ino' => 0, + 'mode' => 0777, + 'nlink' => 1, + 'uid' => 0, + 'gid' => 0, + 'rdev' => '', + 'size' => $size, + 'atime' => $time, + 'mtime' => $time, + 'ctime' => $time, + 'blksize' => -1, + 'blocks' => -1, + ); + return array_values($data) + $data; + //return false; + } + + function stream_metadata($path, $option, $var) { + if($option == STREAM_META_TOUCH) { + $url = parse_url($path); + $varname = $url["host"]; + if(!isset($GLOBALS[$varname])) { + $GLOBALS[$varname] = ''; + } + return true; + } + return false; + } +} From 7cd80888455d5292fa74499d55b89f7dd61c9267 Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Mon, 30 Sep 2013 16:23:43 +0200 Subject: [PATCH 16/21] Add assertions for ->params and array access with json --- tests/lib/appframework/http/RequestTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/lib/appframework/http/RequestTest.php b/tests/lib/appframework/http/RequestTest.php index 847c6610fe..aaa2328be1 100644 --- a/tests/lib/appframework/http/RequestTest.php +++ b/tests/lib/appframework/http/RequestTest.php @@ -115,6 +115,8 @@ class RequestTest extends \PHPUnit_Framework_TestCase { $result = $request->post; $this->assertEquals('John Q. Public', $result['name']); $this->assertEquals('Joey', $result['nickname']); + $this->assertEquals('Joey', $request->params['nickname']); + $this->assertEquals('Joey', $request['nickname']); } public function testPatch() { From 0f13ffb773a95ddf1a941fb76ba995c01693160d Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Mon, 30 Sep 2013 16:24:47 +0200 Subject: [PATCH 17/21] Remove JSON request parsing from Server --- lib/private/appframework/http/request.php | 5 +---- lib/private/server.php | 9 --------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/lib/private/appframework/http/request.php b/lib/private/appframework/http/request.php index 3426f0bf75..8c1c2f8ec8 100644 --- a/lib/private/appframework/http/request.php +++ b/lib/private/appframework/http/request.php @@ -42,14 +42,12 @@ class Request implements \ArrayAccess, \Countable, IRequest { 'env', 'cookies', 'urlParams', - 'params', 'parameters', 'method' ); /** * @param array $vars An associative array with the following optional values: - * @param array 'params' the parsed json array * @param array 'urlParams' the parameters which were matched from the URL * @param array 'get' the $_GET array * @param array|string 'post' the $_POST array or JSON string @@ -74,11 +72,10 @@ class Request implements \ArrayAccess, \Countable, IRequest { if (isset($this->items['post']) && strpos($this->getHeader('Content-Type'), 'application/json') !== false && is_string($this->items['post'])) { - $this->items['post'] = json_decode($this->items['post'], true); + $this->items['params'] = $this->items['post'] = json_decode($this->items['post'], true); } $this->items['parameters'] = array_merge( - $this->items['params'], $this->items['get'], $this->items['post'], $this->items['urlParams'] diff --git a/lib/private/server.php b/lib/private/server.php index cabb15324e..ef2007663c 100644 --- a/lib/private/server.php +++ b/lib/private/server.php @@ -22,14 +22,6 @@ class Server extends SimpleContainer implements IServerContainer { return new ContactsManager(); }); $this->registerService('Request', function($c) { - $params = array(); - - // we json decode the body only in case of content type json - if (isset($_SERVER['CONTENT_TYPE']) && stripos($_SERVER['CONTENT_TYPE'],'json') !== false ) { - $params = json_decode(file_get_contents('php://input'), true); - $params = is_array($params) ? $params: array(); - } - return new Request( array( 'get' => $_GET, @@ -41,7 +33,6 @@ class Server extends SimpleContainer implements IServerContainer { 'method' => (isset($_SERVER) && isset($_SERVER['REQUEST_METHOD'])) ? $_SERVER['REQUEST_METHOD'] : null, - 'params' => $params, 'urlParams' => $c['urlParams'] ) ); From a2cabd4c2abdc8e1c23bd4561d013beae4fb14f9 Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Mon, 30 Sep 2013 16:30:36 +0200 Subject: [PATCH 18/21] Remove getContent() from IRequest --- lib/public/irequest.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/public/irequest.php b/lib/public/irequest.php index b9bcc4bbc2..054f15d9eb 100644 --- a/lib/public/irequest.php +++ b/lib/public/irequest.php @@ -107,12 +107,4 @@ interface IRequest { function getCookie($key); - /** - * Returns the request body content. - * - * @param Boolean $asResource If true, a resource will be returned - * @return string|resource The request body content or a resource to read the body stream. - * @throws \LogicException - */ - //function getContent($asResource = false); } From 965ce5719f1e936c731871360c89e459e9a20bd9 Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Wed, 2 Oct 2013 22:13:40 +0200 Subject: [PATCH 19/21] Modified PUT behaviour Now only non-parable PUT requests return a stream resource. --- lib/private/appframework/http/request.php | 56 +++++++++++---------- tests/lib/appframework/http/RequestTest.php | 56 ++++++++++++--------- 2 files changed, 61 insertions(+), 51 deletions(-) diff --git a/lib/private/appframework/http/request.php b/lib/private/appframework/http/request.php index 8c1c2f8ec8..b6832735fa 100644 --- a/lib/private/appframework/http/request.php +++ b/lib/private/appframework/http/request.php @@ -36,7 +36,6 @@ class Request implements \ArrayAccess, \Countable, IRequest { protected $allowedKeys = array( 'get', 'post', - 'patch', 'files', 'server', 'env', @@ -69,7 +68,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { // Only 'application/x-www-form-urlencoded' requests are automatically // transformed by PHP, 'application/json' must be decoded manually. - if (isset($this->items['post']) + if ($this->method === 'POST' && strpos($this->getHeader('Content-Type'), 'application/json') !== false && is_string($this->items['post'])) { $this->items['params'] = $this->items['post'] = json_decode($this->items['post'], true); @@ -296,9 +295,9 @@ class Request implements \ArrayAccess, \Countable, IRequest { /** * Returns the request body content. * - * If the HTTP request method is PUT a stream resource is returned, otherwise an - * array or a string depending on the Content-Type. For "normal" use an array - * will be returned. + * If the HTTP request method is PUT and the body + * not application/x-www-form-urlencoded or application/json a stream + * resource is returned, otherwise an array. * * @return array|string|resource The request body content or a resource to read the body stream. * @@ -306,7 +305,10 @@ class Request implements \ArrayAccess, \Countable, IRequest { */ protected function getContent() { if ($this->content === false && $this->method === 'PUT') { - throw new \LogicException('"put" can only be accessed once.'); + throw new \LogicException( + '"put" can only be accessed once if not ' + . 'application/x-www-form-urlencoded or application/json.' + ); } if (defined('PHPUNIT_RUN') && PHPUNIT_RUN @@ -316,7 +318,11 @@ class Request implements \ArrayAccess, \Countable, IRequest { $stream = 'php://input'; } - if ($this->method === 'PUT') { + // If the content can't be parsed into an array then return a stream resource. + if ($this->method === 'PUT' + && strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') === false + && strpos($this->getHeader('Content-Type'), 'application/json') === false + ) { $this->content = false; return fopen($stream, 'rb'); } @@ -324,25 +330,23 @@ class Request implements \ArrayAccess, \Countable, IRequest { if (is_null($this->content)) { $this->content = file_get_contents($stream); - if ($this->method === 'PATCH') { - /* - * Normal jquery ajax requests are sent as application/x-www-form-urlencoded - * and in $_GET and $_POST PHP transformes the data into an array. - * The first condition mimics this. - * The second condition allows for sending raw application/json data while - * still getting the result as an array. - * - */ - if (strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') !== false) { - parse_str($this->content, $content); - if(is_array($content)) { - $this->content = $content; - } - } elseif (strpos($this->getHeader('Content-Type'), 'application/json') !== false) { - $content = json_decode($this->content, true); - if(is_array($content)) { - $this->content = $content; - } + /* + * Normal jquery ajax requests are sent as application/x-www-form-urlencoded + * and in $_GET and $_POST PHP transformes the data into an array. + * The first condition mimics this. + * The second condition allows for sending raw application/json data while + * still getting the result as an array. + * + */ + if (strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') !== false) { + parse_str($this->content, $content); + if(is_array($content)) { + $this->content = $content; + } + } elseif (strpos($this->getHeader('Content-Type'), 'application/json') !== false) { + $content = json_decode($this->content, true); + if(is_array($content)) { + $this->content = $content; } } } diff --git a/tests/lib/appframework/http/RequestTest.php b/tests/lib/appframework/http/RequestTest.php index aaa2328be1..acd61e7091 100644 --- a/tests/lib/appframework/http/RequestTest.php +++ b/tests/lib/appframework/http/RequestTest.php @@ -12,6 +12,18 @@ global $data; class RequestTest extends \PHPUnit_Framework_TestCase { + public function setUp() { + require_once __DIR__ . '/requeststream.php'; + if (in_array('fakeinput', stream_get_wrappers())) { + stream_wrapper_unregister('fakeinput'); + } + stream_wrapper_register('fakeinput', 'RequestStream'); + } + + public function tearDown() { + stream_wrapper_unregister('fakeinput'); + } + public function testRequestAccessors() { $vars = array( 'get' => array('name' => 'John Q. Public', 'nickname' => 'Joey'), @@ -34,7 +46,6 @@ class RequestTest extends \PHPUnit_Framework_TestCase { // Always returns null if variable not set. $this->assertEquals(null, $request->{'flickname'}); - require_once __DIR__ . '/requeststream.php'; } // urlParams has precedence over POST which has precedence over GET @@ -123,11 +134,6 @@ class RequestTest extends \PHPUnit_Framework_TestCase { global $data; $data = http_build_query(array('name' => 'John Q. Public', 'nickname' => 'Joey'), '', '&'); - if (in_array('fakeinput', stream_get_wrappers())) { - stream_wrapper_unregister('fakeinput'); - } - stream_wrapper_register('fakeinput', 'RequestStream'); - $vars = array( 'patch' => $data, 'method' => 'PATCH', @@ -141,21 +147,29 @@ class RequestTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('John Q. Public', $result['name']); $this->assertEquals('Joey', $result['nickname']); - - stream_wrapper_unregister('fakeinput'); } - public function testJsonPatch() { + public function testJsonPatchAndPut() { global $data; - $data = '{"name": "John Q. Public", "nickname": null}'; - - if (in_array('fakeinput', stream_get_wrappers())) { - stream_wrapper_unregister('fakeinput'); - } - stream_wrapper_register('fakeinput', 'RequestStream'); + // PUT content + $data = '{"name": "John Q. Public", "nickname": "Joey"}'; + $vars = array( + 'method' => 'PUT', + 'server' => array('CONTENT_TYPE' => 'application/json; utf-8'), + ); + + $request = new Request($vars); + + $this->assertEquals('PUT', $request->method); + $result = $request->put; + + $this->assertEquals('John Q. Public', $result['name']); + $this->assertEquals('Joey', $result['nickname']); + + // PATCH content + $data = '{"name": "John Q. Public", "nickname": null}'; $vars = array( - 'patch' => $data, 'method' => 'PATCH', 'server' => array('CONTENT_TYPE' => 'application/json; utf-8'), ); @@ -167,19 +181,12 @@ class RequestTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('John Q. Public', $result['name']); $this->assertEquals(null, $result['nickname']); - - stream_wrapper_unregister('fakeinput'); } - public function testPutSteam() { + public function testPutStream() { global $data; $data = file_get_contents(__DIR__ . '/../../../data/testimage.png'); - if (in_array('fakeinput', stream_get_wrappers())) { - stream_wrapper_unregister('fakeinput'); - } - stream_wrapper_register('fakeinput', 'RequestStream'); - $vars = array( 'put' => $data, 'method' => 'PUT', @@ -195,7 +202,6 @@ class RequestTest extends \PHPUnit_Framework_TestCase { try { $resource = $request->put; } catch(\LogicException $e) { - stream_wrapper_unregister('fakeinput'); return; } $this->fail('Expected LogicException.'); From 8a018d7a59047ef45bfa71b4f07db11a504085b8 Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Thu, 3 Oct 2013 01:43:33 +0200 Subject: [PATCH 20/21] Fix POST decoding --- lib/private/appframework/http/request.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/private/appframework/http/request.php b/lib/private/appframework/http/request.php index b6832735fa..b45ac2f009 100644 --- a/lib/private/appframework/http/request.php +++ b/lib/private/appframework/http/request.php @@ -70,8 +70,8 @@ class Request implements \ArrayAccess, \Countable, IRequest { // transformed by PHP, 'application/json' must be decoded manually. if ($this->method === 'POST' && strpos($this->getHeader('Content-Type'), 'application/json') !== false - && is_string($this->items['post'])) { - $this->items['params'] = $this->items['post'] = json_decode($this->items['post'], true); + ) { + $this->items['params'] = $this->items['post'] = json_decode(file_get_contents('php://input'), true); } $this->items['parameters'] = array_merge( From aedc427ffd73f9ab249f1722c197205bc366ed8d Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Thu, 3 Oct 2013 03:56:37 +0200 Subject: [PATCH 21/21] Fix fix of POST :P --- lib/private/appframework/http/request.php | 21 +++++++++++---------- tests/lib/appframework/http/RequestTest.php | 4 ++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/private/appframework/http/request.php b/lib/private/appframework/http/request.php index b45ac2f009..f152956c8c 100644 --- a/lib/private/appframework/http/request.php +++ b/lib/private/appframework/http/request.php @@ -31,6 +31,7 @@ use OCP\IRequest; class Request implements \ArrayAccess, \Countable, IRequest { + protected $inputStream; protected $content; protected $items = array(); protected $allowedKeys = array( @@ -66,12 +67,19 @@ class Request implements \ArrayAccess, \Countable, IRequest { : array(); } + if (defined('PHPUNIT_RUN') && PHPUNIT_RUN + && in_array('fakeinput', stream_get_wrappers())) { + $this->inputStream = 'fakeinput://data'; + } else { + $this->inputStream = 'php://input'; + } + // Only 'application/x-www-form-urlencoded' requests are automatically // transformed by PHP, 'application/json' must be decoded manually. if ($this->method === 'POST' && strpos($this->getHeader('Content-Type'), 'application/json') !== false ) { - $this->items['params'] = $this->items['post'] = json_decode(file_get_contents('php://input'), true); + $this->items['params'] = $this->items['post'] = json_decode(file_get_contents($this->inputStream), true); } $this->items['parameters'] = array_merge( @@ -311,24 +319,17 @@ class Request implements \ArrayAccess, \Countable, IRequest { ); } - if (defined('PHPUNIT_RUN') && PHPUNIT_RUN - && in_array('fakeinput', stream_get_wrappers())) { - $stream = 'fakeinput://data'; - } else { - $stream = 'php://input'; - } - // If the content can't be parsed into an array then return a stream resource. if ($this->method === 'PUT' && strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') === false && strpos($this->getHeader('Content-Type'), 'application/json') === false ) { $this->content = false; - return fopen($stream, 'rb'); + return fopen($this->inputStream, 'rb'); } if (is_null($this->content)) { - $this->content = file_get_contents($stream); + $this->content = file_get_contents($this->inputStream); /* * Normal jquery ajax requests are sent as application/x-www-form-urlencoded diff --git a/tests/lib/appframework/http/RequestTest.php b/tests/lib/appframework/http/RequestTest.php index acd61e7091..00473a8c44 100644 --- a/tests/lib/appframework/http/RequestTest.php +++ b/tests/lib/appframework/http/RequestTest.php @@ -115,8 +115,9 @@ class RequestTest extends \PHPUnit_Framework_TestCase { } public function testJsonPost() { + global $data; + $data = '{"name": "John Q. Public", "nickname": "Joey"}'; $vars = array( - 'post' => '{"name": "John Q. Public", "nickname": "Joey"}', 'method' => 'POST', 'server' => array('CONTENT_TYPE' => 'application/json; utf-8'), ); @@ -135,7 +136,6 @@ class RequestTest extends \PHPUnit_Framework_TestCase { $data = http_build_query(array('name' => 'John Q. Public', 'nickname' => 'Joey'), '', '&'); $vars = array( - 'patch' => $data, 'method' => 'PATCH', 'server' => array('CONTENT_TYPE' => 'application/x-www-form-urlencoded'), );