request_handler.php

Go to the documentation of this file.
00001 <?php
00002 /* SVN FILE: $Id: request__handler_8php-source.html 580 2008-07-01 14:45:49Z gwoo $ */
00003 /**
00004  * Request object for handling alternative HTTP requests
00005  *
00006  * Alternative HTTP requests can come from wireless units like mobile phones, palmtop computers, and the like.
00007  * These units have no use for Ajax requests, and this Component can tell how Cake should respond to the different
00008  * needs of a handheld computer and a desktop machine.
00009  *
00010  * CakePHP(tm) :  Rapid Development Framework <http://www.cakephp.org/>
00011  * Copyright 2005-2008, Cake Software Foundation, Inc.
00012  *                              1785 E. Sahara Avenue, Suite 490-204
00013  *                              Las Vegas, Nevada 89104
00014  *
00015  * Licensed under The MIT License
00016  * Redistributions of files must retain the above copyright notice.
00017  *
00018  * @filesource
00019  * @copyright       Copyright 2005-2008, Cake Software Foundation, Inc.
00020  * @link                http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project
00021  * @package         cake
00022  * @subpackage      cake.cake.libs.controller.components
00023  * @since           CakePHP(tm) v 0.10.4.1076
00024  * @version         $Revision: 580 $
00025  * @modifiedby      $LastChangedBy: gwoo $
00026  * @lastmodified    $Date: 2008-07-01 09:45:49 -0500 (Tue, 01 Jul 2008) $
00027  * @license         http://www.opensource.org/licenses/mit-license.php The MIT License
00028  */
00029 
00030 if (!defined('REQUEST_MOBILE_UA')) {
00031     define('REQUEST_MOBILE_UA', '(iPhone|MIDP|AvantGo|BlackBerry|J2ME|Opera Mini|DoCoMo|NetFront|Nokia|PalmOS|PalmSource|portalmmm|Plucker|ReqwirelessWeb|SonyEricsson|Symbian|UP\.Browser|Windows CE|Xiino)');
00032 }
00033 
00034 /**
00035  * Request object for handling HTTP requests
00036  *
00037  * @package     cake
00038  * @subpackage  cake.cake.libs.controller.components
00039  *
00040  */
00041 class RequestHandlerComponent extends Object {
00042 /**
00043  * The layout that will be switched to for Ajax requests
00044  *
00045  * @var string
00046  * @access public
00047  * @see RequestHandler::setAjax()
00048  */
00049     var $ajaxLayout = 'ajax';
00050 /**
00051  * Determines whether or not callbacks will be fired on this component
00052  *
00053  * @var boolean
00054  * @access public
00055  */
00056     var $enabled = true;
00057 /**
00058  * Holds the content-type of the response that is set when using
00059  * RequestHandler::respondAs()
00060  *
00061  * @var string
00062  * @access private
00063  */
00064     var $__responseTypeSet = null;
00065 /**
00066  * Holds the copy of Controller::$params
00067  *
00068  * @var array
00069  * @access public
00070  */
00071     var $params = array();
00072 /**
00073  * Friendly content-type mappings used to set response types and determine
00074  * request types.  Can be modified with RequestHandler::setContent()
00075  *
00076  * @var array
00077  * @access private
00078  * @see RequestHandlerComponent::setContent
00079  */
00080     var $__requestContent = array(
00081         'javascript'    => 'text/javascript',
00082         'js'            => 'text/javascript',
00083         'json'          => 'application/json',
00084         'css'           => 'text/css',
00085         'html'          => array('text/html', '*/*'),
00086         'text'          => 'text/plain',
00087         'txt'           => 'text/plain',
00088         'csv'           => array('application/vnd.ms-excel', 'text/plain'),
00089         'form'          => 'application/x-www-form-urlencoded',
00090         'file'          => 'multipart/form-data',
00091         'xhtml'         => array('application/xhtml+xml', 'application/xhtml', 'text/xhtml'),
00092         'xhtml-mobile'  => 'application/vnd.wap.xhtml+xml',
00093         'xml'           => array('application/xml', 'text/xml'),
00094         'rss'           => 'application/rss+xml',
00095         'atom'          => 'application/atom+xml',
00096         'amf'           => 'application/x-amf',
00097         'wap'           => array('text/vnd.wap.wml', 'text/vnd.wap.wmlscript', 'image/vnd.wap.wbmp'),
00098         'wml'           => 'text/vnd.wap.wml',
00099         'wmlscript'     => 'text/vnd.wap.wmlscript',
00100         'wbmp'          => 'image/vnd.wap.wbmp',
00101         'pdf'           => 'application/pdf',
00102         'zip'           => 'application/x-zip',
00103         'tar'           => 'application/x-tar'
00104     );
00105 /**
00106  * Content-types accepted by the client.  If extension parsing is enabled in the
00107  * Router, and an extension is detected, the corresponding content-type will be
00108  * used as the overriding primary content-type accepted.
00109  *
00110  * @var array
00111  * @access private
00112  * @see Router::parseExtensions()
00113  */
00114     var $__acceptTypes = array();
00115 /**
00116  * The template to use when rendering the given content type.
00117  *
00118  * @var string
00119  * @access private
00120  */
00121     var $__renderType = null;
00122 /**
00123  * Contains the file extension parsed out by the Router
00124  *
00125  * @var string
00126  * @access public
00127  * @see Router::parseExtensions()
00128  */
00129     var $ext = null;
00130 /**
00131  * Flag set when MIME types have been initialized
00132  *
00133  * @var boolean
00134  * @access private
00135  * @see RequestHandler::__initializeTypes()
00136  */
00137     var $__typesInitialized = false;
00138 /**
00139  * Constructor. Parses the accepted content types accepted by the client using HTTP_ACCEPT
00140  *
00141  */
00142     function __construct() {
00143         $this->__acceptTypes = explode(',', env('HTTP_ACCEPT'));
00144 
00145         foreach ($this->__acceptTypes as $i => $type) {
00146             if (strpos($type, ';')) {
00147                 $type = explode(';', $type);
00148                 $this->__acceptTypes[$i] = $type[0];
00149             }
00150         }
00151         parent::__construct();
00152     }
00153 /**
00154  * Initializes the component, gets a reference to Controller::$parameters, and
00155  * checks to see if a file extension has been parsed by the Router.  If yes, the
00156  * corresponding content-type is pushed onto the list of accepted content-types
00157  * as the first item.
00158  *
00159  * @param object $controller A reference to the controller
00160  * @see Router::parseExtensions()
00161  * @access public
00162  */
00163     function initialize(&$controller) {
00164         if (isset($controller->params['url']['ext'])) {
00165             $this->ext = $controller->params['url']['ext'];
00166         }
00167     }
00168 /**
00169  * The startup method of the RequestHandler enables several automatic behaviors
00170  * related to the detection of certain properties of the HTTP request, including:
00171  *
00172  * - Disabling layout rendering for Ajax requests (based on the HTTP_X_REQUESTED_WITH header)
00173  * - If Router::parseExtensions() is enabled, the layout and template type are
00174  *   switched based on the parsed extension.  For example, if controller/action.xml
00175  *   is requested, the view path becomes <i>app/views/controller/xml/action.ctp</i>.
00176  * - If a helper with the same name as the extension exists, it is added to the controller.
00177  * - If the extension is of a type that RequestHandler understands, it will set that
00178  *   Content-type in the response header.
00179  * - If the XML data is POSTed, the data is parsed into an XML object, which is assigned
00180  *   to the $data property of the controller, which can then be saved to a model object.
00181  *
00182  * @param object $controller A reference to the controller
00183  * @access public
00184  */
00185     function startup(&$controller) {
00186         if (!$this->enabled) {
00187             return;
00188         }
00189         $this->__initializeTypes();
00190         $controller->params['isAjax'] = $this->isAjax();
00191 
00192         if (!empty($this->ext) && !in_array($this->ext, array('html', 'htm')) && in_array($this->ext, array_keys($this->__requestContent))) {
00193             $this->renderAs($controller, $this->ext);
00194         } elseif ($this->isAjax()) {
00195             $this->renderAs($controller, 'ajax');
00196         }
00197 
00198         if ($this->requestedWith('xml')) {
00199             if (!class_exists('XmlNode')) {
00200                 App::import('Core', 'Xml');
00201             }
00202             $xml = new Xml(trim(file_get_contents('php://input')));
00203             if (is_object($xml->child('data')) && count($xml->children) == 1) {
00204                 $controller->data = $xml->child('data');
00205             } else {
00206                 $controller->data = $xml;
00207             }
00208         }
00209     }
00210 /**
00211  * Handles (fakes) redirects for Ajax requests using requestAction()
00212  *
00213  * @param object $controller A reference to the controller
00214  * @param mixed $url A string or array containing the redirect location
00215  * @access public
00216  */
00217     function beforeRedirect(&$controller, $url) {
00218         if (!$this->isAjax()) {
00219             return;
00220         }
00221         foreach ($_POST as $key => $val) {
00222             unset($_POST[$key]);
00223         }
00224         echo $this->requestAction($url, array('return'));
00225         $this->_stop();
00226     }
00227 /**
00228  * Returns true if the current HTTP request is Ajax, false otherwise
00229  *
00230  * @return boolean True if call is Ajax
00231  * @access public
00232  */
00233     function isAjax() {
00234         return env('HTTP_X_REQUESTED_WITH') === "XMLHttpRequest";
00235     }
00236 /**
00237  * Returns true if the current HTTP request is coming from a Flash-based client
00238  *
00239  * @return boolean True if call is from Flash
00240  * @access public
00241  */
00242     function isFlash() {
00243         return env('HTTP_USER_AGENT') === "Shockwave Flash";
00244     }
00245 /**
00246  * Returns true if the current request is over HTTPS, false otherwise.
00247  *
00248  * @return bool True if call is over HTTPS
00249  * @access public
00250  */
00251     function isSSL() {
00252         return env('HTTPS');
00253     }
00254 /**
00255  * Returns true if the current call accepts an XML response, false otherwise
00256  *
00257  * @return boolean True if client accepts an XML response
00258  * @access public
00259  */
00260     function isXml() {
00261         return $this->prefers('xml');
00262     }
00263 /**
00264  * Returns true if the current call accepts an RSS response, false otherwise
00265  *
00266  * @return boolean True if client accepts an RSS response
00267  * @access public
00268  */
00269     function isRss() {
00270         return $this->prefers('rss');
00271     }
00272 /**
00273  * Returns true if the current call accepts an Atom response, false otherwise
00274  *
00275  * @return boolean True if client accepts an RSS response
00276  * @access public
00277  */
00278     function isAtom() {
00279         return $this->prefers('atom');
00280     }
00281 /**
00282  * Returns true if user agent string matches a mobile web browser, or if the
00283  * client accepts WAP content.
00284  *
00285  * @return boolean True if user agent is a mobile web browser
00286  * @access public
00287  */
00288     function isMobile() {
00289         preg_match('/' . REQUEST_MOBILE_UA . '/i', env('HTTP_USER_AGENT'), $match);
00290         if (!empty($match) || $this->accepts('wap')) {
00291             return true;
00292         }
00293         return false;
00294     }
00295 /**
00296  * Returns true if the client accepts WAP content
00297  *
00298  * @return bool
00299  * @access public
00300  */
00301     function isWap() {
00302         return $this->prefers('wap');
00303     }
00304 /**
00305  * Returns true if the current call a POST request
00306  *
00307  * @return boolean True if call is a POST
00308  * @access public
00309  */
00310     function isPost() {
00311         return (strtolower(env('REQUEST_METHOD')) == 'post');
00312     }
00313 /**
00314  * Returns true if the current call a PUT request
00315  *
00316  * @return boolean True if call is a PUT
00317  * @access public
00318  */
00319     function isPut() {
00320         return (strtolower(env('REQUEST_METHOD')) == 'put');
00321     }
00322 /**
00323  * Returns true if the current call a GET request
00324  *
00325  * @return boolean True if call is a GET
00326  * @access public
00327  */
00328     function isGet() {
00329         return (strtolower(env('REQUEST_METHOD')) == 'get');
00330     }
00331 /**
00332  * Returns true if the current call a DELETE request
00333  *
00334  * @return boolean True if call is a DELETE
00335  * @access public
00336  */
00337     function isDelete() {
00338         return (strtolower(env('REQUEST_METHOD')) == 'delete');
00339     }
00340 /**
00341  * Gets Prototype version if call is Ajax, otherwise empty string.
00342  * The Prototype library sets a special "Prototype version" HTTP header.
00343  *
00344  * @return string Prototype version of component making Ajax call
00345  * @access public
00346  */
00347     function getAjaxVersion() {
00348         if (env('HTTP_X_PROTOTYPE_VERSION') != null) {
00349             return env('HTTP_X_PROTOTYPE_VERSION');
00350         }
00351         return false;
00352     }
00353 /**
00354  * Adds/sets the Content-type(s) for the given name.  This method allows
00355  * content-types to be mapped to friendly aliases (or extensions), which allows
00356  * RequestHandler to automatically respond to requests of that type in the
00357  * startup method.
00358  *
00359  * @param string $name The name of the Content-type, i.e. "html", "xml", "css"
00360  * @param mixed $type The Content-type or array of Content-types assigned to the name,
00361  *                    i.e. "text/html", or "application/xml"
00362  * @access public
00363  */
00364     function setContent($name, $type = null) {
00365         if (is_array($name)) {
00366             $this->__requestContent = array_merge($this->__requestContent, $name);
00367             return;
00368         }
00369         $this->__requestContent[$name] = $type;
00370     }
00371 /**
00372  * Gets the server name from which this request was referred
00373  *
00374  * @return string Server address
00375  * @access public
00376  */
00377     function getReferrer() {
00378         if (env('HTTP_HOST') != null) {
00379             $sess_host = env('HTTP_HOST');
00380         }
00381 
00382         if (env('HTTP_X_FORWARDED_HOST') != null) {
00383             $sess_host = env('HTTP_X_FORWARDED_HOST');
00384         }
00385         return trim(preg_replace('/:.*/', '', $sess_host));
00386     }
00387 /**
00388  * Gets remote client IP
00389  *
00390  * @return string Client IP address
00391  * @access public
00392  */
00393     function getClientIP() {
00394         if (env('HTTP_X_FORWARDED_FOR') != null) {
00395             $ipaddr = preg_replace('/,.*/', '', env('HTTP_X_FORWARDED_FOR'));
00396         } else {
00397             if (env('HTTP_CLIENT_IP') != null) {
00398                 $ipaddr = env('HTTP_CLIENT_IP');
00399             } else {
00400                 $ipaddr = env('REMOTE_ADDR');
00401             }
00402         }
00403 
00404         if (env('HTTP_CLIENTADDRESS') != null) {
00405             $tmpipaddr = env('HTTP_CLIENTADDRESS');
00406 
00407             if (!empty($tmpipaddr)) {
00408                 $ipaddr = preg_replace('/,.*/', '', $tmpipaddr);
00409             }
00410         }
00411         return trim($ipaddr);
00412     }
00413 /**
00414  * Determines which content types the client accepts.  Acceptance is based on
00415  * the file extension parsed by the Router (if present), and by the HTTP_ACCEPT
00416  * header.
00417  *
00418  * @param mixed $type Can be null (or no parameter), a string type name, or an
00419  *                  array of types
00420  * @return mixed If null or no parameter is passed, returns an array of content
00421  *              types the client accepts.  If a string is passed, returns true
00422  *              if the client accepts it.  If an array is passed, returns true
00423  *              if the client accepts one or more elements in the array.
00424  * @access public
00425  * @see RequestHandlerComponent::setContent()
00426  */
00427     function accepts($type = null) {
00428         $this->__initializeTypes();
00429 
00430         if ($type == null) {
00431             return $this->mapType($this->__acceptTypes);
00432 
00433         } elseif (is_array($type)) {
00434             foreach ($type as $t) {
00435                 if ($this->accepts($t) == true) {
00436                     return true;
00437                 }
00438             }
00439             return false;
00440         } elseif (is_string($type)) {
00441 
00442             if (!in_array($type, array_keys($this->__requestContent))) {
00443                 return false;
00444             }
00445 
00446             $content = $this->__requestContent[$type];
00447 
00448             if (is_array($content)) {
00449                 foreach ($content as $c) {
00450                     if (in_array($c, $this->__acceptTypes)) {
00451                         return true;
00452                     }
00453                 }
00454             } else {
00455                 if (in_array($content, $this->__acceptTypes)) {
00456                     return true;
00457                 }
00458             }
00459         }
00460     }
00461 /**
00462  * Determines the content type of the data the client has sent (i.e. in a POST request)
00463  *
00464  * @param mixed $type Can be null (or no parameter), a string type name, or an array of types
00465  * @access public
00466  */
00467     function requestedWith($type = null) {
00468         if (!$this->isPost() && !$this->isPut()) {
00469             return null;
00470         }
00471 
00472         if ($type == null) {
00473             return $this->mapType(env('CONTENT_TYPE'));
00474         } elseif (is_array($type)) {
00475             foreach ($type as $t) {
00476                 if ($this->requestedWith($t)) {
00477                     return $this->mapType($t);
00478                 }
00479             }
00480             return false;
00481         } elseif (is_string($type)) {
00482             return ($type == $this->mapType(env('CONTENT_TYPE')));
00483         }
00484     }
00485 /**
00486  * Determines which content-types the client prefers.  If no parameters are given,
00487  * the content-type that the client most likely prefers is returned.  If $type is
00488  * an array, the first item in the array that the client accepts is returned.
00489  * Preference is determined primarily by the file extension parsed by the Router
00490  * if provided, and secondarily by the list of content-types provided in
00491  * HTTP_ACCEPT.
00492  *
00493  * @param mixed $type An optional array of 'friendly' content-type names, i.e.
00494  *                     'html', 'xml', 'js', etc.
00495  * @return mixed If $type is null or not provided, the first content-type in the
00496  *                list, based on preference, is returned.
00497  * @access public
00498  * @see RequestHandlerComponent::setContent()
00499  */
00500     function prefers($type = null) {
00501         $this->__initializeTypes();
00502         if ($type == null) {
00503             if (empty($this->ext)) {
00504                 $accept = $this->accepts(null);
00505                 if (is_array($accept)) {
00506                     return $accept[0];
00507                 }
00508                 return $accept;
00509             } else {
00510                 return $this->ext;
00511             }
00512         }
00513         App::import('Core', 'Set');
00514         $types = Set::normalize($type, false);
00515         $accepts = array();
00516 
00517         foreach ($types as $type) {
00518             if ($this->accepts($type)) {
00519                 $accepts[] = $type;
00520             }
00521         }
00522 
00523         if (count($accepts) == 0) {
00524             return false;
00525         } elseif (count($accepts) == 1) {
00526             return $accepts[0];
00527         } else {
00528             $accepts = array_intersect($this->__acceptTypes, $accepts);
00529             return $accepts[0];
00530         }
00531     }
00532 /**
00533  * Sets the layout and template paths for the content type defined by $type.
00534  *
00535  * @param object $controller A reference to a controller object
00536  * @param string $type Type of response to send (e.g: 'ajax')
00537  * @access public
00538  * @see RequestHandlerComponent::setContent()
00539  * @see RequestHandlerComponent::respondAs()
00540  */
00541     function renderAs(&$controller, $type) {
00542         $this->__initializeTypes();
00543         $options = array('charset' => 'UTF-8');
00544 
00545         if (Configure::read('App.encoding') !== null) {
00546             $options = array('charset' => Configure::read('App.encoding'));
00547         }
00548 
00549         if ($type == 'ajax') {
00550             $controller->layout = $this->ajaxLayout;
00551             return $this->respondAs('html', $options);
00552         }
00553         $controller->ext = '.ctp';
00554 
00555         if (empty($this->__renderType)) {
00556             $controller->viewPath .= '/' . $type;
00557         } else {
00558             $controller->viewPath = preg_replace("/\/{$type}$/", '/' . $type, $controller->viewPath);
00559         }
00560         $this->__renderType = $type;
00561         $controller->layoutPath = $type;
00562 
00563         if (in_array($type, array_keys($this->__requestContent))) {
00564             $this->respondAs($type, $options);
00565         }
00566 
00567         $helper = ucfirst($type);
00568         if (!in_array($helper, $controller->helpers) && !array_key_exists($helper, $controller->helpers)) {
00569             if (App::import('Helper', $helper)) {
00570                 $controller->helpers[] = $helper;
00571             }
00572         }
00573     }
00574 /**
00575  * Sets the response header based on type map index name.  If DEBUG is greater
00576  * than 2, the header is not set.
00577  *
00578  * @param mixed $type Friendly type name, i.e. 'html' or 'xml', or a full
00579  *                    content-type, like 'application/x-shockwave'.
00580  * @param array $options If $type is a friendly type name that is associated with
00581  *                     more than one type of content, $index is used to select
00582  *                     which content-type to use.
00583  * @return boolean Returns false if the friendly type name given in $type does
00584  *                 not exist in the type map, or if the Content-type header has
00585  *                 already been set by this method.
00586  * @access public
00587  * @see RequestHandlerComponent::setContent()
00588  */
00589     function respondAs($type, $options = array()) {
00590         $this->__initializeTypes();
00591         if ($this->__responseTypeSet != null) {
00592             return false;
00593         }
00594         if (!array_key_exists($type, $this->__requestContent) && strpos($type, '/') === false) {
00595             return false;
00596         }
00597         $options = array_merge(array('index' => 0, 'charset' => null, 'attachment' => false), $options);
00598 
00599         if (strpos($type, '/') === false && isset($this->__requestContent[$type])) {
00600             $cType = null;
00601             if (is_array($this->__requestContent[$type]) && isset($this->__requestContent[$type][$options['index']])) {
00602                 $cType = $this->__requestContent[$type][$options['index']];
00603             } elseif (is_array($this->__requestContent[$type]) && isset($this->__requestContent[$type][0])) {
00604                 $cType = $this->__requestContent[$type][0];
00605             } elseif (isset($this->__requestContent[$type])) {
00606                 $cType = $this->__requestContent[$type];
00607             } else {
00608                 return false;
00609             }
00610             if (is_array($cType)) {
00611                 if ($this->prefers($cType)) {
00612                     $cType = $this->prefers($cType);
00613                 } else {
00614                     $cType = $cType[0];
00615                 }
00616             }
00617         } else {
00618             $cType = $type;
00619         }
00620 
00621         if ($cType != null) {
00622             $header = 'Content-type: ' . $cType;
00623 
00624             if (!empty($options['charset'])) {
00625                 $header .= '; charset=' . $options['charset'];
00626             }
00627             if (!empty($options['attachment'])) {
00628                 header('Content-Disposition: attachment; filename="' . $options['attachment'] . '"');
00629             }
00630             if (Configure::read() < 2 && !defined('CAKEPHP_SHELL')) {
00631                 @header($header);
00632             }
00633             $this->__responseTypeSet = $cType;
00634             return true;
00635         } else {
00636             return false;
00637         }
00638     }
00639 /**
00640  * Returns the current response type (Content-type header), or null if none has been set
00641  *
00642  * @return mixed A string content type alias, or raw content type if no alias map exists,
00643  *               otherwise null
00644  * @access public
00645  */
00646     function responseType() {
00647         if ($this->__responseTypeSet == null) {
00648             return null;
00649         }
00650         return $this->mapType($this->__responseTypeSet);
00651     }
00652 /**
00653  * Maps a content-type back to an alias
00654  *
00655  * @param mixed $type Content type
00656  * @return mixed Alias
00657  * @access public
00658  */
00659     function mapType($ctype) {
00660         if (is_array($ctype)) {
00661             $out = array();
00662             foreach ($ctype as $t) {
00663                 $out[] = $this->mapType($t);
00664             }
00665             return $out;
00666         } else {
00667             $keys = array_keys($this->__requestContent);
00668             $count = count($keys);
00669 
00670             for ($i = 0; $i < $count; $i++) {
00671                 $name = $keys[$i];
00672                 $type = $this->__requestContent[$name];
00673 
00674                 if (is_array($type) && in_array($ctype, $type)) {
00675                     return $name;
00676                 } elseif (!is_array($type) && $type == $ctype) {
00677                     return $name;
00678                 }
00679             }
00680             return $ctype;
00681         }
00682     }
00683 /**
00684  * Initializes MIME types
00685  *
00686  * @return void
00687  * @access private
00688  */
00689     function __initializeTypes() {
00690         if ($this->__typesInitialized) {
00691             return;
00692         }
00693         if (isset($this->__requestContent[$this->ext])) {
00694             $content = $this->__requestContent[$this->ext];
00695             if (is_array($content)) {
00696                 $content = $content[0];
00697             }
00698             array_unshift($this->__acceptTypes, $content);
00699         }
00700         $this->__typesInitialized = true;
00701     }
00702 }
00703 
00704 ?>