Browse Source

Adding ability to map arbitrary content types and decoding methods to RequestHandlerComponent.
Removing hardcoded XML decoding, but continuing to provide the functionality.
Adding default support for JSON decoding.
Fixing stdin/input mixup.
Fixes #716

mark_story 15 years ago
parent
commit
512e570202

+ 58 - 27
lib/Cake/Controller/Component/RequestHandlerComponent.php

@@ -34,7 +34,6 @@ class RequestHandlerComponent extends Component {
  * The layout that will be switched to for Ajax requests
  *
  * @var string
- * @access public
  * @see RequestHandler::setAjax()
  */
 	public $ajaxLayout = 'ajax';
@@ -43,7 +42,6 @@ class RequestHandlerComponent extends Component {
  * Determines whether or not callbacks will be fired on this component
  *
  * @var boolean
- * @access public
  */
 	public $enabled = true;
 
@@ -51,7 +49,6 @@ class RequestHandlerComponent extends Component {
  * Holds the reference to Controller::$request
  *
  * @var CakeRequest
- * @access public
  */
 	public $request;
 
@@ -59,35 +56,33 @@ class RequestHandlerComponent extends Component {
  * Holds the reference to Controller::$response
  *
  * @var CakeResponse
- * @access public
  */
 	public $response;
 
 /**
- * The template to use when rendering the given content type.
+ * Contains the file extension parsed out by the Router
  *
  * @var string
- * @access private
+ * @see Router::parseExtensions()
  */
-	private $__renderType = null;
+	public $ext = null;
 
 /**
- * Contains the file extension parsed out by the Router
+ * The template to use when rendering the given content type.
  *
  * @var string
- * @access public
- * @see Router::parseExtensions()
  */
-	public $ext = null;
+	private $__renderType = null;
 
 /**
- * Flag set when MIME types have been initialized
+ * A mapping between extensions and deserializers for request bodies of that type.
+ * By default only JSON and XML are mapped, use RequestHandlerComponent::addInputType()
  *
- * @var boolean
- * @access private
- * @see RequestHandler::__initializeTypes()
+ * @var array
  */
-	private $__typesInitialized = false;
+	private $__inputTypeMap = array(
+		'json' => array('json_decode', true)
+	);
 
 /**
  * Constructor. Parses the accepted content types accepted by the client using HTTP_ACCEPT
@@ -96,6 +91,7 @@ class RequestHandlerComponent extends Component {
  * @param array $settings Array of settings.
  */
 	function __construct(ComponentCollection $collection, $settings = array()) {
+		$this->addInputType('xml', array(array($this, '_convertXml')));
 		$this->__acceptTypes = explode(',', env('HTTP_ACCEPT'));
 
 		foreach ($this->__acceptTypes as $i => $type) {
@@ -172,20 +168,35 @@ class RequestHandlerComponent extends Component {
 			$this->respondAs('html', array('charset' => Configure::read('App.encoding')));
 		}
 
-		if ($this->requestedWith('xml')) {
-			try {
-				$xml = $controller->request->input('Xml::build');
-
-				if (isset($xml->data)) {
-					$controller->data = Xml::toArray($xml->data);
-				} else {
-					$controller->data = Xml::toArray($xml);
-				}
-			} catch (Exception $e) {}
+		foreach ($this->__inputTypeMap as $type => $handler) {
+			if ($this->requestedWith($type)) {
+				$input = call_user_func_array(array($controller->request, 'input'), $handler);
+				$controller->request->data = $input;
+			}
 		}
 	}
 
 /**
+ * Helper method to parse xml input data, due to lack of anonymous functions
+ * this lives here.
+ *
+ * @param string $xml 
+ * @return array Xml array data
+ * @access protected
+ */
+	public function _convertXml($xml) {
+		try {
+			$xml = Xml::build($xml);
+			if (isset($xml->data)) {
+				return Xml::toArray($xml->data);
+			}
+			return Xml::toArray($xml);
+		 } catch (XmlException $e) {
+			return array();
+		 }
+	}
+
+/**
  * Handles (fakes) redirects for Ajax requests using requestAction()
  *
  * @param object $controller A reference to the controller
@@ -216,6 +227,7 @@ class RequestHandlerComponent extends Component {
  * Returns true if the current HTTP request is Ajax, false otherwise
  *
  * @return boolean True if call is Ajax
+ * @deprecated use `$this->request->is('ajax')` instead.
  */
 	public function isAjax() {
 		return $this->request->is('ajax');
@@ -225,6 +237,7 @@ class RequestHandlerComponent extends Component {
  * Returns true if the current HTTP request is coming from a Flash-based client
  *
  * @return boolean True if call is from Flash
+ * @deprecated use `$this->request->is('flash')` instead.
  */
 	public function isFlash() {
 		return $this->request->is('flash');
@@ -234,6 +247,7 @@ class RequestHandlerComponent extends Component {
  * Returns true if the current request is over HTTPS, false otherwise.
  *
  * @return bool True if call is over HTTPS
+ * @deprecated use `$this->request->is('ssl')` instead.
  */
 	public function isSSL() {
 		return $this->request->is('ssl');
@@ -367,7 +381,7 @@ class RequestHandlerComponent extends Component {
  * Gets remote client IP
  *
  * @return string Client IP address
- * @deprecated use $this->request->clientIp() from your controller instead.
+ * @deprecated use $this->request->clientIp() from your,  controller instead.
  */
 	public function getClientIP($safe = true) {
 		return $this->request->clientIp($safe);
@@ -641,4 +655,21 @@ class RequestHandlerComponent extends Component {
 		}
 		return null;
 	}
+
+/**
+ * Add a new mapped input type.  Mapped input types are automatically 
+ * converted by RequestHandlerComponent during the startup() callback.
+ *
+ * @param string $type The type alias being converted, ie. json
+ * @param array $handler The handler array for the type.  The first index should
+ *    be the handling callback, all other arguments should be additional parameters
+ *    for the handler.
+ * @return void
+ */
+	public function addInputType($type, $handler) {
+		if (!is_array($handler) || !isset($handler[0]) || !is_callable($handler[0])) {
+			throw new CakeException(__d('cake_dev', 'You must give a handler callback.'));
+		}
+		$this->__inputTypeMap[$type] = $handler;
+	}
 }

+ 16 - 5
lib/Cake/Network/CakeRequest.php

@@ -106,6 +106,15 @@ class CakeRequest implements ArrayAccess {
 			'webOS', 'Windows CE', 'Xiino'
 		))
 	);
+
+/**
+ * Copy of php://input.  Since this stream can only be read once in most SAPI's
+ * keep a copy of it so users don't need to know about that detail.
+ *
+ * @var string
+ */
+	private $__input = '';
+
 /**
  * Constructor 
  *
@@ -708,11 +717,13 @@ class CakeRequest implements ArrayAccess {
  * @return string contents of stdin
  */
 	protected function _readStdin() {
-		$fh = fopen('php://stdin', 'r');
-		rewind($fh);
-		$content = stream_get_contents($fh);
-		fclose($fh);
-		return $content;
+		if (empty($this->__input)) {
+			$fh = fopen('php://input', 'r');
+			$content = stream_get_contents($fh);
+			fclose($fh);
+			$this->__input = $content;
+		}
+		return $this->__input;
 	}
 
 /**

+ 30 - 0
lib/Cake/tests/Case/Controller/Component/RequestHandlerComponentTest.php

@@ -313,6 +313,29 @@ class RequestHandlerComponentTest extends CakeTestCase {
 	}
 
 /**
+ * Test mapping a new type and having startup process it.
+ *
+ * @return void
+ */
+	function testStartupCustomTypeProcess() {
+		if (!function_exists('str_getcsv')) {
+			$this->markTestSkipped('Need "str_getcsv" for this test.');
+		}
+		$_SERVER['REQUEST_METHOD'] = 'POST';
+		$_SERVER['CONTENT_TYPE'] = 'text/csv';
+		$this->Controller->request = $this->getMock('CakeRequest', array('_readStdin'));
+		$this->Controller->request->expects($this->once())
+			->method('_readStdin')
+			->will($this->returnValue('"A","csv","string"'));
+		$this->RequestHandler->addInputType('csv', array('str_getcsv'));
+		$this->RequestHandler->startup($this->Controller);
+		$expected = array(
+			'A', 'csv', 'string'
+		);
+		$this->assertEquals($expected, $this->Controller->request->data);
+	}
+
+/**
  * testNonAjaxRedirect method
  *
  * @access public
@@ -776,4 +799,11 @@ class RequestHandlerComponentTest extends CakeTestCase {
 		$result = ob_get_clean();
 	}
 
+/**
+ * @expectedException CakeException
+ * @return void
+ */
+	function testAddInputTypeException() {
+		$this->RequestHandler->addInputType('csv', array('I am not callable'));
+	}
 }