Added Route::$cache flag, fixes #3212. Includes tests.
[kohana-core.git] / classes / kohana / response.php
blobf9a348f74c9ddf53e4763d2a95bffad144946a04
1 <?php defined('SYSPATH') or die('No direct script access.');
2 /**
3 * Response wrapper. Created as the result of any [Request] execution
4 * or utility method (i.e. Redirect). Implements standard HTTP
5 * response format.
7 * @package Kohana
8 * @category Base
9 * @author Kohana Team
10 * @copyright (c) 2008-2009 Kohana Team
11 * @license http://kohanaphp.com/license
12 * @since 3.1.0
14 class Kohana_Response implements Serializable {
16 /**
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);
34 /**
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(
40 * 'max-age' => 3600,
41 * 'must-revalidate' => NULL,
42 * 'public' => NULL
43 * );
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
50 * @return string
52 public static function create_cache_control(array $cache_control)
54 // Create a buffer
55 $parts = array();
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);
67 /**
68 * Parses the Cache-Control header and returning an array representation of the Cache-Control
69 * header.
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))
76 * {
77 * // Cache-Control header was found
78 * $maxage = $cache_control['max-age'];
79 * }
81 * @param array headers
82 * @return boolean|array
83 * @since 3.1.0
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,
96 'public' => NULL
97 ));
99 else
101 // return
102 return FALSE;
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']);
112 else
114 // Return
115 return FALSE;
119 // HTTP status codes and messages
120 public static $messages = array(
121 // Informational 1xx
122 100 => 'Continue',
123 101 => 'Switching Protocols',
125 // Success 2xx
126 200 => 'OK',
127 201 => 'Created',
128 202 => 'Accepted',
129 203 => 'Non-Authoritative Information',
130 204 => 'No Content',
131 205 => 'Reset Content',
132 206 => 'Partial Content',
134 // Redirection 3xx
135 300 => 'Multiple Choices',
136 301 => 'Moved Permanently',
137 302 => 'Found', // 1.1
138 303 => 'See Other',
139 304 => 'Not Modified',
140 305 => 'Use Proxy',
141 // 306 is deprecated but reserved
142 307 => 'Temporary Redirect',
144 // Client Error 4xx
145 400 => 'Bad Request',
146 401 => 'Unauthorized',
147 402 => 'Payment Required',
148 403 => 'Forbidden',
149 404 => 'Not Found',
150 405 => 'Method Not Allowed',
151 406 => 'Not Acceptable',
152 407 => 'Proxy Authentication Required',
153 408 => 'Request Timeout',
154 409 => 'Conflict',
155 410 => 'Gone',
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',
164 // Server Error 5xx
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
187 public $body = NULL;
190 * @var array cookies to be returned in the response
192 protected $_cookies = array();
195 * Sets up the response object
197 * @param array $config
198 * @return void
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
214 if (Kohana::$expose)
216 $this->headers += array('X-Powered-By' => 'Kohana Framework '.Kohana::VERSION);
221 * Outputs the body when cast to string
223 * @return void
225 public function __toString()
227 return (string) $this->body;
231 * Returns the body of the response
233 * @return string
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()
245 * ->status(404);
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;
262 return $this;
264 else
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.
275 * // Get a header
276 * $accept = $response->headers('Content-Type');
278 * // Set a header
279 * $response->headers('Content-Type', 'text/html');
281 * // Get all headers
282 * $headers = $response->headers();
284 * // Set multiple headers
285 * $response->headers(array('Content-Type' => 'text/html', 'Cache-Control' => 'no-cache'));
287 * @param string $key
288 * @param string $value
289 * @return void
291 public function headers($key = NULL, $value = NULL)
293 if ($key === NULL)
295 return $this->headers;
297 else if (is_array($key))
299 $this->headers = $key;
300 return $this;
302 else if ($value === NULL)
304 return $this->headers[$key];
306 else
308 $this->headers[$key] = $value;
309 return $this;
314 * Sets a cookie to the response
316 * @param string name
317 * @param string value
318 * @param int expiration
319 * @return self
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(
333 'value' => $value,
334 'expiration' => $expiration
337 return $this;
341 * Returns a cookie by name
343 * @param string $name
344 * @param string $default
345 * @return mixed
347 public function get_cookie($name, $default = NULL)
349 return isset($this->_cookies[$name]) ? $this->_cookies[$name]['value'] : $default;
353 * Get all the cookies
355 * @return array
357 public function get_cookies()
359 return $this->_cookies;
363 * Deletes a cookie set to the response
365 * @param string name
366 * @return self
368 public function delete_cookie($name)
370 if (isset($this->_cookies[$name]))
372 unset($this->_cookies[$name]);
375 return $this;
379 * Deletes all cookies from this response
381 * @return self
383 public function delete_cookies()
385 $this->_cookies = array();
386 return $this;
390 * Sends the response status and all set headers.
392 * @return $this
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'];
403 else
405 // Default to using newer protocol
406 $protocol = 'HTTP/1.1';
409 // HTTP status line
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);
424 // Send cookies
425 foreach ($this->_cookies as $name => $value)
427 Cookie::set($name, $value['value'], $value['expiration']);
431 return $this;
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
438 * options to be set:
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
460 * @return void
461 * @throws Kohana_Exception
462 * @uses File::mime_by_ext
463 * @uses File::mime
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;
484 if ( ! isset($mime))
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
497 $file = tmpfile();
499 // Write the current response into the file
500 fwrite($file, $file_data);
502 // File data is no longer needed
503 unset($file_data);
505 else
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);
516 // Get the file size
517 $size = filesize($filename);
519 if ( ! isset($mime))
521 // Get the mime type
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))
546 // Partial Content
547 $this->status = 206;
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
582 ob_end_flush();
585 // Manually stop execution
586 ignore_user_abort(TRUE);
588 if ( ! Kohana::$safe_mode)
590 // Keep the script running forever
591 set_time_limit(0);
594 // Send data in 16kb blocks
595 $block = 1024 * 16;
597 fseek($file, $start);
599 while ( ! feof($file) AND ($pos = ftell($file)) <= $end)
601 if (connection_aborted())
602 break;
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);
613 // Send the data now
614 flush();
617 // Close the file
618 fclose($file);
620 if ( ! empty($options['delete']))
624 // Attempt to remove the file
625 unlink($filename);
627 catch (Exception $e)
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!
645 // Stop execution
646 exit;
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']))
660 return FALSE;
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);
666 return $matches[0];
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
674 * @return array
676 protected function _calculate_byte_range($size)
678 // Defaults to start with when the HTTP_RANGE header doesn't exist.
679 $start = 0;
680 $end = $size - 1;
682 if($range = $this->_parse_byte_range())
684 // We have a byte range from HTTP_RANGE
685 $start = $range[1];
687 if ($start[0] === '-')
689 // A negative value means we start from the end, so -500 would be the
690 // last 500 bytes.
691 $start = $size - abs($start);
694 if (isset($range[2]))
696 // Set the end range
697 $end = $range[2];
701 // Normalize values.
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);
714 * Generate ETag
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).'"';
732 * Check Cache
733 * Checks the browser cache to see the response needs to be returned
735 * @param String Resource ETag
736 * @throws Kohana_Request_Exception
737 * @chainable
739 public function check_cache($etag = null)
741 if (empty($etag))
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
758 $this->status = 304;
759 $this->send_headers();
761 // Stop execution
762 exit;
765 return $this;
769 * Serializes the object to json - handy if you
770 * need to pass the response data to other
771 * systems
773 * @param array array of data to serialize
774 * @return string
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))
792 return $string;
794 else
796 throw new Kohana_Exception('Unable to correctly encode object to json');
801 * JSON encoded object
803 * @param string json encoded object
804 * @return bool
805 * @throws Kohana_Exception
807 public function unserialize($string)
809 // Unserialise object
810 $unserialized = json_decode($string);
812 // If failed
813 if ($unserialized === NULL)
815 // Throw exception
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))
825 // Apply it
826 $this->$key = $value;
830 return TRUE;