1 <?php
defined('SYSPATH') or die('No direct script access.');
3 * Response wrapper. Created as the result of any [Request] execution
4 * or utility method (i.e. Redirect). Implements standard HTTP
10 * @copyright (c) 2008-2009 Kohana Team
11 * @license http://kohanaphp.com/license
14 class Kohana_Response
implements Serializable
{
17 * Factory method to create a new [Response]. Pass properties
18 * in using an associative array.
20 * // Create a new response
21 * $response = Response::factory();
23 * // Create a new response with headers
24 * $response = Response::factory(array('status' => 200));
26 * @param array setup the response object
27 * @return [Kohana_Response]
29 public static function factory(array $config = array())
31 return new Response($config);
35 * Generates a [Cache-Control HTTP](http://en.wikipedia.org/wiki/List_of_HTTP_headers)
36 * header based on the supplied array.
38 * // Set the cache control headers you want to use
39 * $cache_control = array(
41 * 'must-revalidate' => NULL,
45 * // Create the cache control header, creates :
46 * // Cache-Control: max-age=3600, must-revalidate, public
47 * $response->headers['Cache-Control'] = Response::create_cache_control($cache_control);
49 * @param array cache_control parts to render
52 public static function create_cache_control(array $cache_control)
57 // Foreach cache control entry
58 foreach ($cache_control as $key => $value)
60 // Create a cache control fragment
61 $parts[] = empty($value) ?
$key : $key.'='.$value;
63 // Return the rendered parts
64 return implode(', ', $parts);
68 * Parses the Cache-Control header and returning an array representation of the Cache-Control
71 * // Create the cache control header
72 * $response->headers['Cache-Control'] = 'max-age=3600, must-revalidate, public';
74 * // Parse the cache control header
75 * if($cache_control = Request::parse_cache_control($response->headers))
77 * // Cache-Control header was found
78 * $maxage = $cache_control['max-age'];
81 * @param array headers
82 * @return boolean|array
85 public static function parse_cache_control(array $headers)
87 // If there is no Cache-Control header
88 if ( ! isset($headers['Cache-Control']))
90 if (isset($headers['Expires']) and ($timestamp = strtotime($headers['Expires'])) !== FALSE)
92 // Return a parsed version of the Expires header
93 return Response
::create_cache_control(array(
94 'max-age' => $timestamp - time(),
95 'must-revalidate' => NULL,
106 // If no Cache-Control parts are detected
107 if ( (bool) preg_match_all('/(?<key>[a-z\-]+)=?(?<value>\w+)?/', $headers['Cache-Control'], $matches))
109 // Return combined cache-control key/value pairs
110 return array_combine($matches['key'], $matches['value']);
119 // HTTP status codes and messages
120 public static $messages = array(
123 101 => 'Switching Protocols',
129 203 => 'Non-Authoritative Information',
131 205 => 'Reset Content',
132 206 => 'Partial Content',
135 300 => 'Multiple Choices',
136 301 => 'Moved Permanently',
137 302 => 'Found', // 1.1
139 304 => 'Not Modified',
141 // 306 is deprecated but reserved
142 307 => 'Temporary Redirect',
145 400 => 'Bad Request',
146 401 => 'Unauthorized',
147 402 => 'Payment Required',
150 405 => 'Method Not Allowed',
151 406 => 'Not Acceptable',
152 407 => 'Proxy Authentication Required',
153 408 => 'Request Timeout',
156 411 => 'Length Required',
157 412 => 'Precondition Failed',
158 413 => 'Request Entity Too Large',
159 414 => 'Request-URI Too Long',
160 415 => 'Unsupported Media Type',
161 416 => 'Requested Range Not Satisfiable',
162 417 => 'Expectation Failed',
165 500 => 'Internal Server Error',
166 501 => 'Not Implemented',
167 502 => 'Bad Gateway',
168 503 => 'Service Unavailable',
169 504 => 'Gateway Timeout',
170 505 => 'HTTP Version Not Supported',
171 509 => 'Bandwidth Limit Exceeded'
175 * @var integer the response http status
177 public $status = 200;
180 * @var array headers returned in the response
182 public $headers = array();
185 * @var string the response body
190 * @var array cookies to be returned in the response
192 protected $_cookies = array();
195 * Sets up the response object
197 * @param array $config
200 public function __construct(array $config = array())
202 foreach ($config as $key => $value)
204 if (property_exists($this, $key))
206 $this->$key = $value;
210 // Add the default Content-Type header if required
211 $this->headers +
= array('Content-Type' => 'text/html; charset='.Kohana
::$charset);
213 // Add the X-Powered-By header
216 $this->headers +
= array('X-Powered-By' => 'Kohana Framework '.Kohana
::VERSION
);
221 * Outputs the body when cast to string
225 public function __toString()
227 return (string) $this->body
;
231 * Returns the body of the response
235 public function body()
237 return (string) $this->body
;
241 * Sets or gets the HTTP status from this response.
243 * // Set the HTTP status to 404 Not Found
244 * $response = Response::factory()
247 * // Get the current status
248 * $status = $response->status();
250 * @param integer status to set to this response
251 * @return integer|self
253 public function status($status = NULL)
255 if ($status === NULL)
257 return $this->status
;
259 else if (array_key_exists($status, Response
::$messages))
261 $this->status
= (int) $status;
266 throw new Kohana_Exception(__METHOD__
.' unknown status value : :value', array(':value' => $status));
271 * Gets and sets headers to the [Response], allowing chaining
272 * of response methods. If chaining isn't required, direct
273 * access to the property should be used instead.
276 * $accept = $response->headers('Content-Type');
279 * $response->headers('Content-Type', 'text/html');
282 * $headers = $response->headers();
284 * // Set multiple headers
285 * $response->headers(array('Content-Type' => 'text/html', 'Cache-Control' => 'no-cache'));
288 * @param string $value
291 public function headers($key = NULL, $value = NULL)
295 return $this->headers
;
297 else if (is_array($key))
299 $this->headers
= $key;
302 else if ($value === NULL)
304 return $this->headers
[$key];
308 $this->headers
[$key] = $value;
314 * Sets a cookie to the response
317 * @param string value
318 * @param int expiration
321 public function set_cookie($name, $value, $expiration = NULL)
323 if ($expiration === NULL)
325 $expiration = Cookie
::$expiration;
327 else if ($expiration !== 0)
329 $expiration +
= time();
332 $this->_cookies
[$name] = array(
334 'expiration' => $expiration
341 * Returns a cookie by name
343 * @param string $name
344 * @param string $default
347 public function get_cookie($name, $default = NULL)
349 return isset($this->_cookies
[$name]) ?
$this->_cookies
[$name]['value'] : $default;
353 * Get all the cookies
357 public function get_cookies()
359 return $this->_cookies
;
363 * Deletes a cookie set to the response
368 public function delete_cookie($name)
370 if (isset($this->_cookies
[$name]))
372 unset($this->_cookies
[$name]);
379 * Deletes all cookies from this response
383 public function delete_cookies()
385 $this->_cookies
= array();
390 * Sends the response status and all set headers.
394 public function send_headers()
396 if ( ! headers_sent())
398 if (isset($_SERVER['SERVER_PROTOCOL']))
400 // Use the default server protocol
401 $protocol = $_SERVER['SERVER_PROTOCOL'];
405 // Default to using newer protocol
406 $protocol = 'HTTP/1.1';
410 header($protocol.' '.$this->status
.' '.Response
::$messages[$this->status
]);
412 foreach ($this->headers
as $name => $value)
414 if (is_string($name))
416 // Combine the name and value to make a raw header
417 $value = "{$name}: {$value}";
420 // Send the raw header
421 header($value, TRUE);
425 foreach ($this->_cookies
as $name => $value)
427 Cookie
::set($name, $value['value'], $value['expiration']);
435 * Send file download as the response. All execution will be halted when
436 * this method is called! Use TRUE for the filename to send the current
437 * response as the file content. The third parameter allows the following
440 * Type | Option | Description | Default Value
441 * ----------|-----------|------------------------------------|--------------
442 * `boolean` | inline | Display inline instead of download | `FALSE`
443 * `string` | mime_type | Manual mime type | Automatic
444 * `boolean` | delete | Delete the file after sending | `FALSE`
446 * Download a file that already exists:
448 * $request->send_file('media/packages/kohana.zip');
450 * Download generated content as a file:
452 * $request->response = $content;
453 * $request->send_file(TRUE, $filename);
455 * [!!] No further processing can be done after this method is called!
457 * @param string filename with path, or TRUE for the current response
458 * @param string downloaded file name
459 * @param array additional options
461 * @throws Kohana_Exception
462 * @uses File::mime_by_ext
464 * @uses Request::send_headers
466 public function send_file($filename, $download = NULL, array $options = NULL)
468 if ( ! empty($options['mime_type']))
470 // The mime-type has been manually set
471 $mime = $options['mime_type'];
474 if ($filename === TRUE)
476 if (empty($download))
478 throw new Kohana_Exception('Download name must be provided for streaming files');
481 // Temporary files will automatically be deleted
482 $options['delete'] = FALSE;
486 // Guess the mime using the file extension
487 $mime = File
::mime_by_ext(strtolower(pathinfo($download, PATHINFO_EXTENSION
)));
490 // Force the data to be rendered if
491 $file_data = (string) $this->response
;
493 // Get the content size
494 $size = strlen($file_data);
496 // Create a temporary file to hold the current response
499 // Write the current response into the file
500 fwrite($file, $file_data);
502 // File data is no longer needed
507 // Get the complete file path
508 $filename = realpath($filename);
510 if (empty($download))
512 // Use the file name as the download file name
513 $download = pathinfo($filename, PATHINFO_BASENAME
);
517 $size = filesize($filename);
522 $mime = File
::mime($filename);
525 // Open the file for reading
526 $file = fopen($filename, 'rb');
529 if ( ! is_resource($file))
531 throw new Kohana_Exception('Could not read file to send: :file', array(
532 ':file' => $download,
536 // Inline or download?
537 $disposition = empty($options['inline']) ?
'attachment' : 'inline';
539 // Calculate byte range to download.
540 list($start, $end) = $this->_calculate_byte_range($size);
542 if ( ! empty($options['resumable']))
544 if($start > 0 OR $end < ($size - 1))
550 // Range of bytes being sent
551 $this->headers
['Content-Range'] = 'bytes '.$start.'-'.$end.'/'.$size;
552 $this->headers
['Accept-Ranges'] = 'bytes';
555 // Set the headers for a download
556 $this->headers
['Content-Disposition'] = $disposition.'; filename="'.$download.'"';
557 $this->headers
['Content-Type'] = $mime;
558 $this->headers
['Content-Length'] = ($end - $start) +
1;
560 if (Request
::user_agent('browser') === 'Internet Explorer')
562 // Naturally, IE does not act like a real browser...
563 if (Request
::$protocol === 'https')
565 // http://support.microsoft.com/kb/316431
566 $this->headers
['Pragma'] = $this->headers
['Cache-Control'] = 'public';
569 if (version_compare(Request
::user_agent('version'), '8.0', '>='))
571 // http://ajaxian.com/archives/ie-8-security
572 $this->headers
['X-Content-Type-Options'] = 'nosniff';
576 // Send all headers now
577 $this->send_headers();
579 while (ob_get_level())
581 // Flush all output buffers
585 // Manually stop execution
586 ignore_user_abort(TRUE);
588 if ( ! Kohana
::$safe_mode)
590 // Keep the script running forever
594 // Send data in 16kb blocks
597 fseek($file, $start);
599 while ( ! feof($file) AND ($pos = ftell($file)) <= $end)
601 if (connection_aborted())
604 if ($pos +
$block > $end)
606 // Don't read past the buffer.
607 $block = $end - $pos +
1;
610 // Output a block of the file
611 echo fread($file, $block);
620 if ( ! empty($options['delete']))
624 // Attempt to remove the file
629 // Create a text version of the exception
630 $error = Kohana_Exception
::text($e);
632 if (is_object(Kohana
::$log))
634 // Add this exception to the log
635 Kohana
::$log->add(Kohana
::ERROR
, $error);
637 // Make sure the logs are written
638 Kohana
::$log->write();
641 // Do NOT display the exception, it will corrupt the output!
650 * Parse the byte ranges from the HTTP_RANGE header used for
651 * resumable downloads.
653 * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
654 * @return array|FALSE
656 protected function _parse_byte_range()
658 if ( ! isset($_SERVER['HTTP_RANGE']))
663 // TODO, speed this up with the use of string functions.
664 preg_match_all('/(-?[0-9]++(?:-(?![0-9]++))?)(?:-?([0-9]++))?/', $_SERVER['HTTP_RANGE'], $matches, PREG_SET_ORDER
);
670 * Calculates the byte range to use with send_file. If HTTP_RANGE doesn't
671 * exist then the complete byte range is returned
673 * @param integer $size
676 protected function _calculate_byte_range($size)
678 // Defaults to start with when the HTTP_RANGE header doesn't exist.
682 if($range = $this->_parse_byte_range())
684 // We have a byte range from HTTP_RANGE
687 if ($start[0] === '-')
689 // A negative value means we start from the end, so -500 would be the
691 $start = $size - abs($start);
694 if (isset($range[2]))
702 $start = abs(intval($start));
704 // Keep the the end value in bounds and normalize it.
705 $end = min(abs(intval($end)), $size - 1);
707 // Keep the start in bounds.
708 $start = $end < $start ?
0 : max($start, 0);
710 return array($start, $end);
715 * Generates an ETag from the response ready to be returned
717 * @throws Kohana_Request_Exception
718 * @return String Generated ETag
720 public function generate_etag()
722 if ($this->response
=== NULL)
724 throw new Kohana_Request_Exception('No response yet associated with request - cannot auto generate resource ETag');
727 // Generate a unique hash for the response
728 return '"'.sha1($this->body
).'"';
733 * Checks the browser cache to see the response needs to be returned
735 * @param String Resource ETag
736 * @throws Kohana_Request_Exception
739 public function check_cache($etag = null)
743 $etag = $this->generate_etag();
746 // Set the ETag header
747 $this->headers
['ETag'] = $etag;
749 // Add the Cache-Control header if it is not already set
750 // This allows etags to be used with Max-Age, etc
751 $this->headers +
= array(
752 'Cache-Control' => 'must-revalidate',
755 if (isset($_SERVER['HTTP_IF_NONE_MATCH']) AND $_SERVER['HTTP_IF_NONE_MATCH'] === $etag)
757 // No need to send data again
759 $this->send_headers();
769 * Serializes the object to json - handy if you
770 * need to pass the response data to other
773 * @param array array of data to serialize
775 * @throws Kohana_Exception
777 public function serialize(array $toSerialize = array())
779 // Serialize the class properties
780 $toSerialize +
= array
782 'status' => $this->status
,
783 'headers' => $this->headers
,
784 'cookies' => $this->_cookies
,
785 'body' => $this->body
788 $string = json_encode($toSerialize);
790 if (is_string($string))
796 throw new Kohana_Exception('Unable to correctly encode object to json');
801 * JSON encoded object
803 * @param string json encoded object
805 * @throws Kohana_Exception
807 public function unserialize($string)
809 // Unserialise object
810 $unserialized = json_decode($string);
813 if ($unserialized === NULL)
816 throw new Kohana_Exception('Unable to correctly decode object from json');
819 // Foreach key/value pair
820 foreach ($unserialized as $key => $value)
822 // If it belongs here
823 if (property_exists($this, $key))
826 $this->$key = $value;