From d124b6965b4ed9aed03396d97bc148038148372b Mon Sep 17 00:00:00 2001
From: Thomas Tanghus <thomas@tanghus.net>
Date: Sat, 28 Sep 2013 01:35:24 +0200
Subject: [PATCH] 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 @@
+<?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;
+	}
+}
-- 
GitLab