Upgrade to KH 3.3.0
This commit is contained in:
713
includes/kohana/system/classes/Kohana/Response.php
Normal file
713
includes/kohana/system/classes/Kohana/Response.php
Normal file
@@ -0,0 +1,713 @@
|
||||
<?php defined('SYSPATH') OR die('No direct script access.');
|
||||
/**
|
||||
* Response wrapper. Created as the result of any [Request] execution
|
||||
* or utility method (i.e. Redirect). Implements standard HTTP
|
||||
* response format.
|
||||
*
|
||||
* @package Kohana
|
||||
* @category Base
|
||||
* @author Kohana Team
|
||||
* @copyright (c) 2008-2012 Kohana Team
|
||||
* @license http://kohanaphp.com/license
|
||||
* @since 3.1.0
|
||||
*/
|
||||
class Kohana_Response implements HTTP_Response {
|
||||
|
||||
/**
|
||||
* Factory method to create a new [Response]. Pass properties
|
||||
* in using an associative array.
|
||||
*
|
||||
* // Create a new response
|
||||
* $response = Response::factory();
|
||||
*
|
||||
* // Create a new response with headers
|
||||
* $response = Response::factory(array('status' => 200));
|
||||
*
|
||||
* @param array $config Setup the response object
|
||||
* @return Response
|
||||
*/
|
||||
public static function factory(array $config = array())
|
||||
{
|
||||
return new Response($config);
|
||||
}
|
||||
|
||||
// HTTP status codes and messages
|
||||
public static $messages = array(
|
||||
// Informational 1xx
|
||||
100 => 'Continue',
|
||||
101 => 'Switching Protocols',
|
||||
|
||||
// Success 2xx
|
||||
200 => 'OK',
|
||||
201 => 'Created',
|
||||
202 => 'Accepted',
|
||||
203 => 'Non-Authoritative Information',
|
||||
204 => 'No Content',
|
||||
205 => 'Reset Content',
|
||||
206 => 'Partial Content',
|
||||
|
||||
// Redirection 3xx
|
||||
300 => 'Multiple Choices',
|
||||
301 => 'Moved Permanently',
|
||||
302 => 'Found', // 1.1
|
||||
303 => 'See Other',
|
||||
304 => 'Not Modified',
|
||||
305 => 'Use Proxy',
|
||||
// 306 is deprecated but reserved
|
||||
307 => 'Temporary Redirect',
|
||||
|
||||
// Client Error 4xx
|
||||
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 Timeout',
|
||||
409 => 'Conflict',
|
||||
410 => 'Gone',
|
||||
411 => 'Length Required',
|
||||
412 => 'Precondition Failed',
|
||||
413 => 'Request Entity Too Large',
|
||||
414 => 'Request-URI Too Long',
|
||||
415 => 'Unsupported Media Type',
|
||||
416 => 'Requested Range Not Satisfiable',
|
||||
417 => 'Expectation Failed',
|
||||
|
||||
// Server Error 5xx
|
||||
500 => 'Internal Server Error',
|
||||
501 => 'Not Implemented',
|
||||
502 => 'Bad Gateway',
|
||||
503 => 'Service Unavailable',
|
||||
504 => 'Gateway Timeout',
|
||||
505 => 'HTTP Version Not Supported',
|
||||
509 => 'Bandwidth Limit Exceeded'
|
||||
);
|
||||
|
||||
/**
|
||||
* @var integer The response http status
|
||||
*/
|
||||
protected $_status = 200;
|
||||
|
||||
/**
|
||||
* @var HTTP_Header Headers returned in the response
|
||||
*/
|
||||
protected $_header;
|
||||
|
||||
/**
|
||||
* @var string The response body
|
||||
*/
|
||||
protected $_body = '';
|
||||
|
||||
/**
|
||||
* @var array Cookies to be returned in the response
|
||||
*/
|
||||
protected $_cookies = array();
|
||||
|
||||
/**
|
||||
* @var string The response protocol
|
||||
*/
|
||||
protected $_protocol;
|
||||
|
||||
/**
|
||||
* Sets up the response object
|
||||
*
|
||||
* @param array $config Setup the response object
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(array $config = array())
|
||||
{
|
||||
$this->_header = new HTTP_Header;
|
||||
|
||||
foreach ($config as $key => $value)
|
||||
{
|
||||
if (property_exists($this, $key))
|
||||
{
|
||||
if ($key == '_header')
|
||||
{
|
||||
$this->headers($value);
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs the body when cast to string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->_body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets the body of the response
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function body($content = NULL)
|
||||
{
|
||||
if ($content === NULL)
|
||||
return $this->_body;
|
||||
|
||||
$this->_body = (string) $content;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets the HTTP protocol. The standard protocol to use
|
||||
* is `HTTP/1.1`.
|
||||
*
|
||||
* @param string $protocol Protocol to set to the request/response
|
||||
* @return mixed
|
||||
*/
|
||||
public function protocol($protocol = NULL)
|
||||
{
|
||||
if ($protocol)
|
||||
{
|
||||
$this->_protocol = strtoupper($protocol);
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($this->_protocol === NULL)
|
||||
{
|
||||
$this->_protocol = HTTP::$protocol;
|
||||
}
|
||||
|
||||
return $this->_protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or gets the HTTP status from this response.
|
||||
*
|
||||
* // Set the HTTP status to 404 Not Found
|
||||
* $response = Response::factory()
|
||||
* ->status(404);
|
||||
*
|
||||
* // Get the current status
|
||||
* $status = $response->status();
|
||||
*
|
||||
* @param integer $status Status to set to this response
|
||||
* @return mixed
|
||||
*/
|
||||
public function status($status = NULL)
|
||||
{
|
||||
if ($status === NULL)
|
||||
{
|
||||
return $this->_status;
|
||||
}
|
||||
elseif (array_key_exists($status, Response::$messages))
|
||||
{
|
||||
$this->_status = (int) $status;
|
||||
return $this;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Kohana_Exception(__METHOD__.' unknown status value : :value', array(':value' => $status));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and sets headers to the [Response], allowing chaining
|
||||
* of response methods. If chaining isn't required, direct
|
||||
* access to the property should be used instead.
|
||||
*
|
||||
* // Get a header
|
||||
* $accept = $response->headers('Content-Type');
|
||||
*
|
||||
* // Set a header
|
||||
* $response->headers('Content-Type', 'text/html');
|
||||
*
|
||||
* // Get all headers
|
||||
* $headers = $response->headers();
|
||||
*
|
||||
* // Set multiple headers
|
||||
* $response->headers(array('Content-Type' => 'text/html', 'Cache-Control' => 'no-cache'));
|
||||
*
|
||||
* @param mixed $key
|
||||
* @param string $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function headers($key = NULL, $value = NULL)
|
||||
{
|
||||
if ($key === NULL)
|
||||
{
|
||||
return $this->_header;
|
||||
}
|
||||
elseif (is_array($key))
|
||||
{
|
||||
$this->_header->exchangeArray($key);
|
||||
return $this;
|
||||
}
|
||||
elseif ($value === NULL)
|
||||
{
|
||||
return Arr::get($this->_header, $key);
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->_header[$key] = $value;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the length of the body for use with
|
||||
* content header
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function content_length()
|
||||
{
|
||||
return strlen($this->body());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set and get cookies values for this response.
|
||||
*
|
||||
* // Get the cookies set to the response
|
||||
* $cookies = $response->cookie();
|
||||
*
|
||||
* // Set a cookie to the response
|
||||
* $response->cookie('session', array(
|
||||
* 'value' => $value,
|
||||
* 'expiration' => 12352234
|
||||
* ));
|
||||
*
|
||||
* @param mixed $key cookie name, or array of cookie values
|
||||
* @param string $value value to set to cookie
|
||||
* @return string
|
||||
* @return void
|
||||
* @return [Response]
|
||||
*/
|
||||
public function cookie($key = NULL, $value = NULL)
|
||||
{
|
||||
// Handle the get cookie calls
|
||||
if ($key === NULL)
|
||||
return $this->_cookies;
|
||||
elseif ( ! is_array($key) AND ! $value)
|
||||
return Arr::get($this->_cookies, $key);
|
||||
|
||||
// Handle the set cookie calls
|
||||
if (is_array($key))
|
||||
{
|
||||
reset($key);
|
||||
while (list($_key, $_value) = each($key))
|
||||
{
|
||||
$this->cookie($_key, $_value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( ! is_array($value))
|
||||
{
|
||||
$value = array(
|
||||
'value' => $value,
|
||||
'expiration' => Cookie::$expiration
|
||||
);
|
||||
}
|
||||
elseif ( ! isset($value['expiration']))
|
||||
{
|
||||
$value['expiration'] = Cookie::$expiration;
|
||||
}
|
||||
|
||||
$this->_cookies[$key] = $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a cookie set to the response
|
||||
*
|
||||
* @param string $name
|
||||
* @return Response
|
||||
*/
|
||||
public function delete_cookie($name)
|
||||
{
|
||||
unset($this->_cookies[$name]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all cookies from this response
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function delete_cookies()
|
||||
{
|
||||
$this->_cookies = array();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the response status and all set headers.
|
||||
*
|
||||
* @param boolean $replace replace existing headers
|
||||
* @param callback $callback function to handle header output
|
||||
* @return mixed
|
||||
*/
|
||||
public function send_headers($replace = FALSE, $callback = NULL)
|
||||
{
|
||||
return $this->_header->send_headers($this, $replace, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send file download as the response. All execution will be halted when
|
||||
* this method is called! Use TRUE for the filename to send the current
|
||||
* response as the file content. The third parameter allows the following
|
||||
* options to be set:
|
||||
*
|
||||
* Type | Option | Description | Default Value
|
||||
* ----------|-----------|------------------------------------|--------------
|
||||
* `boolean` | inline | Display inline instead of download | `FALSE`
|
||||
* `string` | mime_type | Manual mime type | Automatic
|
||||
* `boolean` | delete | Delete the file after sending | `FALSE`
|
||||
*
|
||||
* Download a file that already exists:
|
||||
*
|
||||
* $request->send_file('media/packages/kohana.zip');
|
||||
*
|
||||
* Download generated content as a file:
|
||||
*
|
||||
* $request->response($content);
|
||||
* $request->send_file(TRUE, $filename);
|
||||
*
|
||||
* [!!] No further processing can be done after this method is called!
|
||||
*
|
||||
* @param string $filename filename with path, or TRUE for the current response
|
||||
* @param string $download downloaded file name
|
||||
* @param array $options additional options
|
||||
* @return void
|
||||
* @throws Kohana_Exception
|
||||
* @uses File::mime_by_ext
|
||||
* @uses File::mime
|
||||
* @uses Request::send_headers
|
||||
*/
|
||||
public function send_file($filename, $download = NULL, array $options = NULL)
|
||||
{
|
||||
if ( ! empty($options['mime_type']))
|
||||
{
|
||||
// The mime-type has been manually set
|
||||
$mime = $options['mime_type'];
|
||||
}
|
||||
|
||||
if ($filename === TRUE)
|
||||
{
|
||||
if (empty($download))
|
||||
{
|
||||
throw new Kohana_Exception('Download name must be provided for streaming files');
|
||||
}
|
||||
|
||||
// Temporary files will automatically be deleted
|
||||
$options['delete'] = FALSE;
|
||||
|
||||
if ( ! isset($mime))
|
||||
{
|
||||
// Guess the mime using the file extension
|
||||
$mime = File::mime_by_ext(strtolower(pathinfo($download, PATHINFO_EXTENSION)));
|
||||
}
|
||||
|
||||
// Force the data to be rendered if
|
||||
$file_data = (string) $this->_body;
|
||||
|
||||
// Get the content size
|
||||
$size = strlen($file_data);
|
||||
|
||||
// Create a temporary file to hold the current response
|
||||
$file = tmpfile();
|
||||
|
||||
// Write the current response into the file
|
||||
fwrite($file, $file_data);
|
||||
|
||||
// File data is no longer needed
|
||||
unset($file_data);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get the complete file path
|
||||
$filename = realpath($filename);
|
||||
|
||||
if (empty($download))
|
||||
{
|
||||
// Use the file name as the download file name
|
||||
$download = pathinfo($filename, PATHINFO_BASENAME);
|
||||
}
|
||||
|
||||
// Get the file size
|
||||
$size = filesize($filename);
|
||||
|
||||
if ( ! isset($mime))
|
||||
{
|
||||
// Get the mime type from the extension of the download file
|
||||
$mime = File::mime_by_ext(pathinfo($download, PATHINFO_EXTENSION));
|
||||
}
|
||||
|
||||
// Open the file for reading
|
||||
$file = fopen($filename, 'rb');
|
||||
}
|
||||
|
||||
if ( ! is_resource($file))
|
||||
{
|
||||
throw new Kohana_Exception('Could not read file to send: :file', array(
|
||||
':file' => $download,
|
||||
));
|
||||
}
|
||||
|
||||
// Inline or download?
|
||||
$disposition = empty($options['inline']) ? 'attachment' : 'inline';
|
||||
|
||||
// Calculate byte range to download.
|
||||
list($start, $end) = $this->_calculate_byte_range($size);
|
||||
|
||||
if ( ! empty($options['resumable']))
|
||||
{
|
||||
if ($start > 0 OR $end < ($size - 1))
|
||||
{
|
||||
// Partial Content
|
||||
$this->_status = 206;
|
||||
}
|
||||
|
||||
// Range of bytes being sent
|
||||
$this->_header['content-range'] = 'bytes '.$start.'-'.$end.'/'.$size;
|
||||
$this->_header['accept-ranges'] = 'bytes';
|
||||
}
|
||||
|
||||
// Set the headers for a download
|
||||
$this->_header['content-disposition'] = $disposition.'; filename="'.$download.'"';
|
||||
$this->_header['content-type'] = $mime;
|
||||
$this->_header['content-length'] = (string) (($end - $start) + 1);
|
||||
|
||||
if (Request::user_agent('browser') === 'Internet Explorer')
|
||||
{
|
||||
// Naturally, IE does not act like a real browser...
|
||||
if (Request::$initial->secure())
|
||||
{
|
||||
// http://support.microsoft.com/kb/316431
|
||||
$this->_header['pragma'] = $this->_header['cache-control'] = 'public';
|
||||
}
|
||||
|
||||
if (version_compare(Request::user_agent('version'), '8.0', '>='))
|
||||
{
|
||||
// http://ajaxian.com/archives/ie-8-security
|
||||
$this->_header['x-content-type-options'] = 'nosniff';
|
||||
}
|
||||
}
|
||||
|
||||
// Send all headers now
|
||||
$this->send_headers();
|
||||
|
||||
while (ob_get_level())
|
||||
{
|
||||
// Flush all output buffers
|
||||
ob_end_flush();
|
||||
}
|
||||
|
||||
// Manually stop execution
|
||||
ignore_user_abort(TRUE);
|
||||
|
||||
if ( ! Kohana::$safe_mode)
|
||||
{
|
||||
// Keep the script running forever
|
||||
set_time_limit(0);
|
||||
}
|
||||
|
||||
// Send data in 16kb blocks
|
||||
$block = 1024 * 16;
|
||||
|
||||
fseek($file, $start);
|
||||
|
||||
while ( ! feof($file) AND ($pos = ftell($file)) <= $end)
|
||||
{
|
||||
if (connection_aborted())
|
||||
break;
|
||||
|
||||
if ($pos + $block > $end)
|
||||
{
|
||||
// Don't read past the buffer.
|
||||
$block = $end - $pos + 1;
|
||||
}
|
||||
|
||||
// Output a block of the file
|
||||
echo fread($file, $block);
|
||||
|
||||
// Send the data now
|
||||
flush();
|
||||
}
|
||||
|
||||
// Close the file
|
||||
fclose($file);
|
||||
|
||||
if ( ! empty($options['delete']))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Attempt to remove the file
|
||||
unlink($filename);
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
// Create a text version of the exception
|
||||
$error = Kohana_Exception::text($e);
|
||||
|
||||
if (is_object(Kohana::$log))
|
||||
{
|
||||
// Add this exception to the log
|
||||
Kohana::$log->add(Log::ERROR, $error);
|
||||
|
||||
// Make sure the logs are written
|
||||
Kohana::$log->write();
|
||||
}
|
||||
|
||||
// Do NOT display the exception, it will corrupt the output!
|
||||
}
|
||||
}
|
||||
|
||||
// Stop execution
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the HTTP_Interaction to a string, producing
|
||||
*
|
||||
* - Protocol
|
||||
* - Headers
|
||||
* - Body
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
if ( ! $this->_header->offsetExists('content-type'))
|
||||
{
|
||||
// Add the default Content-Type header if required
|
||||
$this->_header['content-type'] = Kohana::$content_type.'; charset='.Kohana::$charset;
|
||||
}
|
||||
|
||||
// Set the content length
|
||||
$this->headers('content-length', (string) $this->content_length());
|
||||
|
||||
// If Kohana expose, set the user-agent
|
||||
if (Kohana::$expose)
|
||||
{
|
||||
$this->headers('user-agent', Kohana::version());
|
||||
}
|
||||
|
||||
// Prepare cookies
|
||||
if ($this->_cookies)
|
||||
{
|
||||
if (extension_loaded('http'))
|
||||
{
|
||||
$this->_header['set-cookie'] = http_build_cookie($this->_cookies);
|
||||
}
|
||||
else
|
||||
{
|
||||
$cookies = array();
|
||||
|
||||
// Parse each
|
||||
foreach ($this->_cookies as $key => $value)
|
||||
{
|
||||
$string = $key.'='.$value['value'].'; expires='.date('l, d M Y H:i:s T', $value['expiration']);
|
||||
$cookies[] = $string;
|
||||
}
|
||||
|
||||
// Create the cookie string
|
||||
$this->_header['set-cookie'] = $cookies;
|
||||
}
|
||||
}
|
||||
|
||||
$output = $this->_protocol.' '.$this->_status.' '.Response::$messages[$this->_status]."\r\n";
|
||||
$output .= (string) $this->_header;
|
||||
$output .= $this->_body;
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate ETag
|
||||
* Generates an ETag from the response ready to be returned
|
||||
*
|
||||
* @throws Request_Exception
|
||||
* @return String Generated ETag
|
||||
*/
|
||||
public function generate_etag()
|
||||
{
|
||||
if ($this->_body === '')
|
||||
{
|
||||
throw new Request_Exception('No response yet associated with request - cannot auto generate resource ETag');
|
||||
}
|
||||
|
||||
// Generate a unique hash for the response
|
||||
return '"'.sha1($this->render()).'"';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the byte ranges from the HTTP_RANGE header used for
|
||||
* resumable downloads.
|
||||
*
|
||||
* @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
|
||||
* @return array|FALSE
|
||||
*/
|
||||
protected function _parse_byte_range()
|
||||
{
|
||||
if ( ! isset($_SERVER['HTTP_RANGE']))
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// TODO, speed this up with the use of string functions.
|
||||
preg_match_all('/(-?[0-9]++(?:-(?![0-9]++))?)(?:-?([0-9]++))?/', $_SERVER['HTTP_RANGE'], $matches, PREG_SET_ORDER);
|
||||
|
||||
return $matches[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the byte range to use with send_file. If HTTP_RANGE doesn't
|
||||
* exist then the complete byte range is returned
|
||||
*
|
||||
* @param integer $size
|
||||
* @return array
|
||||
*/
|
||||
protected function _calculate_byte_range($size)
|
||||
{
|
||||
// Defaults to start with when the HTTP_RANGE header doesn't exist.
|
||||
$start = 0;
|
||||
$end = $size - 1;
|
||||
|
||||
if ($range = $this->_parse_byte_range())
|
||||
{
|
||||
// We have a byte range from HTTP_RANGE
|
||||
$start = $range[1];
|
||||
|
||||
if ($start[0] === '-')
|
||||
{
|
||||
// A negative value means we start from the end, so -500 would be the
|
||||
// last 500 bytes.
|
||||
$start = $size - abs($start);
|
||||
}
|
||||
|
||||
if (isset($range[2]))
|
||||
{
|
||||
// Set the end range
|
||||
$end = $range[2];
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize values.
|
||||
$start = abs(intval($start));
|
||||
|
||||
// Keep the the end value in bounds and normalize it.
|
||||
$end = min(abs(intval($end)), $size - 1);
|
||||
|
||||
// Keep the start in bounds.
|
||||
$start = ($end < $start) ? 0 : max($start, 0);
|
||||
|
||||
return array($start, $end);
|
||||
}
|
||||
} // End Kohana_Response
|
Reference in New Issue
Block a user