RequestHandlerComponent.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. <?php
  2. /**
  3. * Request object for handling alternative HTTP requests
  4. *
  5. * Alternative HTTP requests can come from wireless units like mobile phones, palmtop computers,
  6. * and the like. These units have no use for Ajax requests, and this Component can tell how Cake
  7. * should respond to the different needs of a handheld computer and a desktop machine.
  8. *
  9. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  10. * Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. *
  12. * Licensed under The MIT License
  13. * Redistributions of files must retain the above copyright notice.
  14. *
  15. * @copyright Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
  16. * @link http://cakephp.org CakePHP(tm) Project
  17. * @package cake
  18. * @subpackage cake.cake.libs.controller.components
  19. * @since CakePHP(tm) v 0.10.4.1076
  20. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  21. */
  22. App::uses('Xml', 'Core');
  23. /**
  24. * Request object for handling HTTP requests
  25. *
  26. * @package cake
  27. * @subpackage cake.cake.libs.controller.components
  28. * @link http://book.cakephp.org/view/1291/Request-Handling
  29. *
  30. */
  31. class RequestHandlerComponent extends Component {
  32. /**
  33. * The layout that will be switched to for Ajax requests
  34. *
  35. * @var string
  36. * @access public
  37. * @see RequestHandler::setAjax()
  38. */
  39. public $ajaxLayout = 'ajax';
  40. /**
  41. * Determines whether or not callbacks will be fired on this component
  42. *
  43. * @var boolean
  44. * @access public
  45. */
  46. public $enabled = true;
  47. /**
  48. * Holds the reference to Controller::$request
  49. *
  50. * @var CakeRequest
  51. * @access public
  52. */
  53. public $request;
  54. /**
  55. * Holds the reference to Controller::$response
  56. *
  57. * @var CakeResponse
  58. * @access public
  59. */
  60. public $response;
  61. /**
  62. * The template to use when rendering the given content type.
  63. *
  64. * @var string
  65. * @access private
  66. */
  67. private $__renderType = null;
  68. /**
  69. * Contains the file extension parsed out by the Router
  70. *
  71. * @var string
  72. * @access public
  73. * @see Router::parseExtensions()
  74. */
  75. public $ext = null;
  76. /**
  77. * Flag set when MIME types have been initialized
  78. *
  79. * @var boolean
  80. * @access private
  81. * @see RequestHandler::__initializeTypes()
  82. */
  83. private $__typesInitialized = false;
  84. /**
  85. * Constructor. Parses the accepted content types accepted by the client using HTTP_ACCEPT
  86. *
  87. * @param ComponentCollection $collection ComponentCollection object.
  88. * @param array $settings Array of settings.
  89. */
  90. function __construct(ComponentCollection $collection, $settings = array()) {
  91. $this->__acceptTypes = explode(',', env('HTTP_ACCEPT'));
  92. foreach ($this->__acceptTypes as $i => $type) {
  93. if (strpos($type, ';')) {
  94. $type = explode(';', $type);
  95. $this->__acceptTypes[$i] = $type[0];
  96. }
  97. }
  98. parent::__construct($collection, $settings);
  99. }
  100. /**
  101. * Initializes the component, gets a reference to Controller::$parameters, and
  102. * checks to see if a file extension has been parsed by the Router. Or if the
  103. * HTTP_ACCEPT_TYPE is set to a single value that is a supported extension and mapped type.
  104. * If yes, RequestHandler::$ext is set to that value
  105. *
  106. * @param object $controller A reference to the controller
  107. * @param array $settings Array of settings to _set().
  108. * @return void
  109. * @see Router::parseExtensions()
  110. */
  111. public function initialize(&$controller, $settings = array()) {
  112. $this->request = $controller->request;
  113. $this->response = $controller->response;
  114. if (isset($this->request->params['url']['ext'])) {
  115. $this->ext = $this->request->params['url']['ext'];
  116. }
  117. if (empty($this->ext)) {
  118. $accepts = $this->request->accepts();
  119. $extensions = Router::extensions();
  120. if (count($accepts) == 1) {
  121. $mapped = $this->mapType($accepts[0]);
  122. if (in_array($mapped, $extensions)) {
  123. $this->ext = $mapped;
  124. }
  125. }
  126. }
  127. $this->_set($settings);
  128. }
  129. /**
  130. * The startup method of the RequestHandler enables several automatic behaviors
  131. * related to the detection of certain properties of the HTTP request, including:
  132. *
  133. * - Disabling layout rendering for Ajax requests (based on the HTTP_X_REQUESTED_WITH header)
  134. * - If Router::parseExtensions() is enabled, the layout and template type are
  135. * switched based on the parsed extension or Accept-Type header. For example, if `controller/action.xml`
  136. * is requested, the view path becomes `app/views/controller/xml/action.ctp`. Also if
  137. * `controller/action` is requested with `Accept-Type: application/xml` in the headers
  138. * the view path will become `app/views/controller/xml/action.ctp`.
  139. * - If a helper with the same name as the extension exists, it is added to the controller.
  140. * - If the extension is of a type that RequestHandler understands, it will set that
  141. * Content-type in the response header.
  142. * - If the XML data is POSTed, the data is parsed into an XML object, which is assigned
  143. * to the $data property of the controller, which can then be saved to a model object.
  144. *
  145. * @param object $controller A reference to the controller
  146. * @return void
  147. */
  148. public function startup(&$controller) {
  149. $controller->request->params['isAjax'] = $this->request->is('ajax');
  150. $isRecognized = (
  151. !in_array($this->ext, array('html', 'htm')) &&
  152. $this->response->getMimeType($this->ext)
  153. );
  154. if (!empty($this->ext) && $isRecognized) {
  155. $this->renderAs($controller, $this->ext);
  156. } elseif ($this->request->is('ajax')) {
  157. $this->renderAs($controller, 'ajax');
  158. } elseif (empty($this->ext) || in_array($this->ext, array('html', 'htm'))) {
  159. $this->respondAs('html', array('charset' => Configure::read('App.encoding')));
  160. }
  161. if ($this->requestedWith('xml')) {
  162. try {
  163. $xml = Xml::build(trim(file_get_contents('php://input')));
  164. if (isset($xml->data)) {
  165. $controller->data = Xml::toArray($xml->data);
  166. } else {
  167. $controller->data = Xml::toArray($xml);
  168. }
  169. } catch (Exception $e) {}
  170. }
  171. }
  172. /**
  173. * Handles (fakes) redirects for Ajax requests using requestAction()
  174. *
  175. * @param object $controller A reference to the controller
  176. * @param mixed $url A string or array containing the redirect location
  177. * @param mixed HTTP Status for redirect
  178. */
  179. public function beforeRedirect(&$controller, $url, $status = null) {
  180. if (!$this->request->is('ajax')) {
  181. return;
  182. }
  183. foreach ($_POST as $key => $val) {
  184. unset($_POST[$key]);
  185. }
  186. if (is_array($url)) {
  187. $url = Router::url($url + array('base' => false));
  188. }
  189. if (!empty($status)) {
  190. $statusCode = $this->response->httpCodes($status);
  191. $code = key($statusCode);
  192. $this->response->statusCode($code);
  193. }
  194. $this->response->body($this->requestAction($url, array('return', 'bare' => false)));
  195. $this->response->send();
  196. $this->_stop();
  197. }
  198. /**
  199. * Returns true if the current HTTP request is Ajax, false otherwise
  200. *
  201. * @return boolean True if call is Ajax
  202. */
  203. public function isAjax() {
  204. return $this->request->is('ajax');
  205. }
  206. /**
  207. * Returns true if the current HTTP request is coming from a Flash-based client
  208. *
  209. * @return boolean True if call is from Flash
  210. */
  211. public function isFlash() {
  212. return $this->request->is('flash');
  213. }
  214. /**
  215. * Returns true if the current request is over HTTPS, false otherwise.
  216. *
  217. * @return bool True if call is over HTTPS
  218. */
  219. public function isSSL() {
  220. return $this->request->is('ssl');
  221. }
  222. /**
  223. * Returns true if the current call accepts an XML response, false otherwise
  224. *
  225. * @return boolean True if client accepts an XML response
  226. */
  227. public function isXml() {
  228. return $this->prefers('xml');
  229. }
  230. /**
  231. * Returns true if the current call accepts an RSS response, false otherwise
  232. *
  233. * @return boolean True if client accepts an RSS response
  234. */
  235. public function isRss() {
  236. return $this->prefers('rss');
  237. }
  238. /**
  239. * Returns true if the current call accepts an Atom response, false otherwise
  240. *
  241. * @return boolean True if client accepts an RSS response
  242. */
  243. public function isAtom() {
  244. return $this->prefers('atom');
  245. }
  246. /**
  247. * Returns true if user agent string matches a mobile web browser, or if the
  248. * client accepts WAP content.
  249. *
  250. * @return boolean True if user agent is a mobile web browser
  251. */
  252. function isMobile() {
  253. return $this->request->is('mobile') || $this->accepts('wap');
  254. }
  255. /**
  256. * Returns true if the client accepts WAP content
  257. *
  258. * @return bool
  259. */
  260. public function isWap() {
  261. return $this->prefers('wap');
  262. }
  263. /**
  264. * Returns true if the current call a POST request
  265. *
  266. * @return boolean True if call is a POST
  267. * @deprecated Use $this->request->is('post'); from your controller.
  268. */
  269. public function isPost() {
  270. return $this->request->is('post');
  271. }
  272. /**
  273. * Returns true if the current call a PUT request
  274. *
  275. * @return boolean True if call is a PUT
  276. * @deprecated Use $this->request->is('put'); from your controller.
  277. */
  278. public function isPut() {
  279. return $this->request->is('put');
  280. }
  281. /**
  282. * Returns true if the current call a GET request
  283. *
  284. * @return boolean True if call is a GET
  285. * @deprecated Use $this->request->is('get'); from your controller.
  286. */
  287. public function isGet() {
  288. return $this->request->is('get');
  289. }
  290. /**
  291. * Returns true if the current call a DELETE request
  292. *
  293. * @return boolean True if call is a DELETE
  294. * @deprecated Use $this->request->is('delete'); from your controller.
  295. */
  296. public function isDelete() {
  297. return $this->request->is('delete');
  298. }
  299. /**
  300. * Gets Prototype version if call is Ajax, otherwise empty string.
  301. * The Prototype library sets a special "Prototype version" HTTP header.
  302. *
  303. * @return string Prototype version of component making Ajax call
  304. */
  305. public function getAjaxVersion() {
  306. if (env('HTTP_X_PROTOTYPE_VERSION') != null) {
  307. return env('HTTP_X_PROTOTYPE_VERSION');
  308. }
  309. return false;
  310. }
  311. /**
  312. * Adds/sets the Content-type(s) for the given name. This method allows
  313. * content-types to be mapped to friendly aliases (or extensions), which allows
  314. * RequestHandler to automatically respond to requests of that type in the
  315. * startup method.
  316. *
  317. * @param string $name The name of the Content-type, i.e. "html", "xml", "css"
  318. * @param mixed $type The Content-type or array of Content-types assigned to the name,
  319. * i.e. "text/html", or "application/xml"
  320. * @return void
  321. */
  322. public function setContent($name, $type = null) {
  323. $this->response->type(array($name => $type));
  324. }
  325. /**
  326. * Gets the server name from which this request was referred
  327. *
  328. * @return string Server address
  329. * @deprecated use $this->request->referer() from your controller instead
  330. */
  331. public function getReferer() {
  332. return $this->request->referer(false);
  333. }
  334. /**
  335. * Gets remote client IP
  336. *
  337. * @return string Client IP address
  338. * @deprecated use $this->request->clientIp() from your controller instead.
  339. */
  340. public function getClientIP($safe = true) {
  341. return $this->request->clientIp($safe);
  342. }
  343. /**
  344. * Determines which content types the client accepts. Acceptance is based on
  345. * the file extension parsed by the Router (if present), and by the HTTP_ACCEPT
  346. * header. Unlike CakeRequest::accepts() this method deals entirely with mapped content types.
  347. *
  348. * Usage:
  349. *
  350. * `$this->RequestHandler->accepts(array('xml', 'html', 'json'));`
  351. *
  352. * Returns true if the client accepts any of the supplied types.
  353. *
  354. * `$this->RequestHandler->accepts('xml');`
  355. *
  356. * Returns true if the client accepts xml.
  357. *
  358. * @param mixed $type Can be null (or no parameter), a string type name, or an
  359. * array of types
  360. * @return mixed If null or no parameter is passed, returns an array of content
  361. * types the client accepts. If a string is passed, returns true
  362. * if the client accepts it. If an array is passed, returns true
  363. * if the client accepts one or more elements in the array.
  364. * @see RequestHandlerComponent::setContent()
  365. */
  366. public function accepts($type = null) {
  367. $accepted = $this->request->accepts();
  368. if ($type == null) {
  369. return $this->mapType($accepted);
  370. } elseif (is_array($type)) {
  371. foreach ($type as $t) {
  372. $t = $this->mapAlias($t);
  373. if (in_array($t, $accepted)) {
  374. return true;
  375. }
  376. }
  377. return false;
  378. } elseif (is_string($type)) {
  379. $type = $this->mapAlias($type);
  380. return in_array($type, $accepted);
  381. }
  382. return false;
  383. }
  384. /**
  385. * Determines the content type of the data the client has sent (i.e. in a POST request)
  386. *
  387. * @param mixed $type Can be null (or no parameter), a string type name, or an array of types
  388. * @return mixed If a single type is supplied a boolean will be returned. If no type is provided
  389. * The mapped value of CONTENT_TYPE will be returned. If an array is supplied the first type
  390. * in the request content type will be returned.
  391. */
  392. public function requestedWith($type = null) {
  393. if (!$this->request->is('post') && !$this->request->is('put')) {
  394. return null;
  395. }
  396. list($contentType) = explode(';', env('CONTENT_TYPE'));
  397. if ($type == null) {
  398. return $this->mapType($contentType);
  399. } elseif (is_array($type)) {
  400. foreach ($type as $t) {
  401. if ($this->requestedWith($t)) {
  402. return $t;
  403. }
  404. }
  405. return false;
  406. } elseif (is_string($type)) {
  407. return ($type == $this->mapType($contentType));
  408. }
  409. }
  410. /**
  411. * Determines which content-types the client prefers. If no parameters are given,
  412. * the content-type that the client most likely prefers is returned. If $type is
  413. * an array, the first item in the array that the client accepts is returned.
  414. * Preference is determined primarily by the file extension parsed by the Router
  415. * if provided, and secondarily by the list of content-types provided in
  416. * HTTP_ACCEPT.
  417. *
  418. * @param mixed $type An optional array of 'friendly' content-type names, i.e.
  419. * 'html', 'xml', 'js', etc.
  420. * @return mixed If $type is null or not provided, the first content-type in the
  421. * list, based on preference, is returned.
  422. * @see RequestHandlerComponent::setContent()
  423. */
  424. public function prefers($type = null) {
  425. $accepts = $this->accepts();
  426. if ($type == null) {
  427. if (empty($this->ext)) {
  428. if (is_array($accepts)) {
  429. return $accepts[0];
  430. }
  431. return $accepts;
  432. }
  433. return $this->ext;
  434. }
  435. $types = (array)$type;
  436. if (count($types) === 1) {
  437. if (!empty($this->ext)) {
  438. return ($types[0] == $this->ext);
  439. }
  440. return ($types[0] == $accepts[0]);
  441. }
  442. $intersect = array_values(array_intersect($accepts, $types));
  443. if (empty($intersect)) {
  444. return false;
  445. }
  446. return $intersect[0];
  447. }
  448. /**
  449. * Sets the layout and template paths for the content type defined by $type.
  450. *
  451. * ### Usage:
  452. *
  453. * Render the response as an 'ajax' response.
  454. *
  455. * `$this->RequestHandler->renderAs($this, 'ajax');`
  456. *
  457. * Render the response as an xml file and force the result as a file download.
  458. *
  459. * `$this->RequestHandler->renderAs($this, 'xml', array('attachment' => 'myfile.xml');`
  460. *
  461. * @param object $controller A reference to a controller object
  462. * @param string $type Type of response to send (e.g: 'ajax')
  463. * @param array $options Array of options to use
  464. * @return void
  465. * @see RequestHandlerComponent::setContent()
  466. * @see RequestHandlerComponent::respondAs()
  467. */
  468. public function renderAs(&$controller, $type, $options = array()) {
  469. $defaults = array('charset' => 'UTF-8');
  470. if (Configure::read('App.encoding') !== null) {
  471. $defaults['charset'] = Configure::read('App.encoding');
  472. }
  473. $options = array_merge($defaults, $options);
  474. if ($type == 'ajax') {
  475. $controller->layout = $this->ajaxLayout;
  476. return $this->respondAs('html', $options);
  477. }
  478. $controller->ext = '.ctp';
  479. if (empty($this->__renderType)) {
  480. $controller->viewPath .= DS . $type;
  481. } else {
  482. $remove = preg_replace("/([\/\\\\]{$this->__renderType})$/", DS . $type, $controller->viewPath);
  483. $controller->viewPath = $remove;
  484. }
  485. $this->__renderType = $type;
  486. $controller->layoutPath = $type;
  487. if ($this->response->getMimeType($type)) {
  488. $this->respondAs($type, $options);
  489. }
  490. $helper = ucfirst($type);
  491. $isAdded = (
  492. in_array($helper, $controller->helpers) ||
  493. array_key_exists($helper, $controller->helpers)
  494. );
  495. if (!$isAdded) {
  496. if (App::import('Helper', $helper)) {
  497. $controller->helpers[] = $helper;
  498. }
  499. }
  500. }
  501. /**
  502. * Sets the response header based on type map index name. This wraps several methods
  503. * available on CakeResponse. It also allows you to use Content-Type aliases.
  504. *
  505. * @param mixed $type Friendly type name, i.e. 'html' or 'xml', or a full content-type,
  506. * like 'application/x-shockwave'.
  507. * @param array $options If $type is a friendly type name that is associated with
  508. * more than one type of content, $index is used to select which content-type to use.
  509. * @return boolean Returns false if the friendly type name given in $type does
  510. * not exist in the type map, or if the Content-type header has
  511. * already been set by this method.
  512. * @see RequestHandlerComponent::setContent()
  513. */
  514. public function respondAs($type, $options = array()) {
  515. $defaults = array('index' => null, 'charset' => null, 'attachment' => false);
  516. $options = $options + $defaults;
  517. $cType = null;
  518. if (strpos($type, '/') === false) {
  519. $cType = $this->response->getMimeType($type);
  520. if ($cType === false) {
  521. return false;
  522. }
  523. if (is_array($cType) && isset($cType[$options['index']])) {
  524. $cType = $cType[$options['index']];
  525. }
  526. if (is_array($cType)) {
  527. if ($this->prefers($cType)) {
  528. $cType = $this->prefers($cType);
  529. } else {
  530. $cType = $cType[0];
  531. }
  532. }
  533. } else {
  534. $cType = $type;
  535. }
  536. if ($cType != null) {
  537. $this->response->type($cType);
  538. if (!empty($options['charset'])) {
  539. $this->response->charset($options['charset']);
  540. }
  541. if (!empty($options['attachment'])) {
  542. $this->response->download($options['attachment']);
  543. }
  544. return true;
  545. }
  546. return false;
  547. }
  548. /**
  549. * Returns the current response type (Content-type header), or null if not alias exists
  550. *
  551. * @return mixed A string content type alias, or raw content type if no alias map exists,
  552. * otherwise null
  553. */
  554. public function responseType() {
  555. return $this->mapType($this->response->type());
  556. }
  557. /**
  558. * Maps a content-type back to an alias
  559. *
  560. * @param mixed $cType Either a string content type to map, or an array of types.
  561. * @return mixed Aliases for the types provided.
  562. * @deprecated Use $this->response->mapType() in your controller instead.
  563. */
  564. public function mapType($cType) {
  565. return $this->response->mapType($cType);
  566. }
  567. /**
  568. * Maps a content type alias back to its mime-type(s)
  569. *
  570. * @param mixed $alias String alias to convert back into a content type. Or an array of aliases to map.
  571. * @return mixed Null on an undefined alias. String value of the mapped alias type. If an
  572. * alias maps to more than one content type, the first one will be returned.
  573. */
  574. public function mapAlias($alias) {
  575. if (is_array($alias)) {
  576. return array_map(array($this, 'mapAlias'), $alias);
  577. }
  578. $type = $this->response->getMimeType($alias);
  579. if ($type) {
  580. if (is_array($type)) {
  581. return $type[0];
  582. }
  583. return $type;
  584. }
  585. return null;
  586. }
  587. }