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