request_handler.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. <?php
  2. /* SVN FILE: $Id$ */
  3. /**
  4. * Request object for handling alternative HTTP requests
  5. *
  6. * Alternative HTTP requests can come from wireless units like mobile phones, palmtop computers, and the like.
  7. * These units have no use for Ajax requests, and this Component can tell how Cake should respond to the different
  8. * needs of a handheld computer and a desktop machine.
  9. *
  10. * CakePHP(tm) : Rapid Development Framework <http://www.cakephp.org/>
  11. * Copyright 2005-2007, Cake Software Foundation, Inc.
  12. * 1785 E. Sahara Avenue, Suite 490-204
  13. * Las Vegas, Nevada 89104
  14. *
  15. * Licensed under The MIT License
  16. * Redistributions of files must retain the above copyright notice.
  17. *
  18. * @filesource
  19. * @copyright Copyright 2005-2007, Cake Software Foundation, Inc.
  20. * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project
  21. * @package cake
  22. * @subpackage cake.cake.libs.controller.components
  23. * @since CakePHP(tm) v 0.10.4.1076
  24. * @version $Revision$
  25. * @modifiedby $LastChangedBy$
  26. * @lastmodified $Date$
  27. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  28. */
  29. if (!defined('REQUEST_MOBILE_UA')) {
  30. define('REQUEST_MOBILE_UA', '(MIDP|AvantGo|BlackBerry|J2ME|Opera Mini|DoCoMo|NetFront|Nokia|PalmOS|PalmSource|portalmmm|Plucker|ReqwirelessWeb|SonyEricsson|Symbian|UP\.Browser|Windows CE|Xiino)');
  31. }
  32. /**
  33. * Request object for handling HTTP requests
  34. *
  35. * @package cake
  36. * @subpackage cake.cake.libs.controller.components
  37. *
  38. */
  39. class RequestHandlerComponent extends Object {
  40. /**
  41. * The layout that will be switched to for Ajax requests
  42. *
  43. * @var string
  44. * @access public
  45. * @see RequestHandler::setAjax()
  46. */
  47. var $ajaxLayout = 'ajax';
  48. /**
  49. * Determines whether or not callbacks will be fired on this component
  50. *
  51. * @var boolean
  52. * @access public
  53. */
  54. var $enabled = true;
  55. /**
  56. * Holds the content-type of the response that is set when using
  57. * RequestHandler::respondAs()
  58. *
  59. * @var string
  60. * @access private
  61. */
  62. var $__responseTypeSet = null;
  63. /**
  64. * Holds the copy of Controller::$params
  65. *
  66. * @var array
  67. * @access public
  68. */
  69. var $params = array();
  70. /**
  71. * Friendly content-type mappings used to set response types and determine
  72. * request types. Can be modified with RequestHandler::setContent()
  73. *
  74. * @var array
  75. * @access private
  76. * @see RequestHandlerComponent::setContent
  77. */
  78. var $__requestContent = array(
  79. 'javascript' => 'text/javascript',
  80. 'js' => 'text/javascript',
  81. 'css' => 'text/css',
  82. 'html' => array('text/html', '*/*'),
  83. 'text' => 'text/plain',
  84. 'txt' => 'text/plain',
  85. 'form' => 'application/x-www-form-urlencoded',
  86. 'file' => 'multipart/form-data',
  87. 'xhtml' => array('application/xhtml+xml', 'application/xhtml', 'text/xhtml'),
  88. 'xhtml-mobile' => 'application/vnd.wap.xhtml+xml',
  89. 'xml' => array('application/xml', 'text/xml'),
  90. 'rss' => 'application/rss+xml',
  91. 'atom' => 'application/atom+xml',
  92. 'amf' => 'application/x-amf',
  93. 'wap' => array('text/vnd.wap.wml', 'text/vnd.wap.wmlscript', 'image/vnd.wap.wbmp'),
  94. 'wml' => 'text/vnd.wap.wml',
  95. 'wmlscript' => 'text/vnd.wap.wmlscript',
  96. 'wbmp' => 'image/vnd.wap.wbmp',
  97. 'pdf' => 'application/pdf',
  98. 'zip' => 'application/x-zip',
  99. 'tar' => 'application/x-tar'
  100. );
  101. /**
  102. * Content-types accepted by the client. If extension parsing is enabled in the
  103. * Router, and an extension is detected, the corresponding content-type will be
  104. * used as the overriding primary content-type accepted.
  105. *
  106. * @var array
  107. * @access private
  108. * @see Router::parseExtensions()
  109. */
  110. var $__acceptTypes = array();
  111. /**
  112. * The template to use when rendering the given content type.
  113. *
  114. * @var string
  115. * @access private
  116. */
  117. var $__renderType = null;
  118. /**
  119. * Contains the file extension parsed out by the Router
  120. *
  121. * @var string
  122. * @access public
  123. * @see Router::parseExtensions()
  124. */
  125. var $ext = null;
  126. /**
  127. * Constructor. Parses the accepted content types accepted by the client using
  128. * HTTP_ACCEPT
  129. *
  130. * @access public
  131. * @return void
  132. */
  133. function __construct() {
  134. $this->__acceptTypes = explode(',', env('HTTP_ACCEPT'));
  135. foreach($this->__acceptTypes as $i => $type) {
  136. if (strpos($type, ';')) {
  137. $type = explode(';', $type);
  138. $this->__acceptTypes[$i] = $type[0];
  139. }
  140. }
  141. parent::__construct();
  142. }
  143. /**
  144. * Initializes the component, gets a reference to Controller::$parameters, and
  145. * checks to see if a file extension has been parsed by the Router. If yes, the
  146. * corresponding content-type is pushed onto the list of accepted content-types
  147. * as the first item.
  148. *
  149. * @param object A reference to the controller
  150. * @return void
  151. * @see Router::parseExtensions()
  152. */
  153. function initialize(&$controller) {
  154. if (isset($controller->params['url']['ext'])) {
  155. $this->ext = $controller->params['url']['ext'];
  156. if (isset($this->__requestContent[$this->ext])) {
  157. $content = $this->__requestContent[$this->ext];
  158. if (is_array($content)) {
  159. $content = $content[0];
  160. }
  161. array_unshift($this->__acceptTypes, $content);
  162. }
  163. }
  164. }
  165. /**
  166. * The startup method of the RequestHandler enables several automatic behaviors
  167. * related to the detection of certain properties of the HTTP request, including:
  168. *
  169. * - Disabling layout rendering for Ajax requests (based on the HTTP_X_REQUESTED_WITH header)
  170. * - If Router::parseExtensions() is enabled, the layout and template type are
  171. * switched based on the parsed extension. For example, if controller/action.xml
  172. * is requested, the view path becomes <i>app/views/controller/xml/action.ctp</i>.
  173. * - If a helper with the same name as the extension exists, it is added to the controller.
  174. * - If the extension is of a type that RequestHandler understands, it will set that
  175. * Content-type in the response header.
  176. * - If the XML data is POSTed, the data is parsed into an XML object, which is assigned
  177. * to the $data property of the controller, which can then be saved to a model object.
  178. *
  179. * @param object A reference to the controller
  180. * @return void
  181. */
  182. function startup(&$controller) {
  183. if ($this->disableStartup || !$this->enabled) {
  184. return;
  185. }
  186. $controller->params['isAjax'] = $this->isAjax();
  187. if (!empty($this->ext) && !in_array($this->ext, array('html', 'htm')) && in_array($this->ext, array_keys($this->__requestContent))) {
  188. $this->renderAs($controller, $this->ext);
  189. } elseif ($this->isAjax()) {
  190. $this->renderAs($controller, 'ajax');
  191. }
  192. if ($this->requestedWith('xml')) {
  193. if (!class_exists('xmlnode') && !class_exists('XMLNode')) {
  194. uses('xml');
  195. }
  196. $xml = new XML(trim(file_get_contents('php://input')));
  197. if (is_object($xml->child('data')) && count($xml->children) == 1) {
  198. $controller->data = $xml->child('data');
  199. } else {
  200. $controller->data = $xml;
  201. }
  202. }
  203. }
  204. /**
  205. * Returns true if the current HTTP request is Ajax, false otherwise
  206. *
  207. * @return bool True if call is Ajax
  208. */
  209. function isAjax() {
  210. return env('HTTP_X_REQUESTED_WITH') === "XMLHttpRequest";
  211. }
  212. /**
  213. * Returns true if the current request is over HTTPS, false otherwise.
  214. *
  215. * @return bool
  216. */
  217. function isSSL() {
  218. return env('HTTPS');
  219. }
  220. /**
  221. * Returns true if the current call accepts an XML response, false otherwise
  222. *
  223. * @return bool True if client accepts an XML response
  224. */
  225. function isXml() {
  226. return $this->prefers('xml');
  227. }
  228. /**
  229. * Returns true if the current call accepts an RSS response, false otherwise
  230. *
  231. * @return bool True if client accepts an RSS response
  232. */
  233. function isRss() {
  234. return $this->prefers('rss');
  235. }
  236. /**
  237. * Returns true if the current call accepts an Atom response, false otherwise
  238. *
  239. * @return bool True if client accepts an RSS response
  240. */
  241. function isAtom() {
  242. return $this->prefers('atom');
  243. }
  244. /**
  245. * Returns true if user agent string matches a mobile web browser, or if the
  246. * client accepts WAP content.
  247. *
  248. * @return bool True if user agent is a mobile web browser
  249. */
  250. function isMobile() {
  251. return (preg_match('/' . REQUEST_MOBILE_UA . '/i', env('HTTP_USER_AGENT')) > 0 || $this->accepts('wap'));
  252. }
  253. /**
  254. * Returns true if the client accepts WAP content
  255. *
  256. * @return bool
  257. */
  258. function isWap() {
  259. return $this->prefers('wap');
  260. }
  261. /**
  262. * Returns true if the current call a POST request
  263. *
  264. * @return bool True if call is a POST
  265. */
  266. function isPost() {
  267. return (strtolower(env('REQUEST_METHOD')) == 'post');
  268. }
  269. /**
  270. * Returns true if the current call a PUT request
  271. *
  272. * @return bool True if call is a PUT
  273. */
  274. function isPut() {
  275. return (strtolower(env('REQUEST_METHOD')) == 'put');
  276. }
  277. /**
  278. * Returns true if the current call a GET request
  279. *
  280. * @return bool True if call is a GET
  281. */
  282. function isGet() {
  283. return (strtolower(env('REQUEST_METHOD')) == 'get');
  284. }
  285. /**
  286. * Returns true if the current call a DELETE request
  287. *
  288. * @return bool True if call is a DELETE
  289. */
  290. function isDelete() {
  291. return (strtolower(env('REQUEST_METHOD')) == 'delete');
  292. }
  293. /**
  294. * Gets Prototype version if call is Ajax, otherwise empty string.
  295. * The Prototype library sets a special "Prototype version" HTTP header.
  296. *
  297. * @return string Prototype version of component making Ajax call
  298. */
  299. function getAjaxVersion() {
  300. if (env('HTTP_X_PROTOTYPE_VERSION') != null) {
  301. return env('HTTP_X_PROTOTYPE_VERSION');
  302. }
  303. return false;
  304. }
  305. /**
  306. * Adds/sets the Content-type(s) for the given name. This method allows
  307. * content-types to be mapped to friendly aliases (or extensions), which allows
  308. * RequestHandler to automatically respond to requests of that type in the
  309. * startup method.
  310. *
  311. * @param string $name The name of the Content-type, i.e. "html", "xml", "css"
  312. * @param mixed $type The Content-type or array of Content-types assigned to the name,
  313. * i.e. "text/html", or "application/xml"
  314. * @return void
  315. * @access public
  316. */
  317. function setContent($name, $type = null) {
  318. if (is_array($name)) {
  319. $this->__requestContent = am($this->__requestContent, $name);
  320. return;
  321. }
  322. $this->__requestContent[$name] = $type;
  323. }
  324. /**
  325. * Gets the server name from which this request was referred
  326. *
  327. * @return string Server address
  328. * @access public
  329. */
  330. function getReferrer() {
  331. if (env('HTTP_HOST') != null) {
  332. $sess_host = env('HTTP_HOST');
  333. }
  334. if (env('HTTP_X_FORWARDED_HOST') != null) {
  335. $sess_host = env('HTTP_X_FORWARDED_HOST');
  336. }
  337. return trim(preg_replace('/:.*/', '', $sess_host));
  338. }
  339. /**
  340. * Gets remote client IP
  341. *
  342. * @return string Client IP address
  343. * @access public
  344. */
  345. function getClientIP() {
  346. if (env('HTTP_X_FORWARDED_FOR') != null) {
  347. $ipaddr = preg_replace('/,.*/', '', env('HTTP_X_FORWARDED_FOR'));
  348. } else {
  349. if (env('HTTP_CLIENT_IP') != null) {
  350. $ipaddr = env('HTTP_CLIENT_IP');
  351. } else {
  352. $ipaddr = env('REMOTE_ADDR');
  353. }
  354. }
  355. if (env('HTTP_CLIENTADDRESS') != null) {
  356. $tmpipaddr = env('HTTP_CLIENTADDRESS');
  357. if (!empty($tmpipaddr)) {
  358. $ipaddr = preg_replace('/,.*/', '', $tmpipaddr);
  359. }
  360. }
  361. return trim($ipaddr);
  362. }
  363. /**
  364. * Determines which content types the client accepts. Acceptance is based on
  365. * the file extension parsed by the Router (if present), and by the HTTP_ACCEPT
  366. * header.
  367. *
  368. * @param mixed $type Can be null (or no parameter), a string type name, or an
  369. * array of types
  370. * @returns mixed If null or no parameter is passed, returns an array of content
  371. * types the client accepts. If a string is passed, returns true
  372. * if the client accepts it. If an array is passed, returns true
  373. * if the client accepts one or more elements in the array.
  374. * @access public
  375. * @see RequestHandlerComponent::setContent()
  376. */
  377. function accepts($type = null) {
  378. if ($type == null) {
  379. return $this->mapType($this->__acceptTypes);
  380. } else if(is_array($type)) {
  381. foreach($type as $t) {
  382. if ($this->accepts($t) == true) {
  383. return true;
  384. }
  385. }
  386. return false;
  387. } else if(is_string($type)) {
  388. if (!in_array($type, array_keys($this->__requestContent))) {
  389. return false;
  390. }
  391. $content = $this->__requestContent[$type];
  392. if (is_array($content)) {
  393. foreach($content as $c) {
  394. if (in_array($c, $this->__acceptTypes)) {
  395. return true;
  396. }
  397. }
  398. } else {
  399. if (in_array($content, $this->__acceptTypes)) {
  400. return true;
  401. }
  402. }
  403. }
  404. }
  405. /**
  406. * Determines the content type of the data the client has sent (i.e. in a POST request)
  407. *
  408. * @param mixed $type Can be null (or no parameter), a string type name, or an
  409. * array of types
  410. * @access public
  411. */
  412. function requestedWith($type = null) {
  413. if (!$this->isPost() && !$this->isPut()) {
  414. return null;
  415. }
  416. if ($type == null) {
  417. return $this->mapType(env('CONTENT_TYPE'));
  418. } else if(is_array($type)) {
  419. foreach($type as $t) {
  420. if ($this->requestedWith($t)) {
  421. return $this->mapType($t);
  422. }
  423. }
  424. return false;
  425. } else if(is_string($type)) {
  426. return ($type == $this->mapType(env('CONTENT_TYPE')));
  427. }
  428. }
  429. /**
  430. * Determines which content-types the client prefers. If no parameters are given,
  431. * the content-type that the client most likely prefers is returned. If $type is
  432. * an array, the first item in the array that the client accepts is returned.
  433. * Preference is determined primarily by the file extension parsed by the Router
  434. * if provided, and secondarily by the list of content-types provided in
  435. * HTTP_ACCEPT.
  436. *
  437. * @param mixed $type An optional array of 'friendly' content-type names, i.e.
  438. * 'html', 'xml', 'js', etc.
  439. * @returns mixed If $type is null or not provided, the first content-type in the
  440. * list, based on preference, is returned.
  441. * @access public
  442. * @see RequestHandlerComponent::setContent()
  443. */
  444. function prefers($type = null) {
  445. if ($type == null) {
  446. if (!empty($this->ext)) {
  447. $accept = $this->accepts(null);
  448. if (is_array($accept)) {
  449. return $accept[0];
  450. }
  451. return $accept;
  452. } else {
  453. return $this->ext;
  454. }
  455. }
  456. uses('set');
  457. $types = Set::normalize($type, false);
  458. $accepts = array();
  459. foreach ($types as $type) {
  460. if ($this->accepts($type)) {
  461. $accepts[] = $type;
  462. }
  463. }
  464. if (count($accepts) == 0) {
  465. return false;
  466. } elseif (count($accepts) == 1) {
  467. return $accepts[0];
  468. } else {
  469. $accepts = array_intersect($this->__acceptTypes, $accepts);
  470. return $accepts[0];
  471. }
  472. }
  473. /**
  474. * Sets the layout and template paths for the content type defined by $type.
  475. *
  476. * @param object $controller A reference to a controller object
  477. * @param string $type
  478. * @return void
  479. * @access public
  480. * @see RequestHandlerComponent::setContent()
  481. * @see RequestHandlerComponent::respondAs()
  482. */
  483. function renderAs(&$controller, $type) {
  484. if ($type == 'ajax') {
  485. $controller->layout = $this->ajaxLayout;
  486. return $this->respondAs('html', array('charset' => 'UTF-8'));
  487. }
  488. $controller->ext = '.ctp';
  489. if (empty($this->__renderType)) {
  490. $controller->viewPath .= '/' . $type;
  491. } else {
  492. $controller->viewPath = preg_replace("/\/{$type}$/", '/' . $type, $controller->viewPath);
  493. }
  494. $this->__renderType = $type;
  495. $controller->layoutPath = $type;
  496. if (in_array($type, array_keys($this->__requestContent))) {
  497. $this->respondAs($type);
  498. }
  499. if (!in_array(ucfirst($type), $controller->helpers)) {
  500. if (file_exists(HELPERS . $type . '.php') || fileExistsInPath(LIBS . 'view' . DS . 'helpers' . DS . $type . '.php')) {
  501. $controller->helpers[] = ucfirst($type);
  502. }
  503. }
  504. }
  505. /**
  506. * Sets the response header based on type map index name. If DEBUG is greater
  507. * than 2, the header is not set.
  508. *
  509. * @param mixed $type Friendly type name, i.e. 'html' or 'xml', or a full
  510. * content-type, like 'application/x-shockwave'.
  511. * @param array $options If $type is a friendly type name that is associated with
  512. * more than one type of content, $index is used to select
  513. * which content-type to use.
  514. * @return boolean Returns false if the friendly type name given in $type does
  515. * not exist in the type map, or if the Content-type header has
  516. * already been set by this method.
  517. * @access public
  518. * @see RequestHandlerComponent::setContent()
  519. */
  520. function respondAs($type, $options = array()) {
  521. if ($this->__responseTypeSet != null) {
  522. return false;
  523. }
  524. if (!array_key_exists($type, $this->__requestContent) && strpos($type, '/') === false) {
  525. return false;
  526. }
  527. $options = am(array('index' => 0, 'charset' => null, 'attachment' => false), $options);
  528. if (strpos($type, '/') === false && isset($this->__requestContent[$type])) {
  529. $cType = null;
  530. if (is_array($this->__requestContent[$type]) && isset($this->__requestContent[$type][$options['index']])) {
  531. $cType = $this->__requestContent[$type][$options['index']];
  532. } elseif (is_array($this->__requestContent[$type]) && isset($this->__requestContent[$type][0])) {
  533. $cType = $this->__requestContent[$type][0];
  534. } elseif (isset($this->__requestContent[$type])) {
  535. $cType = $this->__requestContent[$type];
  536. } else {
  537. return false;
  538. }
  539. if (is_array($cType)) {
  540. if ($this->prefers($cType)) {
  541. $cType = $this->prefers($cType);
  542. } else {
  543. $cType = $cType[0];
  544. }
  545. }
  546. } else {
  547. $cType = $type;
  548. }
  549. if ($cType != null) {
  550. $header = 'Content-type: ' . $cType;
  551. $headers = array();
  552. if (!empty($options['charset'])) {
  553. $header .= '; charset=' . $options['charset'];
  554. }
  555. if (!empty($options['attachment'])) {
  556. header('Content-Disposition: attachment; filename="' . $options['attachment'] . '"');
  557. }
  558. if (Configure::read() < 2) {
  559. header($header);
  560. foreach ($headers as $h) {
  561. header($h);
  562. }
  563. }
  564. $this->__responseTypeSet = $cType;
  565. return true;
  566. } else {
  567. return false;
  568. }
  569. }
  570. /**
  571. * Returns the current response type (Content-type header), or null if none has been set
  572. *
  573. * @return mixed A string content type alias, or raw content type if no alias map exists,
  574. * otherwise null
  575. * @access public
  576. */
  577. function responseType() {
  578. if ($this->__responseTypeSet == null) {
  579. return null;
  580. }
  581. return $this->mapType($this->__responseTypeSet);
  582. }
  583. /**
  584. * Maps a content-type back to an alias
  585. *
  586. * @param mixed $type
  587. * @return mixed
  588. * @access public
  589. */
  590. function mapType($ctype) {
  591. if (is_array($ctype)) {
  592. $out = array();
  593. foreach ($ctype as $t) {
  594. $out[] = $this->mapType($t);
  595. }
  596. return $out;
  597. } else {
  598. $keys = array_keys($this->__requestContent);
  599. $count = count($keys);
  600. for ($i = 0; $i < $count; $i++) {
  601. $name = $keys[$i];
  602. $type = $this->__requestContent[$name];
  603. if (is_array($type) && in_array($ctype, $type)) {
  604. return $name;
  605. } elseif (!is_array($type) && $type == $ctype) {
  606. return $name;
  607. }
  608. }
  609. return $ctype;
  610. }
  611. }
  612. }
  613. ?>