diff --git a/lib/private/appframework/http/request.php b/lib/private/appframework/http/request.php index 5a86066b48b..3426f0bf752 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 5611180473d..b9bcc4bbc28 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 ff4a8357f06..847c6610fe8 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 00000000000..e1bf5c2c6bb --- /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; + } +}