123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771 |
- <?php
- /**
- * 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) Kohana Team
- * @license https://koseven.ga/LICENSE.md
- * @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 = [])
- {
- return new Response($config);
- }
- // HTTP status codes and messages
- public static $messages = [
- // Informational 1xx
- 100 => 'Continue',
- 101 => 'Switching Protocols',
- 102 => 'Processing',
- 103 => 'Early Hints',
- // Success 2xx
- 200 => 'OK',
- 201 => 'Created',
- 202 => 'Accepted',
- 203 => 'Non-Authoritative Information',
- 204 => 'No Content',
- 205 => 'Reset Content',
- 206 => 'Partial Content',
- 207 => 'Multi-Status',
- 208 => 'Already Reported',
- 226 => 'IM Used',
- // 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',
- 308 => 'Permanent 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',
- 418 => 'I’m a teapot',
- 419 => 'Authentication Timeout',
- 421 => 'Misdirected Request',
- 422 => 'Unprocessable Entity',
- 423 => 'Locked',
- 424 => 'Failed Dependency',
- 425 => 'Too Early',
- 426 => 'Upgrade Required',
- 428 => 'Precondition Required',
- 429 => 'Too Many Requests',
- 431 => 'Request Header Fields Too Large',
- 444 => 'No Response',
- 449 => 'Retry With',
- 451 => 'Unavailable For Legal Reasons',
- 494 => 'Request header too large',
- 495 => 'SSL Certificate Error',
- 496 => 'SSL Certificate Required',
- 497 => 'HTTP Request Sent to HTTPS Port',
- 499 => 'Client Closed Request',
- // Server Error 5xx
- 500 => 'Internal Server Error',
- 501 => 'Not Implemented',
- 502 => 'Bad Gateway',
- 503 => 'Service Unavailable',
- 504 => 'Gateway Timeout',
- 505 => 'HTTP Version Not Supported',
- 507 => 'Insufficient Storage',
- 508 => 'Loop Detected',
- 509 => 'Bandwidth Limit Exceeded',
- 510 => 'Not Extended',
- 511 => 'Network Authentication Required',
- 520 => 'Unknown Error',
- 521 => 'Web Server Is Down',
- 522 => 'Connection Timed Out',
- 523 => 'Origin Is Unreachable',
- 524 => 'A Timeout Occurred',
- 525 => 'SSL Handshake Failed',
- 526 => 'Invalid SSL Certificate',
- ];
- /**
- * @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 = [];
- /**
- * @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 = [])
- {
- $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', [':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);
- foreach ($key as $_key => $_value)
- {
- $this->cookie($_key, $_value);
- }
- }
- else
- {
- if ( ! is_array($value))
- {
- $value = [
- '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 = [];
- 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 a generated file:
- *
- * $csv = tmpfile();
- * fputcsv($csv, ['label1', 'label2']);
- * $request->send_file($csv, $filename);
- *
- * 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|resource|bool $filename filename with path, file stream, 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 if (is_resource($filename) && get_resource_type($filename) === 'stream')
- {
- if (empty($download))
- {
- throw new Kohana_Exception('Download name must be provided for streaming files');
- }
- // Make sure this is a file handle
- $file_meta = stream_get_meta_data($filename);
- if ($file_meta['seekable'] === FALSE)
- {
- throw new Kohana_Exception('Resource must be a file handle');
- }
- // Handle file streams passed in as resources
- $file = $filename;
- $size = fstat($file)['size'];
- }
- 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', [
- ':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);
- // 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'))
- {
- $cookies = version_compare(phpversion('http'), '2.0.0', '>=') ?
- (string) new \http\Cookie($this->_cookies) :
- http_build_cookie($this->_cookies);
- $this->_header['set-cookie'] = $cookies;
- }
- else
- {
- $cookies = [];
- // 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 [$start, $end];
- }
- }
|