Skip to content

Commit

Permalink
Merge branch 'request-input' into 2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
markstory committed May 1, 2011
2 parents 75437b4 + 512e570 commit 093d7f1
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 51 deletions.
85 changes: 58 additions & 27 deletions lib/Cake/Controller/Component/RequestHandlerComponent.php
Expand Up @@ -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';
Expand All @@ -43,51 +42,47 @@ class RequestHandlerComponent extends Component {
* Determines whether or not callbacks will be fired on this component
*
* @var boolean
* @access public
*/
public $enabled = true;

/**
* Holds the reference to Controller::$request
*
* @var CakeRequest
* @access public
*/
public $request;

/**
* 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
Expand All @@ -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) {
Expand Down Expand Up @@ -172,19 +168,34 @@ public function startup($controller) {
$this->respondAs('html', array('charset' => Configure::read('App.encoding')));
}

if ($this->requestedWith('xml')) {
try {
$xml = Xml::build(trim(file_get_contents('php://input')));

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()
*
Expand Down Expand Up @@ -216,6 +227,7 @@ public function beforeRedirect($controller, $url, $status = null, $exit = true)
* 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');
Expand All @@ -225,6 +237,7 @@ public function isAjax() {
* 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');
Expand All @@ -234,6 +247,7 @@ public function isFlash() {
* 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');
Expand Down Expand Up @@ -367,7 +381,7 @@ public function getReferer() {
* 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);
Expand Down Expand Up @@ -641,4 +655,21 @@ public function mapAlias($alias) {
}
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;
}
}
86 changes: 75 additions & 11 deletions lib/Cake/Network/CakeRequest.php
Expand Up @@ -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
*
Expand Down Expand Up @@ -133,28 +142,30 @@ public function __construct($url = null, $parseEnvironment = true) {

/**
* process the post data and set what is there into the object.
* processed data is available at $this->data
*
* @return void
*/
protected function _processPost() {
$this->params['form'] = $_POST;
$this->data = $_POST;
if (ini_get('magic_quotes_gpc') === '1') {
$this->params['form'] = stripslashes_deep($this->params['form']);
$this->data = stripslashes_deep($this->data);
}
if (env('HTTP_X_HTTP_METHOD_OVERRIDE')) {
$this->params['form']['_method'] = env('HTTP_X_HTTP_METHOD_OVERRIDE');
$this->data['_method'] = env('HTTP_X_HTTP_METHOD_OVERRIDE');
}
if (isset($this->params['form']['_method'])) {
if (isset($this->data['_method'])) {
if (!empty($_SERVER)) {
$_SERVER['REQUEST_METHOD'] = $this->params['form']['_method'];
$_SERVER['REQUEST_METHOD'] = $this->data['_method'];
} else {
$_ENV['REQUEST_METHOD'] = $this->params['form']['_method'];
$_ENV['REQUEST_METHOD'] = $this->data['_method'];
}
unset($this->params['form']['_method']);
unset($this->data['_method']);
}
if (isset($this->params['form']['data'])) {
$this->data = $this->params['form']['data'];
unset($this->params['form']['data']);
if (isset($this->data['data'])) {
$data = $this->data['data'];
unset($this->data['data']);
$this->data = Set::merge($this->data, $data);
}
}

Expand Down Expand Up @@ -533,6 +544,14 @@ public static function header($name) {

/**
* Get the HTTP method used for this request.
* There are a few ways to specify a method.
*
* - If your client supports it you can use native HTTP methods.
* - You can set the HTTP-X-Method-Override header.
* - You can submit an input with the name `_method`
*
* Any of these 3 approaches can be used to set the HTTP method used
* by CakePHP internally, and will effect the result of this method.
*
* @return string The name of the HTTP method used.
*/
Expand Down Expand Up @@ -662,6 +681,51 @@ public function data($name) {
return Set::classicExtract($this->data, $name);
}

/**
* Read data from `php://stdin`. Useful when interacting with XML or JSON
* request body content.
*
* Getting input with a decoding function:
*
* `$this->request->input('json_decode');`
*
* Getting input using a decoding function, and additional params:
*
* `$this->request->input('Xml::build', array('return' => 'DOMDocument'));`
*
* Any additional parameters are applied to the callback in the order they are given.
*
* @param string $callback A decoding callback that will convert the string data to another
* representation. Leave empty to access the raw input data. You can also
* supply additional parameters for the decoding callback using var args, see above.
* @return The decoded/processed request data.
*/
public function input($callback = null) {
$input = $this->_readStdin();
$args = func_get_args();
if (!empty($args)) {
$callback = array_shift($args);
array_unshift($args, $input);
return call_user_func_array($callback, $args);
}
return $input;
}

/**
* Read data from php://stdin, mocked in tests.
*
* @return string contents of stdin
*/
protected function _readStdin() {
if (empty($this->__input)) {
$fh = fopen('php://input', 'r');
$content = stream_get_contents($fh);
fclose($fh);
$this->__input = $content;
}
return $this->__input;
}

/**
* Array access read implementation
*
Expand Down Expand Up @@ -711,4 +775,4 @@ public function offsetExists($name) {
public function offsetUnset($name) {
unset($this->params[$name]);
}
}
}
Expand Up @@ -292,6 +292,7 @@ function testAutoAjaxLayout() {
function testStartupCallback() {
$_SERVER['REQUEST_METHOD'] = 'PUT';
$_SERVER['CONTENT_TYPE'] = 'application/xml';
$this->Controller->request = $this->getMock('CakeRequest', array('_readStdin'));
$this->RequestHandler->startup($this->Controller);
$this->assertTrue(is_array($this->Controller->data));
$this->assertFalse(is_object($this->Controller->data));
Expand All @@ -305,11 +306,35 @@ function testStartupCallback() {
function testStartupCallbackCharset() {
$_SERVER['REQUEST_METHOD'] = 'PUT';
$_SERVER['CONTENT_TYPE'] = 'application/xml; charset=UTF-8';
$this->Controller->request = $this->getMock('CakeRequest', array('_readStdin'));
$this->RequestHandler->startup($this->Controller);
$this->assertTrue(is_array($this->Controller->data));
$this->assertFalse(is_object($this->Controller->data));
}

/**
* 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
*
Expand Down Expand Up @@ -774,4 +799,11 @@ function testBeforeRedirectCallingHeader() {
$result = ob_get_clean();
}

/**
* @expectedException CakeException
* @return void
*/
function testAddInputTypeException() {
$this->RequestHandler->addInputType('csv', array('I am not callable'));
}
}

0 comments on commit 093d7f1

Please sign in to comment.