1: <?php
2: /**
3: * CHttpRequest and CCookieCollection class file.
4: *
5: * @author Qiang Xue <qiang.xue@gmail.com>
6: * @link http://www.yiiframework.com/
7: * @copyright 2008-2013 Yii Software LLC
8: * @license http://www.yiiframework.com/license/
9: */
10:
11:
12: /**
13: * CHttpRequest encapsulates the $_SERVER variable and resolves its inconsistency among different Web servers.
14: *
15: * CHttpRequest also manages the cookies sent from and sent to the user.
16: * By setting {@link enableCookieValidation} to true,
17: * cookies sent from the user will be validated to see if they are tampered.
18: * The property {@link getCookies cookies} returns the collection of cookies.
19: * For more details, see {@link CCookieCollection}.
20: *
21: * CHttpRequest is a default application component loaded by {@link CWebApplication}. It can be
22: * accessed via {@link CWebApplication::getRequest()}.
23: *
24: * @property string $url Part of the request URL after the host info.
25: * @property string $hostInfo Schema and hostname part (with port number if needed) of the request URL (e.g. http://www.yiiframework.com).
26: * @property string $baseUrl The relative URL for the application.
27: * @property string $scriptUrl The relative URL of the entry script.
28: * @property string $pathInfo Part of the request URL that is after the entry script and before the question mark.
29: * Note, the returned pathinfo is decoded starting from 1.1.4.
30: * Prior to 1.1.4, whether it is decoded or not depends on the server configuration
31: * (in most cases it is not decoded).
32: * @property string $requestUri The request URI portion for the currently requested URL.
33: * @property string $queryString Part of the request URL that is after the question mark.
34: * @property boolean $isSecureConnection If the request is sent via secure channel (https).
35: * @property string $requestType Request type, such as GET, POST, HEAD, PUT, PATCH, DELETE.
36: * @property boolean $isPostRequest Whether this is a POST request.
37: * @property boolean $isDeleteRequest Whether this is a DELETE request.
38: * @property boolean $isPutRequest Whether this is a PUT request.
39: * @property boolean $isPatchRequest Whether this is a PATCH request.
40: * @property boolean $isAjaxRequest Whether this is an AJAX (XMLHttpRequest) request.
41: * @property boolean $isFlashRequest Whether this is an Adobe Flash or Adobe Flex request.
42: * @property string $serverName Server name.
43: * @property integer $serverPort Server port number.
44: * @property string $urlReferrer URL referrer, null if not present.
45: * @property string $userAgent User agent, null if not present.
46: * @property string $userHostAddress User IP address.
47: * @property string $userHost User host name, null if cannot be determined.
48: * @property string $scriptFile Entry script file path (processed w/ realpath()).
49: * @property array $browser User browser capabilities.
50: * @property string $acceptTypes User browser accept types, null if not present.
51: * @property integer $port Port number for insecure requests.
52: * @property integer $securePort Port number for secure requests.
53: * @property CCookieCollection|CHttpCookie[] $cookies The cookie collection.
54: * @property array $preferredAcceptType The user preferred accept type as an array map, e.g. array('type' => 'application', 'subType' => 'xhtml', 'baseType' => 'xml', 'params' => array('q' => 0.9)).
55: * @property array $preferredAcceptTypes An array of all user accepted types (as array maps like array('type' => 'application', 'subType' => 'xhtml', 'baseType' => 'xml', 'params' => array('q' => 0.9)) ) in order of preference.
56: * @property string $preferredLanguage The user preferred language.
57: * @property array $preferredLanguages An array of all user accepted languages in order of preference.
58: * @property string $csrfToken The random token for CSRF validation.
59: *
60: * @author Qiang Xue <qiang.xue@gmail.com>
61: * @package system.web
62: * @since 1.0
63: */
64: class CHttpRequest extends CApplicationComponent
65: {
66: /**
67: * @var boolean whether cookies should be validated to ensure they are not tampered. Defaults to false.
68: */
69: public $enableCookieValidation=false;
70: /**
71: * @var boolean whether to enable CSRF (Cross-Site Request Forgery) validation. Defaults to false.
72: * By setting this property to true, forms submitted to an Yii Web application must be originated
73: * from the same application. If not, a 400 HTTP exception will be raised.
74: * Note, this feature requires that the user client accepts cookie.
75: * You also need to use {@link CHtml::form} or {@link CHtml::statefulForm} to generate
76: * the needed HTML forms in your pages.
77: * @see http://seclab.stanford.edu/websec/csrf/csrf.pdf
78: */
79: public $enableCsrfValidation=false;
80: /**
81: * @var string the name of the token used to prevent CSRF. Defaults to 'YII_CSRF_TOKEN'.
82: * This property is effectively only when {@link enableCsrfValidation} is true.
83: */
84: public $csrfTokenName='YII_CSRF_TOKEN';
85: /**
86: * @var array the property values (in name-value pairs) used to initialize the CSRF cookie.
87: * Any property of {@link CHttpCookie} may be initialized.
88: * This property is effective only when {@link enableCsrfValidation} is true.
89: */
90: public $csrfCookie;
91:
92: private $_requestUri;
93: private $_pathInfo;
94: private $_scriptFile;
95: private $_scriptUrl;
96: private $_hostInfo;
97: private $_baseUrl;
98: private $_cookies;
99: private $_preferredAcceptTypes;
100: private $_preferredLanguages;
101: private $_csrfToken;
102: private $_restParams;
103: private $_httpVersion;
104:
105: /**
106: * Initializes the application component.
107: * This method overrides the parent implementation by preprocessing
108: * the user request data.
109: */
110: public function init()
111: {
112: parent::init();
113: $this->normalizeRequest();
114: }
115:
116: /**
117: * Normalizes the request data.
118: * This method strips off slashes in request data if get_magic_quotes_gpc() returns true.
119: * It also performs CSRF validation if {@link enableCsrfValidation} is true.
120: */
121: protected function normalizeRequest()
122: {
123: // normalize request
124: if(function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc())
125: {
126: if(isset($_GET))
127: $_GET=$this->stripSlashes($_GET);
128: if(isset($_POST))
129: $_POST=$this->stripSlashes($_POST);
130: if(isset($_REQUEST))
131: $_REQUEST=$this->stripSlashes($_REQUEST);
132: if(isset($_COOKIE))
133: $_COOKIE=$this->stripSlashes($_COOKIE);
134: }
135:
136: if($this->enableCsrfValidation)
137: Yii::app()->attachEventHandler('onBeginRequest',array($this,'validateCsrfToken'));
138: }
139:
140:
141: /**
142: * Strips slashes from input data.
143: * This method is applied when magic quotes is enabled.
144: * @param mixed $data input data to be processed
145: * @return mixed processed data
146: */
147: public function stripSlashes(&$data)
148: {
149: if(is_array($data))
150: {
151: if(count($data) == 0)
152: return $data;
153: $keys=array_map('stripslashes',array_keys($data));
154: $data=array_combine($keys,array_values($data));
155: return array_map(array($this,'stripSlashes'),$data);
156: }
157: else
158: return stripslashes($data);
159: }
160:
161: /**
162: * Returns the named GET or POST parameter value.
163: * If the GET or POST parameter does not exist, the second parameter to this method will be returned.
164: * If both GET and POST contains such a named parameter, the GET parameter takes precedence.
165: * @param string $name the GET parameter name
166: * @param mixed $defaultValue the default parameter value if the GET parameter does not exist.
167: * @return mixed the GET parameter value
168: * @see getQuery
169: * @see getPost
170: */
171: public function getParam($name,$defaultValue=null)
172: {
173: return isset($_GET[$name]) ? $_GET[$name] : (isset($_POST[$name]) ? $_POST[$name] : $defaultValue);
174: }
175:
176: /**
177: * Returns the named GET parameter value.
178: * If the GET parameter does not exist, the second parameter to this method will be returned.
179: * @param string $name the GET parameter name
180: * @param mixed $defaultValue the default parameter value if the GET parameter does not exist.
181: * @return mixed the GET parameter value
182: * @see getPost
183: * @see getParam
184: */
185: public function getQuery($name,$defaultValue=null)
186: {
187: return isset($_GET[$name]) ? $_GET[$name] : $defaultValue;
188: }
189:
190: /**
191: * Returns the named POST parameter value.
192: * If the POST parameter does not exist, the second parameter to this method will be returned.
193: * @param string $name the POST parameter name
194: * @param mixed $defaultValue the default parameter value if the POST parameter does not exist.
195: * @return mixed the POST parameter value
196: * @see getParam
197: * @see getQuery
198: */
199: public function getPost($name,$defaultValue=null)
200: {
201: return isset($_POST[$name]) ? $_POST[$name] : $defaultValue;
202: }
203:
204: /**
205: * Returns the named DELETE parameter value.
206: * If the DELETE parameter does not exist or if the current request is not a DELETE request,
207: * the second parameter to this method will be returned.
208: * If the DELETE request was tunneled through POST via _method parameter, the POST parameter
209: * will be returned instead (available since version 1.1.11).
210: * @param string $name the DELETE parameter name
211: * @param mixed $defaultValue the default parameter value if the DELETE parameter does not exist.
212: * @return mixed the DELETE parameter value
213: * @since 1.1.7
214: */
215: public function getDelete($name,$defaultValue=null)
216: {
217: if($this->getIsDeleteViaPostRequest())
218: return $this->getPost($name, $defaultValue);
219:
220: if($this->getIsDeleteRequest())
221: {
222: $restParams=$this->getRestParams();
223: return isset($restParams[$name]) ? $restParams[$name] : $defaultValue;
224: }
225: else
226: return $defaultValue;
227: }
228:
229: /**
230: * Returns the named PUT parameter value.
231: * If the PUT parameter does not exist or if the current request is not a PUT request,
232: * the second parameter to this method will be returned.
233: * If the PUT request was tunneled through POST via _method parameter, the POST parameter
234: * will be returned instead (available since version 1.1.11).
235: * @param string $name the PUT parameter name
236: * @param mixed $defaultValue the default parameter value if the PUT parameter does not exist.
237: * @return mixed the PUT parameter value
238: * @since 1.1.7
239: */
240: public function getPut($name,$defaultValue=null)
241: {
242: if($this->getIsPutViaPostRequest())
243: return $this->getPost($name, $defaultValue);
244:
245: if($this->getIsPutRequest())
246: {
247: $restParams=$this->getRestParams();
248: return isset($restParams[$name]) ? $restParams[$name] : $defaultValue;
249: }
250: else
251: return $defaultValue;
252: }
253:
254: /**
255: * Returns the named PATCH parameter value.
256: * If the PATCH parameter does not exist or if the current request is not a PATCH request,
257: * the second parameter to this method will be returned.
258: * If the PATCH request was tunneled through POST via _method parameter, the POST parameter
259: * will be returned instead.
260: * @param string $name the PATCH parameter name
261: * @param mixed $defaultValue the default parameter value if the PATCH parameter does not exist.
262: * @return mixed the PATCH parameter value
263: * @since 1.1.16
264: */
265: public function getPatch($name,$defaultValue=null)
266: {
267: if($this->getIsPatchViaPostRequest())
268: return $this->getPost($name, $defaultValue);
269:
270: if($this->getIsPatchRequest())
271: {
272: $restParams=$this->getRestParams();
273: return isset($restParams[$name]) ? $restParams[$name] : $defaultValue;
274: }
275: else
276: return $defaultValue;
277: }
278:
279: /**
280: * Returns request parameters. Typically PUT, PATCH or DELETE.
281: * @return array the request parameters
282: * @since 1.1.7
283: * @since 1.1.13 method became public
284: */
285: public function getRestParams()
286: {
287: if($this->_restParams===null)
288: {
289: $result=array();
290: if(function_exists('mb_parse_str'))
291: mb_parse_str($this->getRawBody(), $result);
292: else
293: parse_str($this->getRawBody(), $result);
294: $this->_restParams=$result;
295: }
296:
297: return $this->_restParams;
298: }
299:
300: /**
301: * Returns the raw HTTP request body.
302: * @return string the request body
303: * @since 1.1.13
304: */
305: public function getRawBody()
306: {
307: static $rawBody;
308: if($rawBody===null)
309: $rawBody=file_get_contents('php://input');
310: return $rawBody;
311: }
312:
313: /**
314: * Returns the currently requested URL.
315: * This is the same as {@link getRequestUri}.
316: * @return string part of the request URL after the host info.
317: */
318: public function getUrl()
319: {
320: return $this->getRequestUri();
321: }
322:
323: /**
324: * Returns the schema and host part of the application URL.
325: * The returned URL does not have an ending slash.
326: * By default this is determined based on the user request information.
327: * You may explicitly specify it by setting the {@link setHostInfo hostInfo} property.
328: * @param string $schema schema to use (e.g. http, https). If empty, the schema used for the current request will be used.
329: * @return string schema and hostname part (with port number if needed) of the request URL (e.g. http://www.yiiframework.com)
330: * @see setHostInfo
331: */
332: public function getHostInfo($schema='')
333: {
334: if($this->_hostInfo===null)
335: {
336: if($secure=$this->getIsSecureConnection())
337: $http='https';
338: else
339: $http='http';
340: if(isset($_SERVER['HTTP_HOST']))
341: $this->_hostInfo=$http.'://'.$_SERVER['HTTP_HOST'];
342: else
343: {
344: $this->_hostInfo=$http.'://'.$_SERVER['SERVER_NAME'];
345: $port=$secure ? $this->getSecurePort() : $this->getPort();
346: if(($port!==80 && !$secure) || ($port!==443 && $secure))
347: $this->_hostInfo.=':'.$port;
348: }
349: }
350: if($schema!=='')
351: {
352: $secure=$this->getIsSecureConnection();
353: if($secure && $schema==='https' || !$secure && $schema==='http')
354: return $this->_hostInfo;
355:
356: $port=$schema==='https' ? $this->getSecurePort() : $this->getPort();
357: if($port!==80 && $schema==='http' || $port!==443 && $schema==='https')
358: $port=':'.$port;
359: else
360: $port='';
361:
362: $pos=strpos($this->_hostInfo,':');
363: return $schema.substr($this->_hostInfo,$pos,strcspn($this->_hostInfo,':',$pos+1)+1).$port;
364: }
365: else
366: return $this->_hostInfo;
367: }
368:
369: /**
370: * Sets the schema and host part of the application URL.
371: * This setter is provided in case the schema and hostname cannot be determined
372: * on certain Web servers.
373: * @param string $value the schema and host part of the application URL.
374: */
375: public function setHostInfo($value)
376: {
377: $this->_hostInfo=rtrim($value,'/');
378: }
379:
380: /**
381: * Returns the relative URL for the application.
382: * This is similar to {@link getScriptUrl scriptUrl} except that
383: * it does not have the script file name, and the ending slashes are stripped off.
384: * @param boolean $absolute whether to return an absolute URL. Defaults to false, meaning returning a relative one.
385: * @return string the relative URL for the application
386: * @see setScriptUrl
387: */
388: public function getBaseUrl($absolute=false)
389: {
390: if($this->_baseUrl===null)
391: $this->_baseUrl=rtrim(dirname($this->getScriptUrl()),'\\/');
392: return $absolute ? $this->getHostInfo() . $this->_baseUrl : $this->_baseUrl;
393: }
394:
395: /**
396: * Sets the relative URL for the application.
397: * By default the URL is determined based on the entry script URL.
398: * This setter is provided in case you want to change this behavior.
399: * @param string $value the relative URL for the application
400: */
401: public function setBaseUrl($value)
402: {
403: $this->_baseUrl=$value;
404: }
405:
406: /**
407: * Returns the relative URL of the entry script.
408: * The implementation of this method referenced Zend_Controller_Request_Http in Zend Framework.
409: * @throws CException when it is unable to determine the entry script URL.
410: * @return string the relative URL of the entry script.
411: */
412: public function getScriptUrl()
413: {
414: if($this->_scriptUrl===null)
415: {
416: $scriptName=basename($_SERVER['SCRIPT_FILENAME']);
417: if(basename($_SERVER['SCRIPT_NAME'])===$scriptName)
418: $this->_scriptUrl=$_SERVER['SCRIPT_NAME'];
419: elseif(basename($_SERVER['PHP_SELF'])===$scriptName)
420: $this->_scriptUrl=$_SERVER['PHP_SELF'];
421: elseif(isset($_SERVER['ORIG_SCRIPT_NAME']) && basename($_SERVER['ORIG_SCRIPT_NAME'])===$scriptName)
422: $this->_scriptUrl=$_SERVER['ORIG_SCRIPT_NAME'];
423: elseif(($pos=strpos($_SERVER['PHP_SELF'],'/'.$scriptName))!==false)
424: $this->_scriptUrl=substr($_SERVER['SCRIPT_NAME'],0,$pos).'/'.$scriptName;
425: elseif(isset($_SERVER['DOCUMENT_ROOT']) && strpos($_SERVER['SCRIPT_FILENAME'],$_SERVER['DOCUMENT_ROOT'])===0)
426: $this->_scriptUrl=str_replace('\\','/',str_replace($_SERVER['DOCUMENT_ROOT'],'',$_SERVER['SCRIPT_FILENAME']));
427: else
428: throw new CException(Yii::t('yii','CHttpRequest is unable to determine the entry script URL.'));
429: }
430: return $this->_scriptUrl;
431: }
432:
433: /**
434: * Sets the relative URL for the application entry script.
435: * This setter is provided in case the entry script URL cannot be determined
436: * on certain Web servers.
437: * @param string $value the relative URL for the application entry script.
438: */
439: public function setScriptUrl($value)
440: {
441: $this->_scriptUrl='/'.trim($value,'/');
442: }
443:
444: /**
445: * Returns the path info of the currently requested URL.
446: * This refers to the part that is after the entry script and before the question mark.
447: * The starting and ending slashes are stripped off.
448: * @return string part of the request URL that is after the entry script and before the question mark.
449: * Note, the returned pathinfo is decoded starting from 1.1.4.
450: * Prior to 1.1.4, whether it is decoded or not depends on the server configuration
451: * (in most cases it is not decoded).
452: * @throws CException if the request URI cannot be determined due to improper server configuration
453: */
454: public function getPathInfo()
455: {
456: if($this->_pathInfo===null)
457: {
458: $pathInfo=$this->getRequestUri();
459:
460: if(($pos=strpos($pathInfo,'?'))!==false)
461: $pathInfo=substr($pathInfo,0,$pos);
462:
463: $pathInfo=$this->decodePathInfo($pathInfo);
464:
465: $scriptUrl=$this->getScriptUrl();
466: $baseUrl=$this->getBaseUrl();
467: if(strpos($pathInfo,$scriptUrl)===0)
468: $pathInfo=substr($pathInfo,strlen($scriptUrl));
469: elseif($baseUrl==='' || strpos($pathInfo,$baseUrl)===0)
470: $pathInfo=substr($pathInfo,strlen($baseUrl));
471: elseif(strpos($_SERVER['PHP_SELF'],$scriptUrl)===0)
472: $pathInfo=substr($_SERVER['PHP_SELF'],strlen($scriptUrl));
473: else
474: throw new CException(Yii::t('yii','CHttpRequest is unable to determine the path info of the request.'));
475:
476: if($pathInfo==='/')
477: $pathInfo='';
478: elseif($pathInfo[0]==='/')
479: $pathInfo=substr($pathInfo,1);
480:
481: if(($posEnd=strlen($pathInfo)-1)>0 && $pathInfo[$posEnd]==='/')
482: $pathInfo=substr($pathInfo,0,$posEnd);
483:
484: $this->_pathInfo=$pathInfo;
485: }
486: return $this->_pathInfo;
487: }
488:
489: /**
490: * Decodes the path info.
491: * This method is an improved variant of the native urldecode() function and used in {@link getPathInfo getPathInfo()} to
492: * decode the path part of the request URI. You may override this method to change the way the path info is being decoded.
493: * @param string $pathInfo encoded path info
494: * @return string decoded path info
495: * @since 1.1.10
496: */
497: protected function decodePathInfo($pathInfo)
498: {
499: $pathInfo = urldecode($pathInfo);
500:
501: // is it UTF-8?
502: // http://w3.org/International/questions/qa-forms-utf-8.html
503: if(preg_match('%^(?:
504: [\x09\x0A\x0D\x20-\x7E] # ASCII
505: | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
506: | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
507: | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
508: | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
509: | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
510: | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
511: | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
512: )*$%xs', $pathInfo))
513: {
514: return $pathInfo;
515: }
516: else
517: {
518: return utf8_encode($pathInfo);
519: }
520: }
521:
522: /**
523: * Returns the request URI portion for the currently requested URL.
524: * This refers to the portion that is after the {@link hostInfo host info} part.
525: * It includes the {@link queryString query string} part if any.
526: * The implementation of this method referenced Zend_Controller_Request_Http in Zend Framework.
527: * @return string the request URI portion for the currently requested URL.
528: * @throws CException if the request URI cannot be determined due to improper server configuration
529: */
530: public function getRequestUri()
531: {
532: if($this->_requestUri===null)
533: {
534: if(isset($_SERVER['HTTP_X_REWRITE_URL'])) // IIS
535: $this->_requestUri=$_SERVER['HTTP_X_REWRITE_URL'];
536: elseif(isset($_SERVER['REQUEST_URI']))
537: {
538: $this->_requestUri=$_SERVER['REQUEST_URI'];
539: if(!empty($_SERVER['HTTP_HOST']))
540: {
541: if(strpos($this->_requestUri,$_SERVER['HTTP_HOST'])!==false)
542: $this->_requestUri=preg_replace('/^\w+:\/\/[^\/]+/','',$this->_requestUri);
543: }
544: else
545: $this->_requestUri=preg_replace('/^(http|https):\/\/[^\/]+/i','',$this->_requestUri);
546: }
547: elseif(isset($_SERVER['ORIG_PATH_INFO'])) // IIS 5.0 CGI
548: {
549: $this->_requestUri=$_SERVER['ORIG_PATH_INFO'];
550: if(!empty($_SERVER['QUERY_STRING']))
551: $this->_requestUri.='?'.$_SERVER['QUERY_STRING'];
552: }
553: else
554: throw new CException(Yii::t('yii','CHttpRequest is unable to determine the request URI.'));
555: }
556:
557: return $this->_requestUri;
558: }
559:
560: /**
561: * Returns part of the request URL that is after the question mark.
562: * @return string part of the request URL that is after the question mark
563: */
564: public function getQueryString()
565: {
566: return isset($_SERVER['QUERY_STRING'])?$_SERVER['QUERY_STRING']:'';
567: }
568:
569: /**
570: * Return if the request is sent via secure channel (https).
571: * @return boolean if the request is sent via secure channel (https)
572: */
573: public function getIsSecureConnection()
574: {
575: return isset($_SERVER['HTTPS']) && (strcasecmp($_SERVER['HTTPS'],'on')===0 || $_SERVER['HTTPS']==1)
576: || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'],'https')===0;
577: }
578:
579: /**
580: * Returns the request type, such as GET, POST, HEAD, PUT, PATCH, DELETE.
581: * Request type can be manually set in POST requests with a parameter named _method. Useful
582: * for RESTful request from older browsers which do not support PUT, PATCH or DELETE
583: * natively (available since version 1.1.11).
584: * @return string request type, such as GET, POST, HEAD, PUT, PATCH, DELETE.
585: */
586: public function getRequestType()
587: {
588: if(isset($_POST['_method']))
589: return strtoupper($_POST['_method']);
590: elseif(isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']))
591: return strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
592:
593: return strtoupper(isset($_SERVER['REQUEST_METHOD'])?$_SERVER['REQUEST_METHOD']:'GET');
594: }
595:
596: /**
597: * Returns whether this is a POST request.
598: * @return boolean whether this is a POST request.
599: */
600: public function getIsPostRequest()
601: {
602: return isset($_SERVER['REQUEST_METHOD']) && !strcasecmp($_SERVER['REQUEST_METHOD'],'POST');
603: }
604:
605: /**
606: * Returns whether this is a DELETE request.
607: * @return boolean whether this is a DELETE request.
608: * @since 1.1.7
609: */
610: public function getIsDeleteRequest()
611: {
612: return (isset($_SERVER['REQUEST_METHOD']) && !strcasecmp($_SERVER['REQUEST_METHOD'],'DELETE')) || $this->getIsDeleteViaPostRequest();
613: }
614:
615: /**
616: * Returns whether this is a DELETE request which was tunneled through POST.
617: * @return boolean whether this is a DELETE request tunneled through POST.
618: * @since 1.1.11
619: */
620: protected function getIsDeleteViaPostRequest()
621: {
622: return isset($_POST['_method']) && !strcasecmp($_POST['_method'],'DELETE');
623: }
624:
625: /**
626: * Returns whether this is a PUT request.
627: * @return boolean whether this is a PUT request.
628: * @since 1.1.7
629: */
630: public function getIsPutRequest()
631: {
632: return (isset($_SERVER['REQUEST_METHOD']) && !strcasecmp($_SERVER['REQUEST_METHOD'],'PUT')) || $this->getIsPutViaPostRequest();
633: }
634:
635: /**
636: * Returns whether this is a PUT request which was tunneled through POST.
637: * @return boolean whether this is a PUT request tunneled through POST.
638: * @since 1.1.11
639: */
640: protected function getIsPutViaPostRequest()
641: {
642: return isset($_POST['_method']) && !strcasecmp($_POST['_method'],'PUT');
643: }
644:
645: /**
646: * Returns whether this is a PATCH request.
647: * @return boolean whether this is a PATCH request.
648: * @since 1.1.16
649: */
650: public function getIsPatchRequest()
651: {
652: return (isset($_SERVER['REQUEST_METHOD']) && !strcasecmp($_SERVER['REQUEST_METHOD'],'PATCH')) || $this->getIsPatchViaPostRequest();
653: }
654:
655: /**
656: * Returns whether this is a PATCH request which was tunneled through POST.
657: * @return boolean whether this is a PATCH request tunneled through POST.
658: * @since 1.1.16
659: */
660: protected function getIsPatchViaPostRequest()
661: {
662: return isset($_POST['_method']) && !strcasecmp($_POST['_method'],'PATCH');
663: }
664:
665: /**
666: * Returns whether this is an AJAX (XMLHttpRequest) request.
667: * @return boolean whether this is an AJAX (XMLHttpRequest) request.
668: */
669: public function getIsAjaxRequest()
670: {
671: return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH']==='XMLHttpRequest';
672: }
673:
674: /**
675: * Returns whether this is an Adobe Flash or Adobe Flex request.
676: * @return boolean whether this is an Adobe Flash or Adobe Flex request.
677: * @since 1.1.11
678: */
679: public function getIsFlashRequest()
680: {
681: return isset($_SERVER['HTTP_USER_AGENT']) && (stripos($_SERVER['HTTP_USER_AGENT'],'Shockwave')!==false || stripos($_SERVER['HTTP_USER_AGENT'],'Flash')!==false);
682: }
683:
684: /**
685: * Returns the server name.
686: * @return string server name
687: */
688: public function getServerName()
689: {
690: return $_SERVER['SERVER_NAME'];
691: }
692:
693: /**
694: * Returns the server port number.
695: * @return integer server port number
696: */
697: public function getServerPort()
698: {
699: return $_SERVER['SERVER_PORT'];
700: }
701:
702: /**
703: * Returns the URL referrer, null if not present
704: * @return string URL referrer, null if not present
705: */
706: public function getUrlReferrer()
707: {
708: return isset($_SERVER['HTTP_REFERER'])?$_SERVER['HTTP_REFERER']:null;
709: }
710:
711: /**
712: * Returns the user agent, null if not present.
713: * @return string user agent, null if not present
714: */
715: public function getUserAgent()
716: {
717: return isset($_SERVER['HTTP_USER_AGENT'])?$_SERVER['HTTP_USER_AGENT']:null;
718: }
719:
720: /**
721: * Returns the user IP address.
722: * @return string user IP address
723: */
724: public function getUserHostAddress()
725: {
726: return isset($_SERVER['REMOTE_ADDR'])?$_SERVER['REMOTE_ADDR']:'127.0.0.1';
727: }
728:
729: /**
730: * Returns the user host name, null if it cannot be determined.
731: * @return string user host name, null if cannot be determined
732: */
733: public function getUserHost()
734: {
735: return isset($_SERVER['REMOTE_HOST'])?$_SERVER['REMOTE_HOST']:null;
736: }
737:
738: /**
739: * Returns entry script file path.
740: * @return string entry script file path (processed w/ realpath())
741: */
742: public function getScriptFile()
743: {
744: if($this->_scriptFile!==null)
745: return $this->_scriptFile;
746: else
747: return $this->_scriptFile=realpath($_SERVER['SCRIPT_FILENAME']);
748: }
749:
750: /**
751: * Returns information about the capabilities of user browser.
752: * @param string $userAgent the user agent to be analyzed. Defaults to null, meaning using the
753: * current User-Agent HTTP header information.
754: * @return array user browser capabilities.
755: * @see http://www.php.net/manual/en/function.get-browser.php
756: */
757: public function getBrowser($userAgent=null)
758: {
759: return get_browser($userAgent,true);
760: }
761:
762: /**
763: * Returns user browser accept types, null if not present.
764: * @return string user browser accept types, null if not present
765: */
766: public function getAcceptTypes()
767: {
768: return isset($_SERVER['HTTP_ACCEPT'])?$_SERVER['HTTP_ACCEPT']:null;
769: }
770:
771: private $_port;
772:
773: /**
774: * Returns the port to use for insecure requests.
775: * Defaults to 80, or the port specified by the server if the current
776: * request is insecure.
777: * You may explicitly specify it by setting the {@link setPort port} property.
778: * @return integer port number for insecure requests.
779: * @see setPort
780: * @since 1.1.3
781: */
782: public function getPort()
783: {
784: if($this->_port===null)
785: $this->_port=!$this->getIsSecureConnection() && isset($_SERVER['SERVER_PORT']) ? (int)$_SERVER['SERVER_PORT'] : 80;
786: return $this->_port;
787: }
788:
789: /**
790: * Sets the port to use for insecure requests.
791: * This setter is provided in case a custom port is necessary for certain
792: * server configurations.
793: * @param integer $value port number.
794: * @since 1.1.3
795: */
796: public function setPort($value)
797: {
798: $this->_port=(int)$value;
799: $this->_hostInfo=null;
800: }
801:
802: private $_securePort;
803:
804: /**
805: * Returns the port to use for secure requests.
806: * Defaults to 443, or the port specified by the server if the current
807: * request is secure.
808: * You may explicitly specify it by setting the {@link setSecurePort securePort} property.
809: * @return integer port number for secure requests.
810: * @see setSecurePort
811: * @since 1.1.3
812: */
813: public function getSecurePort()
814: {
815: if($this->_securePort===null)
816: $this->_securePort=$this->getIsSecureConnection() && isset($_SERVER['SERVER_PORT']) ? (int)$_SERVER['SERVER_PORT'] : 443;
817: return $this->_securePort;
818: }
819:
820: /**
821: * Sets the port to use for secure requests.
822: * This setter is provided in case a custom port is necessary for certain
823: * server configurations.
824: * @param integer $value port number.
825: * @since 1.1.3
826: */
827: public function setSecurePort($value)
828: {
829: $this->_securePort=(int)$value;
830: $this->_hostInfo=null;
831: }
832:
833: /**
834: * Returns the cookie collection.
835: * The result can be used like an associative array. Adding {@link CHttpCookie} objects
836: * to the collection will send the cookies to the client; and removing the objects
837: * from the collection will delete those cookies on the client.
838: * @return CCookieCollection the cookie collection.
839: */
840: public function getCookies()
841: {
842: if($this->_cookies!==null)
843: return $this->_cookies;
844: else
845: return $this->_cookies=new CCookieCollection($this);
846: }
847:
848: /**
849: * Redirects the browser to the specified URL.
850: * @param string $url URL to be redirected to. Note that when URL is not
851: * absolute (not starting with "/") it will be relative to current request URL.
852: * @param boolean $terminate whether to terminate the current application
853: * @param integer $statusCode the HTTP status code. Defaults to 302. See {@link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html}
854: * for details about HTTP status code.
855: */
856: public function redirect($url,$terminate=true,$statusCode=302)
857: {
858: if(strpos($url,'/')===0 && strpos($url,'//')!==0)
859: $url=$this->getHostInfo().$url;
860: header('Location: '.$url, true, $statusCode);
861: if($terminate)
862: Yii::app()->end();
863: }
864:
865: /**
866: * Parses an HTTP Accept header, returning an array map with all parts of each entry.
867: * Each array entry consists of a map with the type, subType, baseType and params, an array map of key-value parameters,
868: * obligatorily including a `q` value (i.e. preference ranking) as a double.
869: * For example, an Accept header value of <code>'application/xhtml+xml;q=0.9;level=1'</code> would give an array entry of
870: * <pre>
871: * array(
872: * 'type' => 'application',
873: * 'subType' => 'xhtml',
874: * 'baseType' => 'xml',
875: * 'params' => array(
876: * 'q' => 0.9,
877: * 'level' => '1',
878: * ),
879: * )
880: * </pre>
881: *
882: * <b>Please note:</b>
883: * To avoid great complexity, there are no steps taken to ensure that quoted strings are treated properly.
884: * If the header text includes quoted strings containing space or the , or ; characters then the results may not be correct!
885: *
886: * See also {@link http://tools.ietf.org/html/rfc2616#section-14.1} for details on Accept header.
887: * @param string $header the accept header value to parse
888: * @return array the user accepted MIME types.
889: */
890: public static function parseAcceptHeader($header)
891: {
892: $matches=array();
893: $accepts=array();
894: // get individual entries with their type, subtype, basetype and params
895: preg_match_all('/(?:\G\s?,\s?|^)(\w+|\*)\/(\w+|\*)(?:\+(\w+))?|(?<!^)\G(?:\s?;\s?(\w+)=([\w\.]+))/',$header,$matches);
896: // the regexp should (in theory) always return an array of 6 arrays
897: if(count($matches)===6)
898: {
899: $i=0;
900: $itemLen=count($matches[1]);
901: while($i<$itemLen)
902: {
903: // fill out a content type
904: $accept=array(
905: 'type'=>$matches[1][$i],
906: 'subType'=>$matches[2][$i],
907: 'baseType'=>null,
908: 'params'=>array(),
909: );
910: // fill in the base type if it exists
911: if($matches[3][$i]!==null && $matches[3][$i]!=='')
912: $accept['baseType']=$matches[3][$i];
913: // continue looping while there is no new content type, to fill in all accompanying params
914: for($i++;$i<$itemLen;$i++)
915: {
916: // if the next content type is null, then the item is a param for the current content type
917: if($matches[1][$i]===null || $matches[1][$i]==='')
918: {
919: // if this is the quality param, convert it to a double
920: if($matches[4][$i]==='q')
921: {
922: // sanity check on q value
923: $q=(double)$matches[5][$i];
924: if($q>1)
925: $q=(double)1;
926: elseif($q<0)
927: $q=(double)0;
928: $accept['params'][$matches[4][$i]]=$q;
929: }
930: else
931: $accept['params'][$matches[4][$i]]=$matches[5][$i];
932: }
933: else
934: break;
935: }
936: // q defaults to 1 if not explicitly given
937: if(!isset($accept['params']['q']))
938: $accept['params']['q']=(double)1;
939: $accepts[] = $accept;
940: }
941: }
942: return $accepts;
943: }
944:
945: /**
946: * Compare function for determining the preference of accepted MIME type array maps
947: * See {@link parseAcceptHeader()} for the format of $a and $b
948: * @param array $a user accepted MIME type as an array map
949: * @param array $b user accepted MIME type as an array map
950: * @return integer -1, 0 or 1 if $a has respectively greater preference, equal preference or less preference than $b (higher preference comes first).
951: */
952: public static function compareAcceptTypes($a,$b)
953: {
954: // check for equal quality first
955: if($a['params']['q']===$b['params']['q'])
956: if(!($a['type']==='*' xor $b['type']==='*'))
957: if (!($a['subType']==='*' xor $b['subType']==='*'))
958: // finally, higher number of parameters counts as greater precedence
959: if(count($a['params'])===count($b['params']))
960: return 0;
961: else
962: return count($a['params'])<count($b['params']) ? 1 : -1;
963: // more specific takes precedence - whichever one doesn't have a * subType
964: else
965: return $a['subType']==='*' ? 1 : -1;
966: // more specific takes precedence - whichever one doesn't have a * type
967: else
968: return $a['type']==='*' ? 1 : -1;
969: else
970: return ($a['params']['q']<$b['params']['q']) ? 1 : -1;
971: }
972:
973: /**
974: * Returns an array of user accepted MIME types in order of preference.
975: * Each array entry consists of a map with the type, subType, baseType and params, an array map of key-value parameters.
976: * See {@link parseAcceptHeader()} for a description of the array map.
977: * @return array the user accepted MIME types, as array maps, in the order of preference.
978: */
979: public function getPreferredAcceptTypes()
980: {
981: if($this->_preferredAcceptTypes===null)
982: {
983: $accepts=self::parseAcceptHeader($this->getAcceptTypes());
984: usort($accepts,array(get_class($this),'compareAcceptTypes'));
985: $this->_preferredAcceptTypes=$accepts;
986: }
987: return $this->_preferredAcceptTypes;
988: }
989:
990: /**
991: * Returns the user preferred accept MIME type.
992: * The MIME type is returned as an array map (see {@link parseAcceptHeader()}).
993: * @return array the user preferred accept MIME type or false if the user does not have any.
994: */
995: public function getPreferredAcceptType()
996: {
997: $preferredAcceptTypes=$this->getPreferredAcceptTypes();
998: return empty($preferredAcceptTypes) ? false : $preferredAcceptTypes[0];
999: }
1000:
1001: /**
1002: * Returns an array of user accepted languages in order of preference.
1003: * The returned language IDs will NOT be canonicalized using {@link CLocale::getCanonicalID}.
1004: * @return array the user accepted languages in the order of preference.
1005: * See {@link http://tools.ietf.org/html/rfc2616#section-14.4}
1006: */
1007: public function getPreferredLanguages()
1008: {
1009: if($this->_preferredLanguages===null)
1010: {
1011: $sortedLanguages=array();
1012: if(isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) && $n=preg_match_all('/([\w\-_]+)(?:\s*;\s*q\s*=\s*(\d*\.?\d*))?/',$_SERVER['HTTP_ACCEPT_LANGUAGE'],$matches))
1013: {
1014: $languages=array();
1015:
1016: for($i=0;$i<$n;++$i)
1017: {
1018: $q=$matches[2][$i];
1019: if($q==='')
1020: $q=1;
1021: if($q)
1022: $languages[]=array((float)$q,$matches[1][$i]);
1023: }
1024:
1025: usort($languages,create_function('$a,$b','if($a[0]==$b[0]) {return 0;} return ($a[0]<$b[0]) ? 1 : -1;'));
1026: foreach($languages as $language)
1027: $sortedLanguages[]=$language[1];
1028: }
1029: $this->_preferredLanguages=$sortedLanguages;
1030: }
1031: return $this->_preferredLanguages;
1032: }
1033:
1034: /**
1035: * Returns the user-preferred language that should be used by this application.
1036: * The language resolution is based on the user preferred languages and the languages
1037: * supported by the application. The method will try to find the best match.
1038: * @param array $languages a list of the languages supported by the application.
1039: * If empty, this method will return the first language returned by [[getPreferredLanguages()]].
1040: * @return string the language that the application should use. false is returned if both [[getPreferredLanguages()]]
1041: * and `$languages` are empty.
1042: */
1043: public function getPreferredLanguage($languages=array())
1044: {
1045: $preferredLanguages=$this->getPreferredLanguages();
1046: if(empty($languages)) {
1047: return !empty($preferredLanguages) ? CLocale::getCanonicalID($preferredLanguages[0]) : false;
1048: }
1049: foreach ($preferredLanguages as $preferredLanguage) {
1050: $preferredLanguage=CLocale::getCanonicalID($preferredLanguage);
1051: foreach ($languages as $language) {
1052: $language=CLocale::getCanonicalID($language);
1053: // en_us==en_us, en==en_us, en_us==en
1054: if($language===$acceptedLanguage || strpos($acceptedLanguage,$language.'_')===0 || strpos($language,$acceptedLanguage.'_')===0) {
1055: return $language;
1056: }
1057: }
1058: }
1059: return reset($languages);
1060: }
1061:
1062: /**
1063: * Sends a file to user.
1064: * @param string $fileName file name
1065: * @param string $content content to be set.
1066: * @param string $mimeType mime type of the content. If null, it will be guessed automatically based on the given file name.
1067: * @param boolean $terminate whether to terminate the current application after calling this method
1068: */
1069: public function sendFile($fileName,$content,$mimeType=null,$terminate=true)
1070: {
1071: if($mimeType===null)
1072: {
1073: if(($mimeType=CFileHelper::getMimeTypeByExtension($fileName))===null)
1074: $mimeType='text/plain';
1075: }
1076:
1077: $fileSize=(function_exists('mb_strlen') ? mb_strlen($content,'8bit') : strlen($content));
1078: $contentStart=0;
1079: $contentEnd=$fileSize-1;
1080:
1081: $httpVersion=$this->getHttpVersion();
1082: if(isset($_SERVER['HTTP_RANGE']))
1083: {
1084: header('Accept-Ranges: bytes');
1085:
1086: //client sent us a multibyte range, can not hold this one for now
1087: if(strpos($_SERVER['HTTP_RANGE'],',')!==false)
1088: {
1089: header("Content-Range: bytes $contentStart-$contentEnd/$fileSize");
1090: throw new CHttpException(416,'Requested Range Not Satisfiable');
1091: }
1092:
1093: $range=str_replace('bytes=','',$_SERVER['HTTP_RANGE']);
1094:
1095: //range requests starts from "-", so it means that data must be dumped the end point.
1096: if($range[0]==='-')
1097: $contentStart=$fileSize-substr($range,1);
1098: else
1099: {
1100: $range=explode('-',$range);
1101: $contentStart=$range[0];
1102:
1103: // check if the last-byte-pos presents in header
1104: if((isset($range[1]) && is_numeric($range[1])))
1105: $contentEnd=$range[1];
1106: }
1107:
1108: /* Check the range and make sure it's treated according to the specs.
1109: * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
1110: */
1111: // End bytes can not be larger than $end.
1112: $contentEnd=($contentEnd > $fileSize) ? $fileSize-1 : $contentEnd;
1113:
1114: // Validate the requested range and return an error if it's not correct.
1115: $wrongContentStart=($contentStart>$contentEnd || $contentStart>$fileSize-1 || $contentStart<0);
1116:
1117: if($wrongContentStart)
1118: {
1119: header("Content-Range: bytes $contentStart-$contentEnd/$fileSize");
1120: throw new CHttpException(416,'Requested Range Not Satisfiable');
1121: }
1122:
1123: header("HTTP/$httpVersion 206 Partial Content");
1124: header("Content-Range: bytes $contentStart-$contentEnd/$fileSize");
1125: }
1126: else
1127: header("HTTP/$httpVersion 200 OK");
1128:
1129: $length=$contentEnd-$contentStart+1; // Calculate new content length
1130:
1131: header('Pragma: public');
1132: header('Expires: 0');
1133: header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
1134: header("Content-Type: $mimeType");
1135: header('Content-Length: '.$length);
1136: header("Content-Disposition: attachment; filename=\"$fileName\"");
1137: header('Content-Transfer-Encoding: binary');
1138: $content=function_exists('mb_substr') ? mb_substr($content,$contentStart,$length) : substr($content,$contentStart,$length);
1139:
1140: if($terminate)
1141: {
1142: // clean up the application first because the file downloading could take long time
1143: // which may cause timeout of some resources (such as DB connection)
1144: ob_start();
1145: Yii::app()->end(0,false);
1146: ob_end_clean();
1147: echo $content;
1148: exit(0);
1149: }
1150: else
1151: echo $content;
1152: }
1153:
1154: /**
1155: * Sends existing file to a browser as a download using x-sendfile.
1156: *
1157: * X-Sendfile is a feature allowing a web application to redirect the request for a file to the webserver
1158: * that in turn processes the request, this way eliminating the need to perform tasks like reading the file
1159: * and sending it to the user. When dealing with a lot of files (or very big files) this can lead to a great
1160: * increase in performance as the web application is allowed to terminate earlier while the webserver is
1161: * handling the request.
1162: *
1163: * The request is sent to the server through a special non-standard HTTP-header.
1164: * When the web server encounters the presence of such header it will discard all output and send the file
1165: * specified by that header using web server internals including all optimizations like caching-headers.
1166: *
1167: * As this header directive is non-standard different directives exists for different web servers applications:
1168: * <ul>
1169: * <li>Apache: {@link http://tn123.org/mod_xsendfile X-Sendfile}</li>
1170: * <li>Lighttpd v1.4: {@link http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file X-LIGHTTPD-send-file}</li>
1171: * <li>Lighttpd v1.5: {@link http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file X-Sendfile}</li>
1172: * <li>Nginx: {@link http://wiki.nginx.org/XSendfile X-Accel-Redirect}</li>
1173: * <li>Cherokee: {@link http://www.cherokee-project.com/doc/other_goodies.html#x-sendfile X-Sendfile and X-Accel-Redirect}</li>
1174: * </ul>
1175: * So for this method to work the X-SENDFILE option/module should be enabled by the web server and
1176: * a proper xHeader should be sent.
1177: *
1178: * <b>Note:</b>
1179: * This option allows to download files that are not under web folders, and even files that are otherwise protected (deny from all) like .htaccess
1180: *
1181: * <b>Side effects</b>:
1182: * If this option is disabled by the web server, when this method is called a download configuration dialog
1183: * will open but the downloaded file will have 0 bytes.
1184: *
1185: * <b>Known issues</b>:
1186: * There is a Bug with Internet Explorer 6, 7 and 8 when X-SENDFILE is used over an SSL connection, it will show
1187: * an error message like this: "Internet Explorer was not able to open this Internet site. The requested site is either unavailable or cannot be found.".
1188: * You can work around this problem by removing the <code>Pragma</code>-header.
1189: *
1190: * <b>Example</b>:
1191: * <pre>
1192: * <?php
1193: * Yii::app()->request->xSendFile('/home/user/Pictures/picture1.jpg',array(
1194: * 'saveName'=>'image1.jpg',
1195: * 'mimeType'=>'image/jpeg',
1196: * 'terminate'=>false,
1197: * ));
1198: * ?>
1199: * </pre>
1200: * @param string $filePath file name with full path
1201: * @param array $options additional options:
1202: * <ul>
1203: * <li>saveName: file name shown to the user, if not set real file name will be used</li>
1204: * <li>mimeType: mime type of the file, if not set it will be guessed automatically based on the file name, if set to null no content-type header will be sent.</li>
1205: * <li>xHeader: appropriate x-sendfile header, defaults to "X-Sendfile"</li>
1206: * <li>terminate: whether to terminate the current application after calling this method, defaults to true</li>
1207: * <li>forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true. (Since version 1.1.9.)</li>
1208: * <li>addHeaders: an array of additional http headers in header-value pairs (available since version 1.1.10)</li>
1209: * </ul>
1210: */
1211: public function xSendFile($filePath, $options=array())
1212: {
1213: if(!isset($options['forceDownload']) || $options['forceDownload'])
1214: $disposition='attachment';
1215: else
1216: $disposition='inline';
1217:
1218: if(!isset($options['saveName']))
1219: $options['saveName']=basename($filePath);
1220:
1221: if(!isset($options['mimeType']))
1222: {
1223: if(($options['mimeType']=CFileHelper::getMimeTypeByExtension($filePath))===null)
1224: $options['mimeType']='text/plain';
1225: }
1226:
1227: if(!isset($options['xHeader']))
1228: $options['xHeader']='X-Sendfile';
1229:
1230: if($options['mimeType']!==null)
1231: header('Content-Type: '.$options['mimeType']);
1232: header('Content-Disposition: '.$disposition.'; filename="'.$options['saveName'].'"');
1233: if(isset($options['addHeaders']))
1234: {
1235: foreach($options['addHeaders'] as $header=>$value)
1236: header($header.': '.$value);
1237: }
1238: header(trim($options['xHeader']).': '.$filePath);
1239:
1240: if(!isset($options['terminate']) || $options['terminate'])
1241: Yii::app()->end();
1242: }
1243:
1244: /**
1245: * Returns the random token used to perform CSRF validation.
1246: * The token will be read from cookie first. If not found, a new token
1247: * will be generated.
1248: * @return string the random token for CSRF validation.
1249: * @see enableCsrfValidation
1250: */
1251: public function getCsrfToken()
1252: {
1253: if($this->_csrfToken===null)
1254: {
1255: $cookie=$this->getCookies()->itemAt($this->csrfTokenName);
1256: if(!$cookie || ($this->_csrfToken=$cookie->value)==null)
1257: {
1258: $cookie=$this->createCsrfCookie();
1259: $this->_csrfToken=$cookie->value;
1260: $this->getCookies()->add($cookie->name,$cookie);
1261: }
1262: }
1263:
1264: return $this->_csrfToken;
1265: }
1266:
1267: /**
1268: * Creates a cookie with a randomly generated CSRF token.
1269: * Initial values specified in {@link csrfCookie} will be applied
1270: * to the generated cookie.
1271: * @return CHttpCookie the generated cookie
1272: * @see enableCsrfValidation
1273: */
1274: protected function createCsrfCookie()
1275: {
1276: $cookie=new CHttpCookie($this->csrfTokenName,sha1(uniqid(mt_rand(),true)));
1277: if(is_array($this->csrfCookie))
1278: {
1279: foreach($this->csrfCookie as $name=>$value)
1280: $cookie->$name=$value;
1281: }
1282: return $cookie;
1283: }
1284:
1285: /**
1286: * Performs the CSRF validation.
1287: * This is the event handler responding to {@link CApplication::onBeginRequest}.
1288: * The default implementation will compare the CSRF token obtained
1289: * from a cookie and from a POST field. If they are different, a CSRF attack is detected.
1290: * @param CEvent $event event parameter
1291: * @throws CHttpException if the validation fails
1292: */
1293: public function validateCsrfToken($event)
1294: {
1295: if ($this->getIsPostRequest() ||
1296: $this->getIsPutRequest() ||
1297: $this->getIsPatchRequest() ||
1298: $this->getIsDeleteRequest())
1299: {
1300: $cookies=$this->getCookies();
1301:
1302: $method=$this->getRequestType();
1303: switch($method)
1304: {
1305: case 'POST':
1306: $userToken=$this->getPost($this->csrfTokenName);
1307: break;
1308: case 'PUT':
1309: $userToken=$this->getPut($this->csrfTokenName);
1310: break;
1311: case 'PATCH':
1312: $userToken=$this->getPatch($this->csrfTokenName);
1313: break;
1314: case 'DELETE':
1315: $userToken=$this->getDelete($this->csrfTokenName);
1316: }
1317:
1318: if (!empty($userToken) && $cookies->contains($this->csrfTokenName))
1319: {
1320: $cookieToken=$cookies->itemAt($this->csrfTokenName)->value;
1321: $valid=$cookieToken===$userToken;
1322: }
1323: else
1324: $valid = false;
1325: if (!$valid)
1326: throw new CHttpException(400,Yii::t('yii','The CSRF token could not be verified.'));
1327: }
1328: }
1329:
1330:
1331: /**
1332: * Returns the version of the HTTP protocol used by client.
1333: *
1334: * @return string the version of the HTTP protocol.
1335: * @since 1.1.16
1336: */
1337: public function getHttpVersion()
1338: {
1339: if($this->_httpVersion===null)
1340: {
1341: if(isset($_SERVER['SERVER_PROTOCOL']) && $_SERVER['SERVER_PROTOCOL']==='HTTP/1.0')
1342: $this->_httpVersion='1.0';
1343: else
1344: $this->_httpVersion='1.1';
1345: }
1346: return $this->_httpVersion;
1347: }
1348: }
1349:
1350:
1351: /**
1352: * CCookieCollection implements a collection class to store cookies.
1353: *
1354: * You normally access it via {@link CHttpRequest::getCookies()}.
1355: *
1356: * Since CCookieCollection extends from {@link CMap}, it can be used
1357: * like an associative array as follows:
1358: * <pre>
1359: * $cookies[$name]=new CHttpCookie($name,$value); // sends a cookie
1360: * $value=$cookies[$name]->value; // reads a cookie value
1361: * unset($cookies[$name]); // removes a cookie
1362: * </pre>
1363: *
1364: * @author Qiang Xue <qiang.xue@gmail.com>
1365: * @package system.web
1366: * @since 1.0
1367: */
1368: class CCookieCollection extends CMap
1369: {
1370: private $_request;
1371: private $_initialized=false;
1372:
1373: /**
1374: * Constructor.
1375: * @param CHttpRequest $request owner of this collection.
1376: */
1377: public function __construct(CHttpRequest $request)
1378: {
1379: $this->_request=$request;
1380: $this->copyfrom($this->getCookies());
1381: $this->_initialized=true;
1382: }
1383:
1384: /**
1385: * @return CHttpRequest the request instance
1386: */
1387: public function getRequest()
1388: {
1389: return $this->_request;
1390: }
1391:
1392: /**
1393: * @return array list of validated cookies
1394: */
1395: protected function getCookies()
1396: {
1397: $cookies=array();
1398: if($this->_request->enableCookieValidation)
1399: {
1400: $sm=Yii::app()->getSecurityManager();
1401: foreach($_COOKIE as $name=>$value)
1402: {
1403: if(is_string($value) && ($value=$sm->validateData($value))!==false)
1404: $cookies[$name]=new CHttpCookie($name,@unserialize($value));
1405: }
1406: }
1407: else
1408: {
1409: foreach($_COOKIE as $name=>$value)
1410: $cookies[$name]=new CHttpCookie($name,$value);
1411: }
1412: return $cookies;
1413: }
1414:
1415: /**
1416: * Adds a cookie with the specified name.
1417: * This overrides the parent implementation by performing additional
1418: * operations for each newly added CHttpCookie object.
1419: * @param mixed $name Cookie name.
1420: * @param CHttpCookie $cookie Cookie object.
1421: * @throws CException if the item to be inserted is not a CHttpCookie object.
1422: */
1423: public function add($name,$cookie)
1424: {
1425: if($cookie instanceof CHttpCookie)
1426: {
1427: $this->remove($name);
1428: parent::add($name,$cookie);
1429: if($this->_initialized)
1430: $this->addCookie($cookie);
1431: }
1432: else
1433: throw new CException(Yii::t('yii','CHttpCookieCollection can only hold CHttpCookie objects.'));
1434: }
1435:
1436: /**
1437: * Removes a cookie with the specified name.
1438: * This overrides the parent implementation by performing additional
1439: * cleanup work when removing a CHttpCookie object.
1440: * Since version 1.1.11, the second parameter is available that can be used to specify
1441: * the options of the CHttpCookie being removed. For example, this may be useful when dealing
1442: * with ".domain.tld" where multiple subdomains are expected to be able to manage cookies:
1443: *
1444: * <pre>
1445: * $options=array('domain'=>'.domain.tld');
1446: * Yii::app()->request->cookies['foo']=new CHttpCookie('cookie','value',$options);
1447: * Yii::app()->request->cookies->remove('cookie',$options);
1448: * </pre>
1449: *
1450: * @param mixed $name Cookie name.
1451: * @param array $options Cookie configuration array consisting of name-value pairs, available since 1.1.11.
1452: * @return CHttpCookie The removed cookie object.
1453: */
1454: public function remove($name,$options=array())
1455: {
1456: if(($cookie=parent::remove($name))!==null)
1457: {
1458: if($this->_initialized)
1459: {
1460: $cookie->configure($options);
1461: $this->removeCookie($cookie);
1462: }
1463: }
1464:
1465: return $cookie;
1466: }
1467:
1468: /**
1469: * Sends a cookie.
1470: * @param CHttpCookie $cookie cookie to be sent
1471: */
1472: protected function addCookie($cookie)
1473: {
1474: $value=$cookie->value;
1475: if($this->_request->enableCookieValidation)
1476: $value=Yii::app()->getSecurityManager()->hashData(serialize($value));
1477: if(version_compare(PHP_VERSION,'5.2.0','>='))
1478: setcookie($cookie->name,$value,$cookie->expire,$cookie->path,$cookie->domain,$cookie->secure,$cookie->httpOnly);
1479: else
1480: setcookie($cookie->name,$value,$cookie->expire,$cookie->path,$cookie->domain,$cookie->secure);
1481: }
1482:
1483: /**
1484: * Deletes a cookie.
1485: * @param CHttpCookie $cookie cookie to be deleted
1486: */
1487: protected function removeCookie($cookie)
1488: {
1489: if(version_compare(PHP_VERSION,'5.2.0','>='))
1490: setcookie($cookie->name,'',0,$cookie->path,$cookie->domain,$cookie->secure,$cookie->httpOnly);
1491: else
1492: setcookie($cookie->name,'',0,$cookie->path,$cookie->domain,$cookie->secure);
1493: }
1494: }
1495: