]> source.dussan.org Git - nextcloud-server.git/commitdiff
Implement PUT an PATCH support
authorThomas Tanghus <thomas@tanghus.net>
Fri, 27 Sep 2013 23:35:24 +0000 (01:35 +0200)
committerThomas Tanghus <thomas@tanghus.net>
Tue, 1 Oct 2013 18:13:13 +0000 (20:13 +0200)
lib/private/appframework/http/request.php
lib/public/irequest.php
tests/lib/appframework/http/RequestTest.php
tests/lib/appframework/http/requeststream.php [new file with mode: 0644]

index 5a86066b48b640d1eeac43672070c465b38df884..3426f0bf7526634b1403cf7b85322fa8da91286e 100644 (file)
@@ -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;
        }
 }
index 5611180473d443c8598d7a50aa9819dc30f95b34..b9bcc4bbc28e7ea2d4ad525f9bc5d51cd18dd84f 100644 (file)
@@ -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);
 }
index ff4a8357f06d9a8ede3ecd16b5a7bf6a896d8dbe..847c6610fe8354bbc99d521130d79de502cfec0d 100644 (file)
@@ -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 (file)
index 0000000..e1bf5c2
--- /dev/null
@@ -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;
+       }
+}