5 * @author Qiang Xue <qiang.xue@gmail.com>
6 * @link https://github.com/pradosoft/prado
7 * @copyright Copyright © 2005-2016 The PRADO Group
8 * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
13 * Includes the THttpResponse adapter.
15 Prado::using('System.Web.THttpResponseAdapter');
20 * THttpResponse implements the mechanism for sending output to client users.
22 * To output a string to client, use {@link write()}. By default, the output is
23 * buffered until {@link flush()} is called or the application ends. The output in
24 * the buffer can also be cleaned by {@link clear()}. To disable output buffering,
25 * set BufferOutput property to false.
27 * To send cookies to client, use {@link getCookies()}.
28 * To redirect client browser to a new URL, use {@link redirect()}.
29 * To send a file to client, use {@link writeFile()}.
31 * By default, THttpResponse is registered with {@link TApplication} as the
32 * response module. It can be accessed via {@link TApplication::getResponse()}.
34 * THttpResponse may be configured in application configuration file as follows
36 * <module id="response" class="System.Web.THttpResponse" CacheExpire="20" CacheControl="nocache" BufferOutput="true" />
38 * where {@link getCacheExpire CacheExpire}, {@link getCacheControl CacheControl}
39 * and {@link getBufferOutput BufferOutput} are optional properties of THttpResponse.
41 * THttpResponse sends charset header if either {@link setCharset() Charset}
42 * or {@link TGlobalization::setCharset() TGlobalization.Charset} is set.
44 * Since 3.1.2, HTTP status code can be set with the {@link setStatusCode StatusCode} property.
46 * Note: Some HTTP Status codes can require additional header or body information. So, if you use {@link setStatusCode StatusCode}
47 * in your application, be sure to add theses informations.
48 * E.g : to make an http authentication :
50 * public function clickAuth ($sender, $param)
52 * $response=$this->getResponse();
53 * $response->setStatusCode(401);
54 * $response->appendHeader('WWW-Authenticate: Basic realm="Test"');
58 * This event handler will sent the 401 status code (Unauthorized) to the browser, with the WWW-Authenticate header field. This
59 * will force the browser to ask for a username and a password.
61 * @author Qiang Xue <qiang.xue@gmail.com>
65 class THttpResponse extends TModule implements ITextWriter
67 const DEFAULT_CONTENTTYPE = 'text/html';
68 const DEFAULT_CHARSET = 'UTF-8';
71 * @var The differents defined status code by RFC 2616 {@link http://www.faqs.org/rfcs/rfc2616}
73 private static $HTTP_STATUS_CODES = array(
74 100 => 'Continue', 101 => 'Switching Protocols',
75 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content',
76 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 307 => 'Temporary Redirect',
77 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed',
78 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported'
82 * @var boolean whether to buffer output
84 private $_bufferOutput=true;
86 * @var boolean if the application is initialized
88 private $_initialized=false;
90 * @var THttpCookieCollection list of cookies to return
92 private $_cookies=null;
94 * @var integer response status code
98 * @var string reason correspond to status code
100 private $_reason='OK';
102 * @var string HTML writer type
104 private $_htmlWriterType='System.Web.UI.THtmlWriter';
106 * @var string content type
108 private $_contentType=null;
110 * @var string|boolean character set, e.g. UTF-8 or false if no character set should be send to client
112 private $_charset='';
114 * @var THttpResponseAdapter adapter.
118 * @var boolean whether http response header has been sent
120 private $_httpHeaderSent;
122 * @var boolean whether content-type header has been sent
124 private $_contentTypeHeaderSent;
128 * Flushes any existing content in buffer.
130 public function __destruct()
132 //if($this->_bufferOutput)
137 * @param THttpResponseAdapter response adapter
139 public function setAdapter(THttpResponseAdapter $adapter)
141 $this->_adapter=$adapter;
145 * @return THttpResponseAdapter response adapter, null if not exist.
147 public function getAdapter()
149 return $this->_adapter;
153 * @return boolean true if adapter exists, false otherwise.
155 public function getHasAdapter()
157 return $this->_adapter!==null;
161 * Initializes the module.
162 * This method is required by IModule and is invoked by application.
163 * It starts output buffer if it is enabled.
164 * @param TXmlElement module configuration
166 public function init($config)
168 if($this->_bufferOutput)
170 $this->_initialized=true;
171 $this->getApplication()->setResponse($this);
175 * @return integer time-to-live for cached session pages in minutes, this has no effect for nocache limiter. Defaults to 180.
177 public function getCacheExpire()
179 return session_cache_expire();
183 * @param integer time-to-live for cached session pages in minutes, this has no effect for nocache limiter.
185 public function setCacheExpire($value)
187 session_cache_expire(TPropertyValue::ensureInteger($value));
191 * @return string cache control method to use for session pages
193 public function getCacheControl()
195 return session_cache_limiter();
199 * @param string cache control method to use for session pages. Valid values
200 * include none/nocache/private/private_no_expire/public
202 public function setCacheControl($value)
204 session_cache_limiter(TPropertyValue::ensureEnum($value,array('none','nocache','private','private_no_expire','public')));
208 * @return string content type, default is text/html
210 public function setContentType($type)
212 if ($this->_contentTypeHeaderSent)
213 throw new Exception('Unable to alter content-type as it has been already sent');
214 $this->_contentType = $type;
218 * @return string current content type
220 public function getContentType()
222 return $this->_contentType;
226 * @return string|boolean output charset.
228 public function getCharset()
230 return $this->_charset;
234 * @param string|boolean output charset.
236 public function setCharset($charset)
238 $this->_charset = (strToLower($charset) === 'false') ? false : (string)$charset;
242 * @return boolean whether to enable output buffer
244 public function getBufferOutput()
246 return $this->_bufferOutput;
250 * @param boolean whether to enable output buffer
251 * @throws TInvalidOperationException if session is started already
253 public function setBufferOutput($value)
255 if($this->_initialized)
256 throw new TInvalidOperationException('httpresponse_bufferoutput_unchangeable');
258 $this->_bufferOutput=TPropertyValue::ensureBoolean($value);
262 * @return integer HTTP status code, defaults to 200
264 public function getStatusCode()
266 return $this->_status;
270 * Set the HTTP status code for the response.
271 * The code and its reason will be sent to client using the currently requested http protocol version (see {@link THttpRequest::getHttpProtocolVersion})
272 * Keep in mind that HTTP/1.0 clients might not understand all status codes from HTTP/1.1
274 * @param integer HTTP status code
275 * @param string HTTP status reason, defaults to standard HTTP reasons
277 public function setStatusCode($status, $reason=null)
279 if ($this->_httpHeaderSent)
280 throw new Exception('Unable to alter response as HTTP header already sent');
281 $status=TPropertyValue::ensureInteger($status);
282 if(isset(self::$HTTP_STATUS_CODES[$status])) {
283 $this->_reason=self::$HTTP_STATUS_CODES[$status];
285 if($reason===null || $reason==='') {
286 throw new TInvalidDataValueException("response_status_reason_missing");
288 $reason=TPropertyValue::ensureString($reason);
289 if(strpos($reason, "\r")!=false || strpos($reason, "\n")!=false) {
290 throw new TInvalidDataValueException("response_status_reason_barchars");
292 $this->_reason=$reason;
294 $this->_status=$status;
298 * @param string HTTP status reason
300 public function getStatusReason() {
301 return $this->_reason;
305 * @return THttpCookieCollection list of output cookies
307 public function getCookies()
309 if($this->_cookies===null)
310 $this->_cookies=new THttpCookieCollection($this);
311 return $this->_cookies;
316 * It may not be sent back to user immediately if output buffer is enabled.
317 * @param string string to be output
319 public function write($str)
321 // when starting output make sure we send the headers first
322 if (!$this->_bufferOutput and !$this->_httpHeaderSent)
323 $this->ensureHeadersSent();
328 * Sends a file back to user.
329 * Make sure not to output anything else after calling this method.
330 * @param string file name
331 * @param string content to be set. If null, the content will be read from the server file pointed to by $fileName.
332 * @param string mime type of the content.
333 * @param array list of headers to be sent. Each array element represents a header string (e.g. 'Content-Type: text/plain').
334 * @param boolean force download of file, even if browser able to display inline. Defaults to 'true'.
335 * @param string force a specific file name on client side. Defaults to 'null' means auto-detect.
336 * @param integer size of file or content in bytes if already known. Defaults to 'null' means auto-detect.
337 * @throws TInvalidDataValueException if the file cannot be found
339 public function writeFile($fileName,$content=null,$mimeType=null,$headers=null,$forceDownload=true,$clientFileName=null,$fileSize=null)
341 static $defaultMimeTypes=array(
346 'jpeg'=>'image/jpeg',
349 'js'=>'javascript/js',
350 'pdf'=>'application/pdf',
351 'xls'=>'application/vnd.ms-excel',
356 $mimeType='text/plain';
357 if(function_exists('mime_content_type'))
358 $mimeType=mime_content_type($fileName);
359 else if(($ext=strrchr($fileName,'.'))!==false)
362 if(isset($defaultMimeTypes[$ext]))
363 $mimeType=$defaultMimeTypes[$ext];
367 if($clientFileName===null)
368 $clientFileName=basename($fileName);
370 $clientFileName=basename($clientFileName);
372 if($fileSize===null || $fileSize < 0)
373 $fileSize = ($content===null?filesize($fileName):strlen($content));
375 $this->sendHttpHeader();
376 if(is_array($headers))
378 foreach($headers as $h)
383 header('Pragma: public');
384 header('Expires: 0');
385 header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
386 header("Content-Type: $mimeType");
387 $this->_contentTypeHeaderSent = true;
390 header('Content-Length: '.$fileSize);
391 header("Content-Disposition: " . ($forceDownload ? 'attachment' : 'inline') . "; filename=\"$clientFileName\"");
392 header('Content-Transfer-Encoding: binary');
400 * Redirects the browser to the specified URL.
401 * The current application will be terminated after this method is invoked.
402 * @param string URL to be redirected to. If the URL is a relative one, the base URL of
403 * the current request will be inserted at the beginning.
405 public function redirect($url)
407 if($this->getHasAdapter())
408 $this->_adapter->httpRedirect($url);
410 $this->httpRedirect($url);
414 * Redirect the browser to another URL and exists the current application.
415 * This method is used internally. Please use {@link redirect} instead.
418 * You can set the set {@link setStatusCode StatusCode} to a value between 300 and 399 before
419 * calling this function to change the type of redirection.
420 * If not specified, StatusCode will be 302 (Found) by default
422 * @param string URL to be redirected to. If the URL is a relative one, the base URL of
423 * the current request will be inserted at the beginning.
425 public function httpRedirect($url)
427 $this->ensureHeadersSent();
430 $url=$this->getRequest()->getBaseUrl().$url;
431 if ($this->_status >= 300 && $this->_status < 400)
432 // The status code has been modified to a valid redirection status, send it
433 header('Location: '.str_replace('&','&',$url), true, $this->_status);
435 header('Location: '.str_replace('&','&',$url));
437 if(!$this->getApplication()->getRequestCompleted())
438 $this->getApplication()->onEndRequest();
444 * Reloads the current page.
445 * The effect of this method call is the same as user pressing the
446 * refresh button on his browser (without post data).
448 public function reload()
450 $this->redirect($this->getRequest()->getRequestUri());
454 * Flush the response contents and headers.
456 public function flush($continueBuffering = true)
458 if($this->getHasAdapter())
459 $this->_adapter->flushContent($continueBuffering);
461 $this->flushContent($continueBuffering);
465 * Ensures that HTTP response and content-type headers are sent
467 public function ensureHeadersSent()
469 $this->ensureHttpHeaderSent();
470 $this->ensureContentTypeHeaderSent();
474 * Outputs the buffered content, sends content-type and charset header.
475 * This method is used internally. Please use {@link flush} instead.
476 * @param boolean whether to continue buffering after flush if buffering was active
478 public function flushContent($continueBuffering = true)
480 Prado::trace("Flushing output",'System.Web.THttpResponse');
481 $this->ensureHeadersSent();
482 if($this->_bufferOutput)
484 // avoid forced send of http headers (ob_flush() does that) if there's no output yet
485 if (ob_get_length()>0)
487 if (!$continueBuffering)
489 $this->_bufferOutput = false;
502 * Ensures that the HTTP header with the status code and status reason are sent
504 protected function ensureHttpHeaderSent()
506 if (!$this->_httpHeaderSent)
507 $this->sendHttpHeader();
511 * Send the HTTP header with the status code (defaults to 200) and status reason (defaults to OK)
513 protected function sendHttpHeader()
515 $protocol=$this->getRequest()->getHttpProtocolVersion();
516 if($this->getRequest()->getHttpProtocolVersion() === null)
517 $protocol='HTTP/1.1';
519 $phpSapiName = substr(php_sapi_name(), 0, 3);
520 $cgi = $phpSapiName == 'cgi' || $phpSapiName == 'fpm';
522 header(($cgi ? 'Status:' : $protocol).' '.$this->_status.' '.$this->_reason, true, TPropertyValue::ensureInteger($this->_status));
524 $this->_httpHeaderSent = true;
528 * Ensures that the HTTP header with the status code and status reason are sent
530 protected function ensureContentTypeHeaderSent()
532 if (!$this->_contentTypeHeaderSent)
533 $this->sendContentTypeHeader();
537 * Sends content type header with optional charset.
539 protected function sendContentTypeHeader()
541 $contentType=$this->_contentType===null?self::DEFAULT_CONTENTTYPE:$this->_contentType;
542 $charset=$this->getCharset();
543 if($charset === false) {
544 $this->appendHeader('Content-Type: '.$contentType);
548 if($charset==='' && ($globalization=$this->getApplication()->getGlobalization(false))!==null)
549 $charset=$globalization->getCharset();
551 if($charset==='') $charset = self::DEFAULT_CHARSET;
552 $this->appendHeader('Content-Type: '.$contentType.';charset='.$charset);
554 $this->_contentTypeHeaderSent = true;
558 * Returns the content in the output buffer.
559 * The buffer will NOT be cleared after calling this method.
560 * Use {@link clear()} is you want to clear the buffer.
561 * @return string output that is in the buffer.
563 public function getContents()
565 Prado::trace("Retrieving output",'System.Web.THttpResponse');
566 return $this->_bufferOutput?ob_get_contents():'';
570 * Clears any existing buffered content.
572 public function clear()
574 if($this->_bufferOutput)
576 Prado::trace("Clearing output",'System.Web.THttpResponse');
580 * @param integer|null Either {@link CASE_UPPER} or {@link CASE_LOWER} or as is null (default)
583 public function getHeaders($case=null)
586 $headers = headers_list();
587 foreach($headers as $header) {
588 $tmp = explode(':', $header);
589 $key = trim(array_shift($tmp));
590 $value = trim(implode(':', $tmp));
591 if(isset($result[$key]))
592 $result[$key] .= ', ' . $value;
594 $result[$key] = $value;
598 return array_change_key_case($result, $case);
605 * @param string header
606 * @param boolean whether the header should replace a previous similar header, or add a second header of the same type
608 public function appendHeader($value, $replace=true)
610 Prado::trace("Sending header '$value'",'System.Web.THttpResponse');
611 header($value, $replace);
615 * Writes a log message into error log.
616 * This method is simple wrapper of PHP function error_log.
617 * @param string The error message that should be logged
618 * @param integer where the error should go
619 * @param string The destination. Its meaning depends on the message parameter as described above
620 * @param string The extra headers. It's used when the message parameter is set to 1. This message type uses the same internal function as mail() does.
621 * @see http://us2.php.net/manual/en/function.error-log.php
623 public function appendLog($message,$messageType=0,$destination='',$extraHeaders='')
625 error_log($message,$messageType,$destination,$extraHeaders);
630 * Do not call this method directly. Operate with the result of {@link getCookies} instead.
631 * @param THttpCookie cook to be sent
633 public function addCookie($cookie)
635 $request=$this->getRequest();
636 if($request->getEnableCookieValidation())
638 $value=$this->getApplication()->getSecurityManager()->hashData($cookie->getValue());
642 $cookie->getExpire(),
644 $cookie->getDomain(),
645 $cookie->getSecure(),
646 $cookie->getHttpOnly()
653 $cookie->getExpire(),
655 $cookie->getDomain(),
656 $cookie->getSecure(),
657 $cookie->getHttpOnly()
664 * Do not call this method directly. Operate with the result of {@link getCookies} instead.
665 * @param THttpCookie cook to be deleted
667 public function removeCookie($cookie)
674 $cookie->getDomain(),
675 $cookie->getSecure(),
676 $cookie->getHttpOnly()
681 * @return string the type of HTML writer to be used, defaults to THtmlWriter
683 public function getHtmlWriterType()
685 return $this->_htmlWriterType;
689 * @param string the type of HTML writer to be used, may be the class name or the namespace
691 public function setHtmlWriterType($value)
693 $this->_htmlWriterType=$value;
697 * Creates a new instance of HTML writer.
698 * If the type of the HTML writer is not supplied, {@link getHtmlWriterType HtmlWriterType} will be assumed.
699 * @param string type of the HTML writer to be created. If null, {@link getHtmlWriterType HtmlWriterType} will be assumed.
701 public function createHtmlWriter($type=null)
704 $type=$this->getHtmlWriterType();
705 if($this->getHasAdapter())
706 return $this->_adapter->createNewHtmlWriter($type, $this);
708 return $this->createNewHtmlWriter($type, $this);
712 * Create a new html writer instance.
713 * This method is used internally. Please use {@link createHtmlWriter} instead.
714 * @param string type of HTML writer to be created.
715 * @param ITextWriter text writer holding the contents.
717 public function createNewHtmlWriter($type, $writer)
719 return Prado::createComponent($type, $writer);