diff options
-rw-r--r-- | lib/private/appframework/http/request.php | 110 | ||||
-rw-r--r-- | lib/private/route.php | 8 | ||||
-rw-r--r-- | lib/private/server.php | 9 | ||||
-rw-r--r-- | lib/public/irequest.php | 30 | ||||
-rw-r--r-- | tests/lib/appframework/http/RequestTest.php | 134 | ||||
-rw-r--r-- | tests/lib/appframework/http/requeststream.php | 107 |
6 files changed, 351 insertions, 47 deletions
diff --git a/lib/private/appframework/http/request.php b/lib/private/appframework/http/request.php index 34605acdfea..f152956c8cf 100644 --- a/lib/private/appframework/http/request.php +++ b/lib/private/appframework/http/request.php @@ -31,6 +31,8 @@ use OCP\IRequest; class Request implements \ArrayAccess, \Countable, IRequest { + protected $inputStream; + protected $content; protected $items = array(); protected $allowedKeys = array( 'get', @@ -40,17 +42,15 @@ 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 '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,13 +62,27 @@ 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(); } + 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($this->inputStream), true); + } + $this->items['parameters'] = array_merge( - $this->items['params'], $this->items['get'], $this->items['post'], $this->items['urlParams'] @@ -141,17 +155,22 @@ 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 \LogicException(sprintf('%s cannot be accessed in a %s request.', $name, $this->method)); + } case 'files': case 'server': case 'env': @@ -159,9 +178,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']; @@ -280,28 +303,55 @@ 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 and the body + * not application/x-www-form-urlencoded or application/json a stream + * resource is returned, otherwise an array. * - * @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 not ' + . 'application/x-www-form-urlencoded or application/json.' + ); + } + + // 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($this->inputStream, 'rb'); + } + + if (is_null($this->content)) { + $this->content = file_get_contents($this->inputStream); + + /* + * 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/private/route.php b/lib/private/route.php index 5901717c094..fb7da456b62 100644 --- a/lib/private/route.php +++ b/lib/private/route.php @@ -52,6 +52,14 @@ class OC_Route extends Route { } /** + * Specify PATCH as the method to use with this route + */ + public function patch() { + $this->method('PATCH'); + return $this; + } + + /** * Defaults to use for this route * * @param array $defaults The defaults diff --git a/lib/private/server.php b/lib/private/server.php index cabb15324ec..ef2007663c6 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'] ) ); diff --git a/lib/public/irequest.php b/lib/public/irequest.php index 9f335b06f2a..054f15d9eb2 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 { @@ -85,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); } diff --git a/tests/lib/appframework/http/RequestTest.php b/tests/lib/appframework/http/RequestTest.php index 0371c870cf2..00473a8c44f 100644 --- a/tests/lib/appframework/http/RequestTest.php +++ b/tests/lib/appframework/http/RequestTest.php @@ -8,12 +8,26 @@ namespace OC\AppFramework\Http; +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'), + 'method' => 'GET', ); $request = new Request($vars); @@ -31,6 +45,7 @@ class RequestTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('Joey', $request->get['nickname']); // Always returns null if variable not set. $this->assertEquals(null, $request->{'flickname'}); + } // urlParams has precedence over POST which has precedence over GET @@ -73,4 +88,123 @@ class RequestTest extends \PHPUnit_Framework_TestCase { $request->{'nickname'} = 'Janey'; } + /** + * @expectedException LogicException + */ + 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']); + } + + public function testJsonPost() { + global $data; + $data = '{"name": "John Q. Public", "nickname": "Joey"}'; + $vars = array( + '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']); + $this->assertEquals('Joey', $request->params['nickname']); + $this->assertEquals('Joey', $request['nickname']); + } + + public function testPatch() { + global $data; + $data = http_build_query(array('name' => 'John Q. Public', 'nickname' => 'Joey'), '', '&'); + + $vars = array( + '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']); + } + + public function testJsonPatchAndPut() { + global $data; + + // 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( + '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']); + } + + public function testPutStream() { + global $data; + $data = file_get_contents(__DIR__ . '/../../../data/testimage.png'); + + $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) { + 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 @@ +<?php +/** + * Copy of http://dk1.php.net/manual/en/stream.streamwrapper.example-1.php + * Used to simulate php://input for Request tests + */ +class RequestStream { + protected $position; + protected $varname; + + function stream_open($path, $mode, $options, &$opened_path) { + $url = parse_url($path); + $this->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; + } +} |